diff --git a/.editorconfig b/.editorconfig
index 198db6e8a1..9c12172107 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -32,10 +32,7 @@ ij_java_generate_final_parameters = true
 ij_java_method_parameters_new_line_after_left_paren = true
 ij_java_method_parameters_right_paren_on_new_line = true
 
-[test-plugin/**/*.java]
-ij_java_use_fq_class_names = false
-
-[Paper-Server/src/main/resources/data/**/*.json]
+[paper-server/src/minecraft/resources/data/**/*.json]
 indent_size = 2
 
 [paper-api-generator/generated/**/*.java]
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 994ade949c..24cccf5f90 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -102,7 +102,7 @@ jobs:
         uses: actions/upload-artifact@v4
         with:
           name: paper-${{ fromJSON(steps.determine.outputs.result).pr }}
-          path: build/libs/paper-paperclip-*-mojmap.jar
+          path: paper-server/build/libs/paper-paperclip-*-mojmap.jar
   event_file:
     name: "Event File"
     # Only run on PRs if the source branch is on someone else's repo
diff --git a/.github/workflows/close_invalid_prs.yml b/.github/workflows/close_invalid_prs.yml
index ef572c25c8..52ac0c1769 100644
--- a/.github/workflows/close_invalid_prs.yml
+++ b/.github/workflows/close_invalid_prs.yml
@@ -9,19 +9,19 @@ jobs:
     if: |
       github.repository != github.event.pull_request.head.repo.full_name &&
       (
-        github.head_ref == 'master' ||
+        github.head_ref == 'main' ||
         github.event.pull_request.head.repo.owner.type != 'User'
       )
     runs-on: ubuntu-latest
     steps:
       - uses: superbrothers/close-pull-request@v3
-        id: "master_branch"
-        if: github.head_ref == 'master'
+        id: "main_branch"
+        if: github.head_ref == 'main'
         with:
-          comment: "Please do not open pull requests from the `master` branch, create a new branch instead."
+          comment: "Please do not open pull requests from the `main` branch, create a new branch instead."
 
       - uses: superbrothers/close-pull-request@v3
         id: "org_account"
-        if: github.event.pull_request.head.repo.owner.type != 'User' && steps.master_branch.outcome == 'skipped'
+        if: github.event.pull_request.head.repo.owner.type != 'User' && steps.main_branch.outcome == 'skipped'
         with:
           comment: "Please do not open pull requests from non-user accounts like organizations. Create a fork on a user account instead."
diff --git a/.gitignore b/.gitignore
index e08033d541..53a798d16e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,4 +48,4 @@ test-plugin.settings.gradle.kts
 paper-api-generator.settings.gradle.kts
 
 # Don't track patched vanilla submodules
-paper-server/src/vanilla/
+paper-server/src/minecraft/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9e48879b0e..d793b54e6a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -66,16 +66,16 @@ Assuming you have already forked the repository:
 2. Type `./gradlew applyPatches` in a terminal to apply the changes from upstream.
 On Windows, replace the `./` with `.\` at the beginning for all `gradlew` commands;
 3. cd into `paper-server` for server changes, and `paper-api` for API changes.
-**Only changes made in `paper-server/src/vanilla` have to deal with the patch system.**
+**Only changes made in `paper-server/src/minecraft` have to deal with the patch system.**
 
-`paper-server/src/vanilla` is not a git repositories in the traditional sense. Its
-initial commits are the decompiled and deobfuscated Vanilla source files. The per-file
+`paper-server/src/minecraft` is not a git repositories in the traditional sense. Its
+initial commits are the decompiled and deobfuscated Minecraft source files. The per-file
 patches are applied on top of these files as a single, large commit, which is then followed
 by the individual feature-patch commits.
 
-### Modifying (per-file) Vanilla patches
+### Modifying (per-file) Minecraft patches
 
-This is generally what you need to do when editing Vanilla files. Updating our
+This is generally what you need to do when editing Minecraft files. Updating our
 per-file patches is as easy as making your changes and then running
 # TODO
 in the root directory. If nothing went wrong, you can rebuild patches with
@@ -169,13 +169,14 @@ move it under the line of the patch you wish to modify;
 
 1. Make your change while at HEAD;
 1. Make a fixup commit. `git commit -a --fixup <hashOfPatchToFix>`;
+   - If you want to modify a per-file patch, use `git commit -a --fixup file`
    - You can also use `--squash` instead of `--fixup` if you want the commit
    message to also be changed.
    - You can get the hash by looking at `git log` or `git blame`; your IDE can
   assist you too.
    - Alternatively, if you only know the name of the patch, you can do
   `git commit -a --fixup "Subject of Patch name"`.
-1. Rebase with autosquash: `git rebase -i --autosquash base`.
+1. Rebase with autosquash: `git rebase -i --autosquash mache/main`.
 This will automatically move your fixup commit to the right place, and you just
 need to "save" the changes.
 1. Type `./gradlew rebuildPatches` in the root directory;
@@ -184,11 +185,11 @@ need to "save" the changes.
 
 ## Rebasing PRs
 
-Steps to rebase a PR to include the latest changes from `master`.  
+Steps to rebase a PR to include the latest changes from `main`.  
 These steps assume the `origin` remote is your fork of this repository and `upstream` is the official PaperMC repository.
 
-1. Pull the latest changes from upstreams master: `git switch main && git pull upstream main`.
-1. Checkout feature/fix branch and rebase on master: `git checkout patch-branch && git rebase main`.
+1. Pull the latest changes from upstreams main: `git switch main && git pull upstream main`.
+1. Checkout feature/fix branch and rebase on main: `git checkout patch-branch && git rebase main`.
 1. Apply updated patches: `./gradlew applyPatches`.
 1. If there are conflicts, fix them.
 1. If your PR creates new feature patches instead of modifying existing ones, ensure your newly-created patch is the last commit by either:
diff --git a/README.md b/README.md
index 567114a550..3223166baa 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-Paper [![Paper Build Status](https://img.shields.io/github/actions/workflow/status/PaperMC/Paper/build.yml?branch=master)](https://github.com/PaperMC/Paper/actions)
+Paper [![Paper Build Status](https://img.shields.io/github/actions/workflow/status/PaperMC/Paper/build.yml?branch=main)](https://github.com/PaperMC/Paper/actions)
 [![Discord](https://img.shields.io/discord/289587909051416579.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/papermc)
 [![GitHub Sponsors](https://img.shields.io/github/sponsors/papermc?label=GitHub%20Sponsors)](https://github.com/sponsors/PaperMC)
 [![Open Collective](https://img.shields.io/opencollective/all/papermc?label=OpenCollective%20Sponsors)](https://opencollective.com/papermc)
@@ -65,7 +65,7 @@ How To (Compiling Jar From Source)
 ------
 To compile Paper, you need JDK 21 and an internet connection.
 
-Clone this repo, run `./gradlew applyPatches`, then `./gradlew createMojmapBundlerJar` from your terminal. You can find the compiled jar in the project root's `build/libs` directory.
+Clone this repo, run `./gradlew applyPatches`, then `./gradlew createMojmapBundlerJar` from your terminal. You can find the compiled jar in the `paper-server/build/libs` directory.
 
 To get a full list of tasks, run `./gradlew tasks`.
 
@@ -73,6 +73,10 @@ How To (Pull Request)
 ------
 See [Contributing](CONTRIBUTING.md)
 
+Old Versions (1.21.3 and below)
+------
+For branches of versions 1.8-1.21.3, please see our [archive repository](https://github.com/PaperMC/Paper-archive).
+
 Support Us
 ------
 First of all, thank you for considering helping out, we really appreciate that!
diff --git a/build-data/dev-imports.txt b/build-data/dev-imports.txt
index 302359e8a3..1d9862a950 100644
--- a/build-data/dev-imports.txt
+++ b/build-data/dev-imports.txt
@@ -12,4 +12,3 @@
 #     mc_data chat_type/chat.json
 #     mc_data dimension_type/overworld.json
 #
-
diff --git a/build-data/paper.at b/build-data/paper.at
index 48a44de360..8e1b527ed3 100644
--- a/build-data/paper.at
+++ b/build-data/paper.at
@@ -1,18 +1,735 @@
-# You can use this file to change the access modifiers on a member
-# This line would make the field rollAmount public in Bee
-#public net.minecraft.world.entity.animal.Bee rollAmount
-# This line would make the field public and remove the final modifier
-#public-f net.minecraft.network.protocol.game.ClientboundChatPacket sender
-# Leave out the member and it will apply to the class itself
-# More info, see here https://mcforge.readthedocs.io/en/latest/advanced/accesstransformers/#access-modifiers
-
-# Remap/Decompile fix (unclear why this is happening)
-public net.minecraft.server.MinecraftServer doRunTask(Lnet/minecraft/server/TickTask;)V
-
-# AT remap issue? todo 1.18
-public net.minecraft.world.level.dimension.end.EndDragonFight findExitPortal()Lnet/minecraft/world/level/block/state/pattern/BlockPattern$BlockPatternMatch;
+# This file is auto generated, any changes may be overridden!
+# See CONTRIBUTING.md on how to add access transformers.
+private-f net.minecraft.network.CompressionDecoder inflater
+private-f net.minecraft.server.MinecraftServer connection
+private-f net.minecraft.server.MinecraftServer levels
+private-f net.minecraft.world.item.ItemStack components
+private-f net.minecraft.world.item.ItemStack item
+private-f net.minecraft.world.level.block.entity.BeehiveBlockEntity stored
+protected net.minecraft.world.entity.LivingEntity skipDropExperience
+protected-f net.minecraft.server.MinecraftServer worldData
+public net.minecraft.ChatFormatting code
+public net.minecraft.Util onThreadException(Ljava/lang/Thread;Ljava/lang/Throwable;)V
+public net.minecraft.advancements.Advancement decorateName(Lnet/minecraft/advancements/DisplayInfo;)Lnet/minecraft/network/chat/Component;
+public net.minecraft.commands.CommandSourceStack source
+public net.minecraft.commands.arguments.DimensionArgument ERROR_INVALID_VALUE
+public net.minecraft.commands.arguments.blocks.BlockInput tag
+public net.minecraft.core.MappedRegistry validateWrite(Lnet/minecraft/resources/ResourceKey;)V
+public net.minecraft.nbt.ListTag <init>(Ljava/util/List;B)V
 public net.minecraft.nbt.TagParser readArrayTag()Lnet/minecraft/nbt/Tag;
-
-# TODO 1.20 remapSpigotAt.at doesn't remap the return type for this method for some reason
-public net/minecraft/world/entity/Display$TextDisplay getText()Lnet/minecraft/network/chat/Component;
-public net/minecraft/world/entity/Display$BlockDisplay getBlockState()Lnet/minecraft/world/level/block/state/BlockState;
+public net.minecraft.nbt.TagParser type(Ljava/lang/String;)Lnet/minecraft/nbt/Tag;
+public net.minecraft.network.Connection address
+public net.minecraft.network.Connection channel
+public net.minecraft.network.chat.HoverEvent$ItemStackInfo components
+public net.minecraft.network.chat.HoverEvent$ItemStackInfo count
+public net.minecraft.network.chat.HoverEvent$ItemStackInfo item
+public net.minecraft.network.chat.TextColor name
+public net.minecraft.network.chat.contents.TranslatableContents filterAllowedArguments(Ljava/lang/Object;)Lcom/mojang/serialization/DataResult;
+public net.minecraft.network.chat.numbers.FixedFormat value
+public net.minecraft.network.chat.numbers.StyledFormat style
+public net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket <init>(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/entity/BlockEntityType;Lnet/minecraft/nbt/CompoundTag;)V
+public net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket blockState
+public net.minecraft.network.protocol.game.ClientboundSetDefaultSpawnPositionPacket pos
+public net.minecraft.network.protocol.game.ServerboundMovePlayerPacket hasPos
+public net.minecraft.network.protocol.game.ServerboundMovePlayerPacket hasRot
+public net.minecraft.network.protocol.game.ServerboundMovePlayerPacket x
+public net.minecraft.network.protocol.game.ServerboundMovePlayerPacket xRot
+public net.minecraft.network.protocol.game.ServerboundMovePlayerPacket y
+public net.minecraft.network.protocol.game.ServerboundMovePlayerPacket yRot
+public net.minecraft.network.protocol.game.ServerboundMovePlayerPacket z
+public net.minecraft.resources.RegistryOps lookupProvider
+public net.minecraft.resources.RegistryOps$HolderLookupAdapter
+public net.minecraft.server.Main forceUpgrade(Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;Lcom/mojang/datafixers/DataFixer;ZLjava/util/function/BooleanSupplier;Lnet/minecraft/core/RegistryAccess;Z)V
+public net.minecraft.server.MinecraftServer LOGGER
+public net.minecraft.server.MinecraftServer doRunTask(Lnet/minecraft/server/TickTask;)V
+public net.minecraft.server.MinecraftServer executor
+public net.minecraft.server.MinecraftServer fixerUpper
+public net.minecraft.server.MinecraftServer playerDataStorage
+public net.minecraft.server.MinecraftServer prepareLevels(Lnet/minecraft/server/level/progress/ChunkProgressListener;)V
+public net.minecraft.server.MinecraftServer progressListenerFactory
+public net.minecraft.server.MinecraftServer resources
+public net.minecraft.server.MinecraftServer serverThread
+public net.minecraft.server.MinecraftServer$ReloadableResources
+public net.minecraft.server.RegistryLayer STATIC_ACCESS
+public net.minecraft.server.ReloadableServerResources
+public net.minecraft.server.ServerAdvancementManager advancements
+public net.minecraft.server.dedicated.DedicatedServerProperties$WorldDimensionData
+public net.minecraft.server.dedicated.Settings getStringRaw(Ljava/lang/String;)Ljava/lang/String;
+public net.minecraft.server.dedicated.Settings properties
+public net.minecraft.server.level.ChunkHolder oldTicketLevel
+public net.minecraft.server.level.ChunkHolder playerProvider
+public net.minecraft.server.level.ChunkLevel ENTITY_TICKING_LEVEL
+public net.minecraft.server.level.ChunkMap addEntity(Lnet/minecraft/world/entity/Entity;)V
+public net.minecraft.server.level.ChunkMap anyPlayerCloseEnoughForSpawning(Lnet/minecraft/world/level/ChunkPos;)Z
+public net.minecraft.server.level.ChunkMap distanceManager
+public net.minecraft.server.level.ChunkMap entityMap
+public net.minecraft.server.level.ChunkMap getVisibleChunkIfPresent(J)Lnet/minecraft/server/level/ChunkHolder;
+public net.minecraft.server.level.ChunkMap level
+public net.minecraft.server.level.ChunkMap progressListener
+public net.minecraft.server.level.ChunkMap save(Lnet/minecraft/world/level/chunk/ChunkAccess;)Z
+public net.minecraft.server.level.ChunkMap serverViewDistance
+public net.minecraft.server.level.ChunkMap setServerViewDistance(I)V
+public net.minecraft.server.level.ChunkMap toDrop
+public net.minecraft.server.level.ChunkMap updatingChunkMap
+public net.minecraft.server.level.ChunkMap visibleChunkMap
+public net.minecraft.server.level.ChunkMap$TrackedEntity
+public net.minecraft.server.level.ChunkMap$TrackedEntity seenBy
+public net.minecraft.server.level.ChunkMap$TrackedEntity serverEntity
+public net.minecraft.server.level.DistanceManager simulationDistance
+public net.minecraft.server.level.DistanceManager tickets
+public net.minecraft.server.level.ServerBossEvent broadcast(Ljava/util/function/Function;)V
+public net.minecraft.server.level.ServerBossEvent visible
+public net.minecraft.server.level.ServerChunkCache mainThread
+public net.minecraft.server.level.ServerChunkCache mainThreadProcessor
+public net.minecraft.server.level.ServerChunkCache spawnEnemies
+public net.minecraft.server.level.ServerChunkCache spawnFriendlies
+public net.minecraft.server.level.ServerChunkCache$MainThreadExecutor
+public net.minecraft.server.level.ServerLevel chunkSource
+public net.minecraft.server.level.ServerLevel entityManager
+public net.minecraft.server.level.ServerLevel findLightningRod(Lnet/minecraft/core/BlockPos;)Ljava/util/Optional;
+public net.minecraft.server.level.ServerLevel getEntities()Lnet/minecraft/world/level/entity/LevelEntityGetter;
+public net.minecraft.server.level.ServerLevel serverLevelData
+public net.minecraft.server.level.ServerPlayer completeUsingItem()V
+public net.minecraft.server.level.ServerPlayer containerSynchronizer
+public net.minecraft.server.level.ServerPlayer findRespawnAndUseSpawnBlock(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/core/BlockPos;FZZ)Ljava/util/Optional;
+public net.minecraft.server.level.ServerPlayer initMenu(Lnet/minecraft/world/inventory/AbstractContainerMenu;)V
+public net.minecraft.server.level.ServerPlayer isChangingDimension
+public net.minecraft.server.level.ServerPlayer language
+public net.minecraft.server.level.ServerPlayer lastSentExp
+public net.minecraft.server.level.ServerPlayer nextContainerCounter()V
+public net.minecraft.server.level.ServerPlayer particleStatus
+public net.minecraft.server.level.ServerPlayer seenCredits
+public net.minecraft.server.level.ServerPlayer triggerDimensionChangeTriggers(Lnet/minecraft/server/level/ServerLevel;)V
+public net.minecraft.server.level.ServerPlayer wardenSpawnTracker
+public net.minecraft.server.level.ServerPlayer$RespawnPosAngle
+public net.minecraft.server.level.ServerPlayerGameMode level
+public net.minecraft.server.level.Ticket key
+public net.minecraft.server.network.ServerGamePacketListenerImpl isChatMessageIllegal(Ljava/lang/String;)Z
+public net.minecraft.server.network.ServerLoginPacketListenerImpl authenticatedProfile
+public net.minecraft.server.network.ServerLoginPacketListenerImpl connection
+public net.minecraft.server.network.ServerLoginPacketListenerImpl state
+public net.minecraft.server.network.ServerLoginPacketListenerImpl$State
+public net.minecraft.server.packs.VanillaPackResourcesBuilder safeGetPath(Ljava/net/URI;)Ljava/nio/file/Path;
+public net.minecraft.server.packs.repository.Pack resources
+public net.minecraft.server.players.PlayerList playerIo
+public net.minecraft.server.players.PlayerList players
+public net.minecraft.server.players.PlayerList updateEntireScoreboard(Lnet/minecraft/server/ServerScoreboard;Lnet/minecraft/server/level/ServerPlayer;)V
+public net.minecraft.server.players.StoredUserEntry getUser()Ljava/lang/Object;
+public net.minecraft.stats.ServerRecipeBook known
+public net.minecraft.tags.TagEntry id
+public net.minecraft.tags.TagEntry required
+public net.minecraft.tags.TagEntry tag
+public net.minecraft.util.datafix.fixes.BlockStateData register(ILjava/lang/String;[Ljava/lang/String;)V
+public net.minecraft.util.datafix.fixes.ItemIdFix ITEM_NAMES
+public net.minecraft.util.datafix.fixes.ItemSpawnEggFix ID_TO_ENTITY
+public net.minecraft.world.BossEvent color
+public net.minecraft.world.BossEvent name
+public net.minecraft.world.BossEvent overlay
+public net.minecraft.world.CompoundContainer container1
+public net.minecraft.world.CompoundContainer container2
+public net.minecraft.world.SimpleContainer items
+public net.minecraft.world.damagesource.DamageSource <init>(Lnet/minecraft/core/Holder;Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/phys/Vec3;)V
+public net.minecraft.world.effect.MobEffect attributeModifiers
+public net.minecraft.world.effect.MobEffect$AttributeTemplate
+public net.minecraft.world.effect.MobEffectInstance hiddenEffect
+public net.minecraft.world.entity.AreaEffectCloud durationOnUse
+public net.minecraft.world.entity.AreaEffectCloud ownerUUID
+public net.minecraft.world.entity.AreaEffectCloud potionContents
+public net.minecraft.world.entity.AreaEffectCloud radiusOnUse
+public net.minecraft.world.entity.AreaEffectCloud radiusPerTick
+public net.minecraft.world.entity.AreaEffectCloud reapplicationDelay
+public net.minecraft.world.entity.AreaEffectCloud updateColor()V
+public net.minecraft.world.entity.AreaEffectCloud waitTime
+public net.minecraft.world.entity.Display DATA_POS_ROT_INTERPOLATION_DURATION_ID
+public net.minecraft.world.entity.Display createTransformation(Lnet/minecraft/network/syncher/SynchedEntityData;)Lcom/mojang/math/Transformation;
+public net.minecraft.world.entity.Display getBillboardConstraints()Lnet/minecraft/world/entity/Display$BillboardConstraints;
+public net.minecraft.world.entity.Display getBrightnessOverride()Lnet/minecraft/util/Brightness;
+public net.minecraft.world.entity.Display getGlowColorOverride()I
+public net.minecraft.world.entity.Display getHeight()F
+public net.minecraft.world.entity.Display getShadowRadius()F
+public net.minecraft.world.entity.Display getShadowStrength()F
+public net.minecraft.world.entity.Display getTransformationInterpolationDelay()I
+public net.minecraft.world.entity.Display getTransformationInterpolationDuration()I
+public net.minecraft.world.entity.Display getViewRange()F
+public net.minecraft.world.entity.Display getWidth()F
+public net.minecraft.world.entity.Display setBillboardConstraints(Lnet/minecraft/world/entity/Display$BillboardConstraints;)V
+public net.minecraft.world.entity.Display setBrightnessOverride(Lnet/minecraft/util/Brightness;)V
+public net.minecraft.world.entity.Display setGlowColorOverride(I)V
+public net.minecraft.world.entity.Display setHeight(F)V
+public net.minecraft.world.entity.Display setShadowRadius(F)V
+public net.minecraft.world.entity.Display setShadowStrength(F)V
+public net.minecraft.world.entity.Display setTransformation(Lcom/mojang/math/Transformation;)V
+public net.minecraft.world.entity.Display setTransformationInterpolationDelay(I)V
+public net.minecraft.world.entity.Display setTransformationInterpolationDuration(I)V
+public net.minecraft.world.entity.Display setViewRange(F)V
+public net.minecraft.world.entity.Display setWidth(F)V
+public net.minecraft.world.entity.Display$BlockDisplay getBlockState()Lnet/minecraft/world/level/block/state/BlockState;
+public net.minecraft.world.entity.Display$BlockDisplay setBlockState(Lnet/minecraft/world/level/block/state/BlockState;)V
+public net.minecraft.world.entity.Display$ItemDisplay getItemStack()Lnet/minecraft/world/item/ItemStack;
+public net.minecraft.world.entity.Display$ItemDisplay getItemTransform()Lnet/minecraft/world/item/ItemDisplayContext;
+public net.minecraft.world.entity.Display$ItemDisplay setItemStack(Lnet/minecraft/world/item/ItemStack;)V
+public net.minecraft.world.entity.Display$ItemDisplay setItemTransform(Lnet/minecraft/world/item/ItemDisplayContext;)V
+public net.minecraft.world.entity.Display$TextDisplay DATA_BACKGROUND_COLOR_ID
+public net.minecraft.world.entity.Display$TextDisplay DATA_LINE_WIDTH_ID
+public net.minecraft.world.entity.Display$TextDisplay getBackgroundColor()I
+public net.minecraft.world.entity.Display$TextDisplay getFlags()B
+public net.minecraft.world.entity.Display$TextDisplay getLineWidth()I
+public net.minecraft.world.entity.Display$TextDisplay getText()Lnet/minecraft/network/chat/Component;
+public net.minecraft.world.entity.Display$TextDisplay getTextOpacity()B
+public net.minecraft.world.entity.Display$TextDisplay setFlags(B)V
+public net.minecraft.world.entity.Display$TextDisplay setText(Lnet/minecraft/network/chat/Component;)V
+public net.minecraft.world.entity.Display$TextDisplay setTextOpacity(B)V
+public net.minecraft.world.entity.Entity FLAG_INVISIBLE
+public net.minecraft.world.entity.Entity getEncodeId()Ljava/lang/String;
+public net.minecraft.world.entity.Entity getFireImmuneTicks()I
+public net.minecraft.world.entity.Entity getSharedFlag(I)Z
+public net.minecraft.world.entity.Entity hasVisualFire
+public net.minecraft.world.entity.Entity isInBubbleColumn()Z
+public net.minecraft.world.entity.Entity isInRain()Z
+public net.minecraft.world.entity.Entity isInvulnerableToBase(Lnet/minecraft/world/damagesource/DamageSource;)Z
+public net.minecraft.world.entity.Entity onGround
+public net.minecraft.world.entity.Entity passengers
+public net.minecraft.world.entity.Entity portalCooldown
+public net.minecraft.world.entity.Entity random
+public net.minecraft.world.entity.Entity setLevel(Lnet/minecraft/world/level/Level;)V
+public net.minecraft.world.entity.Entity setRot(FF)V
+public net.minecraft.world.entity.Entity setSharedFlag(IZ)V
+public net.minecraft.world.entity.Entity unsetRemoved()V
+public net.minecraft.world.entity.Entity wasTouchingWater
+public net.minecraft.world.entity.ExperienceOrb count
+public net.minecraft.world.entity.ExperienceOrb value
+public net.minecraft.world.entity.GlowSquid setDarkTicks(I)V
+public net.minecraft.world.entity.Interaction attack
+public net.minecraft.world.entity.Interaction getHeight()F
+public net.minecraft.world.entity.Interaction getResponse()Z
+public net.minecraft.world.entity.Interaction getWidth()F
+public net.minecraft.world.entity.Interaction interaction
+public net.minecraft.world.entity.Interaction setHeight(F)V
+public net.minecraft.world.entity.Interaction setResponse(Z)V
+public net.minecraft.world.entity.Interaction setWidth(F)V
+public net.minecraft.world.entity.Interaction$PlayerAction
+public net.minecraft.world.entity.ItemBasedSteering boostTime
+public net.minecraft.world.entity.ItemBasedSteering boostTimeTotal()I
+public net.minecraft.world.entity.ItemBasedSteering boosting
+public net.minecraft.world.entity.LightningBolt flashes
+public net.minecraft.world.entity.LightningBolt life
+public net.minecraft.world.entity.LightningBolt visualOnly
+public net.minecraft.world.entity.LivingEntity DATA_ARROW_COUNT_ID
+public net.minecraft.world.entity.LivingEntity DATA_HEALTH_ID
+public net.minecraft.world.entity.LivingEntity LIVING_ENTITY_FLAG_SPIN_ATTACK
+public net.minecraft.world.entity.LivingEntity activeEffects
+public net.minecraft.world.entity.LivingEntity completeUsingItem()V
+public net.minecraft.world.entity.LivingEntity detectEquipmentUpdates()V
+public net.minecraft.world.entity.LivingEntity effectsDirty
+public net.minecraft.world.entity.LivingEntity entityEventForEquipmentBreak(Lnet/minecraft/world/entity/EquipmentSlot;)B
+public net.minecraft.world.entity.LivingEntity getDeathSound()Lnet/minecraft/sounds/SoundEvent;
+public net.minecraft.world.entity.LivingEntity getSoundVolume()F
+public net.minecraft.world.entity.LivingEntity jumping
+public net.minecraft.world.entity.LivingEntity lastHurt
+public net.minecraft.world.entity.LivingEntity lastHurtByMob
+public net.minecraft.world.entity.LivingEntity lastHurtByMobTimestamp
+public net.minecraft.world.entity.LivingEntity lastHurtByPlayer
+public net.minecraft.world.entity.LivingEntity lastHurtByPlayerTime
+public net.minecraft.world.entity.LivingEntity setLivingEntityFlag(IZ)V
+public net.minecraft.world.entity.LivingEntity useItemRemaining
+public net.minecraft.world.entity.Mob armorDropChances
+public net.minecraft.world.entity.Mob getEquipmentDropChance(Lnet/minecraft/world/entity/EquipmentSlot;)F
+public net.minecraft.world.entity.Mob handDropChances
+public net.minecraft.world.entity.Mob isSunBurnTick()Z
+public net.minecraft.world.entity.Mob lootTable
+public net.minecraft.world.entity.Mob lootTableSeed
+public net.minecraft.world.entity.OminousItemSpawner setItem(Lnet/minecraft/world/item/ItemStack;)V
+public net.minecraft.world.entity.OminousItemSpawner spawnItemAfterTicks
+public net.minecraft.world.entity.ai.attributes.AttributeSupplier getAttributeInstance(Lnet/minecraft/core/Holder;)Lnet/minecraft/world/entity/ai/attributes/AttributeInstance;
+public net.minecraft.world.entity.ai.control.MoveControl$Operation
+public net.minecraft.world.entity.ai.gossip.GossipContainer gossips
+public net.minecraft.world.entity.ai.gossip.GossipContainer$EntityGossips
+public net.minecraft.world.entity.ai.gossip.GossipContainer$EntityGossips <init>()V
+public net.minecraft.world.entity.ai.navigation.PathNavigation pathFinder
+public net.minecraft.world.entity.ambient.Bat targetPosition
+public net.minecraft.world.entity.animal.AbstractSchoolingFish leader
+public net.minecraft.world.entity.animal.AbstractSchoolingFish schoolSize
+public net.minecraft.world.entity.animal.Animal inLove
+public net.minecraft.world.entity.animal.Animal loveCause
+public net.minecraft.world.entity.animal.Bee hivePos
+public net.minecraft.world.entity.animal.Bee isRolling()Z
+public net.minecraft.world.entity.animal.Bee numCropsGrownSincePollination
+public net.minecraft.world.entity.animal.Bee setHasNectar(Z)V
+public net.minecraft.world.entity.animal.Bee setHasStung(Z)V
+public net.minecraft.world.entity.animal.Bee setRolling(Z)V
+public net.minecraft.world.entity.animal.Bee stayOutOfHiveCountdown
+public net.minecraft.world.entity.animal.Bee ticksWithoutNectarSinceExitingHive
+public net.minecraft.world.entity.animal.Cat isRelaxStateOne()Z
+public net.minecraft.world.entity.animal.Cat setCollarColor(Lnet/minecraft/world/item/DyeColor;)V
+public net.minecraft.world.entity.animal.Cat setRelaxStateOne(Z)V
+public net.minecraft.world.entity.animal.Fox DATA_TRUSTED_ID_0
+public net.minecraft.world.entity.animal.Fox DATA_TRUSTED_ID_1
+public net.minecraft.world.entity.animal.Fox isDefending()Z
+public net.minecraft.world.entity.animal.Fox setDefending(Z)V
+public net.minecraft.world.entity.animal.Fox setFaceplanted(Z)V
+public net.minecraft.world.entity.animal.Fox setSleeping(Z)V
+public net.minecraft.world.entity.animal.MushroomCow stewEffects
+public net.minecraft.world.entity.animal.Ocelot isTrusting()Z
+public net.minecraft.world.entity.animal.Ocelot setTrusting(Z)V
+public net.minecraft.world.entity.animal.Panda getEatCounter()I
+public net.minecraft.world.entity.animal.Panda setEatCounter(I)V
+public net.minecraft.world.entity.animal.Pig steering
+public net.minecraft.world.entity.animal.Rabbit moreCarrotTicks
+public net.minecraft.world.entity.animal.Rabbit registerGoals()V
+public net.minecraft.world.entity.animal.TropicalFish getPackedVariant()I
+public net.minecraft.world.entity.animal.TropicalFish setPackedVariant(I)V
+public net.minecraft.world.entity.animal.Turtle getHomePos()Lnet/minecraft/core/BlockPos;
+public net.minecraft.world.entity.animal.Turtle isGoingHome()Z
+public net.minecraft.world.entity.animal.Turtle isTravelling()Z
+public net.minecraft.world.entity.animal.Turtle setGoingHome(Z)V
+public net.minecraft.world.entity.animal.Turtle setHasEgg(Z)V
+public net.minecraft.world.entity.animal.Turtle setTravelling(Z)V
+public net.minecraft.world.entity.animal.Wolf isWet
+public net.minecraft.world.entity.animal.Wolf setCollarColor(Lnet/minecraft/world/item/DyeColor;)V
+public net.minecraft.world.entity.animal.allay.Allay canDuplicate()Z
+public net.minecraft.world.entity.animal.allay.Allay duplicateAllay()V
+public net.minecraft.world.entity.animal.allay.Allay duplicationCooldown
+public net.minecraft.world.entity.animal.allay.Allay jukeboxPos
+public net.minecraft.world.entity.animal.allay.Allay resetDuplicationCooldown()V
+public net.minecraft.world.entity.animal.frog.Tadpole age
+public net.minecraft.world.entity.animal.goat.Goat DATA_HAS_LEFT_HORN
+public net.minecraft.world.entity.animal.goat.Goat DATA_HAS_RIGHT_HORN
+public net.minecraft.world.entity.animal.horse.AbstractHorse createInventory()V
+public net.minecraft.world.entity.animal.horse.AbstractHorse inventory
+public net.minecraft.world.entity.animal.horse.Horse setVariantAndMarkings(Lnet/minecraft/world/entity/animal/horse/Variant;Lnet/minecraft/world/entity/animal/horse/Markings;)V
+public net.minecraft.world.entity.animal.horse.SkeletonHorse trapTime
+public net.minecraft.world.entity.animal.sniffer.Sniffer calculateDigPosition()Ljava/util/Optional;
+public net.minecraft.world.entity.animal.sniffer.Sniffer canDig()Z
+public net.minecraft.world.entity.animal.sniffer.Sniffer getExploredPositions()Ljava/util/stream/Stream;
+public net.minecraft.world.entity.animal.sniffer.Sniffer getState()Lnet/minecraft/world/entity/animal/sniffer/Sniffer$State;
+public net.minecraft.world.entity.animal.sniffer.Sniffer storeExploredPosition(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/entity/animal/sniffer/Sniffer;
+public net.minecraft.world.entity.boss.enderdragon.EnderDragon subEntities
+public net.minecraft.world.entity.boss.wither.WitherBoss bossEvent
+public net.minecraft.world.entity.decoration.ArmorStand bodyPose
+public net.minecraft.world.entity.decoration.ArmorStand disabledSlots
+public net.minecraft.world.entity.decoration.ArmorStand headPose
+public net.minecraft.world.entity.decoration.ArmorStand isDisabled(Lnet/minecraft/world/entity/EquipmentSlot;)Z
+public net.minecraft.world.entity.decoration.ArmorStand leftArmPose
+public net.minecraft.world.entity.decoration.ArmorStand leftLegPose
+public net.minecraft.world.entity.decoration.ArmorStand rightArmPose
+public net.minecraft.world.entity.decoration.ArmorStand rightLegPose
+public net.minecraft.world.entity.decoration.ArmorStand setMarker(Z)V
+public net.minecraft.world.entity.decoration.ArmorStand setSmall(Z)V
+public net.minecraft.world.entity.decoration.HangingEntity setDirection(Lnet/minecraft/core/Direction;)V
+public net.minecraft.world.entity.decoration.ItemFrame DATA_ITEM
+public net.minecraft.world.entity.decoration.ItemFrame DATA_ROTATION
+public net.minecraft.world.entity.decoration.ItemFrame dropChance
+public net.minecraft.world.entity.decoration.ItemFrame fixed
+public net.minecraft.world.entity.decoration.ItemFrame setDirection(Lnet/minecraft/core/Direction;)V
+public net.minecraft.world.entity.item.FallingBlockEntity <init>(Lnet/minecraft/world/level/Level;DDDLnet/minecraft/world/level/block/state/BlockState;)V
+public net.minecraft.world.entity.item.FallingBlockEntity blockState
+public net.minecraft.world.entity.item.FallingBlockEntity cancelDrop
+public net.minecraft.world.entity.item.FallingBlockEntity fallDamageMax
+public net.minecraft.world.entity.item.FallingBlockEntity fallDamagePerDistance
+public net.minecraft.world.entity.item.FallingBlockEntity hurtEntities
+public net.minecraft.world.entity.item.ItemEntity age
+public net.minecraft.world.entity.item.ItemEntity health
+public net.minecraft.world.entity.item.ItemEntity pickupDelay
+public net.minecraft.world.entity.item.ItemEntity target
+public net.minecraft.world.entity.item.ItemEntity thrower
+public net.minecraft.world.entity.item.PrimedTnt explosionPower
+public net.minecraft.world.entity.item.PrimedTnt owner
+public net.minecraft.world.entity.monster.Creeper explodeCreeper()V
+public net.minecraft.world.entity.monster.Creeper explosionRadius
+public net.minecraft.world.entity.monster.Creeper maxSwell
+public net.minecraft.world.entity.monster.Creeper swell
+public net.minecraft.world.entity.monster.Drowned groundNavigation
+public net.minecraft.world.entity.monster.Drowned waterNavigation
+public net.minecraft.world.entity.monster.EnderMan teleport()Z
+public net.minecraft.world.entity.monster.EnderMan teleportTowards(Lnet/minecraft/world/entity/Entity;)Z
+public net.minecraft.world.entity.monster.Endermite life
+public net.minecraft.world.entity.monster.Evoker getWololoTarget()Lnet/minecraft/world/entity/animal/Sheep;
+public net.minecraft.world.entity.monster.Evoker setWololoTarget(Lnet/minecraft/world/entity/animal/Sheep;)V
+public net.minecraft.world.entity.monster.Guardian randomStrollGoal
+public net.minecraft.world.entity.monster.Guardian setActiveAttackTarget(I)V
+public net.minecraft.world.entity.monster.Guardian$GuardianAttackGoal
+public net.minecraft.world.entity.monster.Guardian$GuardianAttackGoal attackTime
+public net.minecraft.world.entity.monster.Phantom anchorPoint
+public net.minecraft.world.entity.monster.Pillager inventory
+public net.minecraft.world.entity.monster.Ravager attackTick
+public net.minecraft.world.entity.monster.Ravager roarTick
+public net.minecraft.world.entity.monster.Ravager stunnedTick
+public net.minecraft.world.entity.monster.Shulker DATA_COLOR_ID
+public net.minecraft.world.entity.monster.Shulker getRawPeekAmount()I
+public net.minecraft.world.entity.monster.Shulker setAttachFace(Lnet/minecraft/core/Direction;)V
+public net.minecraft.world.entity.monster.Shulker setRawPeekAmount(I)V
+public net.minecraft.world.entity.monster.Skeleton DATA_STRAY_CONVERSION_ID
+public net.minecraft.world.entity.monster.Skeleton conversionTime
+public net.minecraft.world.entity.monster.Skeleton inPowderSnowTime
+public net.minecraft.world.entity.monster.SpellcasterIllager getCurrentSpell()Lnet/minecraft/world/entity/monster/SpellcasterIllager$IllagerSpell;
+public net.minecraft.world.entity.monster.SpellcasterIllager$IllagerSpell
+public net.minecraft.world.entity.monster.Strider steering
+public net.minecraft.world.entity.monster.Vex hasLimitedLife
+public net.minecraft.world.entity.monster.Vex limitedLifeTicks
+public net.minecraft.world.entity.monster.Vindicator DOOR_BREAKING_PREDICATE
+public net.minecraft.world.entity.monster.Vindicator isJohnny
+public net.minecraft.world.entity.monster.Witch usingTime
+public net.minecraft.world.entity.monster.Zombie DATA_DROWNED_CONVERSION_ID
+public net.minecraft.world.entity.monster.Zombie DOOR_BREAKING_PREDICATE
+public net.minecraft.world.entity.monster.Zombie conversionTime
+public net.minecraft.world.entity.monster.Zombie isSunSensitive()Z
+public net.minecraft.world.entity.monster.Zombie startUnderWaterConversion(I)V
+public net.minecraft.world.entity.monster.ZombieVillager DATA_CONVERTING_ID
+public net.minecraft.world.entity.monster.ZombieVillager conversionStarter
+public net.minecraft.world.entity.monster.ZombieVillager startConverting(Ljava/util/UUID;I)V
+public net.minecraft.world.entity.monster.ZombieVillager villagerConversionTime
+public net.minecraft.world.entity.monster.hoglin.Hoglin cannotBeHunted
+public net.minecraft.world.entity.monster.hoglin.Hoglin isImmuneToZombification()Z
+public net.minecraft.world.entity.monster.hoglin.Hoglin timeInOverworld
+public net.minecraft.world.entity.monster.piglin.AbstractPiglin isImmuneToZombification()Z
+public net.minecraft.world.entity.monster.piglin.AbstractPiglin timeInOverworld
+public net.minecraft.world.entity.monster.piglin.Piglin cannotHunt
+public net.minecraft.world.entity.monster.piglin.Piglin inventory
+public net.minecraft.world.entity.monster.piglin.Piglin isChargingCrossbow()Z
+public net.minecraft.world.entity.monster.warden.WardenSpawnTracker cooldownTicks
+public net.minecraft.world.entity.monster.warden.WardenSpawnTracker increaseWarningLevel()V
+public net.minecraft.world.entity.monster.warden.WardenSpawnTracker ticksSinceLastWarning
+public net.minecraft.world.entity.npc.Villager increaseMerchantCareer()V
+public net.minecraft.world.entity.npc.Villager numberOfRestocksToday
+public net.minecraft.world.entity.npc.Villager releaseAllPois()V
+public net.minecraft.world.entity.npc.Villager setUnhappy()V
+public net.minecraft.world.entity.npc.WanderingTrader getWanderTarget()Lnet/minecraft/core/BlockPos;
+public net.minecraft.world.entity.player.Abilities flyingSpeed
+public net.minecraft.world.entity.player.Abilities walkingSpeed
+public net.minecraft.world.entity.player.Inventory compartments
+public net.minecraft.world.entity.player.Player DATA_PLAYER_MODE_CUSTOMISATION
+public net.minecraft.world.entity.player.Player closeContainer()V
+public net.minecraft.world.entity.player.Player enchantmentSeed
+public net.minecraft.world.entity.player.Player getFireImmuneTicks()I
+public net.minecraft.world.entity.player.Player removeEntitiesOnShoulder()V
+public net.minecraft.world.entity.player.Player setShoulderEntityLeft(Lnet/minecraft/nbt/CompoundTag;)V
+public net.minecraft.world.entity.player.Player setShoulderEntityRight(Lnet/minecraft/nbt/CompoundTag;)V
+public net.minecraft.world.entity.player.Player sleepCounter
+public net.minecraft.world.entity.projectile.AbstractHurtingProjectile assignDirectionalMovement(Lnet/minecraft/world/phys/Vec3;D)V
+public net.minecraft.world.entity.projectile.Arrow NO_EFFECT_COLOR
+public net.minecraft.world.entity.projectile.Arrow getPotionContents()Lnet/minecraft/world/item/alchemy/PotionContents;
+public net.minecraft.world.entity.projectile.Arrow setPotionContents(Lnet/minecraft/world/item/alchemy/PotionContents;)V
+public net.minecraft.world.entity.projectile.Arrow updateColor()V
+public net.minecraft.world.entity.projectile.EvokerFangs warmupDelayTicks
+public net.minecraft.world.entity.projectile.EyeOfEnder life
+public net.minecraft.world.entity.projectile.EyeOfEnder surviveAfterDeath
+public net.minecraft.world.entity.projectile.EyeOfEnder tx
+public net.minecraft.world.entity.projectile.EyeOfEnder ty
+public net.minecraft.world.entity.projectile.EyeOfEnder tz
+public net.minecraft.world.entity.projectile.FireworkRocketEntity DATA_ATTACHED_TO_TARGET
+public net.minecraft.world.entity.projectile.FireworkRocketEntity DATA_ID_FIREWORKS_ITEM
+public net.minecraft.world.entity.projectile.FireworkRocketEntity DATA_SHOT_AT_ANGLE
+public net.minecraft.world.entity.projectile.FireworkRocketEntity attachedToEntity
+public net.minecraft.world.entity.projectile.FireworkRocketEntity getDefaultItem()Lnet/minecraft/world/item/ItemStack;
+public net.minecraft.world.entity.projectile.FireworkRocketEntity life
+public net.minecraft.world.entity.projectile.FireworkRocketEntity lifetime
+public net.minecraft.world.entity.projectile.FishingHook DATA_HOOKED_ENTITY
+public net.minecraft.world.entity.projectile.FishingHook calculateOpenWater(Lnet/minecraft/core/BlockPos;)Z
+public net.minecraft.world.entity.projectile.FishingHook currentState
+public net.minecraft.world.entity.projectile.FishingHook fishAngle
+public net.minecraft.world.entity.projectile.FishingHook hookedIn
+public net.minecraft.world.entity.projectile.FishingHook outOfWaterTime
+public net.minecraft.world.entity.projectile.FishingHook pullEntity(Lnet/minecraft/world/entity/Entity;)V
+public net.minecraft.world.entity.projectile.FishingHook setHookedEntity(Lnet/minecraft/world/entity/Entity;)V
+public net.minecraft.world.entity.projectile.FishingHook timeUntilHooked
+public net.minecraft.world.entity.projectile.FishingHook timeUntilLured
+public net.minecraft.world.entity.projectile.FishingHook$FishHookState
+public net.minecraft.world.entity.projectile.LargeFireball explosionPower
+public net.minecraft.world.entity.projectile.Projectile cachedOwner
+public net.minecraft.world.entity.projectile.Projectile hasBeenShot
+public net.minecraft.world.entity.projectile.Projectile leftOwner
+public net.minecraft.world.entity.projectile.Projectile ownerUUID
+public net.minecraft.world.entity.projectile.ShulkerBullet currentMoveDirection
+public net.minecraft.world.entity.projectile.ShulkerBullet flightSteps
+public net.minecraft.world.entity.projectile.ShulkerBullet targetDeltaX
+public net.minecraft.world.entity.projectile.ShulkerBullet targetDeltaY
+public net.minecraft.world.entity.projectile.ShulkerBullet targetDeltaZ
+public net.minecraft.world.entity.projectile.SpectralArrow duration
+public net.minecraft.world.entity.projectile.ThrownPotion isLingering()Z
+public net.minecraft.world.entity.projectile.ThrownTrident dealtDamage
+public net.minecraft.world.entity.projectile.windcharge.AbstractWindCharge explode(Lnet/minecraft/world/phys/Vec3;)V
+public net.minecraft.world.entity.projectile.windcharge.BreezeWindCharge explode(Lnet/minecraft/world/phys/Vec3;)V
+public net.minecraft.world.entity.projectile.windcharge.WindCharge explode(Lnet/minecraft/world/phys/Vec3;)V
+public net.minecraft.world.entity.raid.Raid heroesOfTheVillage
+public net.minecraft.world.entity.raid.Raid numGroups
+public net.minecraft.world.entity.raid.Raid raidEvent
+public net.minecraft.world.entity.raid.Raid raidOmenLevel
+public net.minecraft.world.entity.raid.Raid ticksActive
+public net.minecraft.world.entity.raid.Raid totalHealth
+public net.minecraft.world.entity.raid.Raider$HoldGroundAttackGoal
+public net.minecraft.world.entity.raid.Raids raidMap
+public net.minecraft.world.entity.vehicle.AbstractBoat getDropItem()Lnet/minecraft/world/item/Item;
+public net.minecraft.world.entity.vehicle.AbstractBoat getStatus()Lnet/minecraft/world/entity/vehicle/AbstractBoat$Status;
+public net.minecraft.world.entity.vehicle.AbstractBoat status
+public net.minecraft.world.entity.vehicle.AbstractMinecartContainer lootTable
+public net.minecraft.world.entity.vehicle.AbstractMinecartContainer lootTableSeed
+public net.minecraft.world.entity.vehicle.MinecartCommandBlock DATA_ID_COMMAND_NAME
+public net.minecraft.world.entity.vehicle.MinecartFurnace fuel
+public net.minecraft.world.entity.vehicle.MinecartTNT explode(D)V
+public net.minecraft.world.entity.vehicle.MinecartTNT explosionPowerBase
+public net.minecraft.world.entity.vehicle.MinecartTNT explosionSpeedFactor
+public net.minecraft.world.entity.vehicle.MinecartTNT fuse
+public net.minecraft.world.flag.FeatureFlag mask
+public net.minecraft.world.flag.FeatureFlag universe
+public net.minecraft.world.flag.FeatureFlagRegistry names
+public net.minecraft.world.food.FoodData exhaustionLevel
+public net.minecraft.world.food.FoodData foodLevel
+public net.minecraft.world.food.FoodData saturationLevel
+public net.minecraft.world.inventory.AbstractContainerMenu quickcraftSlots
+public net.minecraft.world.inventory.AbstractContainerMenu quickcraftStatus
+public net.minecraft.world.inventory.AbstractContainerMenu quickcraftType
+public net.minecraft.world.inventory.AbstractContainerMenu resetQuickCraft()V
+public net.minecraft.world.inventory.AbstractCraftingMenu craftSlots
+public net.minecraft.world.inventory.AbstractCraftingMenu resultSlots
+public net.minecraft.world.inventory.AnvilMenu cost
+public net.minecraft.world.inventory.AnvilMenu itemName
+public net.minecraft.world.inventory.AnvilMenu repairItemCountCost
+public net.minecraft.world.inventory.BrewingStandMenu brewingStandData
+public net.minecraft.world.inventory.CraftingMenu access
+public net.minecraft.world.inventory.DispenserMenu dispenser
+public net.minecraft.world.inventory.HorseInventoryMenu SLOT_BODY_ARMOR
+public net.minecraft.world.inventory.MerchantContainer selectionHint
+public net.minecraft.world.inventory.Slot slot
+public net.minecraft.world.item.AdventureModePredicate predicates
+public net.minecraft.world.item.BucketItem content
+public net.minecraft.world.item.CrossbowItem FIREWORK_POWER
+public net.minecraft.world.item.DebugStickItem handleInteraction(Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/LevelAccessor;Lnet/minecraft/core/BlockPos;ZLnet/minecraft/world/item/ItemStack;)Z
+public net.minecraft.world.item.ItemCooldowns cooldowns
+public net.minecraft.world.item.ItemCooldowns tickCount
+public net.minecraft.world.item.ItemCooldowns$CooldownInstance
+public net.minecraft.world.item.ItemStackLinkedSet TYPE_AND_TAG
+public net.minecraft.world.item.JukeboxSongPlayer song
+public net.minecraft.world.item.MapItem createNewSavedData(Lnet/minecraft/world/level/Level;IIIZZLnet/minecraft/resources/ResourceKey;)Lnet/minecraft/world/level/saveddata/maps/MapId;
+public net.minecraft.world.item.StandingAndWallBlockItem wallBlock
+public net.minecraft.world.item.component.ItemContainerContents MAX_SIZE
+public net.minecraft.world.item.component.ItemContainerContents items
+public net.minecraft.world.item.context.UseOnContext <init>(Lnet/minecraft/world/level/Level;Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/InteractionHand;Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/phys/BlockHitResult;)V
+public net.minecraft.world.item.crafting.RecipeManager recipes
+public net.minecraft.world.item.crafting.RecipeMap byKey
+public net.minecraft.world.item.crafting.RecipeMap byType
+public net.minecraft.world.item.enchantment.ItemEnchantments showInTooltip
+public net.minecraft.world.item.trading.MerchantOffer demand
+public net.minecraft.world.item.trading.MerchantOffer result
+public net.minecraft.world.item.trading.MerchantOffer specialPriceDiff
+public net.minecraft.world.item.trading.MerchantOffer uses
+public net.minecraft.world.level.BaseSpawner delay(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;)V
+public net.minecraft.world.level.BaseSpawner isNearPlayer(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;)Z
+public net.minecraft.world.level.BaseSpawner maxNearbyEntities
+public net.minecraft.world.level.BaseSpawner maxSpawnDelay
+public net.minecraft.world.level.BaseSpawner minSpawnDelay
+public net.minecraft.world.level.BaseSpawner nextSpawnData
+public net.minecraft.world.level.BaseSpawner requiredPlayerRange
+public net.minecraft.world.level.BaseSpawner setNextSpawnData(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/SpawnData;)V
+public net.minecraft.world.level.BaseSpawner spawnCount
+public net.minecraft.world.level.BaseSpawner spawnDelay
+public net.minecraft.world.level.BaseSpawner spawnPotentials
+public net.minecraft.world.level.BaseSpawner spawnRange
+public net.minecraft.world.level.GameRules$Value onChanged(Lnet/minecraft/server/MinecraftServer;)V
+public net.minecraft.world.level.Level getEntities()Lnet/minecraft/world/level/entity/LevelEntityGetter;
+public net.minecraft.world.level.Level levelData
+public net.minecraft.world.level.Level rainLevel
+public net.minecraft.world.level.Level thread
+public net.minecraft.world.level.Level thunderLevel
+public net.minecraft.world.level.NaturalSpawner SPAWNING_CATEGORIES
+public net.minecraft.world.level.StructureManager level
+public net.minecraft.world.level.biome.Biome climateSettings
+public net.minecraft.world.level.biome.Biome getTemperature(Lnet/minecraft/core/BlockPos;I)F
+public net.minecraft.world.level.biome.Biome$ClimateSettings
+public net.minecraft.world.level.block.Block popExperience(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/core/BlockPos;I)V
+public net.minecraft.world.level.block.ChestBlock isBlockedChestByBlock(Lnet/minecraft/world/level/BlockGetter;Lnet/minecraft/core/BlockPos;)Z
+public net.minecraft.world.level.block.ChiseledBookShelfBlock getHitSlot(Lnet/minecraft/world/phys/BlockHitResult;Lnet/minecraft/world/level/block/state/BlockState;)Ljava/util/OptionalInt;
+public net.minecraft.world.level.block.ChiseledBookShelfBlock getSection(F)I
+public net.minecraft.world.level.block.ComposterBlock$EmptyContainer
+public net.minecraft.world.level.block.ComposterBlock$InputContainer
+public net.minecraft.world.level.block.ComposterBlock$OutputContainer
+public net.minecraft.world.level.block.DispenserBlock dispenseFrom(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/core/BlockPos;)V
+public net.minecraft.world.level.block.DropperBlock dispenseFrom(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/core/BlockPos;)V
+public net.minecraft.world.level.block.FireBlock igniteOdds
+public net.minecraft.world.level.block.RedStoneWireBlock canSurvive(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/LevelReader;Lnet/minecraft/core/BlockPos;)Z
+public net.minecraft.world.level.block.RedStoneWireBlock shouldSignal
+public net.minecraft.world.level.block.ShulkerBoxBlock color
+public net.minecraft.world.level.block.SoundType breakSound
+public net.minecraft.world.level.block.SoundType hitSound
+public net.minecraft.world.level.block.TurtleEggBlock decreaseEggs(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;)V
+public net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity cookingTimer
+public net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity cookingTotalTime
+public net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity getTotalCookTime(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity;)I
+public net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity litTimeRemaining
+public net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity recipesUsed
+public net.minecraft.world.level.block.entity.BarrelBlockEntity openersCounter
+public net.minecraft.world.level.block.entity.BarrelBlockEntity playSound(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/sounds/SoundEvent;)V
+public net.minecraft.world.level.block.entity.BarrelBlockEntity updateBlockState(Lnet/minecraft/world/level/block/state/BlockState;Z)V
+public net.minecraft.world.level.block.entity.BaseContainerBlockEntity lockKey
+public net.minecraft.world.level.block.entity.BaseContainerBlockEntity name
+public net.minecraft.world.level.block.entity.BeaconBlockEntity levels
+public net.minecraft.world.level.block.entity.BeaconBlockEntity lockKey
+public net.minecraft.world.level.block.entity.BeaconBlockEntity name
+public net.minecraft.world.level.block.entity.BeaconBlockEntity primaryPower
+public net.minecraft.world.level.block.entity.BeaconBlockEntity secondaryPower
+public net.minecraft.world.level.block.entity.BedBlockEntity color
+public net.minecraft.world.level.block.entity.BeehiveBlockEntity savedFlowerPos
+public net.minecraft.world.level.block.entity.BellBlockEntity resonating
+public net.minecraft.world.level.block.entity.BellBlockEntity resonationTicks
+public net.minecraft.world.level.block.entity.BlockEntity saveId(Lnet/minecraft/nbt/CompoundTag;)V
+public net.minecraft.world.level.block.entity.BlockEntityType validBlocks
+public net.minecraft.world.level.block.entity.BrewingStandBlockEntity brewTime
+public net.minecraft.world.level.block.entity.BrewingStandBlockEntity fuel
+public net.minecraft.world.level.block.entity.BrushableBlockEntity item
+public net.minecraft.world.level.block.entity.BrushableBlockEntity lootTable
+public net.minecraft.world.level.block.entity.BrushableBlockEntity lootTableSeed
+public net.minecraft.world.level.block.entity.CampfireBlockEntity cookingProgress
+public net.minecraft.world.level.block.entity.CampfireBlockEntity cookingTime
+public net.minecraft.world.level.block.entity.ChestBlockEntity openersCounter
+public net.minecraft.world.level.block.entity.ChestBlockEntity playSound(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/sounds/SoundEvent;)V
+public net.minecraft.world.level.block.entity.ChiseledBookShelfBlockEntity lastInteractedSlot
+public net.minecraft.world.level.block.entity.ConduitBlockEntity destroyTarget
+public net.minecraft.world.level.block.entity.ConduitBlockEntity destroyTargetUUID
+public net.minecraft.world.level.block.entity.ConduitBlockEntity effectBlocks
+public net.minecraft.world.level.block.entity.ConduitBlockEntity getDestroyRangeAABB(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/phys/AABB;
+public net.minecraft.world.level.block.entity.CrafterBlockEntity craftingTicksRemaining
+public net.minecraft.world.level.block.entity.DecoratedPotBlockEntity decorations
+public net.minecraft.world.level.block.entity.EnderChestBlockEntity openersCounter
+public net.minecraft.world.level.block.entity.HopperBlockEntity cooldownTime
+public net.minecraft.world.level.block.entity.HopperBlockEntity setCooldown(I)V
+public net.minecraft.world.level.block.entity.LecternBlockEntity bookAccess
+public net.minecraft.world.level.block.entity.LecternBlockEntity setPage(I)V
+public net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity lootTable
+public net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity lootTableSeed
+public net.minecraft.world.level.block.entity.SculkCatalystBlockEntity$CatalystListener bloom(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/util/RandomSource;)V
+public net.minecraft.world.level.block.entity.SculkSensorBlockEntity lastVibrationFrequency
+public net.minecraft.world.level.block.entity.SculkShriekerBlockEntity warningLevel
+public net.minecraft.world.level.block.entity.ShulkerBoxBlockEntity openCount
+public net.minecraft.world.level.block.entity.SignBlockEntity playerWhoMayEdit
+public net.minecraft.world.level.block.entity.SkullBlockEntity noteBlockSound
+public net.minecraft.world.level.block.entity.SkullBlockEntity owner
+public net.minecraft.world.level.block.entity.StructureBlockEntity author
+public net.minecraft.world.level.block.entity.StructureBlockEntity ignoreEntities
+public net.minecraft.world.level.block.entity.StructureBlockEntity integrity
+public net.minecraft.world.level.block.entity.StructureBlockEntity metaData
+public net.minecraft.world.level.block.entity.StructureBlockEntity mirror
+public net.minecraft.world.level.block.entity.StructureBlockEntity mode
+public net.minecraft.world.level.block.entity.StructureBlockEntity rotation
+public net.minecraft.world.level.block.entity.StructureBlockEntity seed
+public net.minecraft.world.level.block.entity.StructureBlockEntity showAir
+public net.minecraft.world.level.block.entity.StructureBlockEntity showBoundingBox
+public net.minecraft.world.level.block.entity.StructureBlockEntity structurePos
+public net.minecraft.world.level.block.entity.StructureBlockEntity structureSize
+public net.minecraft.world.level.block.entity.TheEndGatewayBlockEntity age
+public net.minecraft.world.level.block.entity.TheEndGatewayBlockEntity exactTeleport
+public net.minecraft.world.level.block.entity.TheEndGatewayBlockEntity exitPortal
+public net.minecraft.world.level.block.entity.TrialSpawnerBlockEntity trialSpawner
+public net.minecraft.world.level.block.entity.trialspawner.TrialSpawner isOminous
+public net.minecraft.world.level.block.entity.trialspawner.TrialSpawner stateAccessor
+public net.minecraft.world.level.block.entity.trialspawner.TrialSpawnerData currentMobs
+public net.minecraft.world.level.block.entity.trialspawner.TrialSpawnerData detectedPlayers
+public net.minecraft.world.level.block.entity.trialspawner.TrialSpawnerData nextSpawnData
+public net.minecraft.world.level.block.state.BlockBehaviour getMenuProvider(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/MenuProvider;
+public net.minecraft.world.level.block.state.BlockBehaviour hasCollision
+public net.minecraft.world.level.block.state.BlockBehaviour$BlockStateBase destroySpeed
+public net.minecraft.world.level.block.state.StateHolder PROPERTY_ENTRY_TO_STRING_FUNCTION
+public net.minecraft.world.level.block.state.properties.IntegerProperty max
+public net.minecraft.world.level.block.state.properties.IntegerProperty min
+public net.minecraft.world.level.chunk.ChunkAccess blockEntities
+public net.minecraft.world.level.chunk.ChunkAccess heightmaps
+public net.minecraft.world.level.chunk.ChunkGenerator generationSettingsGetter
+public net.minecraft.world.level.chunk.LevelChunk level
+public net.minecraft.world.level.chunk.LevelChunk loaded
+public net.minecraft.world.level.chunk.LevelChunkSection states
+public net.minecraft.world.level.chunk.PalettedContainer registry
+public net.minecraft.world.level.chunk.storage.EntityStorage entityDeserializerQueue
+public net.minecraft.world.level.chunk.storage.EntityStorage level
+public net.minecraft.world.level.chunk.storage.RegionFileStorage regionCache
+public net.minecraft.world.level.chunk.storage.SerializableChunkData BLOCK_STATE_CODEC
+public net.minecraft.world.level.dimension.end.EndDragonFight GATEWAY_COUNT
+public net.minecraft.world.level.dimension.end.EndDragonFight dragonEvent
+public net.minecraft.world.level.dimension.end.EndDragonFight dragonUUID
+public net.minecraft.world.level.dimension.end.EndDragonFight findExitPortal()Lnet/minecraft/world/level/block/state/pattern/BlockPattern$BlockPatternMatch;
+public net.minecraft.world.level.dimension.end.EndDragonFight gateways
+public net.minecraft.world.level.dimension.end.EndDragonFight level
+public net.minecraft.world.level.dimension.end.EndDragonFight portalLocation
+public net.minecraft.world.level.dimension.end.EndDragonFight previouslyKilled
+public net.minecraft.world.level.dimension.end.EndDragonFight respawnCrystals
+public net.minecraft.world.level.dimension.end.EndDragonFight respawnDragon(Ljava/util/List;)V
+public net.minecraft.world.level.dimension.end.EndDragonFight respawnStage
+public net.minecraft.world.level.dimension.end.EndDragonFight setRespawnStage(Lnet/minecraft/world/level/dimension/end/DragonRespawnAnimation;)V
+public net.minecraft.world.level.dimension.end.EndDragonFight spawnExitPortal(Z)V
+public net.minecraft.world.level.dimension.end.EndDragonFight spawnNewGateway(Lnet/minecraft/core/BlockPos;)V
+public net.minecraft.world.level.entity.PersistentEntitySectionManager ensureChunkQueuedForLoad(J)V
+public net.minecraft.world.level.entity.PersistentEntitySectionManager permanentStorage
+public net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator settings
+public net.minecraft.world.level.levelgen.SurfaceRules$Condition
+public net.minecraft.world.level.levelgen.SurfaceRules$Context
+public net.minecraft.world.level.levelgen.SurfaceRules$Context blockX
+public net.minecraft.world.level.levelgen.SurfaceRules$Context blockY
+public net.minecraft.world.level.levelgen.SurfaceRules$Context blockZ
+public net.minecraft.world.level.levelgen.SurfaceRules$Context context
+public net.minecraft.world.level.levelgen.SurfaceRules$Context randomState
+public net.minecraft.world.level.levelgen.SurfaceRules$LazyCondition
+public net.minecraft.world.level.levelgen.SurfaceRules$LazyYCondition
+public net.minecraft.world.level.levelgen.SurfaceRules$SurfaceRule
+public net.minecraft.world.level.levelgen.SurfaceRules$VerticalGradientConditionSource
+public net.minecraft.world.level.levelgen.structure.placement.StructurePlacement exclusionZone
+public net.minecraft.world.level.levelgen.structure.placement.StructurePlacement frequency
+public net.minecraft.world.level.levelgen.structure.placement.StructurePlacement frequencyReductionMethod
+public net.minecraft.world.level.levelgen.structure.placement.StructurePlacement locateOffset
+public net.minecraft.world.level.levelgen.structure.placement.StructurePlacement salt
+public net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate entityInfoList
+public net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate palettes
+public net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager loadFromGenerated(Lnet/minecraft/resources/ResourceLocation;)Ljava/util/Optional;
+public net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager loadFromResource(Lnet/minecraft/resources/ResourceLocation;)Ljava/util/Optional;
+public net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager readStructure(Ljava/io/InputStream;)Lnet/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate;
+public net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager structureRepository
+public net.minecraft.world.level.material.MapColor MATERIAL_COLORS
+public net.minecraft.world.level.pathfinder.Path nodes
+public net.minecraft.world.level.pathfinder.PathFinder nodeEvaluator
+public net.minecraft.world.level.saveddata.maps.MapItemSavedData carriedBy
+public net.minecraft.world.level.saveddata.maps.MapItemSavedData carriedByPlayers
+public net.minecraft.world.level.saveddata.maps.MapItemSavedData decorations
+public net.minecraft.world.level.saveddata.maps.MapItemSavedData setColorsDirty(II)V
+public net.minecraft.world.level.saveddata.maps.MapItemSavedData setDecorationsDirty()V
+public net.minecraft.world.level.storage.DimensionDataStorage cache
+public net.minecraft.world.level.storage.LevelStorageSource baseDir
+public net.minecraft.world.level.storage.LevelStorageSource$LevelStorageAccess levelDirectory
+public net.minecraft.world.level.storage.PrimaryLevelData settings
+public net.minecraft.world.scores.Objective displayName
+public net.minecraft.world.scores.criteria.ObjectiveCriteria CRITERIA_CACHE
+public-f net.minecraft.server.MinecraftServer potionBrewing
+public-f net.minecraft.server.MinecraftServer storageSource
+public-f net.minecraft.server.ReloadableServerResources commands
+public-f net.minecraft.server.dedicated.DedicatedServer serverLinks
+public-f net.minecraft.server.dedicated.DedicatedServer settings
+public-f net.minecraft.server.dedicated.DedicatedServerProperties pauseWhenEmptySeconds
+public-f net.minecraft.server.level.TicketType timeout
+public-f net.minecraft.server.players.PlayerList maxPlayers
+public-f net.minecraft.world.entity.LivingEntity combatTracker
+public-f net.minecraft.world.entity.LivingEntity invulnerableDuration
+public-f net.minecraft.world.entity.Mob goalSelector
+public-f net.minecraft.world.entity.Mob targetSelector
+public-f net.minecraft.world.entity.ai.attributes.RangedAttribute maxValue
+public-f net.minecraft.world.entity.player.Player gameProfile
+public-f net.minecraft.world.inventory.AbstractContainerMenu dataSlots
+public-f net.minecraft.world.inventory.AbstractContainerMenu lastSlots
+public-f net.minecraft.world.inventory.AbstractContainerMenu remoteDataSlots
+public-f net.minecraft.world.inventory.AbstractContainerMenu remoteSlots
+public-f net.minecraft.world.inventory.AbstractContainerMenu slots
+public-f net.minecraft.world.item.enchantment.ItemEnchantments$Mutable showInTooltip
+public-f net.minecraft.world.item.trading.MerchantOffer baseCostA
+public-f net.minecraft.world.item.trading.MerchantOffer costB
+public-f net.minecraft.world.item.trading.MerchantOffer maxUses
+public-f net.minecraft.world.item.trading.MerchantOffer priceMultiplier
+public-f net.minecraft.world.item.trading.MerchantOffer rewardExp
+public-f net.minecraft.world.item.trading.MerchantOffer xp
+public-f net.minecraft.world.level.LevelSettings hardcore
+public-f net.minecraft.world.level.LevelSettings levelName
+public-f net.minecraft.world.level.block.entity.BannerBlockEntity baseColor
+public-f net.minecraft.world.level.block.entity.trialspawner.TrialSpawner normalConfig
+public-f net.minecraft.world.level.block.entity.trialspawner.TrialSpawner ominousConfig
+public-f net.minecraft.world.level.block.entity.trialspawner.TrialSpawner requiredPlayerRange
+public-f net.minecraft.world.level.block.entity.trialspawner.TrialSpawner targetCooldownLength
+public-f net.minecraft.world.level.saveddata.maps.MapItemSavedData centerX
+public-f net.minecraft.world.level.saveddata.maps.MapItemSavedData centerZ
+public-f net.minecraft.world.level.saveddata.maps.MapItemSavedData dimension
+public-f net.minecraft.world.level.saveddata.maps.MapItemSavedData locked
+public-f net.minecraft.world.level.saveddata.maps.MapItemSavedData scale
+public-f net.minecraft.world.level.saveddata.maps.MapItemSavedData trackingPosition
+public-f net.minecraft.world.level.saveddata.maps.MapItemSavedData unlimitedTracking
diff --git a/build.gradle.kts b/build.gradle.kts
index 5f01c51835..03fd12ce48 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,4 +1,5 @@
 import io.papermc.paperweight.util.*
+import io.papermc.paperweight.util.constants.*
 import org.gradle.api.tasks.testing.logging.TestExceptionFormat
 import org.gradle.api.tasks.testing.logging.TestLogEvent
 import java.io.IOException
@@ -8,9 +9,10 @@ import java.nio.file.Files
 import java.nio.file.SimpleFileVisitor
 import kotlin.io.path.*
 import java.nio.file.Path
+import kotlin.random.Random
 
 plugins {
-    id("io.papermc.paperweight.core") version "2.0.0-SNAPSHOT" apply false
+    id("io.papermc.paperweight.core") version "2.0.0-beta.8" apply false
 }
 
 subprojects {
@@ -23,10 +25,6 @@ subprojects {
         }
     }
 
-    dependencies {
-        "testRuntimeOnly"("org.junit.platform:junit-platform-launcher")
-    }
-
     tasks.withType<AbstractArchiveTask>().configureEach {
         isPreserveFileTimestamps = false
         isReproducibleFileOrder = true
@@ -89,7 +87,6 @@ tasks.register("gibWork") {
     val patchesFolder = layout.projectDirectory.dir("paper-server/patches/").convertToPath()
     val storage = layout.cache.resolve("last-updating-folder").also { it.parent.createDirectories() }
 
-    @OptIn(ExperimentalPathApi::class)
     doLast {
         val html = URI(issue).toURL().readText()
 
@@ -98,18 +95,26 @@ tasks.register("gibWork") {
         val end = html.indexOf("```", start + beginMarker.length)
         val taskList = html.substring(start + beginMarker.length, end)
 
-        val next = taskList.split("\\n").first { it.startsWith("- [ ]") }.replace("- [ ] ", "")
+        // Extract all incomplete tasks and select a random one
+        val incompleteTasks = taskList.split("\\n").filter { it.startsWith("- [ ]") }.map { it.replace("- [ ] ", "") }
+        if (incompleteTasks.isEmpty()) {
+            error("No incomplete tasks found in the task list.")
+        }
+
+        val next = incompleteTasks[Random.nextInt(incompleteTasks.size)]
 
         println("checking out $next...")
         val dir = patchesFolder.resolve("unapplied").resolve(next)
         if (!dir.exists()) {
             error("Unapplied patch folder $next does not exist, did someone else already check it out and forgot to mark it?")
         }
-        dir.copyToRecursively(
-            patchesFolder.resolve("sources").resolve(next)
-                .also { it.createDirectories() }, overwrite = true, followLinks = false
-        )
-        dir.deleteRecursively()
+        dir.listDirectoryEntries("*.patch").forEach { patch ->
+            patch.copyTo(patchesFolder.resolve("sources").resolve(next).resolve(patch.fileName).also { it.createDirectories() }, overwrite = true)
+            patch.deleteIfExists()
+        }
+        if (dir.listDirectoryEntries().isEmpty()) {
+            dir.deleteIfExists()
+        }
 
         storage.writeText(next)
         println("please tick the box in the issue: $issue")
@@ -133,23 +138,24 @@ tasks.register("showWork") {
 }
 
 tasks.register("checkWork") {
+    notCompatibleWithConfigurationCache("This task is interactive")
     fun expandUserHome(path: String): Path {
         return Path.of(path.replaceFirst("^~".toRegex(), System.getProperty("user.home")))
     }
 
-    val input = layout.cache.resolve("last-updating-folder").readText()
-    val patchFolder = layout.projectDirectory.file("paper-server/patches/sources").convertToPath().resolve(input)
-    val sourceFolder = layout.projectDirectory.file("paper-server/src/vanilla/java/").convertToPath().resolve(input)
-    val targetFolder = expandUserHome(
-        providers.gradleProperty("cleanPaperRepo").orNull
-            ?: error("cleanPaperRepo is required, define it in gradle.properties")
-    ).resolve(input)
+    val input = providers.fileContents(layout.projectDirectory.file("$CACHE_PATH/last-updating-folder")).asText.map { it.trim() }
+    val patchFolder = layout.projectDirectory.dir("paper-server/patches/sources").dir(input)
+    val sourceFolder = layout.projectDirectory.dir("paper-server/src/minecraft/java").dir(input)
+    val targetFolder = providers.gradleProperty("cleanPaperRepo").map {
+        expandUserHome(it).resolve(input.get())
+    }
 
     fun copy(back: Boolean = false) {
-        patchFolder.listDirectoryEntries().forEach {
-            val relative = patchFolder.relativize(it).toString().replace(".patch", "")
-            val source = sourceFolder.resolve(relative)
-            val target = targetFolder.resolve(relative)
+        patchFolder.path.listDirectoryEntries().forEach {
+            val relative = patchFolder.path.relativize(it).toString().replace(".patch", "")
+            val source = sourceFolder.path.resolve(relative)
+            val target = targetFolder.get().resolve(relative)
+            if (target.isDirectory()) { return@forEach }
             if (back) {
                 target.copyTo(source, overwrite = true)
             } else {
@@ -159,8 +165,11 @@ tasks.register("checkWork") {
     }
 
     doLast {
+        if (!targetFolder.isPresent) {
+            error("cleanPaperRepo is required, define it in gradle.properties")
+        }
         copy()
-        val files = patchFolder.listDirectoryEntries().map { it.fileName.toString().replace(".patch", "") }
+        val files = patchFolder.path.listDirectoryEntries().map { it.fileName.toString().replace(".patch", "") }
         println("Copied $files from $sourceFolder to $targetFolder")
         println("Make the files compile, then press enter to copy them back!")
         System.`in`.read()
diff --git a/feature-patches/1038-Optimize-isInWorldBounds-and-getBlockState-for-inlin.patch b/feature-patches/1038-Optimize-isInWorldBounds-and-getBlockState-for-inlin.patch
deleted file mode 100644
index bfa353b5dd..0000000000
--- a/feature-patches/1038-Optimize-isInWorldBounds-and-getBlockState-for-inlin.patch
+++ /dev/null
@@ -1,99 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Aikar <aikar@aikar.co>
-Date: Thu, 3 Mar 2016 02:07:55 -0600
-Subject: [PATCH] Optimize isInWorldBounds and getBlockState for inlining
-
-Hot methods, so reduce # of instructions for the method.
-
-Move is valid location test to the BlockPosition class so that it can access local variables.
-
-Replace all calls to the new place to the unnecessary forward.
-
-Optimize getType and getBlockData to manually inline and optimize the calls
-
-diff --git a/src/main/java/net/minecraft/core/Vec3i.java b/src/main/java/net/minecraft/core/Vec3i.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/core/Vec3i.java
-+++ b/src/main/java/net/minecraft/core/Vec3i.java
-@@ -0,0 +0,0 @@ public class Vec3i implements Comparable<Vec3i> {
-         );
-     }
- 
-+    // Paper start
-+    public final boolean isInsideBuildHeightAndWorldBoundsHorizontal(net.minecraft.world.level.LevelHeightAccessor levelHeightAccessor) {
-+        return getX() >= -30000000 && getZ() >= -30000000 && getX() < 30000000 && getZ() < 30000000 && !levelHeightAccessor.isOutsideBuildHeight(getY());
-+    }
-+    // Paper end
-+
-     public Vec3i(int x, int y, int z) {
-         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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/Level.java
-+++ b/src/main/java/net/minecraft/world/level/Level.java
-@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
-     // Paper end
- 
-     public boolean isInWorldBounds(BlockPos pos) {
--        return !this.isOutsideBuildHeight(pos) && Level.isInWorldBoundsHorizontal(pos);
-+        return pos.isInsideBuildHeightAndWorldBoundsHorizontal(this); // Paper - use better/optimized check
-     }
- 
-     public static boolean isInSpawnableBounds(BlockPos pos) {
-diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
-@@ -0,0 +0,0 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh
-         return GameEventListenerRegistry.NOOP;
-     }
- 
-+    public abstract BlockState getBlockState(final int x, final int y, final int z); // Paper
-     @Nullable
-     public abstract BlockState setBlockState(BlockPos pos, BlockState state, boolean moved);
- 
-diff --git a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
-@@ -0,0 +0,0 @@ public class ImposterProtoChunk extends ProtoChunk {
-     public BlockState getBlockState(BlockPos pos) {
-         return this.wrapped.getBlockState(pos);
-     }
-+    // Paper start
-+    @Override
-+    public final BlockState getBlockState(final int x, final int y, final int z) {
-+        return this.wrapped.getBlockStateFinal(x, y, z);
-+    }
-+    // Paper end
- 
-     @Override
-     public FluidState getFluidState(BlockPos pos) {
-diff --git a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
-@@ -0,0 +0,0 @@ public class ProtoChunk extends ChunkAccess {
- 
-     @Override
-     public BlockState getBlockState(BlockPos pos) {
--        int i = pos.getY();
--        if (this.isOutsideBuildHeight(i)) {
-+        // Paper start
-+        return getBlockState(pos.getX(), pos.getY(), pos.getZ());
-+    }
-+    public BlockState getBlockState(final int x, final int y, final int z) {
-+        if (this.isOutsideBuildHeight(y)) {
-             return Blocks.VOID_AIR.defaultBlockState();
-         } else {
--            LevelChunkSection levelChunkSection = this.getSection(this.getSectionIndex(i));
--            return levelChunkSection.hasOnlyAir() ? Blocks.AIR.defaultBlockState() : levelChunkSection.getBlockState(pos.getX() & 15, i & 15, pos.getZ() & 15);
-+            LevelChunkSection levelChunkSection = this.getSections()[this.getSectionIndex(y)];
-+            return levelChunkSection.hasOnlyAir() ? Blocks.AIR.defaultBlockState() : levelChunkSection.getBlockState(x & 15, y & 15, z & 15);
-         }
-     }
-+    // Paper end
- 
-     @Override
-     public FluidState getFluidState(BlockPos pos) {
diff --git a/feature-patches/1039-Improve-Maps-in-item-frames-performance-and-bug-fixe.patch b/feature-patches/1039-Improve-Maps-in-item-frames-performance-and-bug-fixe.patch
deleted file mode 100644
index afafe8972f..0000000000
--- a/feature-patches/1039-Improve-Maps-in-item-frames-performance-and-bug-fixe.patch
+++ /dev/null
@@ -1,124 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Aikar <aikar@aikar.co>
-Date: Fri, 29 Apr 2016 20:02:00 -0400
-Subject: [PATCH] Improve Maps (in item frames) performance and bug fixes
-
-Maps used a modified version of rendering to support plugin controlled
-imaging on maps. The Craft Map Renderer is much slower than Vanilla,
-causing maps in item frames to cause a noticeable hit on server performance.
-
-This updates the map system to not use the Craft system if we detect that no
-custom renderers are in use, defaulting to the much simpler Vanilla system.
-
-Additionally, numerous issues to player position tracking on maps has been fixed.
-
-diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerLevel.java
-+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
-                             {
-                                 if ( iter.next().player == entity )
-                                 {
-+                                    map.decorations.remove(entity.getName().getString()); // Paper
-                                     iter.remove();
-                                 }
-                             }
-diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
-+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
-@@ -0,0 +0,0 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
-                 this.awardStat(Stats.DROP);
-             }
- 
-+            // Paper start - remove player from map on drop
-+            if (itemstack.getItem() == net.minecraft.world.item.Items.FILLED_MAP) {
-+                net.minecraft.world.level.saveddata.maps.MapItemSavedData worldmap = net.minecraft.world.item.MapItem.getSavedData(itemstack, this.level());
-+                if (worldmap != null) {
-+                    worldmap.tickCarriedBy(this, itemstack);
-+                    }
-+                }
-+            // Paper end
-             return entityitem;
-         }
-     }
-diff --git a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java
-+++ b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java
-@@ -0,0 +0,0 @@ public class MapItemSavedData extends SavedData {
-     public final Map<String, MapDecoration> decorations = Maps.newLinkedHashMap();
-     private final Map<String, MapFrame> frameMarkers = Maps.newHashMap();
-     private int trackedDecorationCount;
-+    private org.bukkit.craftbukkit.map.RenderData vanillaRender = new org.bukkit.craftbukkit.map.RenderData(); // Paper
- 
-     // CraftBukkit start
-     public final CraftMapView mapView;
-@@ -0,0 +0,0 @@ public class MapItemSavedData extends SavedData {
-         // CraftBukkit start
-         this.mapView = new CraftMapView(this);
-         this.server = (CraftServer) org.bukkit.Bukkit.getServer();
-+        this.vanillaRender.buffer = colors; // Paper
-         // CraftBukkit end
-     }
- 
-@@ -0,0 +0,0 @@ public class MapItemSavedData extends SavedData {
-         if (abyte.length == 16384) {
-             worldmap.colors = abyte;
-         }
-+        worldmap.vanillaRender.buffer = abyte; // Paper
- 
-         RegistryOps<Tag> registryops = registries.createSerializationContext(NbtOps.INSTANCE);
-         List<MapBanner> list = (List) MapBanner.LIST_CODEC.parse(registryops, nbt.get("banners")).resultOrPartial((s) -> {
-@@ -0,0 +0,0 @@ public class MapItemSavedData extends SavedData {
-             --this.trackedDecorationCount;
-         }
- 
--        this.setDecorationsDirty();
-+        if (mapicon != null) this.setDecorationsDirty(); // Paper - only mark dirty if a change occurs
-     }
- 
-     public static void addTargetDecoration(ItemStack stack, BlockPos pos, String id, Holder<MapDecorationType> decorationType) {
-@@ -0,0 +0,0 @@ public class MapItemSavedData extends SavedData {
- 
-     public class HoldingPlayer {
- 
-+        // Paper start
-+        private void addSeenPlayers(java.util.Collection<MapDecoration> icons) {
-+            org.bukkit.entity.Player player = (org.bukkit.entity.Player) this.player.getBukkitEntity();
-+            MapItemSavedData.this.decorations.forEach((name, mapIcon) -> {
-+                // If this cursor is for a player check visibility with vanish system
-+                org.bukkit.entity.Player other = org.bukkit.Bukkit.getPlayerExact(name); // Spigot
-+                if (other == null || player.canSee(other)) {
-+                    icons.add(mapIcon);
-+                }
-+            });
-+        }
-+        private boolean shouldUseVanillaMap() {
-+            return mapView.getRenderers().size() == 1 && mapView.getRenderers().get(0).getClass() == org.bukkit.craftbukkit.map.CraftMapRenderer.class;
-+        }
-+        // Paper end
-         public final Player player;
-         private boolean dirtyData = true;
-         private int minDirtyX;
-@@ -0,0 +0,0 @@ public class MapItemSavedData extends SavedData {
-         @Nullable
-         Packet<?> nextUpdatePacket(MapId mapId) {
-             MapItemSavedData.MapPatch worldmap_c;
--            org.bukkit.craftbukkit.map.RenderData render = MapItemSavedData.this.mapView.render((org.bukkit.craftbukkit.entity.CraftPlayer) this.player.getBukkitEntity()); // CraftBukkit
-+            if (!this.dirtyData && this.tick % 5 != 0) { this.tick++; return null; } // Paper - this won't end up sending, so don't render it!
-+            boolean vanillaMaps = shouldUseVanillaMap(); // Paper
-+            org.bukkit.craftbukkit.map.RenderData render = !vanillaMaps ? MapItemSavedData.this.mapView.render((org.bukkit.craftbukkit.entity.CraftPlayer) this.player.getBukkitEntity()) : MapItemSavedData.this.vanillaRender; // CraftBukkit // Paper
- 
-             if (this.dirtyData) {
-                 this.dirtyData = false;
-@@ -0,0 +0,0 @@ public class MapItemSavedData extends SavedData {
-                 // CraftBukkit start
-                 java.util.Collection<MapDecoration> icons = new java.util.ArrayList<MapDecoration>();
- 
-+                if (vanillaMaps) addSeenPlayers(icons); // Paper
-+
-                 for (org.bukkit.map.MapCursor cursor : render.cursors) {
-                     if (cursor.isVisible()) {
-                         icons.add(new MapDecoration(CraftMapCursor.CraftType.bukkitToMinecraftHolder(cursor.getType()), cursor.getX(), cursor.getY(), cursor.getDirection(), Optional.ofNullable(PaperAdventure.asVanilla(cursor.caption()))));
diff --git a/feature-patches/1042-Flat-bedrock-generator-settings.patch b/feature-patches/1042-Flat-bedrock-generator-settings.patch
deleted file mode 100644
index 4d8c54d243..0000000000
--- a/feature-patches/1042-Flat-bedrock-generator-settings.patch
+++ /dev/null
@@ -1,292 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Byteflux <byte@byteflux.net>
-Date: Wed, 2 Mar 2016 02:17:54 -0600
-Subject: [PATCH] Flat bedrock generator settings
-
-== AT ==
-public net.minecraft.world.level.levelgen.SurfaceRules$Condition
-public net.minecraft.world.level.levelgen.SurfaceRules$Context
-public net.minecraft.world.level.levelgen.SurfaceRules$Context blockX
-public net.minecraft.world.level.levelgen.SurfaceRules$Context blockY
-public net.minecraft.world.level.levelgen.SurfaceRules$Context blockZ
-public net.minecraft.world.level.levelgen.SurfaceRules$Context context
-public net.minecraft.world.level.levelgen.SurfaceRules$Context randomState
-public net.minecraft.world.level.levelgen.SurfaceRules$LazyYCondition
-public net.minecraft.world.level.levelgen.SurfaceRules$LazyCondition
-public net.minecraft.world.level.levelgen.SurfaceRules$VerticalGradientConditionSource
-public net.minecraft.world.level.levelgen.SurfaceRules$SurfaceRule
-
-Co-authored-by: Noah van der Aa <ndvdaa@gmail.com>
-
-diff --git a/src/main/java/io/papermc/paper/world/worldgen/OptionallyFlatBedrockConditionSource.java b/src/main/java/io/papermc/paper/world/worldgen/OptionallyFlatBedrockConditionSource.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/world/worldgen/OptionallyFlatBedrockConditionSource.java
-@@ -0,0 +0,0 @@
-+package io.papermc.paper.world.worldgen;
-+
-+import com.mojang.serialization.Codec;
-+import com.mojang.serialization.MapCodec;
-+import com.mojang.serialization.codecs.RecordCodecBuilder;
-+import net.minecraft.core.Registry;
-+import net.minecraft.core.registries.BuiltInRegistries;
-+import net.minecraft.core.registries.Registries;
-+import net.minecraft.resources.ResourceKey;
-+import net.minecraft.resources.ResourceLocation;
-+import net.minecraft.util.KeyDispatchDataCodec;
-+import net.minecraft.util.Mth;
-+import net.minecraft.util.RandomSource;
-+import net.minecraft.world.level.levelgen.PositionalRandomFactory;
-+import net.minecraft.world.level.levelgen.SurfaceRules;
-+import net.minecraft.world.level.levelgen.VerticalAnchor;
-+import org.checkerframework.checker.nullness.qual.NonNull;
-+import org.checkerframework.framework.qual.DefaultQualifier;
-+
-+// Modelled off of SurfaceRules$VerticalGradientConditionSource
-+@DefaultQualifier(NonNull.class)
-+public record OptionallyFlatBedrockConditionSource(ResourceLocation randomName, VerticalAnchor trueAtAndBelow, VerticalAnchor falseAtAndAbove, boolean isRoof) implements SurfaceRules.ConditionSource {
-+
-+    private static final ResourceKey<MapCodec<? extends SurfaceRules.ConditionSource>> CODEC_RESOURCE_KEY = ResourceKey.create(
-+        Registries.MATERIAL_CONDITION,
-+        ResourceLocation.fromNamespaceAndPath(ResourceLocation.PAPER_NAMESPACE, "optionally_flat_bedrock_condition_source")
-+    );
-+    private static final KeyDispatchDataCodec<OptionallyFlatBedrockConditionSource> CODEC = KeyDispatchDataCodec.of(RecordCodecBuilder.mapCodec((instance) -> {
-+        return instance.group(
-+            ResourceLocation.CODEC.fieldOf("random_name").forGetter(OptionallyFlatBedrockConditionSource::randomName),
-+            VerticalAnchor.CODEC.fieldOf("true_at_and_below").forGetter(OptionallyFlatBedrockConditionSource::trueAtAndBelow),
-+            VerticalAnchor.CODEC.fieldOf("false_at_and_above").forGetter(OptionallyFlatBedrockConditionSource::falseAtAndAbove),
-+            Codec.BOOL.fieldOf("is_roof").forGetter(OptionallyFlatBedrockConditionSource::isRoof)
-+        ).apply(instance, OptionallyFlatBedrockConditionSource::new);
-+    }));
-+
-+    public static void bootstrap() {
-+        Registry.register(BuiltInRegistries.MATERIAL_CONDITION, CODEC_RESOURCE_KEY, CODEC.codec());
-+    }
-+
-+    @Override
-+    public KeyDispatchDataCodec<? extends SurfaceRules.ConditionSource> codec() {
-+        return CODEC;
-+    }
-+
-+    @Override
-+    public SurfaceRules.Condition apply(final SurfaceRules.Context context) {
-+        boolean hasFlatBedrock = context.context.getWorld().paperConfig().environment.generateFlatBedrock;
-+        int tempTrueAtAndBelowY = this.trueAtAndBelow().resolveY(context.context);
-+        int tempFalseAtAndAboveY = this.falseAtAndAbove().resolveY(context.context);
-+
-+        int flatYLevel = this.isRoof ? Math.max(tempFalseAtAndAboveY, tempTrueAtAndBelowY) - 1 : Math.min(tempFalseAtAndAboveY, tempTrueAtAndBelowY);
-+        final int trueAtAndBelowY = hasFlatBedrock ? flatYLevel : tempTrueAtAndBelowY;
-+        final int falseAtAndAboveY = hasFlatBedrock ? flatYLevel : tempFalseAtAndAboveY;
-+
-+        final PositionalRandomFactory positionalRandomFactory = context.randomState.getOrCreateRandomFactory(this.randomName());
-+
-+        class VerticalGradientCondition extends SurfaceRules.LazyYCondition {
-+            VerticalGradientCondition(SurfaceRules.Context context) {
-+                super(context);
-+            }
-+
-+            @Override
-+            protected boolean compute() {
-+                int blockY = this.context.blockY;
-+                if (blockY <= trueAtAndBelowY) {
-+                    return true;
-+                } else if (blockY >= falseAtAndAboveY) {
-+                    return false;
-+                } else {
-+                    double d = Mth.map(blockY, trueAtAndBelowY, falseAtAndAboveY, 1.0D, 0.0D);
-+                    RandomSource randomSource = positionalRandomFactory.at(this.context.blockX, blockY, this.context.blockZ);
-+                    return (double)randomSource.nextFloat() < d;
-+                }
-+            }
-+        }
-+
-+        return new VerticalGradientCondition(context);
-+    }
-+}
-diff --git a/src/main/java/net/minecraft/server/Bootstrap.java b/src/main/java/net/minecraft/server/Bootstrap.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/Bootstrap.java
-+++ b/src/main/java/net/minecraft/server/Bootstrap.java
-@@ -0,0 +0,0 @@ public class Bootstrap {
-                     CauldronInteraction.bootStrap();
-                     // Paper start
-                     BuiltInRegistries.bootStrap(() -> {
-+                        io.papermc.paper.world.worldgen.OptionallyFlatBedrockConditionSource.bootstrap(); // Paper - Flat bedrock generator settings
-                     });
-                     // Paper end
-                     CreativeModeTabs.validate();
-diff --git a/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java b/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
-+++ b/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
-@@ -0,0 +0,0 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator {
-     @Override
-     public void buildSurface(WorldGenRegion region, StructureManager structures, RandomState noiseConfig, ChunkAccess chunk) {
-         if (!SharedConstants.debugVoidTerrain(chunk.getPos())) {
--            WorldGenerationContext worldgenerationcontext = new WorldGenerationContext(this, region);
-+            WorldGenerationContext worldgenerationcontext = new WorldGenerationContext(this, region, region.getMinecraftWorld()); // Paper - Flat bedrock generator settings
- 
-             this.buildSurface(chunk, worldgenerationcontext, noiseConfig, structures, region.getBiomeManager(), region.registryAccess().lookupOrThrow(Registries.BIOME), Blender.of(region));
-         }
-@@ -0,0 +0,0 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator {
-             return this.createNoiseChunk(ichunkaccess1, structureAccessor, Blender.of(chunkRegion), noiseConfig);
-         });
-         Aquifer aquifer = noisechunk.aquifer();
--        CarvingContext carvingcontext = new CarvingContext(this, chunkRegion.registryAccess(), chunk.getHeightAccessorForGeneration(), noisechunk, noiseConfig, ((NoiseGeneratorSettings) this.settings.value()).surfaceRule());
-+        CarvingContext carvingcontext = new CarvingContext(this, chunkRegion.registryAccess(), chunk.getHeightAccessorForGeneration(), noisechunk, noiseConfig, ((NoiseGeneratorSettings) this.settings.value()).surfaceRule(), chunkRegion.getMinecraftWorld()); // Paper - Flat bedrock generator settings
-         CarvingMask carvingmask = ((ProtoChunk) chunk).getOrCreateCarvingMask();
- 
-         for (int j = -8; j <= 8; ++j) {
-diff --git a/src/main/java/net/minecraft/world/level/levelgen/WorldGenerationContext.java b/src/main/java/net/minecraft/world/level/levelgen/WorldGenerationContext.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/levelgen/WorldGenerationContext.java
-+++ b/src/main/java/net/minecraft/world/level/levelgen/WorldGenerationContext.java
-@@ -0,0 +0,0 @@ import net.minecraft.world.level.chunk.ChunkGenerator;
- public class WorldGenerationContext {
-     private final int minY;
-     private final int height;
-+    private final @javax.annotation.Nullable net.minecraft.world.level.Level level; // Paper - Flat bedrock generator settings
- 
--    public WorldGenerationContext(ChunkGenerator generator, LevelHeightAccessor world) {
-+    public WorldGenerationContext(ChunkGenerator generator, LevelHeightAccessor world) { this(generator, world, null); } // Paper - Flat bedrock generator settings
-+    public WorldGenerationContext(ChunkGenerator generator, LevelHeightAccessor world, @org.jetbrains.annotations.Nullable net.minecraft.world.level.Level level) { // Paper - Flat bedrock generator settings
-         this.minY = Math.max(world.getMinY(), generator.getMinY());
-         this.height = Math.min(world.getHeight(), generator.getGenDepth());
-+        this.level = level; // Paper - Flat bedrock generator settings
-     }
- 
-     public int getMinGenY() {
-@@ -0,0 +0,0 @@ public class WorldGenerationContext {
-     public int getGenDepth() {
-         return this.height;
-     }
-+
-+    // Paper start - Flat bedrock generator settings
-+    public net.minecraft.world.level.Level getWorld() {
-+        if (this.level == null) {
-+            throw new NullPointerException("WorldGenerationContext was initialized without a Level, but WorldGenerationContext#getWorld was called");
-+        }
-+        return this.level;
-+    }
-+    // Paper end - Flat bedrock generator settings
- }
-diff --git a/src/main/java/net/minecraft/world/level/levelgen/carver/CarvingContext.java b/src/main/java/net/minecraft/world/level/levelgen/carver/CarvingContext.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/levelgen/carver/CarvingContext.java
-+++ b/src/main/java/net/minecraft/world/level/levelgen/carver/CarvingContext.java
-@@ -0,0 +0,0 @@ public class CarvingContext extends WorldGenerationContext {
-         LevelHeightAccessor heightLimitView,
-         NoiseChunk chunkNoiseSampler,
-         RandomState noiseConfig,
--        SurfaceRules.RuleSource materialRule
-+        SurfaceRules.RuleSource materialRule, @javax.annotation.Nullable net.minecraft.world.level.Level level  // Paper - Flat bedrock generator settings
-     ) {
--        super(noiseChunkGenerator, heightLimitView);
-+        super(noiseChunkGenerator, heightLimitView, level); // Paper - Flat bedrock generator settings
-         this.registryAccess = registryManager;
-         this.noiseChunk = chunkNoiseSampler;
-         this.randomState = noiseConfig;
-diff --git a/src/main/java/net/minecraft/world/level/levelgen/placement/PlacementContext.java b/src/main/java/net/minecraft/world/level/levelgen/placement/PlacementContext.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/levelgen/placement/PlacementContext.java
-+++ b/src/main/java/net/minecraft/world/level/levelgen/placement/PlacementContext.java
-@@ -0,0 +0,0 @@ public class PlacementContext extends WorldGenerationContext {
-     private final Optional<PlacedFeature> topFeature;
- 
-     public PlacementContext(WorldGenLevel world, ChunkGenerator generator, Optional<PlacedFeature> placedFeature) {
--        super(generator, world);
-+        super(generator, world, world.getLevel()); // Paper - Flat bedrock generator settings
-         this.level = world;
-         this.generator = generator;
-         this.topFeature = placedFeature;
-diff --git a/src/main/resources/data/minecraft/worldgen/noise_settings/amplified.json b/src/main/resources/data/minecraft/worldgen/noise_settings/amplified.json
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/resources/data/minecraft/worldgen/noise_settings/amplified.json
-+++ b/src/main/resources/data/minecraft/worldgen/noise_settings/amplified.json
-@@ -0,0 +0,0 @@
-       {
-         "type": "minecraft:condition",
-         "if_true": {
--          "type": "minecraft:vertical_gradient",
-+          "type": "paper:optionally_flat_bedrock_condition_source",
-+          "is_roof": false,
-           "false_at_and_above": {
-             "above_bottom": 5
-           },
-diff --git a/src/main/resources/data/minecraft/worldgen/noise_settings/caves.json b/src/main/resources/data/minecraft/worldgen/noise_settings/caves.json
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/resources/data/minecraft/worldgen/noise_settings/caves.json
-+++ b/src/main/resources/data/minecraft/worldgen/noise_settings/caves.json
-@@ -0,0 +0,0 @@
-         "if_true": {
-           "type": "minecraft:not",
-           "invert": {
--            "type": "minecraft:vertical_gradient",
-+            "type": "paper:optionally_flat_bedrock_condition_source",
-+            "is_roof": true,
-             "false_at_and_above": {
-               "below_top": 0
-             },
-@@ -0,0 +0,0 @@
-       {
-         "type": "minecraft:condition",
-         "if_true": {
--          "type": "minecraft:vertical_gradient",
-+          "type": "paper:optionally_flat_bedrock_condition_source",
-+          "is_roof": false,
-           "false_at_and_above": {
-             "above_bottom": 5
-           },
-diff --git a/src/main/resources/data/minecraft/worldgen/noise_settings/large_biomes.json b/src/main/resources/data/minecraft/worldgen/noise_settings/large_biomes.json
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/resources/data/minecraft/worldgen/noise_settings/large_biomes.json
-+++ b/src/main/resources/data/minecraft/worldgen/noise_settings/large_biomes.json
-@@ -0,0 +0,0 @@
-       {
-         "type": "minecraft:condition",
-         "if_true": {
--          "type": "minecraft:vertical_gradient",
-+          "type": "paper:optionally_flat_bedrock_condition_source",
-+          "is_roof": false,
-           "false_at_and_above": {
-             "above_bottom": 5
-           },
-diff --git a/src/main/resources/data/minecraft/worldgen/noise_settings/nether.json b/src/main/resources/data/minecraft/worldgen/noise_settings/nether.json
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/resources/data/minecraft/worldgen/noise_settings/nether.json
-+++ b/src/main/resources/data/minecraft/worldgen/noise_settings/nether.json
-@@ -0,0 +0,0 @@
-       {
-         "type": "minecraft:condition",
-         "if_true": {
--          "type": "minecraft:vertical_gradient",
-+          "type": "paper:optionally_flat_bedrock_condition_source",
-+          "is_roof": false,
-           "false_at_and_above": {
-             "above_bottom": 5
-           },
-@@ -0,0 +0,0 @@
-         "if_true": {
-           "type": "minecraft:not",
-           "invert": {
--            "type": "minecraft:vertical_gradient",
-+            "type": "paper:optionally_flat_bedrock_condition_source",
-+            "is_roof": true,
-             "false_at_and_above": {
-               "below_top": 0
-             },
-diff --git a/src/main/resources/data/minecraft/worldgen/noise_settings/overworld.json b/src/main/resources/data/minecraft/worldgen/noise_settings/overworld.json
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/resources/data/minecraft/worldgen/noise_settings/overworld.json
-+++ b/src/main/resources/data/minecraft/worldgen/noise_settings/overworld.json
-@@ -0,0 +0,0 @@
-       {
-         "type": "minecraft:condition",
-         "if_true": {
--          "type": "minecraft:vertical_gradient",
-+          "type": "paper:optionally_flat_bedrock_condition_source",
-+          "is_roof": false,
-           "false_at_and_above": {
-             "above_bottom": 5
-           },
diff --git a/feature-patches/1043-Entity-Activation-Range-2.0.patch b/feature-patches/1043-Entity-Activation-Range-2.0.patch
deleted file mode 100644
index d9a21e0900..0000000000
--- a/feature-patches/1043-Entity-Activation-Range-2.0.patch
+++ /dev/null
@@ -1,775 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Aikar <aikar@aikar.co>
-Date: Fri, 13 May 2016 01:38:06 -0400
-Subject: [PATCH] Entity Activation Range 2.0
-
-Optimizes performance of Activation Range
-
-Adds many new configurations and a new wake up inactive system
-
-Fixes and adds new Immunities to improve gameplay behavior
-
-Adds water Mobs to activation range config and nerfs fish
-Adds flying monsters to control ghast and phantoms
-Adds villagers as separate config
-
-== AT ==
-public net.minecraft.world.entity.Entity isInsidePortal
-
-diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerLevel.java
-+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
- 
-     public void tickNonPassenger(Entity entity) {
-         // Spigot start
--        if (!org.spigotmc.ActivationRange.checkIfActive(entity)) {
-+        /*if (!org.spigotmc.ActivationRange.checkIfActive(entity)) { // Paper - comment out EAR 2
-             entity.tickCount++;
-             entity.inactiveTick();
-             return;
--        }
-+        }*/ // Paper - comment out EAR 2
-         // Spigot end
-         entity.setOldPosAndRot();
-         ProfilerFiller gameprofilerfiller = Profiler.get();
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
-             return BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString();
-         });
-         gameprofilerfiller.incrementCounter("tickNonPassenger");
-+        final boolean isActive = org.spigotmc.ActivationRange.checkIfActive(entity); // Paper - EAR 2
-+        if (isActive) { // Paper - EAR 2
-         entity.tick();
-         entity.postTick(); // CraftBukkit
-+        } else { entity.inactiveTick(); } // Paper - EAR 2
-         gameprofilerfiller.pop();
-         Iterator iterator = entity.getPassengers().iterator();
- 
-         while (iterator.hasNext()) {
-             Entity entity1 = (Entity) iterator.next();
- 
--            this.tickPassenger(entity, entity1);
-+            this.tickPassenger(entity, entity1, isActive); // Paper - EAR 2
-         }
- 
-     }
- 
--    private void tickPassenger(Entity vehicle, Entity passenger) {
-+    private void tickPassenger(Entity vehicle, Entity passenger, boolean isActive) { // Paper - EAR 2
-         if (!passenger.isRemoved() && passenger.getVehicle() == vehicle) {
-             if (passenger instanceof Player || this.entityTickList.contains(passenger)) {
-                 passenger.setOldPosAndRot();
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
-                     return BuiltInRegistries.ENTITY_TYPE.getKey(passenger.getType()).toString();
-                 });
-                 gameprofilerfiller.incrementCounter("tickPassenger");
-+                // Paper start - EAR 2
-+                if (isActive) {
-                 passenger.rideTick();
-                 passenger.postTick(); // CraftBukkit
-+                } else {
-+                passenger.setDeltaMovement(Vec3.ZERO);
-+                passenger.inactiveTick();
-+                // copied from inside of if (isPassenger()) of passengerTick, but that ifPassenger is unnecessary
-+                vehicle.positionRider(passenger);
-+                }
-+                // Paper end - EAR 2
-                 gameprofilerfiller.pop();
-                 Iterator iterator = passenger.getPassengers().iterator();
- 
-                 while (iterator.hasNext()) {
-                     Entity entity2 = (Entity) iterator.next();
- 
--                    this.tickPassenger(passenger, entity2);
-+                    this.tickPassenger(passenger, entity2, isActive); // Paper - EAR 2
-                 }
- 
-             }
-diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/entity/Entity.java
-+++ b/src/main/java/net/minecraft/world/entity/Entity.java
-@@ -0,0 +0,0 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
-     // Spigot end
-     protected int numCollisions = 0; // Paper - Cap entity collisions
-     public boolean fromNetherPortal; // Paper - Add option to nerf pigmen from nether portals
-+    public long activatedImmunityTick = Integer.MIN_VALUE; // Paper - EAR
-+    public boolean isTemporarilyActive; // Paper - EAR
-     public boolean spawnedViaMobSpawner; // Paper - Yes this name is similar to above, upstream took the better one
-     // Paper start - Entity origin API
-     @javax.annotation.Nullable
-@@ -0,0 +0,0 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
-         } else {
-             this.wasOnFire = this.isOnFire();
-             if (type == MoverType.PISTON) {
-+                this.activatedTick = Math.max(this.activatedTick, MinecraftServer.currentTick + 20); // Paper - EAR 2
-+                this.activatedImmunityTick = Math.max(this.activatedImmunityTick, MinecraftServer.currentTick + 20);   // Paper - EAR 2
-                 movement = this.limitPistonMovement(movement);
-                 if (movement.equals(Vec3.ZERO)) {
-                     return;
-@@ -0,0 +0,0 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
-                 this.stuckSpeedMultiplier = Vec3.ZERO;
-                 this.setDeltaMovement(Vec3.ZERO);
-             }
-+            // Paper start - ignore movement changes while inactive.
-+            if (isTemporarilyActive && !(this instanceof ItemEntity) && movement == getDeltaMovement() && type == MoverType.SELF) {
-+                setDeltaMovement(Vec3.ZERO);
-+                gameprofilerfiller.pop();
-+                return;
-+            }
-+            // Paper end
- 
-             movement = this.maybeBackOffFromEdge(movement, type);
-             Vec3 vec3d1 = this.collide(movement);
-diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/entity/Mob.java
-+++ b/src/main/java/net/minecraft/world/entity/Mob.java
-@@ -0,0 +0,0 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab
-         return this.lookControl;
-     }
- 
-+    // Paper start
-+    @Override
-+    public void inactiveTick() {
-+        super.inactiveTick();
-+        if (this.goalSelector.inactiveTick()) {
-+            this.goalSelector.tick();
-+        }
-+        if (this.targetSelector.inactiveTick()) {
-+            this.targetSelector.tick();
-+        }
-+    }
-+    // Paper end
-+
-     public MoveControl getMoveControl() {
-         Entity entity = this.getControlledVehicle();
- 
-diff --git a/src/main/java/net/minecraft/world/entity/PathfinderMob.java b/src/main/java/net/minecraft/world/entity/PathfinderMob.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/entity/PathfinderMob.java
-+++ b/src/main/java/net/minecraft/world/entity/PathfinderMob.java
-@@ -0,0 +0,0 @@ public abstract class PathfinderMob extends Mob {
-         super(type, world);
-     }
- 
-+    public BlockPos movingTarget; public BlockPos getMovingTarget() { return movingTarget; } // Paper
-+
-     public float getWalkTargetValue(BlockPos pos) {
-         return this.getWalkTargetValue(pos, this.level());
-     }
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ public class GoalSelector {
-     private final Map<Goal.Flag, WrappedGoal> lockedFlags = new EnumMap<>(Goal.Flag.class);
-     private final Set<WrappedGoal> availableGoals = new ObjectLinkedOpenHashSet<>();
-     private final EnumSet<Goal.Flag> disabledFlags = EnumSet.noneOf(Goal.Flag.class);
-+    private int curRate; // Paper - EAR 2
- 
-     public void addGoal(int priority, Goal goal) {
-         this.availableGoals.add(new WrappedGoal(priority, goal));
-@@ -0,0 +0,0 @@ public class GoalSelector {
-         this.availableGoals.removeIf(goal -> predicate.test(goal.getGoal()));
-     }
- 
-+    // Paper start - EAR 2
-+    public boolean inactiveTick() {
-+        this.curRate++;
-+        return this.curRate % 3 == 0; // TODO newGoalRate was already unused in 1.20.4, check if this is correct
-+    }
-+    public boolean hasTasks() {
-+        for (WrappedGoal task : this.availableGoals) {
-+            if (task.isRunning()) {
-+                return true;
-+            }
-+        }
-+        return false;
-+    }
-+    // Paper end - EAR 2
-     public void removeGoal(Goal goal) {
-         for (WrappedGoal wrappedGoal : this.availableGoals) {
-             if (wrappedGoal.getGoal() == goal && wrappedGoal.isRunning()) {
-diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java
-+++ b/src/main/java/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java
-@@ -0,0 +0,0 @@ public abstract class MoveToBlockGoal extends Goal {
-     public MoveToBlockGoal(PathfinderMob mob, double speed, int range) {
-         this(mob, speed, range, 1);
-     }
-+    // Paper start - activation range improvements
-+    @Override
-+    public void stop() {
-+        super.stop();
-+        this.blockPos = BlockPos.ZERO;
-+        this.mob.movingTarget = null;
-+    }
-+    // Paper end
- 
-     public MoveToBlockGoal(PathfinderMob mob, double speed, int range, int maxYDifference) {
-         this.mob = mob;
-@@ -0,0 +0,0 @@ public abstract class MoveToBlockGoal extends Goal {
-                         mutableBlockPos.setWithOffset(blockPos, m, k - 1, n);
-                         if (this.mob.isWithinRestriction(mutableBlockPos) && this.isValidTarget(this.mob.level(), mutableBlockPos)) {
-                             this.blockPos = mutableBlockPos;
-+                            this.mob.movingTarget = mutableBlockPos == BlockPos.ZERO ? null : mutableBlockPos.immutable(); // Paper
-                             return true;
-                         }
-                     }
-diff --git a/src/main/java/net/minecraft/world/entity/npc/Villager.java b/src/main/java/net/minecraft/world/entity/npc/Villager.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/entity/npc/Villager.java
-+++ b/src/main/java/net/minecraft/world/entity/npc/Villager.java
-@@ -0,0 +0,0 @@ public class Villager extends AbstractVillager implements ReputationEventHandler
-     @Override
-     public void inactiveTick() {
-         // SPIGOT-3874, SPIGOT-3894, SPIGOT-3846, SPIGOT-5286 :(
--        if (this.level().spigotConfig.tickInactiveVillagers && this.isEffectiveAi()) {
--            this.customServerAiStep((ServerLevel) this.level());
-+        // Paper start
-+        if (this.getUnhappyCounter() > 0) {
-+            this.setUnhappyCounter(this.getUnhappyCounter() - 1);
-+        }
-+        if (this.isEffectiveAi()) {
-+            if (this.level().spigotConfig.tickInactiveVillagers) {
-+                this.customServerAiStep(this.level().getMinecraftWorld());
-+            } else {
-+                this.customServerAiStep(this.level().getMinecraftWorld(), true);
-+            }
-         }
-+        maybeDecayGossip();
-+        // Paper end
-         super.inactiveTick();
-     }
-     // Spigot End
- 
-     @Override
-     protected void customServerAiStep(ServerLevel world) {
-+        // Paper start - EAR 2
-+        this.customServerAiStep(world, false);
-+    }
-+    protected void customServerAiStep(ServerLevel world, final boolean inactive) {
-+        // Paper end - EAR 2
-         ProfilerFiller gameprofilerfiller = Profiler.get();
- 
-         gameprofilerfiller.push("villagerBrain");
--        this.getBrain().tick(world, this);
-+        if (!inactive) this.getBrain().tick(world, this);
-         gameprofilerfiller.pop();
-         if (this.assignProfessionWhenSpawned) {
-             this.assignProfessionWhenSpawned = false;
-@@ -0,0 +0,0 @@ public class Villager extends AbstractVillager implements ReputationEventHandler
-             this.lastTradedPlayer = null;
-         }
- 
--        if (!this.isNoAi() && this.random.nextInt(100) == 0) {
-+        if (!inactive && !this.isNoAi() && this.random.nextInt(100) == 0) { // Paper - EAR 2
-             Raid raid = world.getRaidAt(this.blockPosition());
- 
-             if (raid != null && raid.isActive() && !raid.isOver()) {
-@@ -0,0 +0,0 @@ public class Villager extends AbstractVillager implements ReputationEventHandler
-         if (this.getVillagerData().getProfession() == VillagerProfession.NONE && this.isTrading()) {
-             this.stopTrading();
-         }
-+        if (inactive) return; // Paper - EAR 2
- 
-         super.customServerAiStep(world);
-     }
-diff --git a/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java b/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java
-+++ b/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java
-@@ -0,0 +0,0 @@ public class MinecartHopper extends AbstractMinecartContainer implements Hopper
-         if (bl != this.isEnabled()) {
-             this.setEnabled(bl);
-         }
-+        this.immunize();  // Paper
-     }
- 
-     public boolean isEnabled() {
-@@ -0,0 +0,0 @@ public class MinecartHopper extends AbstractMinecartContainer implements Hopper
- 
-     public boolean suckInItems() {
-         if (HopperBlockEntity.suckInItems(this.level(), this)) {
-+            this.immunize();  // Paper
-             return true;
-         } else {
-             for (ItemEntity itemEntity : this.level()
-                 .getEntitiesOfClass(ItemEntity.class, this.getBoundingBox().inflate(0.25, 0.0, 0.25), EntitySelector.ENTITY_STILL_ALIVE)) {
-                 if (HopperBlockEntity.addItem(this, itemEntity)) {
-+                    this.immunize();  // Paper
-                     return true;
-                 }
-             }
-@@ -0,0 +0,0 @@ public class MinecartHopper extends AbstractMinecartContainer implements Hopper
-     public AbstractContainerMenu createMenu(int syncId, Inventory playerInventory) {
-         return new HopperMenu(syncId, playerInventory, this);
-     }
-+
-+    // Paper start
-+    public void immunize() {
-+        this.activatedImmunityTick = Math.max(this.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 20);
-+    }
-+    // Paper end
-+
- }
-diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/Level.java
-+++ b/src/main/java/net/minecraft/world/level/Level.java
-@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
-     public Map<BlockPos, BlockEntity> capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper - Retain block place order when capturing blockstates
-     public List<ItemEntity> captureDrops;
-     public final it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<SpawnCategory> ticksPerSpawnCategory = new it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<>();
-+    // Paper start
-+    public int wakeupInactiveRemainingAnimals;
-+    public int wakeupInactiveRemainingFlying;
-+    public int wakeupInactiveRemainingMonsters;
-+    public int wakeupInactiveRemainingVillagers;
-+    // Paper end
-     public boolean populating;
-     public final org.spigotmc.SpigotWorldConfig spigotConfig; // Spigot
-     // Paper start - add paper world config
-diff --git a/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java b/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java
-+++ b/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java
-@@ -0,0 +0,0 @@ public class PistonMovingBlockEntity extends BlockEntity {
-                                 }
- 
-                                 entity.setDeltaMovement(e, g, h);
-+                                // Paper - EAR items stuck in in slime pushed by a piston
-+                                entity.activatedTick = Math.max(entity.activatedTick, net.minecraft.server.MinecraftServer.currentTick + 10);
-+                                entity.activatedImmunityTick = Math.max(entity.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 10);
-+                                // Paper end
-                                 break;
-                             }
-                         }
-diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/spigotmc/ActivationRange.java
-+++ b/src/main/java/org/spigotmc/ActivationRange.java
-@@ -0,0 +0,0 @@
- package org.spigotmc;
- 
-+import net.minecraft.core.BlockPos;
- import net.minecraft.server.MinecraftServer;
-+import net.minecraft.server.level.ServerChunkCache;
- import net.minecraft.world.entity.Entity;
- import net.minecraft.world.entity.ExperienceOrb;
-+import net.minecraft.world.entity.FlyingMob;
- import net.minecraft.world.entity.LightningBolt;
- import net.minecraft.world.entity.LivingEntity;
-+import net.minecraft.world.entity.Mob;
- import net.minecraft.world.entity.PathfinderMob;
-+import net.minecraft.world.entity.ai.Brain;
- import net.minecraft.world.entity.ambient.AmbientCreature;
- import net.minecraft.world.entity.animal.Animal;
-+import net.minecraft.world.entity.animal.Bee;
- import net.minecraft.world.entity.animal.Sheep;
-+import net.minecraft.world.entity.animal.WaterAnimal;
-+import net.minecraft.world.entity.animal.horse.Llama;
- import net.minecraft.world.entity.boss.EnderDragonPart;
- import net.minecraft.world.entity.boss.enderdragon.EndCrystal;
- import net.minecraft.world.entity.boss.enderdragon.EnderDragon;
-@@ -0,0 +0,0 @@ import net.minecraft.world.entity.boss.wither.WitherBoss;
- import net.minecraft.world.entity.item.ItemEntity;
- import net.minecraft.world.entity.item.PrimedTnt;
- import net.minecraft.world.entity.monster.Creeper;
--import net.minecraft.world.entity.monster.Monster;
--import net.minecraft.world.entity.monster.Slime;
-+import net.minecraft.world.entity.monster.Enemy;
-+import net.minecraft.world.entity.monster.Pillager;
- import net.minecraft.world.entity.npc.Villager;
- import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.entity.projectile.AbstractArrow;
- import net.minecraft.world.entity.projectile.AbstractHurtingProjectile;
-+import net.minecraft.world.entity.projectile.EyeOfEnder;
- import net.minecraft.world.entity.projectile.FireworkRocketEntity;
- import net.minecraft.world.entity.projectile.ThrowableProjectile;
- import net.minecraft.world.entity.projectile.ThrownTrident;
-@@ -0,0 +0,0 @@ public class ActivationRange
- 
-         AABB boundingBox = new AABB( 0, 0, 0, 0, 0, 0 );
-     }
-+    // Paper start
-+
-+    static net.minecraft.world.entity.schedule.Activity[] VILLAGER_PANIC_IMMUNITIES = {
-+        net.minecraft.world.entity.schedule.Activity.HIDE,
-+        net.minecraft.world.entity.schedule.Activity.PRE_RAID,
-+        net.minecraft.world.entity.schedule.Activity.RAID,
-+        net.minecraft.world.entity.schedule.Activity.PANIC
-+    };
-+
-+    private static int checkInactiveWakeup(Entity entity) {
-+        Level world = entity.level();
-+        SpigotWorldConfig config = world.spigotConfig;
-+        long inactiveFor = MinecraftServer.currentTick - entity.activatedTick;
-+        if (entity.activationType == ActivationType.VILLAGER) {
-+            if (inactiveFor > config.wakeUpInactiveVillagersEvery && world.wakeupInactiveRemainingVillagers > 0) {
-+                world.wakeupInactiveRemainingVillagers--;
-+                return config.wakeUpInactiveVillagersFor;
-+            }
-+        } else if (entity.activationType == ActivationType.ANIMAL) {
-+            if (inactiveFor > config.wakeUpInactiveAnimalsEvery && world.wakeupInactiveRemainingAnimals > 0) {
-+                world.wakeupInactiveRemainingAnimals--;
-+                return config.wakeUpInactiveAnimalsFor;
-+            }
-+        } else if (entity.activationType == ActivationType.FLYING_MONSTER) {
-+            if (inactiveFor > config.wakeUpInactiveFlyingEvery && world.wakeupInactiveRemainingFlying > 0) {
-+                world.wakeupInactiveRemainingFlying--;
-+                return config.wakeUpInactiveFlyingFor;
-+            }
-+        } else if (entity.activationType == ActivationType.MONSTER || entity.activationType == ActivationType.RAIDER) {
-+            if (inactiveFor > config.wakeUpInactiveMonstersEvery && world.wakeupInactiveRemainingMonsters > 0) {
-+                world.wakeupInactiveRemainingMonsters--;
-+                return config.wakeUpInactiveMonstersFor;
-+            }
-+        }
-+        return -1;
-+    }
-+    // Paper end
- 
-     static AABB maxBB = new AABB( 0, 0, 0, 0, 0, 0 );
- 
-@@ -0,0 +0,0 @@ public class ActivationRange
-      */
-     public static ActivationType initializeEntityActivationType(Entity entity)
-     {
-+        if (entity instanceof WaterAnimal) { return ActivationType.WATER; } // Paper
-+        else if (entity instanceof Villager) { return ActivationType.VILLAGER; } // Paper
-+        else if (entity instanceof FlyingMob && entity instanceof Enemy) { return ActivationType.FLYING_MONSTER; } // Paper - doing & Monster incase Flying no longer includes monster in future
-         if ( entity instanceof Raider )
-         {
-             return ActivationType.RAIDER;
--        } else if ( entity instanceof Monster || entity instanceof Slime )
-+        } else if ( entity instanceof Enemy ) // Paper - correct monster check
-         {
-             return ActivationType.MONSTER;
-         } else if ( entity instanceof PathfinderMob || entity instanceof AmbientCreature )
-@@ -0,0 +0,0 @@ public class ActivationRange
-      */
-     public static boolean initializeEntityActivationState(Entity entity, SpigotWorldConfig config)
-     {
--        if ( ( entity.activationType == ActivationType.MISC && config.miscActivationRange == 0 )
--                || ( entity.activationType == ActivationType.RAIDER && config.raiderActivationRange == 0 )
--                || ( entity.activationType == ActivationType.ANIMAL && config.animalActivationRange == 0 )
--                || ( entity.activationType == ActivationType.MONSTER && config.monsterActivationRange == 0 )
-+        if ( ( entity.activationType == ActivationType.MISC && config.miscActivationRange <= 0 )
-+                || ( entity.activationType == ActivationType.RAIDER && config.raiderActivationRange <= 0 )
-+                || ( entity.activationType == ActivationType.ANIMAL && config.animalActivationRange <= 0 )
-+                || ( entity.activationType == ActivationType.MONSTER && config.monsterActivationRange <= 0 )
-+                || ( entity.activationType == ActivationType.VILLAGER && config.villagerActivationRange <= 0 ) // Paper
-+                || ( entity.activationType == ActivationType.WATER && config.waterActivationRange <= 0 ) // Paper
-+                || ( entity.activationType == ActivationType.FLYING_MONSTER && config.flyingMonsterActivationRange <= 0 ) // Paper
-+                || entity instanceof EyeOfEnder // Paper
-                 || entity instanceof Player
-                 || entity instanceof ThrowableProjectile
-                 || entity instanceof EnderDragon
-@@ -0,0 +0,0 @@ public class ActivationRange
-         final int raiderActivationRange = world.spigotConfig.raiderActivationRange;
-         final int animalActivationRange = world.spigotConfig.animalActivationRange;
-         final int monsterActivationRange = world.spigotConfig.monsterActivationRange;
-+        // Paper start
-+        final int waterActivationRange = world.spigotConfig.waterActivationRange;
-+        final int flyingActivationRange = world.spigotConfig.flyingMonsterActivationRange;
-+        final int villagerActivationRange = world.spigotConfig.villagerActivationRange;
-+        world.wakeupInactiveRemainingAnimals = Math.min(world.wakeupInactiveRemainingAnimals + 1, world.spigotConfig.wakeUpInactiveAnimals);
-+        world.wakeupInactiveRemainingVillagers = Math.min(world.wakeupInactiveRemainingVillagers + 1, world.spigotConfig.wakeUpInactiveVillagers);
-+        world.wakeupInactiveRemainingMonsters = Math.min(world.wakeupInactiveRemainingMonsters + 1, world.spigotConfig.wakeUpInactiveMonsters);
-+        world.wakeupInactiveRemainingFlying = Math.min(world.wakeupInactiveRemainingFlying + 1, world.spigotConfig.wakeUpInactiveFlying);
-+        final ServerChunkCache chunkProvider = (ServerChunkCache) world.getChunkSource();
-+        // Paper end
- 
-         int maxRange = Math.max( monsterActivationRange, animalActivationRange );
-         maxRange = Math.max( maxRange, raiderActivationRange );
-         maxRange = Math.max( maxRange, miscActivationRange );
-+        // Paper start
-+        maxRange = Math.max( maxRange, flyingActivationRange );
-+        maxRange = Math.max( maxRange, waterActivationRange );
-+        maxRange = Math.max( maxRange, villagerActivationRange );
-+        // Paper end
-         maxRange = Math.min( ( world.spigotConfig.simulationDistance << 4 ) - 8, maxRange );
- 
-         for ( Player player : world.players() )
-@@ -0,0 +0,0 @@ public class ActivationRange
-                 continue;
-             }
- 
--            ActivationRange.maxBB = player.getBoundingBox().inflate( maxRange, 256, maxRange );
--            ActivationType.MISC.boundingBox = player.getBoundingBox().inflate( miscActivationRange, 256, miscActivationRange );
--            ActivationType.RAIDER.boundingBox = player.getBoundingBox().inflate( raiderActivationRange, 256, raiderActivationRange );
--            ActivationType.ANIMAL.boundingBox = player.getBoundingBox().inflate( animalActivationRange, 256, animalActivationRange );
--            ActivationType.MONSTER.boundingBox = player.getBoundingBox().inflate( monsterActivationRange, 256, monsterActivationRange );
-+            // Paper start
-+            int worldHeight = world.getHeight();
-+            ActivationRange.maxBB = player.getBoundingBox().inflate( maxRange, worldHeight, maxRange );
-+            ActivationType.MISC.boundingBox = player.getBoundingBox().inflate( miscActivationRange, worldHeight, miscActivationRange );
-+            ActivationType.RAIDER.boundingBox = player.getBoundingBox().inflate( raiderActivationRange, worldHeight, raiderActivationRange );
-+            ActivationType.ANIMAL.boundingBox = player.getBoundingBox().inflate( animalActivationRange, worldHeight, animalActivationRange );
-+            ActivationType.MONSTER.boundingBox = player.getBoundingBox().inflate( monsterActivationRange, worldHeight, monsterActivationRange );
-+            ActivationType.WATER.boundingBox = player.getBoundingBox().inflate( waterActivationRange, worldHeight, waterActivationRange );
-+            ActivationType.FLYING_MONSTER.boundingBox = player.getBoundingBox().inflate( flyingActivationRange, worldHeight, flyingActivationRange );
-+            ActivationType.VILLAGER.boundingBox = player.getBoundingBox().inflate( villagerActivationRange, worldHeight, villagerActivationRange );
-+            // Paper end
- 
--            world.getEntities().get(ActivationRange.maxBB, ActivationRange::activateEntity);
-+            // Paper start
-+            java.util.List<Entity> entities = world.getEntities((Entity)null, ActivationRange.maxBB, null);
-+            boolean tickMarkers = world.paperConfig().entities.markers.tick; // Paper - Configurable marker ticking
-+            for (Entity entity : entities) {
-+                // Paper start - Configurable marker ticking
-+                if (!tickMarkers && entity instanceof net.minecraft.world.entity.Marker) {
-+                    continue;
-+                }
-+                // Paper end - Configurable marker ticking
-+                ActivationRange.activateEntity(entity);
-+            }
-+            // Paper end
-         }
-     }
- 
-@@ -0,0 +0,0 @@ public class ActivationRange
-      * @param entity
-      * @return
-      */
--    public static boolean checkEntityImmunities(Entity entity)
-+    public static int checkEntityImmunities(Entity entity) // Paper - return # of ticks to get immunity
-     {
-+        // Paper start
-+        SpigotWorldConfig config = entity.level().spigotConfig;
-+        int inactiveWakeUpImmunity = checkInactiveWakeup(entity);
-+        if (inactiveWakeUpImmunity > -1) {
-+            return inactiveWakeUpImmunity;
-+        }
-+        if (entity.getRemainingFireTicks() > 0) {
-+            return 2;
-+        }
-+        if (entity.activatedImmunityTick >= MinecraftServer.currentTick) {
-+            return 1;
-+        }
-+        long inactiveFor = MinecraftServer.currentTick - entity.activatedTick;
-+        // Paper end
-         // quick checks.
--        if ( entity.wasTouchingWater || entity.getRemainingFireTicks() > 0 )
-+        if ( (entity.activationType != ActivationType.WATER && entity.wasTouchingWater && entity.isPushedByFluid()) ) // Paper
-         {
--            return true;
-+            return 100; // Paper
-+        }
-+        // Paper start
-+        if ( !entity.onGround() || entity.getDeltaMovement().horizontalDistanceSqr() > 9.999999747378752E-6D )
-+        {
-+            return 100;
-         }
-+        // Paper end
-         if ( !( entity instanceof AbstractArrow ) )
-         {
--            if ( !entity.onGround() || !entity.passengers.isEmpty() || entity.isPassenger() )
-+            if ( (!entity.onGround() && !(entity instanceof FlyingMob)) ) // Paper - remove passengers logic
-             {
--                return true;
-+                return 10; // Paper
-             }
-         } else if ( !( (AbstractArrow) entity ).isInGround() )
-         {
--            return true;
-+            return 1; // Paper
-         }
-         // special cases.
-         if ( entity instanceof LivingEntity )
-         {
-             LivingEntity living = (LivingEntity) entity;
--            if ( /*TODO: Missed mapping? living.attackTicks > 0 || */ living.hurtTime > 0 || living.activeEffects.size() > 0 )
-+            if ( living.onClimbable() || living.jumping || living.hurtTime > 0 || living.activeEffects.size() > 0 || living.isFreezing()) // Paper
-             {
--                return true;
-+                return 1; // Paper
-             }
--            if ( entity instanceof PathfinderMob && ( (PathfinderMob) entity ).getTarget() != null )
-+            if ( entity instanceof Mob && ((Mob) entity ).getTarget() != null) // Paper
-             {
--                return true;
-+                return 20; // Paper
-             }
--            if ( entity instanceof Villager && ( (Villager) entity ).canBreed() )
-+            // Paper start
-+            if (entity instanceof Bee) {
-+                Bee bee = (Bee)entity;
-+                BlockPos movingTarget = bee.getMovingTarget();
-+                if (bee.isAngry() ||
-+                    (bee.getHivePos() != null && bee.getHivePos().equals(movingTarget)) ||
-+                    (bee.getSavedFlowerPos() != null && bee.getSavedFlowerPos().equals(movingTarget))
-+                ) {
-+                    return 20;
-+                }
-+            }
-+            if ( entity instanceof Villager ) {
-+                Brain<Villager> behaviorController = ((Villager) entity).getBrain();
-+
-+                if (config.villagersActiveForPanic) {
-+                    for (net.minecraft.world.entity.schedule.Activity activity : VILLAGER_PANIC_IMMUNITIES) {
-+                        if (behaviorController.isActive(activity)) {
-+                            return 20*5;
-+                        }
-+                    }
-+                }
-+
-+                if (config.villagersWorkImmunityAfter > 0 && inactiveFor >= config.villagersWorkImmunityAfter) {
-+                    if (behaviorController.isActive(net.minecraft.world.entity.schedule.Activity.WORK)) {
-+                        return config.villagersWorkImmunityFor;
-+                    }
-+                }
-+            }
-+            if ( entity instanceof Llama && ( (Llama) entity ).inCaravan() )
-             {
--                return true;
-+                return 1;
-             }
-+            // Paper end
-             if ( entity instanceof Animal )
-             {
-                 Animal animal = (Animal) entity;
-                 if ( animal.isBaby() || animal.isInLove() )
-                 {
--                    return true;
-+                    return 5; // Paper
-                 }
-                 if ( entity instanceof Sheep && ( (Sheep) entity ).isSheared() )
-                 {
--                    return true;
-+                    return 1; // Paper
-                 }
-             }
-             if (entity instanceof Creeper && ((Creeper) entity).isIgnited()) { // isExplosive
--                return true;
-+                return 20; // Paper
-+            }
-+            // Paper start
-+            if (entity instanceof Mob && ((Mob) entity).targetSelector.hasTasks() ) {
-+                return 0;
-             }
-+            if (entity instanceof Pillager) {
-+                Pillager pillager = (Pillager) entity;
-+                // TODO:?
-+            }
-+            // Paper end
-         }
-         // SPIGOT-6644: Otherwise the target refresh tick will be missed
-         if (entity instanceof ExperienceOrb) {
--            return true;
-+            return 20; // Paper
-         }
--        return false;
-+        return -1; // Paper
-     }
- 
-     /**
-@@ -0,0 +0,0 @@ public class ActivationRange
-         if (entity instanceof FireworkRocketEntity || (entity instanceof ItemEntity && (entity.tickCount + entity.getId()) % 4 == 0)) { // Paper - Needed for item gravity, see ItemEntity tick
-             return true;
-         }
-+        // Paper start - special case always immunities
-+        // immunize brand new entities, dead entities, and portal scenarios
-+        if (entity.defaultActivationState || entity.tickCount < 20*10 || !entity.isAlive() || (entity.portalProcess != null && !entity.portalProcess.hasExpired()) || entity.portalCooldown > 0) {
-+            return true;
-+        }
-+        // immunize leashed entities
-+        if (entity instanceof Mob && ((Mob)entity).getLeashHolder() instanceof Player) {
-+            return true;
-+        }
-+        // Paper end
- 
--        boolean isActive = entity.activatedTick >= MinecraftServer.currentTick || entity.defaultActivationState;
-+        boolean isActive = entity.activatedTick >= MinecraftServer.currentTick;
-+        entity.isTemporarilyActive = false; // Paper
- 
-         // Should this entity tick?
-         if ( !isActive )
-@@ -0,0 +0,0 @@ public class ActivationRange
-             if ( ( MinecraftServer.currentTick - entity.activatedTick - 1 ) % 20 == 0 )
-             {
-                 // Check immunities every 20 ticks.
--                if ( ActivationRange.checkEntityImmunities( entity ) )
--                {
--                    // Triggered some sort of immunity, give 20 full ticks before we check again.
--                    entity.activatedTick = MinecraftServer.currentTick + 20;
-+                // Paper start
-+                int immunity = checkEntityImmunities(entity);
-+                if (immunity >= 0) {
-+                    entity.activatedTick = MinecraftServer.currentTick + immunity;
-+                } else {
-+                    entity.isTemporarilyActive = true;
-                 }
-+                // Paper end
-                 isActive = true;
-             }
-         }
-diff --git a/src/main/java/org/spigotmc/SpigotWorldConfig.java b/src/main/java/org/spigotmc/SpigotWorldConfig.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/spigotmc/SpigotWorldConfig.java
-+++ b/src/main/java/org/spigotmc/SpigotWorldConfig.java
-@@ -0,0 +0,0 @@ public class SpigotWorldConfig
-     public int monsterActivationRange = 32;
-     public int raiderActivationRange = 64;
-     public int miscActivationRange = 16;
-+    // Paper start
-+    public int flyingMonsterActivationRange = 32;
-+    public int waterActivationRange = 16;
-+    public int villagerActivationRange = 32;
-+    public int wakeUpInactiveAnimals = 4;
-+    public int wakeUpInactiveAnimalsEvery = 60*20;
-+    public int wakeUpInactiveAnimalsFor = 5*20;
-+    public int wakeUpInactiveMonsters = 8;
-+    public int wakeUpInactiveMonstersEvery = 20*20;
-+    public int wakeUpInactiveMonstersFor = 5*20;
-+    public int wakeUpInactiveVillagers = 4;
-+    public int wakeUpInactiveVillagersEvery = 30*20;
-+    public int wakeUpInactiveVillagersFor = 5*20;
-+    public int wakeUpInactiveFlying = 8;
-+    public int wakeUpInactiveFlyingEvery = 10*20;
-+    public int wakeUpInactiveFlyingFor = 5*20;
-+    public int villagersWorkImmunityAfter = 5*20;
-+    public int villagersWorkImmunityFor = 20;
-+    public boolean villagersActiveForPanic = true;
-+    // Paper end
-     public boolean tickInactiveVillagers = true;
-     public boolean ignoreSpectatorActivation = false;
-     private void activationRange()
-     {
-+        boolean hasAnimalsConfig = config.getInt("entity-activation-range.animals", this.animalActivationRange) != this.animalActivationRange; // Paper
-         this.animalActivationRange = this.getInt( "entity-activation-range.animals", this.animalActivationRange );
-         this.monsterActivationRange = this.getInt( "entity-activation-range.monsters", this.monsterActivationRange );
-         this.raiderActivationRange = this.getInt( "entity-activation-range.raiders", this.raiderActivationRange );
-         this.miscActivationRange = this.getInt( "entity-activation-range.misc", this.miscActivationRange );
-+        // Paper start
-+        this.waterActivationRange = this.getInt( "entity-activation-range.water", this.waterActivationRange );
-+        this.villagerActivationRange = this.getInt( "entity-activation-range.villagers", hasAnimalsConfig ? this.animalActivationRange : this.villagerActivationRange );
-+        this.flyingMonsterActivationRange = this.getInt( "entity-activation-range.flying-monsters", this.flyingMonsterActivationRange );
-+
-+        this.wakeUpInactiveAnimals = this.getInt("entity-activation-range.wake-up-inactive.animals-max-per-tick", this.wakeUpInactiveAnimals);
-+        this.wakeUpInactiveAnimalsEvery = this.getInt("entity-activation-range.wake-up-inactive.animals-every", this.wakeUpInactiveAnimalsEvery);
-+        this.wakeUpInactiveAnimalsFor = this.getInt("entity-activation-range.wake-up-inactive.animals-for", this.wakeUpInactiveAnimalsFor);
-+
-+        this.wakeUpInactiveMonsters = this.getInt("entity-activation-range.wake-up-inactive.monsters-max-per-tick", this.wakeUpInactiveMonsters);
-+        this.wakeUpInactiveMonstersEvery = this.getInt("entity-activation-range.wake-up-inactive.monsters-every", this.wakeUpInactiveMonstersEvery);
-+        this.wakeUpInactiveMonstersFor = this.getInt("entity-activation-range.wake-up-inactive.monsters-for", this.wakeUpInactiveMonstersFor);
-+
-+        this.wakeUpInactiveVillagers = this.getInt("entity-activation-range.wake-up-inactive.villagers-max-per-tick", this.wakeUpInactiveVillagers);
-+        this.wakeUpInactiveVillagersEvery = this.getInt("entity-activation-range.wake-up-inactive.villagers-every", this.wakeUpInactiveVillagersEvery);
-+        this.wakeUpInactiveVillagersFor = this.getInt("entity-activation-range.wake-up-inactive.villagers-for", this.wakeUpInactiveVillagersFor);
-+
-+        this.wakeUpInactiveFlying = this.getInt("entity-activation-range.wake-up-inactive.flying-monsters-max-per-tick", this.wakeUpInactiveFlying);
-+        this.wakeUpInactiveFlyingEvery = this.getInt("entity-activation-range.wake-up-inactive.flying-monsters-every", this.wakeUpInactiveFlyingEvery);
-+        this.wakeUpInactiveFlyingFor = this.getInt("entity-activation-range.wake-up-inactive.flying-monsters-for", this.wakeUpInactiveFlyingFor);
-+
-+        this.villagersWorkImmunityAfter = this.getInt( "entity-activation-range.villagers-work-immunity-after", this.villagersWorkImmunityAfter );
-+        this.villagersWorkImmunityFor = this.getInt( "entity-activation-range.villagers-work-immunity-for", this.villagersWorkImmunityFor );
-+        this.villagersActiveForPanic = this.getBoolean( "entity-activation-range.villagers-active-for-panic", this.villagersActiveForPanic );
-+        // Paper end
-         this.tickInactiveVillagers = this.getBoolean( "entity-activation-range.tick-inactive-villagers", this.tickInactiveVillagers );
-         this.ignoreSpectatorActivation = this.getBoolean( "entity-activation-range.ignore-spectators", this.ignoreSpectatorActivation );
-         this.log( "Entity Activation Range: An " + this.animalActivationRange + " / Mo " + this.monsterActivationRange + " / Ra " + this.raiderActivationRange + " / Mi " + this.miscActivationRange + " / Tiv " + this.tickInactiveVillagers + " / Isa " + this.ignoreSpectatorActivation );
diff --git a/feature-patches/1044-Anti-Xray.patch b/feature-patches/1044-Anti-Xray.patch
deleted file mode 100644
index f002f23549..0000000000
--- a/feature-patches/1044-Anti-Xray.patch
+++ /dev/null
@@ -1,1628 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: stonar96 <minecraft.stonar96@gmail.com>
-Date: Thu, 25 Nov 2021 13:27:51 +0100
-Subject: [PATCH] Anti-Xray
-
-
-diff --git a/src/main/java/com/destroystokyo/paper/antixray/BitStorageReader.java b/src/main/java/com/destroystokyo/paper/antixray/BitStorageReader.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/antixray/BitStorageReader.java
-@@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.antixray;
-+
-+public final class BitStorageReader {
-+
-+    private byte[] buffer;
-+    private int bits;
-+    private int mask;
-+    private int longInBufferIndex;
-+    private int bitInLongIndex;
-+    private long current;
-+
-+    public void setBuffer(byte[] buffer) {
-+        this.buffer = buffer;
-+    }
-+
-+    public void setBits(int bits) {
-+        this.bits = bits;
-+        mask = (1 << bits) - 1;
-+    }
-+
-+    public void setIndex(int index) {
-+        longInBufferIndex = index;
-+        bitInLongIndex = 0;
-+        init();
-+    }
-+
-+    private void init() {
-+        if (buffer.length > longInBufferIndex + 7) {
-+            current = ((((long) buffer[longInBufferIndex]) << 56)
-+                | (((long) buffer[longInBufferIndex + 1] & 0xff) << 48)
-+                | (((long) buffer[longInBufferIndex + 2] & 0xff) << 40)
-+                | (((long) buffer[longInBufferIndex + 3] & 0xff) << 32)
-+                | (((long) buffer[longInBufferIndex + 4] & 0xff) << 24)
-+                | (((long) buffer[longInBufferIndex + 5] & 0xff) << 16)
-+                | (((long) buffer[longInBufferIndex + 6] & 0xff) << 8)
-+                | (((long) buffer[longInBufferIndex + 7] & 0xff)));
-+        }
-+    }
-+
-+    public int read() {
-+        if (bitInLongIndex + bits > 64) {
-+            bitInLongIndex = 0;
-+            longInBufferIndex += 8;
-+            init();
-+        }
-+
-+        int value = (int) (current >>> bitInLongIndex) & mask;
-+        bitInLongIndex += bits;
-+        return value;
-+    }
-+}
-diff --git a/src/main/java/com/destroystokyo/paper/antixray/BitStorageWriter.java b/src/main/java/com/destroystokyo/paper/antixray/BitStorageWriter.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/antixray/BitStorageWriter.java
-@@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.antixray;
-+
-+public final class BitStorageWriter {
-+
-+    private byte[] buffer;
-+    private int bits;
-+    private long mask;
-+    private int longInBufferIndex;
-+    private int bitInLongIndex;
-+    private long current;
-+    private boolean dirty;
-+
-+    public void setBuffer(byte[] buffer) {
-+        this.buffer = buffer;
-+    }
-+
-+    public void setBits(int bits) {
-+        this.bits = bits;
-+        mask = (1L << bits) - 1;
-+    }
-+
-+    public void setIndex(int index) {
-+        longInBufferIndex = index;
-+        bitInLongIndex = 0;
-+        init();
-+    }
-+
-+    private void init() {
-+        if (buffer.length > longInBufferIndex + 7) {
-+            current = ((((long) buffer[longInBufferIndex]) << 56)
-+                | (((long) buffer[longInBufferIndex + 1] & 0xff) << 48)
-+                | (((long) buffer[longInBufferIndex + 2] & 0xff) << 40)
-+                | (((long) buffer[longInBufferIndex + 3] & 0xff) << 32)
-+                | (((long) buffer[longInBufferIndex + 4] & 0xff) << 24)
-+                | (((long) buffer[longInBufferIndex + 5] & 0xff) << 16)
-+                | (((long) buffer[longInBufferIndex + 6] & 0xff) << 8)
-+                | (((long) buffer[longInBufferIndex + 7] & 0xff)));
-+        }
-+
-+        dirty = false;
-+    }
-+
-+    public void flush() {
-+        if (dirty && buffer.length > longInBufferIndex + 7) {
-+            buffer[longInBufferIndex] = (byte) (current >> 56 & 0xff);
-+            buffer[longInBufferIndex + 1] = (byte) (current >> 48 & 0xff);
-+            buffer[longInBufferIndex + 2] = (byte) (current >> 40 & 0xff);
-+            buffer[longInBufferIndex + 3] = (byte) (current >> 32 & 0xff);
-+            buffer[longInBufferIndex + 4] = (byte) (current >> 24 & 0xff);
-+            buffer[longInBufferIndex + 5] = (byte) (current >> 16 & 0xff);
-+            buffer[longInBufferIndex + 6] = (byte) (current >> 8 & 0xff);
-+            buffer[longInBufferIndex + 7] = (byte) (current & 0xff);
-+        }
-+    }
-+
-+    public void write(int value) {
-+        if (bitInLongIndex + bits > 64) {
-+            flush();
-+            bitInLongIndex = 0;
-+            longInBufferIndex += 8;
-+            init();
-+        }
-+
-+        current = current & ~(mask << bitInLongIndex) | (value & mask) << bitInLongIndex;
-+        dirty = true;
-+        bitInLongIndex += bits;
-+    }
-+
-+    public void skip() {
-+        bitInLongIndex += bits;
-+
-+        if (bitInLongIndex > 64) {
-+            flush();
-+            bitInLongIndex = bits;
-+            longInBufferIndex += 8;
-+            init();
-+        }
-+    }
-+}
-diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockController.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockController.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockController.java
-@@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.antixray;
-+
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.core.Direction;
-+import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket;
-+import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket;
-+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.server.level.ServerPlayerGameMode;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.world.level.chunk.LevelChunk;
-+
-+public class ChunkPacketBlockController {
-+
-+    public static final ChunkPacketBlockController NO_OPERATION_INSTANCE = new ChunkPacketBlockController();
-+
-+    protected ChunkPacketBlockController() {
-+
-+    }
-+
-+    public BlockState[] getPresetBlockStates(Level level, ChunkPos chunkPos, int chunkSectionY) {
-+        return null;
-+    }
-+
-+    public boolean shouldModify(ServerPlayer player, LevelChunk chunk) {
-+        return false;
-+    }
-+
-+    public ChunkPacketInfo<BlockState> getChunkPacketInfo(ClientboundLevelChunkWithLightPacket chunkPacket, LevelChunk chunk) {
-+        return null;
-+    }
-+
-+    public void modifyBlocks(ClientboundLevelChunkWithLightPacket chunkPacket, ChunkPacketInfo<BlockState> chunkPacketInfo) {
-+        chunkPacket.setReady(true);
-+    }
-+
-+    public void onBlockChange(Level level, BlockPos blockPos, BlockState newBlockState, BlockState oldBlockState, int flags, int maxUpdateDepth) {
-+
-+    }
-+
-+    public void onPlayerLeftClickBlock(ServerPlayerGameMode serverPlayerGameMode, BlockPos blockPos, ServerboundPlayerActionPacket.Action action, Direction direction, int worldHeight, int sequence) {
-+
-+    }
-+}
-diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java
-@@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.antixray;
-+
-+import io.papermc.paper.configuration.WorldConfiguration;
-+import io.papermc.paper.configuration.type.EngineMode;
-+import java.util.ArrayList;
-+import java.util.LinkedHashSet;
-+import java.util.LinkedList;
-+import java.util.List;
-+import java.util.Set;
-+import java.util.concurrent.Executor;
-+import java.util.concurrent.ThreadLocalRandom;
-+import java.util.function.IntSupplier;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.core.Direction;
-+import net.minecraft.core.registries.Registries;
-+import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket;
-+import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket;
-+import net.minecraft.server.MinecraftServer;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.server.level.ServerPlayerGameMode;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.biome.Biomes;
-+import net.minecraft.world.level.block.Block;
-+import net.minecraft.world.level.block.Blocks;
-+import net.minecraft.world.level.block.EntityBlock;
-+import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.world.level.chunk.EmptyLevelChunk;
-+import net.minecraft.world.level.chunk.GlobalPalette;
-+import net.minecraft.world.level.chunk.LevelChunk;
-+import net.minecraft.world.level.chunk.LevelChunkSection;
-+import net.minecraft.world.level.chunk.MissingPaletteEntryException;
-+import net.minecraft.world.level.chunk.Palette;
-+import org.bukkit.Bukkit;
-+
-+public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockController {
-+
-+    private static final Palette<BlockState> GLOBAL_BLOCKSTATE_PALETTE = new GlobalPalette<>(Block.BLOCK_STATE_REGISTRY);
-+    private static final LevelChunkSection EMPTY_SECTION = null;
-+    private final Executor executor;
-+    private final EngineMode engineMode;
-+    private final int maxBlockHeight;
-+    private final int updateRadius;
-+    private final boolean usePermission;
-+    private final BlockState[] presetBlockStates;
-+    private final BlockState[] presetBlockStatesFull;
-+    private final BlockState[] presetBlockStatesStone;
-+    private final BlockState[] presetBlockStatesDeepslate;
-+    private final BlockState[] presetBlockStatesNetherrack;
-+    private final BlockState[] presetBlockStatesEndStone;
-+    private final int[] presetBlockStateBitsGlobal;
-+    private final int[] presetBlockStateBitsStoneGlobal;
-+    private final int[] presetBlockStateBitsDeepslateGlobal;
-+    private final int[] presetBlockStateBitsNetherrackGlobal;
-+    private final int[] presetBlockStateBitsEndStoneGlobal;
-+    private final boolean[] solidGlobal = new boolean[Block.BLOCK_STATE_REGISTRY.size()];
-+    private final boolean[] obfuscateGlobal = new boolean[Block.BLOCK_STATE_REGISTRY.size()];
-+    private final LevelChunkSection[] emptyNearbyChunkSections = {EMPTY_SECTION, EMPTY_SECTION, EMPTY_SECTION, EMPTY_SECTION};
-+    private final int maxBlockHeightUpdatePosition;
-+
-+    public ChunkPacketBlockControllerAntiXray(Level level, Executor executor) {
-+        this.executor = executor;
-+        WorldConfiguration.Anticheat.AntiXray paperWorldConfig = level.paperConfig().anticheat.antiXray;
-+        engineMode = paperWorldConfig.engineMode;
-+        maxBlockHeight = paperWorldConfig.maxBlockHeight >> 4 << 4;
-+        updateRadius = paperWorldConfig.updateRadius;
-+        usePermission = paperWorldConfig.usePermission;
-+        List<Block> toObfuscate;
-+
-+        if (engineMode == EngineMode.HIDE) {
-+            toObfuscate = paperWorldConfig.hiddenBlocks;
-+            presetBlockStates = null;
-+            presetBlockStatesFull = null;
-+            presetBlockStatesStone = new BlockState[]{Blocks.STONE.defaultBlockState()};
-+            presetBlockStatesDeepslate = new BlockState[]{Blocks.DEEPSLATE.defaultBlockState()};
-+            presetBlockStatesNetherrack = new BlockState[]{Blocks.NETHERRACK.defaultBlockState()};
-+            presetBlockStatesEndStone = new BlockState[]{Blocks.END_STONE.defaultBlockState()};
-+            presetBlockStateBitsGlobal = null;
-+            presetBlockStateBitsStoneGlobal = new int[]{GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.STONE.defaultBlockState())};
-+            presetBlockStateBitsDeepslateGlobal = new int[]{GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.DEEPSLATE.defaultBlockState())};
-+            presetBlockStateBitsNetherrackGlobal = new int[]{GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.NETHERRACK.defaultBlockState())};
-+            presetBlockStateBitsEndStoneGlobal = new int[]{GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.END_STONE.defaultBlockState())};
-+        } else {
-+            toObfuscate = new ArrayList<>(paperWorldConfig.replacementBlocks);
-+            List<BlockState> presetBlockStateList = new LinkedList<>();
-+
-+            for (Block block : paperWorldConfig.hiddenBlocks) {
-+
-+                if (!(block instanceof EntityBlock)) {
-+                    toObfuscate.add(block);
-+                    presetBlockStateList.add(block.defaultBlockState());
-+                }
-+            }
-+
-+            // The doc of the LinkedHashSet(Collection<? extends E>) constructor doesn't specify that the insertion order is the predictable iteration order of the specified Collection, although it is in the implementation
-+            Set<BlockState> presetBlockStateSet = new LinkedHashSet<>();
-+            // Therefore addAll(Collection<? extends E>) is used, which guarantees this order in the doc
-+            presetBlockStateSet.addAll(presetBlockStateList);
-+            presetBlockStates = presetBlockStateSet.isEmpty() ? new BlockState[]{Blocks.DIAMOND_ORE.defaultBlockState()} : presetBlockStateSet.toArray(new BlockState[0]);
-+            presetBlockStatesFull = presetBlockStateSet.isEmpty() ? new BlockState[]{Blocks.DIAMOND_ORE.defaultBlockState()} : presetBlockStateList.toArray(new BlockState[0]);
-+            presetBlockStatesStone = null;
-+            presetBlockStatesDeepslate = null;
-+            presetBlockStatesNetherrack = null;
-+            presetBlockStatesEndStone = null;
-+            presetBlockStateBitsGlobal = new int[presetBlockStatesFull.length];
-+
-+            for (int i = 0; i < presetBlockStatesFull.length; i++) {
-+                presetBlockStateBitsGlobal[i] = GLOBAL_BLOCKSTATE_PALETTE.idFor(presetBlockStatesFull[i]);
-+            }
-+
-+            presetBlockStateBitsStoneGlobal = null;
-+            presetBlockStateBitsDeepslateGlobal = null;
-+            presetBlockStateBitsNetherrackGlobal = null;
-+            presetBlockStateBitsEndStoneGlobal = null;
-+        }
-+
-+        for (Block block : toObfuscate) {
-+
-+            // Don't obfuscate air because air causes unnecessary block updates and causes block updates to fail in the void
-+            if (block != null && !block.defaultBlockState().isAir()) {
-+                // Replace all block states of a specified block
-+                for (BlockState blockState : block.getStateDefinition().getPossibleStates()) {
-+                    obfuscateGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(blockState)] = true;
-+                }
-+            }
-+        }
-+
-+        EmptyLevelChunk emptyChunk = new EmptyLevelChunk(level, new ChunkPos(0, 0), MinecraftServer.getServer().registryAccess().lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.PLAINS));
-+        BlockPos zeroPos = new BlockPos(0, 0, 0);
-+
-+        for (int i = 0; i < solidGlobal.length; i++) {
-+            BlockState blockState = GLOBAL_BLOCKSTATE_PALETTE.valueFor(i);
-+
-+            if (blockState != null) {
-+                solidGlobal[i] = blockState.isRedstoneConductor(emptyChunk, zeroPos)
-+                    && blockState.getBlock() != Blocks.SPAWNER && blockState.getBlock() != Blocks.BARRIER && blockState.getBlock() != Blocks.SHULKER_BOX && blockState.getBlock() != Blocks.SLIME_BLOCK && blockState.getBlock() != Blocks.MANGROVE_ROOTS || paperWorldConfig.lavaObscures && blockState == Blocks.LAVA.defaultBlockState();
-+                // Comparing blockState == Blocks.LAVA.defaultBlockState() instead of blockState.getBlock() == Blocks.LAVA ensures that only "stationary lava" is used
-+                // shulker box checks TE.
-+            }
-+        }
-+
-+        maxBlockHeightUpdatePosition = maxBlockHeight + updateRadius - 1;
-+    }
-+
-+    private int getPresetBlockStatesFullLength() {
-+        return engineMode == EngineMode.HIDE ? 1 : presetBlockStatesFull.length;
-+    }
-+
-+    @Override
-+    public BlockState[] getPresetBlockStates(Level level, ChunkPos chunkPos, int chunkSectionY) {
-+        // Return the block states to be added to the paletted containers so that they can be used for obfuscation
-+        int bottomBlockY = chunkSectionY << 4;
-+
-+        if (bottomBlockY < maxBlockHeight) {
-+            if (engineMode == EngineMode.HIDE) {
-+                return switch (level.getWorld().getEnvironment()) {
-+                    case NETHER -> presetBlockStatesNetherrack;
-+                    case THE_END -> presetBlockStatesEndStone;
-+                    default -> bottomBlockY < 0 ? presetBlockStatesDeepslate : presetBlockStatesStone;
-+                };
-+            }
-+
-+            return presetBlockStates;
-+        }
-+
-+        return null;
-+    }
-+
-+    @Override
-+    public boolean shouldModify(ServerPlayer player, LevelChunk chunk) {
-+        return !usePermission || !player.getBukkitEntity().hasPermission("paper.antixray.bypass");
-+    }
-+
-+    @Override
-+    public ChunkPacketInfoAntiXray getChunkPacketInfo(ClientboundLevelChunkWithLightPacket chunkPacket, LevelChunk chunk) {
-+        // Return a new instance to collect data and objects in the right state while creating the chunk packet for thread safe access later
-+        return new ChunkPacketInfoAntiXray(chunkPacket, chunk, this);
-+    }
-+
-+    @Override
-+    public void modifyBlocks(ClientboundLevelChunkWithLightPacket chunkPacket, ChunkPacketInfo<BlockState> chunkPacketInfo) {
-+        if (!(chunkPacketInfo instanceof ChunkPacketInfoAntiXray)) {
-+            chunkPacket.setReady(true);
-+            return;
-+        }
-+
-+        if (!Bukkit.isPrimaryThread()) {
-+            // Plugins?
-+            MinecraftServer.getServer().scheduleOnMain(() -> modifyBlocks(chunkPacket, chunkPacketInfo));
-+            return;
-+        }
-+
-+        LevelChunk chunk = chunkPacketInfo.getChunk();
-+        int x = chunk.getPos().x;
-+        int z = chunk.getPos().z;
-+        Level level = chunk.getLevel();
-+        ((ChunkPacketInfoAntiXray) chunkPacketInfo).setNearbyChunks(level.getChunkIfLoaded(x - 1, z), level.getChunkIfLoaded(x + 1, z), level.getChunkIfLoaded(x, z - 1), level.getChunkIfLoaded(x, z + 1));
-+        executor.execute((Runnable) chunkPacketInfo);
-+    }
-+
-+    // Actually these fields should be variables inside the obfuscate method but in sync mode or with SingleThreadExecutor in async mode it's okay (even without ThreadLocal)
-+    // If an ExecutorService with multiple threads is used, ThreadLocal must be used here
-+    private final ThreadLocal<int[]> presetBlockStateBits = ThreadLocal.withInitial(() -> new int[getPresetBlockStatesFullLength()]);
-+    private static final ThreadLocal<boolean[]> SOLID = ThreadLocal.withInitial(() -> new boolean[Block.BLOCK_STATE_REGISTRY.size()]);
-+    private static final ThreadLocal<boolean[]> OBFUSCATE = ThreadLocal.withInitial(() -> new boolean[Block.BLOCK_STATE_REGISTRY.size()]);
-+    // These boolean arrays represent chunk layers, true means don't obfuscate, false means obfuscate
-+    private static final ThreadLocal<boolean[][]> CURRENT = ThreadLocal.withInitial(() -> new boolean[16][16]);
-+    private static final ThreadLocal<boolean[][]> NEXT = ThreadLocal.withInitial(() -> new boolean[16][16]);
-+    private static final ThreadLocal<boolean[][]> NEXT_NEXT = ThreadLocal.withInitial(() -> new boolean[16][16]);
-+
-+    public void obfuscate(ChunkPacketInfoAntiXray chunkPacketInfoAntiXray) {
-+        int[] presetBlockStateBits = this.presetBlockStateBits.get();
-+        boolean[] solid = SOLID.get();
-+        boolean[] obfuscate = OBFUSCATE.get();
-+        boolean[][] current = CURRENT.get();
-+        boolean[][] next = NEXT.get();
-+        boolean[][] nextNext = NEXT_NEXT.get();
-+        // bitStorageReader, bitStorageWriter and nearbyChunkSections could also be reused (with ThreadLocal if necessary) but it's not worth it
-+        BitStorageReader bitStorageReader = new BitStorageReader();
-+        BitStorageWriter bitStorageWriter = new BitStorageWriter();
-+        LevelChunkSection[] nearbyChunkSections = new LevelChunkSection[4];
-+        LevelChunk chunk = chunkPacketInfoAntiXray.getChunk();
-+        Level level = chunk.getLevel();
-+        int maxChunkSectionIndex = Math.min((maxBlockHeight >> 4) - chunk.getMinSectionY(), chunk.getSectionsCount()) - 1;
-+        boolean[] solidTemp = null;
-+        boolean[] obfuscateTemp = null;
-+        bitStorageReader.setBuffer(chunkPacketInfoAntiXray.getBuffer());
-+        bitStorageWriter.setBuffer(chunkPacketInfoAntiXray.getBuffer());
-+        int numberOfBlocks = presetBlockStateBits.length;
-+        // Keep the lambda expressions as simple as possible. They are used very frequently.
-+        LayeredIntSupplier random = numberOfBlocks == 1 ? (() -> 0) : engineMode == EngineMode.OBFUSCATE_LAYER ? new LayeredIntSupplier() {
-+            // engine-mode: 3
-+            private int state;
-+            private int next;
-+
-+            {
-+                while ((state = ThreadLocalRandom.current().nextInt()) == 0) ;
-+            }
-+
-+            @Override
-+            public void nextLayer() {
-+                // https://en.wikipedia.org/wiki/Xorshift
-+                state ^= state << 13;
-+                state ^= state >>> 17;
-+                state ^= state << 5;
-+                // https://www.pcg-random.org/posts/bounded-rands.html
-+                next = (int) ((Integer.toUnsignedLong(state) * numberOfBlocks) >>> 32);
-+            }
-+
-+            @Override
-+            public int getAsInt() {
-+                return next;
-+            }
-+        } : new LayeredIntSupplier() {
-+            // engine-mode: 2
-+            private int state;
-+
-+            {
-+                while ((state = ThreadLocalRandom.current().nextInt()) == 0) ;
-+            }
-+
-+            @Override
-+            public int getAsInt() {
-+                // https://en.wikipedia.org/wiki/Xorshift
-+                state ^= state << 13;
-+                state ^= state >>> 17;
-+                state ^= state << 5;
-+                // https://www.pcg-random.org/posts/bounded-rands.html
-+                return (int) ((Integer.toUnsignedLong(state) * numberOfBlocks) >>> 32);
-+            }
-+        };
-+
-+        for (int chunkSectionIndex = 0; chunkSectionIndex <= maxChunkSectionIndex; chunkSectionIndex++) {
-+            if (chunkPacketInfoAntiXray.isWritten(chunkSectionIndex) && chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex) != null) {
-+                int[] presetBlockStateBitsTemp;
-+
-+                if (chunkPacketInfoAntiXray.getPalette(chunkSectionIndex) instanceof GlobalPalette) {
-+                    if (engineMode == EngineMode.HIDE) {
-+                        presetBlockStateBitsTemp = switch (level.getWorld().getEnvironment()) {
-+                            case NETHER -> presetBlockStateBitsNetherrackGlobal;
-+                            case THE_END -> presetBlockStateBitsEndStoneGlobal;
-+                            default -> chunkSectionIndex + chunk.getMinSectionY() < 0 ? presetBlockStateBitsDeepslateGlobal : presetBlockStateBitsStoneGlobal;
-+                        };
-+                    } else {
-+                        presetBlockStateBitsTemp = presetBlockStateBitsGlobal;
-+                    }
-+                } else {
-+                    // If it's presetBlockStates, use this.presetBlockStatesFull instead
-+                    BlockState[] presetBlockStatesFull = chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex) == presetBlockStates ? this.presetBlockStatesFull : chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex);
-+                    presetBlockStateBitsTemp = presetBlockStateBits;
-+
-+                    for (int i = 0; i < presetBlockStateBitsTemp.length; i++) {
-+                        // This is thread safe because we only request IDs that are guaranteed to be in the palette and are visible
-+                        // For more details see the comments in the readPalette method
-+                        presetBlockStateBitsTemp[i] = chunkPacketInfoAntiXray.getPalette(chunkSectionIndex).idFor(presetBlockStatesFull[i]);
-+                    }
-+                }
-+
-+                bitStorageWriter.setIndex(chunkPacketInfoAntiXray.getIndex(chunkSectionIndex));
-+
-+                // Check if the chunk section below was not obfuscated
-+                if (chunkSectionIndex == 0 || !chunkPacketInfoAntiXray.isWritten(chunkSectionIndex - 1) || chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex - 1) == null) {
-+                    // If so, initialize some stuff
-+                    bitStorageReader.setBits(chunkPacketInfoAntiXray.getBits(chunkSectionIndex));
-+                    bitStorageReader.setIndex(chunkPacketInfoAntiXray.getIndex(chunkSectionIndex));
-+                    solidTemp = readPalette(chunkPacketInfoAntiXray.getPalette(chunkSectionIndex), solid, solidGlobal);
-+                    obfuscateTemp = readPalette(chunkPacketInfoAntiXray.getPalette(chunkSectionIndex), obfuscate, obfuscateGlobal);
-+                    // Read the blocks of the upper layer of the chunk section below if it exists
-+                    LevelChunkSection belowChunkSection = null;
-+                    boolean skipFirstLayer = chunkSectionIndex == 0 || (belowChunkSection = chunk.getSections()[chunkSectionIndex - 1]) == EMPTY_SECTION;
-+
-+                    for (int z = 0; z < 16; z++) {
-+                        for (int x = 0; x < 16; x++) {
-+                            current[z][x] = true;
-+                            next[z][x] = skipFirstLayer || isTransparent(belowChunkSection, x, 15, z);
-+                        }
-+                    }
-+
-+                    // Abuse the obfuscateLayer method to read the blocks of the first layer of the current chunk section
-+                    bitStorageWriter.setBits(0);
-+                    obfuscateLayer(-1, bitStorageReader, bitStorageWriter, solidTemp, obfuscateTemp, presetBlockStateBitsTemp, current, next, nextNext, emptyNearbyChunkSections, random);
-+                }
-+
-+                bitStorageWriter.setBits(chunkPacketInfoAntiXray.getBits(chunkSectionIndex));
-+                nearbyChunkSections[0] = chunkPacketInfoAntiXray.getNearbyChunks()[0] == null ? EMPTY_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[0].getSections()[chunkSectionIndex];
-+                nearbyChunkSections[1] = chunkPacketInfoAntiXray.getNearbyChunks()[1] == null ? EMPTY_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[1].getSections()[chunkSectionIndex];
-+                nearbyChunkSections[2] = chunkPacketInfoAntiXray.getNearbyChunks()[2] == null ? EMPTY_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[2].getSections()[chunkSectionIndex];
-+                nearbyChunkSections[3] = chunkPacketInfoAntiXray.getNearbyChunks()[3] == null ? EMPTY_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[3].getSections()[chunkSectionIndex];
-+
-+                // Obfuscate all layers of the current chunk section except the upper one
-+                for (int y = 0; y < 15; y++) {
-+                    boolean[][] temp = current;
-+                    current = next;
-+                    next = nextNext;
-+                    nextNext = temp;
-+                    random.nextLayer();
-+                    obfuscateLayer(y, bitStorageReader, bitStorageWriter, solidTemp, obfuscateTemp, presetBlockStateBitsTemp, current, next, nextNext, nearbyChunkSections, random);
-+                }
-+
-+                // Check if the chunk section above doesn't need obfuscation
-+                if (chunkSectionIndex == maxChunkSectionIndex || !chunkPacketInfoAntiXray.isWritten(chunkSectionIndex + 1) || chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex + 1) == null) {
-+                    // If so, obfuscate the upper layer of the current chunk section by reading blocks of the first layer from the chunk section above if it exists
-+                    LevelChunkSection aboveChunkSection;
-+
-+                    if (chunkSectionIndex != chunk.getSectionsCount() - 1 && (aboveChunkSection = chunk.getSections()[chunkSectionIndex + 1]) != EMPTY_SECTION) {
-+                        boolean[][] temp = current;
-+                        current = next;
-+                        next = nextNext;
-+                        nextNext = temp;
-+
-+                        for (int z = 0; z < 16; z++) {
-+                            for (int x = 0; x < 16; x++) {
-+                                if (isTransparent(aboveChunkSection, x, 0, z)) {
-+                                    current[z][x] = true;
-+                                }
-+                            }
-+                        }
-+
-+                        // There is nothing to read anymore
-+                        bitStorageReader.setBits(0);
-+                        solid[0] = true;
-+                        random.nextLayer();
-+                        obfuscateLayer(15, bitStorageReader, bitStorageWriter, solid, obfuscateTemp, presetBlockStateBitsTemp, current, next, nextNext, nearbyChunkSections, random);
-+                    }
-+                } else {
-+                    // If not, initialize the reader and other stuff for the chunk section above to obfuscate the upper layer of the current chunk section
-+                    bitStorageReader.setBits(chunkPacketInfoAntiXray.getBits(chunkSectionIndex + 1));
-+                    bitStorageReader.setIndex(chunkPacketInfoAntiXray.getIndex(chunkSectionIndex + 1));
-+                    solidTemp = readPalette(chunkPacketInfoAntiXray.getPalette(chunkSectionIndex + 1), solid, solidGlobal);
-+                    obfuscateTemp = readPalette(chunkPacketInfoAntiXray.getPalette(chunkSectionIndex + 1), obfuscate, obfuscateGlobal);
-+                    boolean[][] temp = current;
-+                    current = next;
-+                    next = nextNext;
-+                    nextNext = temp;
-+                    random.nextLayer();
-+                    obfuscateLayer(15, bitStorageReader, bitStorageWriter, solidTemp, obfuscateTemp, presetBlockStateBitsTemp, current, next, nextNext, nearbyChunkSections, random);
-+                }
-+
-+                bitStorageWriter.flush();
-+            }
-+        }
-+
-+        chunkPacketInfoAntiXray.getChunkPacket().setReady(true);
-+    }
-+
-+    private void obfuscateLayer(int y, BitStorageReader bitStorageReader, BitStorageWriter bitStorageWriter, boolean[] solid, boolean[] obfuscate, int[] presetBlockStateBits, boolean[][] current, boolean[][] next, boolean[][] nextNext, LevelChunkSection[] nearbyChunkSections, IntSupplier random) {
-+        // First block of first line
-+        int bits = bitStorageReader.read();
-+
-+        if (nextNext[0][0] = !solid[bits]) {
-+            bitStorageWriter.skip();
-+            next[0][1] = true;
-+            next[1][0] = true;
-+        } else {
-+            if (current[0][0] || isTransparent(nearbyChunkSections[2], 0, y, 15) || isTransparent(nearbyChunkSections[0], 15, y, 0)) {
-+                bitStorageWriter.skip();
-+            } else {
-+                bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
-+            }
-+        }
-+
-+        if (!obfuscate[bits]) {
-+            next[0][0] = true;
-+        }
-+
-+        // First line
-+        for (int x = 1; x < 15; x++) {
-+            bits = bitStorageReader.read();
-+
-+            if (nextNext[0][x] = !solid[bits]) {
-+                bitStorageWriter.skip();
-+                next[0][x - 1] = true;
-+                next[0][x + 1] = true;
-+                next[1][x] = true;
-+            } else {
-+                if (current[0][x] || isTransparent(nearbyChunkSections[2], x, y, 15)) {
-+                    bitStorageWriter.skip();
-+                } else {
-+                    bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
-+                }
-+            }
-+
-+            if (!obfuscate[bits]) {
-+                next[0][x] = true;
-+            }
-+        }
-+
-+        // Last block of first line
-+        bits = bitStorageReader.read();
-+
-+        if (nextNext[0][15] = !solid[bits]) {
-+            bitStorageWriter.skip();
-+            next[0][14] = true;
-+            next[1][15] = true;
-+        } else {
-+            if (current[0][15] || isTransparent(nearbyChunkSections[2], 15, y, 15) || isTransparent(nearbyChunkSections[1], 0, y, 0)) {
-+                bitStorageWriter.skip();
-+            } else {
-+                bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
-+            }
-+        }
-+
-+        if (!obfuscate[bits]) {
-+            next[0][15] = true;
-+        }
-+
-+        // All inner lines
-+        for (int z = 1; z < 15; z++) {
-+            // First block
-+            bits = bitStorageReader.read();
-+
-+            if (nextNext[z][0] = !solid[bits]) {
-+                bitStorageWriter.skip();
-+                next[z][1] = true;
-+                next[z - 1][0] = true;
-+                next[z + 1][0] = true;
-+            } else {
-+                if (current[z][0] || isTransparent(nearbyChunkSections[0], 15, y, z)) {
-+                    bitStorageWriter.skip();
-+                } else {
-+                    bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
-+                }
-+            }
-+
-+            if (!obfuscate[bits]) {
-+                next[z][0] = true;
-+            }
-+
-+            // All inner blocks
-+            for (int x = 1; x < 15; x++) {
-+                bits = bitStorageReader.read();
-+
-+                if (nextNext[z][x] = !solid[bits]) {
-+                    bitStorageWriter.skip();
-+                    next[z][x - 1] = true;
-+                    next[z][x + 1] = true;
-+                    next[z - 1][x] = true;
-+                    next[z + 1][x] = true;
-+                } else {
-+                    if (current[z][x]) {
-+                        bitStorageWriter.skip();
-+                    } else {
-+                        bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
-+                    }
-+                }
-+
-+                if (!obfuscate[bits]) {
-+                    next[z][x] = true;
-+                }
-+            }
-+
-+            // Last block
-+            bits = bitStorageReader.read();
-+
-+            if (nextNext[z][15] = !solid[bits]) {
-+                bitStorageWriter.skip();
-+                next[z][14] = true;
-+                next[z - 1][15] = true;
-+                next[z + 1][15] = true;
-+            } else {
-+                if (current[z][15] || isTransparent(nearbyChunkSections[1], 0, y, z)) {
-+                    bitStorageWriter.skip();
-+                } else {
-+                    bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
-+                }
-+            }
-+
-+            if (!obfuscate[bits]) {
-+                next[z][15] = true;
-+            }
-+        }
-+
-+        // First block of last line
-+        bits = bitStorageReader.read();
-+
-+        if (nextNext[15][0] = !solid[bits]) {
-+            bitStorageWriter.skip();
-+            next[15][1] = true;
-+            next[14][0] = true;
-+        } else {
-+            if (current[15][0] || isTransparent(nearbyChunkSections[3], 0, y, 0) || isTransparent(nearbyChunkSections[0], 15, y, 15)) {
-+                bitStorageWriter.skip();
-+            } else {
-+                bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
-+            }
-+        }
-+
-+        if (!obfuscate[bits]) {
-+            next[15][0] = true;
-+        }
-+
-+        // Last line
-+        for (int x = 1; x < 15; x++) {
-+            bits = bitStorageReader.read();
-+
-+            if (nextNext[15][x] = !solid[bits]) {
-+                bitStorageWriter.skip();
-+                next[15][x - 1] = true;
-+                next[15][x + 1] = true;
-+                next[14][x] = true;
-+            } else {
-+                if (current[15][x] || isTransparent(nearbyChunkSections[3], x, y, 0)) {
-+                    bitStorageWriter.skip();
-+                } else {
-+                    bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
-+                }
-+            }
-+
-+            if (!obfuscate[bits]) {
-+                next[15][x] = true;
-+            }
-+        }
-+
-+        // Last block of last line
-+        bits = bitStorageReader.read();
-+
-+        if (nextNext[15][15] = !solid[bits]) {
-+            bitStorageWriter.skip();
-+            next[15][14] = true;
-+            next[14][15] = true;
-+        } else {
-+            if (current[15][15] || isTransparent(nearbyChunkSections[3], 15, y, 0) || isTransparent(nearbyChunkSections[1], 0, y, 15)) {
-+                bitStorageWriter.skip();
-+            } else {
-+                bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
-+            }
-+        }
-+
-+        if (!obfuscate[bits]) {
-+            next[15][15] = true;
-+        }
-+    }
-+
-+    private boolean isTransparent(LevelChunkSection chunkSection, int x, int y, int z) {
-+        if (chunkSection == EMPTY_SECTION) {
-+            return true;
-+        }
-+
-+        try {
-+            return !solidGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(chunkSection.getBlockState(x, y, z))];
-+        } catch (MissingPaletteEntryException e) {
-+            // Race condition / visibility issue / no happens-before relationship
-+            // We don't care and treat the block as transparent
-+            // Internal implementation details of PalettedContainer, LinearPalette, HashMapPalette, CrudeIncrementalIntIdentityHashBiMap, ... guarantee us that no (other) exceptions will occur
-+            return true;
-+        }
-+    }
-+
-+    private boolean[] readPalette(Palette<BlockState> palette, boolean[] temp, boolean[] global) {
-+        if (palette instanceof GlobalPalette) {
-+            return global;
-+        }
-+
-+        try {
-+            for (int i = 0; i < palette.getSize(); i++) {
-+                temp[i] = global[GLOBAL_BLOCKSTATE_PALETTE.idFor(palette.valueFor(i))];
-+            }
-+        } catch (MissingPaletteEntryException e) {
-+            // Race condition / visibility issue / no happens-before relationship
-+            // We don't care because we at least see the state as it was when the chunk packet was created
-+            // Internal implementation details of PalettedContainer, LinearPalette, HashMapPalette, CrudeIncrementalIntIdentityHashBiMap, ... guarantee us that no (other) exceptions will occur until we have all the data that we need here
-+            // Since all palettes have a fixed initial maximum size and there is no internal restructuring and no values are removed from palettes, we are also guaranteed to see the data
-+        }
-+
-+        return temp;
-+    }
-+
-+    @Override
-+    public void onBlockChange(Level level, BlockPos blockPos, BlockState newBlockState, BlockState oldBlockState, int flags, int maxUpdateDepth) {
-+        if (oldBlockState != null && solidGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(oldBlockState)] && !solidGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(newBlockState)] && blockPos.getY() <= maxBlockHeightUpdatePosition) {
-+            updateNearbyBlocks(level, blockPos);
-+        }
-+    }
-+
-+    @Override
-+    public void onPlayerLeftClickBlock(ServerPlayerGameMode serverPlayerGameMode, BlockPos blockPos, ServerboundPlayerActionPacket.Action action, Direction direction, int worldHeight, int sequence) {
-+        if (blockPos.getY() <= maxBlockHeightUpdatePosition) {
-+            updateNearbyBlocks(serverPlayerGameMode.level, blockPos);
-+        }
-+    }
-+
-+    private void updateNearbyBlocks(Level level, BlockPos blockPos) {
-+        if (updateRadius >= 2) {
-+            BlockPos temp = blockPos.west();
-+            updateBlock(level, temp);
-+            updateBlock(level, temp.west());
-+            updateBlock(level, temp.below());
-+            updateBlock(level, temp.above());
-+            updateBlock(level, temp.north());
-+            updateBlock(level, temp.south());
-+            updateBlock(level, temp = blockPos.east());
-+            updateBlock(level, temp.east());
-+            updateBlock(level, temp.below());
-+            updateBlock(level, temp.above());
-+            updateBlock(level, temp.north());
-+            updateBlock(level, temp.south());
-+            updateBlock(level, temp = blockPos.below());
-+            updateBlock(level, temp.below());
-+            updateBlock(level, temp.north());
-+            updateBlock(level, temp.south());
-+            updateBlock(level, temp = blockPos.above());
-+            updateBlock(level, temp.above());
-+            updateBlock(level, temp.north());
-+            updateBlock(level, temp.south());
-+            updateBlock(level, temp = blockPos.north());
-+            updateBlock(level, temp.north());
-+            updateBlock(level, temp = blockPos.south());
-+            updateBlock(level, temp.south());
-+        } else if (updateRadius == 1) {
-+            updateBlock(level, blockPos.west());
-+            updateBlock(level, blockPos.east());
-+            updateBlock(level, blockPos.below());
-+            updateBlock(level, blockPos.above());
-+            updateBlock(level, blockPos.north());
-+            updateBlock(level, blockPos.south());
-+        } else {
-+            // Do nothing if updateRadius <= 0 (test mode)
-+        }
-+    }
-+
-+    private void updateBlock(Level level, BlockPos blockPos) {
-+        BlockState blockState = level.getBlockStateIfLoaded(blockPos);
-+
-+        if (blockState != null && obfuscateGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(blockState)]) {
-+            ((ServerLevel) level).getChunkSource().blockChanged(blockPos);
-+        }
-+    }
-+
-+    @FunctionalInterface
-+    private interface LayeredIntSupplier extends IntSupplier {
-+        default void nextLayer() {
-+
-+        }
-+    }
-+}
-diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfo.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfo.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfo.java
-@@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.antixray;
-+
-+import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket;
-+import net.minecraft.world.level.chunk.LevelChunk;
-+import net.minecraft.world.level.chunk.Palette;
-+
-+public class ChunkPacketInfo<T> {
-+
-+    private final ClientboundLevelChunkWithLightPacket chunkPacket;
-+    private final LevelChunk chunk;
-+    private final int[] bits;
-+    private final Object[] palettes;
-+    private final int[] indexes;
-+    private final Object[][] presetValues;
-+    private byte[] buffer;
-+
-+    public ChunkPacketInfo(ClientboundLevelChunkWithLightPacket chunkPacket, LevelChunk chunk) {
-+        this.chunkPacket = chunkPacket;
-+        this.chunk = chunk;
-+        int sections = chunk.getSectionsCount();
-+        bits = new int[sections];
-+        palettes = new Object[sections];
-+        indexes = new int[sections];
-+        presetValues = new Object[sections][];
-+    }
-+
-+    public ClientboundLevelChunkWithLightPacket getChunkPacket() {
-+        return chunkPacket;
-+    }
-+
-+    public LevelChunk getChunk() {
-+        return chunk;
-+    }
-+
-+    public byte[] getBuffer() {
-+        return buffer;
-+    }
-+
-+    public void setBuffer(byte[] buffer) {
-+        this.buffer = buffer;
-+    }
-+
-+    public int getBits(int chunkSectionIndex) {
-+        return bits[chunkSectionIndex];
-+    }
-+
-+    public void setBits(int chunkSectionIndex, int bits) {
-+        this.bits[chunkSectionIndex] = bits;
-+    }
-+
-+    @SuppressWarnings("unchecked")
-+    public Palette<T> getPalette(int chunkSectionIndex) {
-+        return (Palette<T>) palettes[chunkSectionIndex];
-+    }
-+
-+    public void setPalette(int chunkSectionIndex, Palette<T> palette) {
-+        palettes[chunkSectionIndex] = palette;
-+    }
-+
-+    public int getIndex(int chunkSectionIndex) {
-+        return indexes[chunkSectionIndex];
-+    }
-+
-+    public void setIndex(int chunkSectionIndex, int index) {
-+        indexes[chunkSectionIndex] = index;
-+    }
-+
-+    @SuppressWarnings("unchecked")
-+    public T[] getPresetValues(int chunkSectionIndex) {
-+        return (T[]) presetValues[chunkSectionIndex];
-+    }
-+
-+    public void setPresetValues(int chunkSectionIndex, T[] presetValues) {
-+        this.presetValues[chunkSectionIndex] = presetValues;
-+    }
-+
-+    public boolean isWritten(int chunkSectionIndex) {
-+        return bits[chunkSectionIndex] != 0;
-+    }
-+}
-diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfoAntiXray.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfoAntiXray.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfoAntiXray.java
-@@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.antixray;
-+
-+import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket;
-+import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.world.level.chunk.LevelChunk;
-+
-+public final class ChunkPacketInfoAntiXray extends ChunkPacketInfo<BlockState> implements Runnable {
-+
-+    private final ChunkPacketBlockControllerAntiXray chunkPacketBlockControllerAntiXray;
-+    private LevelChunk[] nearbyChunks;
-+
-+    public ChunkPacketInfoAntiXray(ClientboundLevelChunkWithLightPacket chunkPacket, LevelChunk chunk, ChunkPacketBlockControllerAntiXray chunkPacketBlockControllerAntiXray) {
-+        super(chunkPacket, chunk);
-+        this.chunkPacketBlockControllerAntiXray = chunkPacketBlockControllerAntiXray;
-+    }
-+
-+    public LevelChunk[] getNearbyChunks() {
-+        return nearbyChunks;
-+    }
-+
-+    public void setNearbyChunks(LevelChunk... nearbyChunks) {
-+        this.nearbyChunks = nearbyChunks;
-+    }
-+
-+    @Override
-+    public void run() {
-+        chunkPacketBlockControllerAntiXray.obfuscate(this);
-+    }
-+}
-diff --git a/src/main/java/io/papermc/paper/FeatureHooks.java b/src/main/java/io/papermc/paper/FeatureHooks.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/io/papermc/paper/FeatureHooks.java
-+++ b/src/main/java/io/papermc/paper/FeatureHooks.java
-@@ -0,0 +0,0 @@ import it.unimi.dsi.fastutil.longs.LongSets;
- import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
- import it.unimi.dsi.fastutil.objects.ObjectSet;
- import it.unimi.dsi.fastutil.objects.ObjectSets;
-+import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
- import java.util.Set;
-@@ -0,0 +0,0 @@ public final class FeatureHooks {
-     }
- 
-     public static LevelChunkSection createSection(final Registry<Biome> biomeRegistry, final Level level, final ChunkPos chunkPos, final int chunkSection) {
--        return new LevelChunkSection(biomeRegistry);
-+        return new LevelChunkSection(biomeRegistry, level, chunkPos, chunkSection); // Paper - Anti-Xray - Add parameters
-     }
- 
-     public static void sendChunkRefreshPackets(final List<ServerPlayer> playersInRange, final LevelChunk chunk) {
--        final ClientboundLevelChunkWithLightPacket refreshPacket = new ClientboundLevelChunkWithLightPacket(chunk, chunk.level.getLightEngine(), null, null);
-+        // Paper start - Anti-Xray
-+        final Map<Object, ClientboundLevelChunkWithLightPacket> refreshPackets = new HashMap<>();
-         for (final ServerPlayer player : playersInRange) {
-             if (player.connection == null) continue;
- 
--            player.connection.send(refreshPacket);
-+            final Boolean shouldModify = chunk.getLevel().chunkPacketBlockController.shouldModify(player, chunk);
-+            player.connection.send(refreshPackets.computeIfAbsent(shouldModify, s -> { // Use connection to prevent creating firing event
-+                return new ClientboundLevelChunkWithLightPacket(chunk, chunk.level.getLightEngine(), null, null, (Boolean) s);
-+            }));
-         }
-+        // Paper end - Anti-Xray
-     }
- 
-     public static PalettedContainer<BlockState> emptyPalettedBlockContainer() {
--        return new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES);
-+        return new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES, null); // Paper - Anti-Xray - Add preset block states
-     }
- 
-     public static Set<Long> getSentChunkKeys(final ServerPlayer player) {
-diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundChunksBiomesPacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundChunksBiomesPacket.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/network/protocol/game/ClientboundChunksBiomesPacket.java
-+++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundChunksBiomesPacket.java
-@@ -0,0 +0,0 @@ public record ClientboundChunksBiomesPacket(List<ClientboundChunksBiomesPacket.C
-         }
- 
-         public static void extractChunkData(FriendlyByteBuf buf, LevelChunk chunk) {
-+            int chunkSectionIndex = 0; // Paper - Anti-Xray
-             for (LevelChunkSection levelChunkSection : chunk.getSections()) {
--                levelChunkSection.getBiomes().write(buf);
-+                levelChunkSection.getBiomes().write(buf, null, chunkSectionIndex); // Paper - Anti-Xray
-+                chunkSectionIndex++; // Paper - Anti-Xray
-             }
-         }
- 
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java
-+++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java
-@@ -0,0 +0,0 @@ public class ClientboundLevelChunkPacketData {
-     private final byte[] buffer;
-     private final List<ClientboundLevelChunkPacketData.BlockEntityInfo> blockEntitiesData;
- 
--    public ClientboundLevelChunkPacketData(LevelChunk chunk) {
-+    // Paper start - Anti-Xray - Add chunk packet info
-+    @Deprecated @io.papermc.paper.annotation.DoNotUse public ClientboundLevelChunkPacketData(LevelChunk chunk) { this(chunk, null); }
-+    public ClientboundLevelChunkPacketData(LevelChunk chunk, com.destroystokyo.paper.antixray.ChunkPacketInfo<net.minecraft.world.level.block.state.BlockState> chunkPacketInfo) {
-+        // Paper end
-         this.heightmaps = new CompoundTag();
- 
-         for (Entry<Heightmap.Types, Heightmap> entry : chunk.getHeightmaps()) {
-@@ -0,0 +0,0 @@ public class ClientboundLevelChunkPacketData {
-         }
- 
-         this.buffer = new byte[calculateChunkSize(chunk)];
--        extractChunkData(new FriendlyByteBuf(this.getWriteBuffer()), chunk);
-+
-+        // Paper start - Anti-Xray - Add chunk packet info
-+        if (chunkPacketInfo != null) {
-+            chunkPacketInfo.setBuffer(this.buffer);
-+        }
-+
-+        extractChunkData(new FriendlyByteBuf(this.getWriteBuffer()), chunk, chunkPacketInfo);
-+        // Paper end
-         this.blockEntitiesData = Lists.newArrayList();
- 
-         for (Entry<BlockPos, BlockEntity> entry2 : chunk.getBlockEntities().entrySet()) {
-@@ -0,0 +0,0 @@ public class ClientboundLevelChunkPacketData {
-         return byteBuf;
-     }
- 
--    public static void extractChunkData(FriendlyByteBuf buf, LevelChunk chunk) {
-+    // Paper start - Anti-Xray - Add chunk packet info
-+    @Deprecated @io.papermc.paper.annotation.DoNotUse public static void extractChunkData(FriendlyByteBuf buf, LevelChunk chunk) { ClientboundLevelChunkPacketData.extractChunkData(buf, chunk, null); }
-+    public static void extractChunkData(FriendlyByteBuf buf, LevelChunk chunk, com.destroystokyo.paper.antixray.ChunkPacketInfo<net.minecraft.world.level.block.state.BlockState> chunkPacketInfo) {
-+        int chunkSectionIndex = 0;
-+
-         for (LevelChunkSection levelChunkSection : chunk.getSections()) {
--            levelChunkSection.write(buf);
-+            levelChunkSection.write(buf, chunkPacketInfo, chunkSectionIndex);
-+            chunkSectionIndex++;
-+            // Paper end
-         }
-     }
- 
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java
-+++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java
-@@ -0,0 +0,0 @@ public class ClientboundLevelChunkWithLightPacket implements Packet<ClientGamePa
-     private final int z;
-     private final ClientboundLevelChunkPacketData chunkData;
-     private final ClientboundLightUpdatePacketData lightData;
-+    // Paper start - Async-Anti-Xray - Ready flag for the connection
-+    private volatile boolean ready;
- 
--    public ClientboundLevelChunkWithLightPacket(LevelChunk chunk, LevelLightEngine lightProvider, @Nullable BitSet skyBits, @Nullable BitSet blockBits) {
-+    @Override
-+    public boolean isReady() {
-+        return this.ready;
-+    }
-+
-+    public void setReady(boolean ready) {
-+        this.ready = ready;
-+    }
-+    // Paper end
-+
-+    // Paper start - Anti-Xray - Add chunk packet info
-+    @Deprecated @io.papermc.paper.annotation.DoNotUse public ClientboundLevelChunkWithLightPacket(LevelChunk chunk, LevelLightEngine lightProvider, @Nullable BitSet skyBits, @Nullable BitSet blockBits) { this(chunk, lightProvider, skyBits, blockBits, true); }
-+    public ClientboundLevelChunkWithLightPacket(LevelChunk chunk, LevelLightEngine lightProvider, @Nullable BitSet skyBits, @Nullable BitSet blockBits, boolean modifyBlocks) {
-         ChunkPos chunkPos = chunk.getPos();
-         this.x = chunkPos.x;
-         this.z = chunkPos.z;
--        this.chunkData = new ClientboundLevelChunkPacketData(chunk);
-+        com.destroystokyo.paper.antixray.ChunkPacketInfo<net.minecraft.world.level.block.state.BlockState> chunkPacketInfo = modifyBlocks ? chunk.getLevel().chunkPacketBlockController.getChunkPacketInfo(this, chunk) : null;
-+        this.chunkData = new ClientboundLevelChunkPacketData(chunk, chunkPacketInfo);
-+        // Paper end
-         this.lightData = new ClientboundLightUpdatePacketData(chunkPos, lightProvider, skyBits, blockBits);
-+        chunk.getLevel().chunkPacketBlockController.modifyBlocks(this, chunkPacketInfo); // Paper - Anti-Xray - Modify blocks
-     }
- 
-     private ClientboundLevelChunkWithLightPacket(RegistryFriendlyByteBuf buf) {
-diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerLevel.java
-+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
- 
-     // Add env and gen to constructor, IWorldDataServer -> WorldDataServer
-     public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey<Level> resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List<CustomSpawner> list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) {
--        super(iworlddataserver, resourcekey, minecraftserver.registryAccess(), worlddimension.type(), false, flag, i, minecraftserver.getMaxChainedNeighborUpdates(), gen, biomeProvider, env, spigotConfig -> minecraftserver.paperConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(convertable_conversionsession.levelDirectory.path(), iworlddataserver.getLevelName(), resourcekey.location(), spigotConfig, minecraftserver.registryAccess(), iworlddataserver.getGameRules()))); // Paper - create paper world configs
-+        super(iworlddataserver, resourcekey, minecraftserver.registryAccess(), worlddimension.type(), false, flag, i, minecraftserver.getMaxChainedNeighborUpdates(), gen, biomeProvider, env, spigotConfig -> minecraftserver.paperConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(convertable_conversionsession.levelDirectory.path(), iworlddataserver.getLevelName(), resourcekey.location(), spigotConfig, minecraftserver.registryAccess(), iworlddataserver.getGameRules())), executor); // Paper - create paper world configs; Async-Anti-Xray: Pass executor
-         this.pvpMode = minecraftserver.isPvpAllowed();
-         this.convertable = convertable_conversionsession;
-         this.uuid = WorldUUID.getUUID(convertable_conversionsession.levelDirectory.path().toFile());
-diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java
-+++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java
-@@ -0,0 +0,0 @@ import org.bukkit.event.player.PlayerInteractEvent;
- public class ServerPlayerGameMode {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
--    protected ServerLevel level;
-+    public ServerLevel level; // Paper - Anti-Xray - protected -> public
-     protected final ServerPlayer player;
-     private GameType gameModeForPlayer;
-     @Nullable
-@@ -0,0 +0,0 @@ public class ServerPlayerGameMode {
-             }
- 
-         }
-+
-+        this.level.chunkPacketBlockController.onPlayerLeftClickBlock(this, pos, action, direction, worldHeight, sequence); // Paper - Anti-Xray
-     }
- 
-     public void destroyAndAck(BlockPos pos, int sequence, String reason) {
-diff --git a/src/main/java/net/minecraft/server/network/PlayerChunkSender.java b/src/main/java/net/minecraft/server/network/PlayerChunkSender.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/network/PlayerChunkSender.java
-+++ b/src/main/java/net/minecraft/server/network/PlayerChunkSender.java
-@@ -0,0 +0,0 @@ public class PlayerChunkSender {
-         }
-     }
- 
--    private static void sendChunk(ServerGamePacketListenerImpl handler, ServerLevel world, LevelChunk chunk) {
--        handler.send(new ClientboundLevelChunkWithLightPacket(chunk, world.getLightEngine(), null, null));
-+    public static void sendChunk(ServerGamePacketListenerImpl handler, ServerLevel world, LevelChunk chunk) { // Paper - public
-+        // Paper start - Anti-Xray
-+        final boolean shouldModify = world.chunkPacketBlockController.shouldModify(handler.player, chunk);
-+        handler.send(new ClientboundLevelChunkWithLightPacket(chunk, world.getLightEngine(), null, null, shouldModify));
-+        // Paper end - Anti-Xray
-         // Paper start - PlayerChunkLoadEvent
-         if (io.papermc.paper.event.packet.PlayerChunkLoadEvent.getHandlerList().getRegisteredListeners().length > 0) {
-             new io.papermc.paper.event.packet.PlayerChunkLoadEvent(new org.bukkit.craftbukkit.CraftChunk(chunk), handler.getPlayer().getBukkitEntity()).callEvent();
-diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/players/PlayerList.java
-+++ b/src/main/java/net/minecraft/server/players/PlayerList.java
-@@ -0,0 +0,0 @@ public abstract class PlayerList {
-                     .getOrThrow(net.minecraft.world.level.biome.Biomes.PLAINS);
-             player.connection.send(new net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket(
-                     new net.minecraft.world.level.chunk.EmptyLevelChunk(worldserver1, player.chunkPosition(), plains),
--                    worldserver1.getLightEngine(), (java.util.BitSet)null, (java.util.BitSet) null)
-+                    worldserver1.getLightEngine(), (java.util.BitSet)null, (java.util.BitSet) null, true) // Paper - Anti-Xray
-             );
-         }
-         // 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/Level.java
-+++ b/src/main/java/net/minecraft/world/level/Level.java
-@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
-     }
-     // Paper end - add paper world config
- 
-+    public final com.destroystokyo.paper.antixray.ChunkPacketBlockController chunkPacketBlockController; // Paper - Anti-Xray
-     public static BlockPos lastPhysicsProblem; // Spigot
-     private org.spigotmc.TickLimiter entityLimiter;
-     private org.spigotmc.TickLimiter tileLimiter;
-@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
- 
-     public abstract ResourceKey<LevelStem> getTypeKey();
- 
--    protected Level(WritableLevelData worlddatamutable, ResourceKey<Level> resourcekey, RegistryAccess iregistrycustom, Holder<DimensionType> holder, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function<org.spigotmc.SpigotWorldConfig, io.papermc.paper.configuration.WorldConfiguration> paperWorldConfigCreator) { // Paper - create paper world config
-+    protected Level(WritableLevelData worlddatamutable, ResourceKey<Level> resourcekey, RegistryAccess iregistrycustom, Holder<DimensionType> holder, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function<org.spigotmc.SpigotWorldConfig, io.papermc.paper.configuration.WorldConfiguration> paperWorldConfigCreator, java.util.concurrent.Executor executor) { // Paper - create paper world config & Anti-Xray
-         this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot
-         this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper - create paper world config
-         this.generator = gen;
-@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
-         // CraftBukkit end
-         this.entityLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.entityMaxTickTime);
-         this.tileLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.tileMaxTickTime);
-+        this.chunkPacketBlockController = this.paperConfig().anticheat.antiXray.enabled ? new com.destroystokyo.paper.antixray.ChunkPacketBlockControllerAntiXray(this, executor) : com.destroystokyo.paper.antixray.ChunkPacketBlockController.NO_OPERATION_INSTANCE; // Paper - Anti-Xray
-     }
- 
-     // Paper start - Cancel hit for vanished players
-@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
-             // CraftBukkit end
- 
-             BlockState iblockdata1 = chunk.setBlockState(pos, state, (flags & 64) != 0, (flags & 1024) == 0); // CraftBukkit custom NO_PLACE flag
-+            this.chunkPacketBlockController.onBlockChange(this, pos, state, iblockdata1, flags, maxUpdateDepth); // Paper - Anti-Xray
- 
-             if (iblockdata1 == null) {
-                 // CraftBukkit start - remove blockstate if failed (or the same)
-diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
-@@ -0,0 +0,0 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh
-             }
-         }
- 
--        ChunkAccess.replaceMissingSections(biomeRegistry, this.sections);
-+        this.replaceMissingSections(biomeRegistry, this.sections); // Paper - Anti-Xray - make it a non-static method
-         // CraftBukkit start
-         this.biomeRegistry = biomeRegistry;
-     }
-     public final Registry<Biome> biomeRegistry;
-     // CraftBukkit end
- 
--    private static void replaceMissingSections(Registry<Biome> biomeRegistry, LevelChunkSection[] sectionArray) {
-+    private void replaceMissingSections(Registry<Biome> biomeRegistry, LevelChunkSection[] sectionArray) { // Paper - Anti-Xray - make it a non-static method
-         for (int i = 0; i < sectionArray.length; ++i) {
-             if (sectionArray[i] == null) {
--                sectionArray[i] = new LevelChunkSection(biomeRegistry);
-+                sectionArray[i] = new LevelChunkSection(biomeRegistry, this.levelHeightAccessor instanceof net.minecraft.world.level.Level ? (net.minecraft.world.level.Level) this.levelHeightAccessor : null, this.chunkPos, this.levelHeightAccessor.getSectionYFromSectionIndex(i)); // Paper - Anti-Xray - Add parameters
-             }
-         }
- 
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
-@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess {
-     }
- 
-     public LevelChunk(Level world, ChunkPos pos, UpgradeData upgradeData, LevelChunkTicks<Block> blockTickScheduler, LevelChunkTicks<Fluid> fluidTickScheduler, long inhabitedTime, @Nullable LevelChunkSection[] sectionArrayInitializer, @Nullable LevelChunk.PostLoadProcessor entityLoader, @Nullable BlendingData blendingData) {
--        super(pos, upgradeData, world, world.registryAccess().lookupOrThrow(Registries.BIOME), inhabitedTime, sectionArrayInitializer, blendingData);
-+        super(pos, upgradeData, world, net.minecraft.server.MinecraftServer.getServer().registryAccess().lookupOrThrow(Registries.BIOME), inhabitedTime, sectionArrayInitializer, blendingData); // Paper - Anti-Xray - The world isn't ready yet, use server singleton for registry
-         this.tickersInLevel = Maps.newHashMap();
-         this.unsavedListener = (chunkcoordintpair1) -> {
-         };
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
-@@ -0,0 +0,0 @@ public class LevelChunkSection {
-         this.recalcBlockCounts();
-     }
- 
--    public LevelChunkSection(Registry<Biome> biomeRegistry) {
--        this.states = new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES);
--        this.biomes = new PalettedContainer<>(biomeRegistry.asHolderIdMap(), biomeRegistry.getOrThrow(Biomes.PLAINS), PalettedContainer.Strategy.SECTION_BIOMES);
-+    // Paper start - Anti-Xray - Add parameters
-+    @Deprecated @io.papermc.paper.annotation.DoNotUse public LevelChunkSection(Registry<Biome> biomeRegistry) { this(biomeRegistry, null, null, 0); }
-+    public LevelChunkSection(Registry<Biome> biomeRegistry, net.minecraft.world.level.Level level, net.minecraft.world.level.ChunkPos chunkPos, int chunkSectionY) {
-+    // Paper end
-+        this.states = new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES, level == null || level.chunkPacketBlockController == null ? null : level.chunkPacketBlockController.getPresetBlockStates(level, chunkPos, chunkSectionY)); // Paper - Anti-Xray - Add preset block states
-+        this.biomes = new PalettedContainer<>(biomeRegistry.asHolderIdMap(), biomeRegistry.getOrThrow(Biomes.PLAINS), PalettedContainer.Strategy.SECTION_BIOMES, null); // Paper - Anti-Xray - Add preset biomes
-     }
- 
-     public BlockState getBlockState(int x, int y, int z) {
-@@ -0,0 +0,0 @@ public class LevelChunkSection {
-         this.biomes = datapaletteblock;
-     }
- 
--    public void write(FriendlyByteBuf buf) {
-+    // Paper start - Anti-Xray - Add chunk packet info
-+    @Deprecated @io.papermc.paper.annotation.DoNotUse public void write(FriendlyByteBuf buf) { this.write(buf, null, 0); }
-+    public void write(FriendlyByteBuf buf, com.destroystokyo.paper.antixray.ChunkPacketInfo<BlockState> chunkPacketInfo, int chunkSectionIndex) {
-         buf.writeShort(this.nonEmptyBlockCount);
--        this.states.write(buf);
--        this.biomes.write(buf);
-+        this.states.write(buf, chunkPacketInfo, chunkSectionIndex);
-+        this.biomes.write(buf, null, chunkSectionIndex);
-+        // Paper end
-     }
- 
-     public int getSerializedSize() {
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
-@@ -0,0 +0,0 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
-     private static final int MIN_PALETTE_BITS = 0;
-     private final PaletteResize<T> dummyPaletteResize = (newSize, added) -> 0;
-     public final IdMap<T> registry;
-+    private final T @org.jetbrains.annotations.Nullable [] presetValues; // Paper - Anti-Xray - Add preset values
-     private volatile PalettedContainer.Data<T> data;
-     private final PalettedContainer.Strategy strategy;
-     // private final ThreadingDetector threadingDetector = new ThreadingDetector("PalettedContainer"); // Paper - unused
-@@ -0,0 +0,0 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
-         // this.threadingDetector.checkAndUnlock(); // Paper - disable this
-     }
- 
--    public static <T> Codec<PalettedContainer<T>> codecRW(IdMap<T> idList, Codec<T> entryCodec, PalettedContainer.Strategy paletteProvider, T defaultValue) {
--        PalettedContainerRO.Unpacker<T, PalettedContainer<T>> unpacker = PalettedContainer::unpack;
-+    // Paper start - Anti-Xray - Add preset values
-+    @Deprecated @io.papermc.paper.annotation.DoNotUse public static <T> Codec<PalettedContainer<T>> codecRW(IdMap<T> idList, Codec<T> entryCodec, PalettedContainer.Strategy paletteProvider, T defaultValue) { return PalettedContainer.codecRW(idList, entryCodec, paletteProvider, defaultValue, null); }
-+    public static <T> Codec<PalettedContainer<T>> codecRW(IdMap<T> idList, Codec<T> entryCodec, PalettedContainer.Strategy paletteProvider, T defaultValue, T @org.jetbrains.annotations.Nullable [] presetValues) {
-+        PalettedContainerRO.Unpacker<T, PalettedContainer<T>> unpacker = (idListx, paletteProviderx, serialized) -> {
-+            return unpack(idListx, paletteProviderx, serialized, defaultValue, presetValues);
-+        };
-+        // Paper end
-         return codec(idList, entryCodec, paletteProvider, defaultValue, unpacker);
-     }
- 
-     public static <T> Codec<PalettedContainerRO<T>> codecRO(IdMap<T> idList, Codec<T> entryCodec, PalettedContainer.Strategy paletteProvider, T defaultValue) {
-         PalettedContainerRO.Unpacker<T, PalettedContainerRO<T>> unpacker = (idListx, paletteProviderx, serialized) -> unpack(
--                    idListx, paletteProviderx, serialized
-+                    idListx, paletteProviderx, serialized, defaultValue, null // Paper - Anti-Xray - Add preset values
-                 )
-                 .map(result -> (PalettedContainerRO<T>)result);
-         return codec(idList, entryCodec, paletteProvider, defaultValue, unpacker);
-@@ -0,0 +0,0 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
-             );
-     }
- 
-+    // Paper start - Anti-Xray - Add preset values
-+    @Deprecated @io.papermc.paper.annotation.DoNotUse public PalettedContainer(IdMap<T> idList, PalettedContainer.Strategy paletteProvider, PalettedContainer.Configuration<T> dataProvider, BitStorage storage, List<T> paletteEntries) { this(idList, paletteProvider, dataProvider, storage, paletteEntries, null, null); }
-     public PalettedContainer(
-         IdMap<T> idList,
-         PalettedContainer.Strategy paletteProvider,
-         PalettedContainer.Configuration<T> dataProvider,
-         BitStorage storage,
--        List<T> paletteEntries
-+        List<T> paletteEntries, T defaultValue, T @org.jetbrains.annotations.Nullable [] presetValues
-     ) {
-+        this.presetValues = presetValues;
-         this.registry = idList;
-         this.strategy = paletteProvider;
-         this.data = new PalettedContainer.Data<>(dataProvider, storage, dataProvider.factory().create(dataProvider.bits(), idList, this, paletteEntries));
-+
-+        if (presetValues != null && (dataProvider.factory() == PalettedContainer.Strategy.SINGLE_VALUE_PALETTE_FACTORY ? this.data.palette.valueFor(0) != defaultValue : dataProvider.factory() != PalettedContainer.Strategy.GLOBAL_PALETTE_FACTORY)) {
-+            // In 1.18 Mojang unfortunately removed code that already handled possible resize operations on read from disk for us
-+            // We readd this here but in a smarter way than it was before
-+            int maxSize = 1 << dataProvider.bits();
-+
-+            for (T presetValue : presetValues) {
-+                if (this.data.palette.getSize() >= maxSize) {
-+                    java.util.Set<T> allValues = new java.util.HashSet<>(paletteEntries);
-+                    allValues.addAll(Arrays.asList(presetValues));
-+                    int newBits = Mth.ceillog2(allValues.size());
-+
-+                    if (newBits > dataProvider.bits()) {
-+                        this.onResize(newBits, null);
-+                    }
-+
-+                    break;
-+                }
-+
-+                this.data.palette.idFor(presetValue);
-+            }
-+        }
-+        // Paper end
-     }
- 
--    private PalettedContainer(IdMap<T> idList, PalettedContainer.Strategy paletteProvider, PalettedContainer.Data<T> data) {
-+    // Paper start - Anti-Xray - Add preset values
-+    private PalettedContainer(IdMap<T> idList, PalettedContainer.Strategy paletteProvider, PalettedContainer.Data<T> data, T @org.jetbrains.annotations.Nullable [] presetValues) {
-+        this.presetValues = presetValues;
-+        // Paper end
-         this.registry = idList;
-         this.strategy = paletteProvider;
-         this.data = data;
-     }
- 
--    private PalettedContainer(PalettedContainer<T> container) {
-+    private PalettedContainer(PalettedContainer<T> container, T @org.jetbrains.annotations.Nullable [] presetValues) { // Paper - Anti-Xray - Add preset values
-+        this.presetValues = presetValues; // Paper - Anti-Xray - Add preset values
-         this.registry = container.registry;
-         this.strategy = container.strategy;
-         this.data = container.data.copy(this);
-     }
- 
--    public PalettedContainer(IdMap<T> idList, T object, PalettedContainer.Strategy paletteProvider) {
-+    // Paper start - Anti-Xray - Add preset values
-+    @Deprecated @io.papermc.paper.annotation.DoNotUse public PalettedContainer(IdMap<T> idList, T object, PalettedContainer.Strategy paletteProvider) { this(idList, object, paletteProvider, null); }
-+    public PalettedContainer(IdMap<T> idList, T object, PalettedContainer.Strategy paletteProvider, T @org.jetbrains.annotations.Nullable [] presetValues) {
-+        this.presetValues = presetValues;
-+        // Paper end
-         this.strategy = paletteProvider;
-         this.registry = idList;
-         this.data = this.createOrReuseData(null, 0);
-@@ -0,0 +0,0 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
-     @Override
-     public synchronized int onResize(int newBits, T object) { // Paper - synchronize
-         PalettedContainer.Data<T> data = this.data;
-+
-+        // Paper start - Anti-Xray - Add preset values
-+        if (this.presetValues != null && object != null && data.configuration().factory() == PalettedContainer.Strategy.SINGLE_VALUE_PALETTE_FACTORY) {
-+            int duplicates = 0;
-+            List<T> presetValues = Arrays.asList(this.presetValues);
-+            duplicates += presetValues.contains(object) ? 1 : 0;
-+            duplicates += presetValues.contains(data.palette.valueFor(0)) ? 1 : 0;
-+            newBits = Mth.ceillog2((1 << this.strategy.calculateBitsForSerialization(this.registry, 1 << newBits)) + presetValues.size() - duplicates);
-+        }
-+
-         PalettedContainer.Data<T> data2 = this.createOrReuseData(data, newBits);
-         data2.copyFrom(data.palette, data.storage);
-         this.data = data2;
--        return data2.palette.idFor(object);
-+        this.addPresetValues();
-+        return object == null ? -1 : data2.palette.idFor(object);
-+        // Paper end
-+    }
-+
-+    // Paper start - Anti-Xray - Add preset values
-+    private void addPresetValues() {
-+        if (this.presetValues != null && this.data.configuration().factory() != PalettedContainer.Strategy.GLOBAL_PALETTE_FACTORY) {
-+            for (T presetValue : this.presetValues) {
-+                this.data.palette.idFor(presetValue);
-+            }
-+        }
-     }
-+    // Paper end
- 
-     public T getAndSet(int x, int y, int z, T value) {
-         this.acquire();
-@@ -0,0 +0,0 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
-             data.palette.read(buf);
-             buf.readLongArray(data.storage.getRaw());
-             this.data = data;
-+            this.addPresetValues(); // Paper - Anti-Xray - Add preset values (inefficient, but this isn't used by the server)
-         } finally {
-             this.release();
-         }
-     }
- 
-+    // Paper start - Anti-Xray; Add chunk packet info
-+    @Override
-+    @Deprecated @io.papermc.paper.annotation.DoNotUse public void write(FriendlyByteBuf buf) { this.write(buf, null, 0); }
-     @Override
--    public synchronized void write(FriendlyByteBuf buf) { // Paper - synchronize
-+    public synchronized void write(FriendlyByteBuf buf, @Nullable com.destroystokyo.paper.antixray.ChunkPacketInfo<T> chunkPacketInfo, int chunkSectionIndex) { // Paper - Synchronize
-         this.acquire();
- 
-         try {
--            this.data.write(buf);
-+            this.data.write(buf, chunkPacketInfo, chunkSectionIndex);
-+
-+            if (chunkPacketInfo != null) {
-+                chunkPacketInfo.setPresetValues(chunkSectionIndex, this.presetValues);
-+            }
-+            // Paper end
-         } finally {
-             this.release();
-         }
-     }
- 
-     private static <T> DataResult<PalettedContainer<T>> unpack(
--        IdMap<T> idList, PalettedContainer.Strategy paletteProvider, PalettedContainerRO.PackedData<T> serialized
-+        IdMap<T> idList, PalettedContainer.Strategy paletteProvider, PalettedContainerRO.PackedData<T> serialized, T defaultValue, T @org.jetbrains.annotations.Nullable [] presetValues // Paper - Anti-Xray - Add preset values
-     ) {
-         List<T> list = serialized.paletteEntries();
-         int i = paletteProvider.size();
-@@ -0,0 +0,0 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
-             }
-         }
- 
--        return DataResult.success(new PalettedContainer<>(idList, paletteProvider, configuration, bitStorage, list));
-+        return DataResult.success(new PalettedContainer<>(idList, paletteProvider, configuration, bitStorage, list, defaultValue, presetValues)); // Paper - Anti-Xray - Add preset values
-     }
- 
-     @Override
-@@ -0,0 +0,0 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
- 
-     @Override
-     public PalettedContainer<T> copy() {
--        return new PalettedContainer<>(this);
-+        return new PalettedContainer<>(this, this.presetValues); // Paper - Anti-Xray - Add preset values
-     }
- 
-     @Override
-     public PalettedContainer<T> recreate() {
--        return new PalettedContainer<>(this.registry, this.data.palette.valueFor(0), this.strategy);
-+        return new PalettedContainer<>(this.registry, this.data.palette.valueFor(0), this.strategy, this.presetValues); // Paper - Anti-Xray - Add preset values
-     }
- 
-     @Override
-@@ -0,0 +0,0 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
-             return 1 + this.palette.getSerializedSize() + VarInt.getByteSize(this.storage.getRaw().length) + this.storage.getRaw().length * 8;
-         }
- 
--        public void write(FriendlyByteBuf buf) {
-+        // Paper start - Anti-Xray - Add chunk packet info
-+        public void write(FriendlyByteBuf buf, @Nullable com.destroystokyo.paper.antixray.ChunkPacketInfo<T> chunkPacketInfo, int chunkSectionIndex) {
-             buf.writeByte(this.storage.getBits());
-             this.palette.write(buf);
-+
-+            if (chunkPacketInfo != null) {
-+                chunkPacketInfo.setBits(chunkSectionIndex, this.configuration.bits());
-+                chunkPacketInfo.setPalette(chunkSectionIndex, this.palette);
-+                chunkPacketInfo.setIndex(chunkSectionIndex, buf.writerIndex() + VarInt.getByteSize(this.storage.getRaw().length));
-+            }
-+            // Paper end
-+
-             buf.writeLongArray(this.storage.getRaw());
-         }
- 
-diff --git a/src/main/java/net/minecraft/world/level/chunk/PalettedContainerRO.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainerRO.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainerRO.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainerRO.java
-@@ -0,0 +0,0 @@ public interface PalettedContainerRO<T> {
- 
-     void getAll(Consumer<T> action);
- 
--    void write(FriendlyByteBuf buf);
-+    // Paper start - Anti-Xray - Add chunk packet info
-+    @Deprecated @io.papermc.paper.annotation.DoNotUse void write(FriendlyByteBuf buf);
-+    void write(FriendlyByteBuf buf, @javax.annotation.Nullable com.destroystokyo.paper.antixray.ChunkPacketInfo<T> chunkPacketInfo, int chunkSectionIndex);
-+    // Paper end
- 
-     int getSerializedSize();
- 
-diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java b/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
-@@ -0,0 +0,0 @@ import org.slf4j.Logger;
- // CraftBukkit - persistentDataContainer
- public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chunkPos, int minSectionY, long lastUpdateTime, long inhabitedTime, ChunkStatus chunkStatus, @Nullable BlendingData.Packed blendingData, @Nullable BelowZeroRetrogen belowZeroRetrogen, UpgradeData upgradeData, @Nullable long[] carvingMask, Map<Heightmap.Types, long[]> heightmaps, ChunkAccess.PackedTicks packedTicks, ShortList[] postProcessingSections, boolean lightCorrect, List<SerializableChunkData.SectionData> sectionData, List<CompoundTag> entities, List<CompoundTag> blockEntities, CompoundTag structureData, @Nullable Tag persistentDataContainer) {
- 
--    public static final Codec<PalettedContainer<BlockState>> BLOCK_STATE_CODEC = PalettedContainer.codecRW(Block.BLOCK_STATE_REGISTRY, BlockState.CODEC, PalettedContainer.Strategy.SECTION_STATES, Blocks.AIR.defaultBlockState());
-+    public static final Codec<PalettedContainer<BlockState>> BLOCK_STATE_CODEC = PalettedContainer.codecRW(Block.BLOCK_STATE_REGISTRY, BlockState.CODEC, PalettedContainer.Strategy.SECTION_STATES, Blocks.AIR.defaultBlockState(), null); // Paper start - Anti-Xray
-     private static final Logger LOGGER = LogUtils.getLogger();
-     private static final String TAG_UPGRADE_DATA = "UpgradeData";
-     private static final String BLOCK_TICKS_TAG = "block_ticks";
-@@ -0,0 +0,0 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
- 
-     @Nullable
-     public static SerializableChunkData parse(LevelHeightAccessor world, RegistryAccess registryManager, CompoundTag nbt) {
-+        net.minecraft.server.level.ServerLevel serverLevel = (net.minecraft.server.level.ServerLevel) world; // Paper - Anti-Xray This is is seemingly only called from ChunkMap, where, we have a server level. We'll fight this later if needed.
-         if (!nbt.contains("Status", 8)) {
-             return null;
-         } else {
-@@ -0,0 +0,0 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
-             Codec<PalettedContainer<Holder<Biome>>> codec = makeBiomeCodecRW(iregistry); // CraftBukkit - read/write
- 
-             for (int i1 = 0; i1 < nbttaglist2.size(); ++i1) {
--                CompoundTag nbttagcompound3 = nbttaglist2.getCompound(i1);
-+                CompoundTag nbttagcompound3 = nbttaglist2.getCompound(i1); final CompoundTag sectionData = nbttagcompound3; // Paper - Anti-Xray - OBFHELPER
-                 byte b0 = nbttagcompound3.getByte("Y");
-                 LevelChunkSection chunksection;
- 
-                 if (b0 >= world.getMinSectionY() && b0 <= world.getMaxSectionY()) {
-                     PalettedContainer datapaletteblock;
-+                    // Paper start - Anti-Xray - Add preset block states
-+                    BlockState[] presetBlockStates = serverLevel.chunkPacketBlockController.getPresetBlockStates(serverLevel, chunkcoordintpair, b0);
-+
- 
-                     if (nbttagcompound3.contains("block_states", 10)) {
--                        datapaletteblock = (PalettedContainer) SerializableChunkData.BLOCK_STATE_CODEC.parse(NbtOps.INSTANCE, nbttagcompound3.getCompound("block_states")).promotePartial((s1) -> {
-+                        Codec<PalettedContainer<BlockState>> blockStateCodec = presetBlockStates == null ? BLOCK_STATE_CODEC : PalettedContainer.codecRW(Block.BLOCK_STATE_REGISTRY, BlockState.CODEC, PalettedContainer.Strategy.SECTION_STATES, Blocks.AIR.defaultBlockState(), presetBlockStates); // Paper - Anti-Xray
-+                        datapaletteblock = blockStateCodec.parse(NbtOps.INSTANCE, sectionData.getCompound("block_states")).promotePartial((s1) -> { // Paper - Anti-Xray
-                             logErrors(chunkcoordintpair, b0, s1);
-                         }).getOrThrow(SerializableChunkData.ChunkReadException::new);
-                     } else {
--                        datapaletteblock = new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES);
-+                        datapaletteblock = new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES, presetBlockStates); // Paper - Anti-Xray
-                     }
- 
-                     PalettedContainer object; // CraftBukkit - read/write
-@@ -0,0 +0,0 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
-                             logErrors(chunkcoordintpair, b0, s1);
-                         }).getOrThrow(SerializableChunkData.ChunkReadException::new);
-                     } else {
--                        object = new PalettedContainer<>(iregistry.asHolderIdMap(), iregistry.getOrThrow(Biomes.PLAINS), PalettedContainer.Strategy.SECTION_BIOMES);
-+                        object = new PalettedContainer<>(iregistry.asHolderIdMap(), iregistry.getOrThrow(Biomes.PLAINS), PalettedContainer.Strategy.SECTION_BIOMES, null);  // Paper - Anti-Xray - Add preset biomes
-                     }
- 
-                     chunksection = new LevelChunkSection(datapaletteblock, (PalettedContainer) object); // CraftBukkit - read/write
-@@ -0,0 +0,0 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
- 
-     // CraftBukkit start - read/write
-     private static Codec<PalettedContainer<Holder<Biome>>> makeBiomeCodecRW(Registry<Biome> iregistry) {
--        return PalettedContainer.codecRW(iregistry.asHolderIdMap(), iregistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, iregistry.getOrThrow(Biomes.PLAINS));
-+        return PalettedContainer.codecRW(iregistry.asHolderIdMap(), iregistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, iregistry.getOrThrow(Biomes.PLAINS), null); // Paper - Anti-Xray - Add preset biomes
-     }
-     // CraftBukkit end
- 
diff --git a/feature-patches/1045-Use-Velocity-compression-and-cipher-natives.patch b/feature-patches/1045-Use-Velocity-compression-and-cipher-natives.patch
deleted file mode 100644
index b339b20f9b..0000000000
--- a/feature-patches/1045-Use-Velocity-compression-and-cipher-natives.patch
+++ /dev/null
@@ -1,387 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Andrew Steinborn <git@steinborn.me>
-Date: Mon, 26 Jul 2021 02:15:17 -0400
-Subject: [PATCH] Use Velocity compression and cipher natives
-
-== AT ==
-private-f net.minecraft.network.CompressionDecoder inflater
-
-diff --git a/build.gradle.kts b/build.gradle.kts
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/build.gradle.kts
-+++ b/build.gradle.kts
-@@ -0,0 +0,0 @@ dependencies {
-     runtimeOnly("org.xerial:sqlite-jdbc:3.47.0.0")
-     runtimeOnly("com.mysql:mysql-connector-j:9.1.0")
-     runtimeOnly("com.lmax:disruptor:3.4.4") // Paper
-+    // Paper start - Use Velocity cipher
-+    implementation("com.velocitypowered:velocity-native:3.3.0-SNAPSHOT") {
-+        isTransitive = false
-+    }
-+    // Paper end - Use Velocity cipher
- 
-     runtimeOnly("org.apache.maven:maven-resolver-provider:3.9.6")
-     runtimeOnly("org.apache.maven.resolver:maven-resolver-connector-basic:1.9.18")
-diff --git a/src/main/java/net/minecraft/network/CipherDecoder.java b/src/main/java/net/minecraft/network/CipherDecoder.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/network/CipherDecoder.java
-+++ b/src/main/java/net/minecraft/network/CipherDecoder.java
-@@ -0,0 +0,0 @@ import java.util.List;
- import javax.crypto.Cipher;
- 
- public class CipherDecoder extends MessageToMessageDecoder<ByteBuf> {
--    private final CipherBase cipher;
-+    private final com.velocitypowered.natives.encryption.VelocityCipher cipher; // Paper - Use Velocity cipher
- 
--    public CipherDecoder(Cipher cipher) {
--        this.cipher = new CipherBase(cipher);
-+    public CipherDecoder(com.velocitypowered.natives.encryption.VelocityCipher cipher) {  // Paper - Use Velocity cipher
-+        this.cipher = cipher;  // Paper - Use Velocity cipher
-     }
- 
-     protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
--        list.add(this.cipher.decipher(channelHandlerContext, byteBuf));
-+        // Paper start - Use Velocity cipher
-+        ByteBuf compatible = com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible(channelHandlerContext.alloc(), cipher, byteBuf);
-+        try {
-+            cipher.process(compatible);
-+            list.add(compatible);
-+        } catch (Exception e) {
-+            compatible.release(); // compatible will never be used if we throw an exception
-+            throw e;
-+        }
-+        // Paper end - Use Velocity cipher
-     }
-+
-+    // Paper start - Use Velocity cipher
-+    @Override
-+    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
-+        cipher.close();
-+    }
-+    // Paper end - Use Velocity cipher
- }
-diff --git a/src/main/java/net/minecraft/network/CipherEncoder.java b/src/main/java/net/minecraft/network/CipherEncoder.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/network/CipherEncoder.java
-+++ b/src/main/java/net/minecraft/network/CipherEncoder.java
-@@ -0,0 +0,0 @@ import io.netty.buffer.ByteBuf;
- import io.netty.channel.ChannelHandlerContext;
- import io.netty.handler.codec.MessageToByteEncoder;
- import javax.crypto.Cipher;
-+import java.util.List;
- 
--public class CipherEncoder extends MessageToByteEncoder<ByteBuf> {
--    private final CipherBase cipher;
-+public class CipherEncoder extends io.netty.handler.codec.MessageToMessageEncoder<ByteBuf> { // Paper - Use Velocity cipher; change superclass
-+    private final com.velocitypowered.natives.encryption.VelocityCipher cipher; // Paper - Use Velocity cipher
- 
--    public CipherEncoder(Cipher cipher) {
--        this.cipher = new CipherBase(cipher);
-+    public CipherEncoder(com.velocitypowered.natives.encryption.VelocityCipher cipher) {  // Paper - Use Velocity cipher
-+        this.cipher = cipher;  // Paper - Use Velocity cipher
-     }
- 
--    protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, ByteBuf byteBuf2) throws Exception {
--        this.cipher.encipher(byteBuf, byteBuf2);
-+    protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
-+        // Paper start - Use Velocity cipher
-+        ByteBuf compatible = com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible(channelHandlerContext.alloc(), cipher, byteBuf);
-+        try {
-+            cipher.process(compatible);
-+            list.add(compatible);
-+        } catch (Exception e) {
-+            compatible.release(); // compatible will never be used if we throw an exception
-+            throw e;
-+        }
-+        // Paper end - Use Velocity cipher
-     }
-+
-+    // Paper start - Use Velocity cipher
-+    @Override
-+    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
-+        cipher.close();
-+    }
-+    // Paper end - Use Velocity cipher
- }
-diff --git a/src/main/java/net/minecraft/network/CompressionDecoder.java b/src/main/java/net/minecraft/network/CompressionDecoder.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/network/CompressionDecoder.java
-+++ b/src/main/java/net/minecraft/network/CompressionDecoder.java
-@@ -0,0 +0,0 @@ public class CompressionDecoder extends ByteToMessageDecoder {
-     public static final int MAXIMUM_COMPRESSED_LENGTH = 2097152;
-     public static final int MAXIMUM_UNCOMPRESSED_LENGTH = 8388608;
-     private Inflater inflater;
-+    private com.velocitypowered.natives.compression.VelocityCompressor compressor; // Paper - Use Velocity cipher
-     private int threshold;
-     private boolean validateDecompressed;
- 
-+    // Paper start - Use Velocity cipher
-+    @io.papermc.paper.annotation.DoNotUse
-     public CompressionDecoder(int compressionThreshold, boolean rejectsBadPackets) {
-+        this(null, compressionThreshold, rejectsBadPackets);
-+    }
-+    public CompressionDecoder(com.velocitypowered.natives.compression.VelocityCompressor compressor, int compressionThreshold, boolean rejectsBadPackets) {
-         this.threshold = compressionThreshold;
-         this.validateDecompressed = rejectsBadPackets;
--        this.inflater = new Inflater();
-+        this.inflater = compressor == null ? new Inflater() : null;
-+        this.compressor = compressor;
-+        // Paper end - Use Velocity cipher
-     }
- 
-     protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
-@@ -0,0 +0,0 @@ public class CompressionDecoder extends ByteToMessageDecoder {
-                     }
-                 }
- 
-+                if (inflater != null) { // Paper - Use Velocity cipher; fallback to vanilla inflater
-                 this.setupInflaterInput(byteBuf);
-                 ByteBuf byteBuf2 = this.inflate(channelHandlerContext, i);
-                 this.inflater.reset();
-                 list.add(byteBuf2);
-+                return; // Paper - Use Velocity cipher
-+                } // Paper - use velocity compression
-+
-+                // Paper start - Use Velocity cipher
-+                int claimedUncompressedSize = i; // OBFHELPER
-+                ByteBuf compatibleIn = com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible(channelHandlerContext.alloc(), this.compressor, byteBuf);
-+                ByteBuf uncompressed = com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer(channelHandlerContext.alloc(), this.compressor, claimedUncompressedSize);
-+                try {
-+                    this.compressor.inflate(compatibleIn, uncompressed, claimedUncompressedSize);
-+                    list.add(uncompressed);
-+                    byteBuf.clear();
-+                } catch (Exception e) {
-+                    uncompressed.release();
-+                    throw e;
-+                } finally {
-+                    compatibleIn.release();
-+                }
-+                // Paper end - Use Velocity cipher
-             }
-         }
-     }
- 
-+    // Paper start - Use Velocity cipher
-+    @Override
-+    public void handlerRemoved0(ChannelHandlerContext ctx) throws Exception {
-+        if (this.compressor != null) {
-+            this.compressor.close();
-+        }
-+    }
-+    // Paper end - Use Velocity cipher
-+
-     private void setupInflaterInput(ByteBuf buf) {
-         ByteBuffer byteBuffer;
-         if (buf.nioBufferCount() > 0) {
-@@ -0,0 +0,0 @@ public class CompressionDecoder extends ByteToMessageDecoder {
-         }
-     }
- 
--    public void setThreshold(int compressionThreshold, boolean rejectsBadPackets) {
-+    // Paper start - Use Velocity cipher
-+    public void setThreshold(com.velocitypowered.natives.compression.VelocityCompressor compressor, int compressionThreshold, boolean rejectsBadPackets) {
-+        if (this.compressor == null && compressor != null) { // Only re-configure once. Re-reconfiguring would require closing the native compressor.
-+            this.compressor = compressor;
-+            this.inflater = null;
-+        }
-+    // Paper end - Use Velocity cipher
-         this.threshold = compressionThreshold;
-         this.validateDecompressed = rejectsBadPackets;
-     }
-diff --git a/src/main/java/net/minecraft/network/CompressionEncoder.java b/src/main/java/net/minecraft/network/CompressionEncoder.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/network/CompressionEncoder.java
-+++ b/src/main/java/net/minecraft/network/CompressionEncoder.java
-@@ -0,0 +0,0 @@ import io.netty.handler.codec.MessageToByteEncoder;
- import java.util.zip.Deflater;
- 
- public class CompressionEncoder extends MessageToByteEncoder<ByteBuf> {
--    private final byte[] encodeBuf = new byte[8192];
-+    @javax.annotation.Nullable private final byte[] encodeBuf; // Paper - Use Velocity cipher
-+    @javax.annotation.Nullable // Paper - Use Velocity cipher
-     private final Deflater deflater;
-+    @javax.annotation.Nullable // Paper - Use Velocity cipher
-+    private final com.velocitypowered.natives.compression.VelocityCompressor compressor; // Paper - Use Velocity cipher
-     private int threshold;
- 
-+    // Paper start - Use Velocity cipher
-     public CompressionEncoder(int compressionThreshold) {
-+        this(null, compressionThreshold);
-+    }
-+    public CompressionEncoder(@javax.annotation.Nullable com.velocitypowered.natives.compression.VelocityCompressor compressor, int compressionThreshold) {
-         this.threshold = compressionThreshold;
--        this.deflater = new Deflater();
-+        if (compressor == null) {
-+            this.encodeBuf = new byte[8192];
-+            this.deflater = new Deflater();
-+        } else {
-+            this.encodeBuf = null;
-+            this.deflater = null;
-+        }
-+        this.compressor = compressor;
-+        // Paper end - Use Velocity cipher
-     }
- 
--    protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, ByteBuf byteBuf2) {
-+    protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, ByteBuf byteBuf2) throws Exception { // Paper - Use Velocity cipher
-         int i = byteBuf.readableBytes();
-         if (i > 8388608) {
-             throw new IllegalArgumentException("Packet too big (is " + i + ", should be less than 8388608)");
-@@ -0,0 +0,0 @@ public class CompressionEncoder extends MessageToByteEncoder<ByteBuf> {
-                 VarInt.write(byteBuf2, 0);
-                 byteBuf2.writeBytes(byteBuf);
-             } else {
-+                if (this.deflater != null) { // Paper - Use Velocity cipher
-                 byte[] bs = new byte[i];
-                 byteBuf.readBytes(bs);
-                 VarInt.write(byteBuf2, bs.length);
-@@ -0,0 +0,0 @@ public class CompressionEncoder extends MessageToByteEncoder<ByteBuf> {
-                 }
- 
-                 this.deflater.reset();
-+                // Paper start - Use Velocity cipher
-+                    return;
-+                }
-+
-+                VarInt.write(byteBuf2, i);
-+                final ByteBuf compatibleIn = com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible(channelHandlerContext.alloc(), this.compressor, byteBuf);
-+                try {
-+                    this.compressor.deflate(compatibleIn, byteBuf2);
-+                } finally {
-+                    compatibleIn.release();
-+                }
-             }
-         }
-     }
- 
-+    @Override
-+    protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg, boolean preferDirect) throws Exception{
-+        if (this.compressor != null) {
-+            // We allocate bytes to be compressed plus 1 byte. This covers two cases:
-+            //
-+            // - Compression
-+            //    According to https://github.com/ebiggers/libdeflate/blob/master/libdeflate.h#L103,
-+            //    if the data compresses well (and we do not have some pathological case) then the maximum
-+            //    size the compressed size will ever be is the input size minus one.
-+            // - Uncompressed
-+            //    This is fairly obvious - we will then have one more than the uncompressed size.
-+            final int initialBufferSize = msg.readableBytes() + 1;
-+            return com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer(ctx.alloc(), this.compressor, initialBufferSize);
-+        }
-+
-+        return super.allocateBuffer(ctx, msg, preferDirect);
-+    }
-+
-+    @Override
-+    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
-+        if (this.compressor != null) {
-+            this.compressor.close();
-+            // Paper end - Use Velocity cipher
-+        }
-+    }
-+
-     public int getThreshold() {
-         return this.threshold;
-     }
-diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/network/Connection.java
-+++ b/src/main/java/net/minecraft/network/Connection.java
-@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
-         return networkmanager;
-     }
- 
--    public void setEncryptionKey(Cipher decryptionCipher, Cipher encryptionCipher) {
--        this.encrypted = true;
--        this.channel.pipeline().addBefore("splitter", "decrypt", new CipherDecoder(decryptionCipher));
--        this.channel.pipeline().addBefore("prepender", "encrypt", new CipherEncoder(encryptionCipher));
-+    // Paper start - Use Velocity cipher
-+//    public void setEncryptionKey(Cipher decryptionCipher, Cipher encryptionCipher) {
-+//        this.encrypted = true;
-+//        this.channel.pipeline().addBefore("splitter", "decrypt", new CipherDecoder(decryptionCipher));
-+//        this.channel.pipeline().addBefore("prepender", "encrypt", new CipherEncoder(encryptionCipher));
-+//    }
-+
-+    public void setupEncryption(javax.crypto.SecretKey key) throws net.minecraft.util.CryptException {
-+        if (!this.encrypted) {
-+            try {
-+                com.velocitypowered.natives.encryption.VelocityCipher decryption = com.velocitypowered.natives.util.Natives.cipher.get().forDecryption(key);
-+                com.velocitypowered.natives.encryption.VelocityCipher encryption = com.velocitypowered.natives.util.Natives.cipher.get().forEncryption(key);
-+
-+                this.encrypted = true;
-+                this.channel.pipeline().addBefore("splitter", "decrypt", new CipherDecoder(decryption));
-+                this.channel.pipeline().addBefore("prepender", "encrypt", new CipherEncoder(encryption));
-+            } catch (java.security.GeneralSecurityException e) {
-+                throw new net.minecraft.util.CryptException(e);
-+            }
-+        }
-     }
-+    // Paper end - Use Velocity cipher
- 
-     public boolean isEncrypted() {
-         return this.encrypted;
-@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
- 
-     public void setupCompression(int compressionThreshold, boolean rejectsBadPackets) {
-         if (compressionThreshold >= 0) {
-+            com.velocitypowered.natives.compression.VelocityCompressor compressor = com.velocitypowered.natives.util.Natives.compress.get().create(io.papermc.paper.configuration.GlobalConfiguration.get().misc.compressionLevel.or(-1)); // Paper - Use Velocity cipher
-             ChannelHandler channelhandler = this.channel.pipeline().get("decompress");
- 
-             if (channelhandler instanceof CompressionDecoder) {
-                 CompressionDecoder packetdecompressor = (CompressionDecoder) channelhandler;
- 
--                packetdecompressor.setThreshold(compressionThreshold, rejectsBadPackets);
-+                packetdecompressor.setThreshold(compressor, compressionThreshold, rejectsBadPackets); // Paper - Use Velocity cipher
-             } else {
--                this.channel.pipeline().addAfter("splitter", "decompress", new CompressionDecoder(compressionThreshold, rejectsBadPackets));
-+                this.channel.pipeline().addAfter("splitter", "decompress", new CompressionDecoder(compressor, compressionThreshold, rejectsBadPackets)); // Paper - Use Velocity cipher
-             }
- 
-             channelhandler = this.channel.pipeline().get("compress");
-@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
- 
-                 packetcompressor.setThreshold(compressionThreshold);
-             } else {
--                this.channel.pipeline().addAfter("prepender", "compress", new CompressionEncoder(compressionThreshold));
-+                this.channel.pipeline().addAfter("prepender", "compress", new CompressionEncoder(compressor, compressionThreshold)); // Paper - Use Velocity cipher
-             }
-             this.channel.pipeline().fireUserEventTriggered(io.papermc.paper.network.ConnectionEvent.COMPRESSION_THRESHOLD_SET); // Paper - Add Channel initialization listeners
-         } else {
-diff --git a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java
-+++ b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java
-@@ -0,0 +0,0 @@ public class ServerConnectionListener {
-             }
-             // Paper end - Warn people with console access that HAProxy is in use.
- 
-+            // Paper start - Use Velocity cipher
-+            ServerConnectionListener.LOGGER.info("Paper: Using " + com.velocitypowered.natives.util.Natives.compress.getLoadedVariant() + " compression from Velocity.");
-+            ServerConnectionListener.LOGGER.info("Paper: Using " + com.velocitypowered.natives.util.Natives.cipher.getLoadedVariant() + " cipher from Velocity.");
-+            // Paper end - Use Velocity cipher
-+
-             this.channels.add(((ServerBootstrap) ((ServerBootstrap) (new ServerBootstrap()).channel(oclass)).childHandler(new ChannelInitializer<Channel>() {
-                 protected void initChannel(Channel channel) {
-                     try {
-diff --git a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
-+++ b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
-@@ -0,0 +0,0 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
-             }
- 
-             SecretKey secretkey = packet.getSecretKey(privatekey);
--            Cipher cipher = Crypt.getCipher(2, secretkey);
--            Cipher cipher1 = Crypt.getCipher(1, secretkey);
-+            // Paper start - Use Velocity cipher
-+//            Cipher cipher = Crypt.getCipher(2, secretkey);
-+//            Cipher cipher1 = Crypt.getCipher(1, secretkey);
-+            // Paper end - Use Velocity cipher
- 
-             s = (new BigInteger(Crypt.digestData("", this.server.getKeyPair().getPublic(), secretkey))).toString(16);
-             this.state = ServerLoginPacketListenerImpl.State.AUTHENTICATING;
--            this.connection.setEncryptionKey(cipher, cipher1);
-+            this.connection.setupEncryption(secretkey); // Paper - Use Velocity cipher
-         } catch (CryptException cryptographyexception) {
-             throw new IllegalStateException("Protocol error", cryptographyexception);
-         }
diff --git a/feature-patches/1047-Optimize-GoalSelector-Goal.Flag-Set-operations.patch b/feature-patches/1047-Optimize-GoalSelector-Goal.Flag-Set-operations.patch
deleted file mode 100644
index 4200e29c19..0000000000
--- a/feature-patches/1047-Optimize-GoalSelector-Goal.Flag-Set-operations.patch
+++ /dev/null
@@ -1,171 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf <Spottedleaf@users.noreply.github.com>
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ import net.minecraft.world.entity.Entity;
- import net.minecraft.world.level.Level;
- 
- public abstract class Goal {
--    private final EnumSet<Goal.Flag> flags = EnumSet.noneOf(Goal.Flag.class);
-+    private final EnumSet<Goal.Flag> 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 ca.spottedleaf.moonrise.common.set.OptimizedSmallEnumSet<net.minecraft.world.entity.ai.goal.Goal.Flag> goalTypes = new ca.spottedleaf.moonrise.common.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.addUnchecked(Flag.UNKNOWN_BEHAVIOR);
-+        }
-+    }
-+    // Paper end - remove streams from pathfindergoalselector
- 
-     public abstract boolean canUse();
- 
-@@ -0,0 +0,0 @@ public abstract class Goal {
-     }
- 
-     public void setFlags(EnumSet<Goal.Flag> controls) {
--        this.flags.clear();
--        this.flags.addAll(controls);
-+        // Paper start - remove streams from pathfindergoalselector
-+        this.goalTypes.clear();
-+        this.goalTypes.addAllUnchecked(controls);
-+        if (this.goalTypes.size() == 0) {
-+            this.goalTypes.addUnchecked(Flag.UNKNOWN_BEHAVIOR);
-+        }
-+        // Paper end - remove streams from pathfindergoalselector
-     }
- 
-     @Override
-@@ -0,0 +0,0 @@ public abstract class Goal {
-         return this.getClass().getSimpleName();
-     }
- 
--    public EnumSet<Goal.Flag> getFlags() {
--        return this.flags;
-+    // Paper start - remove streams from pathfindergoalselector
-+    public ca.spottedleaf.moonrise.common.set.OptimizedSmallEnumSet<Goal.Flag> getFlags() {
-+        return this.goalTypes;
-+        // Paper end - remove streams from pathfindergoalselector
-     }
- 
-     // Paper start - Mob Goal API
-     public boolean hasFlag(final Goal.Flag flag) {
--        return this.flags.contains(flag);
-+        return this.goalTypes.hasElement(flag);
-     }
- 
-     public void addFlag(final Goal.Flag flag) {
--        this.flags.add(flag);
-+        this.goalTypes.addUnchecked(flag);
-     }
-     // Paper end - Mob Goal API
- 
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ public class GoalSelector {
-     };
-     private final Map<Goal.Flag, WrappedGoal> lockedFlags = new EnumMap<>(Goal.Flag.class);
-     private final Set<WrappedGoal> availableGoals = new ObjectLinkedOpenHashSet<>();
--    private final EnumSet<Goal.Flag> disabledFlags = EnumSet.noneOf(Goal.Flag.class);
-+    private static final Goal.Flag[] GOAL_FLAG_VALUES = Goal.Flag.values(); // Paper - remove streams from pathfindergoalselector
-+    private final ca.spottedleaf.moonrise.common.set.OptimizedSmallEnumSet<net.minecraft.world.entity.ai.goal.Goal.Flag> goalTypes = new ca.spottedleaf.moonrise.common.set.OptimizedSmallEnumSet<>(Goal.Flag.class); // Paper - remove streams from pathfindergoalselector
-     private int curRate; // Paper - EAR 2
- 
-     public void addGoal(int priority, Goal goal) {
-@@ -0,0 +0,0 @@ public class GoalSelector {
-         this.availableGoals.removeIf(wrappedGoalx -> wrappedGoalx.getGoal() == goal);
-     }
- 
--    private static boolean goalContainsAnyFlags(WrappedGoal goal, EnumSet<Goal.Flag> controls) {
--        for (Goal.Flag flag : goal.getFlags()) {
--            if (controls.contains(flag)) {
--                return true;
--            }
--        }
--
--        return false;
-+    // Paper start
-+    private static boolean goalContainsAnyFlags(WrappedGoal goal, ca.spottedleaf.moonrise.common.set.OptimizedSmallEnumSet<Goal.Flag> controls) {
-+        return goal.getFlags().hasCommonElements(controls);
-     }
- 
-     private static boolean goalCanBeReplacedForAllFlags(WrappedGoal goal, Map<Goal.Flag, WrappedGoal> 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 ^= ca.spottedleaf.concurrentutil.util.IntegerUtil.getTrailingBit(flagIterator);
-+            // Paper end
-             if (!goalsByControl.getOrDefault(flag, NO_GOAL).canBeReplacedBy(goal)) {
-                 return false;
-             }
-@@ -0,0 +0,0 @@ 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();
-             }
-         }
-@@ -0,0 +0,0 @@ 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 ^= ca.spottedleaf.concurrentutil.util.IntegerUtil.getTrailingBit(flagIterator);
-+                    // Paper end
-                     WrappedGoal wrappedGoal3 = this.lockedFlags.getOrDefault(flag, NO_GOAL);
-                     wrappedGoal3.stop();
-                     this.lockedFlags.put(flag, wrappedGoal2);
-@@ -0,0 +0,0 @@ public class GoalSelector {
-     }
- 
-     public void disableControlFlag(Goal.Flag control) {
--        this.disabledFlags.add(control);
-+        this.goalTypes.addUnchecked(control); // Paper - remove streams from pathfindergoalselector
-     }
- 
-     public void enableControlFlag(Goal.Flag control) {
--        this.disabledFlags.remove(control);
-+        this.goalTypes.removeUnchecked(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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ public class WrappedGoal extends Goal {
-     }
- 
-     @Override
--    public EnumSet<Goal.Flag> getFlags() {
-+    // Paper start - remove streams from pathfindergoalselector
-+    public ca.spottedleaf.moonrise.common.set.OptimizedSmallEnumSet<Goal.Flag> getFlags() {
-         return this.goal.getFlags();
-+        // Paper end - remove streams from pathfindergoalselector
-     }
- 
-     public boolean isRunning() {
diff --git a/feature-patches/1048-Optimize-Hoppers.patch b/feature-patches/1048-Optimize-Hoppers.patch
deleted file mode 100644
index 3b088a1787..0000000000
--- a/feature-patches/1048-Optimize-Hoppers.patch
+++ /dev/null
@@ -1,664 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Aikar <aikar@aikar.co>
-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..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/event/inventory/PaperInventoryMoveItemEvent.java
-@@ -0,0 +0,0 @@
-+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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/MinecraftServer.java
-+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
-             ServerLevel worldserver = (ServerLevel) iterator.next();
-             worldserver.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 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
- 
-             gameprofilerfiller.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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/item/ItemStack.java
-+++ b/src/main/java/net/minecraft/world/item/ItemStack.java
-@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ import org.bukkit.inventory.InventoryHolder;
- // CraftBukkit end
- 
- public abstract class BlockEntity {
-+    static boolean ignoreTileUpdates; // Paper - Perf: Optimize Hoppers
- 
-     // CraftBukkit start - data containers
-     private static final CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new CraftPersistentDataTypeRegistry();
-@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ 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<ItemStack> 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;
-@@ -0,0 +0,0 @@ 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();
-                 }
- 
-@@ -0,0 +0,0 @@ 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<ItemStack, Integer> 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<ItemStack, Integer> 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<ItemStack, Integer> STACK_SIZE_TEST = (itemstack, i) -> itemstack.getCount() >= itemstack.getMaxStackSize();
-+    private static final java.util.function.BiPredicate<ItemStack, Integer> 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);
- 
-@@ -0,0 +0,0 @@ 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
--                        ItemStack original = itemstack.copy();
--                        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, original);
--                            blockEntity.setCooldown(world.spigotConfig.hopperTransfer); // Delay hopper checks // Spigot
--                            return false;
--                        }
--                        int origCount = event.getItem().getAmount(); // Spigot
--                        ItemStack itemstack1 = HopperBlockEntity.addItem(blockEntity, iinventory, CraftItemStack.asNMSCopy(event.getItem()), enumdirection);
--                        // CraftBukkit end
--
--                        if (itemstack1.isEmpty()) {
--                            iinventory.setChanged();
--                            return true;
--                        }
--
--                        itemstack.setCount(j);
--                        // Spigot start
--                        itemstack.shrink(origCount - itemstack1.getCount());
--                        if (j <= world.spigotConfig.hopperAmount) {
--                            // Spigot end
--                            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
-+                //        ItemStack original = itemstack.copy();
-+                //        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, original);
-+                //            blockEntity.setCooldown(world.spigotConfig.hopperTransfer); // Delay hopper checks // Spigot
-+                //            return false;
-+                //        }
-+                //        int origCount = event.getItem().getAmount(); // Spigot
-+                //        ItemStack itemstack1 = HopperBlockEntity.addItem(blockEntity, iinventory, CraftItemStack.asNMSCopy(event.getItem()), enumdirection);
-+                //        // CraftBukkit end
-+
-+                //        if (itemstack1.isEmpty()) {
-+                //            iinventory.setChanged();
-+                //            return true;
-+                //        }
-+
-+                //        itemstack.setCount(j);
-+                //        // Spigot start
-+                //        itemstack.shrink(origCount - itemstack1.getCount());
-+                //        if (j <= world.spigotConfig.hopperAmount) {
-+                //          // Spigot end
-+                //            blockEntity.setItem(i, itemstack);
-+                //        }
-+                //    }
-+                //}
-+
-+                // return false;
-+                // Paper end - Perf: Optimize Hoppers
-             }
-         }
-     }
-@@ -0,0 +0,0 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
-                 return false;
-             }
-         }
--
-         return true;
-     }
- 
-@@ -0,0 +0,0 @@ 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;
- 
-@@ -0,0 +0,0 @@ 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
--            ItemStack original = itemstack.copy();
--            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, original);
--
--                if (ihopper instanceof HopperBlockEntity) {
--                    ((HopperBlockEntity) ihopper).setCooldown(world.spigotConfig.hopperTransfer); // Spigot
--                }
--
--                return false;
--            }
--            int origCount = event.getItem().getAmount(); // Spigot
--            ItemStack itemstack1 = HopperBlockEntity.addItem(iinventory, ihopper, CraftItemStack.asNMSCopy(event.getItem()), null);
--            // CraftBukkit end
--
--            if (itemstack1.isEmpty()) {
--                iinventory.setChanged();
--                return true;
--            }
--
--            itemstack.setCount(j);
--            // Spigot start
--            itemstack.shrink(origCount - itemstack1.getCount());
--            if (j <= world.spigotConfig.hopperAmount) {
--                // Spigot end
--                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
-+        //    ItemStack original = itemstack.copy();
-+        //    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, original);
-+
-+        //        if (ihopper instanceof HopperBlockEntity) {
-+        //            ((HopperBlockEntity) ihopper).setCooldown(world.spigotConfig.hopperTransfer); // Spigot
-+        //        }
-+
-+        //        return false;
-+        //    }
-+        //    int origCount = event.getItem().getAmount(); // Spigot
-+        //    ItemStack itemstack1 = HopperBlockEntity.addItem(iinventory, ihopper, CraftItemStack.asNMSCopy(event.getItem()), null);
-+        //    // CraftBukkit end
-+
-+        //    if (itemstack1.isEmpty()) {
-+        //        iinventory.setChanged();
-+        //        return true;
-+        //    }
-+
-+        //    itemstack.setCount(j);
-+        //    // Spigot start
-+        //    itemstack.shrink(origCount - itemstack1.getCount());
-+        //    if (j <= world.spigotConfig.hopperAmount) {
-+        //        // Spigot end
-+        //        iinventory.setItem(i, itemstack);
-+        //    }
-+            // Paper end - Perf: Optimize Hoppers
-         }
- 
-         return false;
-@@ -0,0 +0,0 @@ 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);
- 
-@@ -0,0 +0,0 @@ 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)) {
-@@ -0,0 +0,0 @@ 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);
-         }
- 
-@@ -0,0 +0,0 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
- 
-     @Nullable
-     private static Container getEntityContainer(Level world, double x, double y, double z) {
--        List<Entity> 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<Entity> 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ 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/feature-patches/1049-Optimize-Voxel-Shape-Merging.patch b/feature-patches/1049-Optimize-Voxel-Shape-Merging.patch
deleted file mode 100644
index 16a5277743..0000000000
--- a/feature-patches/1049-Optimize-Voxel-Shape-Merging.patch
+++ /dev/null
@@ -1,123 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Aikar <aikar@aikar.co>
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/phys/shapes/IndirectMerger.java
-+++ b/src/main/java/net/minecraft/world/phys/shapes/IndirectMerger.java
-@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java
-+++ b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java
-@@ -0,0 +0,0 @@ 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) {
-@@ -0,0 +0,0 @@ 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/feature-patches/1050-Optimize-Bit-Operations-by-inlining.patch b/feature-patches/1050-Optimize-Bit-Operations-by-inlining.patch
deleted file mode 100644
index 7d7f2d776f..0000000000
--- a/feature-patches/1050-Optimize-Bit-Operations-by-inlining.patch
+++ /dev/null
@@ -1,213 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Aikar <aikar@aikar.co>
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/core/BlockPos.java
-+++ b/src/main/java/net/minecraft/core/BlockPos.java
-@@ -0,0 +0,0 @@ public class BlockPos extends Vec3i {
-     };
-     private static final Logger LOGGER = LogUtils.getLogger();
-     public static final BlockPos ZERO = new BlockPos(0, 0, 0);
--    public static final int PACKED_HORIZONTAL_LENGTH = 1 + Mth.log2(Mth.smallestEncompassingPowerOfTwo(30000000));
--    public static final int PACKED_Y_LENGTH = 64 - 2 * PACKED_HORIZONTAL_LENGTH;
--    private static final long PACKED_X_MASK = (1L << PACKED_HORIZONTAL_LENGTH) - 1L;
--    private static final long PACKED_Y_MASK = (1L << PACKED_Y_LENGTH) - 1L;
--    private static final long PACKED_Z_MASK = (1L << PACKED_HORIZONTAL_LENGTH) - 1L;
-+    // Paper start - Optimize Bit Operations by inlining
-+    public static final int PACKED_HORIZONTAL_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 Y_OFFSET = 0;
--    private static final int Z_OFFSET = PACKED_Y_LENGTH;
--    private static final int X_OFFSET = PACKED_Y_LENGTH + PACKED_HORIZONTAL_LENGTH;
--    public static final int MAX_HORIZONTAL_COORDINATE = (1 << PACKED_HORIZONTAL_LENGTH) / 2 - 1;
-+    private static final int Z_OFFSET = 12;
-+    private static final int X_OFFSET = 38;
-+    public static final int MAX_HORIZONTAL_COORDINATE = 33554431;
-+    // Paper end - Optimize Bit Operations by inlining
- 
-     public BlockPos(int x, int y, int z) {
-         super(x, y, z);
-@@ -0,0 +0,0 @@ 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_HORIZONTAL_LENGTH >> 64 - PACKED_HORIZONTAL_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_HORIZONTAL_LENGTH >> 64 - PACKED_HORIZONTAL_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) {
-@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/core/SectionPos.java
-+++ b/src/main/java/net/minecraft/core/SectionPos.java
-@@ -0,0 +0,0 @@ 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) {
-@@ -0,0 +0,0 @@ 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) {
-@@ -0,0 +0,0 @@ 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) {
-@@ -0,0 +0,0 @@ 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) {
-@@ -0,0 +0,0 @@ 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() {
-@@ -0,0 +0,0 @@ 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) {
-@@ -0,0 +0,0 @@ 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
-@@ -0,0 +0,0 @@ public class SectionPos extends Vec3i {
-     }
- 
-     public static Stream<SectionPos> 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<SectionPos> 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, j + radius);
-+        return betweenClosedStream(center.x - radius, minY, center.z - radius, center.x + radius, maxY, center.z + radius); // Paper - simplify/inline
-     }
- 
-     public static Stream<SectionPos> betweenClosedStream(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
diff --git a/feature-patches/1051-Remove-streams-from-hot-code.patch b/feature-patches/1051-Remove-streams-from-hot-code.patch
deleted file mode 100644
index 815e75e1bd..0000000000
--- a/feature-patches/1051-Remove-streams-from-hot-code.patch
+++ /dev/null
@@ -1,215 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Josh Roy <10731363+JRoy@users.noreply.github.com>
-Date: Wed, 1 Jul 2020 18:01:49 -0400
-Subject: [PATCH] Remove streams from hot code
-
-Co-authored-by: Bjarne Koll <git@lynxplay.dev>
-Co-authored-by: Spottedleaf <Spottedleaf@users.noreply.github.com>
-
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ public class GateBehavior<E extends LivingEntity> implements BehaviorControl<E>
-         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;
-@@ -0,0 +0,0 @@ public class GateBehavior<E extends LivingEntity> implements BehaviorControl<E>
- 
-     @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<? super E> 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);
-         }
-@@ -0,0 +0,0 @@ public class GateBehavior<E extends LivingEntity> implements BehaviorControl<E>
-     @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<? super E> 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
-@@ -0,0 +0,0 @@ public class GateBehavior<E extends LivingEntity> implements BehaviorControl<E>
- 
-     public static enum RunningPolicy {
-         RUN_ONE {
-+            // Paper start - Perf: Remove streams from hot code
-             @Override
--            public <E extends LivingEntity> void apply(Stream<BehaviorControl<? super E>> tasks, ServerLevel world, E entity, long time) {
--                tasks.filter(task -> task.getStatus() == Behavior.Status.STOPPED).filter(task -> task.tryStart(world, entity, time)).findFirst();
-+            public <E extends LivingEntity> void apply(ShufflingList<BehaviorControl<? super E>> tasks, ServerLevel world, E entity, long time) {
-+                for (final BehaviorControl<? super E> 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 <E extends LivingEntity> void apply(Stream<BehaviorControl<? super E>> tasks, ServerLevel world, E entity, long time) {
--                tasks.filter(task -> task.getStatus() == Behavior.Status.STOPPED).forEach(task -> task.tryStart(world, entity, time));
-+            public <E extends LivingEntity> void apply(ShufflingList<BehaviorControl<? super E>> tasks, ServerLevel world, E entity, long time) {
-+                for (final BehaviorControl<? super E> task : tasks) {
-+                    if (task.getStatus() == Behavior.Status.STOPPED) {
-+                        task.tryStart(world, entity, time);
-+                    }
-+                }
-+                // Paper end - Perf: Remove streams from hot code
-             }
-         };
- 
--        public abstract <E extends LivingEntity> void apply(Stream<BehaviorControl<? super E>> tasks, ServerLevel world, E entity, long time);
-+        public abstract <E extends LivingEntity> void apply(ShufflingList<BehaviorControl<? super E>> 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ 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<GossipContainer.GossipEntry> decompress() {
-+        List<GossipContainer.GossipEntry> list = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>();
-+        for (Map.Entry<UUID, GossipContainer.EntityGossips> 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<GossipContainer.GossipEntry> selectGossipsForTransfer(RandomSource random, int count) {
--        List<GossipContainer.GossipEntry> list = this.unpack().toList();
-+        List<GossipContainer.GossipEntry> list = this.decompress(); // Paper - Perf: Remove streams from hot code
-         if (list.isEmpty()) {
-             return Collections.emptyList();
-         } else {
-@@ -0,0 +0,0 @@ public class GossipContainer {
- 
-     public <T> T store(DynamicOps<T> 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);
-     }
-@@ -0,0 +0,0 @@ public class GossipContainer {
-         final Object2IntMap<GossipType> entries = new Object2IntOpenHashMap<>();
- 
-         public int weightedValue(Predicate<GossipType> 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<GossipType> entry : entries.object2IntEntrySet()) {
-+                if (gossipTypeFilter.test(entry.getKey())) {
-+                    weight += entry.getIntValue() * entry.getKey().weight;
-+                }
-+            }
-+            return weight;
-+        }
-+
-+        public List<GossipContainer.GossipEntry> decompress(UUID uuid) {
-+            List<GossipContainer.GossipEntry> list = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>();
-+            for (Object2IntMap.Entry<GossipType> 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<GossipContainer.GossipEntry> 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ public class NearestItemSensor extends Sensor<Mob> {
-     @Override
-     protected void doTick(ServerLevel world, Mob entity) {
-         Brain<?> brain = entity.getBrain();
--        List<ItemEntity> list = world.getEntitiesOfClass(ItemEntity.class, entity.getBoundingBox().inflate(32.0, 16.0, 32.0), itemEntity -> true);
-+        List<ItemEntity> 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(world, itemEntity.getItem())); // Paper - Perf: Move predicate into getEntities
-         list.sort(Comparator.comparingDouble(entity::distanceToSqr));
--        Optional<ItemEntity> optional = list.stream()
--            .filter(itemEntity -> entity.wantsToPickUp(world, itemEntity.getItem()))
--            .filter(itemEntityx -> itemEntityx.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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/levelgen/Beardifier.java
-+++ b/src/main/java/net/minecraft/world/level/levelgen/Beardifier.java
-@@ -0,0 +0,0 @@ public class Beardifier implements DensityFunctions.BeardifierOrMarker {
-         int j = pos.getMinBlockZ();
-         ObjectList<Beardifier.Rigid> objectList = new ObjectArrayList<>(10);
-         ObjectList<JigsawJunction> 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()) {
-@@ -0,0 +0,0 @@ public class Beardifier implements DensityFunctions.BeardifierOrMarker {
-                             }
-                         }
-                     }
--                }
--            );
-+        } // Paper - Perf: Remove streams from hot code
-         return new Beardifier(objectList.iterator(), objectList2.iterator());
-     }
- 
diff --git a/feature-patches/1053-Fix-entity-type-tags-suggestions-in-selectors.patch b/feature-patches/1053-Fix-entity-type-tags-suggestions-in-selectors.patch
deleted file mode 100644
index d5c45f7291..0000000000
--- a/feature-patches/1053-Fix-entity-type-tags-suggestions-in-selectors.patch
+++ /dev/null
@@ -1,152 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Jake Potrebic <jake.m.potrebic@gmail.com>
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/commands/CommandSourceStack.java
-+++ b/src/main/java/net/minecraft/commands/CommandSourceStack.java
-@@ -0,0 +0,0 @@ public class CommandSourceStack implements ExecutionCommandSource<CommandSourceS
-         return this.source.getBukkitSender(this);
-     }
-     // CraftBukkit end
-+    // Paper start - tell clients to ask server for suggestions for EntityArguments
-+    @Override
-+    public Collection<String> 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/commands/Commands.java
-+++ b/src/main/java/net/minecraft/commands/Commands.java
-@@ -0,0 +0,0 @@ public class Commands {
-         Iterator iterator = children.iterator();
-         // Paper end - Perf: Async command map building
- 
-+        boolean registeredAskServerSuggestionsForTree = false; // Paper - tell clients to ask server for suggestions for EntityArguments
-         while (iterator.hasNext()) {
-             CommandNode<CommandSourceStack> commandnode2 = (CommandNode) iterator.next();
-             // Paper start - Brigadier API
-@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/commands/arguments/EntityArgument.java
-+++ b/src/main/java/net/minecraft/commands/arguments/EntityArgument.java
-@@ -0,0 +0,0 @@ public class EntityArgument implements ArgumentType<EntitySelector> {
-             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 permissions
- 
-             try {
-@@ -0,0 +0,0 @@ public class EntityArgument implements ArgumentType<EntitySelector> {
-             }
- 
-             return argumentparserselector.fillSuggestions(suggestionsbuilder, (suggestionsbuilder1) -> {
--                Collection<String> collection = icompletionprovider.getOnlinePlayerNames();
-+                // Paper start - tell clients to ask server for suggestions for EntityArguments
-+                final Collection<String> 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<String> 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelectorParser.java
-+++ b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelectorParser.java
-@@ -0,0 +0,0 @@ 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, 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ 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<EntitySelectorParser> condition, Component description) {
-         OPTIONS.put(id, new EntitySelectorOptions.Option(handler, condition, description));
-@@ -0,0 +0,0 @@ public class EntitySelectorOptions {
- 
-                         if (reader.isTag()) {
-                             TagKey<EntityType<?>> 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.get(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/feature-patches/1055-Check-distance-in-entity-interactions.patch b/feature-patches/1055-Check-distance-in-entity-interactions.patch
deleted file mode 100644
index d478e65bcc..0000000000
--- a/feature-patches/1055-Check-distance-in-entity-interactions.patch
+++ /dev/null
@@ -1,68 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf <Spottedleaf@users.noreply.github.com>
-Date: Mon, 2 Aug 2021 10:10:40 +0200
-Subject: [PATCH] Check distance in entity interactions
-
-
-diff --git a/src/main/java/net/minecraft/Util.java b/src/main/java/net/minecraft/Util.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/Util.java
-+++ b/src/main/java/net/minecraft/Util.java
-@@ -0,0 +0,0 @@ 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 - Check distance in entity interactions
-     private static Consumer<String> 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/entity/LivingEntity.java
-+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java
-@@ -0,0 +0,0 @@ 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 - Check distance in entity interactions
-                         LivingEntity entityliving = (LivingEntity) entity;
- 
-                         this.blockUsingShield(entityliving);
-@@ -0,0 +0,0 @@ public abstract class LivingEntity extends Entity implements Attackable {
-                         d0 = source.getSourcePosition().x() - this.getX();
-                         d1 = source.getSourcePosition().z() - this.getZ();
-                     }
-+                    // Paper start - Check distance in entity interactions; see for loop in knockback method
-+                    if (Math.abs(d0) > 200) {
-+                        d0 = Math.random() - Math.random();
-+                    }
-+                    if (Math.abs(d1) > 200) {
-+                        d1 = Math.random() - Math.random();
-+                    }
-+                    // Paper end - Check distance in entity interactions
- 
-                     this.knockback(0.4000000059604645D, d0, d1, entity1, entity1 == null ? io.papermc.paper.event.entity.EntityKnockbackEvent.Cause.DAMAGE : io.papermc.paper.event.entity.EntityKnockbackEvent.Cause.ENTITY_ATTACK); // CraftBukkit // Paper - knockback events
-                     if (!flag) {
-@@ -0,0 +0,0 @@ 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 & Check distance in entity interactions
-                     this.blockUsingShield((LivingEntity) entity);
-                 }
-             }
-diff --git a/src/main/java/net/minecraft/world/entity/vehicle/AbstractBoat.java b/src/main/java/net/minecraft/world/entity/vehicle/AbstractBoat.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/entity/vehicle/AbstractBoat.java
-+++ b/src/main/java/net/minecraft/world/entity/vehicle/AbstractBoat.java
-@@ -0,0 +0,0 @@ public abstract class AbstractBoat extends VehicleEntity implements Leashable {
-             double d2 = (double) (this.getWaterLevelAbove() - this.getBbHeight()) + 0.101D;
- 
-             if (this.level().noCollision(this, this.getBoundingBox().move(0.0D, d2 - this.getY(), 0.0D))) {
--                this.setPos(this.getX(), d2, this.getZ());
-+                this.move(MoverType.SELF, new Vec3(0.0D, d2 - this.getY(), 0.0D)); // Paper - Check distance in entity interactions // TODO Still needed??
-                 this.setDeltaMovement(this.getDeltaMovement().multiply(1.0D, 0.0D, 1.0D));
-                 this.lastYd = 0.0D;
-             }
diff --git a/feature-patches/1056-optimize-dirt-and-snow-spreading.patch b/feature-patches/1056-optimize-dirt-and-snow-spreading.patch
deleted file mode 100644
index c13eaa6f09..0000000000
--- a/feature-patches/1056-optimize-dirt-and-snow-spreading.patch
+++ /dev/null
@@ -1,78 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: lukas81298 <lukas81298@gommehd.net>
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java
-+++ b/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java
-@@ -0,0 +0,0 @@ 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;
-@@ -0,0 +0,0 @@ public abstract class SpreadingSnowyDirtBlock extends SnowyDirtBlock {
-     protected abstract MapCodec<? extends SpreadingSnowyDirtBlock> 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;
-@@ -0,0 +0,0 @@ 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, isSnowySetting(world.getBlockState(blockposition1.above())))); // CraftBukkit
-+                    // Paper start - Perf: optimize dirt and snow spreading
-+                    if (pos.getX() == blockposition1.getX() && pos.getY() == blockposition1.getY() && pos.getZ() == blockposition1.getZ()) {
-+                        continue;
-+                    }
-+
-+                    final 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, isSnowySetting(access.getBlockState(blockposition1.above())))); // CraftBukkit
-+                    // Paper end - Perf: optimize dirt and snow spreading
-                     }
-                 }
-             }
diff --git a/feature-patches/1057-Optimise-getChunkAt-calls-for-loaded-chunks.patch b/feature-patches/1057-Optimise-getChunkAt-calls-for-loaded-chunks.patch
deleted file mode 100644
index 9f549ec79e..0000000000
--- a/feature-patches/1057-Optimise-getChunkAt-calls-for-loaded-chunks.patch
+++ /dev/null
@@ -1,60 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf <Spottedleaf@users.noreply.github.com>
-Date: Sat, 25 Jan 2020 17:04:35 -0800
-Subject: [PATCH] Optimise getChunkAt calls for loaded chunks
-
-bypass the need to get a player chunk, then get the either,
-then unwrap it...
-
-diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
-                 return this.getChunk(x, z, leastStatus, create);
-             }, this.mainThreadProcessor).join();
-         } else {
-+            // Paper start - Perf: Optimise getChunkAt calls for loaded chunks
-+            LevelChunk ifLoaded = this.getChunkAtIfLoadedMainThread(x, z);
-+            if (ifLoaded != null) {
-+                return ifLoaded;
-+            }
-+            // Paper end - Perf: Optimise getChunkAt calls for loaded chunks
-             ProfilerFiller gameprofilerfiller = Profiler.get();
- 
-             gameprofilerfiller.incrementCounter("getChunk");
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
-         if (Thread.currentThread() != this.mainThread) {
-             return null;
-         } else {
--            Profiler.get().incrementCounter("getChunkNow");
--            long k = ChunkPos.asLong(chunkX, chunkZ);
--
--            ChunkAccess ichunkaccess;
--
--            for (int l = 0; l < 4; ++l) {
--                if (k == this.lastChunkPos[l] && this.lastChunkStatus[l] == ChunkStatus.FULL) {
--                    ichunkaccess = this.lastChunk[l];
--                    return ichunkaccess instanceof LevelChunk ? (LevelChunk) ichunkaccess : null;
--                }
--            }
--
--            ChunkHolder playerchunk = this.getVisibleChunkIfPresent(k);
--
--            if (playerchunk == null) {
--                return null;
--            } else {
--                ichunkaccess = playerchunk.getChunkIfPresent(ChunkStatus.FULL);
--                if (ichunkaccess != null) {
--                    this.storeInCache(k, ichunkaccess, ChunkStatus.FULL);
--                    if (ichunkaccess instanceof LevelChunk) {
--                        return (LevelChunk) ichunkaccess;
--                    }
--                }
--
--                return null;
--            }
-+            return this.getChunkAtIfLoadedMainThread(chunkX, chunkZ); // Paper - Perf: Optimise getChunkAt calls for loaded chunks
-         }
-     }
- 
diff --git a/feature-patches/1061-Improved-Watchdog-Support.patch b/feature-patches/1061-Improved-Watchdog-Support.patch
deleted file mode 100644
index 3ed20e43df..0000000000
--- a/feature-patches/1061-Improved-Watchdog-Support.patch
+++ /dev/null
@@ -1,455 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Aikar <aikar@aikar.co>
-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/io/papermc/paper/util/LogManagerShutdownThread.java b/src/main/java/io/papermc/paper/util/LogManagerShutdownThread.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/util/LogManagerShutdownThread.java
-@@ -0,0 +0,0 @@
-+package io.papermc.paper.util;
-+
-+public class LogManagerShutdownThread extends Thread {
-+
-+    static LogManagerShutdownThread INSTANCE = new LogManagerShutdownThread();
-+    public static final void hook() {
-+        if (INSTANCE == null) {
-+            throw new IllegalStateException("Cannot re-hook after being unhooked");
-+        }
-+        Runtime.getRuntime().addShutdownHook(INSTANCE);
-+    }
-+
-+    public static final void unhook() {
-+        Runtime.getRuntime().removeShutdownHook(INSTANCE);
-+        INSTANCE = null;
-+    }
-+
-+    private LogManagerShutdownThread() {
-+        super("Log4j2 Shutdown Thread");
-+    }
-+
-+    @Override
-+    public void run() {
-+        org.apache.logging.log4j.LogManager.shutdown();
-+    }
-+}
-diff --git a/src/main/java/net/minecraft/CrashReport.java b/src/main/java/net/minecraft/CrashReport.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/CrashReport.java
-+++ b/src/main/java/net/minecraft/CrashReport.java
-@@ -0,0 +0,0 @@ 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/Main.java b/src/main/java/net/minecraft/server/Main.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/Main.java
-+++ b/src/main/java/net/minecraft/server/Main.java
-@@ -0,0 +0,0 @@ public class Main {
-     @SuppressForbidden(reason = "System.out needed before bootstrap") // CraftBukkit - decompile error
-     @DontObfuscate
-     public static void main(final OptionSet optionset) { // CraftBukkit - replaces main(String[] astring)
-+        io.papermc.paper.util.LogManagerShutdownThread.hook(); // Paper
-         SharedConstants.tryDetectVersion();
-         /* CraftBukkit start - Replace everything
-         OptionParser optionparser = new OptionParser();
-diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/MinecraftServer.java
-+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
-     public java.util.Queue<Runnable> processQueue = new java.util.concurrent.ConcurrentLinkedQueue<Runnable>();
-     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;
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
-     public boolean isIteratingOverLevels = false; // Paper - Throw exception on world create while being ticked
-     private final Set<String> pluginsBlockingSleep = new java.util.HashSet<>(); // Paper - API to allow/disallow tick sleeping
- 
-+    public volatile Thread shutdownThread; // Paper
-+    public volatile boolean abnormalExit = false; // Paper
-+
-     public static <S extends MinecraftServer> S spin(Function<Thread, S> serverFactory) {
-         ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.init(); // Paper - rewrite data converter system
-         AtomicReference<S> atomicreference = new AtomicReference();
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
-         }
-         */
-         // Paper end
-+        io.papermc.paper.util.LogManagerShutdownThread.unhook(); // Paper
-         Runtime.getRuntime().addShutdownHook(new org.bukkit.craftbukkit.util.ServerShutdownThread(this));
-         // CraftBukkit end
-         this.paperConfigurations = services.paperConfigurations(); // Paper - add paper configuration files
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
-     // CraftBukkit start
-     private boolean hasStopped = false;
-     private boolean hasLoggedStop = false; // Paper - Debugging
-+    public volatile boolean hasFullyShutdown = false; // Paper
-     private final Object stopLock = new Object();
-     public final boolean hasStopped() {
-         synchronized (this.stopLock) {
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
-             this.hasStopped = true;
-         }
-         if (!hasLoggedStop && isDebugging()) io.papermc.paper.util.TraceUtil.dumpTraceForThread("Server stopped"); // Paper - Debugging
-+        // Paper start - kill main thread, and kill it hard
-+        shutdownThread = Thread.currentThread();
-+        org.spigotmc.WatchdogThread.doStop(); // Paper
-+        // Paper end
-         // CraftBukkit end
-         if (this.metricsRecorder.isRecording()) {
-             this.cancelRecordingMetrics();
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
-             ca.spottedleaf.moonrise.common.util.MoonriseCommon.haltExecutors();
-         }
-         // Paper end - rewrite chunk system
-+        // Paper start - Improved watchdog support - move final shutdown items here
-+        Util.shutdownExecutors();
-+        try {
-+            net.minecrell.terminalconsole.TerminalConsoleAppender.close(); // Paper - Use TerminalConsoleAppender
-+        } catch (final Exception ignored) {
-+        }
-+        io.papermc.paper.log.CustomLogManager.forceReset(); // Paper - Reset loggers after shutdown
-+        this.onServerExit();
-+        // Paper end - Improved watchdog support - move final shutdown items here
-     }
- 
-     public String getLocalIp() {
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
- 
-     protected void runServer() {
-         try {
-+            long serverStartTime = Util.getNanos(); // Paper
-             if (!this.initServer()) {
-                 throw new IllegalStateException("Failed to initialize server");
-             }
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
- 
-             this.server.spark.enableBeforePlugins(); // Paper - spark
-             // Spigot start
-+            // Paper start - Improved Watchdog Support
-+            LOGGER.info("Running delayed init tasks");
-+            this.server.getScheduler().mainThreadHeartbeat(); // run all 1 tick delay tasks during init,
-+            // this is going to be the first thing the tick process does anyways, so move done and run it after
-+            // everything is init before watchdog tick.
-+            // anything at 3+ won't be caught here but also will trip watchdog....
-+            // tasks are default scheduled at -1 + delay, and first tick will tick at 1
-+            final long actualDoneTimeMs = System.currentTimeMillis() - org.bukkit.craftbukkit.Main.BOOT_TIME.toEpochMilli(); // Paper - Add total time
-+            LOGGER.info("Done ({})! For help, type \"help\"", String.format(java.util.Locale.ROOT, "%.3fs", actualDoneTimeMs / 1000.00D)); // Paper - Add total time
-+            org.spigotmc.WatchdogThread.tick();
-+            // Paper end - Improved Watchdog Support
-             org.spigotmc.WatchdogThread.hasStarted = true; // Paper
-             Arrays.fill( this.recentTps, 20 );
-             // Paper start - further improve server tick loop
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
-                 JvmProfiler.INSTANCE.onServerTick(this.smoothedTickTimeMillis);
-             }
-         } catch (Throwable throwable2) {
-+            // Paper start
-+            if (throwable2 instanceof ThreadDeath) {
-+                MinecraftServer.LOGGER.error("Main thread terminated by WatchDog due to hard crash", throwable2);
-+                return;
-+            }
-+            // Paper end
-             MinecraftServer.LOGGER.error("Encountered an unexpected exception", throwable2);
-             CrashReport crashreport = MinecraftServer.constructOrExtractCrashReport(throwable2);
- 
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
-                     this.services.profileCache().clearExecutor();
-                 }
- 
--                org.spigotmc.WatchdogThread.doStop(); // Spigot
-+                //org.spigotmc.WatchdogThread.doStop(); // Spigot // Paper - move into stop
-                 // CraftBukkit start - Restore terminal to original settings
-                 try {
--                    net.minecrell.terminalconsole.TerminalConsoleAppender.close(); // Paper - Use TerminalConsoleAppender
-+                    //net.minecrell.terminalconsole.TerminalConsoleAppender.close(); // Paper - Move into stop
-                 } catch (Exception ignored) {
-                 }
-                 // CraftBukkit end
--                io.papermc.paper.log.CustomLogManager.forceReset(); // Paper - Reset loggers after shutdown
--                this.onServerExit();
-+                //io.papermc.paper.log.CustomLogManager.forceReset(); // Paper - Reset loggers after shutdown
-+                //this.onServerExit(); // Paper - moved into stop
-             }
- 
-         }
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
- 
-     @Override
-     public TickTask wrapRunnable(Runnable runnable) {
-+        // Paper start - anything that does try to post to main during watchdog crash, run on watchdog
-+        if (this.hasStopped && Thread.currentThread().equals(shutdownThread)) {
-+            runnable.run();
-+            runnable = () -> {};
-+        }
-+        // Paper end
-         return new TickTask(this.tickCount, runnable);
-     }
- 
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
-             this.resources.managers.updateStaticRegistryTags();
-             this.resources.managers.getRecipeManager().finalizeRecipeLoading(this.worldData.enabledFeatures());
-             this.potionBrewing = this.potionBrewing.reload(this.worldData.enabledFeatures()); // Paper - Custom Potion Mixes
--            this.getPlayerList().saveAll();
-+            // Paper start
-+            if (Thread.currentThread() != this.serverThread) {
-+                return;
-+            }
-+            // this.getPlayerList().saveAll(); // Paper - we don't need to save everything, just advancements // TODO Move this to a different patch
-+            for (ServerPlayer player : this.getPlayerList().getPlayers()) {
-+                player.getAdvancements().save();
-+            }
-+            // Paper end
-             this.getPlayerList().reloadResources();
-             this.functionManager.replaceLibrary(this.resources.managers.getFunctionLibrary());
-             this.structureTemplateManager.onResourceManagerReload(this.resources.resourceManager);
-diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
-+++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
-@@ -0,0 +0,0 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
-             long j = Util.getNanos() - i;
-             String s = String.format(Locale.ROOT, "%.3fs", (double) j / 1.0E9D);
- 
--            DedicatedServer.LOGGER.info("Done ({})! For help, type \"help\"", s);
-+            DedicatedServer.LOGGER.info("Done preparing level \"{}\" ({})", this.getLevelIdName(), s); // Paper - clarify startup log messages & add total time
-             if (dedicatedserverproperties.announcePlayerAchievements != null) {
-                 ((GameRules.BooleanValue) this.getGameRules().getRule(GameRules.RULE_ANNOUNCE_ADVANCEMENTS)).set(dedicatedserverproperties.announcePlayerAchievements, this.overworld()); // CraftBukkit - per-world
-             }
-@@ -0,0 +0,0 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
-             // this.remoteStatusListener.stop(); // Paper - don't wait for remote connections
-         }
- 
--        System.exit(0); // CraftBukkit
-+        hasFullyShutdown = true; // Paper
-+        System.exit(this.abnormalExit ? 70 : 0); // CraftBukkit // Paper
-     }
- 
-     @Override
-@@ -0,0 +0,0 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
-     @Override
-     public void stopServer() {
-         super.stopServer();
--        Util.shutdownExecutors();
-+        //Util.shutdownExecutors(); // Paper - moved into super
-         SkullBlockEntity.clear();
-     }
- 
-diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/players/PlayerList.java
-+++ b/src/main/java/net/minecraft/server/players/PlayerList.java
-@@ -0,0 +0,0 @@ public abstract class PlayerList {
-         this.cserver.getPluginManager().callEvent(playerQuitEvent);
-         entityplayer.getBukkitEntity().disconnect(playerQuitEvent.getQuitMessage());
- 
--        entityplayer.doTick(); // SPIGOT-924
-+        if (server.isSameThread()) entityplayer.doTick(); // SPIGOT-924 // Paper - don't tick during emergency shutdowns (Watchdog)
-         // CraftBukkit end
- 
-         // Paper start - Configurable player collision; Remove from collideRule team if needed
-diff --git a/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java b/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java
-+++ b/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java
-@@ -0,0 +0,0 @@ public abstract class BlockableEventLoop<R extends Runnable> implements Profiler
-     public static boolean isNonRecoverable(Throwable exception) {
-         return exception instanceof ReportedException reportedException
-             ? isNonRecoverable(reportedException.getCause())
--            : exception instanceof OutOfMemoryError || exception instanceof StackOverflowError;
-+            : exception instanceof OutOfMemoryError || exception instanceof StackOverflowError || exception instanceof ThreadDeath; // Paper
-     }
- }
-diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/Level.java
-+++ b/src/main/java/net/minecraft/world/level/Level.java
-@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
-@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java
-+++ b/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java
-@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/spigotmc/RestartCommand.java
-+++ b/src/main/java/org/spigotmc/RestartCommand.java
-@@ -0,0 +0,0 @@ public class RestartCommand extends Command
-     // 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/spigotmc/WatchdogThread.java
-+++ b/src/main/java/org/spigotmc/WatchdogThread.java
-@@ -0,0 +0,0 @@ import org.bukkit.Bukkit;
- public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThread // Paper - rewrite chunk system
- {
- 
-+    public static final boolean DISABLE_WATCHDOG = Boolean.getBoolean("disable.watchdog"); // Paper - Improved watchdog support
-     private static WatchdogThread instance;
-     private long timeoutTime;
-     private boolean restart;
-@@ -0,0 +0,0 @@ public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThre
-     {
-         if ( WatchdogThread.instance == null )
-         {
-+            if (timeoutTime <= 0) timeoutTime = 300; // Paper
-             WatchdogThread.instance = new WatchdogThread( timeoutTime * 1000L, restart );
-             WatchdogThread.instance.start();
-         } else
-@@ -0,0 +0,0 @@ public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThre
-             // 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
-@@ -0,0 +0,0 @@ public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThre
- 
-                 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/resources/log4j2.xml
-+++ b/src/main/resources/log4j2.xml
-@@ -0,0 +0,0 @@
- <?xml version="1.0" encoding="UTF-8"?>
--<Configuration status="WARN">
-+<Configuration status="WARN" shutdownHook="disable">
-     <Appenders>
-         <Queue name="ServerGuiConsole">
-             <PatternLayout pattern="[%d{HH:mm:ss} %level]: %msg{nolookups}%n" />
diff --git a/feature-patches/1062-Detail-more-information-in-watchdog-dumps.patch b/feature-patches/1062-Detail-more-information-in-watchdog-dumps.patch
deleted file mode 100644
index 9d5a4deee3..0000000000
--- a/feature-patches/1062-Detail-more-information-in-watchdog-dumps.patch
+++ /dev/null
@@ -1,295 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf <Spottedleaf@users.noreply.github.com>
-Date: Thu, 26 Mar 2020 21:59:32 -0700
-Subject: [PATCH] Detail more information in watchdog dumps
-
-- Dump position, world, velocity, and uuid for currently ticking entities
-- Dump player name, player uuid, position, and world for packet handling
-
-diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/network/Connection.java
-+++ b/src/main/java/net/minecraft/network/Connection.java
-@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
-             if (!(this.packetListener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener)
-                 || loginPacketListener.state != net.minecraft.server.network.ServerLoginPacketListenerImpl.State.VERIFYING
-                 || Connection.joinAttemptsThisTick++ < MAX_PER_TICK) {
-+            // Paper start - detailed watchdog information
-+            net.minecraft.network.protocol.PacketUtils.packetProcessing.push(this.packetListener);
-+            try {
-             tickablepacketlistener.tick();
-+            } finally {
-+                net.minecraft.network.protocol.PacketUtils.packetProcessing.pop();
-+            } // Paper end - detailed watchdog information
-             } // Paper end - Buffer joins to world
-         }
- 
-diff --git a/src/main/java/net/minecraft/network/protocol/PacketUtils.java b/src/main/java/net/minecraft/network/protocol/PacketUtils.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/network/protocol/PacketUtils.java
-+++ b/src/main/java/net/minecraft/network/protocol/PacketUtils.java
-@@ -0,0 +0,0 @@ public class PacketUtils {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
- 
-+    // Paper start - detailed watchdog information
-+    public static final java.util.concurrent.ConcurrentLinkedDeque<PacketListener> packetProcessing = new java.util.concurrent.ConcurrentLinkedDeque<>();
-+    static final java.util.concurrent.atomic.AtomicLong totalMainThreadPacketsProcessed = new java.util.concurrent.atomic.AtomicLong();
-+
-+    public static long getTotalProcessedPackets() {
-+        return totalMainThreadPacketsProcessed.get();
-+    }
-+
-+    public static java.util.List<PacketListener> getCurrentPacketProcessors() {
-+        java.util.List<PacketListener> ret = new java.util.ArrayList<>(4);
-+        for (PacketListener listener : packetProcessing) {
-+            ret.add(listener);
-+        }
-+
-+        return ret;
-+    }
-+    // Paper end - detailed watchdog information
-+
-     public PacketUtils() {}
- 
-     public static <T extends PacketListener> void ensureRunningOnSameThread(Packet<T> packet, T listener, ServerLevel world) throws RunningOnDifferentThreadException {
-@@ -0,0 +0,0 @@ public class PacketUtils {
-     public static <T extends PacketListener> void ensureRunningOnSameThread(Packet<T> packet, T listener, BlockableEventLoop<?> engine) throws RunningOnDifferentThreadException {
-         if (!engine.isSameThread()) {
-             engine.executeIfPossible(() -> {
-+                packetProcessing.push(listener); // Paper - detailed watchdog information
-+                try { // Paper - detailed watchdog information
-                 if (listener instanceof ServerCommonPacketListenerImpl serverCommonPacketListener && serverCommonPacketListener.processedDisconnect) return; // CraftBukkit - Don't handle sync packets for kicked players
-                 if (listener.shouldHandleMessage(packet)) {
-                     try {
-@@ -0,0 +0,0 @@ public class PacketUtils {
-                 } else {
-                     PacketUtils.LOGGER.debug("Ignoring packet due to disconnection: {}", packet);
-                 }
-+                // Paper start - detailed watchdog information
-+                } finally {
-+                    totalMainThreadPacketsProcessed.getAndIncrement();
-+                    packetProcessing.pop();
-+                }
-+                // Paper end - detailed watchdog information
- 
-             });
-             throw RunningOnDifferentThreadException.RUNNING_ON_DIFFERENT_THREAD;
-diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerLevel.java
-+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
- 
-     }
- 
-+    // Paper start - log detailed entity tick information
-+    // TODO replace with varhandle
-+    static final java.util.concurrent.atomic.AtomicReference<Entity> currentlyTickingEntity = new java.util.concurrent.atomic.AtomicReference<>();
-+
-+    public static List<Entity> getCurrentlyTickingEntities() {
-+        Entity ticking = currentlyTickingEntity.get();
-+        List<Entity> ret = java.util.Arrays.asList(ticking == null ? new Entity[0] : new Entity[] { ticking });
-+
-+        return ret;
-+    }
-+    // Paper end - log detailed entity tick information
-+
-     public void tickNonPassenger(Entity entity) {
-+        // Paper start - log detailed entity tick information
-+        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread("Cannot tick an entity off-main");
-+        try {
-+            if (currentlyTickingEntity.get() == null) {
-+                currentlyTickingEntity.lazySet(entity);
-+            }
-+            // Paper end - log detailed entity tick information
-         // Spigot start
-         /*if (!org.spigotmc.ActivationRange.checkIfActive(entity)) { // Paper - comment out EAR 2
-             entity.tickCount++;
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
-             this.tickPassenger(entity, entity1, isActive); // Paper - EAR 2
-         }
- 
-+        // Paper start - log detailed entity tick information
-+        } finally {
-+            if (currentlyTickingEntity.get() == entity) {
-+                currentlyTickingEntity.lazySet(null);
-+            }
-+        }
-+        // Paper end - log detailed entity tick information
-     }
- 
-     private void tickPassenger(Entity vehicle, Entity passenger, boolean isActive) { // Paper - EAR 2
-diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/entity/Entity.java
-+++ b/src/main/java/net/minecraft/world/entity/Entity.java
-@@ -0,0 +0,0 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
-         return this.onGround;
-     }
- 
-+    // Paper start - detailed watchdog information
-+    public final Object posLock = new Object(); // Paper - log detailed entity tick information
-+
-+    private Vec3 moveVector;
-+    private double moveStartX;
-+    private double moveStartY;
-+    private double moveStartZ;
-+
-+    public final Vec3 getMoveVector() {
-+        return this.moveVector;
-+    }
-+
-+    public final double getMoveStartX() {
-+        return this.moveStartX;
-+    }
-+
-+    public final double getMoveStartY() {
-+        return this.moveStartY;
-+    }
-+
-+    public final double getMoveStartZ() {
-+        return this.moveStartZ;
-+    }
-+    // Paper end - detailed watchdog information
-+
-     public void move(MoverType type, Vec3 movement) {
-         final Vec3 originalMovement = movement; // Paper - Expose pre-collision velocity
-+        // Paper start - detailed watchdog information
-+        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread("Cannot move an entity off-main");
-+        synchronized (this.posLock) {
-+            this.moveStartX = this.getX();
-+            this.moveStartY = this.getY();
-+            this.moveStartZ = this.getZ();
-+            this.moveVector = movement;
-+        }
-+        try {
-+        // Paper end - detailed watchdog information
-         if (this.noPhysics) {
-             this.setPos(this.getX() + movement.x, this.getY() + movement.y, this.getZ() + movement.z);
-         } else {
-@@ -0,0 +0,0 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
-                 gameprofilerfiller.pop();
-             }
-         }
-+        // Paper start - detailed watchdog information
-+        } finally {
-+            synchronized (this.posLock) { // Paper
-+                this.moveVector = null;
-+            } // Paper
-+        }
-+        // Paper end - detailed watchdog information
-     }
- 
-     private void applyMovementEmissionAndPlaySound(Entity.MovementEmission moveEffect, Vec3 movement, BlockPos landingPos, BlockState landingState) {
-@@ -0,0 +0,0 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
-     }
- 
-     public void setDeltaMovement(Vec3 velocity) {
-+        synchronized (this.posLock) { // Paper
-         this.deltaMovement = velocity;
-+        } // Paper
-     }
- 
-     public void addDeltaMovement(Vec3 velocity) {
-@@ -0,0 +0,0 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
-         }
-         // Paper end - Fix MC-4
-         if (this.position.x != x || this.position.y != y || this.position.z != z) {
-+            synchronized (this.posLock) { // Paper
-             this.position = new Vec3(x, y, z);
-+            } // Paper
-             int i = Mth.floor(x);
-             int j = Mth.floor(y);
-             int k = Mth.floor(z);
-diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/spigotmc/WatchdogThread.java
-+++ b/src/main/java/org/spigotmc/WatchdogThread.java
-@@ -0,0 +0,0 @@ public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThre
-     private volatile long lastTick;
-     private volatile boolean stopping;
- 
-+    // Paper start - log detailed tick information
-+    private void dumpEntity(net.minecraft.world.entity.Entity entity) {
-+        Logger log = Bukkit.getServer().getLogger();
-+        double posX, posY, posZ;
-+        net.minecraft.world.phys.Vec3 mot;
-+        double moveStartX, moveStartY, moveStartZ;
-+        net.minecraft.world.phys.Vec3 moveVec;
-+        synchronized (entity.posLock) {
-+            posX = entity.getX();
-+            posY = entity.getY();
-+            posZ = entity.getZ();
-+            mot = entity.getDeltaMovement();
-+            moveStartX = entity.getMoveStartX();
-+            moveStartY = entity.getMoveStartY();
-+            moveStartZ = entity.getMoveStartZ();
-+            moveVec = entity.getMoveVector();
-+        }
-+
-+        String entityType = net.minecraft.world.entity.EntityType.getKey(entity.getType()).toString();
-+        java.util.UUID entityUUID = entity.getUUID();
-+        net.minecraft.world.level.Level world = entity.level();
-+
-+        log.log(Level.SEVERE, "Ticking entity: " + entityType + ", entity class: " + entity.getClass().getName());
-+        log.log(Level.SEVERE, "Entity status: removed: " + entity.isRemoved() + ", valid: " + entity.valid + ", alive: " + entity.isAlive() + ", is passenger: " + entity.isPassenger());
-+        log.log(Level.SEVERE, "Entity UUID: " + entityUUID);
-+        log.log(Level.SEVERE, "Position: world: '" + (world == null ? "unknown world?" : world.getWorld().getName()) + "' at location (" + posX + ", " + posY + ", " + posZ + ")");
-+        log.log(Level.SEVERE, "Velocity: " + (mot == null ? "unknown velocity" : mot.toString()) + " (in blocks per tick)");
-+        log.log(Level.SEVERE, "Entity AABB: " + entity.getBoundingBox());
-+        if (moveVec != null) {
-+            log.log(Level.SEVERE, "Move call information: ");
-+            log.log(Level.SEVERE, "Start position: (" + moveStartX + ", " + moveStartY + ", " + moveStartZ + ")");
-+            log.log(Level.SEVERE, "Move vector: " + moveVec.toString());
-+        }
-+    }
-+
-+    private void dumpTickingInfo() {
-+        Logger log = Bukkit.getServer().getLogger();
-+
-+        // ticking entities
-+        for (net.minecraft.world.entity.Entity entity : net.minecraft.server.level.ServerLevel.getCurrentlyTickingEntities()) {
-+            this.dumpEntity(entity);
-+            net.minecraft.world.entity.Entity vehicle = entity.getVehicle();
-+            if (vehicle != null) {
-+                log.log(Level.SEVERE, "Detailing vehicle for above entity:");
-+                this.dumpEntity(vehicle);
-+            }
-+        }
-+
-+        // packet processors
-+        for (net.minecraft.network.PacketListener packetListener : net.minecraft.network.protocol.PacketUtils.getCurrentPacketProcessors()) {
-+            if (packetListener instanceof net.minecraft.server.network.ServerGamePacketListenerImpl) {
-+                net.minecraft.server.level.ServerPlayer player = ((net.minecraft.server.network.ServerGamePacketListenerImpl)packetListener).player;
-+                long totalPackets = net.minecraft.network.protocol.PacketUtils.getTotalProcessedPackets();
-+                if (player == null) {
-+                    log.log(Level.SEVERE, "Handling packet for player connection or ticking player connection (null player): " + packetListener);
-+                    log.log(Level.SEVERE, "Total packets processed on the main thread for all players: " + totalPackets);
-+                } else {
-+                    this.dumpEntity(player);
-+                    net.minecraft.world.entity.Entity vehicle = player.getVehicle();
-+                    if (vehicle != null) {
-+                        log.log(Level.SEVERE, "Detailing vehicle for above entity:");
-+                        this.dumpEntity(vehicle);
-+                    }
-+                    log.log(Level.SEVERE, "Total packets processed on the main thread for all players: " + totalPackets);
-+                }
-+            } else {
-+                log.log(Level.SEVERE, "Handling packet for connection: " + packetListener);
-+            }
-+        }
-+    }
-+    // Paper end - log detailed tick information
-+
-     private WatchdogThread(long timeoutTime, boolean restart)
-     {
-         super( "Paper Watchdog Thread" );
-@@ -0,0 +0,0 @@ public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThre
-                 log.log( Level.SEVERE, "------------------------------" );
-                 log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper
-                 ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(MinecraftServer.getServer(), isLongTimeout); // Paper - rewrite chunk system
-+                this.dumpTickingInfo(); // Paper - log detailed tick information
-                 WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log );
-                 log.log( Level.SEVERE, "------------------------------" );
-                 //
diff --git a/feature-patches/1065-Improve-performance-of-mass-crafts.patch b/feature-patches/1065-Improve-performance-of-mass-crafts.patch
deleted file mode 100644
index c09f2cf750..0000000000
--- a/feature-patches/1065-Improve-performance-of-mass-crafts.patch
+++ /dev/null
@@ -1,92 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Jake Potrebic <jake.m.potrebic@gmail.com>
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/inventory/CraftingContainer.java
-+++ b/src/main/java/net/minecraft/world/inventory/CraftingContainer.java
-@@ -0,0 +0,0 @@ public interface CraftingContainer extends Container, StackedContentsCompatible
-     List<ItemStack> getItems();
- 
-     // CraftBukkit start
--    default RecipeHolder<?> getCurrentRecipe() {
-+    default RecipeHolder<net.minecraft.world.item.crafting.CraftingRecipe> getCurrentRecipe() { // Paper - use correct generic
-         return null;
-     }
- 
--    default void setCurrentRecipe(RecipeHolder<?> recipe) {
-+    default void setCurrentRecipe(RecipeHolder<net.minecraft.world.item.crafting.CraftingRecipe> 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/inventory/CraftingMenu.java
-+++ b/src/main/java/net/minecraft/world/inventory/CraftingMenu.java
-@@ -0,0 +0,0 @@ public class CraftingMenu extends AbstractCraftingMenu {
-         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<RecipeHolder<CraftingRecipe>> 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/inventory/ResultSlot.java
-+++ b/src/main/java/net/minecraft/world/inventory/ResultSlot.java
-@@ -0,0 +0,0 @@ public class ResultSlot extends Slot {
-     private NonNullList<ItemStack> getRemainingItems(CraftingInput input, Level world) {
-         return world instanceof ServerLevel serverLevel
-             ? serverLevel.recipeAccess()
--                .getRecipeFor(RecipeType.CRAFTING, input, serverLevel)
-+                .getRecipeFor(RecipeType.CRAFTING, input, serverLevel, this.craftSlots.getCurrentRecipe()) // Paper - Perf: Improve mass crafting; check last recipe used first
-                 .map(recipe -> recipe.value().getRemainingItems(input))
-                 .orElseGet(() -> copyAllInputItems(input))
-             : CraftingRecipe.defaultCraftingReminder(input);
-diff --git a/src/main/java/net/minecraft/world/inventory/TransientCraftingContainer.java b/src/main/java/net/minecraft/world/inventory/TransientCraftingContainer.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/inventory/TransientCraftingContainer.java
-+++ b/src/main/java/net/minecraft/world/inventory/TransientCraftingContainer.java
-@@ -0,0 +0,0 @@ public class TransientCraftingContainer implements CraftingContainer {
- 
-     // CraftBukkit start - add fields
-     public List<HumanEntity> transaction = new java.util.ArrayList<HumanEntity>();
--    private RecipeHolder<?> currentRecipe;
-+    private RecipeHolder<net.minecraft.world.item.crafting.CraftingRecipe> currentRecipe; // Paper - use correct generic
-     public Container resultInventory;
-     private Player owner;
-     private int maxStack = MAX_STACK;
-@@ -0,0 +0,0 @@ public class TransientCraftingContainer implements CraftingContainer {
-     }
- 
-     @Override
--    public RecipeHolder<?> getCurrentRecipe() {
-+    public RecipeHolder<net.minecraft.world.item.crafting.CraftingRecipe> getCurrentRecipe() { // Paper - use correct generic
-         return this.currentRecipe;
-     }
- 
-     @Override
--    public void setCurrentRecipe(RecipeHolder<?> currentRecipe) {
-+    public void setCurrentRecipe(RecipeHolder<net.minecraft.world.item.crafting.CraftingRecipe> currentRecipe) { // Paper - use correct generic
-         this.currentRecipe = currentRecipe;
-     }
- 
diff --git a/feature-patches/1069-Lag-compensation-ticks.patch b/feature-patches/1069-Lag-compensation-ticks.patch
deleted file mode 100644
index a2f30eb99d..0000000000
--- a/feature-patches/1069-Lag-compensation-ticks.patch
+++ /dev/null
@@ -1,129 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf <Spottedleaf@users.noreply.github.com>
-Date: Sat, 23 Sep 2023 22:05:35 -0700
-Subject: [PATCH] Lag compensation ticks
-
-Areas affected by lag comepnsation:
- - Block breaking and destroying
- - Eating food items
-
-diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/MinecraftServer.java
-+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
- 
-     public volatile Thread shutdownThread; // Paper
-     public volatile boolean abnormalExit = false; // Paper
-+    public static final long SERVER_INIT = System.nanoTime(); // Paper - Lag compensation
- 
-     public static <S extends MinecraftServer> S spin(Function<Thread, S> serverFactory) {
-         ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.init(); // Paper - rewrite data converter system
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
-             worldserver.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 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
-+            worldserver.updateLagCompensationTick(); // Paper - lag compensation
- 
-             gameprofilerfiller.push(() -> {
-                 String s = String.valueOf(worldserver);
-diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerLevel.java
-+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
-         );
-     }
-     // Paper end - chunk tick iteration
-+    // Paper start - lag compensation
-+    private long lagCompensationTick = net.minecraft.server.MinecraftServer.SERVER_INIT;
-+
-+    public long getLagCompensationTick() {
-+        return this.lagCompensationTick;
-+    }
-+
-+    public void updateLagCompensationTick() {
-+        this.lagCompensationTick = (System.nanoTime() - net.minecraft.server.MinecraftServer.SERVER_INIT) / (java.util.concurrent.TimeUnit.MILLISECONDS.toNanos(50L));
-+    }
-+    // Paper end - lag compensation
- 
-     // Add env and gen to constructor, IWorldDataServer -> WorldDataServer
-     public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey<Level> resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List<CustomSpawner> list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) {
-diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java
-+++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java
-@@ -0,0 +0,0 @@ public class ServerPlayerGameMode {
-     }
- 
-     public void tick() {
--        this.gameTicks = MinecraftServer.currentTick; // CraftBukkit;
-+        this.gameTicks = (int)this.level.getLagCompensationTick(); // CraftBukkit; // Paper - lag compensation
-         BlockState iblockdata;
- 
-         if (this.hasDelayedDestroy) {
-diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/entity/LivingEntity.java
-+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java
-@@ -0,0 +0,0 @@ public abstract class LivingEntity extends Entity implements Attackable {
-         this.resendPossiblyDesyncedDataValues(java.util.List.of(DATA_LIVING_ENTITY_FLAGS), serverPlayer);
-     }
-     // Paper end - Properly cancel usable items
-+    // Paper start - lag compensate eating
-+    protected long eatStartTime;
-+    protected int totalEatTimeTicks;
-+    // Paper end - lag compensate eating
-     private void updatingUsingItem() {
-         if (this.isUsingItem()) {
-             if (ItemStack.isSameItem(this.getItemInHand(this.getUsedItemHand()), this.useItem)) {
-@@ -0,0 +0,0 @@ public abstract class LivingEntity extends Entity implements Attackable {
- 
-     protected void updateUsingItem(ItemStack stack) {
-         stack.onUseTick(this.level(), this, this.getUseItemRemainingTicks());
--        if (--this.useItemRemaining == 0 && !this.level().isClientSide && !stack.useOnRelease()) {
-+        // Paper start - lag compensate eating
-+        // we add 1 to the expected time to avoid lag compensating when we should not
-+        final boolean shouldLagCompensate = this.useItem.has(DataComponents.FOOD) && this.eatStartTime != -1 && (System.nanoTime() - this.eatStartTime) > ((1L + this.totalEatTimeTicks) * 50L * (1000L * 1000L));
-+        if ((--this.useItemRemaining == 0 || shouldLagCompensate) && !this.level().isClientSide && !stack.useOnRelease()) {
-+            this.useItemRemaining = 0;
-+            // Paper end - lag compensate eating
-             this.completeUsingItem();
-         }
- 
-@@ -0,0 +0,0 @@ public abstract class LivingEntity extends Entity implements Attackable {
- 
-         if (!itemstack.isEmpty() && !this.isUsingItem() || forceUpdate) { // Paper - Prevent consuming the wrong itemstack
-             this.useItem = itemstack;
--            this.useItemRemaining = itemstack.getUseDuration(this);
-+            // Paper start - lag compensate eating
-+            this.useItemRemaining = this.totalEatTimeTicks = itemstack.getUseDuration(this);
-+            this.eatStartTime = System.nanoTime();
-+            // Paper end - lag compensate eating
-             if (!this.level().isClientSide) {
-                 this.setLivingEntityFlag(1, true);
-                 this.setLivingEntityFlag(2, hand == InteractionHand.OFF_HAND);
-@@ -0,0 +0,0 @@ public abstract class LivingEntity extends Entity implements Attackable {
-                 }
-             } else if (!this.isUsingItem() && !this.useItem.isEmpty()) {
-                 this.useItem = ItemStack.EMPTY;
--                this.useItemRemaining = 0;
-+                // Paper start - lag compensate eating
-+                this.useItemRemaining = this.totalEatTimeTicks = 0;
-+                this.eatStartTime = -1L;
-+                // Paper end - lag compensate eating
-             }
-         }
- 
-@@ -0,0 +0,0 @@ public abstract class LivingEntity extends Entity implements Attackable {
-         }
- 
-         this.useItem = ItemStack.EMPTY;
--        this.useItemRemaining = 0;
-+        // Paper start - lag compensate eating
-+        this.useItemRemaining = this.totalEatTimeTicks = 0;
-+        this.eatStartTime = -1L;
-+        // Paper end - lag compensate eating
-     }
- 
-     public boolean isBlocking() {
diff --git a/feature-patches/1071-Optional-per-player-mob-spawns.patch b/feature-patches/1071-Optional-per-player-mob-spawns.patch
deleted file mode 100644
index dda7294b2a..0000000000
--- a/feature-patches/1071-Optional-per-player-mob-spawns.patch
+++ /dev/null
@@ -1,232 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: kickash32 <kickash32@gmail.com>
-Date: Mon, 19 Aug 2019 01:27:58 +0500
-Subject: [PATCH] Optional per player mob spawns
-
-
-diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ChunkMap.java
-+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-     }
- 
-     // Paper start
-+    // Paper start - Optional per player mob spawns
-+    public void updatePlayerMobTypeMap(final Entity entity) {
-+        if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) {
-+            return;
-+        }
-+        final int index = entity.getType().getCategory().ordinal();
-+
-+        final ca.spottedleaf.moonrise.common.list.ReferenceList<ServerPlayer> inRange =
-+            this.level.moonrise$getNearbyPlayers().getPlayers(entity.chunkPosition(), ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.TICK_VIEW_DISTANCE);
-+        if (inRange == null) {
-+            return;
-+        }
-+        final ServerPlayer[] backingSet = inRange.getRawDataUnchecked();
-+        for (int i = 0, len = inRange.size(); i < len; i++) {
-+            ++(backingSet[i].mobCounts[index]);
-+        }
-+    }
-     public int getMobCountNear(final ServerPlayer player, final net.minecraft.world.entity.MobCategory mobCategory) {
--        return -1;
-+        return player.mobCounts[mobCategory.ordinal()];
-+        // Paper end - Optional per player mob spawns
-     }
-     // Paper end
- 
-diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
-                     gameprofilerfiller.popPush("shuffleChunks");
-                     // Paper start - chunk tick iteration optimisation
-                     this.shuffleRandom.setSeed(this.level.random.nextLong());
--                    Util.shuffle(list, this.shuffleRandom);
-+                    if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) Util.shuffle(list, this.shuffleRandom); // Paper - Optional per player mob spawns; do not need this when per-player is enabled
-                     // Paper end - chunk tick iteration optimisation
-                     this.tickChunks(gameprofilerfiller, j, list);
-                     gameprofilerfiller.pop();
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
-     private void tickChunks(ProfilerFiller profiler, long timeDelta, List<LevelChunk> chunks) {
-         profiler.popPush("naturalSpawnCount");
-         int j = this.distanceManager.getNaturalSpawnChunkCount();
--        NaturalSpawner.SpawnState spawnercreature_d = NaturalSpawner.createState(j, this.level.getAllEntities(), this::getFullChunk, new LocalMobCapCalculator(this.chunkMap));
-+        // Paper start - Optional per player mob spawns
-+        final int naturalSpawnChunkCount = j;
-+        NaturalSpawner.SpawnState spawnercreature_d; // moved down
-+        if ((this.spawnFriendlies || this.spawnEnemies) && this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { // don't count mobs when animals and monsters are disabled
-+            // re-set mob counts
-+            for (ServerPlayer player : this.level.players) {
-+                Arrays.fill(player.mobCounts, 0);
-+            }
-+            spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, null, true);
-+        } else {
-+            spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false);
-+        }
-+        // Paper end - Optional per player mob spawns
- 
-         this.lastSpawnState = spawnercreature_d;
-         profiler.popPush("spawnAndTick");
-diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
-+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
-@@ -0,0 +0,0 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple
-     public boolean queueHealthUpdatePacket;
-     public net.minecraft.network.protocol.game.ClientboundSetHealthPacket queuedHealthUpdatePacket;
-     // Paper end - cancellable death event
-+    // Paper start - Optional per player mob spawns
-+    public static final int MOBCATEGORY_TOTAL_ENUMS = net.minecraft.world.entity.MobCategory.values().length;
-+    public final int[] mobCounts = new int[MOBCATEGORY_TOTAL_ENUMS]; // Paper
-+    // Paper end - Optional per player mob spawns
- 
-     // CraftBukkit start
-     public CraftPlayer.TransferCookieConnection transferCookieConnection;
-diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java
-+++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java
-@@ -0,0 +0,0 @@ public final class NaturalSpawner {
-     private NaturalSpawner() {}
- 
-     public static NaturalSpawner.SpawnState createState(int spawningChunkCount, Iterable<Entity> entities, NaturalSpawner.ChunkGetter chunkSource, LocalMobCapCalculator densityCapper) {
-+        // Paper start - Optional per player mob spawns
-+        return createState(spawningChunkCount, entities, chunkSource, densityCapper, false);
-+    }
-+
-+    public static NaturalSpawner.SpawnState createState(int spawningChunkCount, Iterable<Entity> entities, NaturalSpawner.ChunkGetter chunkSource, LocalMobCapCalculator densityCapper, boolean countMobs) {
-+        // Paper end - Optional per player mob spawns
-         PotentialCalculator spawnercreatureprobabilities = new PotentialCalculator();
-         Object2IntOpenHashMap<MobCategory> object2intopenhashmap = new Object2IntOpenHashMap();
-         Iterator iterator = entities.iterator();
-@@ -0,0 +0,0 @@ public final class NaturalSpawner {
-                         spawnercreatureprobabilities.addCharge(entity.blockPosition(), biomesettingsmobs_b.charge());
-                     }
- 
--                    if (entity instanceof Mob) {
-+                    if (densityCapper != null && entity instanceof Mob) { // Paper - Optional per player mob spawns
-                         densityCapper.addMob(chunk.getPos(), enumcreaturetype);
-                     }
- 
-                     object2intopenhashmap.addTo(enumcreaturetype, 1);
-+                    // Paper start - Optional per player mob spawns
-+                    if (countMobs) {
-+                        chunk.level.getChunkSource().chunkMap.updatePlayerMobTypeMap(entity);
-+                    }
-+                    // Paper end - Optional per player mob spawns
-                 });
-             }
-         }
-@@ -0,0 +0,0 @@ public final class NaturalSpawner {
-                 continue;
-             }
- 
--            if ((flag || !enumcreaturetype.isFriendly()) && (flag1 || enumcreaturetype.isFriendly()) && (flag2 || !enumcreaturetype.isPersistent()) && spawnercreature_d.canSpawnForCategoryGlobal(enumcreaturetype, limit)) {
-+            if ((flag || !enumcreaturetype.isFriendly()) && (flag1 || enumcreaturetype.isFriendly()) && (flag2 || !enumcreaturetype.isPersistent()) && (worldserver.paperConfig().entities.spawning.perPlayerMobSpawns || spawnercreature_d.canSpawnForCategoryGlobal(enumcreaturetype, limit))) { // Paper - Optional per player mob spawns; remove global check, check later during the local one
-                 // CraftBukkit end
-                 list.add(enumcreaturetype);
-             }
-@@ -0,0 +0,0 @@ public final class NaturalSpawner {
-         while (iterator.hasNext()) {
-             MobCategory enumcreaturetype = (MobCategory) iterator.next();
- 
--            if (info.canSpawnForCategoryLocal(enumcreaturetype, chunk.getPos())) {
-+            // Paper start - Optional per player mob spawns
-+            final boolean canSpawn;
-+            int maxSpawns = Integer.MAX_VALUE;
-+            if (world.paperConfig().entities.spawning.perPlayerMobSpawns) {
-+                // Copied from getFilteredSpawningCategories
-+                int limit = enumcreaturetype.getMaxInstancesPerChunk();
-+                SpawnCategory spawnCategory = CraftSpawnCategory.toBukkit(enumcreaturetype);
-+                if (CraftSpawnCategory.isValidForLimits(spawnCategory)) {
-+                    limit = world.getWorld().getSpawnLimit(spawnCategory);
-+                }
-+
-+                // Apply per-player limit
-+                int minDiff = Integer.MAX_VALUE;
-+                final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerPlayer> inRange =
-+                    world.moonrise$getNearbyPlayers().getPlayers(chunk.getPos(), ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.TICK_VIEW_DISTANCE);
-+                if (inRange != null) {
-+                    final net.minecraft.server.level.ServerPlayer[] backingSet = inRange.getRawDataUnchecked();
-+                    for (int k = 0, len = inRange.size(); k < len; k++) {
-+                        minDiff = Math.min(limit - world.getChunkSource().chunkMap.getMobCountNear(backingSet[k], enumcreaturetype), minDiff);
-+                    }
-+                }
-+
-+                maxSpawns = (minDiff == Integer.MAX_VALUE) ? 0 : minDiff;
-+                canSpawn = maxSpawns > 0;
-+            } else {
-+                canSpawn = info.canSpawnForCategoryLocal(enumcreaturetype, chunk.getPos());
-+            }
-+            if (canSpawn) {
-+                // Paper end - Optional per player mob spawns
-                 Objects.requireNonNull(info);
-                 NaturalSpawner.SpawnPredicate spawnercreature_c = info::canSpawn;
- 
-                 Objects.requireNonNull(info);
--                NaturalSpawner.spawnCategoryForChunk(enumcreaturetype, world, chunk, spawnercreature_c, info::afterSpawn);
-+                // Paper start - Optional per player mob spawns
-+                NaturalSpawner.spawnCategoryForChunk(enumcreaturetype, world, chunk, spawnercreature_c, info::afterSpawn,
-+                    maxSpawns, world.paperConfig().entities.spawning.perPlayerMobSpawns ? world.getChunkSource().chunkMap::updatePlayerMobTypeMap : null);
-+                // Paper end - Optional per player mob spawns
-             }
-         }
- 
-@@ -0,0 +0,0 @@ public final class NaturalSpawner {
-     // Paper end - Add mobcaps commands
- 
-     public static void spawnCategoryForChunk(MobCategory group, ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner) {
-+        // Paper start - Optional per player mob spawns
-+        spawnCategoryForChunk(group, world, chunk, checker, runner, Integer.MAX_VALUE, null);
-+    }
-+    public static void spawnCategoryForChunk(MobCategory group, ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner, int maxSpawns, Consumer<Entity> trackEntity) {
-+        // Paper end - Optional per player mob spawns
-         BlockPos blockposition = NaturalSpawner.getRandomPosWithin(world, chunk);
- 
-         if (blockposition.getY() >= world.getMinY() + 1) {
--            NaturalSpawner.spawnCategoryForPosition(group, world, chunk, blockposition, checker, runner);
-+            NaturalSpawner.spawnCategoryForPosition(group, world, chunk, blockposition, checker, runner, maxSpawns, trackEntity); // Paper - Optional per player mob spawns
-         }
-     }
- 
-@@ -0,0 +0,0 @@ public final class NaturalSpawner {
-         });
-     }
- 
-+    // Paper start - Optional per player mob spawns
-     public static void spawnCategoryForPosition(MobCategory group, ServerLevel world, ChunkAccess chunk, BlockPos pos, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner) {
-+        spawnCategoryForPosition(group, world,chunk, pos, checker, runner, Integer.MAX_VALUE, null);
-+    }
-+    public static void spawnCategoryForPosition(MobCategory group, ServerLevel world, ChunkAccess chunk, BlockPos pos, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner, int maxSpawns, Consumer<Entity> trackEntity) {
-+    // Paper end - Optional per player mob spawns
-         StructureManager structuremanager = world.structureManager();
-         ChunkGenerator chunkgenerator = world.getChunkSource().getGenerator();
-         int i = pos.getY();
-@@ -0,0 +0,0 @@ public final class NaturalSpawner {
-                                                 ++j;
-                                                 ++k1;
-                                                 runner.run(entityinsentient, chunk);
-+                                                // Paper start - Optional per player mob spawns
-+                                                if (trackEntity != null) {
-+                                                    trackEntity.accept(entityinsentient);
-+                                                }
-+                                                // Paper end - Optional per player mob spawns
-                                             }
-                                             // CraftBukkit end
--                                            if (j >= entityinsentient.getMaxSpawnClusterSize()) {
-+                                            if (j >= entityinsentient.getMaxSpawnClusterSize() || j >= maxSpawns) { // Paper - Optional per player mob spawns
-                                                 return;
-                                             }
- 
-@@ -0,0 +0,0 @@ public final class NaturalSpawner {
-             MobCategory enumcreaturetype = entitytypes.getCategory();
- 
-             this.mobCategoryCounts.addTo(enumcreaturetype, 1);
--            this.localMobCapCalculator.addMob(new ChunkPos(blockposition), enumcreaturetype);
-+            if (this.localMobCapCalculator != null) this.localMobCapCalculator.addMob(new ChunkPos(blockposition), enumcreaturetype); // Paper - Optional per player mob spawns
-         }
- 
-         public int getSpawnableChunkCount() {
diff --git a/feature-patches/1072-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch b/feature-patches/1072-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch
deleted file mode 100644
index 46e601aaff..0000000000
--- a/feature-patches/1072-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch
+++ /dev/null
@@ -1,89 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: kickash32 <kickash32@gmail.com>
-Date: Mon, 5 Apr 2021 01:42:35 -0400
-Subject: [PATCH] Improve cancelling PreCreatureSpawnEvent with per player mob
- spawns
-
-
-diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ChunkMap.java
-+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-             ++(backingSet[i].mobCounts[index]);
-         }
-     }
-+    // Paper start - per player mob count backoff
-+    public void updateFailurePlayerMobTypeMap(int chunkX, int chunkZ, net.minecraft.world.entity.MobCategory mobCategory) {
-+        if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) {
-+            return;
-+        }
-+        int idx = mobCategory.ordinal();
-+        final ca.spottedleaf.moonrise.common.list.ReferenceList<ServerPlayer> inRange =
-+            this.level.moonrise$getNearbyPlayers().getPlayersByChunk(chunkX, chunkZ, ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.TICK_VIEW_DISTANCE);
-+        if (inRange == null) {
-+            return;
-+        }
-+        final ServerPlayer[] backingSet = inRange.getRawDataUnchecked();
-+        for (int i = 0, len = inRange.size(); i < len; i++) {
-+            ++(backingSet[i].mobBackoffCounts[idx]);
-+        }
-+    }
-+    // Paper end - per player mob count backoff
-     public int getMobCountNear(final ServerPlayer player, final net.minecraft.world.entity.MobCategory mobCategory) {
--        return player.mobCounts[mobCategory.ordinal()];
-+        return player.mobCounts[mobCategory.ordinal()] + player.mobBackoffCounts[mobCategory.ordinal()]; // Paper - per player mob count backoff
-         // Paper end - Optional per player mob spawns
-     }
-     // Paper end
-diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
-         if ((this.spawnFriendlies || this.spawnEnemies) && this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { // don't count mobs when animals and monsters are disabled
-             // re-set mob counts
-             for (ServerPlayer player : this.level.players) {
--                Arrays.fill(player.mobCounts, 0);
-+                // Paper start - per player mob spawning backoff
-+                for (int ii = 0; ii < ServerPlayer.MOBCATEGORY_TOTAL_ENUMS; ii++) {
-+                    player.mobCounts[ii] = 0;
-+
-+                    int newBackoff = player.mobBackoffCounts[ii] - 1; // TODO make configurable bleed // TODO use nonlinear algorithm?
-+                    if (newBackoff < 0) {
-+                        newBackoff = 0;
-+                    }
-+                    player.mobBackoffCounts[ii] = newBackoff;
-+                }
-+                // Paper end - per player mob spawning backoff
-             }
-             spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, null, true);
-         } else {
-diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
-+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
-@@ -0,0 +0,0 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple
-     public static final int MOBCATEGORY_TOTAL_ENUMS = net.minecraft.world.entity.MobCategory.values().length;
-     public final int[] mobCounts = new int[MOBCATEGORY_TOTAL_ENUMS]; // Paper
-     // Paper end - Optional per player mob spawns
-+    public final int[] mobBackoffCounts = new int[MOBCATEGORY_TOTAL_ENUMS]; // Paper - per player mob count backoff
- 
-     // CraftBukkit start
-     public CraftPlayer.TransferCookieConnection transferCookieConnection;
-diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java
-+++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java
-@@ -0,0 +0,0 @@ public final class NaturalSpawner {
- 
-                                     // Paper start - PreCreatureSpawnEvent
-                                     PreSpawnStatus doSpawning = isValidSpawnPostitionForType(world, group, structuremanager, chunkgenerator, biomesettingsmobs_c, blockposition_mutableblockposition, d2);
-+                                    // Paper start - per player mob count backoff
-+                                    if (doSpawning == PreSpawnStatus.ABORT || doSpawning == PreSpawnStatus.CANCELLED) {
-+                                        world.getChunkSource().chunkMap.updateFailurePlayerMobTypeMap(blockposition_mutableblockposition.getX() >> 4, blockposition_mutableblockposition.getZ() >> 4, group);
-+                                    }
-+                                    // Paper end - per player mob count backoff
-                                     if (doSpawning == PreSpawnStatus.ABORT) {
-                                         return;
-                                     }
diff --git a/feature-patches/1076-Implement-chunk-view-API.patch b/feature-patches/1076-Implement-chunk-view-API.patch
deleted file mode 100644
index ede618a2ed..0000000000
--- a/feature-patches/1076-Implement-chunk-view-API.patch
+++ /dev/null
@@ -1,51 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Flo0 <flo.roma@web.de>
-Date: Thu, 5 Dec 2024 12:15:07 +0100
-Subject: [PATCH] Implement chunk view API
-
-
-diff --git a/src/main/java/io/papermc/paper/FeatureHooks.java b/src/main/java/io/papermc/paper/FeatureHooks.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/io/papermc/paper/FeatureHooks.java
-+++ b/src/main/java/io/papermc/paper/FeatureHooks.java
-@@ -0,0 +0,0 @@ package io.papermc.paper;
- import io.papermc.paper.command.PaperSubcommand;
- import io.papermc.paper.command.subcommands.ChunkDebugCommand;
- import io.papermc.paper.command.subcommands.FixLightCommand;
-+import it.unimi.dsi.fastutil.longs.LongIterator;
- import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
--import it.unimi.dsi.fastutil.longs.LongSet;
- import it.unimi.dsi.fastutil.longs.LongSets;
- import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
- import it.unimi.dsi.fastutil.objects.ObjectSet;
-@@ -0,0 +0,0 @@ public final class FeatureHooks {
-     }
- 
-     public static Set<Long> getSentChunkKeys(final ServerPlayer player) {
--        final LongSet keys = new LongOpenHashSet();
--        player.getChunkTrackingView().forEach(pos -> keys.add(pos.longKey));
--        return LongSets.unmodifiable(keys);
-+        return LongSets.unmodifiable(player.moonrise$getChunkLoader().getSentChunksRaw().clone());
-     }
- 
-     public static Set<Chunk> getSentChunks(final ServerPlayer player) {
--        final ObjectSet<Chunk> chunks = new ObjectOpenHashSet<>();
-+        final LongOpenHashSet rawChunkKeys = player.moonrise$getChunkLoader().getSentChunksRaw();
-+        final ObjectSet<org.bukkit.Chunk> chunks = new ObjectOpenHashSet<>(rawChunkKeys.size());
-         final World world = player.serverLevel().getWorld();
--        player.getChunkTrackingView().forEach(pos -> {
--            final org.bukkit.Chunk chunk = world.getChunkAt(pos.longKey);
--            chunks.add(chunk);
--        });
-+        final LongIterator iter = player.moonrise$getChunkLoader().getSentChunksRaw().longIterator();
-+        while (iter.hasNext()) {
-+            chunks.add(world.getChunkAt(iter.nextLong(), false));
-+        }
-         return ObjectSets.unmodifiable(chunks);
-     }
- 
-     public static boolean isChunkSent(final ServerPlayer player, final long chunkKey) {
--        return player.getChunkTrackingView().contains(new ChunkPos(chunkKey));
-+        return player.moonrise$getChunkLoader().getSentChunksRaw().contains(chunkKey);
-     }
- }
diff --git a/gradle.properties b/gradle.properties
index 3bf9414dba..27969fea17 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -4,8 +4,7 @@ mcVersion=1.21.4
 
 # Set to true while updating Minecraft version
 updatingMinecraft=false
-#cleanPaperRepo=~/IdeaProjects/Paper/Paper-Server/src/main/java
-updateTaskListIssue=https://github.com/PaperMC/testing/issues/2
+updateTaskListIssue=https://github.com/PaperMC/Paper/issues/11736
 
 org.gradle.configuration-cache=true
 org.gradle.caching=true
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index e2847c8200..cea7a793a8 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
 networkTimeout=10000
 validateDistributionUrl=true
 zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
index f5feea6d6b..f3b75f3b0d 100755
--- a/gradlew
+++ b/gradlew
@@ -86,8 +86,7 @@ done
 # shellcheck disable=SC2034
 APP_BASE_NAME=${0##*/}
 # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
-APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
-' "$PWD" ) || exit
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
 
 # Use the maximum available, or set MAX_FD != -1 to use that value.
 MAX_FD=maximum
diff --git a/paper-api/build.gradle.kts b/paper-api/build.gradle.kts
index 832a2b3e86..7dd682b4b9 100644
--- a/paper-api/build.gradle.kts
+++ b/paper-api/build.gradle.kts
@@ -39,7 +39,7 @@ abstract class MockitoAgentProvider : CommandLineArgumentProvider {
 // Paper end - configure mockito agent that is needed in newer java versions
 
 dependencies {
-    api("com.mojang:brigadier:1.2.9") // Paper - Brigadier command api
+    api("com.mojang:brigadier:1.3.10") // Paper - Brigadier command api
     // api dependencies are listed transitively to API consumers
     api("com.google.guava:guava:33.3.1-jre")
     api("com.google.code.gson:gson:2.11.0")
@@ -92,6 +92,7 @@ dependencies {
     testImplementation("org.mockito:mockito-core:5.14.1")
     testImplementation("org.ow2.asm:asm-tree:9.7.1")
     mockitoAgent("org.mockito:mockito-core:5.14.1") { isTransitive = false } // Paper - configure mockito agent that is needed in newer java versions
+    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
 }
 
 // Paper start
@@ -121,8 +122,13 @@ configurations {
             }
             outgoing {
                 capability(mainCapability)
+                // Paper-MojangAPI has been merged into Paper-API
                 capability("io.papermc.paper:paper-mojangapi:${project.version}")
                 capability("com.destroystokyo.paper:paper-mojangapi:${project.version}")
+                // Conflict with old coordinates
+                capability("com.destroystokyo.paper:paper-api:${project.version}")
+                capability("org.spigotmc:spigot-api:${project.version}")
+                capability("org.bukkit:bukkit:${project.version}")
             }
         }
     }
diff --git a/paper-api/src/main/java/com/destroystokyo/paper/event/block/BlockDestroyEvent.java b/paper-api/src/main/java/com/destroystokyo/paper/event/block/BlockDestroyEvent.java
index 122ccdef02..78084f4c90 100644
--- a/paper-api/src/main/java/com/destroystokyo/paper/event/block/BlockDestroyEvent.java
+++ b/paper-api/src/main/java/com/destroystokyo/paper/event/block/BlockDestroyEvent.java
@@ -55,7 +55,7 @@ public class BlockDestroyEvent extends BlockExpEvent implements Cancellable {
      * @param effectBlock block effect
      */
     public void setEffectBlock(final BlockData effectBlock) {
-        this.effectBlock = effectBlock;
+        this.effectBlock = effectBlock.clone();
     }
 
     /**
diff --git a/paper-api/src/main/java/com/destroystokyo/paper/event/player/PlayerJumpEvent.java b/paper-api/src/main/java/com/destroystokyo/paper/event/player/PlayerJumpEvent.java
index 1d07c3d6bf..e3d4c3f912 100644
--- a/paper-api/src/main/java/com/destroystokyo/paper/event/player/PlayerJumpEvent.java
+++ b/paper-api/src/main/java/com/destroystokyo/paper/event/player/PlayerJumpEvent.java
@@ -79,7 +79,7 @@ public class PlayerJumpEvent extends PlayerEvent implements Cancellable {
     public void setFrom(final Location from) {
         Preconditions.checkArgument(from != null, "Cannot use null from location!");
         Preconditions.checkArgument(from.getWorld() != null, "Cannot use from location with null world!");
-        this.from = from;
+        this.from = from.clone();
     }
 
     /**
diff --git a/paper-api/src/main/java/com/destroystokyo/paper/event/player/PlayerSetSpawnEvent.java b/paper-api/src/main/java/com/destroystokyo/paper/event/player/PlayerSetSpawnEvent.java
index 41d42d73cf..1688c2c4ee 100644
--- a/paper-api/src/main/java/com/destroystokyo/paper/event/player/PlayerSetSpawnEvent.java
+++ b/paper-api/src/main/java/com/destroystokyo/paper/event/player/PlayerSetSpawnEvent.java
@@ -66,7 +66,7 @@ public class PlayerSetSpawnEvent extends PlayerEvent implements Cancellable {
      * @param location the spawn location, or {@code null} to remove the spawn location
      */
     public void setLocation(final @Nullable Location location) {
-        this.location = location;
+        this.location = location != null ? location.clone() : null;
     }
 
     /**
diff --git a/paper-api/src/main/java/io/papermc/paper/command/brigadier/argument/CustomArgumentType.java b/paper-api/src/main/java/io/papermc/paper/command/brigadier/argument/CustomArgumentType.java
index 91d40ef0bd..14be89fab5 100644
--- a/paper-api/src/main/java/io/papermc/paper/command/brigadier/argument/CustomArgumentType.java
+++ b/paper-api/src/main/java/io/papermc/paper/command/brigadier/argument/CustomArgumentType.java
@@ -37,10 +37,31 @@ public interface CustomArgumentType<T, N> extends ArgumentType<T> {
      * @param reader string reader input
      * @return parsed value
      * @throws CommandSyntaxException if an error occurs while parsing
+     * @see #parse(StringReader, Object)
      */
     @Override
     T parse(final StringReader reader) throws CommandSyntaxException;
 
+    /**
+     * Parses the argument into the custom type ({@code T}). Keep in mind
+     * that this parsing will be done on the server. This means that if
+     * you throw a {@link CommandSyntaxException} during parsing, this
+     * will only show up to the user after the user has executed the command
+     * not while they are still entering it.
+     * <p>
+     * This method provides the command source for additional context when parsing. You
+     * may have to do your own {@code instanceof} checks for {@link io.papermc.paper.command.brigadier.CommandSourceStack}.
+     *
+     * @param reader string reader input
+     * @param source source of the command
+     * @return parsed value
+     * @throws CommandSyntaxException if an error occurs while parsing
+     */
+    @Override
+    default <S> T parse(final StringReader reader, final S source) throws CommandSyntaxException {
+        return ArgumentType.super.parse(reader, source);
+    }
+
     /**
      * Gets the native type that this argument uses,
      * the type that is sent to the client.
@@ -95,13 +116,35 @@ public interface CustomArgumentType<T, N> extends ArgumentType<T> {
             return this.convert(this.getNativeType().parse(reader));
         }
 
+        @ApiStatus.NonExtendable
+        @Override
+        default <S> T parse(final StringReader reader, final S source) throws CommandSyntaxException {
+            return this.convert(this.getNativeType().parse(reader, source), source);
+        }
+
         /**
          * Converts the value from the native type to the custom argument type.
          *
          * @param nativeType native argument provided value
          * @return converted value
          * @throws CommandSyntaxException if an exception occurs while parsing
+         * @see #convert(Object, Object)
          */
         T convert(N nativeType) throws CommandSyntaxException;
+
+        /**
+         * Converts the value from the native type to the custom argument type.
+         * <p>
+         * This method provides the command source for additional context when converting. You
+         * may have to do your own {@code instanceof} checks for {@link io.papermc.paper.command.brigadier.CommandSourceStack}.
+         *
+         * @param nativeType native argument provided value
+         * @param source     source of the command
+         * @return converted value
+         * @throws CommandSyntaxException if an exception occurs while parsing
+         */
+        default <S> T convert(final N nativeType, final S source) throws CommandSyntaxException {
+            return this.convert(nativeType);
+        }
     }
 }
diff --git a/paper-api/src/main/java/io/papermc/paper/event/entity/EntityMoveEvent.java b/paper-api/src/main/java/io/papermc/paper/event/entity/EntityMoveEvent.java
index 49ace39539..00fef88d56 100644
--- a/paper-api/src/main/java/io/papermc/paper/event/entity/EntityMoveEvent.java
+++ b/paper-api/src/main/java/io/papermc/paper/event/entity/EntityMoveEvent.java
@@ -53,7 +53,7 @@ public class EntityMoveEvent extends EntityEvent implements Cancellable {
      */
     public void setFrom(final Location from) {
         this.validateLocation(from);
-        this.from = from;
+        this.from = from.clone();
     }
 
     /**
@@ -72,7 +72,7 @@ public class EntityMoveEvent extends EntityEvent implements Cancellable {
      */
     public void setTo(final Location to) {
         this.validateLocation(to);
-        this.to = to;
+        this.to = to.clone();
     }
 
     /**
diff --git a/paper-api/src/main/java/io/papermc/paper/event/world/border/WorldBorderCenterChangeEvent.java b/paper-api/src/main/java/io/papermc/paper/event/world/border/WorldBorderCenterChangeEvent.java
index 74fe5ad505..5f43aefe7a 100644
--- a/paper-api/src/main/java/io/papermc/paper/event/world/border/WorldBorderCenterChangeEvent.java
+++ b/paper-api/src/main/java/io/papermc/paper/event/world/border/WorldBorderCenterChangeEvent.java
@@ -52,7 +52,7 @@ public class WorldBorderCenterChangeEvent extends WorldBorderEvent implements Ca
      * @param newCenter the new center
      */
     public void setNewCenter(final Location newCenter) {
-        this.newCenter = newCenter;
+        this.newCenter = newCenter.clone();
     }
 
     @Override
diff --git a/paper-api/src/main/java/io/papermc/paper/potion/PotionMix.java b/paper-api/src/main/java/io/papermc/paper/potion/PotionMix.java
index 01b2a7c7a6..41152c98c0 100644
--- a/paper-api/src/main/java/io/papermc/paper/potion/PotionMix.java
+++ b/paper-api/src/main/java/io/papermc/paper/potion/PotionMix.java
@@ -30,9 +30,9 @@ public final class PotionMix implements Keyed {
      */
     public PotionMix(final NamespacedKey key, final ItemStack result, final RecipeChoice input, final RecipeChoice ingredient) {
         this.key = key;
-        this.result = result;
-        this.input = input;
-        this.ingredient = ingredient;
+        this.result = result.clone();
+        this.input = input.clone();
+        this.ingredient = ingredient.clone();
     }
 
     /**
@@ -58,7 +58,7 @@ public final class PotionMix implements Keyed {
      * @return the result itemstack
      */
     public ItemStack getResult() {
-        return this.result;
+        return this.result.clone();
     }
 
     /**
@@ -67,7 +67,7 @@ public final class PotionMix implements Keyed {
      * @return the bottom 3 slot ingredients
      */
     public RecipeChoice getInput() {
-        return this.input;
+        return this.input.clone();
     }
 
     /**
@@ -76,7 +76,7 @@ public final class PotionMix implements Keyed {
      * @return the top slot input
      */
     public RecipeChoice getIngredient() {
-        return this.ingredient;
+        return this.ingredient.clone();
     }
 
     @Override
diff --git a/paper-api/src/main/java/org/bukkit/Vibration.java b/paper-api/src/main/java/org/bukkit/Vibration.java
index bbc01e7c19..2c0e18e8dd 100644
--- a/paper-api/src/main/java/org/bukkit/Vibration.java
+++ b/paper-api/src/main/java/org/bukkit/Vibration.java
@@ -79,7 +79,7 @@ public class Vibration {
             private final Location block;
 
             public BlockDestination(@NotNull Location block) {
-                this.block = block;
+                this.block = block.clone();
             }
 
             public BlockDestination(@NotNull Block block) {
@@ -88,7 +88,7 @@ public class Vibration {
 
             @NotNull
             public Location getLocation() {
-                return block;
+                return block.clone();
             }
 
             @NotNull
diff --git a/paper-api/src/main/java/org/bukkit/entity/Mob.java b/paper-api/src/main/java/org/bukkit/entity/Mob.java
index 9a10262a95..ca773a807e 100644
--- a/paper-api/src/main/java/org/bukkit/entity/Mob.java
+++ b/paper-api/src/main/java/org/bukkit/entity/Mob.java
@@ -191,7 +191,7 @@ public interface Mob extends LivingEntity, Lootable, io.papermc.paper.entity.Lea
      * set by {@link #setAggressive(boolean)}. {@link Panda}'s are always
      * aggressive if their combined {@link Panda.Gene} is {@link Panda.Gene#AGGRESSIVE}.
      *
-     * @return wether the mob is aggressive or not
+     * @return whether the mob is aggressive or not
      */
     boolean isAggressive();
 
@@ -199,7 +199,7 @@ public interface Mob extends LivingEntity, Lootable, io.papermc.paper.entity.Lea
      * Some mobs will raise their arm(s) when aggressive,
      * see {@link #isAggressive()} for full list.
      *
-     * @param aggressive wether the mob should be aggressive or not
+     * @param aggressive whether the mob should be aggressive or not
      * @see #isAggressive()
      */
     void setAggressive(boolean aggressive);
diff --git a/paper-api/src/main/java/org/bukkit/event/block/BlockDispenseEvent.java b/paper-api/src/main/java/org/bukkit/event/block/BlockDispenseEvent.java
index 14d1eb5d93..e8ed75ba7d 100644
--- a/paper-api/src/main/java/org/bukkit/event/block/BlockDispenseEvent.java
+++ b/paper-api/src/main/java/org/bukkit/event/block/BlockDispenseEvent.java
@@ -65,7 +65,7 @@ public class BlockDispenseEvent extends BlockEvent implements Cancellable {
      * @param vel the velocity of the item being dispensed
      */
     public void setVelocity(@NotNull Vector vel) {
-        velocity = vel;
+        velocity = vel.clone();
     }
 
     @Override
diff --git a/paper-api/src/main/java/org/bukkit/event/block/FluidLevelChangeEvent.java b/paper-api/src/main/java/org/bukkit/event/block/FluidLevelChangeEvent.java
index 9bd0440c37..c61f6b1bd5 100644
--- a/paper-api/src/main/java/org/bukkit/event/block/FluidLevelChangeEvent.java
+++ b/paper-api/src/main/java/org/bukkit/event/block/FluidLevelChangeEvent.java
@@ -43,7 +43,7 @@ public class FluidLevelChangeEvent extends BlockEvent implements Cancellable {
         Preconditions.checkArgument(newData != null, "newData null");
         Preconditions.checkArgument(this.newData.getMaterial().equals(newData.getMaterial()), "Cannot change fluid type");
 
-        this.newData = newData;
+        this.newData = newData.clone();
     }
 
     @Override
diff --git a/paper-api/src/main/java/org/bukkit/event/entity/EntityDeathEvent.java b/paper-api/src/main/java/org/bukkit/event/entity/EntityDeathEvent.java
index 086bec9daa..42ffb81708 100644
--- a/paper-api/src/main/java/org/bukkit/event/entity/EntityDeathEvent.java
+++ b/paper-api/src/main/java/org/bukkit/event/entity/EntityDeathEvent.java
@@ -11,7 +11,7 @@ import org.jetbrains.annotations.Nullable;
 /**
  * Thrown whenever a LivingEntity dies
  */
-public class EntityDeathEvent extends EntityEvent implements org.bukkit.event.Cancellable {  // Paper - make cancellable
+public class EntityDeathEvent extends EntityEvent implements org.bukkit.event.Cancellable { // Paper - make cancellable
     private static final HandlerList handlers = new HandlerList();
     private final DamageSource damageSource;
     private final List<ItemStack> drops;
diff --git a/paper-api/src/main/java/org/bukkit/event/entity/EntityKnockbackEvent.java b/paper-api/src/main/java/org/bukkit/event/entity/EntityKnockbackEvent.java
index 0d465629ec..6c0c2f78de 100644
--- a/paper-api/src/main/java/org/bukkit/event/entity/EntityKnockbackEvent.java
+++ b/paper-api/src/main/java/org/bukkit/event/entity/EntityKnockbackEvent.java
@@ -99,7 +99,7 @@ public class EntityKnockbackEvent extends EntityEvent implements Cancellable {
     public void setFinalKnockback(@NotNull Vector knockback) {
         Preconditions.checkArgument(knockback != null, "Knockback cannot be null");
 
-        this.knockback = knockback;
+        this.knockback = knockback.clone();
     }
 
     @Override
diff --git a/paper-api/src/main/java/org/bukkit/event/entity/EntityTeleportEvent.java b/paper-api/src/main/java/org/bukkit/event/entity/EntityTeleportEvent.java
index a7918049ae..bffad21c02 100644
--- a/paper-api/src/main/java/org/bukkit/event/entity/EntityTeleportEvent.java
+++ b/paper-api/src/main/java/org/bukkit/event/entity/EntityTeleportEvent.java
@@ -52,7 +52,7 @@ public class EntityTeleportEvent extends EntityEvent implements Cancellable {
      * @param from New location this entity moved from
      */
     public void setFrom(@NotNull Location from) {
-        this.from = from;
+        this.from = from.clone();
     }
 
     /**
@@ -71,7 +71,7 @@ public class EntityTeleportEvent extends EntityEvent implements Cancellable {
      * @param to New Location this entity moved to
      */
     public void setTo(@Nullable Location to) {
-        this.to = to;
+        this.to = to != null ? to.clone() : null;
     }
 
     @NotNull
diff --git a/paper-api/src/main/java/org/bukkit/event/player/PlayerInteractEvent.java b/paper-api/src/main/java/org/bukkit/event/player/PlayerInteractEvent.java
index 69c800d367..5e52f4dc26 100644
--- a/paper-api/src/main/java/org/bukkit/event/player/PlayerInteractEvent.java
+++ b/paper-api/src/main/java/org/bukkit/event/player/PlayerInteractEvent.java
@@ -241,7 +241,7 @@ public class PlayerInteractEvent extends PlayerEvent implements Cancellable {
     @Nullable
     @Deprecated // Paper
     public Vector getClickedPosition() {
-        return clickedPosistion;
+        return clickedPosistion.clone();
     }
 
     // Paper start
diff --git a/paper-api/src/main/java/org/bukkit/event/player/PlayerMoveEvent.java b/paper-api/src/main/java/org/bukkit/event/player/PlayerMoveEvent.java
index b484abf3b0..237d654773 100644
--- a/paper-api/src/main/java/org/bukkit/event/player/PlayerMoveEvent.java
+++ b/paper-api/src/main/java/org/bukkit/event/player/PlayerMoveEvent.java
@@ -70,7 +70,7 @@ public class PlayerMoveEvent extends PlayerEvent implements Cancellable {
      */
     public void setFrom(@NotNull Location from) {
         validateLocation(from);
-        this.from = from;
+        this.from = from.clone();
     }
 
     /**
@@ -90,7 +90,7 @@ public class PlayerMoveEvent extends PlayerEvent implements Cancellable {
      */
     public void setTo(@NotNull Location to) {
         validateLocation(to);
-        this.to = to;
+        this.to = to.clone();
     }
 
     // Paper start - PlayerMoveEvent improvements
diff --git a/paper-api/src/main/java/org/bukkit/event/player/PlayerRespawnEvent.java b/paper-api/src/main/java/org/bukkit/event/player/PlayerRespawnEvent.java
index 4d925774f7..d1dd5cf0ad 100644
--- a/paper-api/src/main/java/org/bukkit/event/player/PlayerRespawnEvent.java
+++ b/paper-api/src/main/java/org/bukkit/event/player/PlayerRespawnEvent.java
@@ -69,7 +69,7 @@ public class PlayerRespawnEvent extends PlayerEvent {
         Preconditions.checkArgument(respawnLocation != null, "Respawn location can not be null");
         Preconditions.checkArgument(respawnLocation.getWorld() != null, "Respawn world can not be null");
 
-        this.respawnLocation = respawnLocation;
+        this.respawnLocation = respawnLocation.clone();
     }
 
     /**
diff --git a/paper-api/src/main/java/org/bukkit/event/player/PlayerVelocityEvent.java b/paper-api/src/main/java/org/bukkit/event/player/PlayerVelocityEvent.java
index 61e098d94a..87b15dcabf 100644
--- a/paper-api/src/main/java/org/bukkit/event/player/PlayerVelocityEvent.java
+++ b/paper-api/src/main/java/org/bukkit/event/player/PlayerVelocityEvent.java
@@ -45,7 +45,7 @@ public class PlayerVelocityEvent extends PlayerEvent implements Cancellable {
      * @param velocity The velocity vector that will be sent to the player
      */
     public void setVelocity(@NotNull Vector velocity) {
-        this.velocity = velocity;
+        this.velocity = velocity.clone();
     }
 
     @NotNull
diff --git a/paper-api/src/main/java/org/bukkit/event/world/AsyncStructureSpawnEvent.java b/paper-api/src/main/java/org/bukkit/event/world/AsyncStructureSpawnEvent.java
index 5f05b32bdf..978790b97b 100644
--- a/paper-api/src/main/java/org/bukkit/event/world/AsyncStructureSpawnEvent.java
+++ b/paper-api/src/main/java/org/bukkit/event/world/AsyncStructureSpawnEvent.java
@@ -45,7 +45,7 @@ public class AsyncStructureSpawnEvent extends WorldEvent implements Cancellable
      */
     @NotNull
     public BoundingBox getBoundingBox() {
-        return boundingBox;
+        return boundingBox.clone();
     }
 
     /**
diff --git a/paper-api/src/main/java/org/bukkit/inventory/EquipmentSlotGroup.java b/paper-api/src/main/java/org/bukkit/inventory/EquipmentSlotGroup.java
index 2c4353e8fa..83b5128f91 100644
--- a/paper-api/src/main/java/org/bukkit/inventory/EquipmentSlotGroup.java
+++ b/paper-api/src/main/java/org/bukkit/inventory/EquipmentSlotGroup.java
@@ -25,7 +25,7 @@ public final class EquipmentSlotGroup implements Predicate<EquipmentSlot> {
     public static final EquipmentSlotGroup LEGS = get("legs", EquipmentSlot.LEGS);
     public static final EquipmentSlotGroup CHEST = get("chest", EquipmentSlot.CHEST);
     public static final EquipmentSlotGroup HEAD = get("head", EquipmentSlot.HEAD);
-    public static final EquipmentSlotGroup ARMOR = get("armor", (test) -> test == EquipmentSlot.FEET || test == EquipmentSlot.LEGS || test == EquipmentSlot.CHEST || test == EquipmentSlot.HEAD || test == EquipmentSlot.BODY, EquipmentSlot.CHEST);  // Paper - add missing slot type
+    public static final EquipmentSlotGroup ARMOR = get("armor", (test) -> test == EquipmentSlot.FEET || test == EquipmentSlot.LEGS || test == EquipmentSlot.CHEST || test == EquipmentSlot.HEAD || test == EquipmentSlot.BODY, EquipmentSlot.CHEST); // Paper - add missing slot type
     public static final EquipmentSlotGroup BODY = get("body", EquipmentSlot.BODY); // Paper - add missing slot group
     //
     private final String key;
diff --git a/paper-api/src/main/java/org/bukkit/loot/LootContext.java b/paper-api/src/main/java/org/bukkit/loot/LootContext.java
index 4a8b2538a8..470f712e8d 100644
--- a/paper-api/src/main/java/org/bukkit/loot/LootContext.java
+++ b/paper-api/src/main/java/org/bukkit/loot/LootContext.java
@@ -24,7 +24,7 @@ public final class LootContext {
     private LootContext(@NotNull Location location, float luck, int lootingModifier, @Nullable Entity lootedEntity, @Nullable HumanEntity killer) {
         Preconditions.checkArgument(location != null, "LootContext location cannot be null");
         Preconditions.checkArgument(location.getWorld() != null, "LootContext World cannot be null");
-        this.location = location;
+        this.location = location.clone();
         this.luck = luck;
         this.lootingModifier = lootingModifier;
         this.lootedEntity = lootedEntity;
@@ -38,7 +38,7 @@ public final class LootContext {
      */
     @NotNull
     public Location getLocation() {
-        return location;
+        return location.clone();
     }
 
     /**
@@ -110,7 +110,7 @@ public final class LootContext {
          * @param location the location the LootContext should use
          */
         public Builder(@NotNull Location location) {
-            this.location = location;
+            this.location = location.clone();
         }
 
         /**
diff --git a/paper-api/src/main/java/org/bukkit/util/Transformation.java b/paper-api/src/main/java/org/bukkit/util/Transformation.java
index 39f9e50c7d..a23ed4193b 100644
--- a/paper-api/src/main/java/org/bukkit/util/Transformation.java
+++ b/paper-api/src/main/java/org/bukkit/util/Transformation.java
@@ -27,9 +27,9 @@ public class Transformation {
         Preconditions.checkArgument(scale != null, "scale cannot be null");
         Preconditions.checkArgument(rightRotation != null, "rightRotation cannot be null");
 
-        this.translation = translation;
+        this.translation = new Vector3f(translation);
         this.leftRotation = new Quaternionf(leftRotation);
-        this.scale = scale;
+        this.scale = new Vector3f(scale);
         this.rightRotation = new Quaternionf(rightRotation);
     }
 
@@ -39,10 +39,10 @@ public class Transformation {
         Preconditions.checkArgument(scale != null, "scale cannot be null");
         Preconditions.checkArgument(rightRotation != null, "rightRotation cannot be null");
 
-        this.translation = translation;
-        this.leftRotation = leftRotation;
-        this.scale = scale;
-        this.rightRotation = rightRotation;
+        this.translation = new Vector3f(translation);
+        this.leftRotation = new Quaternionf(leftRotation);
+        this.scale = new Vector3f(scale);
+        this.rightRotation = new Quaternionf(rightRotation);
     }
 
     /**
@@ -52,7 +52,7 @@ public class Transformation {
      */
     @NotNull
     public Vector3f getTranslation() {
-        return this.translation;
+        return new Vector3f(this.translation);
     }
 
     /**
@@ -62,7 +62,7 @@ public class Transformation {
      */
     @NotNull
     public Quaternionf getLeftRotation() {
-        return this.leftRotation;
+        return new Quaternionf(this.leftRotation);
     }
 
     /**
@@ -72,7 +72,7 @@ public class Transformation {
      */
     @NotNull
     public Vector3f getScale() {
-        return this.scale;
+        return new Vector3f(this.scale);
     }
 
     /**
@@ -82,7 +82,7 @@ public class Transformation {
      */
     @NotNull
     public Quaternionf getRightRotation() {
-        return this.rightRotation;
+        return new Quaternionf(this.rightRotation);
     }
 
     @Override
diff --git a/paper-api/src/main/java/org/spigotmc/event/player/PlayerSpawnLocationEvent.java b/paper-api/src/main/java/org/spigotmc/event/player/PlayerSpawnLocationEvent.java
index 2515887c20..eeedfedc47 100644
--- a/paper-api/src/main/java/org/spigotmc/event/player/PlayerSpawnLocationEvent.java
+++ b/paper-api/src/main/java/org/spigotmc/event/player/PlayerSpawnLocationEvent.java
@@ -37,7 +37,7 @@ public class PlayerSpawnLocationEvent extends PlayerEvent {
      * @param location the spawn location
      */
     public void setSpawnLocation(@NotNull Location location) {
-        this.spawnLocation = location;
+        this.spawnLocation = location.clone();
     }
 
     @NotNull
diff --git a/paper-server/build.gradle.kts b/paper-server/build.gradle.kts
index 7a8c6d0fbf..266795c487 100644
--- a/paper-server/build.gradle.kts
+++ b/paper-server/build.gradle.kts
@@ -11,9 +11,9 @@ plugins {
 val paperMavenPublicUrl = "https://repo.papermc.io/repository/maven-public/"
 
 dependencies {
-    mache("io.papermc:mache:1.21.4+build.5")
+    mache("io.papermc:mache:1.21.4+build.6")
     paperclip("io.papermc:paperclip:3.0.3")
-    remapper("net.fabricmc:tiny-remapper:0.10.3:fat")
+    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
 }
 
 paperweight {
@@ -23,23 +23,24 @@ paperweight {
 
     paper {
         reobfMappingsPatch = layout.projectDirectory.file("../build-data/reobf-mappings-patch.tiny")
-        reobfPackagesToFix.addAll(
-            "co.aikar.timings",
-            "com.destroystokyo.paper",
-            "com.mojang",
-            "io.papermc.paper",
-            "ca.spottedleaf",
-            "net.kyori.adventure.bossbar",
-            "net.minecraft",
-            "org.bukkit.craftbukkit",
-            "org.spigotmc",
-        )
     }
 
     spigot {
         buildDataRef = "3edaf46ec1eed4115ce1b18d2846cded42577e42"
         packageVersion = "v1_21_R3" // also needs to be updated in MappingEnvironment
     }
+
+    reobfPackagesToFix.addAll(
+        "co.aikar.timings",
+        "com.destroystokyo.paper",
+        "com.mojang",
+        "io.papermc.paper",
+        "ca.spottedleaf",
+        "net.kyori.adventure.bossbar",
+        "net.minecraft",
+        "org.bukkit.craftbukkit",
+        "org.spigotmc",
+    )
 }
 
 tasks.generateDevelopmentBundle {
@@ -58,45 +59,45 @@ abstract class Services {
 }
 val services = objects.newInstance<Services>()
 
-publishing {
-    if (project.providers.gradleProperty("publishDevBundle").isPresent) {
-        val devBundleComponent = services.softwareComponentFactory.adhoc("devBundle")
-        components.add(devBundleComponent)
+if (project.providers.gradleProperty("publishDevBundle").isPresent) {
+    val devBundleComponent = services.softwareComponentFactory.adhoc("devBundle")
+    components.add(devBundleComponent)
 
-        val devBundle = configurations.consumable("devBundle") {
-            attributes.attribute(DevBundleOutput.ATTRIBUTE, objects.named(DevBundleOutput.ZIP))
-            outgoing.artifact(tasks.generateDevelopmentBundle.flatMap { it.devBundleFile })
-        }
-        devBundleComponent.addVariantsFromConfiguration(devBundle.get()) {}
+    val devBundle = configurations.consumable("devBundle") {
+        attributes.attribute(DevBundleOutput.ATTRIBUTE, objects.named(DevBundleOutput.ZIP))
+        outgoing.artifact(tasks.generateDevelopmentBundle.flatMap { it.devBundleFile })
+    }
+    devBundleComponent.addVariantsFromConfiguration(devBundle.get()) {}
 
-        val runtime = configurations.consumable("serverRuntimeClasspath") {
-            attributes.attribute(DevBundleOutput.ATTRIBUTE, objects.named(DevBundleOutput.SERVER_DEPENDENCIES))
-            attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
-            extendsFrom(configurations.runtimeClasspath.get())
-        }
-        devBundleComponent.addVariantsFromConfiguration(runtime.get()) {
-            mapToMavenScope("runtime")
-        }
+    val runtime = configurations.consumable("serverRuntimeClasspath") {
+        attributes.attribute(DevBundleOutput.ATTRIBUTE, objects.named(DevBundleOutput.SERVER_DEPENDENCIES))
+        attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
+        extendsFrom(configurations.runtimeClasspath.get())
+    }
+    devBundleComponent.addVariantsFromConfiguration(runtime.get()) {
+        mapToMavenScope("runtime")
+    }
 
-        val compile = configurations.consumable("serverCompileClasspath") {
-            attributes.attribute(DevBundleOutput.ATTRIBUTE, objects.named(DevBundleOutput.SERVER_DEPENDENCIES))
-            attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_API))
-            extendsFrom(configurations.compileClasspath.get())
-        }
-        devBundleComponent.addVariantsFromConfiguration(compile.get()) {
-            mapToMavenScope("compile")
-        }
+    val compile = configurations.consumable("serverCompileClasspath") {
+        attributes.attribute(DevBundleOutput.ATTRIBUTE, objects.named(DevBundleOutput.SERVER_DEPENDENCIES))
+        attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_API))
+        extendsFrom(configurations.compileClasspath.get())
+    }
+    devBundleComponent.addVariantsFromConfiguration(compile.get()) {
+        mapToMavenScope("compile")
+    }
 
-        tasks.withType(GenerateMavenPom::class).configureEach {
-            doLast {
-                val text = destination.readText()
-                // Remove dependencies from pom, dev bundle is designed for gradle module metadata consumers
-                destination.writeText(
-                    text.substringBefore("<dependencies>") + text.substringAfter("</dependencies>")
-                )
-            }
+    tasks.withType(GenerateMavenPom::class).configureEach {
+        doLast {
+            val text = destination.readText()
+            // Remove dependencies from pom, dev bundle is designed for gradle module metadata consumers
+            destination.writeText(
+                text.substringBefore("<dependencies>") + text.substringAfter("</dependencies>")
+            )
         }
+    }
 
+    publishing {
         publications.create<MavenPublication>("devBundle") {
             artifactId = "dev-bundle"
             from(devBundleComponent)
@@ -149,6 +150,11 @@ dependencies {
     runtimeOnly("org.xerial:sqlite-jdbc:3.47.0.0")
     runtimeOnly("com.mysql:mysql-connector-j:9.1.0")
     runtimeOnly("com.lmax:disruptor:3.4.4") // Paper
+    // Paper start - Use Velocity cipher
+    implementation("com.velocitypowered:velocity-native:3.3.0-SNAPSHOT") {
+        isTransitive = false
+    }
+    // Paper end - Use Velocity cipher
 
     runtimeOnly("org.apache.maven:maven-resolver-provider:3.9.6")
     runtimeOnly("org.apache.maven.resolver:maven-resolver-connector-basic:1.9.18")
@@ -176,12 +182,12 @@ dependencies {
     // Paper end - spark
 }
 
-
 tasks.jar {
     manifest {
         val git = Git(rootProject.layout.projectDirectory.path)
         val mcVersion = rootProject.providers.gradleProperty("mcVersion").get()
         val build = System.getenv("BUILD_NUMBER") ?: null
+        val buildTime = if (build != null) Instant.now() else Instant.EPOCH
         val gitHash = git.exec(providers, "rev-parse", "--short=7", "HEAD").get().trim()
         val implementationVersion = "$mcVersion-${build ?: "DEV"}-$gitHash"
         val date = git.exec(providers, "show", "-s", "--format=%ci", gitHash).get().trim() // Paper
@@ -197,7 +203,7 @@ tasks.jar {
             "Brand-Id" to "papermc:paper",
             "Brand-Name" to "Paper",
             "Build-Number" to (build ?: ""),
-            "Build-Time" to Instant.now().toString(),
+            "Build-Time" to buildTime.toString(),
             "Git-Branch" to gitBranch, // Paper
             "Git-Commit" to gitHash, // Paper
         )
@@ -256,7 +262,7 @@ fun TaskContainer.registerRunTask(
     name: String,
     block: JavaExec.() -> Unit
 ): TaskProvider<JavaExec> = register<JavaExec>(name) {
-    group = "paper"
+    group = "runs"
     mainClass.set("org.bukkit.craftbukkit.Main")
     standardInput = System.`in`
     workingDir = rootProject.layout.projectDirectory
@@ -266,7 +272,7 @@ fun TaskContainer.registerRunTask(
         languageVersion.set(JavaLanguageVersion.of(21))
         vendor.set(JvmVendorSpec.JETBRAINS)
     })
-    jvmArgs("-XX:+AllowEnhancedClassRedefinition", "-XX:+AllowRedefinitionToAddDeleteMethods")
+    jvmArgs("-XX:+AllowEnhancedClassRedefinition")
 
     if (rootProject.childProjects["test-plugin"] != null) {
         val testPluginJar = rootProject.project(":test-plugin").tasks.jar.flatMap { it.archiveFile }
@@ -312,21 +318,21 @@ tasks.registerRunTask("runDevServer") {
 
 tasks.registerRunTask("runBundler") {
     description = "Spin up a test server from the Mojang mapped bundler jar"
-    classpath(tasks.named<io.papermc.paperweight.tasks.CreateBundlerJar>("createMojmapBundlerJar").flatMap { it.outputZip })
+    classpath(tasks.createMojmapBundlerJar.flatMap { it.outputZip })
     mainClass.set(null as String?)
 }
 tasks.registerRunTask("runReobfBundler") {
     description = "Spin up a test server from the reobf bundler jar"
-    classpath(rootProject.tasks.named<io.papermc.paperweight.tasks.CreateBundlerJar>("createReobfBundlerJar").flatMap { it.outputZip })
+    classpath(tasks.createReobfBundlerJar.flatMap { it.outputZip })
     mainClass.set(null as String?)
 }
 tasks.registerRunTask("runPaperclip") {
     description = "Spin up a test server from the Mojang mapped Paperclip jar"
-    classpath(tasks.named<io.papermc.paperweight.tasks.CreatePaperclipJar>("createMojmapPaperclipJar").flatMap { it.outputZip })
+    classpath(tasks.createMojmapPaperclipJar.flatMap { it.outputZip })
     mainClass.set(null as String?)
 }
 tasks.registerRunTask("runReobfPaperclip") {
     description = "Spin up a test server from the reobf Paperclip jar"
-    classpath(rootProject.tasks.named<io.papermc.paperweight.tasks.CreatePaperclipJar>("createReobfPaperclipJar").flatMap { it.outputZip })
+    classpath(tasks.createReobfPaperclipJar.flatMap { it.outputZip })
     mainClass.set(null as String?)
 }
diff --git a/paper-server/patches/features/.gitkeep b/paper-server/patches/features/.gitkeep
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/feature-patches/1040-Optimize-Network-Manager-and-add-advanced-packet-sup.patch b/paper-server/patches/features/0001-Optimize-Network-Manager-and-add-advanced-packet-sup.patch
similarity index 73%
rename from feature-patches/1040-Optimize-Network-Manager-and-add-advanced-packet-sup.patch
rename to paper-server/patches/features/0001-Optimize-Network-Manager-and-add-advanced-packet-sup.patch
index 51ccefe733..02589fdaae 100644
--- a/feature-patches/1040-Optimize-Network-Manager-and-add-advanced-packet-sup.patch
+++ b/paper-server/patches/features/0001-Optimize-Network-Manager-and-add-advanced-packet-sup.patch
@@ -27,34 +27,34 @@ and then catch exceptions and close if they fire.
 
 Part of this commit was authored by: Spottedleaf, sandtechnology
 
-diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/network/Connection.java
-+++ b/src/main/java/net/minecraft/network/Connection.java
-@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
+diff --git a/net/minecraft/network/Connection.java b/net/minecraft/network/Connection.java
+index 42f44c7cb0bd55ddfacd18acb0e596e7a953870e..ad8f8428b75e37097487cdfbd0db2421ee4cbe37 100644
+--- a/net/minecraft/network/Connection.java
++++ b/net/minecraft/network/Connection.java
+@@ -85,7 +85,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
      private static final ProtocolInfo<ServerHandshakePacketListener> INITIAL_PROTOCOL = HandshakeProtocols.SERVERBOUND;
      private final PacketFlow receiving;
      private volatile boolean sendLoginDisconnect = true;
 -    private final Queue<Consumer<Connection>> pendingActions = Queues.newConcurrentLinkedQueue();
-+    private final Queue<WrappedConsumer> pendingActions = Queues.newConcurrentLinkedQueue(); // Paper
++    private final Queue<WrappedConsumer> pendingActions = Queues.newConcurrentLinkedQueue(); // Paper - Optimize network
      public Channel channel;
      public SocketAddress address;
-     // Spigot Start
-@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
-     public java.net.InetSocketAddress virtualHost;
-     private static boolean enableExplicitFlush = Boolean.getBoolean("paper.explicit-flush"); // Paper - Disable explicit network manager flushing
-     // Paper end
+     // Spigot start
+@@ -145,6 +145,10 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
+     }
+     // Paper end - packet limiter
+     @Nullable public SocketAddress haProxyAddress; // Paper - Add API to get player's proxy address
 +    // Paper start - Optimize network
 +    public boolean isPending = true;
 +    public boolean queueImmunity;
 +    // Paper end - Optimize network
  
-     // Paper start - add utility methods
-     public final net.minecraft.server.level.ServerPlayer getPlayer() {
-@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
+     public Connection(PacketFlow receiving) {
+         this.receiving = receiving;
+@@ -423,11 +427,38 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
      }
  
-     public void send(Packet<?> packet, @Nullable PacketSendListener callbacks, boolean flush) {
+     public void send(Packet<?> packet, @Nullable PacketSendListener listener, boolean flush) {
 -        if (this.isConnected()) {
 -            this.flushQueue();
 +        // Paper start - Optimize network: Handle oversized packets better
@@ -67,17 +67,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        if (connected && (InnerUtil.canSendImmediate(this, packet)
 +            || (io.papermc.paper.util.MCUtil.isMainThread() && packet.isReady() && this.pendingActions.isEmpty()
 +            && (packet.getExtraPackets() == null || packet.getExtraPackets().isEmpty())))) {
-             this.sendPacket(packet, callbacks, flush);
+             this.sendPacket(packet, listener, flush);
          } else {
--            this.pendingActions.add((networkmanager) -> {
--                networkmanager.sendPacket(packet, callbacks, flush);
--            });
--        }
+-            this.pendingActions.add(connection -> connection.sendPacket(packet, listener, flush));
 +            // Write the packets to the queue, then flush - antixray hooks there already
 +            final java.util.List<Packet<?>> extraPackets = InnerUtil.buildExtraPackets(packet);
 +            final boolean hasExtraPackets = extraPackets != null && !extraPackets.isEmpty();
 +            if (!hasExtraPackets) {
-+                this.pendingActions.add(new PacketSendAction(packet, callbacks, flush));
++                this.pendingActions.add(new PacketSendAction(packet, listener, flush));
 +            } else {
 +                final java.util.List<PacketSendAction> actions = new java.util.ArrayList<>(1 + extraPackets.size());
 +                actions.add(new PacketSendAction(packet, null, false)); // Delay the future listener until the end of the extra packets
@@ -85,31 +82,30 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                for (int i = 0, len = extraPackets.size(); i < len;) {
 +                    final Packet<?> extraPacket = extraPackets.get(i);
 +                    final boolean end = ++i == len;
-+                    actions.add(new PacketSendAction(extraPacket, end ? callbacks : null, end)); // Append listener to the end
++                    actions.add(new PacketSendAction(extraPacket, end ? listener : null, end)); // Append listener to the end
 +                }
 +
 +                this.pendingActions.addAll(actions);
 +            }
- 
++
 +            this.flushQueue();
 +            // Paper end - Optimize network
-+        }
-     }
- 
-     public void runOnceConnected(Consumer<Connection> task) {
-@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
-             this.flushQueue();
-             task.accept(this);
-         } else {
--            this.pendingActions.add(task);
-+            this.pendingActions.add(new WrappedConsumer(task)); // Paper - Optimize network
          }
- 
-     }
-@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
      }
  
-     private void doSendPacket(Packet<?> packet, @Nullable PacketSendListener callbacks, boolean flush) {
+@@ -436,7 +467,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
+             this.flushQueue();
+             action.accept(this);
+         } else {
+-            this.pendingActions.add(action);
++            this.pendingActions.add(new WrappedConsumer(action)); // Paper - Optimize network
+         }
+     }
+ 
+@@ -450,6 +481,14 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
+     }
+ 
+     private void doSendPacket(Packet<?> packet, @Nullable PacketSendListener sendListener, boolean flush) {
 +        // Paper start - Optimize network
 +        final net.minecraft.server.level.ServerPlayer player = this.getPlayer();
 +        if (!this.isConnected()) {
@@ -118,18 +114,18 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +        try {
 +        // Paper end - Optimize network
-         ChannelFuture channelfuture = flush ? this.channel.writeAndFlush(packet) : this.channel.write(packet);
- 
-         if (callbacks != null) {
-@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
+         ChannelFuture channelFuture = flush ? this.channel.writeAndFlush(packet) : this.channel.write(packet);
+         if (sendListener != null) {
+             channelFuture.addListener(future -> {
+@@ -465,14 +504,24 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
              });
          }
  
 +        // Paper start - Optimize network
 +        if (packet.hasFinishListener()) {
-+            channelfuture.addListener((ChannelFutureListener) channelFuture -> packet.onPacketDispatchFinish(player, channelFuture));
++            channelFuture.addListener((ChannelFutureListener) future -> packet.onPacketDispatchFinish(player, future));
 +        }
-         channelfuture.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
+         channelFuture.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
 +        } catch (final Exception e) {
 +            LOGGER.error("NetworkException: {}", player, e);
 +            this.disconnect(Component.translatable("disconnect.genericReason", "Internal Exception: " + e.getMessage()));
@@ -145,16 +141,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -            this.pendingActions.add(Connection::flush);
 +            this.pendingActions.add(new WrappedConsumer(Connection::flush)); // Paper - Optimize network
          }
- 
      }
-@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
  
+@@ -484,16 +533,57 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
+         }
      }
  
 -    private void flushQueue() {
 -        if (this.channel != null && this.channel.isOpen()) {
--            Queue queue = this.pendingActions;
--
 +    // Paper start - Optimize network: Rewrite this to be safer if ran off main thread
 +    private boolean flushQueue() {
 +        if (!this.isConnected()) {
@@ -165,7 +159,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        } else if (this.isPending) {
 +            // Should only happen during login/status stages
              synchronized (this.pendingActions) {
--                Consumer consumer;
+-                Consumer<Connection> consumer;
+-                while ((consumer = this.pendingActions.poll()) != null) {
+-                    consumer.accept(this);
 +                return this.processQueue();
 +            }
 +        }
@@ -176,9 +172,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        if (this.pendingActions.isEmpty()) {
 +            return true;
 +        }
- 
--                while ((consumer = (Consumer) this.pendingActions.poll()) != null) {
--                    consumer.accept(this);
++
 +        // If we are on main, we are safe here in that nothing else should be processing queue off main anymore
 +        // But if we are not on main due to login/status, the parent is synchronized on packetQueue
 +        final java.util.Iterator<WrappedConsumer> iterator = this.pendingActions.iterator();
@@ -199,12 +193,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                if (!packet.isReady()) {
 +                    return false;
                  }
-+            }
- 
+             }
++
 +            iterator.remove();
 +            if (queued.tryMarkConsumed()) {
 +                queued.accept(this);
-             }
++            }
          }
 +        return true;
      }
@@ -212,35 +206,35 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      private static final int MAX_PER_TICK = io.papermc.paper.configuration.GlobalConfiguration.get().misc.maxJoinsPerTick; // Paper - Buffer joins to world
      private static int joinAttemptsThisTick; // Paper - Buffer joins to world
-@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
-     public void disconnect(DisconnectionDetails disconnectionInfo) {
-         // Spigot Start
-         this.preparing = false;
+@@ -563,6 +653,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
+ 
+     public void disconnect(DisconnectionDetails disconnectionDetails) {
+         this.preparing = false; // Spigot
 +        this.clearPacketQueue(); // Paper - Optimize network
-         // Spigot End
          if (this.channel == null) {
-             this.delayedDisconnect = disconnectionInfo;
-@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
+             this.delayedDisconnect = disconnectionDetails;
+         }
+@@ -751,7 +842,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
      public void handleDisconnection() {
          if (this.channel != null && !this.channel.isOpen()) {
              if (this.disconnectionHandled) {
--                Connection.LOGGER.warn("handleDisconnection() called twice");
-+                // Connection.LOGGER.warn("handleDisconnection() called twice"); // Paper - Don't log useless message
+-                LOGGER.warn("handleDisconnection() called twice");
++                // LOGGER.warn("handleDisconnection() called twice"); // Paper - Don't log useless message
              } else {
                  this.disconnectionHandled = true;
-                 PacketListener packetlistener = this.getPacketListener();
-@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
- 
-                     packetlistener1.onDisconnect(disconnectiondetails);
+                 PacketListener packetListener = this.getPacketListener();
+@@ -762,7 +853,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
+                     );
+                     packetListener1.onDisconnect(disconnectionDetails);
                  }
 -                this.pendingActions.clear(); // Free up packet queue.
 +                this.clearPacketQueue(); // Paper - Optimize network
                  // Paper start - Add PlayerConnectionCloseEvent
-                 final PacketListener packetListener = this.getPacketListener();
                  if (packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl commonPacketListener) {
-@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
-     public void setBandwidthLogger(LocalSampleLogger log) {
-         this.bandwidthDebugMonitor = new BandwidthDebugMonitor(log);
+                     /* Player was logged in, either game listener or configuration listener */
+@@ -797,4 +888,93 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
+     public void setBandwidthLogger(LocalSampleLogger bandwithLogger) {
+         this.bandwidthDebugMonitor = new BandwidthDebugMonitor(bandwithLogger);
      }
 +
 +    // Paper start - Optimize network
@@ -332,11 +326,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - Optimize network
  }
-diff --git a/src/main/java/net/minecraft/network/protocol/Packet.java b/src/main/java/net/minecraft/network/protocol/Packet.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/network/protocol/Packet.java
-+++ b/src/main/java/net/minecraft/network/protocol/Packet.java
-@@ -0,0 +0,0 @@ public interface Packet<T extends PacketListener> {
+diff --git a/net/minecraft/network/protocol/Packet.java b/net/minecraft/network/protocol/Packet.java
+index 65ff8b9112ec76eeac48c679044fc02ae7d4ffeb..e4789584cbe43959681a8522c66eab58369bebd0 100644
+--- a/net/minecraft/network/protocol/Packet.java
++++ b/net/minecraft/network/protocol/Packet.java
+@@ -35,4 +35,32 @@ public interface Packet<T extends PacketListener> {
      static <B extends ByteBuf, T extends Packet<?>> StreamCodec<B, T> codec(StreamMemberEncoder<B, T> encoder, StreamDecoder<B, T> decoder) {
          return StreamCodec.ofMember(encoder, decoder);
      }
@@ -352,7 +346,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     * @param player Null if not at PLAY stage yet
 +     * @param future Can be null if packet was cancelled
 +     */
-+    default void onPacketDispatchFinish(@org.jetbrains.annotations.Nullable net.minecraft.server.level.ServerPlayer player, @org.jetbrains.annotations.Nullable io.netty.channel.ChannelFuture future) {}
++    default void onPacketDispatchFinish(@org.jetbrains.annotations.Nullable net.minecraft.server.level.ServerPlayer player, @org.jetbrains.annotations.Nullable io.netty.channel.ChannelFuture future) {
++    }
 +
 +    default boolean hasFinishListener() {
 +        return false;
@@ -368,28 +363,29 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end
  }
-diff --git a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java
-+++ b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java
-@@ -0,0 +0,0 @@ public class ServerConnectionListener {
-     final List<Connection> connections = Collections.synchronizedList(Lists.newArrayList());
+diff --git a/net/minecraft/server/network/ServerConnectionListener.java b/net/minecraft/server/network/ServerConnectionListener.java
+index b873af7183d9b1aabc87e63d254a4326f17b21c9..7de11ba404f0b60e7f7b7c16954811a343688219 100644
+--- a/net/minecraft/server/network/ServerConnectionListener.java
++++ b/net/minecraft/server/network/ServerConnectionListener.java
+@@ -66,11 +66,13 @@ public class ServerConnectionListener {
+ 
      // Paper start - prevent blocking on adding a new connection while the server is ticking
      private final java.util.Queue<Connection> pending = new java.util.concurrent.ConcurrentLinkedQueue<>();
 +    private static final boolean disableFlushConsolidation = Boolean.getBoolean("Paper.disableFlushConsolidate"); // Paper - Optimize network
+ 
      private final void addPending() {
          Connection connection;
-         while ((connection = pending.poll()) != null) {
-             connections.add(connection);
+         while ((connection = this.pending.poll()) != null) {
+             this.connections.add(connection);
 +            connection.isPending = false; // Paper - Optimize network
          }
      }
      // Paper end - prevent blocking on adding a new connection while the server is ticking
-@@ -0,0 +0,0 @@ public class ServerConnectionListener {
-                         ;
-                     }
+@@ -120,6 +122,7 @@ public class ServerConnectionListener {
+                                     } catch (ChannelException var5) {
+                                     }
  
-+                    if (!disableFlushConsolidation) channel.pipeline().addFirst(new io.netty.handler.flush.FlushConsolidationHandler()); // Paper - Optimize network
-                     ChannelPipeline channelpipeline = channel.pipeline().addLast("timeout", new ReadTimeoutHandler(30));
- 
-                     if (ServerConnectionListener.this.server.repliesToStatus()) {
++                                    if (!disableFlushConsolidation) channel.pipeline().addFirst(new io.netty.handler.flush.FlushConsolidationHandler()); // Paper - Optimize network
+                                     ChannelPipeline channelPipeline = channel.pipeline().addLast("timeout", new ReadTimeoutHandler(30));
+                                     if (ServerConnectionListener.this.server.repliesToStatus()) {
+                                         channelPipeline.addLast("legacy_query", new LegacyQueryHandler(ServerConnectionListener.this.getServer()));
diff --git a/feature-patches/1041-Allow-Saving-of-Oversized-Chunks.patch b/paper-server/patches/features/0002-Allow-Saving-of-Oversized-Chunks.patch
similarity index 66%
rename from feature-patches/1041-Allow-Saving-of-Oversized-Chunks.patch
rename to paper-server/patches/features/0002-Allow-Saving-of-Oversized-Chunks.patch
index 6c06181f47..950f460736 100644
--- a/feature-patches/1041-Allow-Saving-of-Oversized-Chunks.patch
+++ b/paper-server/patches/features/0002-Allow-Saving-of-Oversized-Chunks.patch
@@ -30,36 +30,23 @@ This fix also maintains compatability if someone switches server jars to one wit
 this fix, as the data will remain in the oversized file. Once the server returns
 to a jar with this fix, the data will be restored.
 
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ import java.nio.file.LinkOption;
- import java.nio.file.Path;
- import java.nio.file.StandardCopyOption;
- import java.nio.file.StandardOpenOption;
-+import java.util.zip.InflaterInputStream; // Paper
- import javax.annotation.Nullable;
- import net.minecraft.Util;
- import net.minecraft.resources.ResourceLocation;
- import net.minecraft.util.profiling.jfr.JvmProfiler;
-+import net.minecraft.nbt.CompoundTag; // Paper
-+import net.minecraft.nbt.NbtIo; // Paper
- import net.minecraft.world.level.ChunkPos;
- import org.slf4j.Logger;
- 
-@@ -0,0 +0,0 @@ public class RegionFile implements AutoCloseable {
-         this.usedSectors = new RegionBitmap();
-         this.info = storageKey;
+diff --git a/net/minecraft/world/level/chunk/storage/RegionFile.java b/net/minecraft/world/level/chunk/storage/RegionFile.java
+index d0854fa02be52f560fc91adeb8495a6bd22f6f74..783a2d80f6197dd0af0dc81909f0353a8ea2ecf4 100644
+--- a/net/minecraft/world/level/chunk/storage/RegionFile.java
++++ b/net/minecraft/world/level/chunk/storage/RegionFile.java
+@@ -53,6 +53,7 @@ public class RegionFile implements AutoCloseable {
+         this.info = info;
          this.path = path;
-+        initOversizedState(); // Paper
-         this.version = compressionFormat;
-         if (!Files.isDirectory(directory, new LinkOption[0])) {
-             throw new IllegalArgumentException("Expected directory, got " + String.valueOf(directory.toAbsolutePath()));
-@@ -0,0 +0,0 @@ public class RegionFile implements AutoCloseable {
- 
+         this.version = version;
++        this.initOversizedState(); // Paper
+         if (!Files.isDirectory(externalFileDir)) {
+             throw new IllegalArgumentException("Expected directory, got " + externalFileDir.toAbsolutePath());
+         } else {
+@@ -423,4 +424,75 @@ public class RegionFile implements AutoCloseable {
+     interface CommitOp {
+         void run() throws IOException;
      }
- 
++
 +    // Paper start
 +    private final byte[] oversized = new byte[1024];
 +    private int oversizedCount;
@@ -78,9 +65,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    private static int getChunkIndex(int x, int z) {
 +        return (x & 31) + (z & 31) * 32;
 +    }
++
 +    synchronized boolean isOversized(int x, int z) {
 +        return this.oversized[getChunkIndex(x, z)] == 1;
 +    }
++
 +    synchronized void setOversized(int x, int z, boolean oversized) throws IOException {
 +        final int offset = getChunkIndex(x, z);
 +        boolean previous = this.oversized[offset] == 1;
@@ -120,22 +109,20 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return this.path.getParent().resolve(this.path.getFileName().toString().replaceAll("\\.mca$", "") + "_oversized_" + x + "_" + z + ".nbt");
 +    }
 +
-+    synchronized CompoundTag getOversizedData(int x, int z) throws IOException {
++    synchronized net.minecraft.nbt.CompoundTag getOversizedData(int x, int z) throws IOException {
 +        Path file = getOversizedFile(x, z);
-+        try (DataInputStream out = new DataInputStream(new java.io.BufferedInputStream(new InflaterInputStream(Files.newInputStream(file))))) {
-+            return NbtIo.read((java.io.DataInput) out);
++        try (DataInputStream out = new DataInputStream(new java.io.BufferedInputStream(new java.util.zip.InflaterInputStream(Files.newInputStream(file))))) {
++            return net.minecraft.nbt.NbtIo.read((java.io.DataInput) out);
 +        }
 +
 +    }
 +    // Paper end
-     private class ChunkBuffer extends ByteArrayOutputStream {
- 
-         private final ChunkPos pos;
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ public final class RegionFileStorage implements AutoCloseable {
+ }
+diff --git a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+index 5ac84d6b47e7fdc16e1c09b739829de3d316bf5b..51bf310423013d0ae9d3202d66e36a053a767197 100644
+--- a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
++++ b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+@@ -47,6 +47,43 @@ public final class RegionFileStorage implements AutoCloseable {
          }
      }
  
@@ -148,7 +135,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        synchronized (regionfile) {
 +            try (DataInputStream datainputstream = regionfile.getChunkDataInputStream(chunkCoordinate)) {
 +                CompoundTag oversizedData = regionfile.getOversizedData(chunkCoordinate.x, chunkCoordinate.z);
-+                CompoundTag chunk = NbtIo.read((DataInput) datainputstream);
++                CompoundTag chunk = NbtIo.read(datainputstream);
 +                if (oversizedData == null) {
 +                    return chunk;
 +                }
@@ -177,26 +164,26 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    // Paper end
 +
      @Nullable
-     public CompoundTag read(ChunkPos pos) throws IOException {
+     public CompoundTag read(ChunkPos chunkPos) throws IOException {
          // CraftBukkit start - SPIGOT-5680: There's no good reason to preemptively create files on read, save that for writing
-@@ -0,0 +0,0 @@ public final class RegionFileStorage implements AutoCloseable {
+@@ -55,6 +92,12 @@ public final class RegionFileStorage implements AutoCloseable {
+             return null;
+         }
          // CraftBukkit end
-         DataInputStream datainputstream = regionfile.getChunkDataInputStream(pos);
- 
 +        // Paper start
-+        if (regionfile.isOversized(pos.x, pos.z)) {
-+            printOversizedLog("Loading Oversized Chunk!", regionfile.getPath(), pos.x, pos.z);
-+            return readOversizedChunk(regionfile, pos);
++        if (regionFile.isOversized(chunkPos.x, chunkPos.z)) {
++            printOversizedLog("Loading Oversized Chunk!", regionFile.getPath(), chunkPos.x, chunkPos.z);
++            return readOversizedChunk(regionFile, chunkPos);
 +        }
 +        // Paper end
-         CompoundTag nbttagcompound;
-         label43:
-         {
-@@ -0,0 +0,0 @@ public final class RegionFileStorage implements AutoCloseable {
  
-             try {
-                 NbtIo.write(nbt, (DataOutput) dataoutputstream);
-+                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
-             } catch (Throwable throwable) {
-                 if (dataoutputstream != null) {
-                     try {
+         CompoundTag var4;
+         try (DataInputStream chunkDataInputStream = regionFile.getChunkDataInputStream(chunkPos)) {
+@@ -90,6 +133,7 @@ public final class RegionFileStorage implements AutoCloseable {
+         } else {
+             try (DataOutputStream chunkDataOutputStream = regionFile.getChunkDataOutputStream(chunkPos)) {
+                 NbtIo.write(chunkData, chunkDataOutputStream);
++                regionFile.setOversized(chunkPos.x, chunkPos.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
+             }
+         }
+     }
diff --git a/paper-server/patches/features/0003-Entity-Activation-Range-2.0.patch b/paper-server/patches/features/0003-Entity-Activation-Range-2.0.patch
new file mode 100644
index 0000000000..adff760447
--- /dev/null
+++ b/paper-server/patches/features/0003-Entity-Activation-Range-2.0.patch
@@ -0,0 +1,861 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <aikar@aikar.co>
+Date: Fri, 13 May 2016 01:38:06 -0400
+Subject: [PATCH] Entity Activation Range 2.0
+
+Optimizes performance of Activation Range
+
+Adds many new configurations and a new wake up inactive system
+
+Fixes and adds new Immunities to improve gameplay behavior
+
+Adds water Mobs to activation range config and nerfs fish
+Adds flying monsters to control ghast and phantoms
+Adds villagers as separate config
+
+diff --git a/io/papermc/paper/entity/activation/ActivationRange.java b/io/papermc/paper/entity/activation/ActivationRange.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..bd888ef719b9bfc93bace0b1d0fb771ac659f515
+--- /dev/null
++++ b/io/papermc/paper/entity/activation/ActivationRange.java
+@@ -0,0 +1,318 @@
++package io.papermc.paper.entity.activation;
++
++import net.minecraft.core.BlockPos;
++import net.minecraft.server.MinecraftServer;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.entity.ExperienceOrb;
++import net.minecraft.world.entity.FlyingMob;
++import net.minecraft.world.entity.LightningBolt;
++import net.minecraft.world.entity.LivingEntity;
++import net.minecraft.world.entity.Mob;
++import net.minecraft.world.entity.ai.Brain;
++import net.minecraft.world.entity.animal.Animal;
++import net.minecraft.world.entity.animal.Bee;
++import net.minecraft.world.entity.animal.Sheep;
++import net.minecraft.world.entity.animal.horse.Llama;
++import net.minecraft.world.entity.boss.EnderDragonPart;
++import net.minecraft.world.entity.boss.enderdragon.EndCrystal;
++import net.minecraft.world.entity.boss.enderdragon.EnderDragon;
++import net.minecraft.world.entity.boss.wither.WitherBoss;
++import net.minecraft.world.entity.item.ItemEntity;
++import net.minecraft.world.entity.item.PrimedTnt;
++import net.minecraft.world.entity.monster.Creeper;
++import net.minecraft.world.entity.monster.Pillager;
++import net.minecraft.world.entity.npc.Villager;
++import net.minecraft.world.entity.player.Player;
++import net.minecraft.world.entity.projectile.AbstractArrow;
++import net.minecraft.world.entity.projectile.AbstractHurtingProjectile;
++import net.minecraft.world.entity.projectile.EyeOfEnder;
++import net.minecraft.world.entity.projectile.FireworkRocketEntity;
++import net.minecraft.world.entity.projectile.ThrowableProjectile;
++import net.minecraft.world.entity.projectile.ThrownTrident;
++import net.minecraft.world.entity.schedule.Activity;
++import net.minecraft.world.level.Level;
++import net.minecraft.world.phys.AABB;
++import org.spigotmc.SpigotWorldConfig;
++
++public final class ActivationRange {
++
++    private ActivationRange() {
++    }
++
++    static Activity[] VILLAGER_PANIC_IMMUNITIES = {
++        Activity.HIDE,
++        Activity.PRE_RAID,
++        Activity.RAID,
++        Activity.PANIC
++    };
++
++    private static int checkInactiveWakeup(final Entity entity) {
++        final Level world = entity.level();
++        final SpigotWorldConfig config = world.spigotConfig;
++        final long inactiveFor = MinecraftServer.currentTick - entity.activatedTick;
++        if (entity.activationType == ActivationType.VILLAGER) {
++            if (inactiveFor > config.wakeUpInactiveVillagersEvery && world.wakeupInactiveRemainingVillagers > 0) {
++                world.wakeupInactiveRemainingVillagers--;
++                return config.wakeUpInactiveVillagersFor;
++            }
++        } else if (entity.activationType == ActivationType.ANIMAL) {
++            if (inactiveFor > config.wakeUpInactiveAnimalsEvery && world.wakeupInactiveRemainingAnimals > 0) {
++                world.wakeupInactiveRemainingAnimals--;
++                return config.wakeUpInactiveAnimalsFor;
++            }
++        } else if (entity.activationType == ActivationType.FLYING_MONSTER) {
++            if (inactiveFor > config.wakeUpInactiveFlyingEvery && world.wakeupInactiveRemainingFlying > 0) {
++                world.wakeupInactiveRemainingFlying--;
++                return config.wakeUpInactiveFlyingFor;
++            }
++        } else if (entity.activationType == ActivationType.MONSTER || entity.activationType == ActivationType.RAIDER) {
++            if (inactiveFor > config.wakeUpInactiveMonstersEvery && world.wakeupInactiveRemainingMonsters > 0) {
++                world.wakeupInactiveRemainingMonsters--;
++                return config.wakeUpInactiveMonstersFor;
++            }
++        }
++        return -1;
++    }
++
++    static AABB maxBB = new AABB(0, 0, 0, 0, 0, 0);
++
++    /**
++     * These entities are excluded from Activation range checks.
++     *
++     * @param entity
++     * @param config
++     * @return boolean If it should always tick.
++     */
++    public static boolean initializeEntityActivationState(final Entity entity, final SpigotWorldConfig config) {
++        return (entity.activationType == ActivationType.MISC && config.miscActivationRange == 0)
++            || (entity.activationType == ActivationType.RAIDER && config.raiderActivationRange == 0)
++            || (entity.activationType == ActivationType.ANIMAL && config.animalActivationRange == 0)
++            || (entity.activationType == ActivationType.MONSTER && config.monsterActivationRange == 0)
++            || (entity.activationType == ActivationType.VILLAGER && config.villagerActivationRange <= 0)
++            || (entity.activationType == ActivationType.WATER && config.waterActivationRange <= 0)
++            || (entity.activationType == ActivationType.FLYING_MONSTER && config.flyingMonsterActivationRange <= 0)
++            || entity instanceof EyeOfEnder
++            || entity instanceof Player
++            || entity instanceof ThrowableProjectile
++            || entity instanceof EnderDragon
++            || entity instanceof EnderDragonPart
++            || entity instanceof WitherBoss
++            || entity instanceof AbstractHurtingProjectile
++            || entity instanceof LightningBolt
++            || entity instanceof PrimedTnt
++            || entity instanceof net.minecraft.world.entity.item.FallingBlockEntity
++            || entity instanceof net.minecraft.world.entity.vehicle.AbstractMinecart
++            || entity instanceof net.minecraft.world.entity.vehicle.AbstractBoat
++            || entity instanceof EndCrystal
++            || entity instanceof FireworkRocketEntity
++            || entity instanceof ThrownTrident;
++    }
++
++    /**
++     * Find what entities are in range of the players in the world and set
++     * active if in range.
++     *
++     * @param world
++     */
++    public static void activateEntities(final Level world) {
++        final int miscActivationRange = world.spigotConfig.miscActivationRange;
++        final int raiderActivationRange = world.spigotConfig.raiderActivationRange;
++        final int animalActivationRange = world.spigotConfig.animalActivationRange;
++        final int monsterActivationRange = world.spigotConfig.monsterActivationRange;
++        final int waterActivationRange = world.spigotConfig.waterActivationRange;
++        final int flyingActivationRange = world.spigotConfig.flyingMonsterActivationRange;
++        final int villagerActivationRange = world.spigotConfig.villagerActivationRange;
++        world.wakeupInactiveRemainingAnimals = Math.min(world.wakeupInactiveRemainingAnimals + 1, world.spigotConfig.wakeUpInactiveAnimals);
++        world.wakeupInactiveRemainingVillagers = Math.min(world.wakeupInactiveRemainingVillagers + 1, world.spigotConfig.wakeUpInactiveVillagers);
++        world.wakeupInactiveRemainingMonsters = Math.min(world.wakeupInactiveRemainingMonsters + 1, world.spigotConfig.wakeUpInactiveMonsters);
++        world.wakeupInactiveRemainingFlying = Math.min(world.wakeupInactiveRemainingFlying + 1, world.spigotConfig.wakeUpInactiveFlying);
++
++        int maxRange = Math.max(monsterActivationRange, animalActivationRange);
++        maxRange = Math.max(maxRange, raiderActivationRange);
++        maxRange = Math.max(maxRange, miscActivationRange);
++        maxRange = Math.max(maxRange, flyingActivationRange);
++        maxRange = Math.max(maxRange, waterActivationRange);
++        maxRange = Math.max(maxRange, villagerActivationRange);
++        maxRange = Math.min((world.spigotConfig.simulationDistance << 4) - 8, maxRange);
++
++        for (final Player player : world.players()) {
++            player.activatedTick = MinecraftServer.currentTick;
++            if (world.spigotConfig.ignoreSpectatorActivation && player.isSpectator()) {
++                continue;
++            }
++
++            final int worldHeight = world.getHeight();
++            ActivationRange.maxBB = player.getBoundingBox().inflate(maxRange, worldHeight, maxRange);
++            ActivationType.MISC.boundingBox = player.getBoundingBox().inflate(miscActivationRange, worldHeight, miscActivationRange);
++            ActivationType.RAIDER.boundingBox = player.getBoundingBox().inflate(raiderActivationRange, worldHeight, raiderActivationRange);
++            ActivationType.ANIMAL.boundingBox = player.getBoundingBox().inflate(animalActivationRange, worldHeight, animalActivationRange);
++            ActivationType.MONSTER.boundingBox = player.getBoundingBox().inflate(monsterActivationRange, worldHeight, monsterActivationRange);
++            ActivationType.WATER.boundingBox = player.getBoundingBox().inflate(waterActivationRange, worldHeight, waterActivationRange);
++            ActivationType.FLYING_MONSTER.boundingBox = player.getBoundingBox().inflate(flyingActivationRange, worldHeight, flyingActivationRange);
++            ActivationType.VILLAGER.boundingBox = player.getBoundingBox().inflate(villagerActivationRange, worldHeight, villagerActivationRange);
++
++            final java.util.List<Entity> entities = world.getEntities((Entity) null, ActivationRange.maxBB, e -> true);
++            final boolean tickMarkers = world.paperConfig().entities.markers.tick;
++            for (final Entity entity : entities) {
++                if (!tickMarkers && entity instanceof net.minecraft.world.entity.Marker) {
++                    continue;
++                }
++
++                ActivationRange.activateEntity(entity);
++            }
++        }
++    }
++
++    /**
++     * Tries to activate an entity.
++     *
++     * @param entity
++     */
++    private static void activateEntity(final Entity entity) {
++        if (MinecraftServer.currentTick > entity.activatedTick) {
++            if (entity.defaultActivationState) {
++                entity.activatedTick = MinecraftServer.currentTick;
++                return;
++            }
++            if (entity.activationType.boundingBox.intersects(entity.getBoundingBox())) {
++                entity.activatedTick = MinecraftServer.currentTick;
++            }
++        }
++    }
++
++    /**
++     * If an entity is not in range, do some more checks to see if we should
++     * give it a shot.
++     *
++     * @param entity
++     * @return
++     */
++    public static int checkEntityImmunities(final Entity entity) { // return # of ticks to get immunity
++        final SpigotWorldConfig config = entity.level().spigotConfig;
++        final int inactiveWakeUpImmunity = checkInactiveWakeup(entity);
++        if (inactiveWakeUpImmunity > -1) {
++            return inactiveWakeUpImmunity;
++        }
++        if (entity.getRemainingFireTicks() > 0) {
++            return 2;
++        }
++        if (entity.activatedImmunityTick >= MinecraftServer.currentTick) {
++            return 1;
++        }
++        final long inactiveFor = MinecraftServer.currentTick - entity.activatedTick;
++        if ((entity.activationType != ActivationType.WATER && entity.isInWater() && entity.isPushedByFluid())) {
++            return 100;
++        }
++        if (!entity.onGround() || entity.getDeltaMovement().horizontalDistanceSqr() > 9.999999747378752E-6D) {
++            return 100;
++        }
++        if (!(entity instanceof final AbstractArrow arrow)) {
++            if ((!entity.onGround() && !(entity instanceof FlyingMob))) {
++                return 10;
++            }
++        } else if (!arrow.isInGround()) {
++            return 1;
++        }
++        // special cases.
++        if (entity instanceof final LivingEntity living) {
++            if (living.onClimbable() || living.jumping || living.hurtTime > 0 || !living.activeEffects.isEmpty() || living.isFreezing()) {
++                return 1;
++            }
++            if (entity instanceof final Mob mob && mob.getTarget() != null) {
++                return 20;
++            }
++            if (entity instanceof final Bee bee) {
++                final BlockPos movingTarget = bee.getMovingTarget();
++                if (bee.isAngry() ||
++                    (bee.getHivePos() != null && bee.getHivePos().equals(movingTarget)) ||
++                    (bee.getSavedFlowerPos() != null && bee.getSavedFlowerPos().equals(movingTarget))
++                ) {
++                    return 20;
++                }
++            }
++            if (entity instanceof final Villager villager) {
++                final Brain<Villager> behaviorController = villager.getBrain();
++
++                if (config.villagersActiveForPanic) {
++                    for (final Activity activity : VILLAGER_PANIC_IMMUNITIES) {
++                        if (behaviorController.isActive(activity)) {
++                            return 20 * 5;
++                        }
++                    }
++                }
++
++                if (config.villagersWorkImmunityAfter > 0 && inactiveFor >= config.villagersWorkImmunityAfter) {
++                    if (behaviorController.isActive(Activity.WORK)) {
++                        return config.villagersWorkImmunityFor;
++                    }
++                }
++            }
++            if (entity instanceof final Llama llama && llama.inCaravan()) {
++                return 1;
++            }
++            if (entity instanceof final Animal animal) {
++                if (animal.isBaby() || animal.isInLove()) {
++                    return 5;
++                }
++                if (entity instanceof final Sheep sheep && sheep.isSheared()) {
++                    return 1;
++                }
++            }
++            if (entity instanceof final Creeper creeper && creeper.isIgnited()) { // isExplosive
++                return 20;
++            }
++            if (entity instanceof final Mob mob && mob.targetSelector.hasTasks()) {
++                return 0;
++            }
++            if (entity instanceof final Pillager pillager) {
++                // TODO:?
++            }
++        }
++        // SPIGOT-6644: Otherwise the target refresh tick will be missed
++        if (entity instanceof ExperienceOrb) {
++            return 20;
++        }
++        return -1;
++    }
++
++    /**
++     * Checks if the entity is active for this tick.
++     *
++     * @param entity
++     * @return
++     */
++    public static boolean checkIfActive(final Entity entity) {
++        // Never safe to skip fireworks or item gravity
++        if (entity instanceof FireworkRocketEntity || (entity instanceof ItemEntity && (entity.tickCount + entity.getId()) % 4 == 0)) { // Needed for item gravity, see ItemEntity tick
++            return true;
++        }
++        // special case always immunities
++        // immunize brand-new entities, dead entities, and portal scenarios
++        if (entity.defaultActivationState || entity.tickCount < 20 * 10 || !entity.isAlive() || (entity.portalProcess != null && !entity.portalProcess.hasExpired()) || entity.portalCooldown > 0) {
++            return true;
++        }
++        // immunize leashed entities
++        if (entity instanceof final Mob mob && mob.getLeashHolder() instanceof Player) {
++            return true;
++        }
++
++        boolean isActive = entity.activatedTick >= MinecraftServer.currentTick;
++        entity.isTemporarilyActive = false;
++
++        // Should this entity tick?
++        if (!isActive) {
++            if ((MinecraftServer.currentTick - entity.activatedTick - 1) % 20 == 0) {
++                // Check immunities every 20 ticks.
++                final int immunity = checkEntityImmunities(entity);
++                if (immunity >= 0) {
++                    entity.activatedTick = MinecraftServer.currentTick + immunity;
++                } else {
++                    entity.isTemporarilyActive = true;
++                }
++                isActive = true;
++            }
++        }
++        // removed the original's dumb tick skipping for active entities
++        return isActive;
++    }
++}
+diff --git a/net/minecraft/server/level/ChunkMap.java b/net/minecraft/server/level/ChunkMap.java
+index d95413af04121fe91ca0f3b0c70025b9808acef9..ad665c7535c615d2b03a3e7864be435f933235dd 100644
+--- a/net/minecraft/server/level/ChunkMap.java
++++ b/net/minecraft/server/level/ChunkMap.java
+@@ -4,7 +4,6 @@ import com.google.common.collect.ImmutableList;
+ import com.google.common.collect.Iterables;
+ import com.google.common.collect.Lists;
+ import com.google.common.collect.Queues;
+-import com.google.common.collect.Sets;
+ import com.google.common.collect.ImmutableList.Builder;
+ import com.mojang.datafixers.DataFixer;
+ import com.mojang.logging.LogUtils;
+@@ -19,7 +18,6 @@ import it.unimi.dsi.fastutil.longs.LongIterator;
+ import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
+ import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
+ import it.unimi.dsi.fastutil.longs.LongSet;
+-import it.unimi.dsi.fastutil.longs.Long2ObjectMap.Entry;
+ import java.io.IOException;
+ import java.io.Writer;
+ import java.nio.file.Path;
+@@ -95,7 +93,6 @@ import net.minecraft.world.level.levelgen.structure.StructureStart;
+ import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
+ import net.minecraft.world.level.storage.DimensionDataStorage;
+ import net.minecraft.world.level.storage.LevelStorageSource;
+-import net.minecraft.world.phys.Vec3;
+ import org.apache.commons.lang3.mutable.MutableBoolean;
+ import org.slf4j.Logger;
+ 
+diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java
+index 3164f3784131babf9a6663335517a12df7e88a7b..da8848e2a3e3745949eb2356a049451d30f763a7 100644
+--- a/net/minecraft/server/level/ServerLevel.java
++++ b/net/minecraft/server/level/ServerLevel.java
+@@ -551,6 +551,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+                 profilerFiller.pop();
+             }
+ 
++            io.papermc.paper.entity.activation.ActivationRange.activateEntities(this); // Paper - EAR
+             this.entityTickList
+                 .forEach(
+                     entity -> {
+@@ -979,12 +980,15 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+         entity.tickCount++;
+         profilerFiller.push(() -> BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString());
+         profilerFiller.incrementCounter("tickNonPassenger");
++        final boolean isActive = io.papermc.paper.entity.activation.ActivationRange.checkIfActive(entity); // Paper - EAR 2
++        if (isActive) { // Paper - EAR 2
+         entity.tick();
+         entity.postTick(); // CraftBukkit
++        } else {entity.inactiveTick();} // Paper - EAR 2
+         profilerFiller.pop();
+ 
+         for (Entity entity1 : entity.getPassengers()) {
+-            this.tickPassenger(entity, entity1);
++            this.tickPassenger(entity, entity1, isActive); // Paper - EAR 2
+         }
+         // Paper start - log detailed entity tick information
+         } finally {
+@@ -995,7 +999,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+         // Paper end - log detailed entity tick information
+     }
+ 
+-    private void tickPassenger(Entity ridingEntity, Entity passengerEntity) {
++    private void tickPassenger(Entity ridingEntity, Entity passengerEntity, final boolean isActive) { // Paper - EAR 2
+         if (passengerEntity.isRemoved() || passengerEntity.getVehicle() != ridingEntity) {
+             passengerEntity.stopRiding();
+         } else if (passengerEntity instanceof Player || this.entityTickList.contains(passengerEntity)) {
+@@ -1004,12 +1008,21 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+             ProfilerFiller profilerFiller = Profiler.get();
+             profilerFiller.push(() -> BuiltInRegistries.ENTITY_TYPE.getKey(passengerEntity.getType()).toString());
+             profilerFiller.incrementCounter("tickPassenger");
++            // Paper start - EAR 2
++            if (isActive) {
+             passengerEntity.rideTick();
+             passengerEntity.postTick(); // CraftBukkit
++            } else {
++                passengerEntity.setDeltaMovement(Vec3.ZERO);
++                passengerEntity.inactiveTick();
++                // copied from inside of if (isPassenger()) of passengerTick, but that ifPassenger is unnecessary
++                ridingEntity.positionRider(passengerEntity);
++            }
++            // Paper end - EAR 2
+             profilerFiller.pop();
+ 
+             for (Entity entity : passengerEntity.getPassengers()) {
+-                this.tickPassenger(passengerEntity, entity);
++                this.tickPassenger(passengerEntity, entity, isActive); // Paper - EAR 2
+             }
+         }
+     }
+diff --git a/net/minecraft/world/entity/AgeableMob.java b/net/minecraft/world/entity/AgeableMob.java
+index a9f01e616ef6b0d74caf57cd68eb371a4fd30fd5..179f4e4b9b1eb57f78bbb2f9fa34b11ea79b7a88 100644
+--- a/net/minecraft/world/entity/AgeableMob.java
++++ b/net/minecraft/world/entity/AgeableMob.java
+@@ -126,6 +126,23 @@ public abstract class AgeableMob extends PathfinderMob {
+         super.onSyncedDataUpdated(key);
+     }
+ 
++    // Paper start - EAR 2
++    @Override
++    public void inactiveTick() {
++        super.inactiveTick();
++        if (this.level().isClientSide || this.ageLocked) { // CraftBukkit
++            this.refreshDimensions();
++        } else {
++            int age = this.getAge();
++            if (age < 0) {
++                this.setAge(++age);
++            } else if (age > 0) {
++                this.setAge(--age);
++            }
++        }
++    }
++    // Paper end - EAR 2
++
+     @Override
+     public void aiStep() {
+         super.aiStep();
+diff --git a/net/minecraft/world/entity/AreaEffectCloud.java b/net/minecraft/world/entity/AreaEffectCloud.java
+index 24735284fda151414d99faad401d25ba60995f9a..23b342cc31c7e72ade0e1ccad86a9ccf34380f13 100644
+--- a/net/minecraft/world/entity/AreaEffectCloud.java
++++ b/net/minecraft/world/entity/AreaEffectCloud.java
+@@ -128,6 +128,16 @@ public class AreaEffectCloud extends Entity implements TraceableEntity {
+         this.duration = duration;
+     }
+ 
++    // Paper start - EAR 2
++    @Override
++    public void inactiveTick() {
++        super.inactiveTick();
++        if (this.tickCount >= this.waitTime + this.duration) {
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
++        }
++    }
++    // Paper end - EAR 2
++
+     @Override
+     public void tick() {
+         super.tick();
+diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java
+index e7889c9c7b155db46730f5e168bb7fd3d1732a8c..334859c5ff7023c730513301cc11c9837b2c7823 100644
+--- a/net/minecraft/world/entity/Entity.java
++++ b/net/minecraft/world/entity/Entity.java
+@@ -380,6 +380,15 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+     public boolean fixedPose = false; // Paper - Expand Pose API
+     private final int despawnTime; // Paper - entity despawn time limit
+     public final io.papermc.paper.entity.activation.ActivationType activationType = io.papermc.paper.entity.activation.ActivationType.activationTypeFor(this); // Paper - EAR 2/tracking ranges
++    // Paper start - EAR 2
++    public final boolean defaultActivationState;
++    public long activatedTick = Integer.MIN_VALUE;
++    public boolean isTemporarilyActive;
++    public long activatedImmunityTick = Integer.MIN_VALUE;
++
++    public void inactiveTick() {
++    }
++    // Paper end - EAR 2
+ 
+     public void setOrigin(@javax.annotation.Nonnull org.bukkit.Location location) {
+         this.origin = location.toVector();
+@@ -417,6 +426,13 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+         this.position = Vec3.ZERO;
+         this.blockPosition = BlockPos.ZERO;
+         this.chunkPosition = ChunkPos.ZERO;
++        // Paper start - EAR 2
++        if (level != null) {
++            this.defaultActivationState = io.papermc.paper.entity.activation.ActivationRange.initializeEntityActivationState(this, level.spigotConfig);
++        } else {
++            this.defaultActivationState = false;
++        }
++        // Paper end - EAR 2
+         SynchedEntityData.Builder builder = new SynchedEntityData.Builder(this);
+         builder.define(DATA_SHARED_FLAGS_ID, (byte)0);
+         builder.define(DATA_AIR_SUPPLY_ID, this.getMaxAirSupply());
+@@ -981,6 +997,8 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+         } else {
+             this.wasOnFire = this.isOnFire();
+             if (type == MoverType.PISTON) {
++                this.activatedTick = Math.max(this.activatedTick, MinecraftServer.currentTick + 20); // Paper - EAR 2
++                this.activatedImmunityTick = Math.max(this.activatedImmunityTick, MinecraftServer.currentTick + 20);   // Paper - EAR 2
+                 movement = this.limitPistonMovement(movement);
+                 if (movement.equals(Vec3.ZERO)) {
+                     return;
+@@ -994,6 +1012,13 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+                 this.stuckSpeedMultiplier = Vec3.ZERO;
+                 this.setDeltaMovement(Vec3.ZERO);
+             }
++            // Paper start - ignore movement changes while inactive.
++            if (isTemporarilyActive && !(this instanceof ItemEntity) && movement == getDeltaMovement() && type == MoverType.SELF) {
++                setDeltaMovement(Vec3.ZERO);
++                profilerFiller.pop();
++                return;
++            }
++            // Paper end
+ 
+             movement = this.maybeBackOffFromEdge(movement, type);
+             Vec3 vec3 = this.collide(movement);
+diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java
+index a71a153a91de5a564b946091d8d3ccbb330e4b89..195e1151f7b2a32d6c4eb67edd1952e38f58b266 100644
+--- a/net/minecraft/world/entity/LivingEntity.java
++++ b/net/minecraft/world/entity/LivingEntity.java
+@@ -3094,6 +3094,14 @@ public abstract class LivingEntity extends Entity implements Attackable {
+         return false;
+     }
+ 
++    // Paper start - EAR 2
++    @Override
++    public void inactiveTick() {
++        super.inactiveTick();
++        ++this.noActionTime; // Above all the floats
++    }
++    // Paper end - EAR 2
++
+     @Override
+     public void tick() {
+         super.tick();
+diff --git a/net/minecraft/world/entity/Mob.java b/net/minecraft/world/entity/Mob.java
+index f7d69db61d1293510428ae275e8a50571dde5ddf..1ed07fd23985a6bf8cf8300f74c92b7531a79fc6 100644
+--- a/net/minecraft/world/entity/Mob.java
++++ b/net/minecraft/world/entity/Mob.java
+@@ -215,6 +215,19 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab
+         return this.lookControl;
+     }
+ 
++    // Paper start
++    @Override
++    public void inactiveTick() {
++        super.inactiveTick();
++        if (this.goalSelector.inactiveTick()) {
++            this.goalSelector.tick();
++        }
++        if (this.targetSelector.inactiveTick()) {
++            this.targetSelector.tick();
++        }
++    }
++    // Paper end
++
+     public MoveControl getMoveControl() {
+         return this.getControlledVehicle() instanceof Mob mob ? mob.getMoveControl() : this.moveControl;
+     }
+diff --git a/net/minecraft/world/entity/PathfinderMob.java b/net/minecraft/world/entity/PathfinderMob.java
+index 0caf50ec50f056b83a20bbc6a2fe0144593aef39..af59a700755654eb68d6bf57d0712c4a2ac6c09b 100644
+--- a/net/minecraft/world/entity/PathfinderMob.java
++++ b/net/minecraft/world/entity/PathfinderMob.java
+@@ -17,6 +17,8 @@ public abstract class PathfinderMob extends Mob {
+         super(entityType, level);
+     }
+ 
++    public BlockPos movingTarget; public BlockPos getMovingTarget() { return movingTarget; } // Paper
++
+     public float getWalkTargetValue(BlockPos pos) {
+         return this.getWalkTargetValue(pos, this.level());
+     }
+diff --git a/net/minecraft/world/entity/ai/goal/GoalSelector.java b/net/minecraft/world/entity/ai/goal/GoalSelector.java
+index 9338e63cc28413f5559bb0122ef5e04a84bd51d1..eeba224bd575451ba6023df65ef9d9b97f7f1c71 100644
+--- a/net/minecraft/world/entity/ai/goal/GoalSelector.java
++++ b/net/minecraft/world/entity/ai/goal/GoalSelector.java
+@@ -25,6 +25,7 @@ public class GoalSelector {
+     private final Map<Goal.Flag, WrappedGoal> lockedFlags = new EnumMap<>(Goal.Flag.class);
+     private final Set<WrappedGoal> availableGoals = new ObjectLinkedOpenHashSet<>();
+     private final EnumSet<Goal.Flag> disabledFlags = EnumSet.noneOf(Goal.Flag.class);
++    private int curRate; // Paper - EAR 2
+ 
+     public void addGoal(int priority, Goal goal) {
+         this.availableGoals.add(new WrappedGoal(priority, goal));
+@@ -35,6 +36,22 @@ public class GoalSelector {
+         this.availableGoals.removeIf(wrappedGoal -> filter.test(wrappedGoal.getGoal()));
+     }
+ 
++    // Paper start - EAR 2
++    public boolean inactiveTick() {
++        this.curRate++;
++        return this.curRate % 3 == 0; // TODO newGoalRate was already unused in 1.20.4, check if this is correct
++    }
++
++    public boolean hasTasks() {
++        for (WrappedGoal task : this.availableGoals) {
++            if (task.isRunning()) {
++                return true;
++            }
++        }
++        return false;
++    }
++    // Paper end - EAR 2
++
+     public void removeGoal(Goal goal) {
+         for (WrappedGoal wrappedGoal : this.availableGoals) {
+             if (wrappedGoal.getGoal() == goal && wrappedGoal.isRunning()) {
+diff --git a/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java b/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java
+index 789fea258d70e60d38271ebb31270562dc7eb3ab..d0ab3db7bbd2942db19f473474371b20ce822608 100644
+--- a/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java
++++ b/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java
+@@ -23,6 +23,14 @@ public abstract class MoveToBlockGoal extends Goal {
+     public MoveToBlockGoal(PathfinderMob mob, double speedModifier, int searchRange) {
+         this(mob, speedModifier, searchRange, 1);
+     }
++    // Paper start - activation range improvements
++    @Override
++    public void stop() {
++        super.stop();
++        this.blockPos = BlockPos.ZERO;
++        this.mob.movingTarget = null;
++    }
++    // Paper end
+ 
+     public MoveToBlockGoal(PathfinderMob mob, double speedModifier, int searchRange, int verticalSearchRange) {
+         this.mob = mob;
+@@ -113,6 +121,7 @@ public abstract class MoveToBlockGoal extends Goal {
+                         mutableBlockPos.setWithOffset(blockPos, i4, i2 - 1, i5);
+                         if (this.mob.isWithinRestriction(mutableBlockPos) && this.isValidTarget(this.mob.level(), mutableBlockPos)) {
+                             this.blockPos = mutableBlockPos;
++                            this.mob.movingTarget = mutableBlockPos == BlockPos.ZERO ? null : mutableBlockPos.immutable(); // Paper
+                             return true;
+                         }
+                     }
+diff --git a/net/minecraft/world/entity/item/ItemEntity.java b/net/minecraft/world/entity/item/ItemEntity.java
+index 8b034b6bda937b25dbb3d09b8293fed6d7dc512c..52a7ed0d991758bad0dcedcb7f97fb15ac6c6d04 100644
+--- a/net/minecraft/world/entity/item/ItemEntity.java
++++ b/net/minecraft/world/entity/item/ItemEntity.java
+@@ -124,6 +124,29 @@ public class ItemEntity extends Entity implements TraceableEntity {
+         return 0.04;
+     }
+ 
++    // Paper start - EAR 2
++    @Override
++    public void inactiveTick() {
++        super.inactiveTick();
++        if (this.pickupDelay > 0 && this.pickupDelay != 32767) {
++            this.pickupDelay--;
++        }
++        if (this.age != -32768) {
++            this.age++;
++        }
++
++        if (!this.level().isClientSide && this.age >= this.despawnRate) {// Paper - Alternative item-despawn-rate
++            // CraftBukkit start - fire ItemDespawnEvent
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.callItemDespawnEvent(this).isCancelled()) {
++                this.age = 0;
++                return;
++            }
++            // CraftBukkit end
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
++        }
++    }
++    // Paper end - EAR 2
++
+     @Override
+     public void tick() {
+         if (this.getItem().isEmpty()) {
+diff --git a/net/minecraft/world/entity/npc/Villager.java b/net/minecraft/world/entity/npc/Villager.java
+index 27568a1604d2dd5d46e836bbc25431929e218aa1..2b83262e4a13eae86df82913ce4f3121e3631a43 100644
+--- a/net/minecraft/world/entity/npc/Villager.java
++++ b/net/minecraft/world/entity/npc/Villager.java
+@@ -265,11 +265,35 @@ public class Villager extends AbstractVillager implements ReputationEventHandler
+         return this.assignProfessionWhenSpawned;
+     }
+ 
++    // Paper start - EAR 2
++    @Override
++    public void inactiveTick() {
++        // SPIGOT-3874, SPIGOT-3894, SPIGOT-3846, SPIGOT-5286 :(
++        if (this.getUnhappyCounter() > 0) {
++            this.setUnhappyCounter(this.getUnhappyCounter() - 1);
++        }
++        if (this.isEffectiveAi()) {
++            if (this.level().spigotConfig.tickInactiveVillagers) {
++                this.customServerAiStep(this.level().getMinecraftWorld());
++            } else {
++                this.customServerAiStep(this.level().getMinecraftWorld(), true);
++            }
++        }
++        maybeDecayGossip();
++        super.inactiveTick();
++    }
++    // Paper end - EAR 2
++
+     @Override
+     protected void customServerAiStep(ServerLevel level) {
++        // Paper start - EAR 2
++        this.customServerAiStep(level, false);
++    }
++    protected void customServerAiStep(ServerLevel level, final boolean inactive) {
++        // Paper end - EAR 2
+         ProfilerFiller profilerFiller = Profiler.get();
+         profilerFiller.push("villagerBrain");
+-        this.getBrain().tick(level, this);
++        if (!inactive) this.getBrain().tick(level, this); // Paper - EAR 2
+         profilerFiller.pop();
+         if (this.assignProfessionWhenSpawned) {
+             this.assignProfessionWhenSpawned = false;
+@@ -293,7 +317,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler
+             this.lastTradedPlayer = null;
+         }
+ 
+-        if (!this.isNoAi() && this.random.nextInt(100) == 0) {
++        if (!inactive && !this.isNoAi() && this.random.nextInt(100) == 0) { // Paper - EAR 2
+             Raid raidAt = level.getRaidAt(this.blockPosition());
+             if (raidAt != null && raidAt.isActive() && !raidAt.isOver()) {
+                 level.broadcastEntityEvent(this, (byte)42);
+@@ -303,6 +327,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler
+         if (this.getVillagerData().getProfession() == VillagerProfession.NONE && this.isTrading()) {
+             this.stopTrading();
+         }
++        if (inactive) return; // Paper - EAR 2
+ 
+         super.customServerAiStep(level);
+     }
+diff --git a/net/minecraft/world/entity/projectile/Arrow.java b/net/minecraft/world/entity/projectile/Arrow.java
+index c1e09e701757a300183b62d343ded03033e63aa7..56574f8ef879159edc0114da09300143a2c79a35 100644
+--- a/net/minecraft/world/entity/projectile/Arrow.java
++++ b/net/minecraft/world/entity/projectile/Arrow.java
+@@ -66,6 +66,16 @@ public class Arrow extends AbstractArrow {
+         builder.define(ID_EFFECT_COLOR, -1);
+     }
+ 
++    // Paper start - EAR 2
++    @Override
++    public void inactiveTick() {
++        if (this.isInGround()) {
++            this.life++;
++        }
++        super.inactiveTick();
++    }
++    // Paper end
++
+     @Override
+     public void tick() {
+         super.tick();
+diff --git a/net/minecraft/world/entity/projectile/FireworkRocketEntity.java b/net/minecraft/world/entity/projectile/FireworkRocketEntity.java
+index 7c0862c50b44555fb27ce7dc46f4ec95a3eb0022..774ca9e0b56fd175ae246051de762d0c4256ca58 100644
+--- a/net/minecraft/world/entity/projectile/FireworkRocketEntity.java
++++ b/net/minecraft/world/entity/projectile/FireworkRocketEntity.java
+@@ -102,6 +102,21 @@ public class FireworkRocketEntity extends Projectile implements ItemSupplier {
+         return super.shouldRender(x, y, z) && !this.isAttachedToEntity();
+     }
+ 
++    // Paper start - EAR 2
++    @Override
++    public void inactiveTick() {
++        this.life++;
++        if (this.life > this.lifetime && this.level() instanceof ServerLevel serverLevel) {
++            // CraftBukkit start
++            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callFireworkExplodeEvent(this).isCancelled()) {
++                this.explode(serverLevel);
++            }
++            // CraftBukkit end
++        }
++        super.inactiveTick();
++    }
++    // Paper end - EAR 2
++
+     @Override
+     public void tick() {
+         super.tick();
+diff --git a/net/minecraft/world/entity/vehicle/MinecartHopper.java b/net/minecraft/world/entity/vehicle/MinecartHopper.java
+index c553cf0592dfa606dbbb1e6854d3377b9feb5efb..8341e7f01606fca90e69384c16fc19bb9e20d1b7 100644
+--- a/net/minecraft/world/entity/vehicle/MinecartHopper.java
++++ b/net/minecraft/world/entity/vehicle/MinecartHopper.java
+@@ -47,6 +47,7 @@ public class MinecartHopper extends AbstractMinecartContainer implements Hopper
+         if (flag != this.isEnabled()) {
+             this.setEnabled(flag);
+         }
++        this.immunize(); // Paper
+     }
+ 
+     public boolean isEnabled() {
+@@ -100,11 +101,13 @@ public class MinecartHopper extends AbstractMinecartContainer implements Hopper
+ 
+     public boolean suckInItems() {
+         if (HopperBlockEntity.suckInItems(this.level(), this)) {
++            this.immunize(); // Paper
+             return true;
+         } else {
+             for (ItemEntity itemEntity : this.level()
+                 .getEntitiesOfClass(ItemEntity.class, this.getBoundingBox().inflate(0.25, 0.0, 0.25), EntitySelector.ENTITY_STILL_ALIVE)) {
+                 if (HopperBlockEntity.addItem(this, itemEntity)) {
++                    this.immunize(); // Paper
+                     return true;
+                 }
+             }
+@@ -139,4 +142,11 @@ public class MinecartHopper extends AbstractMinecartContainer implements Hopper
+     public AbstractContainerMenu createMenu(int id, Inventory playerInventory) {
+         return new HopperMenu(id, playerInventory, this);
+     }
++
++    // Paper start
++    public void immunize() {
++        this.activatedImmunityTick = Math.max(this.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 20);
++    }
++    // Paper end
++
+ }
+diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java
+index 32f184288f6065259c921293922c1b0163df4dc4..0f346faa82b988e86834c38837f6f11bea7f31c6 100644
+--- a/net/minecraft/world/level/Level.java
++++ b/net/minecraft/world/level/Level.java
+@@ -153,6 +153,12 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+     public Map<BlockPos, BlockEntity> capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper - Retain block place order when capturing blockstates
+     public List<net.minecraft.world.entity.item.ItemEntity> captureDrops;
+     public final it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<SpawnCategory> ticksPerSpawnCategory = new it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<>();
++    // Paper start
++    public int wakeupInactiveRemainingAnimals;
++    public int wakeupInactiveRemainingFlying;
++    public int wakeupInactiveRemainingMonsters;
++    public int wakeupInactiveRemainingVillagers;
++    // Paper end
+     public boolean populating;
+     public final org.spigotmc.SpigotWorldConfig spigotConfig; // Spigot
+     // Paper start - add paper world config
+diff --git a/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java b/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java
+index 754cfdcd5a28287aa3545aaffdce1e391cbefc1e..1e6e940fca9d96ef410c7bf05524bd9b24db4a79 100644
+--- a/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java
++++ b/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java
+@@ -149,6 +149,10 @@ public class PistonMovingBlockEntity extends BlockEntity {
+                                 }
+ 
+                                 entity.setDeltaMovement(d1, d2, d3);
++                                // Paper - EAR items stuck in slime pushed by a piston
++                                entity.activatedTick = Math.max(entity.activatedTick, net.minecraft.server.MinecraftServer.currentTick + 10);
++                                entity.activatedImmunityTick = Math.max(entity.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 10);
++                                // Paper end
+                                 break;
+                             }
+                         }
diff --git a/paper-server/patches/features/0004-Anti-Xray.patch b/paper-server/patches/features/0004-Anti-Xray.patch
new file mode 100644
index 0000000000..1991ec6d25
--- /dev/null
+++ b/paper-server/patches/features/0004-Anti-Xray.patch
@@ -0,0 +1,623 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: stonar96 <minecraft.stonar96@gmail.com>
+Date: Thu, 25 Nov 2021 13:27:51 +0100
+Subject: [PATCH] Anti-Xray
+
+
+diff --git a/io/papermc/paper/FeatureHooks.java b/io/papermc/paper/FeatureHooks.java
+index b97e0a8d4c429776b86def10739faee089e2bc9b..184e6c6fe2ba522d0ea0774604839320c4152371 100644
+--- a/io/papermc/paper/FeatureHooks.java
++++ b/io/papermc/paper/FeatureHooks.java
+@@ -7,6 +7,7 @@ import it.unimi.dsi.fastutil.longs.LongSets;
+ import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
+ import it.unimi.dsi.fastutil.objects.ObjectSet;
+ import it.unimi.dsi.fastutil.objects.ObjectSets;
++import java.util.HashMap;
+ import java.util.List;
+ import java.util.Map;
+ import java.util.Set;
+@@ -36,20 +37,25 @@ public final class FeatureHooks {
+     }
+ 
+     public static LevelChunkSection createSection(final Registry<Biome> biomeRegistry, final Level level, final ChunkPos chunkPos, final int chunkSection) {
+-        return new LevelChunkSection(biomeRegistry);
++        return new LevelChunkSection(biomeRegistry, level, chunkPos, chunkSection); // Paper - Anti-Xray - Add parameters
+     }
+ 
+     public static void sendChunkRefreshPackets(final List<ServerPlayer> playersInRange, final LevelChunk chunk) {
+-        final ClientboundLevelChunkWithLightPacket refreshPacket = new ClientboundLevelChunkWithLightPacket(chunk, chunk.level.getLightEngine(), null, null);
++        // Paper start - Anti-Xray
++        final Map<Object, ClientboundLevelChunkWithLightPacket> refreshPackets = new HashMap<>();
+         for (final ServerPlayer player : playersInRange) {
+             if (player.connection == null) continue;
+ 
+-            player.connection.send(refreshPacket);
++            final Boolean shouldModify = chunk.getLevel().chunkPacketBlockController.shouldModify(player, chunk);
++            player.connection.send(refreshPackets.computeIfAbsent(shouldModify, s -> { // Use connection to prevent creating firing event
++                return new ClientboundLevelChunkWithLightPacket(chunk, chunk.level.getLightEngine(), null, null, (Boolean) s);
++            }));
+         }
++        // Paper end - Anti-Xray
+     }
+ 
+     public static PalettedContainer<BlockState> emptyPalettedBlockContainer() {
+-        return new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES);
++        return new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES, null); // Paper - Anti-Xray - Add preset block states
+     }
+ 
+     public static Set<Long> getSentChunkKeys(final ServerPlayer player) {
+diff --git a/net/minecraft/network/protocol/game/ClientboundChunksBiomesPacket.java b/net/minecraft/network/protocol/game/ClientboundChunksBiomesPacket.java
+index d4872b7f4e9591b3b1c67406312905851303f521..cb41460e94161675e2ab43f4b1b5286ee38e2e13 100644
+--- a/net/minecraft/network/protocol/game/ClientboundChunksBiomesPacket.java
++++ b/net/minecraft/network/protocol/game/ClientboundChunksBiomesPacket.java
+@@ -70,8 +70,10 @@ public record ClientboundChunksBiomesPacket(List<ClientboundChunksBiomesPacket.C
+         }
+ 
+         public static void extractChunkData(FriendlyByteBuf buffer, LevelChunk chunk) {
++            int chunkSectionIndex = 0; // Paper - Anti-Xray
+             for (LevelChunkSection levelChunkSection : chunk.getSections()) {
+-                levelChunkSection.getBiomes().write(buffer);
++                levelChunkSection.getBiomes().write(buffer, null, chunkSectionIndex); // Paper - Anti-Xray
++                chunkSectionIndex++; // Paper - Anti-Xray
+             }
+         }
+ 
+diff --git a/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java b/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java
+index 5d1943d37dfad0c12e77179f0866851532d983e9..3aea76690bc3e35758d3bf274777130af17d8a0f 100644
+--- a/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java
++++ b/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java
+@@ -28,7 +28,13 @@ public class ClientboundLevelChunkPacketData {
+     private final byte[] buffer;
+     private final List<ClientboundLevelChunkPacketData.BlockEntityInfo> blockEntitiesData;
+ 
++    // Paper start - Anti-Xray - Add chunk packet info
++    @Deprecated @io.papermc.paper.annotation.DoNotUse
+     public ClientboundLevelChunkPacketData(LevelChunk levelChunk) {
++        this(levelChunk, null);
++    }
++    public ClientboundLevelChunkPacketData(LevelChunk levelChunk, io.papermc.paper.antixray.ChunkPacketInfo<net.minecraft.world.level.block.state.BlockState> chunkPacketInfo) {
++        // Paper end
+         this.heightmaps = new CompoundTag();
+ 
+         for (Entry<Heightmap.Types, Heightmap> entry : levelChunk.getHeightmaps()) {
+@@ -38,7 +44,11 @@ public class ClientboundLevelChunkPacketData {
+         }
+ 
+         this.buffer = new byte[calculateChunkSize(levelChunk)];
+-        extractChunkData(new FriendlyByteBuf(this.getWriteBuffer()), levelChunk);
++        // Paper start - Anti-Xray - Add chunk packet info
++        if (chunkPacketInfo != null) {
++            chunkPacketInfo.setBuffer(this.buffer);
++        }
++        extractChunkData(new FriendlyByteBuf(this.getWriteBuffer()), levelChunk, chunkPacketInfo);
+         this.blockEntitiesData = Lists.newArrayList();
+ 
+         for (Entry<BlockPos, BlockEntity> entryx : levelChunk.getBlockEntities().entrySet()) {
+@@ -85,9 +95,17 @@ public class ClientboundLevelChunkPacketData {
+         return byteBuf;
+     }
+ 
++    // Paper start - Anti-Xray - Add chunk packet info
++    @Deprecated @io.papermc.paper.annotation.DoNotUse
+     public static void extractChunkData(FriendlyByteBuf buffer, LevelChunk chunk) {
++        ClientboundLevelChunkPacketData.extractChunkData(buffer, chunk, null);
++    }
++    public static void extractChunkData(FriendlyByteBuf buffer, LevelChunk chunk, io.papermc.paper.antixray.ChunkPacketInfo<net.minecraft.world.level.block.state.BlockState> chunkPacketInfo) {
++        int chunkSectionIndex = 0;
+         for (LevelChunkSection levelChunkSection : chunk.getSections()) {
+-            levelChunkSection.write(buffer);
++            levelChunkSection.write(buffer, chunkPacketInfo, chunkSectionIndex);
++            chunkSectionIndex++;
++            // Paper end  - Anti-Xray - Add chunk packet info
+         }
+     }
+ 
+diff --git a/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java b/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java
+index 3a384175f8e7f204234bbaf3081bdc20c47a0d4b..5699bc15eba92e22433a20cb8326b59f2ebd3036 100644
+--- a/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java
++++ b/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java
+@@ -18,18 +18,31 @@ public class ClientboundLevelChunkWithLightPacket implements Packet<ClientGamePa
+     private final int z;
+     private final ClientboundLevelChunkPacketData chunkData;
+     private final ClientboundLightUpdatePacketData lightData;
+-    // Paper start - Anti-Xray
++    // Paper start - Async-Anti-Xray - Ready flag for the connection, add chunk packet info
++    private volatile boolean ready;
++
++    @Override
++    public boolean isReady() {
++        return this.ready;
++    }
++
+     public void setReady(final boolean ready) {
+-        // Empty hook, updated by feature patch
++        this.ready = ready;
+     }
+-    // Paper end - Anti-Xray
+ 
++    @Deprecated @io.papermc.paper.annotation.DoNotUse
+     public ClientboundLevelChunkWithLightPacket(LevelChunk chunk, LevelLightEngine lightEngine, @Nullable BitSet skyLight, @Nullable BitSet blockLight) {
++        this(chunk, lightEngine, skyLight, blockLight, true);
++    }
++    public ClientboundLevelChunkWithLightPacket(LevelChunk chunk, LevelLightEngine lightEngine, @Nullable BitSet skyLight, @Nullable BitSet blockLight, boolean modifyBlocks) {
++        // Paper end - Anti-Xray
+         ChunkPos pos = chunk.getPos();
+         this.x = pos.x;
+         this.z = pos.z;
+-        this.chunkData = new ClientboundLevelChunkPacketData(chunk);
++        io.papermc.paper.antixray.ChunkPacketInfo<net.minecraft.world.level.block.state.BlockState> chunkPacketInfo = modifyBlocks ? chunk.getLevel().chunkPacketBlockController.getChunkPacketInfo(this, chunk) : null; // Paper - Ant-Xray
++        this.chunkData = new ClientboundLevelChunkPacketData(chunk, chunkPacketInfo); // Paper - Anti-Xray
+         this.lightData = new ClientboundLightUpdatePacketData(pos, lightEngine, skyLight, blockLight);
++        chunk.getLevel().chunkPacketBlockController.modifyBlocks(this, chunkPacketInfo); // Paper - Anti-Xray - Modify blocks
+     }
+ 
+     private ClientboundLevelChunkWithLightPacket(RegistryFriendlyByteBuf buffer) {
+diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java
+index da8848e2a3e3745949eb2356a049451d30f763a7..192977dd661ee795ada13db895db770293e9b402 100644
+--- a/net/minecraft/server/level/ServerLevel.java
++++ b/net/minecraft/server/level/ServerLevel.java
+@@ -348,7 +348,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+         org.bukkit.generator.BiomeProvider biomeProvider // CraftBukkit
+     ) {
+         // CraftBukkit start
+-        super(serverLevelData, dimension, server.registryAccess(), levelStem.type(), false, isDebug, biomeZoomSeed, server.getMaxChainedNeighborUpdates(), gen, biomeProvider, env, spigotConfig -> server.paperConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(levelStorageAccess.levelDirectory.path(), serverLevelData.getLevelName(), dimension.location(), spigotConfig, server.registryAccess(), serverLevelData.getGameRules()))); // Paper - create paper world configs
++        super(serverLevelData, dimension, server.registryAccess(), levelStem.type(), false, isDebug, biomeZoomSeed, server.getMaxChainedNeighborUpdates(), gen, biomeProvider, env, spigotConfig -> server.paperConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(levelStorageAccess.levelDirectory.path(), serverLevelData.getLevelName(), dimension.location(), spigotConfig, server.registryAccess(), serverLevelData.getGameRules())), dispatcher); // Paper - create paper world configs; Async-Anti-Xray: Pass executor
+         this.pvpMode = server.isPvpAllowed();
+         this.levelStorageAccess = levelStorageAccess;
+         this.uuid = org.bukkit.craftbukkit.util.WorldUUID.getUUID(levelStorageAccess.levelDirectory.path().toFile());
+diff --git a/net/minecraft/server/level/ServerPlayerGameMode.java b/net/minecraft/server/level/ServerPlayerGameMode.java
+index 47ed3ad5c0b4753f58e0bafff5e5e70b2f0bb40b..623c069f1fe079e020c6391a3db1a3d95cd3dbf5 100644
+--- a/net/minecraft/server/level/ServerPlayerGameMode.java
++++ b/net/minecraft/server/level/ServerPlayerGameMode.java
+@@ -299,6 +299,7 @@ public class ServerPlayerGameMode {
+                 org.bukkit.craftbukkit.event.CraftEventFactory.callBlockDamageAbortEvent(this.player, pos, this.player.getInventory().getSelected()); // CraftBukkit
+             }
+         }
++        this.level.chunkPacketBlockController.onPlayerLeftClickBlock(this, pos, action, face, maxBuildHeight, sequence); // Paper - Anti-Xray
+     }
+ 
+     public void destroyAndAck(BlockPos pos, int sequence, String message) {
+diff --git a/net/minecraft/server/network/PlayerChunkSender.java b/net/minecraft/server/network/PlayerChunkSender.java
+index 342bc843c384761e883de861044f4f8930ae8763..14878690a88fd4de3e2c127086607e6c819c636c 100644
+--- a/net/minecraft/server/network/PlayerChunkSender.java
++++ b/net/minecraft/server/network/PlayerChunkSender.java
+@@ -78,8 +78,11 @@ public class PlayerChunkSender {
+         }
+     }
+ 
+-    private static void sendChunk(ServerGamePacketListenerImpl packetListener, ServerLevel level, LevelChunk chunk) {
+-        packetListener.send(new ClientboundLevelChunkWithLightPacket(chunk, level.getLightEngine(), null, null));
++    // Paper start - Anti-Xray
++    public static void sendChunk(ServerGamePacketListenerImpl packetListener, ServerLevel level, LevelChunk chunk) {
++        final boolean shouldModify = level.chunkPacketBlockController.shouldModify(packetListener.player, chunk);
++        packetListener.send(new ClientboundLevelChunkWithLightPacket(chunk, level.getLightEngine(), null, null, shouldModify));
++        // Paper end - Anti-Xray
+         // Paper start - PlayerChunkLoadEvent
+         if (io.papermc.paper.event.packet.PlayerChunkLoadEvent.getHandlerList().getRegisteredListeners().length > 0) {
+             new io.papermc.paper.event.packet.PlayerChunkLoadEvent(new org.bukkit.craftbukkit.CraftChunk(chunk), packetListener.getPlayer().getBukkitEntity()).callEvent();
+diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java
+index 38fb0f569ffcd96e0eb6cb6f0769155a17d62874..3d5d84c1c1d431e9b369aa727ab0876f90ff5d61 100644
+--- a/net/minecraft/server/players/PlayerList.java
++++ b/net/minecraft/server/players/PlayerList.java
+@@ -403,7 +403,7 @@ public abstract class PlayerList {
+                     .getOrThrow(net.minecraft.world.level.biome.Biomes.PLAINS);
+             player.connection.send(new net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket(
+                     new net.minecraft.world.level.chunk.EmptyLevelChunk(serverLevel, player.chunkPosition(), plains),
+-                    serverLevel.getLightEngine(), (java.util.BitSet)null, (java.util.BitSet) null)
++                    serverLevel.getLightEngine(), (java.util.BitSet)null, (java.util.BitSet) null, true) // Paper - Anti-Xray
+             );
+         }
+         // Paper end - Send empty chunk
+diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java
+index 0f346faa82b988e86834c38837f6f11bea7f31c6..771d6ed6a7c889c09efd4ff6e20298c851eaa79f 100644
+--- a/net/minecraft/world/level/Level.java
++++ b/net/minecraft/world/level/Level.java
+@@ -168,6 +168,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+     }
+     // Paper end - add paper world config
+ 
++    public final io.papermc.paper.antixray.ChunkPacketBlockController chunkPacketBlockController; // Paper - Anti-Xray
+     public static BlockPos lastPhysicsProblem; // Spigot
+     private org.spigotmc.TickLimiter entityLimiter;
+     private org.spigotmc.TickLimiter tileLimiter;
+@@ -214,7 +215,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+         org.bukkit.generator.BiomeProvider biomeProvider, // CraftBukkit
+         org.bukkit.World.Environment env, // CraftBukkit
+         java.util.function.Function<org.spigotmc.SpigotWorldConfig, // Spigot - create per world config
+-        io.papermc.paper.configuration.WorldConfiguration> paperWorldConfigCreator // Paper - create paper world config
++        io.papermc.paper.configuration.WorldConfiguration> paperWorldConfigCreator, // Paper - create paper world config
++        java.util.concurrent.Executor executor // Paper - Anti-Xray
+     ) {
+         this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) levelData).getLevelName()); // Spigot
+         this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper - create paper world config
+@@ -295,6 +297,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+         // CraftBukkit end
+         this.entityLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.entityMaxTickTime);
+         this.tileLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.tileMaxTickTime);
++        this.chunkPacketBlockController = this.paperConfig().anticheat.antiXray.enabled ? new io.papermc.paper.antixray.ChunkPacketBlockControllerAntiXray(this, executor) : io.papermc.paper.antixray.ChunkPacketBlockController.NO_OPERATION_INSTANCE; // Paper - Anti-Xray
+     }
+ 
+     // Paper start - Cancel hit for vanished players
+@@ -495,6 +498,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+             // CraftBukkit end
+ 
+             BlockState blockState = chunkAt.setBlockState(pos, state, (flags & 64) != 0, (flags & 1024) == 0); // CraftBukkit custom NO_PLACE flag
++            this.chunkPacketBlockController.onBlockChange(this, pos, state, blockState, flags, recursionLeft); // Paper - Anti-Xray
+ 
+             if (blockState == null) {
+                 // CraftBukkit start - remove blockstate if failed (or the same)
+diff --git a/net/minecraft/world/level/chunk/ChunkAccess.java b/net/minecraft/world/level/chunk/ChunkAccess.java
+index 809b3c37d3749c76c3c243cd91c593d03693e9b3..860d1c9729c4ee97ec6f40f7aa969829070b27c0 100644
+--- a/net/minecraft/world/level/chunk/ChunkAccess.java
++++ b/net/minecraft/world/level/chunk/ChunkAccess.java
+@@ -114,14 +114,14 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh
+             }
+         }
+ 
+-        replaceMissingSections(biomeRegistry, this.sections);
++        this.replaceMissingSections(biomeRegistry, this.sections); // Paper - Anti-Xray - make it a non-static method
+         this.biomeRegistry = biomeRegistry; // CraftBukkit
+     }
+ 
+-    private static void replaceMissingSections(Registry<Biome> biomeRegistry, LevelChunkSection[] sections) {
++    private void replaceMissingSections(Registry<Biome> biomeRegistry, LevelChunkSection[] sections) { // Paper - Anti-Xray - make it a non-static method
+         for (int i = 0; i < sections.length; i++) {
+             if (sections[i] == null) {
+-                sections[i] = new LevelChunkSection(biomeRegistry);
++                sections[i] = new LevelChunkSection(biomeRegistry, this.levelHeightAccessor instanceof net.minecraft.world.level.Level ? (net.minecraft.world.level.Level) this.levelHeightAccessor : null, this.chunkPos, this.levelHeightAccessor.getSectionYFromSectionIndex(i)); // Paper - Anti-Xray - Add parameters
+             }
+         }
+     }
+diff --git a/net/minecraft/world/level/chunk/LevelChunk.java b/net/minecraft/world/level/chunk/LevelChunk.java
+index 51a136cf015de730ca0d1b48cf618a2ed69ea89f..96b0342ab7b922aa16d07b6c00542e6cb66c974a 100644
+--- a/net/minecraft/world/level/chunk/LevelChunk.java
++++ b/net/minecraft/world/level/chunk/LevelChunk.java
+@@ -109,7 +109,7 @@ public class LevelChunk extends ChunkAccess {
+         @Nullable LevelChunk.PostLoadProcessor postLoad,
+         @Nullable BlendingData blendingData
+     ) {
+-        super(pos, data, level, level.registryAccess().lookupOrThrow(Registries.BIOME), inhabitedTime, sections, blendingData);
++        super(pos, data, level, net.minecraft.server.MinecraftServer.getServer().registryAccess().lookupOrThrow(Registries.BIOME), inhabitedTime, sections, blendingData); // Paper - Anti-Xray - The world isn't ready yet, use server singleton for registry
+         this.level = (net.minecraft.server.level.ServerLevel) level; // CraftBukkit - type
+         this.gameEventListenerRegistrySections = new Int2ObjectOpenHashMap<>();
+ 
+diff --git a/net/minecraft/world/level/chunk/LevelChunkSection.java b/net/minecraft/world/level/chunk/LevelChunkSection.java
+index baa9f3e2e6e45c250930658e82bad70a3a292b05..fc21c3268c4b4fda2933d71f0913db28e3796653 100644
+--- a/net/minecraft/world/level/chunk/LevelChunkSection.java
++++ b/net/minecraft/world/level/chunk/LevelChunkSection.java
+@@ -38,9 +38,15 @@ public class LevelChunkSection {
+         this.recalcBlockCounts();
+     }
+ 
++    // Paper start - Anti-Xray - Add parameters
++    @Deprecated @io.papermc.paper.annotation.DoNotUse
+     public LevelChunkSection(Registry<Biome> biomeRegistry) {
+-        this.states = new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES);
+-        this.biomes = new PalettedContainer<>(biomeRegistry.asHolderIdMap(), biomeRegistry.getOrThrow(Biomes.PLAINS), PalettedContainer.Strategy.SECTION_BIOMES);
++        this(biomeRegistry, null, null, 0);
++    }
++    public LevelChunkSection(Registry<Biome> biomeRegistry, net.minecraft.world.level.Level level, net.minecraft.world.level.ChunkPos chunkPos, int chunkSectionY) {
++        // Paper end - Anti-Xray
++        this.states = new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES, level == null || level.chunkPacketBlockController == null ? null : level.chunkPacketBlockController.getPresetBlockStates(level, chunkPos, chunkSectionY)); // Paper - Anti-Xray - Add preset block states
++        this.biomes = new PalettedContainer<>(biomeRegistry.asHolderIdMap(), biomeRegistry.getOrThrow(Biomes.PLAINS), PalettedContainer.Strategy.SECTION_BIOMES, null); // Paper - Anti-Xray - Add preset biomes
+     }
+ 
+     public BlockState getBlockState(int x, int y, int z) {
+@@ -168,10 +174,16 @@ public class LevelChunkSection {
+         this.biomes = palettedContainer;
+     }
+ 
++    // Paper start - Anti-Xray - Add chunk packet info
++    @Deprecated @io.papermc.paper.annotation.DoNotUse
+     public void write(FriendlyByteBuf buffer) {
++        this.write(buffer, null, 0);
++    }
++    public void write(FriendlyByteBuf buffer, io.papermc.paper.antixray.ChunkPacketInfo<BlockState> chunkPacketInfo, int chunkSectionIndex) {
+         buffer.writeShort(this.nonEmptyBlockCount);
+-        this.states.write(buffer);
+-        this.biomes.write(buffer);
++        this.states.write(buffer, chunkPacketInfo, chunkSectionIndex);
++        this.biomes.write(buffer, null, chunkSectionIndex);
++        // Paper end - Anti-Xray
+     }
+ 
+     public int getSerializedSize() {
+diff --git a/net/minecraft/world/level/chunk/PalettedContainer.java b/net/minecraft/world/level/chunk/PalettedContainer.java
+index e8ec28ce3fe13561b45c4654e174776d9d2d7b71..a6028a54c75de068515e95913b21160ab4326985 100644
+--- a/net/minecraft/world/level/chunk/PalettedContainer.java
++++ b/net/minecraft/world/level/chunk/PalettedContainer.java
+@@ -28,6 +28,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
+     private static final int MIN_PALETTE_BITS = 0;
+     private final PaletteResize<T> dummyPaletteResize = (bits, objectAdded) -> 0;
+     public final IdMap<T> registry;
++    private final T @org.jetbrains.annotations.Nullable [] presetValues; // Paper - Anti-Xray - Add preset values
+     private volatile PalettedContainer.Data<T> data;
+     private final PalettedContainer.Strategy strategy;
+     //private final ThreadingDetector threadingDetector = new ThreadingDetector("PalettedContainer"); // Paper - unused
+@@ -40,13 +41,21 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
+         // this.threadingDetector.checkAndUnlock(); // Paper - disable this - use proper synchronization
+     }
+ 
++    // Paper start - Anti-Xray - Add preset values
++    @Deprecated @io.papermc.paper.annotation.DoNotUse
+     public static <T> Codec<PalettedContainer<T>> codecRW(IdMap<T> registry, Codec<T> codec, PalettedContainer.Strategy strategy, T value) {
+-        PalettedContainerRO.Unpacker<T, PalettedContainer<T>> unpacker = PalettedContainer::unpack;
++        return PalettedContainer.codecRW(registry, codec, strategy, value, null);
++    }
++    public static <T> Codec<PalettedContainer<T>> codecRW(IdMap<T> registry, Codec<T> codec, PalettedContainer.Strategy strategy, T value, T @org.jetbrains.annotations.Nullable [] presetValues) {
++        PalettedContainerRO.Unpacker<T, PalettedContainer<T>> unpacker = (idListx, paletteProviderx, serialized) -> {
++            return unpack(idListx, paletteProviderx, serialized, value, presetValues);
++        };
++        // Paper end
+         return codec(registry, codec, strategy, value, unpacker);
+     }
+ 
+     public static <T> Codec<PalettedContainerRO<T>> codecRO(IdMap<T> registry, Codec<T> codec, PalettedContainer.Strategy strategy, T value) {
+-        PalettedContainerRO.Unpacker<T, PalettedContainerRO<T>> unpacker = (registry1, strategy1, packedData) -> unpack(registry1, strategy1, packedData)
++        PalettedContainerRO.Unpacker<T, PalettedContainerRO<T>> unpacker = (registry1, strategy1, packedData) -> unpack(registry1, strategy1, packedData, value, null) // Paper - Anti-Xray - Add preset values
+             .map(container -> (PalettedContainerRO<T>)container);
+         return codec(registry, codec, strategy, value, unpacker);
+     }
+@@ -66,27 +75,66 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
+             );
+     }
+ 
++    // Paper start - Anti-Xray - Add preset values
++    @Deprecated @io.papermc.paper.annotation.DoNotUse
++    public PalettedContainer(IdMap<T> registry, PalettedContainer.Strategy strategy, PalettedContainer.Configuration<T> configuration, BitStorage storage, List<T> values) {
++        this(registry, strategy, configuration, storage, values, null, null);
++    }
+     public PalettedContainer(
+-        IdMap<T> registry, PalettedContainer.Strategy strategy, PalettedContainer.Configuration<T> configuration, BitStorage storage, List<T> values
++        IdMap<T> registry, PalettedContainer.Strategy strategy, PalettedContainer.Configuration<T> configuration, BitStorage storage, List<T> values, T defaultValue, T @org.jetbrains.annotations.Nullable [] presetValues
+     ) {
++        this.presetValues = presetValues;
+         this.registry = registry;
+         this.strategy = strategy;
+         this.data = new PalettedContainer.Data<>(configuration, storage, configuration.factory().create(configuration.bits(), registry, this, values));
++        if (presetValues != null && (configuration.factory() == PalettedContainer.Strategy.SINGLE_VALUE_PALETTE_FACTORY ? this.data.palette.valueFor(0) != defaultValue : configuration.factory() != PalettedContainer.Strategy.GLOBAL_PALETTE_FACTORY)) {
++            // In 1.18 Mojang unfortunately removed code that already handled possible resize operations on read from disk for us
++            // We readd this here but in a smarter way than it was before
++            int maxSize = 1 << configuration.bits();
++
++            for (T presetValue : presetValues) {
++                if (this.data.palette.getSize() >= maxSize) {
++                    java.util.Set<T> allValues = new java.util.HashSet<>(values);
++                    allValues.addAll(Arrays.asList(presetValues));
++                    int newBits = Mth.ceillog2(allValues.size());
++
++                    if (newBits > configuration.bits()) {
++                        this.onResize(newBits, null);
++                    }
++
++                    break;
++                }
++
++                this.data.palette.idFor(presetValue);
++            }
++        }
++        // Paper end
+     }
+ 
+-    private PalettedContainer(IdMap<T> registry, PalettedContainer.Strategy strategy, PalettedContainer.Data<T> data) {
++    // Paper start - Anti-Xray - Add preset values
++    private PalettedContainer(IdMap<T> registry, PalettedContainer.Strategy strategy, PalettedContainer.Data<T> data, T @org.jetbrains.annotations.Nullable [] presetValues) {
++        this.presetValues = presetValues;
++        // Paper end - Anti-Xray
+         this.registry = registry;
+         this.strategy = strategy;
+         this.data = data;
+     }
+ 
+-    private PalettedContainer(PalettedContainer<T> other) {
++    private PalettedContainer(PalettedContainer<T> other, T @org.jetbrains.annotations.Nullable [] presetValues) { // Paper - Anti-Xray - Add preset values
++        this.presetValues = presetValues; // Paper - Anti-Xray - Add preset values
+         this.registry = other.registry;
+         this.strategy = other.strategy;
+         this.data = other.data.copy(this);
+     }
+ 
++    // Paper start - Anti-Xray - Add preset values
++    @Deprecated @io.papermc.paper.annotation.DoNotUse
+     public PalettedContainer(IdMap<T> registry, T palette, PalettedContainer.Strategy strategy) {
++        this(registry, palette, strategy, null);
++    }
++    public PalettedContainer(IdMap<T> registry, T palette, PalettedContainer.Strategy strategy, T @org.jetbrains.annotations.Nullable [] presetValues) {
++        this.presetValues = presetValues;
++        // Paper end - Anti-Xray
+         this.strategy = strategy;
+         this.registry = registry;
+         this.data = this.createOrReuseData(null, 0);
+@@ -101,11 +149,30 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
+     @Override
+     public synchronized int onResize(int bits, T objectAdded) { // Paper - synchronize
+         PalettedContainer.Data<T> data = this.data;
++        // Paper start - Anti-Xray - Add preset values
++        if (this.presetValues != null && objectAdded != null && data.configuration().factory() == PalettedContainer.Strategy.SINGLE_VALUE_PALETTE_FACTORY) {
++            int duplicates = 0;
++            List<T> presetValues = Arrays.asList(this.presetValues);
++            duplicates += presetValues.contains(objectAdded) ? 1 : 0;
++            duplicates += presetValues.contains(data.palette.valueFor(0)) ? 1 : 0;
++            bits = Mth.ceillog2((1 << this.strategy.calculateBitsForSerialization(this.registry, 1 << bits)) + presetValues.size() - duplicates);
++        }
++        // Paper end - Anti-Xray
+         PalettedContainer.Data<T> data1 = this.createOrReuseData(data, bits);
+         data1.copyFrom(data.palette, data.storage);
+         this.data = data1;
+-        return data1.palette.idFor(objectAdded);
++        // Paper start - Anti-Xray
++        this.addPresetValues();
++        return objectAdded == null ? -1 : data1.palette.idFor(objectAdded);
++    }
++    private void addPresetValues() {
++        if (this.presetValues != null && this.data.configuration().factory() != PalettedContainer.Strategy.GLOBAL_PALETTE_FACTORY) {
++            for (T presetValue : this.presetValues) {
++                this.data.palette.idFor(presetValue);
++            }
++        }
+     }
++    // Paper end - Anti-Xray
+ 
+     public synchronized T getAndSet(int x, int y, int z, T state) { // Paper - synchronize
+         this.acquire();
+@@ -172,24 +239,35 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
+             data.palette.read(buffer);
+             buffer.readLongArray(data.storage.getRaw());
+             this.data = data;
++            this.addPresetValues(); // Paper - Anti-Xray - Add preset values (inefficient, but this isn't used by the server)
+         } finally {
+             this.release();
+         }
+     }
+ 
++    // Paper start - Anti-Xray; Add chunk packet info
+     @Override
+-    public synchronized void write(FriendlyByteBuf buffer) { // Paper - synchronize
++    @Deprecated @io.papermc.paper.annotation.DoNotUse
++    public void write(FriendlyByteBuf buffer) {
++        this.write(buffer, null, 0);
++    }
++    @Override
++    public synchronized void write(FriendlyByteBuf buffer, @Nullable io.papermc.paper.antixray.ChunkPacketInfo<T> chunkPacketInfo, int chunkSectionIndex) { // Paper - Synchronize
+         this.acquire();
+ 
+         try {
+-            this.data.write(buffer);
++            this.data.write(buffer, chunkPacketInfo, chunkSectionIndex);
++            if (chunkPacketInfo != null) {
++                chunkPacketInfo.setPresetValues(chunkSectionIndex, this.presetValues);
++            }
++            // Paper end - Anti-Xray
+         } finally {
+             this.release();
+         }
+     }
+ 
+     private static <T> DataResult<PalettedContainer<T>> unpack(
+-        IdMap<T> registry, PalettedContainer.Strategy strategy, PalettedContainerRO.PackedData<T> packedData
++        IdMap<T> registry, PalettedContainer.Strategy strategy, PalettedContainerRO.PackedData<T> packedData, T defaultValue, T @org.jetbrains.annotations.Nullable [] presetValues // Paper - Anti-Xray - Add preset values
+     ) {
+         List<T> list = packedData.paletteEntries();
+         int size = strategy.size();
+@@ -222,7 +300,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
+             }
+         }
+ 
+-        return DataResult.success(new PalettedContainer<>(registry, strategy, configuration, bitStorage, list));
++        return DataResult.success(new PalettedContainer<>(registry, strategy, configuration, bitStorage, list, defaultValue, presetValues)); // Paper - Anti-Xray - Add preset values
+     }
+ 
+     @Override
+@@ -280,12 +358,12 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
+ 
+     @Override
+     public PalettedContainer<T> copy() {
+-        return new PalettedContainer<>(this);
++        return new PalettedContainer<>(this, this.presetValues); // Paper - Anti-Xray - Add preset values
+     }
+ 
+     @Override
+     public PalettedContainer<T> recreate() {
+-        return new PalettedContainer<>(this.registry, this.data.palette.valueFor(0), this.strategy);
++        return new PalettedContainer<>(this.registry, this.data.palette.valueFor(0), this.strategy, this.presetValues); // Paper - Anti-Xray - Add preset values
+     }
+ 
+     @Override
+@@ -324,9 +402,16 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
+             return 1 + this.palette.getSerializedSize() + VarInt.getByteSize(this.storage.getRaw().length) + this.storage.getRaw().length * 8;
+         }
+ 
+-        public void write(FriendlyByteBuf buffer) {
++        // Paper start - Anti-Xray - Add chunk packet info
++        public void write(FriendlyByteBuf buffer, @Nullable io.papermc.paper.antixray.ChunkPacketInfo<T> chunkPacketInfo, int chunkSectionIndex) {
+             buffer.writeByte(this.storage.getBits());
+             this.palette.write(buffer);
++            if (chunkPacketInfo != null) {
++                chunkPacketInfo.setBits(chunkSectionIndex, this.configuration.bits());
++                chunkPacketInfo.setPalette(chunkSectionIndex, this.palette);
++                chunkPacketInfo.setIndex(chunkSectionIndex, buffer.writerIndex() + VarInt.getByteSize(this.storage.getRaw().length));
++            }
++            // Paper end
+             buffer.writeLongArray(this.storage.getRaw());
+         }
+ 
+diff --git a/net/minecraft/world/level/chunk/PalettedContainerRO.java b/net/minecraft/world/level/chunk/PalettedContainerRO.java
+index bfbb1a2bb4abbb369a24f2f01439e9ea3e16794b..8d6ed8be4d93f7d4e6ea80c351020d88ee98aa4d 100644
+--- a/net/minecraft/world/level/chunk/PalettedContainerRO.java
++++ b/net/minecraft/world/level/chunk/PalettedContainerRO.java
+@@ -14,7 +14,10 @@ public interface PalettedContainerRO<T> {
+ 
+     void getAll(Consumer<T> consumer);
+ 
+-    void write(FriendlyByteBuf buffer);
++    // Paper start - Anti-Xray - Add chunk packet info
++    @Deprecated @io.papermc.paper.annotation.DoNotUse void write(FriendlyByteBuf buffer);
++    void write(FriendlyByteBuf buffer, @javax.annotation.Nullable io.papermc.paper.antixray.ChunkPacketInfo<T> chunkPacketInfo, int chunkSectionIndex);
++    // Paper end
+ 
+     int getSerializedSize();
+ 
+diff --git a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
+index 37437a86d74291fab1de9495008aafb15dfadce0..cf6e2053d81f7b0f8c8e58b9c0fad3285ebc047d 100644
+--- a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
++++ b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
+@@ -94,7 +94,7 @@ public record SerializableChunkData(
+     , @Nullable net.minecraft.nbt.Tag persistentDataContainer // CraftBukkit - persistentDataContainer
+ ) {
+     public static final Codec<PalettedContainer<BlockState>> BLOCK_STATE_CODEC = PalettedContainer.codecRW(
+-        Block.BLOCK_STATE_REGISTRY, BlockState.CODEC, PalettedContainer.Strategy.SECTION_STATES, Blocks.AIR.defaultBlockState()
++        Block.BLOCK_STATE_REGISTRY, BlockState.CODEC, PalettedContainer.Strategy.SECTION_STATES, Blocks.AIR.defaultBlockState(), null // Paper - Anti-Xray
+     );
+     private static final Logger LOGGER = LogUtils.getLogger();
+     private static final String TAG_UPGRADE_DATA = "UpgradeData";
+@@ -128,6 +128,7 @@ public record SerializableChunkData(
+ 
+     @Nullable
+     public static SerializableChunkData parse(LevelHeightAccessor levelHeightAccessor, RegistryAccess registries, CompoundTag tag) {
++        net.minecraft.server.level.ServerLevel serverLevel = (net.minecraft.server.level.ServerLevel) levelHeightAccessor; // Paper - Anti-Xray This is is seemingly only called from ChunkMap, where, we have a server level. We'll fight this later if needed.
+         if (!tag.contains("Status", 8)) {
+             return null;
+         } else {
+@@ -212,18 +213,21 @@ public record SerializableChunkData(
+             Codec<PalettedContainer<Holder<Biome>>> codec = makeBiomeCodecRW(registry); // CraftBukkit - read/write
+ 
+             for (int i2 = 0; i2 < list7.size(); i2++) {
+-                CompoundTag compound2 = list7.getCompound(i2);
++                CompoundTag compound2 = list7.getCompound(i2); final CompoundTag sectionData = compound2; // Paper - Anti-Xray - OBFHELPER
+                 int _byte = compound2.getByte("Y");
+                 LevelChunkSection levelChunkSection;
+                 if (_byte >= levelHeightAccessor.getMinSectionY() && _byte <= levelHeightAccessor.getMaxSectionY()) {
++                    // Paper start - Anti-Xray - Add preset block states
++                    BlockState[] presetBlockStates = serverLevel.chunkPacketBlockController.getPresetBlockStates(serverLevel, chunkPos, _byte);
+                     PalettedContainer<BlockState> palettedContainer;
+                     if (compound2.contains("block_states", 10)) {
+-                        palettedContainer = BLOCK_STATE_CODEC.parse(NbtOps.INSTANCE, compound2.getCompound("block_states"))
++                        Codec<PalettedContainer<BlockState>> blockStateCodec = presetBlockStates == null ? BLOCK_STATE_CODEC : PalettedContainer.codecRW(Block.BLOCK_STATE_REGISTRY, BlockState.CODEC, PalettedContainer.Strategy.SECTION_STATES, Blocks.AIR.defaultBlockState(), presetBlockStates); // Paper - Anti-Xray
++                        palettedContainer = blockStateCodec.parse(NbtOps.INSTANCE, sectionData.getCompound("block_states")) // Paper - Anti-Xray
+                             .promotePartial(string -> logErrors(chunkPos, _byte, string))
+                             .getOrThrow(SerializableChunkData.ChunkReadException::new);
+                     } else {
+                         palettedContainer = new PalettedContainer<>(
+-                            Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES
++                            Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES, presetBlockStates // Paper - Anti-Xray
+                         );
+                     }
+ 
+@@ -234,7 +238,7 @@ public record SerializableChunkData(
+                             .getOrThrow(SerializableChunkData.ChunkReadException::new);
+                     } else {
+                         palettedContainerRo = new PalettedContainer<>(
+-                            registry.asHolderIdMap(), registry.getOrThrow(Biomes.PLAINS), PalettedContainer.Strategy.SECTION_BIOMES
++                            registry.asHolderIdMap(), registry.getOrThrow(Biomes.PLAINS), PalettedContainer.Strategy.SECTION_BIOMES, null // Paper - Anti-Xray - Add preset biomes
+                         );
+                     }
+ 
+@@ -414,7 +418,7 @@ public record SerializableChunkData(
+ 
+     // CraftBukkit start - read/write
+     private static Codec<PalettedContainer<Holder<Biome>>> makeBiomeCodecRW(Registry<Biome> iregistry) {
+-        return PalettedContainer.codecRW(iregistry.asHolderIdMap(), iregistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, iregistry.getOrThrow(Biomes.PLAINS));
++        return PalettedContainer.codecRW(iregistry.asHolderIdMap(), iregistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, iregistry.getOrThrow(Biomes.PLAINS), null); // Paper - Anti-Xray - Add preset biomes
+     }
+     // CraftBukkit end
+ 
diff --git a/paper-server/patches/features/0005-Use-Velocity-compression-and-cipher-natives.patch b/paper-server/patches/features/0005-Use-Velocity-compression-and-cipher-natives.patch
new file mode 100644
index 0000000000..9011d342f0
--- /dev/null
+++ b/paper-server/patches/features/0005-Use-Velocity-compression-and-cipher-natives.patch
@@ -0,0 +1,354 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Andrew Steinborn <git@steinborn.me>
+Date: Mon, 26 Jul 2021 02:15:17 -0400
+Subject: [PATCH] Use Velocity compression and cipher natives
+
+
+diff --git a/net/minecraft/network/CipherDecoder.java b/net/minecraft/network/CipherDecoder.java
+index 429325ffb7db2b85ed271ddf3da64c6fdc593673..d764552aea825af7bbf0f47c9d88af527d9dbe62 100644
+--- a/net/minecraft/network/CipherDecoder.java
++++ b/net/minecraft/network/CipherDecoder.java
+@@ -7,14 +7,30 @@ import java.util.List;
+ import javax.crypto.Cipher;
+ 
+ public class CipherDecoder extends MessageToMessageDecoder<ByteBuf> {
+-    private final CipherBase cipher;
++    private final com.velocitypowered.natives.encryption.VelocityCipher cipher; // Paper - Use Velocity cipher
+ 
+-    public CipherDecoder(Cipher cipher) {
+-        this.cipher = new CipherBase(cipher);
++    public CipherDecoder(com.velocitypowered.natives.encryption.VelocityCipher cipher) { // Paper - Use Velocity cipher
++        this.cipher = cipher; // Paper - Use Velocity cipher
+     }
+ 
+     @Override
+     protected void decode(ChannelHandlerContext context, ByteBuf in, List<Object> out) throws Exception {
+-        out.add(this.cipher.decipher(context, in));
++        // Paper start - Use Velocity cipher
++        ByteBuf compatible = com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible(context.alloc(), this.cipher, in);
++        try {
++            this.cipher.process(compatible);
++            out.add(compatible);
++        } catch (Exception e) {
++            compatible.release(); // compatible will never be used if we throw an exception
++            throw e;
++        }
++        // Paper end - Use Velocity cipher
+     }
++
++    // Paper start - Use Velocity cipher
++    @Override
++    public void handlerRemoved(ChannelHandlerContext ctx) {
++        this.cipher.close();
++    }
++    // Paper end - Use Velocity cipher
+ }
+diff --git a/net/minecraft/network/CipherEncoder.java b/net/minecraft/network/CipherEncoder.java
+index 992b9c7aed57ce29cdd2b4f66737d39db214f0cf..b927c85923147d2b346e892a3e4deee48b3d073e 100644
+--- a/net/minecraft/network/CipherEncoder.java
++++ b/net/minecraft/network/CipherEncoder.java
+@@ -5,15 +5,31 @@ import io.netty.channel.ChannelHandlerContext;
+ import io.netty.handler.codec.MessageToByteEncoder;
+ import javax.crypto.Cipher;
+ 
+-public class CipherEncoder extends MessageToByteEncoder<ByteBuf> {
+-    private final CipherBase cipher;
++public class CipherEncoder extends io.netty.handler.codec.MessageToMessageEncoder<ByteBuf> { // Paper - Use Velocity cipher; change superclass
++    private final com.velocitypowered.natives.encryption.VelocityCipher cipher; // Paper - Use Velocity cipher
+ 
+-    public CipherEncoder(Cipher cipher) {
+-        this.cipher = new CipherBase(cipher);
++    public CipherEncoder(com.velocitypowered.natives.encryption.VelocityCipher cipher) { // Paper - Use Velocity cipher
++        this.cipher = cipher; // Paper - Use Velocity cipher
+     }
+ 
++    // Paper start - Use Velocity cipher
+     @Override
+-    protected void encode(ChannelHandlerContext context, ByteBuf message, ByteBuf out) throws Exception {
+-        this.cipher.encipher(message, out);
++    protected void encode(ChannelHandlerContext context, ByteBuf message, java.util.List<Object> list) throws Exception {
++        ByteBuf compatible = com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible(context.alloc(), this.cipher, message);
++        try {
++            this.cipher.process(compatible);
++            list.add(compatible);
++        } catch (Exception e) {
++            compatible.release(); // compatible will never be used if we throw an exception
++            throw e;
++        }
++        // Paper end - Use Velocity cipher
+     }
++
++    // Paper start - Use Velocity cipher
++    @Override
++    public void handlerRemoved(ChannelHandlerContext ctx) {
++        this.cipher.close();
++    }
++    // Paper end - Use Velocity cipher
+ }
+diff --git a/net/minecraft/network/CompressionDecoder.java b/net/minecraft/network/CompressionDecoder.java
+index fcf0e557fbcbd5f306625096d859578fe8511734..2c7f935fcecb24a4394fdde523219a5b5984a673 100644
+--- a/net/minecraft/network/CompressionDecoder.java
++++ b/net/minecraft/network/CompressionDecoder.java
+@@ -12,14 +12,22 @@ import java.util.zip.Inflater;
+ public class CompressionDecoder extends ByteToMessageDecoder {
+     public static final int MAXIMUM_COMPRESSED_LENGTH = 2097152;
+     public static final int MAXIMUM_UNCOMPRESSED_LENGTH = 8388608;
++    private com.velocitypowered.natives.compression.VelocityCompressor compressor; // Paper - Use Velocity cipher
+     private Inflater inflater;
+     private int threshold;
+     private boolean validateDecompressed;
+ 
++    // Paper start - Use Velocity cipher
++    @io.papermc.paper.annotation.DoNotUse
+     public CompressionDecoder(int threshold, boolean validateDecompressed) {
++        this(null, threshold, validateDecompressed);
++    }
++    public CompressionDecoder(com.velocitypowered.natives.compression.VelocityCompressor compressor, int threshold, boolean validateDecompressed) {
+         this.threshold = threshold;
+         this.validateDecompressed = validateDecompressed;
+-        this.inflater = new Inflater();
++        this.inflater = compressor == null ? new Inflater() : null;
++        this.compressor = compressor;
++        // Paper end - Use Velocity cipher
+     }
+ 
+     @Override
+@@ -39,14 +47,42 @@ public class CompressionDecoder extends ByteToMessageDecoder {
+                     }
+                 }
+ 
++                if (inflater != null) { // Paper - Use Velocity cipher; fallback to vanilla inflater
+                 this.setupInflaterInput(in);
+                 ByteBuf byteBuf = this.inflate(context, i);
+                 this.inflater.reset();
+                 out.add(byteBuf);
++                return; // Paper - Use Velocity cipher
++                } // Paper - use velocity compression
++
++                // Paper start - Use Velocity cipher
++                int claimedUncompressedSize = i; // OBFHELPER
++                ByteBuf compatibleIn = com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible(context.alloc(), this.compressor, in);
++                ByteBuf uncompressed = com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer(context.alloc(), this.compressor, claimedUncompressedSize);
++                try {
++                    this.compressor.inflate(compatibleIn, uncompressed, claimedUncompressedSize);
++                    out.add(uncompressed);
++                    in.clear();
++                } catch (Exception e) {
++                    uncompressed.release();
++                    throw e;
++                } finally {
++                    compatibleIn.release();
++                }
++                // Paper end - Use Velocity cipher
+             }
+         }
+     }
+ 
++    // Paper start - Use Velocity cipher
++    @Override
++    public void handlerRemoved0(ChannelHandlerContext ctx) {
++        if (this.compressor != null) {
++            this.compressor.close();
++        }
++    }
++    // Paper end - Use Velocity cipher
++
+     private void setupInflaterInput(ByteBuf buffer) {
+         ByteBuffer byteBuffer;
+         if (buffer.nioBufferCount() > 0) {
+@@ -81,7 +117,13 @@ public class CompressionDecoder extends ByteToMessageDecoder {
+         }
+     }
+ 
+-    public void setThreshold(int threshold, boolean validateDecompressed) {
++    // Paper start - Use Velocity cipher
++    public void setThreshold(com.velocitypowered.natives.compression.VelocityCompressor compressor, int threshold, boolean validateDecompressed) {
++        if (this.compressor == null && compressor != null) { // Only re-configure once. Re-reconfiguring would require closing the native compressor.
++            this.compressor = compressor;
++            this.inflater = null;
++        }
++        // Paper end - Use Velocity cipher
+         this.threshold = threshold;
+         this.validateDecompressed = validateDecompressed;
+     }
+diff --git a/net/minecraft/network/CompressionEncoder.java b/net/minecraft/network/CompressionEncoder.java
+index bc674b08a41d5529fe06c6d3f077051cf4138f73..ea8a894158c44c2e7943dea43ecd8e1f0075b18f 100644
+--- a/net/minecraft/network/CompressionEncoder.java
++++ b/net/minecraft/network/CompressionEncoder.java
+@@ -6,17 +6,31 @@ import io.netty.handler.codec.MessageToByteEncoder;
+ import java.util.zip.Deflater;
+ 
+ public class CompressionEncoder extends MessageToByteEncoder<ByteBuf> {
+-    private final byte[] encodeBuf = new byte[8192];
++    @javax.annotation.Nullable private final byte[] encodeBuf; // Paper - Use Velocity cipher
++    @javax.annotation.Nullable // Paper - Use Velocity cipher
+     private final Deflater deflater;
++    @javax.annotation.Nullable private final com.velocitypowered.natives.compression.VelocityCompressor compressor; // Paper - Use Velocity cipher
+     private int threshold;
+ 
++    // Paper start - Use Velocity cipher
+     public CompressionEncoder(int threshold) {
++        this(null, threshold);
++    }
++    public CompressionEncoder(@javax.annotation.Nullable com.velocitypowered.natives.compression.VelocityCompressor compressor, int threshold) {
+         this.threshold = threshold;
+-        this.deflater = new Deflater();
++        if (compressor == null) {
++            this.encodeBuf = new byte[8192];
++            this.deflater = new Deflater();
++        } else {
++            this.encodeBuf = null;
++            this.deflater = null;
++        }
++        this.compressor = compressor;
++        // Paper end - Use Velocity cipher
+     }
+ 
+     @Override
+-    protected void encode(ChannelHandlerContext context, ByteBuf encodingByteBuf, ByteBuf byteBuf) {
++    protected void encode(ChannelHandlerContext context, ByteBuf encodingByteBuf, ByteBuf byteBuf) throws Exception { // Paper - Use Velocity cipher
+         int i = encodingByteBuf.readableBytes();
+         if (i > 8388608) {
+             throw new IllegalArgumentException("Packet too big (is " + i + ", should be less than 8388608)");
+@@ -25,6 +39,7 @@ public class CompressionEncoder extends MessageToByteEncoder<ByteBuf> {
+                 VarInt.write(byteBuf, 0);
+                 byteBuf.writeBytes(encodingByteBuf);
+             } else {
++                if (this.deflater != null) { // Paper - Use Velocity cipher
+                 byte[] bytes = new byte[i];
+                 encodingByteBuf.readBytes(bytes);
+                 VarInt.write(byteBuf, bytes.length);
+@@ -37,6 +52,17 @@ public class CompressionEncoder extends MessageToByteEncoder<ByteBuf> {
+                 }
+ 
+                 this.deflater.reset();
++                    // Paper start - Use Velocity cipher
++                    return;
++                }
++
++                VarInt.write(byteBuf, i);
++                final ByteBuf compatibleIn = com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible(context.alloc(), this.compressor, encodingByteBuf);
++                try {
++                    this.compressor.deflate(compatibleIn, byteBuf);
++                } finally {
++                    compatibleIn.release();
++                }
+             }
+         }
+     }
+@@ -48,4 +74,31 @@ public class CompressionEncoder extends MessageToByteEncoder<ByteBuf> {
+     public void setThreshold(int threshold) {
+         this.threshold = threshold;
+     }
++
++    // Paper start - Use Velocity cipher
++    @Override
++    protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg, boolean preferDirect) throws Exception {
++        if (this.compressor != null) {
++            // We allocate bytes to be compressed plus 1 byte. This covers two cases:
++            //
++            // - Compression
++            //    According to https://github.com/ebiggers/libdeflate/blob/master/libdeflate.h#L103,
++            //    if the data compresses well (and we do not have some pathological case) then the maximum
++            //    size the compressed size will ever be is the input size minus one.
++            // - Uncompressed
++            //    This is fairly obvious - we will then have one more than the uncompressed size.
++            final int initialBufferSize = msg.readableBytes() + 1;
++            return com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer(ctx.alloc(), this.compressor, initialBufferSize);
++        }
++
++        return super.allocateBuffer(ctx, msg, preferDirect);
++    }
++
++    @Override
++    public void handlerRemoved(ChannelHandlerContext ctx) {
++        if (this.compressor != null) {
++            this.compressor.close();
++        }
++    }
++    // Paper end - Use Velocity cipher
+ }
+diff --git a/net/minecraft/network/Connection.java b/net/minecraft/network/Connection.java
+index ad8f8428b75e37097487cdfbd0db2421ee4cbe37..208efae06c7c44f220d4192219a86ec55c98a2fe 100644
+--- a/net/minecraft/network/Connection.java
++++ b/net/minecraft/network/Connection.java
+@@ -772,11 +772,22 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
+         return connection;
+     }
+ 
+-    public void setEncryptionKey(Cipher decryptingCipher, Cipher encryptingCipher) {
+-        this.encrypted = true;
+-        this.channel.pipeline().addBefore("splitter", "decrypt", new CipherDecoder(decryptingCipher));
+-        this.channel.pipeline().addBefore("prepender", "encrypt", new CipherEncoder(encryptingCipher));
++    // Paper start - Use Velocity cipher
++    public void setEncryptionKey(javax.crypto.SecretKey key) throws net.minecraft.util.CryptException {
++        if (!this.encrypted) {
++            try {
++                com.velocitypowered.natives.encryption.VelocityCipher decryptionCipher = com.velocitypowered.natives.util.Natives.cipher.get().forDecryption(key);
++                com.velocitypowered.natives.encryption.VelocityCipher encryptionCipher = com.velocitypowered.natives.util.Natives.cipher.get().forEncryption(key);
++
++                this.encrypted = true;
++                this.channel.pipeline().addBefore("splitter", "decrypt", new CipherDecoder(decryptionCipher));
++                this.channel.pipeline().addBefore("prepender", "encrypt", new CipherEncoder(encryptionCipher));
++            } catch (java.security.GeneralSecurityException e) {
++                throw new net.minecraft.util.CryptException(e);
++            }
++        }
+     }
++    // Paper end - Use Velocity cipher
+ 
+     public boolean isEncrypted() {
+         return this.encrypted;
+@@ -815,16 +826,17 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
+     // Paper end - add proper async disconnect
+     public void setupCompression(int threshold, boolean validateDecompressed) {
+         if (threshold >= 0) {
++            com.velocitypowered.natives.compression.VelocityCompressor compressor = com.velocitypowered.natives.util.Natives.compress.get().create(io.papermc.paper.configuration.GlobalConfiguration.get().misc.compressionLevel.or(-1)); // Paper - Use Velocity cipher
+             if (this.channel.pipeline().get("decompress") instanceof CompressionDecoder compressionDecoder) {
+-                compressionDecoder.setThreshold(threshold, validateDecompressed);
++                compressionDecoder.setThreshold(compressor, threshold, validateDecompressed); // Paper - Use Velocity cipher
+             } else {
+-                this.channel.pipeline().addAfter("splitter", "decompress", new CompressionDecoder(threshold, validateDecompressed));
++                this.channel.pipeline().addAfter("splitter", "decompress", new CompressionDecoder(compressor, threshold, validateDecompressed)); // Paper - Use Velocity cipher
+             }
+ 
+             if (this.channel.pipeline().get("compress") instanceof CompressionEncoder compressionEncoder) {
+                 compressionEncoder.setThreshold(threshold);
+             } else {
+-                this.channel.pipeline().addAfter("prepender", "compress", new CompressionEncoder(threshold));
++                this.channel.pipeline().addAfter("prepender", "compress", new CompressionEncoder(compressor, threshold)); // Paper - Use Velocity cipher
+             }
+             this.channel.pipeline().fireUserEventTriggered(io.papermc.paper.network.ConnectionEvent.COMPRESSION_THRESHOLD_SET); // Paper - Add Channel initialization listeners
+         } else {
+diff --git a/net/minecraft/server/network/ServerConnectionListener.java b/net/minecraft/server/network/ServerConnectionListener.java
+index 7de11ba404f0b60e7f7b7c16954811a343688219..bd07e6a5aa1883786d789ea71711a0c0c0a95c26 100644
+--- a/net/minecraft/server/network/ServerConnectionListener.java
++++ b/net/minecraft/server/network/ServerConnectionListener.java
+@@ -108,6 +108,10 @@ public class ServerConnectionListener {
+                 LOGGER.warn("Using HAProxy, please ensure the server port is adequately firewalled.");
+             }
+             // Paper end - Warn people with console access that HAProxy is in use.
++            // Paper start - Use Velocity cipher
++            ServerConnectionListener.LOGGER.info("Paper: Using " + com.velocitypowered.natives.util.Natives.compress.getLoadedVariant() + " compression from Velocity.");
++            ServerConnectionListener.LOGGER.info("Paper: Using " + com.velocitypowered.natives.util.Natives.cipher.getLoadedVariant() + " cipher from Velocity.");
++            // Paper end - Use Velocity cipher
+ 
+             this.channels
+                 .add(
+diff --git a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
+index bb28453d230921d662ed69c8dd48021f428ef060..6689aeacf50d1498e8d23cce696fb4fecbd1cf39 100644
+--- a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
++++ b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
+@@ -276,11 +276,9 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
+             }
+ 
+             SecretKey secretKey = packet.getSecretKey(_private);
+-            Cipher cipher = Crypt.getCipher(2, secretKey);
+-            Cipher cipher1 = Crypt.getCipher(1, secretKey);
+             string = new BigInteger(Crypt.digestData("", this.server.getKeyPair().getPublic(), secretKey)).toString(16);
+             this.state = ServerLoginPacketListenerImpl.State.AUTHENTICATING;
+-            this.connection.setEncryptionKey(cipher, cipher1);
++            this.connection.setEncryptionKey(secretKey); // Paper - Use Velocity cipher
+         } catch (CryptException var7) {
+             throw new IllegalStateException("Protocol error", var7);
+         }
diff --git a/feature-patches/1046-Optimize-Collision-to-not-load-chunks.patch b/paper-server/patches/features/0006-Optimize-Collision-to-not-load-chunks.patch
similarity index 53%
rename from feature-patches/1046-Optimize-Collision-to-not-load-chunks.patch
rename to paper-server/patches/features/0006-Optimize-Collision-to-not-load-chunks.patch
index 8026cceeb7..ca6297da96 100644
--- a/feature-patches/1046-Optimize-Collision-to-not-load-chunks.patch
+++ b/paper-server/patches/features/0006-Optimize-Collision-to-not-load-chunks.patch
@@ -13,44 +13,44 @@ 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/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/entity/Entity.java
-+++ b/src/main/java/net/minecraft/world/entity/Entity.java
-@@ -0,0 +0,0 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java
+index 334859c5ff7023c730513301cc11c9837b2c7823..45f69a914d5a0565196c4105d61541047301470f 100644
+--- a/net/minecraft/world/entity/Entity.java
++++ b/net/minecraft/world/entity/Entity.java
+@@ -218,6 +218,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;
+     private org.bukkit.craftbukkit.entity.CraftEntity bukkitEntity;
  
-     public CraftEntity getBukkitEntity() {
-diff --git a/src/main/java/net/minecraft/world/level/BlockCollisions.java b/src/main/java/net/minecraft/world/level/BlockCollisions.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/BlockCollisions.java
-+++ b/src/main/java/net/minecraft/world/level/BlockCollisions.java
-@@ -0,0 +0,0 @@ public class BlockCollisions<T> extends AbstractIterator<T> {
+     public org.bukkit.craftbukkit.entity.CraftEntity getBukkitEntity() {
+diff --git a/net/minecraft/world/level/BlockCollisions.java b/net/minecraft/world/level/BlockCollisions.java
+index fd2c338db43aad070cc32c24891b40599c544ac9..2861ea4b699d403b1245f8be5a62503d366ded65 100644
+--- a/net/minecraft/world/level/BlockCollisions.java
++++ b/net/minecraft/world/level/BlockCollisions.java
+@@ -80,16 +80,37 @@ public class BlockCollisions<T> extends AbstractIterator<T> {
      @Override
      protected T computeNext() {
          while (this.cursor.advance()) {
 -            int i = this.cursor.nextX();
--            int j = this.cursor.nextY();
--            int k = this.cursor.nextZ();
+-            int i1 = this.cursor.nextY();
+-            int i2 = 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))
++            int i1 = this.cursor.nextY(); final int y = i1; // Paper - OBFHELPER
++            int i2 = this.cursor.nextZ(); final int z = i2; // Paper - OBFHELPER
+             int nextType = this.cursor.getNextType();
+             if (nextType != 3) {
+-                BlockGetter chunk = this.getChunk(i, i2);
+-                if (chunk != null) {
+-                    this.pos.set(i, i1, i2);
+-                    BlockState blockState = chunk.getBlockState(this.pos);
+-                    if ((!this.onlySuffocatingBlocks || blockState.isSuffocating(chunk, this.pos))
 +                // 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;
++                    @Nullable final Entity source = this.context instanceof net.minecraft.world.phys.shapes.EntityCollisionContext entityContext ? entityContext.getEntity() : null;
++                    final 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) {
@@ -72,25 +72,24 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                    }
 +                    if (true // onlySuffocatingBlocks is only true on the client, so we don't care about it here
 +                    // Paper end - ensure we don't load chunks
-                         && (l != 1 || blockState.hasLargeCollisionShape())
-                         && (l != 2 || blockState.is(Blocks.MOVING_PISTON))) {
-                         VoxelShape voxelShape = this.context.getCollisionShape(blockState, this.collisionGetter, this.pos);
-diff --git a/src/main/java/net/minecraft/world/level/CollisionGetter.java b/src/main/java/net/minecraft/world/level/CollisionGetter.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/CollisionGetter.java
-+++ b/src/main/java/net/minecraft/world/level/CollisionGetter.java
-@@ -0,0 +0,0 @@ public interface CollisionGetter extends BlockGetter {
+                         && (nextType != 1 || blockState.hasLargeCollisionShape())
+                         && (nextType != 2 || blockState.is(Blocks.MOVING_PISTON))) {
+                         VoxelShape collisionShape = this.context.getCollisionShape(blockState, this.collisionGetter, this.pos);
+diff --git a/net/minecraft/world/level/CollisionGetter.java b/net/minecraft/world/level/CollisionGetter.java
+index cb54c3aadd8f3c719d3f7ef1fda4aa517919b7c3..844f76a38884e823a558fe59c421ffd4711f80b4 100644
+--- a/net/minecraft/world/level/CollisionGetter.java
++++ b/net/minecraft/world/level/CollisionGetter.java
+@@ -50,11 +50,13 @@ public interface CollisionGetter extends BlockGetter {
      }
  
-     default boolean noCollision(@Nullable Entity entity, AABB box, boolean checkFluid) {
--        for (VoxelShape voxelShape : checkFluid ? this.getBlockAndLiquidCollisions(entity, box) : this.getBlockCollisions(entity, box)) {
+     default boolean noCollision(@Nullable Entity entity, AABB collisionBox, boolean checkLiquid) {
 +        try { if (entity != null) entity.collisionLoadChunks = true; // Paper
-+            for (VoxelShape voxelShape : checkFluid ? this.getBlockAndLiquidCollisions(entity, box) : this.getBlockCollisions(entity, box)) {
+         for (VoxelShape voxelShape : checkLiquid ? this.getBlockAndLiquidCollisions(entity, collisionBox) : this.getBlockCollisions(entity, collisionBox)) {
              if (!voxelShape.isEmpty()) {
                  return false;
              }
          }
 +        } finally { if (entity != null) entity.collisionLoadChunks = false; } // Paper
  
-         if (!this.getEntityCollisions(entity, box).isEmpty()) {
+         if (!this.getEntityCollisions(entity, collisionBox).isEmpty()) {
              return false;
diff --git a/paper-server/patches/features/0007-Optimize-GoalSelector-Goal.Flag-Set-operations.patch b/paper-server/patches/features/0007-Optimize-GoalSelector-Goal.Flag-Set-operations.patch
new file mode 100644
index 0000000000..8d68059b74
--- /dev/null
+++ b/paper-server/patches/features/0007-Optimize-GoalSelector-Goal.Flag-Set-operations.patch
@@ -0,0 +1,168 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Spottedleaf <Spottedleaf@users.noreply.github.com>
+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/net/minecraft/world/entity/ai/goal/Goal.java b/net/minecraft/world/entity/ai/goal/Goal.java
+index d4f8387254a65b25fc808932a069298d0f8da091..5f5bf0e710ecff09a571091e5a923332be70cb74 100644
+--- a/net/minecraft/world/entity/ai/goal/Goal.java
++++ b/net/minecraft/world/entity/ai/goal/Goal.java
+@@ -7,7 +7,15 @@ import net.minecraft.world.entity.Entity;
+ import net.minecraft.world.level.Level;
+ 
+ public abstract class Goal {
+-    private final EnumSet<Goal.Flag> flags = EnumSet.noneOf(Goal.Flag.class);
++    private final ca.spottedleaf.moonrise.common.set.OptimizedSmallEnumSet<net.minecraft.world.entity.ai.goal.Goal.Flag> goalTypes = new ca.spottedleaf.moonrise.common.set.OptimizedSmallEnumSet<>(Goal.Flag.class); // Paper - remove streams from GoalSelector
++
++    // Paper start - remove streams from GoalSelector; make sure types are not empty
++    protected Goal() {
++        if (this.goalTypes.size() == 0) {
++            this.goalTypes.addUnchecked(Flag.UNKNOWN_BEHAVIOR);
++        }
++    }
++    // Paper end - remove streams from GoalSelector
+ 
+     public abstract boolean canUse();
+ 
+@@ -33,8 +41,13 @@ public abstract class Goal {
+     }
+ 
+     public void setFlags(EnumSet<Goal.Flag> flagSet) {
+-        this.flags.clear();
+-        this.flags.addAll(flagSet);
++        // Paper start - remove streams from GoalSelector
++        this.goalTypes.clear();
++        this.goalTypes.addAllUnchecked(flagSet);
++        if (this.goalTypes.size() == 0) {
++            this.goalTypes.addUnchecked(Flag.UNKNOWN_BEHAVIOR);
++        }
++        // Paper end - remove streams from GoalSelector
+     }
+ 
+     @Override
+@@ -42,18 +55,20 @@ public abstract class Goal {
+         return this.getClass().getSimpleName();
+     }
+ 
+-    public EnumSet<Goal.Flag> getFlags() {
+-        return this.flags;
++    // Paper start - remove streams from GoalSelector
++    public ca.spottedleaf.moonrise.common.set.OptimizedSmallEnumSet<Goal.Flag> getFlags() {
++        return this.goalTypes;
++        // Paper end - remove streams from GoalSelector
+     }
+ 
+ 
+     // Paper start - Mob Goal API
+     public boolean hasFlag(final Goal.Flag flag) {
+-        return this.flags.contains(flag);
++        return this.goalTypes.hasElement(flag);
+     }
+ 
+     public void addFlag(final Goal.Flag flag) {
+-        this.flags.add(flag);
++        this.goalTypes.addUnchecked(flag);
+     }
+     // Paper end - Mob Goal API
+ 
+diff --git a/net/minecraft/world/entity/ai/goal/GoalSelector.java b/net/minecraft/world/entity/ai/goal/GoalSelector.java
+index eeba224bd575451ba6023df65ef9d9b97f7f1c71..a927c2790c8ab9ccaa7161b970e10b0b44817dd8 100644
+--- a/net/minecraft/world/entity/ai/goal/GoalSelector.java
++++ b/net/minecraft/world/entity/ai/goal/GoalSelector.java
+@@ -24,7 +24,8 @@ public class GoalSelector {
+     };
+     private final Map<Goal.Flag, WrappedGoal> lockedFlags = new EnumMap<>(Goal.Flag.class);
+     private final Set<WrappedGoal> availableGoals = new ObjectLinkedOpenHashSet<>();
+-    private final EnumSet<Goal.Flag> disabledFlags = EnumSet.noneOf(Goal.Flag.class);
++    private static final Goal.Flag[] GOAL_FLAG_VALUES = Goal.Flag.values(); // Paper - remove streams from GoalSelector
++    private final ca.spottedleaf.moonrise.common.set.OptimizedSmallEnumSet<net.minecraft.world.entity.ai.goal.Goal.Flag> goalTypes = new ca.spottedleaf.moonrise.common.set.OptimizedSmallEnumSet<>(Goal.Flag.class); // Paper - remove streams from GoalSelector
+     private int curRate; // Paper - EAR 2
+ 
+     public void addGoal(int priority, Goal goal) {
+@@ -62,18 +63,18 @@ public class GoalSelector {
+         this.availableGoals.removeIf(wrappedGoal1 -> wrappedGoal1.getGoal() == goal);
+     }
+ 
+-    private static boolean goalContainsAnyFlags(WrappedGoal goal, EnumSet<Goal.Flag> flag) {
+-        for (Goal.Flag flag1 : goal.getFlags()) {
+-            if (flag.contains(flag1)) {
+-                return true;
+-            }
+-        }
+-
+-        return false;
++    // Paper start - Perf: optimize goal types
++    private static boolean goalContainsAnyFlags(WrappedGoal goal, ca.spottedleaf.moonrise.common.set.OptimizedSmallEnumSet<Goal.Flag> flags) {
++        return goal.getFlags().hasCommonElements(flags);
+     }
+ 
+     private static boolean goalCanBeReplacedForAllFlags(WrappedGoal goal, Map<Goal.Flag, WrappedGoal> flag) {
+-        for (Goal.Flag flag1 : goal.getFlags()) {
++        long flagIterator = goal.getFlags().getBackingSet();
++        int wrappedGoalSize = goal.getFlags().size();
++        for (int i = 0; i < wrappedGoalSize; ++i) {
++            final Goal.Flag flag1 = GOAL_FLAG_VALUES[Long.numberOfTrailingZeros(flagIterator)];
++            flagIterator ^= ca.spottedleaf.concurrentutil.util.IntegerUtil.getTrailingBit(flagIterator);
++            // Paper end - Perf: optimize goal types
+             if (!flag.getOrDefault(flag1, NO_GOAL).canBeReplacedBy(goal)) {
+                 return false;
+             }
+@@ -87,7 +88,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();
+             }
+         }
+@@ -97,11 +98,14 @@ public class GoalSelector {
+         profilerFiller.push("goalUpdate");
+ 
+         for (WrappedGoal wrappedGoalx : this.availableGoals) {
+-            if (!wrappedGoalx.isRunning()
+-                && !goalContainsAnyFlags(wrappedGoalx, this.disabledFlags)
+-                && goalCanBeReplacedForAllFlags(wrappedGoalx, this.lockedFlags)
+-                && wrappedGoalx.canUse()) {
+-                for (Goal.Flag flag : wrappedGoalx.getFlags()) {
++            // Paper start
++            if (!wrappedGoalx.isRunning() && !goalContainsAnyFlags(wrappedGoalx, this.goalTypes) && goalCanBeReplacedForAllFlags(wrappedGoalx, this.lockedFlags) && wrappedGoalx.canUse()) {
++                long flagIterator = wrappedGoalx.getFlags().getBackingSet();
++                int wrappedGoalSize = wrappedGoalx.getFlags().size();
++                for (int i = 0; i < wrappedGoalSize; ++i) {
++                    final Goal.Flag flag = GOAL_FLAG_VALUES[Long.numberOfTrailingZeros(flagIterator)];
++                    flagIterator ^= ca.spottedleaf.concurrentutil.util.IntegerUtil.getTrailingBit(flagIterator);
++                    // Paper end
+                     WrappedGoal wrappedGoal1 = this.lockedFlags.getOrDefault(flag, NO_GOAL);
+                     wrappedGoal1.stop();
+                     this.lockedFlags.put(flag, wrappedGoalx);
+@@ -133,11 +137,11 @@ public class GoalSelector {
+     }
+ 
+     public void disableControlFlag(Goal.Flag flag) {
+-        this.disabledFlags.add(flag);
++        this.goalTypes.addUnchecked(flag); // Paper - remove streams from GoalSelector
+     }
+ 
+     public void enableControlFlag(Goal.Flag flag) {
+-        this.disabledFlags.remove(flag);
++        this.goalTypes.removeUnchecked(flag); // Paper - remove streams from GoalSelector
+     }
+ 
+     public void setControlFlag(Goal.Flag flag, boolean enabled) {
+diff --git a/net/minecraft/world/entity/ai/goal/WrappedGoal.java b/net/minecraft/world/entity/ai/goal/WrappedGoal.java
+index 4bdbd323b642ed3422948fe24780be8b503602dc..2c2ab6a1df9d3d23773e44ce4041cc1c21b55163 100644
+--- a/net/minecraft/world/entity/ai/goal/WrappedGoal.java
++++ b/net/minecraft/world/entity/ai/goal/WrappedGoal.java
+@@ -69,7 +69,7 @@ public class WrappedGoal extends Goal {
+     }
+ 
+     @Override
+-    public EnumSet<Goal.Flag> getFlags() {
++    public ca.spottedleaf.moonrise.common.set.OptimizedSmallEnumSet<Goal.Flag> getFlags() { // Paper - remove streams from GoalSelector
+         return this.goal.getFlags();
+     }
+ 
diff --git a/paper-server/patches/features/0008-Optimize-Voxel-Shape-Merging.patch b/paper-server/patches/features/0008-Optimize-Voxel-Shape-Merging.patch
new file mode 100644
index 0000000000..b9255fa095
--- /dev/null
+++ b/paper-server/patches/features/0008-Optimize-Voxel-Shape-Merging.patch
@@ -0,0 +1,122 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <aikar@aikar.co>
+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/net/minecraft/world/phys/shapes/IndirectMerger.java b/net/minecraft/world/phys/shapes/IndirectMerger.java
+index 60b56a5086b8aad0fad693f686b89138b3a4c80d..5b079ec43c259368d0eed4e8385880712abda4f7 100644
+--- a/net/minecraft/world/phys/shapes/IndirectMerger.java
++++ b/net/minecraft/world/phys/shapes/IndirectMerger.java
+@@ -10,12 +10,32 @@ 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 = {1, 1};
++    private static final int[] INFINITE_B_0 = {0, 0};
++    private static final int[] INFINITE_C = {0, 1};
++    // Paper end
+ 
+     public IndirectMerger(DoubleList lower, DoubleList upper, boolean excludeUpper, boolean excludeLower) {
+         double d = Double.NaN;
+         int size = lower.size();
+         int size1 = upper.size();
+         int i = size + size1;
++        // Paper start - optimize common path of infinity doublelist
++        double tail = lower.getDouble(size - 1);
++        double head = lower.getDouble(0);
++        if (head == Double.NEGATIVE_INFINITY && tail == Double.POSITIVE_INFINITY && !excludeUpper && !excludeLower && (size == 2 || size == 4)) {
++            this.result = upper.toDoubleArray();
++            this.resultLength = upper.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[i];
+         this.firstIndices = new int[i];
+         this.secondIndices = new int[i];
+diff --git a/net/minecraft/world/phys/shapes/Shapes.java b/net/minecraft/world/phys/shapes/Shapes.java
+index e1b4c4b53844b0755e0640a05e8782fd9a7700a2..e759221fb54aa510d2d8bbba47e1d794367aec6d 100644
+--- a/net/minecraft/world/phys/shapes/Shapes.java
++++ b/net/minecraft/world/phys/shapes/Shapes.java
+@@ -279,9 +279,22 @@ public final class Shapes {
+     }
+ 
+     @VisibleForTesting
+-    protected static IndexMerger createIndexMerger(int size, DoubleList list1, DoubleList list2, boolean excludeUpper, boolean excludeLower) {
++    private static IndexMerger createIndexMerger(int size, DoubleList list1, DoubleList list2, boolean excludeUpper, boolean excludeLower) { // 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 (list1.getDouble(0) == Double.NEGATIVE_INFINITY && list1.getDouble(list1.size() - 1) == Double.POSITIVE_INFINITY) {
++            return new IndirectMerger(list1, list2, excludeUpper, excludeLower);
++        }
++        // Split out rest to hopefully inline the above
++        return lessCommonMerge(size, list1, list2, excludeUpper, excludeLower);
++    }
++
++    private static IndexMerger lessCommonMerge(int size, DoubleList list1, DoubleList list2, boolean excludeUpper, boolean excludeLower) {
++        // Paper end - fast track the most common scenario
+         int i = list1.size() - 1;
+         int i1 = list2.size() - 1;
++        // Paper note - Rewrite below as optimized order if instead of nasty ternary
+         if (list1 instanceof CubePointRange && list2 instanceof CubePointRange) {
+             long l = lcm(i, i1);
+             if (size * l <= 256L) {
+@@ -289,14 +302,21 @@ public final class Shapes {
+             }
+         }
+ 
+-        if (list1.getDouble(i) < list2.getDouble(0) - 1.0E-7) {
++        // Paper start - Identical happens more often than Disjoint
++        if (i == i1 && Objects.equals(list1, list2)) {
++            if (list1 instanceof IdenticalMerger) {
++                return (IndexMerger) list1;
++            } else if (list2 instanceof IdenticalMerger) {
++                return (IndexMerger) list2;
++            }
++            return new IdenticalMerger(list1);
++        } else if (list1.getDouble(i) < list2.getDouble(0) - 1.0E-7) {
++            // Paper end - Identical happens more often than Disjoint
+             return new NonOverlappingMerger(list1, list2, false);
+         } else if (list2.getDouble(i1) < list1.getDouble(0) - 1.0E-7) {
+             return new NonOverlappingMerger(list2, list1, true);
+         } else {
+-            return (IndexMerger)(i == i1 && Objects.equals(list1, list2)
+-                ? new IdenticalMerger(list1)
+-                : new IndirectMerger(list1, list2, excludeUpper, excludeLower));
++            return new IndirectMerger(list1, list2, excludeUpper, excludeLower); // Paper - Identical happens more often than Disjoint
+         }
+     }
+ 
diff --git a/paper-server/patches/features/0009-Fix-entity-type-tags-suggestions-in-selectors.patch b/paper-server/patches/features/0009-Fix-entity-type-tags-suggestions-in-selectors.patch
new file mode 100644
index 0000000000..cd578f9f57
--- /dev/null
+++ b/paper-server/patches/features/0009-Fix-entity-type-tags-suggestions-in-selectors.patch
@@ -0,0 +1,152 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jake Potrebic <jake.m.potrebic@gmail.com>
+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/net/minecraft/commands/CommandSourceStack.java b/net/minecraft/commands/CommandSourceStack.java
+index 704a63890a06d793f8ac3452838917e7c7335232..75262c8c9eaecb4a88a94f4076d67119c67a97da 100644
+--- a/net/minecraft/commands/CommandSourceStack.java
++++ b/net/minecraft/commands/CommandSourceStack.java
+@@ -652,4 +652,20 @@ public class CommandSourceStack implements ExecutionCommandSource<CommandSourceS
+         return this.source.getBukkitSender(this);
+     }
+     // CraftBukkit end
++    // Paper start - tell clients to ask server for suggestions for EntityArguments
++    @Override
++    public Collection<String> 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/net/minecraft/commands/Commands.java b/net/minecraft/commands/Commands.java
+index fa8c5ba4e0efd0c36613aaa8eaafba0cb70ceb87..19ccf3abf14c67f72a1ca065e4a304f50e645ef4 100644
+--- a/net/minecraft/commands/Commands.java
++++ b/net/minecraft/commands/Commands.java
+@@ -509,6 +509,7 @@ public class Commands {
+         Map<CommandNode<CommandSourceStack>, CommandNode<SharedSuggestionProvider>> commandNodeToSuggestionNode
+     ) {
+         commandNodeToSuggestionNode.keySet().removeIf((node) -> !org.spigotmc.SpigotConfig.sendNamespaced && node.getName().contains(":")); // Paper - Remove namedspaced from result nodes to prevent redirect trimming ~ see comment below
++        boolean registeredAskServerSuggestionsForTree = false; // Paper - tell clients to ask server for suggestions for EntityArguments
+         for (CommandNode<CommandSourceStack> commandNode : children) { // Paper - Perf: Async command map building; pass copy of children
+             // Paper start - Brigadier API
+             if (commandNode.clientNode != null) {
+@@ -572,6 +573,12 @@ public class Commands {
+                     RequiredArgumentBuilder<SharedSuggestionProvider, ?> requiredArgumentBuilder = (RequiredArgumentBuilder<SharedSuggestionProvider, ?>)argumentBuilder;
+                     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/net/minecraft/commands/arguments/EntityArgument.java b/net/minecraft/commands/arguments/EntityArgument.java
+index 0a01df6ebd14afe79bc76364cb1df5e0c5c08074..7a06ad9940d25e163178370bfa9ccc3bbd3d1189 100644
+--- a/net/minecraft/commands/arguments/EntityArgument.java
++++ b/net/minecraft/commands/arguments/EntityArgument.java
+@@ -138,7 +138,7 @@ public class EntityArgument implements ArgumentType<EntitySelector> {
+             final boolean permission = sharedSuggestionProvider instanceof CommandSourceStack stack
+                 ? stack.bypassSelectorPermissions || stack.hasPermission(2, "minecraft.command.selector")
+                 : sharedSuggestionProvider.hasPermission(2);
+-            EntitySelectorParser entitySelectorParser = new EntitySelectorParser(stringReader, permission);
++            EntitySelectorParser entitySelectorParser = new EntitySelectorParser(stringReader, permission, true); // Paper - tell clients to ask server for suggestions for EntityArguments
+             // Paper end - Fix EntityArgument permissions
+ 
+             try {
+@@ -149,7 +149,19 @@ public class EntityArgument implements ArgumentType<EntitySelector> {
+             return entitySelectorParser.fillSuggestions(
+                 builder,
+                 offsetBuilder -> {
+-                    Collection<String> onlinePlayerNames = sharedSuggestionProvider.getOnlinePlayerNames();
++                    // Paper start - tell clients to ask server for suggestions for EntityArguments
++                    final Collection<String> onlinePlayerNames;
++                    if (sharedSuggestionProvider instanceof CommandSourceStack commandSourceStack && commandSourceStack.getEntity() instanceof ServerPlayer sourcePlayer) {
++                        onlinePlayerNames = new java.util.ArrayList<>();
++                        for (final ServerPlayer player : commandSourceStack.getServer().getPlayerList().getPlayers()) {
++                            if (sourcePlayer.getBukkitEntity().canSee(player.getBukkitEntity())) {
++                                onlinePlayerNames.add(player.getGameProfile().getName());
++                            }
++                        }
++                    } else {
++                        onlinePlayerNames = sharedSuggestionProvider.getOnlinePlayerNames();
++                    }
++                    // Paper end - tell clients to ask server for suggestions for EntityArguments
+                     Iterable<String> iterable = (Iterable<String>)(this.playersOnly
+                         ? onlinePlayerNames
+                         : Iterables.concat(onlinePlayerNames, sharedSuggestionProvider.getSelectedEntities()));
+diff --git a/net/minecraft/commands/arguments/selector/EntitySelectorParser.java b/net/minecraft/commands/arguments/selector/EntitySelectorParser.java
+index a6f232747df631f6afe440606bea94c588f1a0dd..fb42630741674c6cbd20b7d45d78dea1dc73a78f 100644
+--- a/net/minecraft/commands/arguments/selector/EntitySelectorParser.java
++++ b/net/minecraft/commands/arguments/selector/EntitySelectorParser.java
+@@ -115,8 +115,15 @@ 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, boolean allowSelectors) {
++        // Paper start - tell clients to ask server for suggestions for EntityArguments
++        this(reader, allowSelectors, false);
++    }
++    public EntitySelectorParser(StringReader reader, boolean allowSelectors, boolean parsingEntityArgumentSuggestions) {
++        this.parsingEntityArgumentSuggestions = parsingEntityArgumentSuggestions;
++        // Paper end - tell clients to ask server for suggestions for EntityArguments
+         this.reader = reader;
+         this.allowSelectors = allowSelectors;
+     }
+diff --git a/net/minecraft/commands/arguments/selector/options/EntitySelectorOptions.java b/net/minecraft/commands/arguments/selector/options/EntitySelectorOptions.java
+index f6b58139aace70436034f0a16370236d975cb4ae..ee9949c41d38817b21b6f4fd728059a46fddf135 100644
+--- a/net/minecraft/commands/arguments/selector/options/EntitySelectorOptions.java
++++ b/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(
+         type -> Component.translatableEscape("argument.entity.options.type.invalid", type)
+     );
++    // 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<EntitySelectorParser> predicate, Component tooltip) {
+         OPTIONS.put(id, new EntitySelectorOptions.Option(handler, predicate, tooltip));
+@@ -299,6 +312,12 @@ public class EntitySelectorOptions {
+ 
+                         if (parser.isTag()) {
+                             TagKey<EntityType<?>> tagKey = TagKey.create(Registries.ENTITY_TYPE, ResourceLocation.read(parser.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 (parser.parsingEntityArgumentSuggestions && io.papermc.paper.configuration.GlobalConfiguration.get().commands.fixTargetSelectorTagCompletion && net.minecraft.core.registries.BuiltInRegistries.ENTITY_TYPE.get(tagKey).isEmpty()) {
++                                parser.getReader().setCursor(cursor);
++                                throw ERROR_ENTITY_TAG_INVALID.createWithContext(parser.getReader(), tagKey);
++                            }
++                            // Paper end - tell clients to ask server for suggestions for EntityArguments
+                             parser.addPredicate(entity -> entity.getType().is(tagKey) != shouldInvertValue);
+                         } else {
+                             ResourceLocation resourceLocation = ResourceLocation.read(parser.getReader());
diff --git a/feature-patches/1054-Handle-Oversized-block-entities-in-chunks.patch b/paper-server/patches/features/0010-Handle-Oversized-block-entities-in-chunks.patch
similarity index 52%
rename from feature-patches/1054-Handle-Oversized-block-entities-in-chunks.patch
rename to paper-server/patches/features/0010-Handle-Oversized-block-entities-in-chunks.patch
index da2af1a0ea..3ae9000bc3 100644
--- a/feature-patches/1054-Handle-Oversized-block-entities-in-chunks.patch
+++ b/paper-server/patches/features/0010-Handle-Oversized-block-entities-in-chunks.patch
@@ -8,17 +8,17 @@ creating too large of a packet to sed.
 
 Co-authored-by: Spottedleaf <Spottedleaf@users.noreply.github.com>
 
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java
-+++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java
-@@ -0,0 +0,0 @@ public class ClientboundLevelChunkPacketData {
+diff --git a/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java b/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java
+index 3aea76690bc3e35758d3bf274777130af17d8a0f..9e321ef1c3d5803519b243685f4ee598dc0cf640 100644
+--- a/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java
++++ b/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<ClientboundLevelChunkPacketData.BlockEntityInfo> blockEntitiesData;
 +    // Paper start - Handle oversized block entities in chunks
 +    private final java.util.List<net.minecraft.network.protocol.Packet<?>> extraPackets = new java.util.ArrayList<>();
-+    private static final int TE_LIMIT = Integer.getInteger("Paper.excessiveTELimit", 750);
++    private static final int BLOCK_ENTITY_LIMIT = Integer.getInteger("Paper.excessiveTELimit", 750);
 +
 +    public List<net.minecraft.network.protocol.Packet<?>> getExtraPackets() {
 +        return this.extraPackets;
@@ -26,31 +26,31 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    // 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); }
-@@ -0,0 +0,0 @@ public class ClientboundLevelChunkPacketData {
-         extractChunkData(new FriendlyByteBuf(this.getWriteBuffer()), chunk, chunkPacketInfo);
-         // Paper end
+     @Deprecated @io.papermc.paper.annotation.DoNotUse
+@@ -50,8 +58,18 @@ public class ClientboundLevelChunkPacketData {
+         }
+         extractChunkData(new FriendlyByteBuf(this.getWriteBuffer()), levelChunk, chunkPacketInfo);
          this.blockEntitiesData = Lists.newArrayList();
 +        int totalTileEntities = 0; // Paper - Handle oversized block entities in chunks
  
-         for (Entry<BlockPos, BlockEntity> entry2 : chunk.getBlockEntities().entrySet()) {
+         for (Entry<BlockPos, BlockEntity> entryx : levelChunk.getBlockEntities().entrySet()) {
 +            // Paper start - Handle oversized block entities in chunks
-+            if (++totalTileEntities > TE_LIMIT) {
-+                var packet = entry2.getValue().getUpdatePacket();
++            if (++totalTileEntities > BLOCK_ENTITY_LIMIT) {
++                net.minecraft.network.protocol.Packet<ClientGamePacketListener> packet = entryx.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()));
+             this.blockEntitiesData.add(ClientboundLevelChunkPacketData.BlockEntityInfo.create(entryx.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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java
-+++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java
-@@ -0,0 +0,0 @@ public class ClientboundLevelChunkWithLightPacket implements Packet<ClientGamePa
+diff --git a/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java b/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java
+index 5699bc15eba92e22433a20cb8326b59f2ebd3036..8578d1f78ddd1bb75f3230f04bfaa35af9f5f822 100644
+--- a/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java
++++ b/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java
+@@ -84,4 +84,11 @@ public class ClientboundLevelChunkWithLightPacket implements Packet<ClientGamePa
      public ClientboundLightUpdatePacketData getLightData() {
          return this.lightData;
      }
diff --git a/paper-server/patches/features/0011-optimize-dirt-and-snow-spreading.patch b/paper-server/patches/features/0011-optimize-dirt-and-snow-spreading.patch
new file mode 100644
index 0000000000..2423e0adc9
--- /dev/null
+++ b/paper-server/patches/features/0011-optimize-dirt-and-snow-spreading.patch
@@ -0,0 +1,78 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: lukas81298 <lukas81298@gommehd.net>
+Date: Fri, 22 Jan 2021 21:50:18 +0100
+Subject: [PATCH] optimize dirt and snow spreading
+
+
+diff --git a/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java b/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java
+index 11937aa74efe08bdbd66a619c7a825f91d971afd..722f2b9a24679e0fc67aae2cd27051f96f962efe 100644
+--- a/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java
++++ b/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java
+@@ -17,8 +17,13 @@ public abstract class SpreadingSnowyDirtBlock extends SnowyDirtBlock {
+     }
+ 
+     private static boolean canBeGrass(BlockState state, LevelReader levelReader, BlockPos pos) {
++        // Paper start - Perf: optimize dirt and snow spreading
++        return canBeGrass(levelReader.getChunk(pos), state, levelReader, pos);
++    }
++    private static boolean canBeGrass(net.minecraft.world.level.chunk.ChunkAccess chunk, BlockState state, LevelReader levelReader, BlockPos pos) {
++        // Paper end - Perf: optimize dirt and snow spreading
+         BlockPos blockPos = pos.above();
+-        BlockState blockState = levelReader.getBlockState(blockPos);
++        BlockState blockState = chunk.getBlockState(blockPos); // Paper - Perf: optimize dirt and snow spreading
+         if (blockState.is(Blocks.SNOW) && blockState.getValue(SnowLayerBlock.LAYERS) == 1) {
+             return true;
+         } else if (blockState.getFluidState().getAmount() == 8) {
+@@ -33,14 +38,27 @@ public abstract class SpreadingSnowyDirtBlock extends SnowyDirtBlock {
+     protected abstract MapCodec<? extends SpreadingSnowyDirtBlock> codec();
+ 
+     private static boolean canPropagate(BlockState state, LevelReader level, BlockPos pos) {
++        // Paper start - Perf: optimize dirt and snow spreading
++        return canPropagate(level.getChunk(pos), state, level, pos);
++    }
++
++    private static boolean canPropagate(net.minecraft.world.level.chunk.ChunkAccess chunk, BlockState state, LevelReader level, BlockPos pos) {
++        // Paper end - Perf: optimize dirt and snow spreading
+         BlockPos blockPos = pos.above();
+-        return canBeGrass(state, level, pos) && !level.getFluidState(blockPos).is(FluidTags.WATER);
++        return canBeGrass(chunk, state, level, pos) && !chunk.getFluidState(blockPos).is(FluidTags.WATER); // Paper - Perf: optimize dirt and snow spreading
+     }
+ 
+     @Override
+     protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+         if (this instanceof GrassBlock && level.paperConfig().tickRates.grassSpread != 1 && (level.paperConfig().tickRates.grassSpread < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % level.paperConfig().tickRates.grassSpread != 0)) { return; } // Paper - Configurable random tick rates for blocks
+-        if (!canBeGrass(state, level, pos)) {
++        // Paper start - Perf: optimize dirt and snow spreading
++        final net.minecraft.world.level.chunk.ChunkAccess cachedBlockChunk = level.getChunkIfLoaded(pos);
++        if (cachedBlockChunk == null) { // Is this needed?
++            return;
++        }
++
++        if (!canBeGrass(cachedBlockChunk, state, level, pos)) {
++            // Paper end - Perf: optimize dirt and snow spreading
+             // CraftBukkit start
+             if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(level, pos, Blocks.DIRT.defaultBlockState()).isCancelled()) {
+                 return;
+@@ -53,8 +71,20 @@ public abstract class SpreadingSnowyDirtBlock extends SnowyDirtBlock {
+ 
+                 for (int i = 0; i < 4; i++) {
+                     BlockPos blockPos = pos.offset(random.nextInt(3) - 1, random.nextInt(5) - 3, random.nextInt(3) - 1);
+-                    if (level.getBlockState(blockPos).is(Blocks.DIRT) && canPropagate(blockState, level, blockPos)) {
+-                        org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(level, pos, blockPos, blockState.setValue(SNOWY, Boolean.valueOf(isSnowySetting(level.getBlockState(blockPos.above()))))); // CraftBukkit
++                    // Paper start - Perf: optimize dirt and snow spreading
++                    if (pos.getX() == blockPos.getX() && pos.getY() == blockPos.getY() && pos.getZ() == blockPos.getZ()) {
++                        continue;
++                    }
++
++                    final net.minecraft.world.level.chunk.ChunkAccess access;
++                    if (cachedBlockChunk.locX == blockPos.getX() >> 4 && cachedBlockChunk.locZ == blockPos.getZ() >> 4) {
++                        access = cachedBlockChunk;
++                    } else {
++                        access = level.getChunkAt(blockPos);
++                    }
++                    if (access.getBlockState(blockPos).is(Blocks.DIRT) && SpreadingSnowyDirtBlock.canPropagate(access, blockState, level, blockPos)) {
++                        org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(level, pos, blockPos, (BlockState) blockState.setValue(SpreadingSnowyDirtBlock.SNOWY, isSnowySetting(access.getBlockState(blockPos.above())))); // CraftBukkit
++                        // Paper end - Perf: optimize dirt and snow spreading
+                     }
+                 }
+             }
diff --git a/paper-server/patches/features/0012-Optimise-getChunkAt-calls-for-loaded-chunks.patch b/paper-server/patches/features/0012-Optimise-getChunkAt-calls-for-loaded-chunks.patch
new file mode 100644
index 0000000000..6b1a406c1e
--- /dev/null
+++ b/paper-server/patches/features/0012-Optimise-getChunkAt-calls-for-loaded-chunks.patch
@@ -0,0 +1,57 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Spottedleaf <Spottedleaf@users.noreply.github.com>
+Date: Sat, 25 Jan 2020 17:04:35 -0800
+Subject: [PATCH] Optimise getChunkAt calls for loaded chunks
+
+bypass the need to get a player chunk, then get the either,
+then unwrap it...
+
+diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java
+index c3553385074108d686425a7637b8975076d906b9..2f49dbc919f7f5eea9abce6106723c72f5ae45fb 100644
+--- a/net/minecraft/server/level/ServerChunkCache.java
++++ b/net/minecraft/server/level/ServerChunkCache.java
+@@ -218,6 +218,12 @@ public class ServerChunkCache extends ChunkSource {
+         if (Thread.currentThread() != this.mainThread) {
+             return CompletableFuture.<ChunkAccess>supplyAsync(() -> this.getChunk(x, z, chunkStatus, requireChunk), this.mainThreadProcessor).join();
+         } else {
++            // Paper start - Perf: Optimise getChunkAt calls for loaded chunks
++            LevelChunk ifLoaded = this.getChunkAtIfCachedImmediately(x, z);
++            if (ifLoaded != null) {
++                return ifLoaded;
++            }
++            // Paper end - Perf: Optimise getChunkAt calls for loaded chunks
+             ProfilerFiller profilerFiller = Profiler.get();
+             profilerFiller.incrementCounter("getChunk");
+             long packedChunkPos = ChunkPos.asLong(x, z);
+@@ -252,30 +258,7 @@ public class ServerChunkCache extends ChunkSource {
+         if (Thread.currentThread() != this.mainThread) {
+             return null;
+         } else {
+-            Profiler.get().incrementCounter("getChunkNow");
+-            long packedChunkPos = ChunkPos.asLong(chunkX, chunkZ);
+-
+-            for (int i = 0; i < 4; i++) {
+-                if (packedChunkPos == this.lastChunkPos[i] && this.lastChunkStatus[i] == ChunkStatus.FULL) {
+-                    ChunkAccess chunkAccess = this.lastChunk[i];
+-                    return chunkAccess instanceof LevelChunk ? (LevelChunk)chunkAccess : null;
+-                }
+-            }
+-
+-            ChunkHolder visibleChunkIfPresent = this.getVisibleChunkIfPresent(packedChunkPos);
+-            if (visibleChunkIfPresent == null) {
+-                return null;
+-            } else {
+-                ChunkAccess chunkAccess = visibleChunkIfPresent.getChunkIfPresent(ChunkStatus.FULL);
+-                if (chunkAccess != null) {
+-                    this.storeInCache(packedChunkPos, chunkAccess, ChunkStatus.FULL);
+-                    if (chunkAccess instanceof LevelChunk) {
+-                        return (LevelChunk)chunkAccess;
+-                    }
+-                }
+-
+-                return null;
+-            }
++            return this.getChunkAtIfCachedImmediately(chunkX, chunkZ); // Paper - Perf: Optimise getChunkAt calls for loaded chunks
+         }
+     }
+ 
diff --git a/paper-server/patches/features/0013-Optimize-Bit-Operations-by-inlining.patch b/paper-server/patches/features/0013-Optimize-Bit-Operations-by-inlining.patch
new file mode 100644
index 0000000000..688e72d605
--- /dev/null
+++ b/paper-server/patches/features/0013-Optimize-Bit-Operations-by-inlining.patch
@@ -0,0 +1,212 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <aikar@aikar.co>
+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/net/minecraft/core/BlockPos.java b/net/minecraft/core/BlockPos.java
+index 98f0b1cf19d7a035849a9a2fa25e2be3a4c5a980..a81694a22e94cca6f7110f7d5b205d1303f4e071 100644
+--- a/net/minecraft/core/BlockPos.java
++++ b/net/minecraft/core/BlockPos.java
+@@ -51,15 +51,17 @@ public class BlockPos extends Vec3i {
+     };
+     private static final Logger LOGGER = LogUtils.getLogger();
+     public static final BlockPos ZERO = new BlockPos(0, 0, 0);
+-    public static final int PACKED_HORIZONTAL_LENGTH = 1 + Mth.log2(Mth.smallestEncompassingPowerOfTwo(30000000));
+-    public static final int PACKED_Y_LENGTH = 64 - 2 * PACKED_HORIZONTAL_LENGTH;
+-    private static final long PACKED_X_MASK = (1L << PACKED_HORIZONTAL_LENGTH) - 1L;
+-    private static final long PACKED_Y_MASK = (1L << PACKED_Y_LENGTH) - 1L;
+-    private static final long PACKED_Z_MASK = (1L << PACKED_HORIZONTAL_LENGTH) - 1L;
++    // Paper start - Optimize Bit Operations by inlining
++    public static final int PACKED_HORIZONTAL_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 Y_OFFSET = 0;
+-    private static final int Z_OFFSET = PACKED_Y_LENGTH;
+-    private static final int X_OFFSET = PACKED_Y_LENGTH + PACKED_HORIZONTAL_LENGTH;
+-    public static final int MAX_HORIZONTAL_COORDINATE = (1 << PACKED_HORIZONTAL_LENGTH) / 2 - 1;
++    private static final int Z_OFFSET = 12;
++    private static final int X_OFFSET = 38;
++    public static final int MAX_HORIZONTAL_COORDINATE = 33554431;
++    // Paper end - Optimize Bit Operations by inlining
+ 
+     public BlockPos(int x, int y, int z) {
+         super(x, y, z);
+@@ -69,28 +71,29 @@ public class BlockPos extends Vec3i {
+         this(vector.getX(), vector.getY(), vector.getZ());
+     }
+ 
++    public static long getAdjacent(int baseX, int baseY, int baseZ, Direction direction) { return asLong(baseX + direction.getStepX(), baseY + direction.getStepY(), baseZ + direction.getStepZ()); } // Paper
+     public static long offset(long pos, Direction direction) {
+         return offset(pos, direction.getStepX(), direction.getStepY(), direction.getStepZ());
+     }
+ 
+     public static long offset(long pos, int dx, int dy, int dz) {
+-        return asLong(getX(pos) + dx, getY(pos) + dy, getZ(pos) + dz);
++        return asLong((int) (pos >> 38) + dx, (int) ((pos << 52) >> 52) + dy, (int) ((pos << 26) >> 38) + dz); // Paper - simplify/inline
+     }
+ 
+     public static int getX(long packedPos) {
+-        return (int)(packedPos << 64 - X_OFFSET - PACKED_HORIZONTAL_LENGTH >> 64 - PACKED_HORIZONTAL_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_HORIZONTAL_LENGTH >> 64 - PACKED_HORIZONTAL_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) {
+@@ -114,10 +117,7 @@ public class BlockPos extends Vec3i {
+     }
+ 
+     public static long asLong(int x, int y, int z) {
+-        long l = 0L;
+-        l |= (x & PACKED_X_MASK) << X_OFFSET;
+-        l |= (y & PACKED_Y_MASK) << 0;
+-        return l | (z & PACKED_Z_MASK) << Z_OFFSET;
++        return ((x & 67108863L) << 38) | ((y &  4095L)) | ((z & 67108863L) << 12); // Paper - inline constants and simplify
+     }
+ 
+     public static long getFlatIndex(long packedPos) {
+diff --git a/net/minecraft/core/SectionPos.java b/net/minecraft/core/SectionPos.java
+index 1780d8e14cea62971da75e4dcc80d1805434037b..ce8c394ea1a7bc5bf5c568c82e6158b19df517d8 100644
+--- a/net/minecraft/core/SectionPos.java
++++ b/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 direction) {
++        return (((long) ((x >> 4) + direction.getStepX()) & 4194303L) << 42) | (((long) ((y >> 4) + direction.getStepY()) & 1048575L)) | (((long) ((z >> 4) + direction.getStepZ()) & 4194303L) << 20);
++    }
++    public static long getAdjacentFromSectionPos(int x, int y, int z, Direction direction) {
++        return (((long) (x + direction.getStepX()) & 4194303L) << 42) | (((long) ((y) + direction.getStepY()) & 1048575L)) | (((long) (z + direction.getStepZ()) & 4194303L) << 20);
++    }
++    // Paper end
+     public static long offset(long packed, int dx, int dy, int dz) {
+-        return asLong(x(packed) + dx, y(packed) + dy, z(packed) + dz);
++        return (((long) ((int) (packed >> 42) + dx) & 4194303L) << 42) | (((long) ((int) (packed << 44 >> 44) + dy) & 1048575L)) | (((long) ((int) (packed << 22 >> 42) + dz) & 4194303L) << 20); // Simplify to reduce instruction count
+     }
+ 
+     public static int posToSectionCoord(double pos) {
+@@ -86,10 +94,7 @@ public class SectionPos extends Vec3i {
+     }
+ 
+     public static short sectionRelativePos(BlockPos pos) {
+-        int relativeBlockPosX = sectionRelative(pos.getX());
+-        int relativeBlockPosY = sectionRelative(pos.getY());
+-        int relativeBlockPosZ = sectionRelative(pos.getZ());
+-        return (short)(relativeBlockPosX << 8 | relativeBlockPosZ << 4 | relativeBlockPosY << 0);
++        return (short) ((pos.getX() & 15) << 8 | (pos.getZ() & 15) << 4 | pos.getY() & 15); // Paper - simplify/inline
+     }
+ 
+     public static int sectionRelativeX(short x) {
+@@ -152,16 +157,16 @@ public class SectionPos extends Vec3i {
+         return this.getZ();
+     }
+ 
+-    public int minBlockX() {
+-        return sectionToBlockCoord(this.x());
++    public final int minBlockX() { // Paper - final
++        return this.getX() << 4; // Paper - inline
+     }
+ 
+-    public int minBlockY() {
+-        return sectionToBlockCoord(this.y());
++    public final int minBlockY() { // Paper - final
++        return this.getY() << 4; // Paper - inline
+     }
+ 
+-    public int minBlockZ() {
+-        return sectionToBlockCoord(this.z());
++    public final int minBlockZ() { // Paper - final
++        return this.getZ() << 4; // Paper - inline
+     }
+ 
+     public int maxBlockX() {
+@@ -177,7 +182,7 @@ public class SectionPos extends Vec3i {
+     }
+ 
+     public static long blockToSection(long levelPos) {
+-        return asLong(blockToSectionCoord(BlockPos.getX(levelPos)), blockToSectionCoord(BlockPos.getY(levelPos)), blockToSectionCoord(BlockPos.getZ(levelPos)));
++        return (((long) (int) (levelPos >> 42) & 4194303L) << 42) | (((long) (int) ((levelPos << 52) >> 56) & 1048575L)) | (((long) (int) ((levelPos << 26) >> 42) & 4194303L) << 20); // Simplify to reduce instruction count
+     }
+ 
+     public static long getZeroNode(int x, int z) {
+@@ -205,15 +210,17 @@ public class SectionPos extends Vec3i {
+         return asLong(blockToSectionCoord(blockPos.getX()), blockToSectionCoord(blockPos.getY()), blockToSectionCoord(blockPos.getZ()));
+     }
+ 
++    // Paper start
++    public static long blockPosAsSectionLong(int x, int y, int z) {
++        return (((long) (x >> 4) & 4194303L) << 42) | (((long) (y >> 4) & 1048575L)) | (((long) (z >> 4) & 4194303L) << 20);
++    }
++    // Paper end
+     public static long asLong(int x, int y, int z) {
+-        long l = 0L;
+-        l |= (x & 4194303L) << 42;
+-        l |= (y & 1048575L) << 0;
+-        return l | (z & 4194303L) << 20;
++        return ((x & 4194303L) << 42) | ((y & 1048575L)) | ((z & 4194303L) << 20); // Paper - Simplify to reduce instruction count
+     }
+ 
+     public long asLong() {
+-        return asLong(this.x(), this.y(), this.z());
++        return ((this.getX() & 4194303L) << 42) | ((this.getY() & 1048575L)) | ((this.getZ() & 4194303L) << 20); // Paper - Simplify to reduce instruction count
+     }
+ 
+     @Override
+@@ -226,16 +233,13 @@ public class SectionPos extends Vec3i {
+     }
+ 
+     public static Stream<SectionPos> cube(SectionPos center, int radius) {
+-        int sectionX = center.x();
+-        int sectionY = center.y();
+-        int sectionZ = center.z();
+-        return betweenClosedStream(sectionX - radius, sectionY - radius, sectionZ - radius, sectionX + radius, sectionY + radius, sectionZ + 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<SectionPos> aroundChunk(ChunkPos chunkPos, int x, int y, int z) {
++    public static Stream<SectionPos> aroundChunk(ChunkPos chunkPos, int radius, int minY, int maxY) { // Paper - fix params
+         int i = chunkPos.x;
+         int i1 = chunkPos.z;
+-        return betweenClosedStream(i - x, y, i1 - x, i + x, z, i1 + x);
++        return betweenClosedStream(i - radius, minY, i1 - radius, i + radius, maxY, i1 + radius); // Paper - simplify/inline
+     }
+ 
+     public static Stream<SectionPos> betweenClosedStream(final int x1, final int y1, final int z1, final int x2, final int y2, final int z2) {
diff --git a/paper-server/patches/features/0014-Remove-streams-from-hot-code.patch b/paper-server/patches/features/0014-Remove-streams-from-hot-code.patch
new file mode 100644
index 0000000000..afbe9f1e4c
--- /dev/null
+++ b/paper-server/patches/features/0014-Remove-streams-from-hot-code.patch
@@ -0,0 +1,223 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Josh Roy <10731363+JRoy@users.noreply.github.com>
+Date: Wed, 1 Jul 2020 18:01:49 -0400
+Subject: [PATCH] Remove streams from hot code
+
+Co-authored-by: Bjarne Koll <git@lynxplay.dev>
+Co-authored-by: Spottedleaf <Spottedleaf@users.noreply.github.com>
+
+diff --git a/net/minecraft/world/entity/ai/behavior/GateBehavior.java b/net/minecraft/world/entity/ai/behavior/GateBehavior.java
+index c215d97c24e6501e1a48a76fc08bf48ff4dfe462..bd31d1cac0d022a72bd536c41d1ef811886e7068 100644
+--- a/net/minecraft/world/entity/ai/behavior/GateBehavior.java
++++ b/net/minecraft/world/entity/ai/behavior/GateBehavior.java
+@@ -57,7 +57,7 @@ public class GateBehavior<E extends LivingEntity> implements BehaviorControl<E>
+         if (this.hasRequiredMemories(entity)) {
+             this.status = Behavior.Status.RUNNING;
+             this.orderPolicy.apply(this.behaviors);
+-            this.runningPolicy.apply(this.behaviors.stream(), level, entity, gameTime);
++            this.runningPolicy.apply(this.behaviors, level, entity, gameTime); // Paper - Perf: Remove streams from hot code
+             return true;
+         } else {
+             return false;
+@@ -66,10 +66,13 @@ public class GateBehavior<E extends LivingEntity> implements BehaviorControl<E>
+ 
+     @Override
+     public final void tickOrStop(ServerLevel level, E entity, long gameTime) {
+-        this.behaviors
+-            .stream()
+-            .filter(behavior -> behavior.getStatus() == Behavior.Status.RUNNING)
+-            .forEach(behavior -> behavior.tickOrStop(level, entity, gameTime));
++        // Paper start - Perf: Remove streams from hot code
++        for (final BehaviorControl<? super E> behavior : this.behaviors) {
++            if (behavior.getStatus() == Behavior.Status.RUNNING) {
++                behavior.tickOrStop(level, entity, gameTime);
++            }
++        }
++        // Paper end - Perf: Remove streams from hot code
+         if (this.behaviors.stream().noneMatch(behavior -> behavior.getStatus() == Behavior.Status.RUNNING)) {
+             this.doStop(level, entity, gameTime);
+         }
+@@ -78,11 +81,16 @@ public class GateBehavior<E extends LivingEntity> implements BehaviorControl<E>
+     @Override
+     public final void doStop(ServerLevel level, E entity, long gameTime) {
+         this.status = Behavior.Status.STOPPED;
+-        this.behaviors
+-            .stream()
+-            .filter(behavior -> behavior.getStatus() == Behavior.Status.RUNNING)
+-            .forEach(behavior -> behavior.doStop(level, entity, gameTime));
+-        this.exitErasedMemories.forEach(entity.getBrain()::eraseMemory);
++        // Paper start - Perf: Remove streams from hot code
++        for (final BehaviorControl<? super E> behavior : this.behaviors) {
++            if (behavior.getStatus() == Behavior.Status.RUNNING) {
++                behavior.doStop(level, entity, gameTime);
++            }
++        }
++        for (final MemoryModuleType<?> exitErasedMemory : this.exitErasedMemories) {
++            entity.getBrain().eraseMemory(exitErasedMemory);
++        }
++        // Paper end - Perf: Remove streams from hot code
+     }
+ 
+     @Override
+@@ -116,20 +124,30 @@ public class GateBehavior<E extends LivingEntity> implements BehaviorControl<E>
+ 
+     public static enum RunningPolicy {
+         RUN_ONE {
++            // Paper start - Perf: Remove streams from hot code
+             @Override
+-            public <E extends LivingEntity> void apply(Stream<BehaviorControl<? super E>> behaviors, ServerLevel level, E owner, long gameTime) {
+-                behaviors.filter(behavior -> behavior.getStatus() == Behavior.Status.STOPPED)
+-                    .filter(behavior -> behavior.tryStart(level, owner, gameTime))
+-                    .findFirst();
++            public <E extends LivingEntity> void apply(ShufflingList<BehaviorControl<? super E>> behaviors, ServerLevel level, E owner, long gameTime) {
++                for (final BehaviorControl<? super E> behavior : behaviors) {
++                    if (behavior.getStatus() == Behavior.Status.STOPPED && behavior.tryStart(level, owner, gameTime)) {
++                        break;
++                    }
++                }
++                // Paper end - Perf: Remove streams from hot code
+             }
+         },
+         TRY_ALL {
++            // Paper start - Perf: Remove streams from hot code
+             @Override
+-            public <E extends LivingEntity> void apply(Stream<BehaviorControl<? super E>> behaviors, ServerLevel level, E owner, long gameTime) {
+-                behaviors.filter(behavior -> behavior.getStatus() == Behavior.Status.STOPPED).forEach(behavior -> behavior.tryStart(level, owner, gameTime));
++            public <E extends LivingEntity> void apply(ShufflingList<BehaviorControl<? super E>> behaviors, ServerLevel level, E owner, long gameTime) {
++                for (final BehaviorControl<? super E> behavior : behaviors) {
++                    if (behavior.getStatus() == Behavior.Status.STOPPED) {
++                        behavior.tryStart(level, owner, gameTime);
++                    }
++                }
++                // Paper end - Perf: Remove streams from hot code
+             }
+         };
+ 
+-        public abstract <E extends LivingEntity> void apply(Stream<BehaviorControl<? super E>> behaviors, ServerLevel level, E owner, long gameTime);
++        public abstract <E extends LivingEntity> void apply(ShufflingList<BehaviorControl<? super E>> behaviors, ServerLevel level, E owner, long gameTime); // Paper - Perf: Remove streams from hot code
+     }
+ }
+diff --git a/net/minecraft/world/entity/ai/gossip/GossipContainer.java b/net/minecraft/world/entity/ai/gossip/GossipContainer.java
+index 2c839dc80f451c83135828a97aced1a531004bab..b74a4ce1b629d440681a1f5c026997ccaf1d0373 100644
+--- a/net/minecraft/world/entity/ai/gossip/GossipContainer.java
++++ b/net/minecraft/world/entity/ai/gossip/GossipContainer.java
+@@ -59,8 +59,22 @@ public class GossipContainer {
+         return this.gossips.entrySet().stream().flatMap(gossip -> gossip.getValue().unpack(gossip.getKey()));
+     }
+ 
++    // Paper start - Perf: Remove streams from hot code
++    private List<GossipContainer.GossipEntry> decompress() {
++        List<GossipContainer.GossipEntry> list = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>();
++        for (Map.Entry<UUID, GossipContainer.EntityGossips> 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<GossipContainer.GossipEntry> selectGossipsForTransfer(RandomSource random, int amount) {
+-        List<GossipContainer.GossipEntry> list = this.unpack().toList();
++        List<GossipContainer.GossipEntry> 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> T store(DynamicOps<T> ops) {
+         return GossipContainer.GossipEntry.LIST_CODEC
+-            .encodeStart(ops, this.unpack().toList())
++            .encodeStart(ops, this.decompress()) // Paper - Perf: Remove streams from hot code
+             .resultOrPartial(errorMessage -> LOGGER.warn("Failed to serialize gossips: {}", errorMessage))
+             .orElseGet(ops::emptyList);
+     }
+@@ -172,12 +186,23 @@ public class GossipContainer {
+         final Object2IntMap<GossipType> entries = new Object2IntOpenHashMap<>();
+ 
+         public int weightedValue(Predicate<GossipType> gossipType) {
+-            return this.entries
+-                .object2IntEntrySet()
+-                .stream()
+-                .filter(gossip -> gossipType.test(gossip.getKey()))
+-                .mapToInt(gossip -> gossip.getIntValue() * gossip.getKey().weight)
+-                .sum();
++            // Paper start - Perf: Remove streams from hot code
++            int weight = 0;
++            for (Object2IntMap.Entry<GossipType> entry : entries.object2IntEntrySet()) {
++                if (gossipType.test(entry.getKey())) {
++                    weight += entry.getIntValue() * entry.getKey().weight;
++                }
++            }
++            return weight;
++        }
++
++        public List<GossipContainer.GossipEntry> decompress(UUID uuid) {
++            List<GossipContainer.GossipEntry> list = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>();
++            for (Object2IntMap.Entry<GossipType> 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<GossipContainer.GossipEntry> unpack(UUID identifier) {
+diff --git a/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java b/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java
+index 38873e56e95dc772b184e4271f7af1fb411ac9f8..09fd13e2d958da8326276c4dadf25bf488aff5ac 100644
+--- a/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java
++++ b/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java
+@@ -24,13 +24,17 @@ public class NearestItemSensor extends Sensor<Mob> {
+     @Override
+     protected void doTick(ServerLevel level, Mob entity) {
+         Brain<?> brain = entity.getBrain();
+-        List<ItemEntity> entitiesOfClass = level.getEntitiesOfClass(ItemEntity.class, entity.getBoundingBox().inflate(32.0, 16.0, 32.0), itemEntity -> true);
++        List<ItemEntity> entitiesOfClass = level.getEntitiesOfClass(ItemEntity.class, entity.getBoundingBox().inflate(32.0, 16.0, 32.0), itemEntity -> itemEntity.closerThan(entity, MAX_DISTANCE_TO_WANTED_ITEM) && entity.wantsToPickUp(level, itemEntity.getItem())); // Paper - Perf: Move predicate into getEntities
+         entitiesOfClass.sort(Comparator.comparingDouble(entity::distanceToSqr));
+-        Optional<ItemEntity> optional = entitiesOfClass.stream()
+-            .filter(itemEntity -> entity.wantsToPickUp(level, 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 (final ItemEntity itemEntity : entitiesOfClass) {
++            if (entity.hasLineOfSight(itemEntity)) { // Paper - Perf: Move predicate into getEntities
++                nearest = itemEntity;
++                break;
++            }
++        }
++        brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_WANTED_ITEM, Optional.ofNullable(nearest));
++        // Paper end - Perf: remove streams from hot code
+     }
+ }
+diff --git a/net/minecraft/world/level/levelgen/Beardifier.java b/net/minecraft/world/level/levelgen/Beardifier.java
+index 1a09da5aa1ae047a002d6779326c2a29e47d32b5..131923282c9ecbcb1d7f45a826da907c02bd2716 100644
+--- a/net/minecraft/world/level/levelgen/Beardifier.java
++++ b/net/minecraft/world/level/levelgen/Beardifier.java
+@@ -35,9 +35,10 @@ public class Beardifier implements DensityFunctions.BeardifierOrMarker {
+         int minBlockZ = chunkPos.getMinBlockZ();
+         ObjectList<Beardifier.Rigid> list = new ObjectArrayList<>(10);
+         ObjectList<JigsawJunction> list1 = new ObjectArrayList<>(32);
+-        structureManager.startsForStructure(chunkPos, structure -> structure.terrainAdaptation() != TerrainAdjustment.NONE)
+-            .forEach(
+-                structureStart -> {
++        // Paper start - Perf: Remove streams from hot code
++        for (net.minecraft.world.level.levelgen.structure.StructureStart structureStart : structureManager.startsForStructure(chunkPos, structure -> {
++            return structure.terrainAdaptation() != TerrainAdjustment.NONE;
++        })) { // Paper end - Perf: Remove streams from hot code
+                     TerrainAdjustment terrainAdjustment = structureStart.getStructure().terrainAdaptation();
+ 
+                     for (StructurePiece structurePiece : structureStart.getPieces()) {
+@@ -65,8 +66,7 @@ public class Beardifier implements DensityFunctions.BeardifierOrMarker {
+                             }
+                         }
+                     }
+-                }
+-            );
++        } // Paper - Perf: Remove streams from hot code
+         return new Beardifier(list.iterator(), list1.iterator());
+     }
+ 
diff --git a/feature-patches/1052-Optimize-Pathfinder-Remove-Streams-Optimized-collect.patch b/paper-server/patches/features/0015-Optimize-Pathfinder-Remove-Streams-Optimized-collect.patch
similarity index 57%
rename from feature-patches/1052-Optimize-Pathfinder-Remove-Streams-Optimized-collect.patch
rename to paper-server/patches/features/0015-Optimize-Pathfinder-Remove-Streams-Optimized-collect.patch
index fabf3cd051..84bcbe2f2e 100644
--- a/feature-patches/1052-Optimize-Pathfinder-Remove-Streams-Optimized-collect.patch
+++ b/paper-server/patches/features/0015-Optimize-Pathfinder-Remove-Streams-Optimized-collect.patch
@@ -15,100 +15,103 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java
-+++ b/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java
-@@ -0,0 +0,0 @@ public class PathFinder {
-         if (node == null) {
+diff --git a/net/minecraft/world/level/pathfinder/PathFinder.java b/net/minecraft/world/level/pathfinder/PathFinder.java
+index a6ef296f7af1f784e1f0772947a6cb3519a3bc2a..81de6c1bbef1cafd3036e736dd305fbedc8368c6 100644
+--- a/net/minecraft/world/level/pathfinder/PathFinder.java
++++ b/net/minecraft/world/level/pathfinder/PathFinder.java
+@@ -43,8 +43,12 @@ public class PathFinder {
+         if (start == null) {
              return null;
          } else {
--            Map<Target, BlockPos> map = positions.stream()
--                .collect(Collectors.toMap(pos -> this.nodeEvaluator.getTarget((double)pos.getX(), (double)pos.getY(), (double)pos.getZ()), Function.identity()));
+-            Map<Target, BlockPos> map = targetPositions.stream()
+-                .collect(Collectors.toMap(pos -> this.nodeEvaluator.getTarget(pos.getX(), pos.getY(), pos.getZ()), Function.identity()));
 +            // Paper start - Perf: remove streams and optimize collection
 +            List<Map.Entry<Target, BlockPos>> map = Lists.newArrayList();
-+            for (final BlockPos pos : positions) {
++            for (BlockPos pos : targetPositions) {
 +                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(node, map, followRange, distance, rangeMultiplier);
+             Path path = this.findPath(start, map, maxRange, accuracy, searchDepthMultiplier);
              this.nodeEvaluator.done();
              return path;
-@@ -0,0 +0,0 @@ public class PathFinder {
+@@ -52,19 +56,19 @@ public class PathFinder {
      }
  
      @Nullable
--    private Path findPath(Node startNode, Map<Target, BlockPos> positions, float followRange, int distance, float rangeMultiplier) {
-+    // Paper start - Perf: remove streams and optimize collection
-+    private Path findPath(Node startNode, List<Map.Entry<Target, BlockPos>> positions, float followRange, int distance, float rangeMultiplier) {
+-    private Path findPath(Node node, Map<Target, BlockPos> targetPositions, float maxRange, int accuracy, float searchDepthMultiplier) {
++    private Path findPath(Node node, List<Map.Entry<Target, BlockPos>> positions, float maxRange, int accuracy, float searchDepthMultiplier) { // Paper - optimize collection
          ProfilerFiller profilerFiller = Profiler.get();
          profilerFiller.push("find_path");
          profilerFiller.markForCharting(MetricCategory.PATH_FINDING);
--        Set<Target> set = positions.keySet();
-+        // Set<Target> 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;
+-        Set<Target> set = targetPositions.keySet();
++        // Set<Target> set = targetPositions.keySet(); // Paper
+         node.g = 0.0F;
+-        node.h = this.getBestH(node, set);
++        node.h = this.getBestH(node, positions); // Paper - optimize collection
+         node.f = node.h;
          this.openSet.clear();
-         this.openSet.insert(startNode);
--        Set<Node> set2 = ImmutableSet.of();
-+        // Set<Node> set2 = ImmutableSet.of(); // Paper - unused - diff on change
+         this.openSet.insert(node);
+-        Set<Node> set1 = ImmutableSet.of();
++        // Set<Node> set1 = ImmutableSet.of(); // Paper - unused - diff on change
          int i = 0;
--        Set<Target> set3 = Sets.newHashSetWithExpectedSize(set.size());
+-        Set<Target> set2 = Sets.newHashSetWithExpectedSize(set.size());
 +        List<Map.Entry<Target, BlockPos>> entryList = Lists.newArrayListWithExpectedSize(positions.size()); // Paper - optimize collection
-         int j = (int)((float)this.maxVisitedNodes * rangeMultiplier);
+         int i1 = (int)(this.maxVisitedNodes * searchDepthMultiplier);
  
          while (!this.openSet.isEmpty()) {
-@@ -0,0 +0,0 @@ public class PathFinder {
-             Node node = this.openSet.pop();
-             node.closed = true;
+@@ -75,14 +79,18 @@ public class PathFinder {
+             Node node1 = this.openSet.pop();
+             node1.closed = true;
  
 -            for (Target target : set) {
 +            // Paper start - optimize collection
-+            for (int i1 = 0; i1 < positions.size(); i1++) {
-+                final Map.Entry<Target, BlockPos> entry = positions.get(i1);
++            for (int positionIndex = 0, size = positions.size(); positionIndex < size; positionIndex++) {
++                final Map.Entry<Target, BlockPos> entry = positions.get(positionIndex);
 +                Target target = entry.getKey();
-                 if (node.distanceManhattan(target) <= (float)distance) {
+                 if (node1.distanceManhattan(target) <= accuracy) {
                      target.setReached();
--                    set3.add(target);
+-                    set2.add(target);
 +                    entryList.add(entry);
 +                    // Paper end - Perf: remove streams and optimize collection
                  }
              }
  
--            if (!set3.isEmpty()) {
+-            if (!set2.isEmpty()) {
 +            if (!entryList.isEmpty()) { // Paper - Perf: remove streams and optimize collection; rename
                  break;
              }
  
-@@ -0,0 +0,0 @@ public class PathFinder {
-                     if (node2.walkedDistance < followRange && (!node2.inOpenSet() || g < node2.g)) {
-                         node2.cameFrom = node;
-                         node2.g = g;
+@@ -97,7 +105,7 @@ public class PathFinder {
+                     if (node2.walkedDistance < maxRange && (!node2.inOpenSet() || f1 < node2.g)) {
+                         node2.cameFrom = node1;
+                         node2.g = f1;
 -                        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 {
-@@ -0,0 +0,0 @@ public class PathFinder {
+@@ -109,25 +117,34 @@ public class PathFinder {
              }
          }
  
--        Optional<Path> optional = !set3.isEmpty()
--            ? set3.stream().map(node -> this.reconstructPath(node.getBestNode(), positions.get(node), true)).min(Comparator.comparingInt(Path::getNodeCount))
+-        Optional<Path> optional = !set2.isEmpty()
+-            ? set2.stream()
+-                .map(pathfinder -> this.reconstructPath(pathfinder.getBestNode(), targetPositions.get(pathfinder), true))
+-                .min(Comparator.comparingInt(Path::getNodeCount))
 -            : set.stream()
--                .map(targetx -> this.reconstructPath(targetx.getBestNode(), positions.get(targetx), false))
+-                .map(pathfinder -> this.reconstructPath(pathfinder.getBestNode(), targetPositions.get(pathfinder), 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<Path> comparator = entryListIsEmpty ? Comparator.comparingInt(Path::getNodeCount)
++        Comparator<Path> comparator = entryListIsEmpty
++            ? Comparator.comparingInt(Path::getNodeCount)
 +            : Comparator.comparingDouble(Path::getDistToTarget).thenComparingInt(Path::getNodeCount);
 +        for (Map.Entry<Target, BlockPos> entry : entryListIsEmpty ? positions : entryList) {
 +            Path path = this.reconstructPath(entry.getKey().getBestNode(), entry.getValue(), !entryListIsEmpty);
-+            if (best == null || comparator.compare(path, best) < 0)
++            if (best == null || comparator.compare(path, best) < 0) {
 +                best = path;
++            }
 +        }
          profilerFiller.pop();
 -        return optional.isEmpty() ? null : optional.get();
@@ -116,8 +119,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - Perf: remove streams and optimize collection
      }
  
-     protected float distance(Node a, Node b) {
-         return a.distanceTo(b);
+     protected float distance(Node first, Node second) {
+         return first.distanceTo(second);
      }
  
 -    private float getBestH(Node node, Set<Target> targets) {
@@ -129,6 +132,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        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);
+             float f1 = node.distanceTo(target);
+             target.updateBest(f1, node);
+             f = Math.min(f1, f);
diff --git a/feature-patches/1058-Rewrite-dataconverter-system.patch b/paper-server/patches/features/0016-Rewrite-dataconverter-system.patch
similarity index 94%
rename from feature-patches/1058-Rewrite-dataconverter-system.patch
rename to paper-server/patches/features/0016-Rewrite-dataconverter-system.patch
index f1790227a0..1f0f67af84 100644
--- a/feature-patches/1058-Rewrite-dataconverter-system.patch
+++ b/paper-server/patches/features/0016-Rewrite-dataconverter-system.patch
@@ -6,12 +6,12 @@ Subject: [PATCH] Rewrite dataconverter system
 Please see https://github.com/PaperMC/DataConverter
 for details.
 
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/converters/DataConverter.java b/src/main/java/ca/spottedleaf/dataconverter/converters/DataConverter.java
+diff --git a/ca/spottedleaf/dataconverter/converters/DataConverter.java b/ca/spottedleaf/dataconverter/converters/DataConverter.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..1863c606be715683d53863a0c9293525d199c9cf
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/converters/DataConverter.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/converters/DataConverter.java
+@@ -0,0 +1,54 @@
 +package ca.spottedleaf.dataconverter.converters;
 +
 +import java.util.Comparator;
@@ -66,12 +66,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return getVersion(encoded) + "." + getStep(encoded);
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/converters/datatypes/DataHook.java b/src/main/java/ca/spottedleaf/dataconverter/converters/datatypes/DataHook.java
+diff --git a/ca/spottedleaf/dataconverter/converters/datatypes/DataHook.java b/ca/spottedleaf/dataconverter/converters/datatypes/DataHook.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..0b92c5c66ad3a5198873f98287a5ced71c231d09
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/converters/datatypes/DataHook.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/converters/datatypes/DataHook.java
+@@ -0,0 +1,9 @@
 +package ca.spottedleaf.dataconverter.converters.datatypes;
 +
 +public interface DataHook<T, R> {
@@ -81,12 +81,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public R postHook(final T data, final long fromVersion, final long toVersion);
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/converters/datatypes/DataType.java b/src/main/java/ca/spottedleaf/dataconverter/converters/datatypes/DataType.java
+diff --git a/ca/spottedleaf/dataconverter/converters/datatypes/DataType.java b/ca/spottedleaf/dataconverter/converters/datatypes/DataType.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..b56a7f9ace3b947fed49101b6e9936721fb99ea5
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/converters/datatypes/DataType.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/converters/datatypes/DataType.java
+@@ -0,0 +1,7 @@
 +package ca.spottedleaf.dataconverter.converters.datatypes;
 +
 +public abstract class DataType<T, R> {
@@ -94,12 +94,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public abstract R convert(final T data, final long fromVersion, final long toVersion);
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/converters/datatypes/DataWalker.java b/src/main/java/ca/spottedleaf/dataconverter/converters/datatypes/DataWalker.java
+diff --git a/ca/spottedleaf/dataconverter/converters/datatypes/DataWalker.java b/ca/spottedleaf/dataconverter/converters/datatypes/DataWalker.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..ca55b3f7e7208e629e88d4c7bfa9517384a26fef
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/converters/datatypes/DataWalker.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/converters/datatypes/DataWalker.java
+@@ -0,0 +1,9 @@
 +package ca.spottedleaf.dataconverter.converters.datatypes;
 +
 +import ca.spottedleaf.dataconverter.types.MapType;
@@ -109,12 +109,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public T walk(final T data, final long fromVersion, final long toVersion);
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/MCDataConverter.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/MCDataConverter.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/MCDataConverter.java b/ca/spottedleaf/dataconverter/minecraft/MCDataConverter.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..a27d3d41109271834b6c37fa22d4b80d9e4b88c8
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/MCDataConverter.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/MCDataConverter.java
+@@ -0,0 +1,79 @@
 +package ca.spottedleaf.dataconverter.minecraft;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -194,12 +194,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private MCDataConverter() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/MCVersionRegistry.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/MCVersionRegistry.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/MCVersionRegistry.java b/ca/spottedleaf/dataconverter/minecraft/MCVersionRegistry.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..344c8c4f3207b6c8b565e5ad6db2470a272b77c3
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/MCVersionRegistry.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/MCVersionRegistry.java
+@@ -0,0 +1,447 @@
 +package ca.spottedleaf.dataconverter.minecraft;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -647,12 +647,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private MCVersionRegistry() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/MCVersions.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/MCVersions.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/MCVersions.java b/ca/spottedleaf/dataconverter/minecraft/MCVersions.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..94da5d6d2f43dae07cfc6750b23689fd4a175d2a
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/MCVersions.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/MCVersions.java
+@@ -0,0 +1,568 @@
 +package ca.spottedleaf.dataconverter.minecraft;
 +
 +@SuppressWarnings("unused")
@@ -1221,12 +1221,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private MCVersions() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/advancements/ConverterAbstractAdvancementsRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/advancements/ConverterAbstractAdvancementsRename.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/advancements/ConverterAbstractAdvancementsRename.java b/ca/spottedleaf/dataconverter/minecraft/converters/advancements/ConverterAbstractAdvancementsRename.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..ae3aed21c1fccb688e9a1665e2d317a77508d157
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/advancements/ConverterAbstractAdvancementsRename.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/advancements/ConverterAbstractAdvancementsRename.java
+@@ -0,0 +1,28 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.advancements;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -1255,12 +1255,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/advancements/ConverterCriteriaRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/advancements/ConverterCriteriaRename.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/advancements/ConverterCriteriaRename.java b/ca/spottedleaf/dataconverter/minecraft/converters/advancements/ConverterCriteriaRename.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..b2a4d16e6a2f9d71dbfa692922671581c2bec136
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/advancements/ConverterCriteriaRename.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/advancements/ConverterCriteriaRename.java
+@@ -0,0 +1,42 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.advancements;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -1303,12 +1303,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterAbstractAttributesRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterAbstractAttributesRename.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterAbstractAttributesRename.java b/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterAbstractAttributesRename.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f227c0565a0c475fcb06991b485507d50bbd2ad0
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterAbstractAttributesRename.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterAbstractAttributesRename.java
+@@ -0,0 +1,60 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.attributes;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -1369,12 +1369,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private ConverterAbstractAttributesRename() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterAbstractOldAttributesRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterAbstractOldAttributesRename.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterAbstractOldAttributesRename.java b/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterAbstractOldAttributesRename.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..1b871c78e77015d0216a0ecc61aa05689ccfab10
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterAbstractOldAttributesRename.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterAbstractOldAttributesRename.java
+@@ -0,0 +1,57 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.attributes;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -1432,12 +1432,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private ConverterAbstractOldAttributesRename() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterEntityAttributesBaseValueUpdater.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterEntityAttributesBaseValueUpdater.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterEntityAttributesBaseValueUpdater.java b/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterEntityAttributesBaseValueUpdater.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f64b7a1999f9f81ed752626f46803174a9889e9d
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterEntityAttributesBaseValueUpdater.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterEntityAttributesBaseValueUpdater.java
+@@ -0,0 +1,45 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.attributes;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -1483,12 +1483,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return null;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/blockname/ConverterAbstractBlockRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/blockname/ConverterAbstractBlockRename.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/blockname/ConverterAbstractBlockRename.java b/ca/spottedleaf/dataconverter/minecraft/converters/blockname/ConverterAbstractBlockRename.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..7b47879a7c2e8c21fae43bf5247585c716d75565
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/blockname/ConverterAbstractBlockRename.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/blockname/ConverterAbstractBlockRename.java
+@@ -0,0 +1,64 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.blockname;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -1553,12 +1553,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        });
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterAddBlendingData.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterAddBlendingData.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterAddBlendingData.java b/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterAddBlendingData.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..d4cd5362e77eb71cb8eb45ffcc73185e01be1157
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterAddBlendingData.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterAddBlendingData.java
+@@ -0,0 +1,65 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.chunk;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -1624,12 +1624,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return null;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterFlattenChunk.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterFlattenChunk.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterFlattenChunk.java b/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterFlattenChunk.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..300c2d14818b1e0cfe7341aba573ec75d0581b26
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterFlattenChunk.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterFlattenChunk.java
+@@ -0,0 +1,1016 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.chunk;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -2646,12 +2646,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterRenameStatus.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterRenameStatus.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterRenameStatus.java b/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterRenameStatus.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..084c67a46bc5ec7f5a4bef3216805a87b32c83d0
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterRenameStatus.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterRenameStatus.java
+@@ -0,0 +1,32 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.chunk;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -2684,12 +2684,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return null;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/custom/V3818_Commands.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/custom/V3818_Commands.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/custom/V3818_Commands.java b/ca/spottedleaf/dataconverter/minecraft/converters/custom/V3818_Commands.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..cd190605a2c3d8631f85a74a634f7951eec6f0b1
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/custom/V3818_Commands.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/custom/V3818_Commands.java
+@@ -0,0 +1,304 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.custom;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -2994,12 +2994,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:hanging_sign", signTileConverter);
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterAbstractEntityRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterAbstractEntityRename.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterAbstractEntityRename.java b/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterAbstractEntityRename.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..6684915d6c0c44328a9296dc3ceb530e69482083
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterAbstractEntityRename.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterAbstractEntityRename.java
+@@ -0,0 +1,38 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.entity;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -3038,12 +3038,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterEntityToVariant.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterEntityToVariant.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterEntityToVariant.java b/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterEntityToVariant.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..985af815e3c23ad7c8b774eac46a7202d3020234
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterEntityToVariant.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterEntityToVariant.java
+@@ -0,0 +1,44 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.entity;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -3088,12 +3088,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return null;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterEntityVariantRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterEntityVariantRename.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterEntityVariantRename.java b/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterEntityVariantRename.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..ed5dcf6f8160742c07e23e98c85409209350a7d4
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterEntityVariantRename.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterEntityVariantRename.java
+@@ -0,0 +1,37 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.entity;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -3131,12 +3131,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return null;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterFlattenEntity.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterFlattenEntity.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterFlattenEntity.java b/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterFlattenEntity.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..afad2d92f78d4727ff4440ad2778f018d5a2a609
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterFlattenEntity.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterFlattenEntity.java
+@@ -0,0 +1,371 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.entity;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -3508,12 +3508,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return null;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/AddFlagIfAbsent.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/AddFlagIfAbsent.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/helpers/AddFlagIfAbsent.java b/ca/spottedleaf/dataconverter/minecraft/converters/helpers/AddFlagIfAbsent.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..4ab607f946782cc483535564e86fa9753dd7897a
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/AddFlagIfAbsent.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/helpers/AddFlagIfAbsent.java
+@@ -0,0 +1,30 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.helpers;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -3544,12 +3544,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return null;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/ConverterAbstractStringValueTypeRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/ConverterAbstractStringValueTypeRename.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/helpers/ConverterAbstractStringValueTypeRename.java b/ca/spottedleaf/dataconverter/minecraft/converters/helpers/ConverterAbstractStringValueTypeRename.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..bc79670f47aaa413ea3e96ef6a32e14099ad8a58
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/ConverterAbstractStringValueTypeRename.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/helpers/ConverterAbstractStringValueTypeRename.java
+@@ -0,0 +1,24 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.helpers;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -3574,12 +3574,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperBlockFlatteningV1450.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperBlockFlatteningV1450.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperBlockFlatteningV1450.java b/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperBlockFlatteningV1450.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..4f4f4cb6037c2a46ffcf427f5812164bbb98b8b7
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperBlockFlatteningV1450.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperBlockFlatteningV1450.java
+@@ -0,0 +1,1829 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.helpers;
 +
 +import ca.spottedleaf.dataconverter.types.MapType;
@@ -5409,12 +5409,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        finalizeMaps();
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperItemNameV102.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperItemNameV102.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperItemNameV102.java b/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperItemNameV102.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..86f6aa3e3fa886976809f350fc5eb16f6a026ed9
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperItemNameV102.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperItemNameV102.java
+@@ -0,0 +1,533 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.helpers;
 +
 +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
@@ -5948,12 +5948,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return POTION_NAMES[id & 127];
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperSpawnEggNameV105.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperSpawnEggNameV105.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperSpawnEggNameV105.java b/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperSpawnEggNameV105.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..bcc586cb68148fd960dd685eecce853169a92ed5
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperSpawnEggNameV105.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperSpawnEggNameV105.java
+@@ -0,0 +1,77 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.helpers;
 +
 +public final class HelperSpawnEggNameV105 {
@@ -6031,12 +6031,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return ID_TO_STRING[id & 255];
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/RenameHelper.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/RenameHelper.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/helpers/RenameHelper.java b/ca/spottedleaf/dataconverter/minecraft/converters/helpers/RenameHelper.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..28dcc6f1425a46c6c76dd16a67aeab0ec72d1d6a
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/RenameHelper.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/helpers/RenameHelper.java
+@@ -0,0 +1,106 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.helpers;
 +
 +import ca.spottedleaf.dataconverter.types.ListType;
@@ -6143,12 +6143,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private RenameHelper() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemname/ConverterAbstractItemRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemname/ConverterAbstractItemRename.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/itemname/ConverterAbstractItemRename.java b/ca/spottedleaf/dataconverter/minecraft/converters/itemname/ConverterAbstractItemRename.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..94569f0ccff0d3a09eafd4ba73572d9db0a0ac5b
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemname/ConverterAbstractItemRename.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/itemname/ConverterAbstractItemRename.java
+@@ -0,0 +1,18 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.itemname;
 +
 +import ca.spottedleaf.dataconverter.minecraft.converters.helpers.ConverterAbstractStringValueTypeRename;
@@ -6167,12 +6167,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterEnchantmentsRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterEnchantmentsRename.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterEnchantmentsRename.java b/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterEnchantmentsRename.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..06596b56a1f89900e5f23f7f4a12bd1d5d02b7c8
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterEnchantmentsRename.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterEnchantmentsRename.java
+@@ -0,0 +1,38 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.itemstack;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -6211,12 +6211,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return null;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterFlattenItemStack.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterFlattenItemStack.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterFlattenItemStack.java b/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterFlattenItemStack.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..21176b8b96be6cb93d3dc1a74ae9f53f1ad4740c
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterFlattenItemStack.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterFlattenItemStack.java
+@@ -0,0 +1,460 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.itemstack;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -6677,12 +6677,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return null;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterFlattenSpawnEgg.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterFlattenSpawnEgg.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterFlattenSpawnEgg.java b/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterFlattenSpawnEgg.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..4fa31e40b0a6f571a853299b4e242de921ccbda0
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterFlattenSpawnEgg.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterFlattenSpawnEgg.java
+@@ -0,0 +1,87 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.itemstack;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -6770,12 +6770,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return null;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterItemStackToDataComponents.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterItemStackToDataComponents.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterItemStackToDataComponents.java b/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterItemStackToDataComponents.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..2d29d89cc45866822189a62bffbe1a8fe57c477b
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterItemStackToDataComponents.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterItemStackToDataComponents.java
+@@ -0,0 +1,1245 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.itemstack;
 +
 +import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper;
@@ -8021,12 +8021,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/leveldat/ConverterRemoveFeatureFlag.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/leveldat/ConverterRemoveFeatureFlag.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/leveldat/ConverterRemoveFeatureFlag.java b/ca/spottedleaf/dataconverter/minecraft/converters/leveldat/ConverterRemoveFeatureFlag.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..4c537b661b7a28193add3267ec2d639add49423b
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/leveldat/ConverterRemoveFeatureFlag.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/leveldat/ConverterRemoveFeatureFlag.java
+@@ -0,0 +1,46 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.leveldat;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -8073,12 +8073,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return null;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/options/ConverterAbstractOptionsRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/options/ConverterAbstractOptionsRename.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/options/ConverterAbstractOptionsRename.java b/ca/spottedleaf/dataconverter/minecraft/converters/options/ConverterAbstractOptionsRename.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..769dd8447976b66dcfc36283ede4ae16f1e4206d
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/options/ConverterAbstractOptionsRename.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/options/ConverterAbstractOptionsRename.java
+@@ -0,0 +1,28 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.options;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -8107,12 +8107,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/particle/ConverterParticleToNBT.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/particle/ConverterParticleToNBT.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/particle/ConverterParticleToNBT.java b/ca/spottedleaf/dataconverter/minecraft/converters/particle/ConverterParticleToNBT.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..2cf90187ea8bc54b06cebd54ae2582ca66d91132
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/particle/ConverterParticleToNBT.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/particle/ConverterParticleToNBT.java
+@@ -0,0 +1,270 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.particle;
 +
 +import ca.spottedleaf.dataconverter.types.ListType;
@@ -8383,12 +8383,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private ConverterParticleToNBT() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/poi/ConverterAbstractPOIRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/poi/ConverterAbstractPOIRename.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/poi/ConverterAbstractPOIRename.java b/ca/spottedleaf/dataconverter/minecraft/converters/poi/ConverterAbstractPOIRename.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..57e210bf2bb189b15a32899011c4800b19668a5e
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/poi/ConverterAbstractPOIRename.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/poi/ConverterAbstractPOIRename.java
+@@ -0,0 +1,53 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.poi;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -8442,12 +8442,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        });
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/poi/ConverterPoiDelete.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/poi/ConverterPoiDelete.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/poi/ConverterPoiDelete.java b/ca/spottedleaf/dataconverter/minecraft/converters/poi/ConverterPoiDelete.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..36aa9c3eedb3f2e2f577efed3622fed74268bce1
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/poi/ConverterPoiDelete.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/poi/ConverterPoiDelete.java
+@@ -0,0 +1,53 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.poi;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -8501,12 +8501,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return null;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/recipe/ConverterAbstractRecipeRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/recipe/ConverterAbstractRecipeRename.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/recipe/ConverterAbstractRecipeRename.java b/ca/spottedleaf/dataconverter/minecraft/converters/recipe/ConverterAbstractRecipeRename.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..8f35cbbd78a629712f9ae3cd5d180269f015a11d
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/recipe/ConverterAbstractRecipeRename.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/recipe/ConverterAbstractRecipeRename.java
+@@ -0,0 +1,18 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.recipe;
 +
 +import ca.spottedleaf.dataconverter.minecraft.converters.helpers.ConverterAbstractStringValueTypeRename;
@@ -8525,12 +8525,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        ConverterAbstractStringValueTypeRename.register(version, subVersion, MCTypeRegistry.RECIPE, renamer);
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/stats/ConverterAbstractStatsRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/stats/ConverterAbstractStatsRename.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/stats/ConverterAbstractStatsRename.java b/ca/spottedleaf/dataconverter/minecraft/converters/stats/ConverterAbstractStatsRename.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..a1985c85aa9193699d7d20e6f4f11b6e9744ee70
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/stats/ConverterAbstractStatsRename.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/stats/ConverterAbstractStatsRename.java
+@@ -0,0 +1,66 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.stats;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -8597,12 +8597,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        });
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/stats/ConverterFlattenStats.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/stats/ConverterFlattenStats.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/stats/ConverterFlattenStats.java b/ca/spottedleaf/dataconverter/minecraft/converters/stats/ConverterFlattenStats.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..891be75bf5c4af56e839c88b26f0a828554ae5c4
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/stats/ConverterFlattenStats.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/stats/ConverterFlattenStats.java
+@@ -0,0 +1,321 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.stats;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -8924,12 +8924,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        };
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/tileentity/ConverterAbstractTileEntityRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/tileentity/ConverterAbstractTileEntityRename.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/tileentity/ConverterAbstractTileEntityRename.java b/ca/spottedleaf/dataconverter/minecraft/converters/tileentity/ConverterAbstractTileEntityRename.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..ab05dda0cc2083418443d0dee23ccc0a6f754ea0
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/tileentity/ConverterAbstractTileEntityRename.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/converters/tileentity/ConverterAbstractTileEntityRename.java
+@@ -0,0 +1,34 @@
 +package ca.spottedleaf.dataconverter.minecraft.converters.tileentity;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -8964,12 +8964,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/DynamicDataType.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/DynamicDataType.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/datatypes/DynamicDataType.java b/ca/spottedleaf/dataconverter/minecraft/datatypes/DynamicDataType.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..dfa750bdaef7d7b6dadbc5665c1461f7e6df08ca
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/DynamicDataType.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/datatypes/DynamicDataType.java
+@@ -0,0 +1,128 @@
 +package ca.spottedleaf.dataconverter.minecraft.datatypes;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -9098,12 +9098,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return ret;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/IDDataType.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/IDDataType.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/datatypes/IDDataType.java b/ca/spottedleaf/dataconverter/minecraft/datatypes/IDDataType.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..b093a9eeeea3f7c1c220485b7144d22c6fd504a0
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/IDDataType.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/datatypes/IDDataType.java
+@@ -0,0 +1,166 @@
 +package ca.spottedleaf.dataconverter.minecraft.datatypes;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -9270,12 +9270,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return ret;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/MCDataType.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/MCDataType.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/datatypes/MCDataType.java b/ca/spottedleaf/dataconverter/minecraft/datatypes/MCDataType.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..075574f33476882ddc6787e3b8bac8643a414eb0
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/MCDataType.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/datatypes/MCDataType.java
+@@ -0,0 +1,129 @@
 +package ca.spottedleaf.dataconverter.minecraft.datatypes;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -9405,12 +9405,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return ret;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/MCTypeRegistry.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/MCTypeRegistry.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/datatypes/MCTypeRegistry.java b/ca/spottedleaf/dataconverter/minecraft/datatypes/MCTypeRegistry.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..d42bff4fec99eb0b19d132794f4e3306b6dddb0f
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/MCTypeRegistry.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/datatypes/MCTypeRegistry.java
+@@ -0,0 +1,335 @@
 +package ca.spottedleaf.dataconverter.minecraft.datatypes;
 +
 +import ca.spottedleaf.dataconverter.minecraft.versions.*;
@@ -9746,12 +9746,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private MCTypeRegistry() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/MCValueType.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/MCValueType.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/datatypes/MCValueType.java b/ca/spottedleaf/dataconverter/minecraft/datatypes/MCValueType.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..13c1381261909ef672fbeb665907f01f2d5c1ced
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/MCValueType.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/datatypes/MCValueType.java
+@@ -0,0 +1,86 @@
 +package ca.spottedleaf.dataconverter.minecraft.datatypes;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -9838,12 +9838,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return ret;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/hooks/DataHookEnforceNamespacedID.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/hooks/DataHookEnforceNamespacedID.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/hooks/DataHookEnforceNamespacedID.java b/ca/spottedleaf/dataconverter/minecraft/hooks/DataHookEnforceNamespacedID.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f7dced8a47ebdd262ae815ff9bc453312343ce49
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/hooks/DataHookEnforceNamespacedID.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/hooks/DataHookEnforceNamespacedID.java
+@@ -0,0 +1,29 @@
 +package ca.spottedleaf.dataconverter.minecraft.hooks;
 +
 +import ca.spottedleaf.dataconverter.converters.datatypes.DataHook;
@@ -9873,12 +9873,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return null;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/hooks/DataHookValueTypeEnforceNamespaced.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/hooks/DataHookValueTypeEnforceNamespaced.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/hooks/DataHookValueTypeEnforceNamespaced.java b/ca/spottedleaf/dataconverter/minecraft/hooks/DataHookValueTypeEnforceNamespaced.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..7f88487e7db589070512fafef1eb243ae29a379a
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/hooks/DataHookValueTypeEnforceNamespaced.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/hooks/DataHookValueTypeEnforceNamespaced.java
+@@ -0,0 +1,20 @@
 +package ca.spottedleaf.dataconverter.minecraft.hooks;
 +
 +import ca.spottedleaf.dataconverter.converters.datatypes.DataHook;
@@ -9899,12 +9899,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return null;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/util/ComponentUtils.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/util/ComponentUtils.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/util/ComponentUtils.java b/ca/spottedleaf/dataconverter/minecraft/util/ComponentUtils.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..17ded002b5546de8be4a5238c20ccfda460a98bb
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/util/ComponentUtils.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/util/ComponentUtils.java
+@@ -0,0 +1,82 @@
 +package ca.spottedleaf.dataconverter.minecraft.util;
 +
 +import com.google.gson.JsonElement;
@@ -9987,12 +9987,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private ComponentUtils() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V100.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V100.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V100.java b/ca/spottedleaf/dataconverter/minecraft/versions/V100.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..91b1d0be9d697a4fa8bc5b448b329df1f5deabc4
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V100.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V100.java
+@@ -0,0 +1,161 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -10154,12 +10154,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V100() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V101.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V101.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V101.java b/ca/spottedleaf/dataconverter/minecraft/versions/V101.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..32d54d5960088b547b3ca09bff28b0752dddd77c
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V101.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V101.java
+@@ -0,0 +1,45 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -10205,12 +10205,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V101() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V102.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V102.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V102.java b/ca/spottedleaf/dataconverter/minecraft/versions/V102.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..00bb3cff8f3d220d65a18f9b82b4b5361588b109
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V102.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V102.java
+@@ -0,0 +1,86 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -10297,12 +10297,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V102() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1022.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1022.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1022.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1022.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..4f35484ed524dbf09cf9e8b1bb999fc98ec0bb0f
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1022.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1022.java
+@@ -0,0 +1,43 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -10346,12 +10346,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1022() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V105.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V105.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V105.java b/ca/spottedleaf/dataconverter/minecraft/versions/V105.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..189b682da7eacea118610e466e8648675fccf776
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V105.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V105.java
+@@ -0,0 +1,49 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -10401,12 +10401,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V105() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V106.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V106.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V106.java b/ca/spottedleaf/dataconverter/minecraft/versions/V106.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..fa9b11b46f0fbcaabcaed02a7fc3f5af3337ec27
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V106.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V106.java
+@@ -0,0 +1,83 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -10490,12 +10490,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V106() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V107.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V107.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V107.java b/ca/spottedleaf/dataconverter/minecraft/versions/V107.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..e9d288c41c40d96ac7c6b605babc436d6a5796f3
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V107.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V107.java
+@@ -0,0 +1,43 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -10539,12 +10539,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V107() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V108.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V108.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V108.java b/ca/spottedleaf/dataconverter/minecraft/versions/V108.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..ba9487bc35bedfd7261d4a4fd9476de070f65f33
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V108.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V108.java
+@@ -0,0 +1,46 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -10591,12 +10591,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V108() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V109.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V109.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V109.java b/ca/spottedleaf/dataconverter/minecraft/versions/V109.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..5df0c8da6415a4651e5678a170bc8ff32dd66337
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V109.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V109.java
+@@ -0,0 +1,41 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -10638,12 +10638,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V109() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V110.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V110.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V110.java b/ca/spottedleaf/dataconverter/minecraft/versions/V110.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..b089fc93b88c5a7b4bb1eb0e105120b5393de1b1
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V110.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V110.java
+@@ -0,0 +1,38 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -10682,12 +10682,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V110() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V111.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V111.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V111.java b/ca/spottedleaf/dataconverter/minecraft/versions/V111.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..0c69cf9b419049dc5338abb408fa3f0390e4e353
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V111.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V111.java
+@@ -0,0 +1,64 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -10752,12 +10752,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1125.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1125.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1125.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1125.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..41ceef54e202420616ad57e9f9c200457c7d2848
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1125.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1125.java
+@@ -0,0 +1,101 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -10859,12 +10859,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1125() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V113.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V113.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V113.java b/ca/spottedleaf/dataconverter/minecraft/versions/V113.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..7b7d02eac9e121c45b557b664e156327d182c015
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V113.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V113.java
+@@ -0,0 +1,40 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -10905,12 +10905,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V113() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1344.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1344.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1344.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1344.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..b735165f9b296730b77339875255aa982e18a40a
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1344.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1344.java
+@@ -0,0 +1,176 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -11087,12 +11087,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1344() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V135.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V135.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V135.java b/ca/spottedleaf/dataconverter/minecraft/versions/V135.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..b003819eb395039dca8141179b57632e90db1d4d
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V135.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V135.java
+@@ -0,0 +1,62 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -11155,12 +11155,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V135() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V143.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V143.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V143.java b/ca/spottedleaf/dataconverter/minecraft/versions/V143.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..90889dddd8a510fe69c47413f5fe3ed4a756fedb
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V143.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V143.java
+@@ -0,0 +1,17 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -11178,12 +11178,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V143() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1446.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1446.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1446.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1446.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..0e198bef171c92d53725d338bb793b1e269f2997
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1446.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1446.java
+@@ -0,0 +1,35 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -11219,12 +11219,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1446() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1450.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1450.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1450.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1450.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..bf6f57bc84785622aea35dc70872db6d4d9516a1
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1450.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1450.java
+@@ -0,0 +1,24 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -11249,12 +11249,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1450() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1451.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1451.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1451.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1451.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..2f6a43d858645baeb3c69959479b6835dd7bd7a8
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1451.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1451.java
+@@ -0,0 +1,513 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -11768,12 +11768,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1451() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1456.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1456.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1456.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1456.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..47682ffbc10805a4cba73dca43198e52c0ce63df
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1456.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1456.java
+@@ -0,0 +1,37 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -11811,12 +11811,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1456() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1458.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1458.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1458.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1458.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..95822caa64d6c8a780bb120bedd2728355d26b84
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1458.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1458.java
+@@ -0,0 +1,87 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -11904,12 +11904,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1458() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1460.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1460.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1460.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1460.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..bf64be7255b02461d218a821ac9b36ba5bc83b13
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1460.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1460.java
+@@ -0,0 +1,45 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -11955,12 +11955,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1460() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1466.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1466.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1466.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1466.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..d870aaca4ff623c71604f889c2e667bfe50fe696
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1466.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1466.java
+@@ -0,0 +1,142 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -12103,12 +12103,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1466() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V147.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V147.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V147.java b/ca/spottedleaf/dataconverter/minecraft/versions/V147.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..af9c6ee26580eb10bf8426f5b61c26df63a910a6
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V147.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V147.java
+@@ -0,0 +1,26 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -12135,12 +12135,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V147() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1470.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1470.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1470.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1470.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..2bf1baee2321b3cb584ab6355f43263d6c8ec0be
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1470.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1470.java
+@@ -0,0 +1,31 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -12172,12 +12172,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1470() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1474.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1474.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1474.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1474.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..99f0f34cc14639ed8ed73b847f74cdc607607af8
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1474.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1474.java
+@@ -0,0 +1,34 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -12212,12 +12212,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1474() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1475.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1475.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1475.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1475.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..2ae50eea847671f3995688901c79caf520440d7a
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1475.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1475.java
+@@ -0,0 +1,22 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -12240,12 +12240,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1475() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1480.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1480.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1480.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1480.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..7180c1168bffb9fe70d18fe7414a5372518413a8
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1480.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1480.java
+@@ -0,0 +1,45 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -12291,12 +12291,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1480() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1483.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1483.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1483.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1483.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..56d9babebba8b8ba6be07ea413e9c04ffea84023
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1483.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1483.java
+@@ -0,0 +1,30 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -12327,12 +12327,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1483() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1484.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1484.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1484.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1484.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..cdbb9379f66aa6edc05c5e6cb2bdeae97f1ea38b
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1484.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1484.java
+@@ -0,0 +1,75 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -12408,12 +12408,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1484() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1486.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1486.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1486.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1486.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..a9e42da41064ea293a71dbf2d681a857b2e1812e
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1486.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1486.java
+@@ -0,0 +1,39 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -12453,12 +12453,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1486() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1487.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1487.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1487.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1487.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..884049818efdf273443fb3d1c2d7250564fbdbf7
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1487.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1487.java
+@@ -0,0 +1,27 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -12486,12 +12486,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1487() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1488.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1488.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1488.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1488.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..907a5e2a26ee046e292508e1f06d5f26d10af8c1
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1488.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1488.java
+@@ -0,0 +1,93 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -12585,12 +12585,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1488() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1490.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1490.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1490.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1490.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..1e99de15732bdd283835a9531f76e29ddab91f46
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1490.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1490.java
+@@ -0,0 +1,30 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -12621,12 +12621,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1490() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1492.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1492.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1492.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1492.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..1259216b43434d0f7c7be10a081fd05057c253cf
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1492.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1492.java
+@@ -0,0 +1,151 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -12778,12 +12778,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1492() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1494.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1494.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1494.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1494.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..b72fe109aa8c60425c00aad234d60a1c70dda60b
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1494.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1494.java
+@@ -0,0 +1,88 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -12872,12 +12872,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1494() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1496.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1496.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1496.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1496.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..10349a70b865b19cca471a16548fd49910a2b0e7
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1496.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1496.java
+@@ -0,0 +1,370 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -13248,12 +13248,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1500.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1500.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1500.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1500.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..fae8cf61c9900544cdecd223f72e1311c8a1cfb1
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1500.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1500.java
+@@ -0,0 +1,23 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -13277,12 +13277,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1500() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1501.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1501.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1501.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1501.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..dbfb51b74c54a9a479de49ecb295854fc69aef64
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1501.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1501.java
+@@ -0,0 +1,78 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -13361,12 +13361,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1501() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1502.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1502.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1502.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1502.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..cd07718649f0e2ca66f1ec3b0aba81611333ba09
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1502.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1502.java
+@@ -0,0 +1,77 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -13444,12 +13444,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1502() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1506.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1506.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1506.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1506.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..ce87995961605c80f24371c9c64706ae76e3edea
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1506.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1506.java
+@@ -0,0 +1,219 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -13669,12 +13669,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1506() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1510.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1510.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1510.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1510.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..dfc9d1e89983c73e06ce3c8a22c29f49af4a935c
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1510.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1510.java
+@@ -0,0 +1,111 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -13786,12 +13786,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1510() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1514.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1514.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1514.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1514.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..6bcc0de5987db4d9ac28fabefbb58c28f2065d96
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1514.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1514.java
+@@ -0,0 +1,68 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -13860,12 +13860,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1514() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1515.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1515.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1515.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1515.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..d2093732e06ddccdd8a34bbfcaee6ede3aae96d0
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1515.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1515.java
+@@ -0,0 +1,28 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -13894,12 +13894,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1515() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1624.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1624.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1624.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1624.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f198495e1bad7a1cb84f41c1ea96b1d0e7943c9e
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1624.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1624.java
+@@ -0,0 +1,110 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -14010,12 +14010,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V165.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V165.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V165.java b/ca/spottedleaf/dataconverter/minecraft/versions/V165.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..810a838edeea95bb5d0b4b351e65417b762fc45c
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V165.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V165.java
+@@ -0,0 +1,41 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -14057,12 +14057,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V165() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1800.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1800.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1800.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1800.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..7f65def5a0f48af268183d9c3b74937924b47b75
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1800.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1800.java
+@@ -0,0 +1,36 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -14099,12 +14099,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1800() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1801.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1801.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1801.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1801.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..9e1a3af9fb261e585542495f189f898eaa6d9263
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1801.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1801.java
+@@ -0,0 +1,18 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -14123,12 +14123,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1801() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1802.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1802.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1802.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1802.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..aeae0c62efa1e189fe4b0da585c8a2a101bb5ede
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1802.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1802.java
+@@ -0,0 +1,28 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -14157,12 +14157,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1802() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1803.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1803.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1803.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1803.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..ad12a97fe28b6f05973f0927245c944dcf184c46
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1803.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1803.java
+@@ -0,0 +1,46 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -14209,12 +14209,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1803() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1904.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1904.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1904.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1904.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..2066f320d774319bec84007ca7ed137eb78d91d1
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1904.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1904.java
+@@ -0,0 +1,42 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -14257,12 +14257,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1904() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1905.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1905.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1905.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1905.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..a4bd2c65fe5a4b4d3e430e5c7eee79435afac4ee
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1905.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1905.java
+@@ -0,0 +1,34 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -14297,12 +14297,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1905() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1906.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1906.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1906.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1906.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..dbf3215a781555d048077565851884eeb48402b1
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1906.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1906.java
+@@ -0,0 +1,20 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -14323,12 +14323,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1906() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1909.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1909.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1909.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1909.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..ede4d0bfc0fe0e4a3a6fb906037a4c964baac6e6
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1909.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1909.java
+@@ -0,0 +1,16 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -14345,12 +14345,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1909() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1911.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1911.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1911.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1911.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..02204cd67dc614e95f2ab95ed413ce62baec296f
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1911.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1911.java
+@@ -0,0 +1,49 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -14400,12 +14400,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1911() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1914.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1914.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1914.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1914.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..a965a5941e3624db725a4f101405357df11598c8
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1914.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1914.java
+@@ -0,0 +1,28 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -14434,12 +14434,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1914() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1917.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1917.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1917.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1917.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f8b5f5818ed4e839b62777a5d5e9baf70b12a6f0
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1917.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1917.java
+@@ -0,0 +1,25 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -14465,12 +14465,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1917() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1918.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1918.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1918.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1918.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f97f21e12af1e02aacc1591a88b5da3d7e3f4cfa
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1918.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1918.java
+@@ -0,0 +1,65 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -14536,12 +14536,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1918() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1920.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1920.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1920.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1920.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..fe2d58caf2371f1c430dea209210357f36392a96
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1920.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1920.java
+@@ -0,0 +1,75 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -14617,12 +14617,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1920() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1925.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1925.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1925.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1925.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..7f2db47a58baf1851abb9269b13fb08d4740081a
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1925.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1925.java
+@@ -0,0 +1,30 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -14653,12 +14653,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1925() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1928.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1928.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1928.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1928.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f1f7cd60d3fb1d7d3de92091681932607b452d25
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1928.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1928.java
+@@ -0,0 +1,33 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -14692,12 +14692,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1928() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1929.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1929.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1929.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1929.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..cc377819db8182b466b92aba9a9c0d2c483f941d
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1929.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1929.java
+@@ -0,0 +1,34 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -14732,12 +14732,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1929() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1931.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1931.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1931.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1931.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..0ae698a80e81a1648bb90149d9f0effdec8e777c
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1931.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1931.java
+@@ -0,0 +1,19 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -14757,12 +14757,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    private V1931() {}
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1936.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1936.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1936.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1936.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..ddebd1ea2eec5e469d4857503965084d78afce19
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1936.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1936.java
+@@ -0,0 +1,37 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -14800,12 +14800,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1936() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1946.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1946.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1946.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1946.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..70d3ab9fe12fab7282edc18938faa94a34d3decb
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1946.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1946.java
+@@ -0,0 +1,41 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -14847,12 +14847,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1946() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1948.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1948.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1948.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1948.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..19b0a1197cdf5988f21ba332883b65df646ff0c1
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1948.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1948.java
+@@ -0,0 +1,39 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -14892,12 +14892,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1948() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1953.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1953.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1953.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1953.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..c7887c54c85dd7a198aa5c1597c02b2d6887bf71
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1953.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1953.java
+@@ -0,0 +1,26 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -14924,12 +14924,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1953() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1955.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1955.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1955.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1955.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..8654f8c7f759720e1e1dd8ae94656699f151c407
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1955.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1955.java
+@@ -0,0 +1,93 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -15023,12 +15023,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1955() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1961.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1961.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1961.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1961.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..4b1b9b55e2491bd98efddfb28e2aa1074140a1c2
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1961.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1961.java
+@@ -0,0 +1,29 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -15058,12 +15058,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1961() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1963.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1963.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V1963.java b/ca/spottedleaf/dataconverter/minecraft/versions/V1963.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..023d8b9aa7d95c674847d9c5dbe0061adcbdc4d3
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1963.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V1963.java
+@@ -0,0 +1,39 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -15103,12 +15103,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V1963() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2100.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2100.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2100.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2100.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..cec032b20e834a8c6c8901e6fb2d127d7c80b353
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2100.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2100.java
+@@ -0,0 +1,51 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -15160,12 +15160,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2100() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2202.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2202.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2202.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2202.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..c9a23cf055353ee49f07263ea01161de2c035138
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2202.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2202.java
+@@ -0,0 +1,49 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -15215,12 +15215,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2202() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2209.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2209.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2209.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2209.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..7439d0e948f144d93a1fa7b57c2b478a54835d6d
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2209.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2209.java
+@@ -0,0 +1,28 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -15249,12 +15249,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2209() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2211.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2211.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2211.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2211.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..20904d3e18b317a2f7e5d6063fcf94dda27b5768
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2211.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2211.java
+@@ -0,0 +1,31 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -15286,12 +15286,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2211() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2218.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2218.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2218.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2218.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..8297fe9ab7007399847f3e7ac84519f0dec08576
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2218.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2218.java
+@@ -0,0 +1,33 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -15325,12 +15325,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2218() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2501.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2501.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2501.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2501.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f2be8817fe733ae30729952a2aae13d2396b8111
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2501.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2501.java
+@@ -0,0 +1,65 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -15396,12 +15396,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2501() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2502.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2502.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2502.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2502.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..540ae9aab0acdfbd3800db0468c52e973cb8d93f
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2502.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2502.java
+@@ -0,0 +1,18 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -15420,12 +15420,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2502() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2503.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2503.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2503.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2503.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..994960d0e67ed0af48d33e9a3db5d1757d85eac5
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2503.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2503.java
+@@ -0,0 +1,73 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -15499,12 +15499,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2503() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2505.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2505.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2505.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2505.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..9342d9efeb1980c7cb67bf0620d12bd9f71165ee
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2505.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2505.java
+@@ -0,0 +1,48 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -15553,12 +15553,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2505() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2508.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2508.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2508.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2508.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f9e9d88e4cca15d2d4fdcbc0dbcae4c35c02284a
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2508.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2508.java
+@@ -0,0 +1,27 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -15586,12 +15586,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2508() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2509.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2509.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2509.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2509.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..b948564d01726d9891a0733896b3e5cec937bd6d
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2509.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2509.java
+@@ -0,0 +1,33 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -15625,12 +15625,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2509() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2511.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2511.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2511.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2511.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..a640878469c7ea155cde1cca728b15f2a4bacd73
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2511.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2511.java
+@@ -0,0 +1,97 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -15728,12 +15728,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2511() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2514.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2514.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2514.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2514.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..dcd2b1689bbd845238c86cea9dae0c5153d01499
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2514.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2514.java
+@@ -0,0 +1,590 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -16324,12 +16324,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2514() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2516.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2516.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2516.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2516.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..99f65d84ffaa75db3d2b4568c92d85d3ef20b77f
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2516.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2516.java
+@@ -0,0 +1,37 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -16367,12 +16367,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2516() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2518.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2518.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2518.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2518.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..35eccf43fd7e31071a9d64883212cddf021ae861
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2518.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2518.java
+@@ -0,0 +1,65 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -16438,12 +16438,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2518() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2519.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2519.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2519.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2519.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..7cb7106037b18c0cf8ddff1f9ba25d4f987a6326
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2519.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2519.java
+@@ -0,0 +1,18 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -16462,12 +16462,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2519() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2522.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2522.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2522.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2522.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..9a4d47d78596e2275745673f31f772f0252f2cda
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2522.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2522.java
+@@ -0,0 +1,18 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -16486,12 +16486,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2522() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2523.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2523.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2523.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2523.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..7777d83d63dc177f0bac72290ed2e5c3cbd028be
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2523.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2523.java
+@@ -0,0 +1,41 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -16533,12 +16533,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2523() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2527.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2527.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2527.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2527.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..157f4b1673f7b71942949d979890b30a5f9e2ca3
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2527.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2527.java
+@@ -0,0 +1,123 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -16662,12 +16662,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2527() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2528.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2528.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2528.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2528.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..e7197d098b3d6269d3a4fd9be0432d85f0504dfd
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2528.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2528.java
+@@ -0,0 +1,30 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -16698,12 +16698,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2528() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2529.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2529.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2529.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2529.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..4e54a4ee0c14109609d8d8f1bc6c0c5dabf4fb07
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2529.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2529.java
+@@ -0,0 +1,25 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -16729,12 +16729,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2529() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2531.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2531.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2531.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2531.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..9306ab25feae6315e48aeeb71de960bdf62bcf76
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2531.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2531.java
+@@ -0,0 +1,63 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -16798,12 +16798,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2531() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2533.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2533.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2533.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2533.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f8d493674380d53398c853899da76024c9d84984
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2533.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2533.java
+@@ -0,0 +1,42 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -16846,12 +16846,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2533() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2535.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2535.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2535.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2535.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..c0f6135fff38100c1955d64ee3f4ff984308e503
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2535.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2535.java
+@@ -0,0 +1,34 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -16886,12 +16886,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2535() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2538.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2538.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2538.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2538.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..99d1df6362b290fdaa65385168ff6588647a8056
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2538.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2538.java
+@@ -0,0 +1,43 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -16935,12 +16935,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2538() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2550.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2550.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2550.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2550.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f64f2c2d6051b7e7024a0ebc42c1dd8dc6434cf9
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2550.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2550.java
+@@ -0,0 +1,346 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -17287,12 +17287,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2550() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2551.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2551.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2551.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2551.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..9cfecd222ef41fdb4f31517a0821d7532386285f
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2551.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2551.java
+@@ -0,0 +1,103 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -17396,12 +17396,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2551() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2552.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2552.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2552.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2552.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..9e6c7dc40d509cf424976831382425ab7eceb024
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2552.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2552.java
+@@ -0,0 +1,22 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -17424,12 +17424,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2552() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2553.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2553.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2553.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2553.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f019774923bf08fc0f7dc7cafd5fb66fdd7427f8
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2553.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2553.java
+@@ -0,0 +1,77 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -17507,12 +17507,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2553() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2558.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2558.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2558.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2558.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..137a530c1b979e7257b77f405885aa9f4d376c11
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2558.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2558.java
+@@ -0,0 +1,48 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -17561,12 +17561,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2558() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2568.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2568.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2568.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2568.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..e50fbc38dbf9198c0c652b506e50780eca368bb0
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2568.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2568.java
+@@ -0,0 +1,18 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -17585,12 +17585,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2568() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2671.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2671.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2671.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2671.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..140bfff947e540452f3794eda2f1e2122f8d3f27
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2671.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2671.java
+@@ -0,0 +1,18 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -17609,12 +17609,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2671() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2679.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2679.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2679.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2679.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..7ec79da7e8871d6beca05a25c70d8c6811531faa
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2679.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2679.java
+@@ -0,0 +1,38 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -17653,12 +17653,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2679() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2680.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2680.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2680.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2680.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..87cbe1c717635908a30c57028346a1abeb21e6a6
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2680.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2680.java
+@@ -0,0 +1,27 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -17686,12 +17686,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2680() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2684.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2684.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2684.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2684.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..0c996642f561d2471a506a34f5efe6dae5cd1fb3
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2684.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2684.java
+@@ -0,0 +1,16 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -17708,12 +17708,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2684() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2686.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2686.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2686.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2686.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..1f1685cb0e1427e88dc5c970b0cb58aae0393396
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2686.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2686.java
+@@ -0,0 +1,18 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -17732,12 +17732,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2686() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2688.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2688.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2688.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2688.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..15a7bf7b7ea883d7a3cee9183b92e12838efd690
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2688.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2688.java
+@@ -0,0 +1,22 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -17760,12 +17760,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2688() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2690.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2690.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2690.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2690.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..39ffcec6e78229dd62abfd42c1ac64c3ccccc6dc
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2690.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2690.java
+@@ -0,0 +1,45 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -17811,12 +17811,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2690() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2691.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2691.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2691.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2691.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..bb87bcedfc2ed8d19b266e925beca0f54b50a0ab
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2691.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2691.java
+@@ -0,0 +1,29 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -17846,12 +17846,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2691() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2693.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2693.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2693.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2693.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..a242e8e9a7a7c80c00ec0d64542b3d7dc3103e24
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2693.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2693.java
+@@ -0,0 +1,16 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -17868,12 +17868,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2693() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2696.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2696.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2696.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2696.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..ce568002e54924e001c12271f0bde7183bc23c61
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2696.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2696.java
+@@ -0,0 +1,42 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -17916,12 +17916,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2696() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2700.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2700.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2700.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2700.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..e6a2f29b20aa6d7cd431fc63c2d8ed70dc9a2ab8
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2700.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2700.java
+@@ -0,0 +1,22 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -17944,12 +17944,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2700() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2701.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2701.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2701.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2701.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..dc1604f48a9f15721e709f2e128210085520c15e
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2701.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2701.java
+@@ -0,0 +1,205 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -18155,12 +18155,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2701() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2702.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2702.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2702.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2702.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..cc89ca8a01c2589c807be2a7560bcc6051417379
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2702.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2702.java
+@@ -0,0 +1,35 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -18196,12 +18196,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2702() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2707.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2707.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2707.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2707.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..3c5fc48f39c08249a61199c7f72dddee65fd98af
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2707.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2707.java
+@@ -0,0 +1,22 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -18224,12 +18224,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2707() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2710.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2710.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2710.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2710.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..0967c8c794869a972c1283cab6b3f3cef1d77aec
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2710.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2710.java
+@@ -0,0 +1,21 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -18251,12 +18251,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2710() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2717.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2717.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2717.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2717.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..e0d6b2f6b00e0bfce205efa889de1765ef22793a
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2717.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2717.java
+@@ -0,0 +1,25 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -18282,12 +18282,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2717() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2825.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2825.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2825.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2825.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..cd00c9398791967be6dd10f7183c61902431b27a
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2825.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2825.java
+@@ -0,0 +1,16 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -18304,12 +18304,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2825() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2831.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2831.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2831.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2831.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..1b692e4866d99c89705289ad1f386f467382b1c7
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2831.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2831.java
+@@ -0,0 +1,71 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -18381,12 +18381,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2831() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2832.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2832.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2832.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2832.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..21d1617d222d0b82b1c5222a0ef1a1fa9da02ab8
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2832.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2832.java
+@@ -0,0 +1,929 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -19316,12 +19316,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2832() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2833.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2833.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2833.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2833.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..4c58b3b53d8526cbde6cf1e8c90cabdaceaf2a03
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2833.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2833.java
+@@ -0,0 +1,31 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -19353,12 +19353,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2833() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2838.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2838.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2838.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2838.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..356963228d884a0a74e6d7c9922b4ced627bbfb0
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2838.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2838.java
+@@ -0,0 +1,62 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -19421,12 +19421,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2838() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2841.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2841.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2841.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2841.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..bd8117a101d308e59251f927feb0692a6b22f547
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2841.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2841.java
+@@ -0,0 +1,210 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -19637,12 +19637,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2842.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2842.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2842.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2842.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..03b3d8e2b97a346a45e6c57cb07474baa3bb6096
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2842.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2842.java
+@@ -0,0 +1,78 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -19721,12 +19721,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2842() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2843.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2843.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2843.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2843.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..28a7596b62f8a918f342e2d06eda050a7f2bad0b
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2843.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2843.java
+@@ -0,0 +1,111 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -19838,12 +19838,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2843() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2846.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2846.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2846.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2846.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..e32224267d53d82ba15942141a5cb7a19eb380f2
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2846.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2846.java
+@@ -0,0 +1,23 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -19867,12 +19867,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2846() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2852.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2852.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2852.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2852.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..224ee1d9a3ba68e5a617c2c0846be47feef0bed1
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2852.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2852.java
+@@ -0,0 +1,31 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -19904,12 +19904,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2852() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2967.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2967.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2967.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2967.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..eddfdecffcaf5e4cd7e5c3a79864816ffbaacae1
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2967.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2967.java
+@@ -0,0 +1,58 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -19968,12 +19968,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V2967() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2970.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2970.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V2970.java b/ca/spottedleaf/dataconverter/minecraft/versions/V2970.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..ce083fca547170bb1e1014e868beaf535e940fc3
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2970.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V2970.java
+@@ -0,0 +1,209 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -20183,12 +20183,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3077.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3077.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3077.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3077.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..06fe7dd2580cd8eaad9e0c7de8d0e27287d1b0a9
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3077.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3077.java
+@@ -0,0 +1,40 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -20229,12 +20229,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3077() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3078.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3078.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3078.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3078.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..a0e89f59c75f6d34480f4b8c4f8fa013dddc5d52
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3078.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3078.java
+@@ -0,0 +1,23 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -20258,12 +20258,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3078() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3081.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3081.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3081.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3081.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..fb108cb7f50755b52f537a888ca155aa9db39b3a
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3081.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3081.java
+@@ -0,0 +1,21 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -20285,12 +20285,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3081() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3082.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3082.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3082.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3082.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..79768b25a32a5333f8cb6ec6e8c422478a6891df
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3082.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3082.java
+@@ -0,0 +1,16 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -20307,12 +20307,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3082() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3083.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3083.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3083.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3083.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..e6e70ff4a28446fd8c4c663e0e44791ec4e5ac0a
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3083.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3083.java
+@@ -0,0 +1,23 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -20336,12 +20336,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3083() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3084.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3084.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3084.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3084.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..6a096226995e89285054b4ab35ed3e14ae4da694
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3084.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3084.java
+@@ -0,0 +1,42 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -20384,12 +20384,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3084() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3086.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3086.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3086.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3086.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f06412a417ba12111c9e8f30b747ed3ad6dcbcb6
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3086.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3086.java
+@@ -0,0 +1,54 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -20444,12 +20444,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3086() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3087.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3087.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3087.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3087.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..b296229502491b54f6352ee1f9db0023296b36ec
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3087.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3087.java
+@@ -0,0 +1,24 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -20474,12 +20474,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3087() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3088.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3088.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3088.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3088.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..2752dfd1a7ff896e2ed736846980da5adde6e657
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3088.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3088.java
+@@ -0,0 +1,25 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -20505,12 +20505,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3088() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3090.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3090.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3090.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3090.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..b1cfe038364e12d542d93c3108887173b2e05262
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3090.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3090.java
+@@ -0,0 +1,25 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -20536,12 +20536,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3090() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3093.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3093.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3093.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3093.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..d0677d68b393da9b151c7b2add2fbbd8608e315f
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3093.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3093.java
+@@ -0,0 +1,24 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -20566,12 +20566,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3093() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3094.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3094.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3094.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3094.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..9b9ef34db7dbae8574c4bb3d474592e7991441d5
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3094.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3094.java
+@@ -0,0 +1,44 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -20616,12 +20616,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3094() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3097.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3097.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3097.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3097.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..c70d6dc72d1d913904b71640fe3476c644449ee2
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3097.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3097.java
+@@ -0,0 +1,63 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -20685,12 +20685,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3097() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3108.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3108.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3108.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3108.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..0a1ef2e55a1f9cd6381a2c6fdc04f81ee190ba81
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3108.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3108.java
+@@ -0,0 +1,29 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -20720,12 +20720,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3108() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3201.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3201.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3201.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3201.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..04b84c8466d4fa8f2ad21aae2de44273c05495b6
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3201.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3201.java
+@@ -0,0 +1,35 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -20761,12 +20761,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3201() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3203.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3203.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3203.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3203.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..db9eb946638447445649f4576b3698c0774e44bb
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3203.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3203.java
+@@ -0,0 +1,20 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -20787,12 +20787,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3203() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3204.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3204.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3204.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3204.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..87053c0c1de258770e7630830307ab484915aad8
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3204.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3204.java
+@@ -0,0 +1,16 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -20809,12 +20809,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3204() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3209.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3209.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3209.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3209.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..85270a36c5f75b1c6be49e461b302c3339c95750
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3209.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3209.java
+@@ -0,0 +1,18 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -20833,12 +20833,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3209() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3214.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3214.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3214.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3214.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..0096664e25dca8d690c6154324f97efdb1ada723
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3214.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3214.java
+@@ -0,0 +1,30 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -20869,12 +20869,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3214() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3319.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3319.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3319.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3319.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..0a4a1f690f568b8977e9b2caaf7fba15cb3307a5
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3319.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3319.java
+@@ -0,0 +1,23 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -20898,12 +20898,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3319() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3322.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3322.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3322.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3322.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..53827b9b8999e7b284f3df0f41c98dbbc7d69c1c
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3322.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3322.java
+@@ -0,0 +1,84 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -20988,12 +20988,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3322() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3325.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3325.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3325.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3325.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..2120a5928446f2597fb261d5d6e91c3c9700cb22
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3325.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3325.java
+@@ -0,0 +1,19 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -21013,12 +21013,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3325() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3326.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3326.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3326.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3326.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..6a8d3f6fd18d941e5b0b18fc5208b7fe1f9fd724
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3326.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3326.java
+@@ -0,0 +1,20 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -21039,12 +21039,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3326() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3327.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3327.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3327.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3327.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..7051d4f01b6f43f3d435d21d65b83ba702ffda41
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3327.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3327.java
+@@ -0,0 +1,19 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -21064,12 +21064,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3327() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3328.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3328.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3328.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3328.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..75a3cbc8e6749abd4bceff710d2f7c3ca6df9d70
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3328.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3328.java
+@@ -0,0 +1,15 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -21085,12 +21085,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3328() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3438.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3438.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3438.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3438.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..30c23572a5989f0bd6bff6e424ee59c84bc8d8f1
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3438.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3438.java
+@@ -0,0 +1,47 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -21138,12 +21138,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3438() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3439.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3439.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3439.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3439.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..5c09f745e5200393cf4ecdcb5b42466c2e2d94d9
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3439.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3439.java
+@@ -0,0 +1,96 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -21240,12 +21240,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3439() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3440.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3440.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3440.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3440.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..914df7885582a1fde398e755dbfaf00e19ce16b3
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3440.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3440.java
+@@ -0,0 +1,29 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -21275,12 +21275,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3440() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3441.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3441.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3441.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3441.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..2cf41561b4a229ba4d60540f85ba0a8946bb9753
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3441.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3441.java
+@@ -0,0 +1,17 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -21298,12 +21298,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3441() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3447.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3447.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3447.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3447.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..5db4a5222bf80f01c128d97ec849b89003837beb
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3447.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3447.java
+@@ -0,0 +1,49 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -21353,12 +21353,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3447() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3448.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3448.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3448.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3448.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..6f447d59677be4630b53e948419a0ae2a3414315
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3448.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3448.java
+@@ -0,0 +1,28 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -21387,12 +21387,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3448() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3450.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3450.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3450.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3450.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..9e7f34a40280a7704c4a4d1c03a7dda8e030aff5
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3450.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3450.java
+@@ -0,0 +1,23 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -21416,12 +21416,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3450() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3451.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3451.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3451.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3451.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..ea97d596bfb4b9c6b9b7d0534604e042a775ea78
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3451.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3451.java
+@@ -0,0 +1,38 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -21460,12 +21460,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3451() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3459.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3459.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3459.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3459.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..e8a1fcd9e67b151a360e11089289154a14dde27c
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3459.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3459.java
+@@ -0,0 +1,38 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -21504,12 +21504,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3459() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3564.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3564.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3564.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3564.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..5c64ec5b9bdcc279bc1b86e6bb0b877003b213cb
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3564.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3564.java
+@@ -0,0 +1,93 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -21603,12 +21603,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3564() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3565.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3565.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3565.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3565.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..685021e87236c5b7ce5ee0b5422d7bf856ea6652
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3565.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3565.java
+@@ -0,0 +1,32 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -21641,12 +21641,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3565() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3566.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3566.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3566.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3566.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..a5b91dc5eb4e9206cbb4489ce14195d7517ef4e0
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3566.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3566.java
+@@ -0,0 +1,58 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -21705,12 +21705,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3566() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3568.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3568.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3568.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3568.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..39b4bd2d0bb36ef242467e89010ef5fc1490de9b
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3568.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3568.java
+@@ -0,0 +1,245 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -21956,12 +21956,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3568() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3682.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3682.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3682.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3682.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f9e3eb71b268f7bc1940f0199fddfa1ac27401b8
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3682.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3682.java
+@@ -0,0 +1,16 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -21978,12 +21978,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3682() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3683.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3683.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3683.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3683.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..855a95b4ef686fa9d2cefdef5664290528e4dc60
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3683.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3683.java
+@@ -0,0 +1,33 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -22017,12 +22017,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3683() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3685.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3685.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3685.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3685.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..603467a7a2b6da93181a0a32eedb30d1614b0069
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3685.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3685.java
+@@ -0,0 +1,64 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -22087,12 +22087,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3685() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3689.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3689.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3689.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3689.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..ccda8d0f7c0a284fd4f91622dc33b832b4a95c45
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3689.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3689.java
+@@ -0,0 +1,37 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -22130,12 +22130,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3689() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3692.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3692.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3692.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3692.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..1fc62f9cadb990790420376d5c80b14775b71a48
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3692.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3692.java
+@@ -0,0 +1,25 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -22161,12 +22161,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3692() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3799.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3799.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3799.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3799.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..0c34d445825e8b49249af852b1f2f09c8b5a2574
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3799.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3799.java
+@@ -0,0 +1,14 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -22181,12 +22181,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3799() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3800.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3800.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3800.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3800.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..a40397feb5962bd5f4a44cc85bb359f5f99ff03a
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3800.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3800.java
+@@ -0,0 +1,23 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -22210,12 +22210,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3800() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3803.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3803.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3803.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3803.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..7ff5e2f1a386d75b6d0d6fc3160f2241bf74b262
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3803.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3803.java
+@@ -0,0 +1,24 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -22240,12 +22240,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3803() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3807.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3807.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3807.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3807.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..a76916cdb7cf91b8ba5461524472b3e455f02885
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3807.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3807.java
+@@ -0,0 +1,72 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -22318,12 +22318,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3807() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3808.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3808.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3808.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3808.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..78a10f89218eb0edf121f88978b4fe13e1b1bf44
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3808.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3808.java
+@@ -0,0 +1,82 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -22406,12 +22406,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3808() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3809.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3809.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3809.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3809.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..c45a1a77adbb5dc5ba8c3dae0bb480450520c731
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3809.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3809.java
+@@ -0,0 +1,41 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -22453,12 +22453,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3809() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3812.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3812.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3812.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3812.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f0c0748b003648e5fe06d0b6dd1b21948ac0e54a
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3812.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3812.java
+@@ -0,0 +1,48 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -22507,12 +22507,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3812() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3813.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3813.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3813.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3813.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..920d7734d883d74e8334102b22cabce24a79db7e
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3813.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3813.java
+@@ -0,0 +1,131 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -22644,12 +22644,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    
 +    private static record RenamePair(String from, String to) {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3814.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3814.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3814.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3814.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..c4cc52620afb728533efe988bf2066ffc947f2d6
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3814.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3814.java
+@@ -0,0 +1,21 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -22671,12 +22671,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3814() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3816.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3816.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3816.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3816.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f50b81d931a1908d405bb72e0679983a742d5223
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3816.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3816.java
+@@ -0,0 +1,14 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -22691,12 +22691,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3816() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3818.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3818.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3818.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3818.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..a1a4659538c8f678319ddc7d61b400051c8a4953
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3818.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3818.java
+@@ -0,0 +1,340 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -23037,12 +23037,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3818() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3820.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3820.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3820.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3820.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..c45dda60ed8da6802181f7f169a5b97f591b00ee
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3820.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3820.java
+@@ -0,0 +1,78 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -23121,12 +23121,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3820() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3825.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3825.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3825.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3825.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..26e27331223bc5671db49bb730a754597815b8cc
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3825.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3825.java
+@@ -0,0 +1,153 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -23280,12 +23280,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3825() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3828.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3828.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3828.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3828.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f752bb2fca2e4cd438c0540460912d4bc2c6f25e
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3828.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3828.java
+@@ -0,0 +1,37 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -23323,12 +23323,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3828() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3833.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3833.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3833.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3833.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f097881401855137f5d4ac25ba1468e635a702b5
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3833.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3833.java
+@@ -0,0 +1,36 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -23365,12 +23365,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3833() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3938.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3938.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3938.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3938.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..2a6d144c2f074403bde8a62377ca6986c0c12a84
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3938.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3938.java
+@@ -0,0 +1,24 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -23395,12 +23395,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3938() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3939.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3939.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3939.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3939.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..632c8008484e844d962405c6ef8fb9f09fc6c977
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3939.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3939.java
+@@ -0,0 +1,22 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -23423,12 +23423,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3939() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3943.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3943.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3943.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3943.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..1cd426cf78d62d428406caa319cc5c8649e7f36c
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3943.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3943.java
+@@ -0,0 +1,34 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -23463,12 +23463,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3943() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3945.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3945.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V3945.java b/ca/spottedleaf/dataconverter/minecraft/versions/V3945.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..74c13c46390e4533a9eb2c8ae5d9846db55efa94
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3945.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V3945.java
+@@ -0,0 +1,244 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -23713,12 +23713,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V3945() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4054.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4054.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V4054.java b/ca/spottedleaf/dataconverter/minecraft/versions/V4054.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..d65e05285ef238aa8c6d660aa42fcd69e07d9430
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4054.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V4054.java
+@@ -0,0 +1,46 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -23765,12 +23765,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V4054() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4055.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4055.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V4055.java b/ca/spottedleaf/dataconverter/minecraft/versions/V4055.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..45b141a651d954554fcca68f36c0b3344328d902
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4055.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V4055.java
+@@ -0,0 +1,41 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -23812,12 +23812,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V4055() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4057.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4057.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V4057.java b/ca/spottedleaf/dataconverter/minecraft/versions/V4057.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..b0949ac2035662ba1c943b4bfab2f19e985e6864
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4057.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V4057.java
+@@ -0,0 +1,33 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -23851,12 +23851,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V4057() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4059.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4059.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V4059.java b/ca/spottedleaf/dataconverter/minecraft/versions/V4059.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..0047a20dab2ffd6b39a8bcb8ed9f3878f20e31c2
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4059.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V4059.java
+@@ -0,0 +1,128 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -23985,12 +23985,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V4059() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4061.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4061.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V4061.java b/ca/spottedleaf/dataconverter/minecraft/versions/V4061.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..630263a61b5db4207c1a5051e3e2249ab3dd3957
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4061.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V4061.java
+@@ -0,0 +1,112 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -24103,12 +24103,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V4061() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4064.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4064.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V4064.java b/ca/spottedleaf/dataconverter/minecraft/versions/V4064.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..85eb8f37f89faed8b366c1d1c850b028bfcb2164
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4064.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V4064.java
+@@ -0,0 +1,36 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -24145,12 +24145,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V4064() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4067.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4067.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V4067.java b/ca/spottedleaf/dataconverter/minecraft/versions/V4067.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..855c5a99951996ffe4eabb24a69321043cce41d7
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4067.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V4067.java
+@@ -0,0 +1,143 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -24294,12 +24294,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V4067() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4068.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4068.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V4068.java b/ca/spottedleaf/dataconverter/minecraft/versions/V4068.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..817682bb5830242eca25cc1939ed2bda9f1c460b
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4068.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V4068.java
+@@ -0,0 +1,65 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -24365,12 +24365,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V4068() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4070.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4070.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V4070.java b/ca/spottedleaf/dataconverter/minecraft/versions/V4070.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..b85673d792d4b1c317d312ba607a0d30c2f57ea9
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4070.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V4070.java
+@@ -0,0 +1,22 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -24393,12 +24393,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V4070() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4071.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4071.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V4071.java b/ca/spottedleaf/dataconverter/minecraft/versions/V4071.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..3b0855353f40e8ce54b86305152aa35af9154c6f
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4071.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V4071.java
+@@ -0,0 +1,21 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -24420,12 +24420,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V4071() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4081.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4081.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V4081.java b/ca/spottedleaf/dataconverter/minecraft/versions/V4081.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..22eae4d39c3887ef4991fd21856c32c43c543f88
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4081.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V4081.java
+@@ -0,0 +1,27 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -24453,12 +24453,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V4081() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4173.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4173.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V4173.java b/ca/spottedleaf/dataconverter/minecraft/versions/V4173.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..4e89460386bbc75b8380835b5df3ca821d6a9c82
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4173.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V4173.java
+@@ -0,0 +1,24 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -24483,12 +24483,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V4173() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4175.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4175.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V4175.java b/ca/spottedleaf/dataconverter/minecraft/versions/V4175.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..c4c6e75b8aae973fc4e4ac9f6e03ecbb5a38ef99
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4175.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V4175.java
+@@ -0,0 +1,40 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -24529,12 +24529,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V4175() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4176.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4176.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V4176.java b/ca/spottedleaf/dataconverter/minecraft/versions/V4176.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..c1a74d545333224d9e8c79667bf42b2617fbe346
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4176.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V4176.java
+@@ -0,0 +1,44 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -24579,12 +24579,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V4176() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4180.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4180.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V4180.java b/ca/spottedleaf/dataconverter/minecraft/versions/V4180.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..c8eb7ba000310d1165c63fb9eef3787872f299bb
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4180.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V4180.java
+@@ -0,0 +1,22 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -24607,12 +24607,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V4180() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4181.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4181.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V4181.java b/ca/spottedleaf/dataconverter/minecraft/versions/V4181.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..9119204ef25d78b04c5afc58965df56725ac7079
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4181.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V4181.java
+@@ -0,0 +1,36 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -24649,12 +24649,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V4181() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4185.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4185.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V4185.java b/ca/spottedleaf/dataconverter/minecraft/versions/V4185.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..8b4041d3d3a4a001bf06eaedbddad1b297122b12
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4185.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V4185.java
+@@ -0,0 +1,17 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -24672,12 +24672,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V4185() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4187.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4187.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V4187.java b/ca/spottedleaf/dataconverter/minecraft/versions/V4187.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..7d09c4218d0db8119d1681bf95900be830557fa3
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V4187.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V4187.java
+@@ -0,0 +1,69 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -24747,12 +24747,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V4187() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V501.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V501.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V501.java b/ca/spottedleaf/dataconverter/minecraft/versions/V501.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..a7a4d6446b7765ac485af82df660aafab05955bf
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V501.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V501.java
+@@ -0,0 +1,18 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -24771,12 +24771,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V501() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V502.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V502.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V502.java b/ca/spottedleaf/dataconverter/minecraft/versions/V502.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..7f88b435378305a3a66e1e54b85afd9b019513ee
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V502.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V502.java
+@@ -0,0 +1,45 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -24822,12 +24822,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V502() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V505.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V505.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V505.java b/ca/spottedleaf/dataconverter/minecraft/versions/V505.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..3faf2c3265600141003355771f38a7879e0f769a
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V505.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V505.java
+@@ -0,0 +1,23 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -24851,12 +24851,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V505() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V700.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V700.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V700.java b/ca/spottedleaf/dataconverter/minecraft/versions/V700.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..3b65108c6a1ac469bb8f81a933b6475f3ea9f63f
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V700.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V700.java
+@@ -0,0 +1,32 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -24889,12 +24889,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V700() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V701.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V701.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V701.java b/ca/spottedleaf/dataconverter/minecraft/versions/V701.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..55f00e218f04e1e095ccc7d62282d87d7eb8f8c7
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V701.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V701.java
+@@ -0,0 +1,41 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -24936,12 +24936,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V701() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V702.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V702.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V702.java b/ca/spottedleaf/dataconverter/minecraft/versions/V702.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..c0d74b4822be60c637f26b2ef1e172fdf9e89d01
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V702.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V702.java
+@@ -0,0 +1,56 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -24998,12 +24998,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V702() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V703.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V703.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V703.java b/ca/spottedleaf/dataconverter/minecraft/versions/V703.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..cc593df4a09d6cb93196d8cfb34ebac43e61ebbe
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V703.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V703.java
+@@ -0,0 +1,67 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -25071,12 +25071,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V703() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V704.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V704.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V704.java b/ca/spottedleaf/dataconverter/minecraft/versions/V704.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..e6777f58d7d4722cabd30fa495cee054f58b3e48
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V704.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V704.java
+@@ -0,0 +1,440 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -25517,12 +25517,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V704() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V705.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V705.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V705.java b/ca/spottedleaf/dataconverter/minecraft/versions/V705.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..e0efac6a303d4c9623e03acdf07f89c2cacc9f04
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V705.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V705.java
+@@ -0,0 +1,221 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -25744,12 +25744,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V705() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V804.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V804.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V804.java b/ca/spottedleaf/dataconverter/minecraft/versions/V804.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..81a2006d5e2059df0979c6380a16255767bcd89a
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V804.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V804.java
+@@ -0,0 +1,59 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -25809,12 +25809,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V804() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V806.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V806.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V806.java b/ca/spottedleaf/dataconverter/minecraft/versions/V806.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f4ebe856d03d9837214e9a1c93f1b1e79aa7bb08
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V806.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V806.java
+@@ -0,0 +1,39 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -25854,12 +25854,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V806() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V808.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V808.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V808.java b/ca/spottedleaf/dataconverter/minecraft/versions/V808.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..c6b6038255e16bd15873bb7fe596b721fcec365e
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V808.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V808.java
+@@ -0,0 +1,29 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -25889,12 +25889,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V808() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V813.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V813.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V813.java b/ca/spottedleaf/dataconverter/minecraft/versions/V813.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..68810919e168f36de160033aa659060487d94bd8
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V813.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V813.java
+@@ -0,0 +1,64 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -25959,12 +25959,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V813() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V816.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V816.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V816.java b/ca/spottedleaf/dataconverter/minecraft/versions/V816.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..dc9fba23654262b1489e4f8056a7f4b222ab1179
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V816.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V816.java
+@@ -0,0 +1,27 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.converters.DataConverter;
@@ -25992,12 +25992,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V816() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V820.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V820.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V820.java b/ca/spottedleaf/dataconverter/minecraft/versions/V820.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..0d59cb380e625bb2658216d4a6cb8faebdd147c5
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V820.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V820.java
+@@ -0,0 +1,21 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -26019,12 +26019,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V820() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V99.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V99.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V99.java b/ca/spottedleaf/dataconverter/minecraft/versions/V99.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f0e26849e28ce7ce362927ec81b281e51bd1e591
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V99.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V99.java
+@@ -0,0 +1,363 @@
 +package ca.spottedleaf.dataconverter.minecraft.versions;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCVersions;
@@ -26388,12 +26388,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private V99() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/block_name/DataWalkerBlockNames.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/block_name/DataWalkerBlockNames.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/walkers/block_name/DataWalkerBlockNames.java b/ca/spottedleaf/dataconverter/minecraft/walkers/block_name/DataWalkerBlockNames.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..930e014858ef635ebe25f7f92dc81ba0eaac50a8
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/block_name/DataWalkerBlockNames.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/walkers/block_name/DataWalkerBlockNames.java
+@@ -0,0 +1,11 @@
 +package ca.spottedleaf.dataconverter.minecraft.walkers.block_name;
 +
 +import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry;
@@ -26405,12 +26405,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        super(MCTypeRegistry.BLOCK_NAME, paths);
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/game_event/GameEventListenerWalker.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/game_event/GameEventListenerWalker.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/walkers/game_event/GameEventListenerWalker.java b/ca/spottedleaf/dataconverter/minecraft/walkers/game_event/GameEventListenerWalker.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..64fc063748d4839d787a773d2c7258dcffc6bc21
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/game_event/GameEventListenerWalker.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/walkers/game_event/GameEventListenerWalker.java
+@@ -0,0 +1,26 @@
 +package ca.spottedleaf.dataconverter.minecraft.walkers.game_event;
 +
 +import ca.spottedleaf.dataconverter.converters.datatypes.DataWalker;
@@ -26437,12 +26437,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return null;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/generic/DataWalkerListPaths.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/generic/DataWalkerListPaths.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/walkers/generic/DataWalkerListPaths.java b/ca/spottedleaf/dataconverter/minecraft/walkers/generic/DataWalkerListPaths.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..20c8efdb746c9d3b9d87bf991dc44e11e1ea697c
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/generic/DataWalkerListPaths.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/walkers/generic/DataWalkerListPaths.java
+@@ -0,0 +1,38 @@
 +package ca.spottedleaf.dataconverter.minecraft.walkers.generic;
 +
 +import ca.spottedleaf.dataconverter.converters.datatypes.DataType;
@@ -26481,12 +26481,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return null;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/generic/DataWalkerTypePaths.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/generic/DataWalkerTypePaths.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/walkers/generic/DataWalkerTypePaths.java b/ca/spottedleaf/dataconverter/minecraft/walkers/generic/DataWalkerTypePaths.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..4205546d7b2c4a07d23a017004989875b7beb3c6
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/generic/DataWalkerTypePaths.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/walkers/generic/DataWalkerTypePaths.java
+@@ -0,0 +1,34 @@
 +package ca.spottedleaf.dataconverter.minecraft.walkers.generic;
 +
 +import ca.spottedleaf.dataconverter.converters.datatypes.DataType;
@@ -26521,12 +26521,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return null;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/generic/WalkerUtils.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/generic/WalkerUtils.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/walkers/generic/WalkerUtils.java b/ca/spottedleaf/dataconverter/minecraft/walkers/generic/WalkerUtils.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..4feedd9e48c3a85bd75b9c0a3b09c91fa9532a93
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/generic/WalkerUtils.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/walkers/generic/WalkerUtils.java
+@@ -0,0 +1,183 @@
 +package ca.spottedleaf.dataconverter.minecraft.walkers.generic;
 +
 +import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper;
@@ -26710,12 +26710,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/item_name/DataWalkerItemNames.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/item_name/DataWalkerItemNames.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/walkers/item_name/DataWalkerItemNames.java b/ca/spottedleaf/dataconverter/minecraft/walkers/item_name/DataWalkerItemNames.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..14e291efd864d97dcf83db01c09b9daaae1949bd
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/item_name/DataWalkerItemNames.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/walkers/item_name/DataWalkerItemNames.java
+@@ -0,0 +1,11 @@
 +package ca.spottedleaf.dataconverter.minecraft.walkers.item_name;
 +
 +import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry;
@@ -26727,12 +26727,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        super(MCTypeRegistry.ITEM_NAME, paths);
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/itemstack/DataWalkerItemLists.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/itemstack/DataWalkerItemLists.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/walkers/itemstack/DataWalkerItemLists.java b/ca/spottedleaf/dataconverter/minecraft/walkers/itemstack/DataWalkerItemLists.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..5b4402c3cc4e68e9c591e8bbb4a2542d8e2214d4
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/itemstack/DataWalkerItemLists.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/walkers/itemstack/DataWalkerItemLists.java
+@@ -0,0 +1,12 @@
 +package ca.spottedleaf.dataconverter.minecraft.walkers.itemstack;
 +
 +import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry;
@@ -26745,12 +26745,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        super(MCTypeRegistry.ITEM_STACK, paths);
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/itemstack/DataWalkerItems.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/itemstack/DataWalkerItems.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/walkers/itemstack/DataWalkerItems.java b/ca/spottedleaf/dataconverter/minecraft/walkers/itemstack/DataWalkerItems.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..04770e8378ac8784895cdfe400a47b0b601c2187
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/itemstack/DataWalkerItems.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/walkers/itemstack/DataWalkerItems.java
+@@ -0,0 +1,12 @@
 +package ca.spottedleaf.dataconverter.minecraft.walkers.itemstack;
 +
 +import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry;
@@ -26763,12 +26763,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        super(MCTypeRegistry.ITEM_STACK, paths);
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/tile_entity/DataWalkerTileEntities.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/tile_entity/DataWalkerTileEntities.java
+diff --git a/ca/spottedleaf/dataconverter/minecraft/walkers/tile_entity/DataWalkerTileEntities.java b/ca/spottedleaf/dataconverter/minecraft/walkers/tile_entity/DataWalkerTileEntities.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..d9cc21bf41cb4b377752b684f8e59818cd620103
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/tile_entity/DataWalkerTileEntities.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/minecraft/walkers/tile_entity/DataWalkerTileEntities.java
+@@ -0,0 +1,12 @@
 +package ca.spottedleaf.dataconverter.minecraft.walkers.tile_entity;
 +
 +import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry;
@@ -26781,12 +26781,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        super(MCTypeRegistry.TILE_ENTITY, paths);
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/ListType.java b/src/main/java/ca/spottedleaf/dataconverter/types/ListType.java
+diff --git a/ca/spottedleaf/dataconverter/types/ListType.java b/ca/spottedleaf/dataconverter/types/ListType.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..19f7e95f754e8385bbe60fd2fb7fc95b6a4ebd7c
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/types/ListType.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/types/ListType.java
+@@ -0,0 +1,272 @@
 +package ca.spottedleaf.dataconverter.types;
 +
 +public interface ListType {
@@ -27059,12 +27059,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public void addString(final int index, final String string);
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/MapType.java b/src/main/java/ca/spottedleaf/dataconverter/types/MapType.java
+diff --git a/ca/spottedleaf/dataconverter/types/MapType.java b/ca/spottedleaf/dataconverter/types/MapType.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..b8dad91ad3b8692448134c4f12cf9853dc06fccc
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/types/MapType.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/types/MapType.java
+@@ -0,0 +1,223 @@
 +package ca.spottedleaf.dataconverter.types;
 +
 +import java.util.Set;
@@ -27288,12 +27288,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        throw new IllegalArgumentException("Object " + value + " is not a valid type!");
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/ObjectType.java b/src/main/java/ca/spottedleaf/dataconverter/types/ObjectType.java
+diff --git a/ca/spottedleaf/dataconverter/types/ObjectType.java b/ca/spottedleaf/dataconverter/types/ObjectType.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..1aab91233ddb98c3af5d424bac120891f1ee16c7
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/types/ObjectType.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/types/ObjectType.java
+@@ -0,0 +1,72 @@
 +package ca.spottedleaf.dataconverter.types;
 +
 +public enum ObjectType {
@@ -27366,12 +27366,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return null;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/TypeUtil.java b/src/main/java/ca/spottedleaf/dataconverter/types/TypeUtil.java
+diff --git a/ca/spottedleaf/dataconverter/types/TypeUtil.java b/ca/spottedleaf/dataconverter/types/TypeUtil.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..156a2ea46f8f88a02e88b50d7bb7be82ecd41919
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/types/TypeUtil.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/types/TypeUtil.java
+@@ -0,0 +1,9 @@
 +package ca.spottedleaf.dataconverter.types;
 +
 +public interface TypeUtil {
@@ -27381,12 +27381,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public <K> MapType<K> createEmptyMap();
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/Types.java b/src/main/java/ca/spottedleaf/dataconverter/types/Types.java
+diff --git a/ca/spottedleaf/dataconverter/types/Types.java b/ca/spottedleaf/dataconverter/types/Types.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..2ab9e3b579f20c9a189518496c522155630a36c4
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/types/Types.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/types/Types.java
+@@ -0,0 +1,15 @@
 +package ca.spottedleaf.dataconverter.types;
 +
 +import ca.spottedleaf.dataconverter.types.json.JsonTypeCompressedUtil;
@@ -27402,12 +27402,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    // why does this exist
 +    public static final TypeUtil JSON_COMPRESSED = new JsonTypeCompressedUtil();
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonListType.java b/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonListType.java
+diff --git a/ca/spottedleaf/dataconverter/types/json/JsonListType.java b/ca/spottedleaf/dataconverter/types/json/JsonListType.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f6f57cb3a215876976b5eecae810b8b20925f2e2
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonListType.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/types/json/JsonListType.java
+@@ -0,0 +1,415 @@
 +package ca.spottedleaf.dataconverter.types.json;
 +
 +import ca.spottedleaf.dataconverter.types.ListType;
@@ -27823,12 +27823,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        throw new UnsupportedOperationException();
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonMapType.java b/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonMapType.java
+diff --git a/ca/spottedleaf/dataconverter/types/json/JsonMapType.java b/ca/spottedleaf/dataconverter/types/json/JsonMapType.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..b6ad4623894454675f4be52ecdb4655d6623b385
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonMapType.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/types/json/JsonMapType.java
+@@ -0,0 +1,474 @@
 +package ca.spottedleaf.dataconverter.types.json;
 +
 +import ca.spottedleaf.dataconverter.types.ListType;
@@ -28303,12 +28303,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        this.map.addProperty(key, val);
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonTypeCompressedUtil.java b/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonTypeCompressedUtil.java
+diff --git a/ca/spottedleaf/dataconverter/types/json/JsonTypeCompressedUtil.java b/ca/spottedleaf/dataconverter/types/json/JsonTypeCompressedUtil.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..9c3093b66b847b5248bde923243fce78842bf67f
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonTypeCompressedUtil.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/types/json/JsonTypeCompressedUtil.java
+@@ -0,0 +1,18 @@
 +package ca.spottedleaf.dataconverter.types.json;
 +
 +import ca.spottedleaf.dataconverter.types.ListType;
@@ -28327,12 +28327,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return new JsonMapType(true);
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonTypeUtil.java b/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonTypeUtil.java
+diff --git a/ca/spottedleaf/dataconverter/types/json/JsonTypeUtil.java b/ca/spottedleaf/dataconverter/types/json/JsonTypeUtil.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..9410ae68395a09c7710bdbb2ccc6acf6633cad23
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonTypeUtil.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/types/json/JsonTypeUtil.java
+@@ -0,0 +1,81 @@
 +package ca.spottedleaf.dataconverter.types.json;
 +
 +import ca.spottedleaf.dataconverter.types.ListType;
@@ -28414,12 +28414,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return ret;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/nbt/NBTListType.java b/src/main/java/ca/spottedleaf/dataconverter/types/nbt/NBTListType.java
+diff --git a/ca/spottedleaf/dataconverter/types/nbt/NBTListType.java b/ca/spottedleaf/dataconverter/types/nbt/NBTListType.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..bf4e9ea17222cfa8f7cee9e46775302c9c2e6328
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/types/nbt/NBTListType.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/types/nbt/NBTListType.java
+@@ -0,0 +1,440 @@
 +package ca.spottedleaf.dataconverter.types.nbt;
 +
 +import ca.spottedleaf.dataconverter.types.ObjectType;
@@ -28860,12 +28860,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        this.list.add(index, StringTag.valueOf(string));
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/nbt/NBTMapType.java b/src/main/java/ca/spottedleaf/dataconverter/types/nbt/NBTMapType.java
+diff --git a/ca/spottedleaf/dataconverter/types/nbt/NBTMapType.java b/ca/spottedleaf/dataconverter/types/nbt/NBTMapType.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..01b6796c6ac168a82f41cf4fddbd32a1c8a86484
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/types/nbt/NBTMapType.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/types/nbt/NBTMapType.java
+@@ -0,0 +1,454 @@
 +package ca.spottedleaf.dataconverter.types.nbt;
 +
 +import ca.spottedleaf.dataconverter.types.ListType;
@@ -29320,12 +29320,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        this.map.putString(key, val);
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/nbt/NBTTypeUtil.java b/src/main/java/ca/spottedleaf/dataconverter/types/nbt/NBTTypeUtil.java
+diff --git a/ca/spottedleaf/dataconverter/types/nbt/NBTTypeUtil.java b/ca/spottedleaf/dataconverter/types/nbt/NBTTypeUtil.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..62c0f4073aff301bf5b3187e0d4446fd8d0ac475
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/types/nbt/NBTTypeUtil.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/types/nbt/NBTTypeUtil.java
+@@ -0,0 +1,18 @@
 +package ca.spottedleaf.dataconverter.types.nbt;
 +
 +import ca.spottedleaf.dataconverter.types.ListType;
@@ -29344,12 +29344,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return new NBTMapType();
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/util/CommandArgumentUpgrader.java b/src/main/java/ca/spottedleaf/dataconverter/util/CommandArgumentUpgrader.java
+diff --git a/ca/spottedleaf/dataconverter/util/CommandArgumentUpgrader.java b/ca/spottedleaf/dataconverter/util/CommandArgumentUpgrader.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..40da70d5cf584a9730f9fe81c355cf8513fba475
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/util/CommandArgumentUpgrader.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/util/CommandArgumentUpgrader.java
+@@ -0,0 +1,592 @@
 +package ca.spottedleaf.dataconverter.util;
 +
 +import ca.spottedleaf.dataconverter.minecraft.MCDataConverter;
@@ -29942,12 +29942,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        );
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/util/Int2IntArraySortedMap.java b/src/main/java/ca/spottedleaf/dataconverter/util/Int2IntArraySortedMap.java
+diff --git a/ca/spottedleaf/dataconverter/util/Int2IntArraySortedMap.java b/ca/spottedleaf/dataconverter/util/Int2IntArraySortedMap.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..6596de3d9ebae583c252aa061f0cfdf8778ea1a5
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/util/Int2IntArraySortedMap.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/util/Int2IntArraySortedMap.java
+@@ -0,0 +1,77 @@
 +package ca.spottedleaf.dataconverter.util;
 +
 +import it.unimi.dsi.fastutil.ints.Int2IntFunction;
@@ -30025,12 +30025,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return this.val[index];
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/util/Int2ObjectArraySortedMap.java b/src/main/java/ca/spottedleaf/dataconverter/util/Int2ObjectArraySortedMap.java
+diff --git a/ca/spottedleaf/dataconverter/util/Int2ObjectArraySortedMap.java b/ca/spottedleaf/dataconverter/util/Int2ObjectArraySortedMap.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..de9d632489609136c712a9adaee941fd38fad440
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/util/Int2ObjectArraySortedMap.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/util/Int2ObjectArraySortedMap.java
+@@ -0,0 +1,74 @@
 +package ca.spottedleaf.dataconverter.util;
 +
 +import java.util.Arrays;
@@ -30105,12 +30105,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return this.val[index];
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/util/IntegerUtil.java b/src/main/java/ca/spottedleaf/dataconverter/util/IntegerUtil.java
+diff --git a/ca/spottedleaf/dataconverter/util/IntegerUtil.java b/ca/spottedleaf/dataconverter/util/IntegerUtil.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..4bbf38c812feeb30d2aa5f3fcf482bfcbed79d05
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/util/IntegerUtil.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/util/IntegerUtil.java
+@@ -0,0 +1,239 @@
 +package ca.spottedleaf.dataconverter.util;
 +
 +public final class IntegerUtil {
@@ -30350,12 +30350,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        throw new RuntimeException();
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/util/Long2IntArraySortedMap.java b/src/main/java/ca/spottedleaf/dataconverter/util/Long2IntArraySortedMap.java
+diff --git a/ca/spottedleaf/dataconverter/util/Long2IntArraySortedMap.java b/ca/spottedleaf/dataconverter/util/Long2IntArraySortedMap.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..94705bb141b550589faa9a0408402d8636c61907
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/util/Long2IntArraySortedMap.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/util/Long2IntArraySortedMap.java
+@@ -0,0 +1,76 @@
 +package ca.spottedleaf.dataconverter.util;
 +
 +import it.unimi.dsi.fastutil.longs.Long2IntFunction;
@@ -30432,12 +30432,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return this.val[index];
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/util/Long2ObjectArraySortedMap.java b/src/main/java/ca/spottedleaf/dataconverter/util/Long2ObjectArraySortedMap.java
+diff --git a/ca/spottedleaf/dataconverter/util/Long2ObjectArraySortedMap.java b/ca/spottedleaf/dataconverter/util/Long2ObjectArraySortedMap.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..6f634c8825589a23f46ad7b54354475c9a95bd1b
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/util/Long2ObjectArraySortedMap.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/util/Long2ObjectArraySortedMap.java
+@@ -0,0 +1,76 @@
 +package ca.spottedleaf.dataconverter.util;
 +
 +import java.util.Arrays;
@@ -30514,12 +30514,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return this.val[index];
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/dataconverter/util/NamespaceUtil.java b/src/main/java/ca/spottedleaf/dataconverter/util/NamespaceUtil.java
+diff --git a/ca/spottedleaf/dataconverter/util/NamespaceUtil.java b/ca/spottedleaf/dataconverter/util/NamespaceUtil.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..5a6536377c9c1e1753e930ff2a6bb98ea57055c7
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/dataconverter/util/NamespaceUtil.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/dataconverter/util/NamespaceUtil.java
+@@ -0,0 +1,39 @@
 +package ca.spottedleaf.dataconverter.util;
 +
 +import ca.spottedleaf.dataconverter.types.MapType;
@@ -30559,11 +30559,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return correct.equals(value) ? null : correct;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java b/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java
-@@ -0,0 +0,0 @@ public final class PaperHooks implements PlatformHooks {
+diff --git a/ca/spottedleaf/moonrise/paper/PaperHooks.java b/ca/spottedleaf/moonrise/paper/PaperHooks.java
+index 729eb5d052465e4093e9d8c5d4fe8463b2efaca6..5577287398db2bb9d21f14409ef580b8ab9ea018 100644
+--- a/ca/spottedleaf/moonrise/paper/PaperHooks.java
++++ b/ca/spottedleaf/moonrise/paper/PaperHooks.java
+@@ -204,6 +204,43 @@ public final class PaperHooks extends BaseChunkSystemHooks implements PlatformHo
      @Override
      public CompoundTag convertNBT(final DSL.TypeReference type, final DataFixer dataFixer, final CompoundTag nbt,
                                    final int fromVersion, final int toVersion) {
@@ -30607,68 +30607,68 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          return (CompoundTag)dataFixer.update(
              type, new Dynamic<>(NbtOps.INSTANCE, nbt), fromVersion, toVersion
          ).getValue();
-diff --git a/src/main/java/net/minecraft/data/structures/StructureUpdater.java b/src/main/java/net/minecraft/data/structures/StructureUpdater.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/data/structures/StructureUpdater.java
-+++ b/src/main/java/net/minecraft/data/structures/StructureUpdater.java
-@@ -0,0 +0,0 @@ public class StructureUpdater implements SnbtToNbt.Filter {
-             LOGGER.warn("SNBT Too old, do not forget to update: {} < {}: {}", i, 4173, name);
+diff --git a/net/minecraft/data/structures/StructureUpdater.java b/net/minecraft/data/structures/StructureUpdater.java
+index 1110ca4075a1bbaa46b66686435dab91b275c945..c2218630c3074c8b3f82364e37503b12bd0a0d74 100644
+--- a/net/minecraft/data/structures/StructureUpdater.java
++++ b/net/minecraft/data/structures/StructureUpdater.java
+@@ -27,7 +27,7 @@ public class StructureUpdater implements SnbtToNbt.Filter {
+             LOGGER.warn("SNBT Too old, do not forget to update: {} < {}: {}", dataVersion, 4173, structureLocationPath);
          }
  
--        CompoundTag compoundTag = DataFixTypes.STRUCTURE.updateToCurrentVersion(DataFixers.getDataFixer(), nbt, i);
-+        CompoundTag compoundTag = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.STRUCTURE, nbt, i, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion()); // Paper
+-        CompoundTag compoundTag = DataFixTypes.STRUCTURE.updateToCurrentVersion(DataFixers.getDataFixer(), tag, dataVersion);
++        CompoundTag compoundTag = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.STRUCTURE, tag, dataVersion, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion()); // Paper
          structureTemplate.load(BuiltInRegistries.BLOCK, compoundTag);
          return structureTemplate.save(new CompoundTag());
      }
-diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/MinecraftServer.java
-+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
-     private final Set<String> pluginsBlockingSleep = new java.util.HashSet<>(); // Paper - API to allow/disallow tick sleeping
+diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
+index 0d65bf24f515b80701150fdc430f324a533cb478..b92a3da5c325e69f5601416d4205fb33429742b3 100644
+--- a/net/minecraft/server/MinecraftServer.java
++++ b/net/minecraft/server/MinecraftServer.java
+@@ -305,6 +305,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+     public static final long SERVER_INIT = System.nanoTime(); // Paper - Lag compensation
  
-     public static <S extends MinecraftServer> S spin(Function<Thread, S> serverFactory) {
+     public static <S extends MinecraftServer> S spin(Function<Thread, S> threadFunction) {
 +        ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.init(); // Paper - rewrite data converter system
-         AtomicReference<S> atomicreference = new AtomicReference();
-         Thread thread = new Thread(() -> {
-             ((MinecraftServer) atomicreference.get()).runServer();
-diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java
-@@ -0,0 +0,0 @@ public class ChunkStorage implements AutoCloseable {
+         AtomicReference<S> atomicReference = new AtomicReference<>();
+         Thread thread = new ca.spottedleaf.moonrise.common.util.TickThread(() -> atomicReference.get().runServer(), "Server thread");
+         thread.setUncaughtExceptionHandler((thread1, exception) -> LOGGER.error("Uncaught exception in server thread", exception));
+diff --git a/net/minecraft/world/level/chunk/storage/ChunkStorage.java b/net/minecraft/world/level/chunk/storage/ChunkStorage.java
+index 1b931e68634e72c3465a99aa29aa53009163046b..80bc7ad9ad076968d06279dedd845d5946cf2501 100644
+--- a/net/minecraft/world/level/chunk/storage/ChunkStorage.java
++++ b/net/minecraft/world/level/chunk/storage/ChunkStorage.java
+@@ -85,7 +85,7 @@ public class ChunkStorage implements AutoCloseable {
          } else {
              try {
                  // CraftBukkit start
--                if (i < 1466) {
-+                if (false && i < 1466) { // Paper - no longer needed, data converter system / DFU handles it now
-                     CompoundTag level = nbttagcompound.getCompound("Level");
+-                if (version < 1466) {
++                if (false && version < 1466) { // Paper - no longer needed, data converter system / DFU handles it now
+                     CompoundTag level = chunkData.getCompound("Level");
                      if (level.getBoolean("TerrainPopulated") && !level.getBoolean("LightPopulated")) {
-                         ServerChunkCache cps = (generatoraccess == null) ? null : ((ServerLevel) generatoraccess).getChunkSource();
-@@ -0,0 +0,0 @@ public class ChunkStorage implements AutoCloseable {
+                         net.minecraft.server.level.ServerChunkCache cps = (generatoraccess == null) ? null : ((net.minecraft.server.level.ServerLevel) generatoraccess).getChunkSource();
+@@ -96,7 +96,7 @@ public class ChunkStorage implements AutoCloseable {
+                 }
                  // CraftBukkit end
- 
-                 if (i < 1493) {
--                    nbttagcompound = DataFixTypes.CHUNK.update(this.fixerUpper, nbttagcompound, i, 1493);
-+                    nbttagcompound = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.CHUNK, nbttagcompound, i, 1493); // Paper - replace chunk converter
-                     if (nbttagcompound.getCompound("Level").getBoolean("hasLegacyStructureData")) {
-                         LegacyStructureDataHandler persistentstructurelegacy = this.getLegacyStructureHandler(resourcekey, supplier);
- 
-@@ -0,0 +0,0 @@ public class ChunkStorage implements AutoCloseable {
+                 if (version < 1493) {
+-                    chunkData = DataFixTypes.CHUNK.update(this.fixerUpper, chunkData, version, 1493);
++                    chunkData = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.CHUNK, chunkData, version, 1493); // Paper - replace chunk converter
+                     if (chunkData.getCompound("Level").getBoolean("hasLegacyStructureData")) {
+                         LegacyStructureDataHandler legacyStructureHandler = this.getLegacyStructureHandler(levelKey, storage);
+                         chunkData = legacyStructureHandler.updateFromLegacy(chunkData);
+@@ -113,7 +113,7 @@ public class ChunkStorage implements AutoCloseable {
                  // Spigot end
  
-                 ChunkStorage.injectDatafixingContext(nbttagcompound, resourcekey, optional);
--                nbttagcompound = DataFixTypes.CHUNK.updateToCurrentVersion(this.fixerUpper, nbttagcompound, Math.max(1493, i));
-+                nbttagcompound = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.CHUNK, nbttagcompound, Math.max(1493, i), SharedConstants.getCurrentVersion().getDataVersion().getVersion()); // Paper - replace chunk converter
+                 injectDatafixingContext(chunkData, levelKey, chunkGeneratorKey);
+-                chunkData = DataFixTypes.CHUNK.updateToCurrentVersion(this.fixerUpper, chunkData, Math.max(1493, version));
++                chunkData = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.CHUNK, chunkData, Math.max(1493, version), SharedConstants.getCurrentVersion().getDataVersion().getVersion()); // Paper - replace chunk converter
                  // Spigot start
                  if (stopBelowZero) {
-                     nbttagcompound.putString("Status", net.minecraft.core.registries.BuiltInRegistries.CHUNK_STATUS.getKey(ChunkStatus.SPAWN).toString());
-diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
-@@ -0,0 +0,0 @@ public class SimpleRegionStorage implements AutoCloseable {
-         return this.worker.store(pos, nbt);
+                     chunkData.putString("Status", net.minecraft.core.registries.BuiltInRegistries.CHUNK_STATUS.getKey(net.minecraft.world.level.chunk.status.ChunkStatus.SPAWN).toString());
+diff --git a/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java b/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
+index 6be673172548c1382c7402ec4e1ec6ef51f702d3..41ddaceb7485626b1f2ee258c2142eb3114c106e 100644
+--- a/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
++++ b/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
+@@ -32,13 +32,30 @@ public class SimpleRegionStorage implements AutoCloseable {
+         return this.worker.store(chunkPos, data);
      }
  
 +    // Paper start - rewrite data conversion system
@@ -30683,122 +30683,73 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - rewrite data conversion system
 +
-     public CompoundTag upgradeChunkTag(CompoundTag nbt, int oldVersion) {
--        int i = NbtUtils.getDataVersion(nbt, oldVersion);
--        return this.dataFixType.updateToCurrentVersion(this.fixerUpper, nbt, i);
+     public CompoundTag upgradeChunkTag(CompoundTag tag, int version) {
+-        int dataVersion = NbtUtils.getDataVersion(tag, version);
+-        return this.dataFixType.updateToCurrentVersion(this.fixerUpper, tag, dataVersion);
 +        // Paper start - rewrite data conversion system
-+        final int dataVer = NbtUtils.getDataVersion(nbt, oldVersion);
-+        return ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(this.getDataConverterType(), nbt, dataVer, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion());
++        final int dataVer = NbtUtils.getDataVersion(tag, version);
++        return ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(this.getDataConverterType(), tag, dataVer, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion());
 +        // Paper end - rewrite data conversion system
      }
  
-     public Dynamic<Tag> upgradeChunkTag(Dynamic<Tag> nbt, int oldVersion) {
--        return this.dataFixType.updateToCurrentVersion(this.fixerUpper, nbt, oldVersion);
+     public Dynamic<Tag> upgradeChunkTag(Dynamic<Tag> tag, int version) {
+-        return this.dataFixType.updateToCurrentVersion(this.fixerUpper, tag, version);
 +        // Paper start - rewrite data conversion system
-+        final CompoundTag converted = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(this.getDataConverterType(), (CompoundTag)nbt.getValue(), oldVersion, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion());
-+        return new Dynamic<>(net.minecraft.nbt.NbtOps.INSTANCE, converted);
++        final CompoundTag converted = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(this.getDataConverterType(), (CompoundTag)tag.getValue(), version, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion());
++        return new Dynamic<>(tag.getOps(), converted);
 +        // Paper end - rewrite data conversion system
      }
  
-     public CompletableFuture<Void> synchronize(boolean sync) {
-diff --git a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java
-+++ b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java
-@@ -0,0 +0,0 @@ public class StructureCheck {
+     public CompletableFuture<Void> synchronize(boolean flushStorage) {
+diff --git a/net/minecraft/world/level/levelgen/structure/StructureCheck.java b/net/minecraft/world/level/levelgen/structure/StructureCheck.java
+index b348d06b261b23eef02c7b14b3010669de9a1b7e..06b54c0bec4031689d5c2da5cfea4ef28dbd16bc 100644
+--- a/net/minecraft/world/level/levelgen/structure/StructureCheck.java
++++ b/net/minecraft/world/level/levelgen/structure/StructureCheck.java
+@@ -151,7 +151,7 @@ public class StructureCheck {
  
-                 CompoundTag compoundTag2;
+                 CompoundTag compoundTag1;
                  try {
--                    compoundTag2 = DataFixTypes.CHUNK.updateToCurrentVersion(this.fixerUpper, compoundTag, i);
-+                    compoundTag2 = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.CHUNK, compoundTag, i, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion()); // Paper - replace chunk converter
+-                    compoundTag1 = DataFixTypes.CHUNK.updateToCurrentVersion(this.fixerUpper, compoundTag, version);
++                    compoundTag1 = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.CHUNK, compoundTag, version, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion()); // Paper - replace chunk converter
                  } catch (Exception var12) {
-                     LOGGER.warn("Failed to partially datafix chunk {}", pos, var12);
+                     LOGGER.warn("Failed to partially datafix chunk {}", chunkPos, var12);
                      return StructureCheckResult.CHUNK_LOAD_NEEDED;
-diff --git a/src/main/java/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager.java b/src/main/java/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager.java
-+++ b/src/main/java/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager.java
-@@ -0,0 +0,0 @@ public class StructureTemplateManager {
+diff --git a/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager.java b/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager.java
+index 408ba448c2f127e27e30bfcc6f35f0bdcf86d298..80533af16a59e9ad7e38d1c37b213529a4ecf5b8 100644
+--- a/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager.java
++++ b/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager.java
+@@ -245,7 +245,7 @@ public class StructureTemplateManager {
      public StructureTemplate readStructure(CompoundTag nbt) {
          StructureTemplate structureTemplate = new StructureTemplate();
-         int i = NbtUtils.getDataVersion(nbt, 500);
--        structureTemplate.load(this.blockLookup, DataFixTypes.STRUCTURE.updateToCurrentVersion(this.fixerUpper, nbt, i));
-+        structureTemplate.load(this.blockLookup, ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.STRUCTURE, nbt, i, SharedConstants.getCurrentVersion().getDataVersion().getVersion())); // Paper
+         int dataVersion = NbtUtils.getDataVersion(nbt, 500);
+-        structureTemplate.load(this.blockLookup, DataFixTypes.STRUCTURE.updateToCurrentVersion(this.fixerUpper, nbt, dataVersion));
++        structureTemplate.load(this.blockLookup, ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.STRUCTURE, nbt, dataVersion, SharedConstants.getCurrentVersion().getDataVersion().getVersion())); // Paper - rewrite data conversion system
          return structureTemplate;
      }
  
-diff --git a/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java b/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java
-+++ b/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java
-@@ -0,0 +0,0 @@ public class LevelStorageSource {
-     static Dynamic<?> readLevelDataTagFixed(Path path, DataFixer dataFixer) throws IOException {
-         CompoundTag nbttagcompound = LevelStorageSource.readLevelDataTagRaw(path);
-         CompoundTag nbttagcompound1 = nbttagcompound.getCompound("Data");
--        int i = NbtUtils.getDataVersion(nbttagcompound1, -1);
-+        int i = NbtUtils.getDataVersion(nbttagcompound1, -1); final int version = i; // Paper - obfuscation helpers
-         Dynamic<?> dynamic = DataFixTypes.LEVEL.updateToCurrentVersion(dataFixer, new Dynamic(NbtOps.INSTANCE, nbttagcompound1), i);
- 
-+        // Paper start - replace data conversion system
-         dynamic = dynamic.update("Player", (dynamic1) -> {
--            return DataFixTypes.PLAYER.updateToCurrentVersion(dataFixer, dynamic1, i);
-+            return new Dynamic<>(
-+                NbtOps.INSTANCE,
-+                ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(
-+                    ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.PLAYER,
-+                    (net.minecraft.nbt.CompoundTag)dynamic1.getValue(),
-+                    version, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion()
-+                )
-+            );
-         });
-+        // Paper end - replace data conversion system
-         dynamic = dynamic.update("WorldGenSettings", (dynamic1) -> {
-             return DataFixTypes.WORLD_GEN_SETTINGS.updateToCurrentVersion(dataFixer, dynamic1, i);
-         });
-diff --git a/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java b/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java
-+++ b/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java
-@@ -0,0 +0,0 @@ public class PlayerDataStorage {
-         }).map((nbttagcompound) -> {
-             int i = NbtUtils.getDataVersion(nbttagcompound, -1);
- 
--            nbttagcompound = DataFixTypes.PLAYER.updateToCurrentVersion(this.fixerUpper, nbttagcompound, i);
-+            nbttagcompound = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.PLAYER, nbttagcompound, i, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion()); // Paper - rewrite data conversion system
-             // entityhuman.load(nbttagcompound); // CraftBukkit - handled above
-             return nbttagcompound;
-         });
-diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
-+++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
-@@ -0,0 +0,0 @@ public final class CraftMagicNumbers implements UnsafeValues {
- 
-         net.minecraft.nbt.CompoundTag compound = deserializeNbtFromBytes(data);
-         final int dataVersion = compound.getInt("DataVersion");
--        compound = (net.minecraft.nbt.CompoundTag) MinecraftServer.getServer().fixerUpper.update(References.ITEM_STACK, new Dynamic<>(NbtOps.INSTANCE, compound), dataVersion, this.getDataVersion()).getValue();
-+        compound = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.ITEM_STACK, compound, dataVersion, this.getDataVersion()); // Paper - replace data conversion system
-         return CraftItemStack.asCraftMirror(net.minecraft.world.item.ItemStack.parse(MinecraftServer.getServer().registryAccess(), compound).orElseThrow());
+diff --git a/net/minecraft/world/level/storage/LevelStorageSource.java b/net/minecraft/world/level/storage/LevelStorageSource.java
+index feb4cd21e26598509dc140028496a6f4254c0680..de43e54698125ce9f319d4889dd49f7029fe95e0 100644
+--- a/net/minecraft/world/level/storage/LevelStorageSource.java
++++ b/net/minecraft/world/level/storage/LevelStorageSource.java
+@@ -227,7 +227,7 @@ public class LevelStorageSource {
+         CompoundTag compound = levelDataTagRaw.getCompound("Data");
+         int dataVersion = NbtUtils.getDataVersion(compound, -1);
+         Dynamic<?> dynamic = DataFixTypes.LEVEL.updateToCurrentVersion(dataFixer, new Dynamic<>(NbtOps.INSTANCE, compound), dataVersion);
+-        dynamic = dynamic.update("Player", dynamic1 -> DataFixTypes.PLAYER.updateToCurrentVersion(dataFixer, dynamic1, dataVersion));
++        dynamic = dynamic.update("Player", dynamic1 -> new Dynamic(dynamic1.getOps(), ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.PLAYER, (net.minecraft.nbt.CompoundTag)dynamic1.getValue(), dataVersion, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion()))); // Paper - replace data conversion system
+         return dynamic.update("WorldGenSettings", dynamic1 -> DataFixTypes.WORLD_GEN_SETTINGS.updateToCurrentVersion(dataFixer, dynamic1, dataVersion));
      }
  
-@@ -0,0 +0,0 @@ public final class CraftMagicNumbers implements UnsafeValues {
+diff --git a/net/minecraft/world/level/storage/PlayerDataStorage.java b/net/minecraft/world/level/storage/PlayerDataStorage.java
+index 6c9640f5432e9110e7811b6db246d268c6243feb..45f2800c4862a726490048576fca8e1f24252676 100644
+--- a/net/minecraft/world/level/storage/PlayerDataStorage.java
++++ b/net/minecraft/world/level/storage/PlayerDataStorage.java
+@@ -115,7 +115,7 @@ public class PlayerDataStorage {
  
-         final int dataVersion = data.get("DataVersion").getAsInt();
-         final int currentVersion = org.bukkit.craftbukkit.util.CraftMagicNumbers.INSTANCE.getDataVersion();
--        data = (com.google.gson.JsonObject) MinecraftServer.getServer().fixerUpper.update(References.ITEM_STACK, new Dynamic<>(com.mojang.serialization.JsonOps.INSTANCE, data), dataVersion, currentVersion).getValue();
-+        data = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertJson(
-+            ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.ITEM_STACK,
-+            data, false, dataVersion, currentVersion
-+        );
-         com.mojang.serialization.DynamicOps<com.google.gson.JsonElement> ops = MinecraftServer.getServer().registryAccess().createSerializationContext(com.mojang.serialization.JsonOps.INSTANCE);
-         return CraftItemStack.asCraftMirror(net.minecraft.world.item.ItemStack.CODEC.parse(ops, data).getOrThrow(IllegalArgumentException::new));
-     }
-@@ -0,0 +0,0 @@ public final class CraftMagicNumbers implements UnsafeValues {
- 
-         net.minecraft.nbt.CompoundTag compound = deserializeNbtFromBytes(data);
-         int dataVersion = compound.getInt("DataVersion");
--        compound = (net.minecraft.nbt.CompoundTag) MinecraftServer.getServer().fixerUpper.update(References.ENTITY, new Dynamic<>(NbtOps.INSTANCE, compound), dataVersion, this.getDataVersion()).getValue();
-+        compound = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.ENTITY, compound, dataVersion, this.getDataVersion());
-         if (!preserveUUID) {
-             // Generate a new UUID so we don't have to worry about deserializing the same entity twice
-             compound.remove("UUID");
+         return optional.or(() -> this.load(name, uuid, ".dat_old")).map(compoundTag -> { // CraftBukkit
+             int dataVersion = NbtUtils.getDataVersion(compoundTag, -1);
+-            compoundTag = DataFixTypes.PLAYER.updateToCurrentVersion(this.fixerUpper, compoundTag, dataVersion);
++            compoundTag = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.PLAYER, compoundTag, dataVersion, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion()); // Paper - rewrite data conversion system
+             // player.load(compoundTag); // CraftBukkit - handled above
+             return compoundTag;
+         });
diff --git a/feature-patches/1059-Moonrise-optimisation-patches.patch b/paper-server/patches/features/0017-Moonrise-optimisation-patches.patch
similarity index 84%
rename from feature-patches/1059-Moonrise-optimisation-patches.patch
rename to paper-server/patches/features/0017-Moonrise-optimisation-patches.patch
index 5373ef9f77..337302aaa2 100644
--- a/feature-patches/1059-Moonrise-optimisation-patches.patch
+++ b/paper-server/patches/features/0017-Moonrise-optimisation-patches.patch
@@ -17,18 +17,18 @@ Currently includes:
 
 See https://github.com/Tuinity/Moonrise
 
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
+diff --git a/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java b/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..1b8193587814225c2ef2c5d9e667436eb50ff6c5
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
+@@ -0,0 +1,273 @@
 +package ca.spottedleaf.moonrise.common.misc;
 +
++import ca.spottedleaf.moonrise.common.PlatformHooks;
 +import ca.spottedleaf.moonrise.common.list.ReferenceList;
 +import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
 +import ca.spottedleaf.moonrise.common.util.MoonriseConstants;
-+import ca.spottedleaf.moonrise.common.util.ChunkSystem;
 +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel;
 +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData;
 +import ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants;
@@ -146,8 +146,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        players[NearbyMapType.GENERAL.ordinal()].update(chunk.x, chunk.z, GENERAL_AREA_VIEW_DISTANCE);
 +        players[NearbyMapType.GENERAL_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_SMALL_VIEW_DISTANCE);
 +        players[NearbyMapType.GENERAL_REALLY_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_REALLY_SMALL_VIEW_DISTANCE);
-+        players[NearbyMapType.TICK_VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getTickViewDistance(player));
-+        players[NearbyMapType.VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getViewDistance(player));
++        players[NearbyMapType.TICK_VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, PlatformHooks.get().getTickViewDistance(player));
++        players[NearbyMapType.VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, PlatformHooks.get().getViewDistance(player));
 +        players[NearbyMapType.SPAWN_RANGE.ordinal()].update(chunk.x, chunk.z, ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); // Moonrise - chunk tick iteration
 +    }
 +
@@ -296,62 +296,65 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
-@@ -0,0 +0,0 @@ package ca.spottedleaf.moonrise.common.util;
+diff --git a/ca/spottedleaf/moonrise/paper/PaperHooks.java b/ca/spottedleaf/moonrise/paper/PaperHooks.java
+index 5577287398db2bb9d21f14409ef580b8ab9ea018..4d344559a20a0c35c181e297e81788c747363ec9 100644
+--- a/ca/spottedleaf/moonrise/paper/PaperHooks.java
++++ b/ca/spottedleaf/moonrise/paper/PaperHooks.java
+@@ -268,7 +268,7 @@ public final class PaperHooks extends BaseChunkSystemHooks implements PlatformHo
  
- import ca.spottedleaf.concurrentutil.util.Priority;
- import ca.spottedleaf.moonrise.common.PlatformHooks;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk;
-+import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader;
-+import ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemServerChunkCache;
-+import ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel;
- import com.mojang.logging.LogUtils;
- import net.minecraft.server.level.ChunkHolder;
- import net.minecraft.server.level.FullChunkStatus;
- import net.minecraft.server.level.ServerLevel;
- import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.server.level.progress.ChunkProgressListener;
- import net.minecraft.world.entity.Entity;
- import net.minecraft.world.level.chunk.ChunkAccess;
- import net.minecraft.world.level.chunk.LevelChunk;
-@@ -0,0 +0,0 @@ import java.util.function.Consumer;
- public final class ChunkSystem {
+     @Override
+     public void postLoadProtoChunk(final ServerLevel world, final ProtoChunk chunk) {
+-        net.minecraft.world.level.chunk.status.ChunkStatusTasks.postLoadProtoChunk(world, chunk.getEntities());
++        net.minecraft.world.level.chunk.status.ChunkStatusTasks.postLoadProtoChunk(world, chunk.getEntities(), chunk.getPos()); // Paper - rewrite chunk system - add ChunkPos param
+     }
  
-     private static final Logger LOGGER = LogUtils.getLogger();
--    private static final net.minecraft.world.level.chunk.status.ChunkStep FULL_CHUNK_STEP = net.minecraft.world.level.chunk.status.ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FULL);
+     @Override
+diff --git a/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java b/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java
+index 2a03573a6a6753f1c87dbde26ff5208c827aa13a..ece1261b67033e946dfc20a96872708755bffe0a 100644
+--- a/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java
++++ b/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java
+@@ -23,218 +23,59 @@ import java.util.function.Consumer;
+ 
+ public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.common.util.ChunkSystemHooks {
+ 
+-    private static final Logger LOGGER = LogUtils.getLogger();
+-    private static final ChunkStep FULL_CHUNK_STEP = ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FULL);
+-    private static final TicketType<Long> CHUNK_LOAD = TicketType.create("chunk_load", Long::compareTo);
+-
+-    private long chunkLoadCounter = 0L;
 -
 -    private static int getDistance(final ChunkStatus status) {
 -        return FULL_CHUNK_STEP.getAccumulatedRadiusOf(status);
 -    }
- 
-     public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) {
-         scheduleChunkTask(level, chunkX, chunkZ, run, Priority.NORMAL);
+-
+     @Override
+     public void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) {
+-        this.scheduleChunkTask(level, chunkX, chunkZ, run, Priority.NORMAL);
++        scheduleChunkTask(level, chunkX, chunkZ, run, Priority.NORMAL);
      }
  
-     public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final Priority priority) {
+     @Override
+     public void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final Priority priority) {
 -        level.chunkSource.mainThreadProcessor.execute(run);
-+        ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkTask(chunkX, chunkZ, run, priority);
++        ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkTask(chunkX, chunkZ, run, priority);
      }
  
-     public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen,
-                                          final ChunkStatus toStatus, final boolean addTicket, final Priority priority,
-                                          final Consumer<ChunkAccess> onComplete) {
+     @Override
+     public void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen,
+                                   final ChunkStatus toStatus, final boolean addTicket, final Priority priority,
+                                   final Consumer<ChunkAccess> onComplete) {
 -        if (gen) {
--            scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+-            this.scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
 -            return;
 -        }
--        scheduleChunkLoad(level, chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> {
+-        this.scheduleChunkLoad(level, chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> {
 -            if (chunk == null) {
 -                if (onComplete != null) {
 -                    onComplete.accept(null);
 -                }
 -            } else {
 -                if (chunk.getPersistedStatus().isOrAfter(toStatus)) {
--                    scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+-                    BaseChunkSystemHooks.this.scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
 -                } else {
 -                    if (onComplete != null) {
 -                        onComplete.accept(null);
@@ -359,24 +362,22 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -                }
 -            }
 -        });
-+        ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, gen, toStatus, addTicket, priority, onComplete);
++        ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, gen, toStatus, addTicket, priority, onComplete);
      }
  
--    static final net.minecraft.server.level.TicketType<Long> CHUNK_LOAD = net.minecraft.server.level.TicketType.create("chunk_load", Long::compareTo);
--
--    private static long chunkLoadCounter = 0L;
-     public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus,
-                                          final boolean addTicket, final Priority priority, final Consumer<ChunkAccess> onComplete) {
--        if (!org.bukkit.Bukkit.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) {
--            scheduleChunkTask(level, chunkX, chunkZ, () -> {
--                scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+     @Override
+     public void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus,
+                                   final boolean addTicket, final Priority priority, final Consumer<ChunkAccess> onComplete) {
+-        if (!Bukkit.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) {
+-            this.scheduleChunkTask(level, chunkX, chunkZ, () -> {
+-                BaseChunkSystemHooks.this.scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
 -            }, priority);
 -            return;
 -        }
 -
 -        final int minLevel = 33 + getDistance(toStatus);
--        final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
--        final net.minecraft.world.level.ChunkPos chunkPos = new net.minecraft.world.level.ChunkPos(chunkX, chunkZ);
+-        final Long chunkReference = addTicket ? Long.valueOf(++this.chunkLoadCounter) : null;
+-        final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
 -
 -        if (addTicket) {
 -            level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
@@ -399,51 +400,52 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -            }
 -        };
 -
--        final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+-        final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
 -
 -        if (holder == null || holder.getTicketLevel() > minLevel) {
 -            loadCallback.accept(null);
 -            return;
 -        }
 -
--        final java.util.concurrent.CompletableFuture<net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.ChunkAccess>> loadFuture = holder.scheduleChunkGenerationTask(toStatus, level.chunkSource.chunkMap);
+-        final CompletableFuture<ChunkResult<ChunkAccess>> loadFuture = holder.scheduleChunkGenerationTask(toStatus, level.chunkSource.chunkMap);
 -
 -        if (loadFuture.isDone()) {
 -            loadCallback.accept(loadFuture.join().orElse(null));
 -            return;
 -        }
 -
--        loadFuture.whenCompleteAsync((final net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.ChunkAccess> result, final Throwable thr) -> {
+-        loadFuture.whenCompleteAsync((final ChunkResult<ChunkAccess> result, final Throwable thr) -> {
 -            if (thr != null) {
 -                loadCallback.accept(null);
 -                return;
 -            }
 -            loadCallback.accept(result.orElse(null));
 -        }, (final Runnable r) -> {
--            scheduleChunkTask(level, chunkX, chunkZ, r, Priority.HIGHEST);
+-            BaseChunkSystemHooks.this.scheduleChunkTask(level, chunkX, chunkZ, r, Priority.HIGHEST);
 -        });
-+        ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
++        ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
      }
  
-     public static void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ,
-                                             final FullChunkStatus toStatus, final boolean addTicket,
-                                             final Priority priority, final Consumer<LevelChunk> onComplete) {
+     @Override
+     public void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ,
+                                      final FullChunkStatus toStatus, final boolean addTicket,
+                                      final Priority priority, final Consumer<LevelChunk> onComplete) {
 -        // This method goes unused until the chunk system rewrite
 -        if (toStatus == FullChunkStatus.INACCESSIBLE) {
 -            throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status");
 -        }
 -
--        if (!org.bukkit.Bukkit.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) {
--            scheduleChunkTask(level, chunkX, chunkZ, () -> {
--                scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+-        if (!Bukkit.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) {
+-            this.scheduleChunkTask(level, chunkX, chunkZ, () -> {
+-                BaseChunkSystemHooks.this.scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
 -            }, priority);
 -            return;
 -        }
 -
 -        final int minLevel = 33 - (toStatus.ordinal() - 1);
 -        final int radius = toStatus.ordinal() - 1;
--        final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
--        final net.minecraft.world.level.ChunkPos chunkPos = new net.minecraft.world.level.ChunkPos(chunkX, chunkZ);
+-        final Long chunkReference = addTicket ? Long.valueOf(++this.chunkLoadCounter) : null;
+-        final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
 -
 -        if (addTicket) {
 -            level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
@@ -460,20 +462,20 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -                com.destroystokyo.paper.util.SneakyThrow.sneaky(thr);
 -            } finally {
 -                if (addTicket) {
--                    level.chunkSource.addTicketAtLevel(net.minecraft.server.level.TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
+-                    level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
 -                    level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
 -                }
 -            }
 -        };
 -
--        final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+-        final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
 -
 -        if (holder == null || holder.getTicketLevel() > minLevel) {
 -            loadCallback.accept(null);
 -            return;
 -        }
 -
--        final java.util.concurrent.CompletableFuture<net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.LevelChunk>> tickingState;
+-        final CompletableFuture<ChunkResult<LevelChunk>> tickingState;
 -        switch (toStatus) {
 -            case FULL: {
 -                tickingState = holder.getFullChunkFuture();
@@ -497,185 +499,191 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -            return;
 -        }
 -
--        tickingState.whenCompleteAsync((final net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.LevelChunk> result, final Throwable thr) -> {
+-        tickingState.whenCompleteAsync((final ChunkResult<LevelChunk> result, final Throwable thr) -> {
 -            if (thr != null) {
 -                loadCallback.accept(null);
 -                return;
 -            }
 -            loadCallback.accept(result.orElse(null));
 -        }, (final Runnable r) -> {
--            scheduleChunkTask(level, chunkX, chunkZ, r, Priority.HIGHEST);
+-            BaseChunkSystemHooks.this.scheduleChunkTask(level, chunkX, chunkZ, r, Priority.HIGHEST);
 -        });
-+        ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
++        ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
      }
  
-     public static List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) {
--        return new java.util.ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values());
-+        return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHolders();
+     @Override
+     public List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) {
+-        return new ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values());
++        return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHolders();
      }
  
-     public static List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level) {
--        return new java.util.ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values());
-+        return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHolders();
+     @Override
+     public List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level) {
+-        return new ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values());
++        return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHolders();
      }
  
-     public static int getVisibleChunkHolderCount(final ServerLevel level) {
+     @Override
+     public int getVisibleChunkHolderCount(final ServerLevel level) {
 -        return level.chunkSource.chunkMap.visibleChunkMap.size();
-+        return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.size();
++        return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.size();
      }
  
-     public static int getUpdatingChunkHolderCount(final ServerLevel level) {
+     @Override
+     public int getUpdatingChunkHolderCount(final ServerLevel level) {
 -        return level.chunkSource.chunkMap.updatingChunkMap.size();
-+        return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.size();
++        return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.size();
      }
  
-     public static boolean hasAnyChunkHolders(final ServerLevel level) {
-@@ -0,0 +0,0 @@ public final class ChunkSystem {
+     @Override
+     public boolean hasAnyChunkHolders(final ServerLevel level) {
+-        return this.getUpdatingChunkHolderCount(level) != 0;
++        return getUpdatingChunkHolderCount(level) != 0;
      }
  
-     public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) {
+     @Override
+@@ -244,89 +85,110 @@ public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.co
+ 
+     @Override
+     public void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) {
+-
 +        // Update progress listener for LevelLoadingScreen
-+        final ChunkProgressListener progressListener = level.getChunkSource().chunkMap.progressListener;
++        final net.minecraft.server.level.progress.ChunkProgressListener progressListener = level.getChunkSource().chunkMap.progressListener;
 +        if (progressListener != null) {
-+            ChunkSystem.scheduleChunkTask(level, holder.getPos().x, holder.getPos().z, () -> {
++            this.scheduleChunkTask(level, holder.getPos().x, holder.getPos().z, () -> {
 +                progressListener.onStatusChange(holder.getPos(), null);
 +            });
 +        }
-+    }
- 
-+    public static void onChunkPreBorder(final LevelChunk chunk, final ChunkHolder holder) {
-+        ((ChunkSystemServerChunkCache)((ServerLevel)chunk.getLevel()).getChunkSource())
-+                .moonrise$setFullChunk(chunk.getPos().x, chunk.getPos().z, chunk);
      }
  
-     public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) {
+     @Override
+     public void onChunkPreBorder(final LevelChunk chunk, final ChunkHolder holder) {
 -
-+        ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().add(
-+                ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
++        ((ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemServerChunkCache)((ServerLevel)chunk.getLevel()).getChunkSource())
++            .moonrise$setFullChunk(chunk.getPos().x, chunk.getPos().z, chunk);
+     }
+ 
+     @Override
+     public void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) {
+-
++        ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().add(
++            ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
 +        );
 +        chunk.loadCallback();
      }
  
-     public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
-+        ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().remove(
-+                ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
+     @Override
+     public void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
+-
++        ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().remove(
++            ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
 +        );
 +        chunk.unloadCallback();
-+    }
- 
-+    public static void onChunkPostNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
-+        ((ChunkSystemServerChunkCache)((ServerLevel)chunk.getLevel()).getChunkSource())
-+                .moonrise$setFullChunk(chunk.getPos().x, chunk.getPos().z, null);
      }
  
-     public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) {
+     @Override
+     public void onChunkPostNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
 -
-+        ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().add(
-+                ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
++        ((ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemServerChunkCache)((ServerLevel)chunk.getLevel()).getChunkSource())
++            .moonrise$setFullChunk(chunk.getPos().x, chunk.getPos().z, null);
+     }
+ 
+     @Override
+     public void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) {
+-
++        ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().add(
++            ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
 +        );
-+        if (!((ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) {
++        if (!((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) {
 +            chunk.postProcessGeneration((ServerLevel)chunk.getLevel());
 +        }
 +        ((ServerLevel)chunk.getLevel()).startTickingChunk(chunk);
 +        ((ServerLevel)chunk.getLevel()).getChunkSource().chunkMap.tickingGenerated.incrementAndGet();
-+        ((ChunkTickServerLevel)(ServerLevel)chunk.getLevel()).moonrise$markChunkForPlayerTicking(chunk); // Moonrise - chunk tick iteration
++        ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel)(ServerLevel)chunk.getLevel()).moonrise$markChunkForPlayerTicking(chunk); // Moonrise - chunk tick iteration
      }
  
-     public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) {
+     @Override
+     public void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) {
 -
-+        ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().remove(
-+                ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
++        ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().remove(
++            ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
 +        );
-+        ((ChunkTickServerLevel)(ServerLevel)chunk.getLevel()).moonrise$removeChunkForPlayerTicking(chunk); // Moonrise - chunk tick iteration
++        ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel)(ServerLevel)chunk.getLevel()).moonrise$removeChunkForPlayerTicking(chunk); // Moonrise - chunk tick iteration
      }
  
-     public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
+     @Override
+     public void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
 -
-+        ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().add(
-+                ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
++        ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().add(
++            ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
 +        );
      }
  
-     public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
+     @Override
+     public void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
 -
-+        ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().remove(
-+                ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
++        ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().remove(
++            ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
 +        );
      }
  
-     public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) {
+     @Override
+     public ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) {
 -        return level.chunkSource.chunkMap.getUnloadingChunkHolder(chunkX, chunkZ);
 +        return null;
      }
  
-     public static int getSendViewDistance(final ServerPlayer player) {
--        return getViewDistance(player);
-+        return RegionizedPlayerChunkLoader.getAPISendViewDistance(player);
+     @Override
+     public int getSendViewDistance(final ServerPlayer player) {
+-        return this.getViewDistance(player);
++        return ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.getAPISendViewDistance(player);
      }
  
-     public static int getViewDistance(final ServerPlayer player) {
+     @Override
+     public int getViewDistance(final ServerPlayer player) {
 -        final ServerLevel level = player.serverLevel();
 -        if (level == null) {
--            return org.bukkit.Bukkit.getViewDistance();
+-            return Bukkit.getViewDistance();
 -        }
 -        return level.chunkSource.chunkMap.serverViewDistance;
-+        return RegionizedPlayerChunkLoader.getAPIViewDistance(player);
++        return ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.getAPIViewDistance(player);
      }
  
-     public static int getTickViewDistance(final ServerPlayer player) {
+     @Override
+     public int getTickViewDistance(final ServerPlayer player) {
 -        final ServerLevel level = player.serverLevel();
 -        if (level == null) {
--            return org.bukkit.Bukkit.getSimulationDistance();
+-            return Bukkit.getSimulationDistance();
 -        }
 -        return level.chunkSource.chunkMap.distanceManager.simulationDistance;
-+        return RegionizedPlayerChunkLoader.getAPITickViewDistance(player);
-+    }
-+
-+    public static void addPlayerToDistanceMaps(final ServerLevel world, final ServerPlayer player) {
-+        ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().addPlayer(player);
-+    }
-+
-+    public static void removePlayerFromDistanceMaps(final ServerLevel world, final ServerPlayer player) {
-+        ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().removePlayer(player);
-+    }
-+
-+    public static void updateMaps(final ServerLevel world, final ServerPlayer player) {
-+        ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().updatePlayer(player);
-     }
- 
-     private ChunkSystem() {}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java b/src/main/java/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java
-@@ -0,0 +0,0 @@ import net.minecraft.world.level.levelgen.PositionalRandomFactory;
- /**
-  * Avoid costly CAS of superclass
-  */
--public final class ThreadUnsafeRandom implements BitRandomSource {
-+public class ThreadUnsafeRandom implements BitRandomSource { // Paper - replace random
- 
-     private static final long MULTIPLIER = 25214903917L;
-     private static final long ADDEND = 11L;
-diff --git a/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java b/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java
-@@ -0,0 +0,0 @@ public final class PaperHooks implements PlatformHooks {
- 
-     @Override
-     public void postLoadProtoChunk(final ServerLevel world, final ProtoChunk chunk) {
--        net.minecraft.world.level.chunk.status.ChunkStatusTasks.postLoadProtoChunk(world, chunk.getEntities());
-+        net.minecraft.world.level.chunk.status.ChunkStatusTasks.postLoadProtoChunk(world, chunk.getEntities(), chunk.getPos()); // Paper - rewrite chunk system - add ChunkPos param
++        return ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.getAPITickViewDistance(player);
      }
  
      @Override
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java
+     public void addPlayerToDistanceMaps(final ServerLevel world, final ServerPlayer player) {
+-
++        ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().addPlayer(player);
+     }
+ 
+     @Override
+     public void removePlayerFromDistanceMaps(final ServerLevel world, final ServerPlayer player) {
+-
++        ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().removePlayer(player);
+     }
+ 
+     @Override
+     public void updateMaps(final ServerLevel world, final ServerPlayer player) {
+-
++        ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().updatePlayer(player);
+     }
+ }
+\ No newline at end of file
+diff --git a/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java b/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..93bc56daec4526f373c84763b8c7ccb4a30e800b
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java
+@@ -0,0 +1,10 @@
 +package ca.spottedleaf.moonrise.patches.block_counting;
 +
 +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
@@ -686,12 +694,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public Int2ObjectOpenHashMap<ShortArrayList> moonrise$countEntries();
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java b/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java
+diff --git a/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java b/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..0d1443a113c07d7655e7b927a899447f70db8fa9
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java
+@@ -0,0 +1,11 @@
 +package ca.spottedleaf.moonrise.patches.block_counting;
 +
 +import ca.spottedleaf.moonrise.common.list.ShortList;
@@ -703,12 +711,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public ShortList moonrise$getTickingBlockList();
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java
+diff --git a/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java b/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..89e75b454695e174c5619104eeb15eb923a2d9a7
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java
+@@ -0,0 +1,12 @@
 +package ca.spottedleaf.moonrise.patches.blockstate_propertyaccess;
 +
 +public interface PropertyAccess<T> {
@@ -721,12 +729,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    public void moonrise$setById(final T[] values);
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccessStateHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccessStateHolder.java
+diff --git a/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccessStateHolder.java b/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccessStateHolder.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..01da52b9e8a786824f199a057b62ce0431ecbc43
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccessStateHolder.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccessStateHolder.java
+@@ -0,0 +1,7 @@
 +package ca.spottedleaf.moonrise.patches.blockstate_propertyaccess;
 +
 +public interface PropertyAccessStateHolder {
@@ -734,12 +742,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public long moonrise$getTableIndex();
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/util/ZeroCollidingReferenceStateTable.java b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/util/ZeroCollidingReferenceStateTable.java
+diff --git a/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/util/ZeroCollidingReferenceStateTable.java b/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/util/ZeroCollidingReferenceStateTable.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..866f38eb0f379ffbe2888023a7d1c290f521a231
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/util/ZeroCollidingReferenceStateTable.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/util/ZeroCollidingReferenceStateTable.java
+@@ -0,0 +1,230 @@
 +package ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util;
 +
 +import ca.spottedleaf.concurrentutil.util.IntegerUtil;
@@ -970,12 +978,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java b/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..44bb25554634af2ec0b2e9b3d9231304d5dff034
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java
+@@ -0,0 +1,39 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system;
 +
 +import ca.spottedleaf.moonrise.common.PlatformHooks;
@@ -1015,12 +1023,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private ChunkSystemConverters() {}
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java b/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..c7da23900228aab3a5673eb5adfada5091140319
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java
+@@ -0,0 +1,44 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.entity;
 +
 +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData;
@@ -1065,12 +1073,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    public boolean moonrise$hasAnyPlayerPassengers();
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..a814512fcfb85312474ae2c2c21443843bf57831
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java
+@@ -0,0 +1,31 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.io;
 +
 +import net.minecraft.nbt.CompoundTag;
@@ -1102,12 +1110,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            final int chunkX, final int chunkZ, final MoonriseRegionFileIO.RegionDataController.ReadData readData
 +    ) throws IOException;
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..1acea58838f057ab87efd103cbecb6f5aeaef393
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java
+@@ -0,0 +1,1700 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.io;
 +
 +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
@@ -2808,12 +2816,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..a36ab89f5c37f5f9ab0152f087bb4cf3560f8581
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java
+@@ -0,0 +1,50 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller;
 +
 +import ca.spottedleaf.moonrise.patches.chunk_system.io.ChunkSystemRegionFileStorage;
@@ -2864,12 +2872,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$finishRead(chunkX, chunkZ, readData);
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..828c868f68c2a20bf90d0f7ec253fdeb591f15f6
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java
+@@ -0,0 +1,73 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller;
 +
 +import ca.spottedleaf.moonrise.patches.chunk_system.io.ChunkSystemRegionFileStorage;
@@ -2943,12 +2951,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..bd0d782852f9cfe5bc0b5339ecf4d82c10332ec9
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java
+@@ -0,0 +1,45 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller;
 +
 +import ca.spottedleaf.moonrise.patches.chunk_system.io.ChunkSystemRegionFileStorage;
@@ -2994,12 +3002,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$finishRead(chunkX, chunkZ, readData);
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemChunkMap.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemChunkMap.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemChunkMap.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemChunkMap.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..47a4d3376d08dde94a39254bec21473ff27f53e6
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemChunkMap.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemChunkMap.java
+@@ -0,0 +1,10 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.level;
 +
 +import net.minecraft.world.level.ChunkPos;
@@ -3010,12 +3018,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public void moonrise$writeFinishCallback(final ChunkPos pos) throws IOException;
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..5d4d650186b18eb00782429d53d861564d8e4ba9
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java
+@@ -0,0 +1,33 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.level;
 +
 +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData;
@@ -3049,12 +3057,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public boolean moonrise$areChunksLoaded(final int fromX, final int fromZ, final int toX, final int toZ);
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..0b58701342d573fa43cdd06681534854a0e51d77
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java
+@@ -0,0 +1,10 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.level;
 +
 +import net.minecraft.world.level.chunk.ChunkAccess;
@@ -3065,12 +3073,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public ChunkAccess moonrise$syncLoadNonFull(final int chunkX, final int chunkZ, final ChunkStatus status);
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..c278f8ef806f0b45c28cc3040c7db052cb51e053
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java
+@@ -0,0 +1,62 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.level;
 +
 +import ca.spottedleaf.concurrentutil.util.Priority;
@@ -3133,12 +3141,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    public ReferenceList<ServerChunkCache.ChunkAndHolder> moonrise$getEntityTickingChunks();
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..8b9dc582627b46843f4b5ea6f8c3df2d8cac46fa
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java
+@@ -0,0 +1,21 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk;
 +
 +import ca.spottedleaf.moonrise.common.misc.NearbyPlayers;
@@ -3160,12 +3168,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return --this.referenceCount;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..7d049d750df88762566f13a9c4fc7574a2df4825
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java
+@@ -0,0 +1,26 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk;
 +
 +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
@@ -3192,12 +3200,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public LevelChunk moonrise$getFullChunk();
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f4bc44bb266763345c4e6f859c89352c769a104d
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java
+@@ -0,0 +1,26 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk;
 +
 +import net.minecraft.world.level.chunk.status.ChunkStatus;
@@ -3224,12 +3232,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public AtomicBoolean moonrise$getWarnedAboutNoImmediateComplete();
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..aacd543f03b35908011d0c2891e978cc093ebcf5
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java
+@@ -0,0 +1,12 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk;
 +
 +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
@@ -3242,12 +3250,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public ChunkHolderManager moonrise$getChunkHolderManager();
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..5b092bca7027e37aeee8f4b852ad896dd0d5febc
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java
+@@ -0,0 +1,13 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk;
 +
 +import net.minecraft.server.level.ServerChunkCache;
@@ -3261,12 +3269,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public void moonrise$setChunkAndHolder(final ServerChunkCache.ChunkAndHolder holder);
 +
 +}
-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
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..7aea4e343581b977d11af90f9f65eac3532eade1
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java
+@@ -0,0 +1,569 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.level.entity;
 +
 +import ca.spottedleaf.moonrise.common.PlatformHooks;
@@ -3836,12 +3844,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..7554c109c35397bc1a43dd80e87764fd78645bbf
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java
+@@ -0,0 +1,1002 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.level.entity;
 +
 +import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
@@ -4844,12 +4852,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        public void onRemove(final Entity.RemovalReason reason) {}
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..a038215156a163b0b1cbc870ada5b4ac85ed1335
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java
+@@ -0,0 +1,129 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.level.entity.client;
 +
 +import ca.spottedleaf.moonrise.common.PlatformHooks;
@@ -4979,12 +4987,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..2ff58cf753c60913ee73aae015182e9c5560d529
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java
+@@ -0,0 +1,114 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.level.entity.dfl;
 +
 +import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
@@ -5099,19 +5107,18 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        public void onSectionChange(final Entity entity) {}
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..26207443b1223119c03db478d7e816d9cdf8e618
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java
+@@ -0,0 +1,115 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server;
 +
 +import ca.spottedleaf.moonrise.common.PlatformHooks;
 +import ca.spottedleaf.moonrise.common.list.ReferenceList;
 +import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
 +import ca.spottedleaf.moonrise.common.util.TickThread;
-+import ca.spottedleaf.moonrise.common.util.ChunkSystem;
 +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
 +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices;
 +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup;
@@ -5218,15 +5225,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    @Override
 +    protected boolean screenEntity(final Entity entity, final boolean fromDisk, final boolean event) {
-+        return ChunkSystem.screenEntity(this.serverWorld, entity, fromDisk, event);
++        return PlatformHooks.get().screenEntity(this.serverWorld, entity, fromDisk, event);
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..458d1fc5e1222912512e6c59b56f6fca347d9ee9
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java
+@@ -0,0 +1,17 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.level.poi;
 +
 +import ca.spottedleaf.moonrise.patches.chunk_system.level.storage.ChunkSystemSectionStorage;
@@ -5244,12 +5251,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public void moonrise$checkConsistency(final ChunkAccess chunk);
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..89b956b8fdf1a0d862a843104511005e2990a897
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java
+@@ -0,0 +1,12 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.level.poi;
 +
 +import net.minecraft.world.entity.ai.village.poi.PoiSection;
@@ -5262,12 +5269,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public Optional<PoiSection> moonrise$asOptional();
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..bbf9d6c1c9525d97160806819a57be03eca290f1
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java
+@@ -0,0 +1,204 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.level.poi;
 +
 +import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
@@ -5472,12 +5479,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return ret;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..524752744e37a2db0e3ea089468bdf497129bfef
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java
+@@ -0,0 +1,13 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.level.storage;
 +
 +import net.minecraft.nbt.CompoundTag;
@@ -5491,12 +5498,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public void moonrise$close() throws IOException;
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java b/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..003a857e70ead858e8437e3c1bfaf22f4daba0df
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java
+@@ -0,0 +1,15 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.player;
 +
 +public interface ChunkSystemServerPlayer {
@@ -5512,12 +5519,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder();
 +
 +}
-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
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..dd2509996bfd08e8c3f9f2be042229eac6d7692d
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java
+@@ -0,0 +1,1092 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.player;
 +
 +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
@@ -6610,12 +6617,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java b/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..7eafc5b7cba23d8dec92ecc1050afe3fd8c9e309
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java
+@@ -0,0 +1,144 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.queue;
 +
 +import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
@@ -6761,12 +6768,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +}
 \ No newline at end of file
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..b5817aa8f537593f6d9fc6b612c82ccccb250ac7
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
+@@ -0,0 +1,1456 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
 +
 +import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock;
@@ -6776,7 +6783,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
 +import ca.spottedleaf.moonrise.common.util.TickThread;
 +import ca.spottedleaf.moonrise.common.util.WorldUtil;
-+import ca.spottedleaf.moonrise.common.util.ChunkSystem;
 +import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
 +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
 +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices;
@@ -7613,7 +7619,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    private NewChunkHolder createChunkHolder(final long position) {
 +        final NewChunkHolder ret = new NewChunkHolder(this.world, CoordinateUtils.getChunkX(position), CoordinateUtils.getChunkZ(position), this.taskScheduler);
 +
-+        ChunkSystem.onChunkHolderCreate(this.world, ret.vanillaChunkHolder);
++        PlatformHooks.get().onChunkHolderCreate(this.world, ret.vanillaChunkHolder);
 +
 +        return ret;
 +    }
@@ -7821,7 +7827,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    private void removeChunkHolder(final NewChunkHolder holder) {
 +        holder.onUnload();
 +        this.autoSaveQueue.remove(holder);
-+        ChunkSystem.onChunkHolderDelete(this.world, holder.vanillaChunkHolder);
++        PlatformHooks.get().onChunkHolderDelete(this.world, holder.vanillaChunkHolder);
 +        this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ));
 +    }
 +
@@ -8224,12 +8230,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return ret;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..67532b85073b7978254a0b04caadfe822679e61f
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java
+@@ -0,0 +1,1055 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
 +
 +import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
@@ -9285,12 +9291,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
-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
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..e4a5fa25ed368fc4662c30934da2963ef446d782
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java
+@@ -0,0 +1,1997 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
 +
 +import ca.spottedleaf.concurrentutil.completable.CallbackCompletable;
@@ -9304,7 +9310,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
 +import ca.spottedleaf.moonrise.common.util.TickThread;
 +import ca.spottedleaf.moonrise.common.util.WorldUtil;
-+import ca.spottedleaf.moonrise.common.util.ChunkSystem;
 +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData;
 +import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
 +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel;
@@ -10563,10 +10568,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                    // state upgrade
 +                    if (!current.isOrAfter(FullChunkStatus.FULL) && pending.isOrAfter(FullChunkStatus.FULL)) {
 +                        this.updateCurrentState(FullChunkStatus.FULL);
-+                        ChunkSystem.onChunkPreBorder(chunk, this.vanillaChunkHolder);
++                        PlatformHooks.get().onChunkPreBorder(chunk, this.vanillaChunkHolder);
 +                        this.scheduler.chunkHolderManager.ensureInAutosave(this);
 +                        this.changeEntityChunkStatus(FullChunkStatus.FULL);
-+                        ChunkSystem.onChunkBorder(chunk, this.vanillaChunkHolder);
++                        PlatformHooks.get().onChunkBorder(chunk, this.vanillaChunkHolder);
 +                        this.onFullChunkLoadChange(true, changedFullStatus);
 +                        this.completeFullStatusConsumers(FullChunkStatus.FULL, chunk);
 +                    }
@@ -10574,34 +10579,34 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                    if (!current.isOrAfter(FullChunkStatus.BLOCK_TICKING) && pending.isOrAfter(FullChunkStatus.BLOCK_TICKING)) {
 +                        this.updateCurrentState(FullChunkStatus.BLOCK_TICKING);
 +                        this.changeEntityChunkStatus(FullChunkStatus.BLOCK_TICKING);
-+                        ChunkSystem.onChunkTicking(chunk, this.vanillaChunkHolder);
++                        PlatformHooks.get().onChunkTicking(chunk, this.vanillaChunkHolder);
 +                        this.completeFullStatusConsumers(FullChunkStatus.BLOCK_TICKING, chunk);
 +                    }
 +
 +                    if (!current.isOrAfter(FullChunkStatus.ENTITY_TICKING) && pending.isOrAfter(FullChunkStatus.ENTITY_TICKING)) {
 +                        this.updateCurrentState(FullChunkStatus.ENTITY_TICKING);
 +                        this.changeEntityChunkStatus(FullChunkStatus.ENTITY_TICKING);
-+                        ChunkSystem.onChunkEntityTicking(chunk, this.vanillaChunkHolder);
++                        PlatformHooks.get().onChunkEntityTicking(chunk, this.vanillaChunkHolder);
 +                        this.completeFullStatusConsumers(FullChunkStatus.ENTITY_TICKING, chunk);
 +                    }
 +                } else {
 +                    if (current.isOrAfter(FullChunkStatus.ENTITY_TICKING) && !pending.isOrAfter(FullChunkStatus.ENTITY_TICKING)) {
 +                        this.changeEntityChunkStatus(FullChunkStatus.BLOCK_TICKING);
-+                        ChunkSystem.onChunkNotEntityTicking(chunk, this.vanillaChunkHolder);
++                        PlatformHooks.get().onChunkNotEntityTicking(chunk, this.vanillaChunkHolder);
 +                        this.updateCurrentState(FullChunkStatus.BLOCK_TICKING);
 +                    }
 +
 +                    if (current.isOrAfter(FullChunkStatus.BLOCK_TICKING) && !pending.isOrAfter(FullChunkStatus.BLOCK_TICKING)) {
 +                        this.changeEntityChunkStatus(FullChunkStatus.FULL);
-+                        ChunkSystem.onChunkNotTicking(chunk, this.vanillaChunkHolder);
++                        PlatformHooks.get().onChunkNotTicking(chunk, this.vanillaChunkHolder);
 +                        this.updateCurrentState(FullChunkStatus.FULL);
 +                    }
 +
 +                    if (current.isOrAfter(FullChunkStatus.FULL) && !pending.isOrAfter(FullChunkStatus.FULL)) {
 +                        this.onFullChunkLoadChange(false, changedFullStatus);
 +                        this.changeEntityChunkStatus(FullChunkStatus.INACCESSIBLE);
-+                        ChunkSystem.onChunkNotBorder(chunk, this.vanillaChunkHolder);
-+                        ChunkSystem.onChunkPostNotBorder(chunk, this.vanillaChunkHolder);
++                        PlatformHooks.get().onChunkNotBorder(chunk, this.vanillaChunkHolder);
++                        PlatformHooks.get().onChunkPostNotBorder(chunk, this.vanillaChunkHolder);
 +                        this.updateCurrentState(FullChunkStatus.INACCESSIBLE);
 +                    }
 +                }
@@ -11289,12 +11294,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return ret;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..6b468c621b74449a6218391f6477cf63cfc98c7c
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java
+@@ -0,0 +1,215 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
 +
 +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
@@ -11510,12 +11515,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    protected abstract void raisePriorityScheduled(final Priority priority);
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..310a8f80debadd64c2d962ebf83b7d0505ce6e42
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java
+@@ -0,0 +1,1457 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
 +
 +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
@@ -12973,12 +12978,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +     */
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..5f4b99d8c5453f8ad2e600a57ea4e7dafa2d45f8
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java
+@@ -0,0 +1,729 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.executor;
 +
 +import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
@@ -13708,12 +13713,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..6ab353b0d2465c3680bb3c8d0852ba0f65c00fd2
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java
+@@ -0,0 +1,151 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
 +
 +import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
@@ -13865,12 +13870,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        this.convertToFullTask.raisePriority(priority);
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..4538ccfaea83d217ed85eaf16e82393c7f286489
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java
+@@ -0,0 +1,181 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
 +
 +import ca.spottedleaf.concurrentutil.util.Priority;
@@ -14052,12 +14057,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..1440c9e2b106616884edcb20201113320817ed9f
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java
+@@ -0,0 +1,494 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
 +
 +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
@@ -14552,12 +14557,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..002ee365aa70d8e6a6e6bd5c95988bd17db4395a
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java
+@@ -0,0 +1,101 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
 +
 +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
@@ -14660,12 +14665,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +}
 \ No newline at end of file
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..25d8da4773dcee5096053e7e3788bfc224d705a7
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java
+@@ -0,0 +1,218 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
 +
 +import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
@@ -14884,12 +14889,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        this.generateTask.raisePriority(priority);
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..bdcd1879457bafcca4e76523aac0555968f37c0b
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java
+@@ -0,0 +1,674 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
 +
 +import ca.spottedleaf.concurrentutil.completable.CallbackCompletable;
@@ -15564,12 +15569,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java b/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..cb6af3712bf9f6f6b8f7a459c309c75dabe83a50
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java
+@@ -0,0 +1,9 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.server;
 +
 +public interface ChunkSystemMinecraftServer {
@@ -15579,12 +15584,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public void moonrise$executeMidTickTasks();
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java b/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..ea759ce6f10f2a5a4e107ab7528030fe931ba223
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java
+@@ -0,0 +1,9 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.status;
 +
 +import net.minecraft.world.level.chunk.status.ChunkStatus;
@@ -15594,12 +15599,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public ChunkStatus moonrise$getRequiredStatusAtRadius(final int radius);
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..51c126735ace8fdde89ad97b5cab62f244212db0
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java
+@@ -0,0 +1,12 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.storage;
 +
 +import net.minecraft.world.level.chunk.storage.RegionFile;
@@ -15612,12 +15617,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    public void moonrise$write(final RegionFile regionFile) throws IOException;
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..129a35ff2db5b3bb6736810fc180796ce55e1875
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java
+@@ -0,0 +1,9 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.storage;
 +
 +import net.minecraft.world.level.chunk.storage.RegionFileStorage;
@@ -15627,12 +15632,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public RegionFileStorage moonrise$getRegionStorage();
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemRegionFile.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemRegionFile.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemRegionFile.java b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemRegionFile.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..3bd1b59250dbab15097a64d515999b278636795a
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemRegionFile.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemRegionFile.java
+@@ -0,0 +1,12 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.storage;
 +
 +import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
@@ -15645,12 +15650,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite(final CompoundTag data, final ChunkPos pos) throws IOException;
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java b/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..786e6ad17cd6216ef0aadaa7cf10044a0c19c933
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java
+@@ -0,0 +1,9 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.ticket;
 +
 +public interface ChunkSystemTicket<T> {
@@ -15660,12 +15665,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public void moonrise$setRemoveDelay(final long removeDelay);
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java b/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..2add7fd15a2210286aeb9af5024263333340d34c
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java
+@@ -0,0 +1,9 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.ticks;
 +
 +public interface ChunkSystemLevelChunkTicks {
@@ -15675,12 +15680,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public void moonrise$clearDirty();
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java b/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..ce3bb903c9ccb7efa0f004cf79b291dcb1cb7a23
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java
+@@ -0,0 +1,15 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.util;
 +
 +import net.minecraft.util.SortedArraySet;
@@ -15696,12 +15701,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public T moonrise$removeAndGet(final T object);
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java b/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..93fd23027c00cef76562098306737272fda1350a
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java
+@@ -0,0 +1,321 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.util;
 +
 +import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
@@ -16023,12 +16028,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return ret.elements();
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/stream/ExternalChunkStreamMarker.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/stream/ExternalChunkStreamMarker.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/util/stream/ExternalChunkStreamMarker.java b/ca/spottedleaf/moonrise/patches/chunk_system/util/stream/ExternalChunkStreamMarker.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..7ef3dcca89ed7578c6c0f5565131889110063056
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/stream/ExternalChunkStreamMarker.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/util/stream/ExternalChunkStreamMarker.java
+@@ -0,0 +1,37 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.util.stream;
 +
 +import java.io.DataInputStream;
@@ -16066,12 +16071,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        super(getWrapped(in));
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java b/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..ea6b6ed27b212719feb31610faac974899688839
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java
+@@ -0,0 +1,12 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.world;
 +
 +import net.minecraft.world.entity.Entity;
@@ -16084,12 +16089,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public List<Entity> moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate<? super Entity> predicate);
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemServerChunkCache.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemServerChunkCache.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemServerChunkCache.java b/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemServerChunkCache.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..4b9e2fa963c14f65f15407c1814c543c2999ea32
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemServerChunkCache.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemServerChunkCache.java
+@@ -0,0 +1,11 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.world;
 +
 +import net.minecraft.world.level.chunk.LevelChunk;
@@ -16101,12 +16106,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ);
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickConstants.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickConstants.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickConstants.java b/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickConstants.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..e97e7d276faf055c89207385d3820debffb06463
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickConstants.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickConstants.java
+@@ -0,0 +1,7 @@
 +package ca.spottedleaf.moonrise.patches.chunk_tick_iteration;
 +
 +public final class ChunkTickConstants {
@@ -16114,12 +16119,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public static final int PLAYER_SPAWN_TRACK_RANGE = 8;
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickDistanceManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickDistanceManager.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickDistanceManager.java b/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickDistanceManager.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f28fd0e01e2bdda0daf9d775e514a7253d32d8d0
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickDistanceManager.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickDistanceManager.java
+@@ -0,0 +1,16 @@
 +package ca.spottedleaf.moonrise.patches.chunk_tick_iteration;
 +
 +import net.minecraft.core.SectionPos;
@@ -16136,12 +16141,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                                      final boolean oldIgnore, final boolean newIgnore);
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickServerLevel.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickServerLevel.java
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickServerLevel.java b/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickServerLevel.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..6af03fd7807d4c71dbf85028d18dc850978ef429
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickServerLevel.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickServerLevel.java
+@@ -0,0 +1,19 @@
 +package ca.spottedleaf.moonrise.patches.chunk_tick_iteration;
 +
 +import ca.spottedleaf.moonrise.common.list.ReferenceList;
@@ -16161,12 +16166,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public void moonrise$removePlayerTickingRequest(final int chunkX, final int chunkZ);
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
+diff --git a/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..e04bd54744335fb5398c6e4f7ce8b981f35bfb7d
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
+@@ -0,0 +1,2183 @@
 +package ca.spottedleaf.moonrise.patches.collisions;
 +
 +import ca.spottedleaf.moonrise.common.util.WorldUtil;
@@ -18350,12 +18355,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        throw new RuntimeException();
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java
+diff --git a/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java b/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..35c8aaf0bfa42717f45eed1d1072e1614874de91
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java
+@@ -0,0 +1,28 @@
 +package ca.spottedleaf.moonrise.patches.collisions;
 +
 +import net.minecraft.core.BlockPos;
@@ -18384,12 +18389,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        this.outOfWorld = outOfWorld;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java
+diff --git a/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java b/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..a38ab583200ebf68ca68fdddf2d12077720b72b7
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java
+@@ -0,0 +1,29 @@
 +package ca.spottedleaf.moonrise.patches.collisions.block;
 +
 +import net.minecraft.world.phys.shapes.VoxelShape;
@@ -18419,12 +18424,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    public VoxelShape moonrise$getConstantContextCollisionShape();
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedShapeData.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedShapeData.java
+diff --git a/ca/spottedleaf/moonrise/patches/collisions/shape/CachedShapeData.java b/ca/spottedleaf/moonrise/patches/collisions/shape/CachedShapeData.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..5a6b16be4b8c0cc92d017bc592bc4818dba17da7
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedShapeData.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/collisions/shape/CachedShapeData.java
+@@ -0,0 +1,10 @@
 +package ca.spottedleaf.moonrise.patches.collisions.shape;
 +
 +public record CachedShapeData(
@@ -18435,12 +18440,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        boolean isEmpty, boolean hasSingleAABB
 +) {
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java
+diff --git a/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java b/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..9d33ead3a97d86b371e4d9ad9fed80d789bed844
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java
+@@ -0,0 +1,39 @@
 +package ca.spottedleaf.moonrise.patches.collisions.shape;
 +
 +import net.minecraft.world.phys.AABB;
@@ -18480,12 +18485,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return new CachedToAABBs(cache.aabbs, true, resX, resY, resZ);
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java
+diff --git a/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java b/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..07fe5e02c2d0a27d2fe37bb45761654dc2d02e5d
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java
+@@ -0,0 +1,7 @@
 +package ca.spottedleaf.moonrise.patches.collisions.shape;
 +
 +public interface CollisionDiscreteVoxelShape {
@@ -18493,12 +18498,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public CachedShapeData moonrise$getOrCreateCachedShapeData();
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java
+diff --git a/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java b/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..05d7b3f9d8659c259f3ed0537c57e6e43eb6e288
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java
+@@ -0,0 +1,40 @@
 +package ca.spottedleaf.moonrise.patches.collisions.shape;
 +
 +import net.minecraft.core.Direction;
@@ -18539,12 +18544,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    // uses a cache internally
 +    public VoxelShape moonrise$orUnoptimized(final VoxelShape other);
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java
+diff --git a/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java b/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..44831fc18efb7534dc6e4822f3c9b5cdc4dcc33e
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java
+@@ -0,0 +1,10 @@
 +package ca.spottedleaf.moonrise.patches.collisions.shape;
 +
 +import net.minecraft.world.phys.shapes.VoxelShape;
@@ -18555,12 +18560,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +) {
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/CollisionDirection.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/CollisionDirection.java
+diff --git a/ca/spottedleaf/moonrise/patches/collisions/util/CollisionDirection.java b/ca/spottedleaf/moonrise/patches/collisions/util/CollisionDirection.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f62359e5d6aa9a9cdb015441dbdb6182dc302f02
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/CollisionDirection.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/collisions/util/CollisionDirection.java
+@@ -0,0 +1,9 @@
 +package ca.spottedleaf.moonrise.patches.collisions.util;
 +
 +public interface CollisionDirection {
@@ -18570,12 +18575,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public int moonrise$uniqueId();
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java
+diff --git a/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java b/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..cf9ffdeff6bf0b62a45f7a44dbfe0dd7d17dc4f4
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java
+@@ -0,0 +1,7 @@
 +package ca.spottedleaf.moonrise.patches.collisions.util;
 +
 +import net.minecraft.core.Direction;
@@ -18583,12 +18588,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +public record FluidOcclusionCacheKey(BlockState first, BlockState second, Direction direction, boolean result) {
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerEntity.java b/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerEntity.java
+diff --git a/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerEntity.java b/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerEntity.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..5f5734c00ce8245a1ff69b2d4c3036579d5392e0
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerEntity.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerEntity.java
+@@ -0,0 +1,11 @@
 +package ca.spottedleaf.moonrise.patches.entity_tracker;
 +
 +import net.minecraft.server.level.ChunkMap;
@@ -18600,12 +18605,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public void moonrise$setTrackedEntity(final ChunkMap.TrackedEntity trackedEntity);
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java b/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java
+diff --git a/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java b/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..8e7472157a98de607c03769a91f64c8369fd3ea6
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java
+@@ -0,0 +1,15 @@
 +package ca.spottedleaf.moonrise.patches.entity_tracker;
 +
 +import ca.spottedleaf.moonrise.common.misc.NearbyPlayers;
@@ -18621,12 +18626,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public boolean moonrise$hasPlayers();
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java b/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java
+diff --git a/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java b/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..4a7abd239a9c59aa98947e7993962d75e9051902
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java
+@@ -0,0 +1,9 @@
 +package ca.spottedleaf.moonrise.patches.fast_palette;
 +
 +public interface FastPalette<T> {
@@ -18636,12 +18641,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java b/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java
+diff --git a/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java b/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..4503f3495846a7d7ed082b9e24636044e4fbccd1
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java
+@@ -0,0 +1,9 @@
 +package ca.spottedleaf.moonrise.patches.fast_palette;
 +
 +public interface FastPaletteData<T> {
@@ -18651,23 +18656,23 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public void moonrise$setPalette(final T[] palette);
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/fluid/FluidFluidState.java b/src/main/java/ca/spottedleaf/moonrise/patches/fluid/FluidFluidState.java
+diff --git a/ca/spottedleaf/moonrise/patches/fluid/FluidFluidState.java b/ca/spottedleaf/moonrise/patches/fluid/FluidFluidState.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..107c97089354edd35f330582f5e0c8a18e792a6e
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/fluid/FluidFluidState.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/fluid/FluidFluidState.java
+@@ -0,0 +1,5 @@
 +package ca.spottedleaf.moonrise.patches.fluid;
 +
 +public interface FluidFluidState {
 +    public void moonrise$initCaches();
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java
+diff --git a/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java b/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..540c14a6d2c216cd3ef2a9c4056e15712bf8cb8c
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java
+@@ -0,0 +1,9 @@
 +package ca.spottedleaf.moonrise.patches.getblock;
 +
 +import net.minecraft.world.level.block.state.BlockState;
@@ -18677,12 +18682,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public BlockState moonrise$getBlock(final int x, final int y, final int z);
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java
+diff --git a/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java b/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..8e6d79b7c10ef25f5478b72c53c555423d615a2f
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java
+@@ -0,0 +1,7 @@
 +package ca.spottedleaf.moonrise.patches.starlight.blockstate;
 +
 +public interface StarlightAbstractBlockState {
@@ -18690,12 +18695,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public boolean starlight$isConditionallyFullOpaque();
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java
+diff --git a/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java b/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..ed80017c8f257b981d626a37ffc5480d9b326558
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java
+@@ -0,0 +1,18 @@
 +package ca.spottedleaf.moonrise.patches.starlight.chunk;
 +
 +import ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray;
@@ -18714,12 +18719,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public boolean[] starlight$getBlockEmptinessMap();
 +    public void starlight$setBlockEmptinessMap(final boolean[] emptinessMap);
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java
+diff --git a/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java b/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..fa7b784a89626e8528c249d7889a598bd7ee3d49
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java
+@@ -0,0 +1,280 @@
 +package ca.spottedleaf.moonrise.patches.starlight.light;
 +
 +import ca.spottedleaf.moonrise.common.PlatformHooks;
@@ -19000,12 +19005,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java
+diff --git a/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java b/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..4ca68a903e67606fc4ef0bfa9862a73797121c8b
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java
+@@ -0,0 +1,440 @@
 +package ca.spottedleaf.moonrise.patches.starlight.light;
 +
 +import net.minecraft.world.level.chunk.DataLayer;
@@ -19446,12 +19451,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java
+diff --git a/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java b/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f9aef289e9a2d6f63c98c72c56ef32b8793f57f4
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java
+@@ -0,0 +1,681 @@
 +package ca.spottedleaf.moonrise.patches.starlight.light;
 +
 +import ca.spottedleaf.moonrise.common.util.WorldUtil;
@@ -20133,12 +20138,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return startY;
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java
+diff --git a/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java b/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..8aeb5fb87f94a35659347a09a638420699b52a6f
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java
+@@ -0,0 +1,1438 @@
 +package ca.spottedleaf.moonrise.patches.starlight.light;
 +
 +import ca.spottedleaf.concurrentutil.util.IntegerUtil;
@@ -21577,12 +21582,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        this.performLightIncrease(lightAccess);
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java
+diff --git a/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java b/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..571db5f9bf94745a8afe2cd313e593fb15db5e37
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java
+@@ -0,0 +1,931 @@
 +package ca.spottedleaf.moonrise.patches.starlight.light;
 +
 +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
@@ -22514,12 +22519,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java
+diff --git a/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java b/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..7fe59ab70557aa6a484a02db2b2007fdd9e4bbb8
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java
+@@ -0,0 +1,29 @@
 +package ca.spottedleaf.moonrise.patches.starlight.light;
 +
 +import net.minecraft.core.SectionPos;
@@ -22549,12 +22554,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/storage/StarlightSectionData.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/storage/StarlightSectionData.java
+diff --git a/ca/spottedleaf/moonrise/patches/starlight/storage/StarlightSectionData.java b/ca/spottedleaf/moonrise/patches/starlight/storage/StarlightSectionData.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..40d004afdc6449530f5bb2d7c7638b8ee3e3a577
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/storage/StarlightSectionData.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/starlight/storage/StarlightSectionData.java
+@@ -0,0 +1,13 @@
 +package ca.spottedleaf.moonrise.patches.starlight.storage;
 +
 +public interface StarlightSectionData {
@@ -22568,12 +22573,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public void starlight$setSkyLightState(final int state);
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java
+diff --git a/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java b/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..689ce367164e79e0426eeecb81dbbc521d4bc742
 --- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java
-@@ -0,0 +0,0 @@
++++ b/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java
+@@ -0,0 +1,189 @@
 +package ca.spottedleaf.moonrise.patches.starlight.util;
 +
 +import ca.spottedleaf.moonrise.common.util.WorldUtil;
@@ -22763,20 +22768,21 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private SaveUtil() {}
 +}
-diff --git a/src/main/java/io/papermc/paper/FeatureHooks.java b/src/main/java/io/papermc/paper/FeatureHooks.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/io/papermc/paper/FeatureHooks.java
-+++ b/src/main/java/io/papermc/paper/FeatureHooks.java
-@@ -0,0 +0,0 @@
+diff --git a/io/papermc/paper/FeatureHooks.java b/io/papermc/paper/FeatureHooks.java
+index 184e6c6fe2ba522d0ea0774604839320c4152371..460bb584db04b582f3297ae419183f430aff1ec0 100644
+--- a/io/papermc/paper/FeatureHooks.java
++++ b/io/papermc/paper/FeatureHooks.java
+@@ -1,6 +1,9 @@
  package io.papermc.paper;
  
  import io.papermc.paper.command.PaperSubcommand;
 +import io.papermc.paper.command.subcommands.ChunkDebugCommand;
 +import io.papermc.paper.command.subcommands.FixLightCommand;
++import it.unimi.dsi.fastutil.longs.LongIterator;
  import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
  import it.unimi.dsi.fastutil.longs.LongSet;
  import it.unimi.dsi.fastutil.longs.LongSets;
-@@ -0,0 +0,0 @@ import org.bukkit.World;
+@@ -31,9 +34,12 @@ import org.bukkit.World;
  public final class FeatureHooks {
  
      public static void initChunkTaskScheduler(final boolean useParallelGen) {
@@ -22789,12 +22795,194 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      public static LevelChunkSection createSection(final Registry<Biome> biomeRegistry, final Level level, final ChunkPos chunkPos, final int chunkSection) {
-diff --git a/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java b/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java
+@@ -59,18 +65,19 @@ public final class FeatureHooks {
+     }
+ 
+     public static Set<Long> getSentChunkKeys(final ServerPlayer player) {
+-        final LongSet keys = new LongOpenHashSet();
+-        player.getChunkTrackingView().forEach(pos -> keys.add(pos.longKey));
+-        return LongSets.unmodifiable(keys);
++        return LongSets.unmodifiable(player.moonrise$getChunkLoader().getSentChunksRaw().clone()); // Paper - rewrite chunk system
+     }
+ 
+     public static Set<Chunk> getSentChunks(final ServerPlayer player) {
+-        final ObjectSet<Chunk> chunks = new ObjectOpenHashSet<>();
++        // Paper start - rewrite chunk system
++        final LongOpenHashSet rawChunkKeys = player.moonrise$getChunkLoader().getSentChunksRaw();
++        final ObjectSet<org.bukkit.Chunk> chunks = new ObjectOpenHashSet<>(rawChunkKeys.size());
+         final World world = player.serverLevel().getWorld();
+-        player.getChunkTrackingView().forEach(pos -> {
+-            final org.bukkit.Chunk chunk = world.getChunkAt(pos.longKey);
+-            chunks.add(chunk);
+-        });
++        final LongIterator iter = player.moonrise$getChunkLoader().getSentChunksRaw().longIterator();
++        while (iter.hasNext()) {
++            chunks.add(world.getChunkAt(iter.nextLong(), false));
++        }
++        // Paper end - rewrite chunk system
+         return ObjectSets.unmodifiable(chunks);
+     }
+ 
+@@ -79,89 +86,30 @@ public final class FeatureHooks {
+     }
+ 
+     public static boolean isSpiderCollidingWithWorldBorder(final Spider spider) {
+-        return true; // ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isCollidingWithBorder(spider.level().getWorldBorder(), spider.getBoundingBox().inflate(ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON))
++        return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isCollidingWithBorder(spider.level().getWorldBorder(), spider.getBoundingBox().inflate(ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)); // Paper - rewrite collision system
+     }
+ 
+     public static void dumpAllChunkLoadInfo(net.minecraft.server.MinecraftServer server, boolean isLongTimeout) {
++        ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(server, isLongTimeout); // Paper - rewrite chunk system
+     }
+ 
+     private static void dumpEntity(final Entity entity) {
+     }
+ 
+     public static org.bukkit.entity.Entity[] getChunkEntities(net.minecraft.server.level.ServerLevel world, int chunkX, int chunkZ) {
+-        world.getChunk(chunkX, chunkZ); // ensure full loaded
+-
+-        net.minecraft.world.level.entity.PersistentEntitySectionManager<net.minecraft.world.entity.Entity> entityManager = world.entityManager;
+-        long pair = ChunkPos.asLong(chunkX, chunkZ);
+-
+-        if (entityManager.areEntitiesLoaded(pair)) {
+-            return entityManager.getEntities(new ChunkPos(chunkX, chunkZ)).stream()
+-                .map(net.minecraft.world.entity.Entity::getBukkitEntity)
+-                .filter(java.util.Objects::nonNull).toArray(org.bukkit.entity.Entity[]::new);
+-        }
+-
+-        entityManager.ensureChunkQueuedForLoad(pair); // Start entity loading
+-
+-        // SPIGOT-6772: Use entity mailbox and re-schedule entities if they get unloaded
+-        net.minecraft.util.thread.ConsecutiveExecutor mailbox = ((net.minecraft.world.level.chunk.storage.EntityStorage) entityManager.permanentStorage).entityDeserializerQueue;
+-        java.util.function.BooleanSupplier supplier = () -> {
+-            // only execute inbox if our entities are not present
+-            if (entityManager.areEntitiesLoaded(pair)) {
+-                return true;
+-            }
+-
+-            if (!entityManager.isPending(pair)) {
+-                // Our entities got unloaded, this should normally not happen.
+-                entityManager.ensureChunkQueuedForLoad(pair); // Re-start entity loading
+-            }
+-
+-            // tick loading inbox, which loads the created entities to the world
+-            // (if present)
+-            entityManager.tick();
+-            // check if our entities are loaded
+-            return entityManager.areEntitiesLoaded(pair);
+-        };
+-
+-        // now we wait until the entities are loaded,
+-        // the converting from NBT to entity object is done on the main Thread which is why we wait
+-        while (!supplier.getAsBoolean()) {
+-            if (mailbox.size() != 0) {
+-                mailbox.run();
+-            } else {
+-                Thread.yield();
+-                java.util.concurrent.locks.LockSupport.parkNanos("waiting for entity loading", 100000L);
+-            }
+-        }
+-
+-        return entityManager.getEntities(new ChunkPos(chunkX, chunkZ)).stream()
+-            .map(net.minecraft.world.entity.Entity::getBukkitEntity)
+-            .filter(java.util.Objects::nonNull).toArray(org.bukkit.entity.Entity[]::new);
++        return world.getChunkEntities(chunkX, chunkZ); // Paper - rewrite chunk system
+     }
+ 
+     public static java.util.Collection<org.bukkit.plugin.Plugin> getPluginChunkTickets(net.minecraft.server.level.ServerLevel world,
+                                                                                        int x, int z) {
+-        net.minecraft.server.level.DistanceManager chunkDistanceManager = world.getChunkSource().chunkMap.distanceManager;
+-        net.minecraft.util.SortedArraySet<net.minecraft.server.level.Ticket<?>> tickets = chunkDistanceManager.tickets.get(ChunkPos.asLong(x, z));
+-
+-        if (tickets == null) {
+-            return java.util.Collections.emptyList();
+-        }
+-
+-        com.google.common.collect.ImmutableList.Builder<org.bukkit.plugin.Plugin> ret = com.google.common.collect.ImmutableList.builder();
+-        for (net.minecraft.server.level.Ticket<?> ticket : tickets) {
+-            if (ticket.getType() == net.minecraft.server.level.TicketType.PLUGIN_TICKET) {
+-                ret.add((org.bukkit.plugin.Plugin) ticket.key);
+-            }
+-        }
+-
+-        return ret.build();
++        return world.moonrise$getChunkTaskScheduler().chunkHolderManager.getPluginChunkTickets(x, z); // Paper - rewrite chunk system
+     }
+ 
+     public static Map<org.bukkit.plugin.Plugin, java.util.Collection<org.bukkit.Chunk>> getPluginChunkTickets(net.minecraft.server.level.ServerLevel world) {
+         Map<org.bukkit.plugin.Plugin, com.google.common.collect.ImmutableList.Builder<Chunk>> ret = new HashMap<>();
+         net.minecraft.server.level.DistanceManager chunkDistanceManager = world.getChunkSource().chunkMap.distanceManager;
+ 
+-        for (it.unimi.dsi.fastutil.longs.Long2ObjectMap.Entry<net.minecraft.util.SortedArraySet<net.minecraft.server.level.Ticket<?>>> chunkTickets : chunkDistanceManager.tickets.long2ObjectEntrySet()) {
++        for (it.unimi.dsi.fastutil.longs.Long2ObjectMap.Entry<net.minecraft.util.SortedArraySet<net.minecraft.server.level.Ticket<?>>> chunkTickets : chunkDistanceManager.moonrise$getChunkHolderManager().getTicketsCopy().long2ObjectEntrySet()) { // Paper - rewrite chunk system
+             long chunkKey = chunkTickets.getLongKey();
+             net.minecraft.util.SortedArraySet<net.minecraft.server.level.Ticket<?>> tickets = chunkTickets.getValue();
+ 
+@@ -183,15 +131,15 @@ public final class FeatureHooks {
+     }
+ 
+     public static int getViewDistance(net.minecraft.server.level.ServerLevel world) {
+-        return world.getChunkSource().chunkMap.serverViewDistance;
++        return world.moonrise$getPlayerChunkLoader().getAPIViewDistance(); // Paper - rewrite chunk system
+     }
+ 
+     public static int getSimulationDistance(net.minecraft.server.level.ServerLevel world) {
+-        return world.getChunkSource().chunkMap.getDistanceManager().simulationDistance;
++        return world.moonrise$getPlayerChunkLoader().getAPITickDistance(); // Paper - rewrite chunk system
+     }
+ 
+     public static int getSendViewDistance(net.minecraft.server.level.ServerLevel world) {
+-        return getViewDistance(world);
++        return world.moonrise$getPlayerChunkLoader().getAPISendViewDistance(); // Paper - rewrite chunk system
+     }
+ 
+     public static void setViewDistance(net.minecraft.server.level.ServerLevel world, int distance) {
+@@ -209,31 +157,31 @@ public final class FeatureHooks {
+     }
+ 
+     public static void setSendViewDistance(net.minecraft.server.level.ServerLevel world, int distance) {
+-        throw new UnsupportedOperationException("Not implemented yet");
++        world.chunkSource.setSendViewDistance(distance); // Paper - rewrite chunk system
+     }
+ 
+     public static void tickEntityManager(net.minecraft.server.level.ServerLevel world) {
+-        world.entityManager.tick();
++        // Paper - rewrite chunk system
+     }
+ 
+     public static void closeEntityManager(net.minecraft.server.level.ServerLevel world, boolean save) {
+-        world.entityManager.close(save);
++        // Paper - rewrite chunk system
+     }
+ 
+     public static java.util.concurrent.Executor getWorldgenExecutor() {
+-        return net.minecraft.Util.backgroundExecutor();
++        return Runnable::run; // Paper - rewrite chunk system
+     }
+ 
+     public static void setViewDistance(ServerPlayer player, int distance) {
+-        throw new UnsupportedOperationException("Not implemented yet");
++        ((ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer)player).moonrise$getViewDistanceHolder().setLoadViewDistance(distance == -1 ? distance : distance + 1); // Paper - rewrite chunk system
+     }
+ 
+     public static void setSimulationDistance(ServerPlayer player, int distance) {
+-        throw new UnsupportedOperationException("Not implemented yet");
++        ((ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer)player).moonrise$getViewDistanceHolder().setTickViewDistance(distance); // Paper - rewrite chunk system
+     }
+ 
+     public static void setSendViewDistance(ServerPlayer player, int distance) {
+-        throw new UnsupportedOperationException("Not implemented yet");
++        ((ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer)player).moonrise$getViewDistanceHolder().setSendViewDistance(distance); // Paper - rewrite chunk system
+     }
+ 
+ }
+\ No newline at end of file
+diff --git a/io/papermc/paper/command/subcommands/ChunkDebugCommand.java b/io/papermc/paper/command/subcommands/ChunkDebugCommand.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..2dca7afbd93cfbb8686f336fcd3b45dd01fba0fc
 --- /dev/null
-+++ b/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java
-@@ -0,0 +0,0 @@
++++ b/io/papermc/paper/command/subcommands/ChunkDebugCommand.java
+@@ -0,0 +1,277 @@
 +package io.papermc.paper.command.subcommands;
 +
 +import ca.spottedleaf.moonrise.common.util.JsonUtil;
@@ -23072,12 +23260,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +}
-diff --git a/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java b/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java
+diff --git a/io/papermc/paper/command/subcommands/FixLightCommand.java b/io/papermc/paper/command/subcommands/FixLightCommand.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..85950a1aa732ab8c01ad28bec9e0de140e1a172e
 --- /dev/null
-+++ b/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java
-@@ -0,0 +0,0 @@
++++ b/io/papermc/paper/command/subcommands/FixLightCommand.java
+@@ -0,0 +1,116 @@
 +package io.papermc.paper.command.subcommands;
 +
 +import ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider;
@@ -23194,12 +23382,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        sender.getBukkitEntity().sendMessage(text().color(BLUE).append(text("Relighting "), text(pending[0], DARK_AQUA), text(" chunks")));
 +    }
 +}
-diff --git a/src/main/java/io/papermc/paper/threadedregions/TickRegions.java b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java
+diff --git a/io/papermc/paper/threadedregions/TickRegions.java b/io/papermc/paper/threadedregions/TickRegions.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..8424cf9d4617b4732d44cc460d25b04481068989
 --- /dev/null
-+++ b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java
-@@ -0,0 +0,0 @@
++++ b/io/papermc/paper/threadedregions/TickRegions.java
+@@ -0,0 +1,10 @@
 +package io.papermc.paper.threadedregions;
 +
 +// placeholder class for Folia
@@ -23210,11 +23398,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +}
-diff --git a/src/main/java/net/minecraft/core/Direction.java b/src/main/java/net/minecraft/core/Direction.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/core/Direction.java
-+++ b/src/main/java/net/minecraft/core/Direction.java
-@@ -0,0 +0,0 @@ import org.joml.Quaternionf;
+diff --git a/net/minecraft/core/Direction.java b/net/minecraft/core/Direction.java
+index 3d3eec1db91cb47395f40c4f47aa77164ad42175..216f97207dac88cc1dc3df59c6ee8a62c7614b4a 100644
+--- a/net/minecraft/core/Direction.java
++++ b/net/minecraft/core/Direction.java
+@@ -28,7 +28,7 @@ import org.joml.Quaternionf;
  import org.joml.Vector3f;
  import org.joml.Vector4f;
  
@@ -23223,7 +23411,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      DOWN(0, 1, -1, "down", Direction.AxisDirection.NEGATIVE, Direction.Axis.Y, new Vec3i(0, -1, 0)),
      UP(1, 0, -1, "up", Direction.AxisDirection.POSITIVE, Direction.Axis.Y, new Vec3i(0, 1, 0)),
      NORTH(2, 3, 2, "north", Direction.AxisDirection.NEGATIVE, Direction.Axis.Z, new Vec3i(0, 0, -1)),
-@@ -0,0 +0,0 @@ public enum Direction implements StringRepresentable {
+@@ -62,6 +62,46 @@ public enum Direction implements StringRepresentable {
      private final int adjY;
      private final int adjZ;
      // Paper end - Perf: Inline shift direction fields
@@ -23269,8 +23457,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    // Paper end - optimise collisions
  
      private Direction(
-         final int id,
-@@ -0,0 +0,0 @@ public enum Direction implements StringRepresentable {
+         final int data3d,
+@@ -147,14 +187,13 @@ public enum Direction implements StringRepresentable {
      }
  
      public Quaternionf getRotation() {
@@ -23292,7 +23480,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      public int get3DDataValue() {
-@@ -0,0 +0,0 @@ public enum Direction implements StringRepresentable {
+@@ -178,7 +217,7 @@ public enum Direction implements StringRepresentable {
      }
  
      public Direction getOpposite() {
@@ -23301,7 +23489,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      public Direction getClockWise(Direction.Axis axis) {
-@@ -0,0 +0,0 @@ public enum Direction implements StringRepresentable {
+@@ -600,4 +639,17 @@ public enum Direction implements StringRepresentable {
              return this.faces.length;
          }
      }
@@ -23319,11 +23507,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - optimise collisions
  }
-diff --git a/src/main/java/net/minecraft/core/MappedRegistry.java b/src/main/java/net/minecraft/core/MappedRegistry.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/core/MappedRegistry.java
-+++ b/src/main/java/net/minecraft/core/MappedRegistry.java
-@@ -0,0 +0,0 @@ public class MappedRegistry<T> implements WritableRegistry<T> {
+diff --git a/net/minecraft/core/MappedRegistry.java b/net/minecraft/core/MappedRegistry.java
+index 47b1fafd91b39e73c4e9134b0b8048000fba108a..76994c1491221c06cca5405ba239e6ff642b19ed 100644
+--- a/net/minecraft/core/MappedRegistry.java
++++ b/net/minecraft/core/MappedRegistry.java
+@@ -50,6 +50,19 @@ public class MappedRegistry<T> implements WritableRegistry<T> {
          return this.getTags();
      }
  
@@ -23340,53 +23528,44 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - fluid method optimisations
 +
-     public MappedRegistry(ResourceKey<? extends Registry<T>> key, Lifecycle lifecycle) {
-         this(key, lifecycle, false);
+     public MappedRegistry(ResourceKey<? extends Registry<T>> key, Lifecycle registryLifecycle) {
+         this(key, registryLifecycle, false);
      }
-@@ -0,0 +0,0 @@ public class MappedRegistry<T> implements WritableRegistry<T> {
-             this.toId.put(value, i);
-             this.registrationInfos.put(key, info);
-             this.registryLifecycle = this.registryLifecycle.add(info.lifecycle());
+@@ -114,6 +127,7 @@ public class MappedRegistry<T> implements WritableRegistry<T> {
+             this.toId.put(value, size);
+             this.registrationInfos.put(key, registrationInfo);
+             this.registryLifecycle = this.registryLifecycle.add(registrationInfo.lifecycle());
 +            this.injectFluidRegister(key, value); // Paper - fluid method optimisations
              return reference;
          }
      }
-diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/Main.java
-+++ b/src/main/java/net/minecraft/server/Main.java
-@@ -0,0 +0,0 @@ public class Main {
- 
-             convertable_conversionsession.saveDataTag(iregistrycustom_dimension, savedata);
+diff --git a/net/minecraft/server/Main.java b/net/minecraft/server/Main.java
+index 2b46ca9a2a046063cad422bec00d76107537b091..9aa664537cc37e44db46d5a2a64ae3116938c681 100644
+--- a/net/minecraft/server/Main.java
++++ b/net/minecraft/server/Main.java
+@@ -321,6 +321,7 @@ public class Main {
+             WorldData worldData = worldStem.worldData();
+             levelStorageAccess.saveDataTag(frozen, worldData);
              */
 +            Class.forName(net.minecraft.world.entity.npc.VillagerTrades.class.getName()); // Paper - load this sync so it won't fail later async
-             final DedicatedServer dedicatedserver = (DedicatedServer) MinecraftServer.spin((thread) -> {
-                 DedicatedServer dedicatedserver1 = new DedicatedServer(optionset, worldLoader.get(), thread, convertable_conversionsession, resourcepackrepository, worldstem, dedicatedserversettings, DataFixers.getDataFixer(), services, LoggerChunkProgressListener::createFromGameruleRadius);
- 
-diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/MinecraftServer.java
-+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
-@@ -0,0 +0,0 @@ import org.bukkit.event.server.ServerLoadEvent;
- // CraftBukkit end
- 
+             final DedicatedServer dedicatedServer = MinecraftServer.spin(
+                 thread1 -> {
+                     DedicatedServer dedicatedServer1 = new DedicatedServer(
+diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
+index b92a3da5c325e69f5601416d4205fb33429742b3..d967d605c2e4227ae980c30f1c8b86edbc680d6d 100644
+--- a/net/minecraft/server/MinecraftServer.java
++++ b/net/minecraft/server/MinecraftServer.java
+@@ -173,7 +173,7 @@ import net.minecraft.world.phys.Vec2;
+ import net.minecraft.world.phys.Vec3;
+ import org.slf4j.Logger;
  
 -public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTask> implements ServerInfo, ChunkIOErrorReporter, CommandSource {
 +public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTask> implements ServerInfo, ChunkIOErrorReporter, CommandSource, ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer { // Paper - rewrite chunk system
- 
      private static MinecraftServer SERVER; // Paper
      public static final Logger LOGGER = LogUtils.getLogger();
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
-     public static <S extends MinecraftServer> S spin(Function<Thread, S> serverFactory) {
-         ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.init(); // Paper - rewrite data converter system
-         AtomicReference<S> atomicreference = new AtomicReference();
--        Thread thread = new Thread(() -> {
-+        Thread thread = new ca.spottedleaf.moonrise.common.util.TickThread(() -> { // Paper - rewrite chunk system
-             ((MinecraftServer) atomicreference.get()).runServer();
-         }, "Server thread");
- 
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
-         return s0;
+     public static final net.kyori.adventure.text.logger.slf4j.ComponentLogger COMPONENT_LOGGER = net.kyori.adventure.text.logger.slf4j.ComponentLogger.logger(LOGGER.getName()); // Paper
+@@ -320,6 +320,77 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+         return minecraftServer;
      }
  
 +    // Paper start - rewrite chunk system
@@ -23460,99 +23639,97 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - rewrite chunk system
 +
-     public MinecraftServer(OptionSet options, WorldLoader.DataLoadContext worldLoader, Thread thread, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PackRepository resourcepackrepository, WorldStem worldstem, Proxy proxy, DataFixer datafixer, Services services, ChunkProgressListenerFactory worldloadlistenerfactory) {
-         super("Server");
-         SERVER = this; // Paper - better singleton
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+     public MinecraftServer(
+         // CraftBukkit start
+         joptsimple.OptionSet options,
+@@ -651,7 +722,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
          this.forceDifficulty();
-         for (ServerLevel worldserver : this.getAllLevels()) {
-             this.prepareLevels(worldserver.getChunkSource().chunkMap.progressListener, worldserver);
--            worldserver.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API
+         for (ServerLevel serverLevel : this.getAllLevels()) {
+             this.prepareLevels(serverLevel.getChunkSource().chunkMap.progressListener, serverLevel);
+-            serverLevel.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API
 +            // Paper - rewrite chunk system
-             this.server.getPluginManager().callEvent(new org.bukkit.event.world.WorldLoadEvent(worldserver.getWorld()));
+             this.server.getPluginManager().callEvent(new org.bukkit.event.world.WorldLoadEvent(serverLevel.getWorld()));
          }
  
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+@@ -850,6 +921,11 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
      public abstract boolean shouldRconBroadcast();
  
-     public boolean saveAllChunks(boolean suppressLogs, boolean flush, boolean force) {
+     public boolean saveAllChunks(boolean suppressLog, boolean flush, boolean forced) {
 +        // Paper start - add close param
-+        return this.saveAllChunks(suppressLogs, flush, force, false);
++        return this.saveAllChunks(suppressLog, flush, forced, false);
 +    }
-+    public boolean saveAllChunks(boolean suppressLogs, boolean flush, boolean force, boolean close) {
++    public boolean saveAllChunks(boolean suppressLog, boolean flush, boolean forced, boolean close) {
 +        // Paper end - add close param
-         boolean flag3 = false;
+         boolean flag = false;
  
-         for (Iterator iterator = this.getAllLevels().iterator(); iterator.hasNext(); flag3 = true) {
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
-                 MinecraftServer.LOGGER.info("Saving chunks for level '{}'/{}", worldserver, worldserver.dimension().location());
+         for (ServerLevel serverLevel : this.getAllLevels()) {
+@@ -857,7 +933,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+                 LOGGER.info("Saving chunks for level '{}'/{}", serverLevel, serverLevel.dimension().location());
              }
  
--            worldserver.save((ProgressListener) null, flush, worldserver.noSave && !force);
-+            worldserver.save((ProgressListener) null, flush, worldserver.noSave && !force, close); // Paper - add close param
+-            serverLevel.save(null, flush, serverLevel.noSave && !forced);
++            serverLevel.save(null, flush, serverLevel.noSave && !forced, close); // Paper - add close param
+             flag = true;
          }
  
-         // CraftBukkit start - moved to WorldServer.save
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+@@ -950,7 +1026,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
              }
          }
  
--        while (this.levels.values().stream().anyMatch((worldserver1) -> {
-+        while (false && this.levels.values().stream().anyMatch((worldserver1) -> { // Paper - rewrite chunk system
-             return worldserver1.getChunkSource().chunkMap.hasWork();
-         })) {
+-        while (this.levels.values().stream().anyMatch(level -> level.getChunkSource().chunkMap.hasWork())) {
++        while (false && this.levels.values().stream().anyMatch(level -> level.getChunkSource().chunkMap.hasWork())) { // Paper - rewrite chunk system
              this.nextTickTimeNanos = Util.getNanos() + TimeUtil.NANOSECONDS_PER_MILLISECOND;
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ 
+             for (ServerLevel serverLevelx : this.getAllLevels()) {
+@@ -961,17 +1037,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
              this.waitUntilNextTick();
          }
  
 -        this.saveAllChunks(false, true, false);
--        iterator = this.getAllLevels().iterator();
 -
--        while (iterator.hasNext()) {
--            worldserver = (ServerLevel) iterator.next();
--            if (worldserver != null) {
+-        for (ServerLevel serverLevelx : this.getAllLevels()) {
+-            if (serverLevelx != null) {
 -                try {
--                    worldserver.close();
--                } catch (IOException ioexception) {
--                    MinecraftServer.LOGGER.error("Exception closing the level", ioexception);
+-                    serverLevelx.close();
+-                } catch (IOException var5) {
+-                    LOGGER.error("Exception closing the level", (Throwable)var5);
 -                }
 -            }
 -        }
-+        this.saveAllChunks(false, true, true, true); // Paper - rewrite chunk system
++        this.saveAllChunks(false, true, false, true); // Paper - rewrite chunk system
  
          this.isSaving = false;
          this.resources.close();
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+@@ -991,6 +1057,14 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+             this.getProfileCache().save(false); // Paper - Perf: Async GameProfileCache saving
          }
          // Spigot end
- 
 +        // Paper start - rewrite chunk system
 +        LOGGER.info("Waiting for I/O tasks to complete...");
 +        ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.flush((MinecraftServer)(Object)this);
 +        LOGGER.info("All I/O tasks to complete");
-+        if ((Object)this instanceof DedicatedServer) {
++        if ((Object)this instanceof net.minecraft.server.dedicated.DedicatedServer) {
 +            ca.spottedleaf.moonrise.common.util.MoonriseCommon.haltExecutors();
 +        }
 +        // Paper end - rewrite chunk system
-     }
- 
-     public String getLocalIp() {
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
-                         this.tickServer(flag ? () -> {
-                             return false;
-                         } : this::haveTime);
-+                        // Paper start - rewrite chunk system
-+                        final Throwable crash = this.chunkSystemCrash;
-+                        if (crash != null) {
-+                            this.chunkSystemCrash = null;
-+                            throw new RuntimeException("Chunk system crash propagated to tick()", crash);
-+                        }
-+                        // Paper end - rewrite chunk system
-                         this.tickFrame.end();
-                         gameprofilerfiller.popPush("nextTickWait");
-                         this.mayHaveDelayedTasks = true;
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+         // Paper start - Improved watchdog support - move final shutdown items here
+         Util.shutdownExecutors();
+         try {
+@@ -1175,6 +1249,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+                     profilerFiller.push("tick");
+                     this.tickFrame.start();
+                     this.tickServer(flag ? () -> false : this::haveTime);
++                    // Paper start - rewrite chunk system
++                    final Throwable crash = this.chunkSystemCrash;
++                    if (crash != null) {
++                        this.chunkSystemCrash = null;
++                        throw new RuntimeException("Chunk system crash propagated to tick()", crash);
++                    }
++                    // Paper end - rewrite chunk system
+                     this.tickFrame.end();
+                     profilerFiller.popPush("nextTickWait");
+                     this.mayHaveDelayedTasks = true;
+@@ -1345,6 +1426,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
  
      private boolean pollTaskInternal() {
          if (super.pollTask()) {
@@ -23560,8 +23737,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
              return true;
          } else {
              boolean ret = false; // Paper - force execution of all worlds, do not just bias the first
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
- 
+@@ -2472,6 +2554,12 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+         }
      }
  
 +    // Paper start - rewrite chunk system
@@ -23570,28 +23747,27 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return ca.spottedleaf.moonrise.common.util.TickThread.isTickThread();
 +    }
 +    // Paper end - rewrite chunk system
-+
+ 
      // CraftBukkit start
      public boolean isDebugging() {
-         return false;
-diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
-+++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
-@@ -0,0 +0,0 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
-         return world.dimension() == net.minecraft.world.level.Level.NETHER ? this.getProperties().allowNether : true;
+diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java
+index f8c81d795b19e73d56d6e0196c75e441ab4c2bef..97a294d2f5c1ddf0af7ffec3e1425eb329c5751b 100644
+--- a/net/minecraft/server/dedicated/DedicatedServer.java
++++ b/net/minecraft/server/dedicated/DedicatedServer.java
+@@ -433,7 +433,33 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
+         return level.dimension() != Level.NETHER || this.getProperties().allowNether;
      }
  
 +    private static final java.util.concurrent.atomic.AtomicInteger ASYNC_DEBUG_CHUNKS_COUNT = new java.util.concurrent.atomic.AtomicInteger(); // Paper - rewrite chunk system
 +
-     public void handleConsoleInput(String command, CommandSourceStack commandSource) {
+     public void handleConsoleInput(String msg, CommandSourceStack source) {
 +        // Paper start - rewrite chunk system
-+        if (command.equalsIgnoreCase("paper debug chunks --async")) {
++        if (msg.equalsIgnoreCase("paper debug chunks --async")) {
 +            LOGGER.info("Scheduling async debug chunks");
 +            Runnable run = () -> {
 +                LOGGER.info("Async debug chunks executing");
 +                ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(this, false);
-+                CommandSender sender = MinecraftServer.getServer().console;
++                org.bukkit.command.CommandSender sender = MinecraftServer.getServer().console;
 +                java.io.File file = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getChunkDebugFile();
 +                sender.sendMessage(net.kyori.adventure.text.Component.text("Writing chunk information dump to " + file, net.kyori.adventure.text.format.NamedTextColor.GREEN));
 +                try {
@@ -23609,42 +23785,41 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            return;
 +        }
 +        // Paper end - rewrite chunk system
-         this.serverCommandQueue.add(new ConsoleInput(command, commandSource)); // Paper - Perf: use proper queue
+         this.serverCommandQueue.add(new ConsoleInput(msg, source)); // Paper - Perf: use proper queue
      }
  
-diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
-+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
-@@ -0,0 +0,0 @@ import net.minecraft.world.level.lighting.LevelLightEngine;
- import net.minecraft.server.MinecraftServer;
- // CraftBukkit end
+diff --git a/net/minecraft/server/level/ChunkHolder.java b/net/minecraft/server/level/ChunkHolder.java
+index b95de132c53f82d270de396787dda3be8bc6c910..656041c9539b6834b4d37b353eb6b810a7763ff4 100644
+--- a/net/minecraft/server/level/ChunkHolder.java
++++ b/net/minecraft/server/level/ChunkHolder.java
+@@ -29,27 +29,112 @@ import net.minecraft.world.level.chunk.LevelChunkSection;
+ import net.minecraft.world.level.chunk.status.ChunkStatus;
+ import net.minecraft.world.level.lighting.LevelLightEngine;
  
 -public class ChunkHolder extends GenerationChunkHolder {
 +public class ChunkHolder extends GenerationChunkHolder implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder { // Paper - rewrite chunk system
- 
      public static final ChunkResult<LevelChunk> UNLOADED_LEVEL_CHUNK = ChunkResult.error("Unloaded level chunk");
-     private static final CompletableFuture<ChunkResult<LevelChunk>> UNLOADED_LEVEL_CHUNK_FUTURE = CompletableFuture.completedFuture(ChunkHolder.UNLOADED_LEVEL_CHUNK);
+     private static final CompletableFuture<ChunkResult<LevelChunk>> UNLOADED_LEVEL_CHUNK_FUTURE = CompletableFuture.completedFuture(UNLOADED_LEVEL_CHUNK);
      private final LevelHeightAccessor levelHeightAccessor;
--    private volatile CompletableFuture<ChunkResult<LevelChunk>> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage
--    private volatile CompletableFuture<ChunkResult<LevelChunk>> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage
--    private volatile CompletableFuture<ChunkResult<LevelChunk>> entityTickingChunkFuture; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage
+-    private volatile CompletableFuture<ChunkResult<LevelChunk>> fullChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage
+-    private volatile CompletableFuture<ChunkResult<LevelChunk>> tickingChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage
+-    private volatile CompletableFuture<ChunkResult<LevelChunk>> entityTickingChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage
 -    public int oldTicketLevel;
 -    private int ticketLevel;
 -    private int queueLevel;
 +    // Paper - rewrite chunk system
      private boolean hasChangedSections;
      private final ShortSet[] changedBlocksPerSection;
-     private final BitSet blockChangedLightSectionFilter;
-     private final BitSet skyChangedLightSectionFilter;
+     private final BitSet blockChangedLightSectionFilter = new BitSet();
+     private final BitSet skyChangedLightSectionFilter = new BitSet();
      private final LevelLightEngine lightEngine;
 -    private final ChunkHolder.LevelChangeListener onLevelChange;
 +    // Paper - rewrite chunk system
      public final ChunkHolder.PlayerProvider playerProvider;
 -    private boolean wasAccessibleSinceLastSave;
--    private CompletableFuture<?> pendingFullStateConfirmation;
--    private CompletableFuture<?> sendSync;
--    private CompletableFuture<?> saveSync;
+-    private CompletableFuture<?> pendingFullStateConfirmation = CompletableFuture.completedFuture(null);
+-    private CompletableFuture<?> sendSync = CompletableFuture.completedFuture(null);
+-    private CompletableFuture<?> saveSync = CompletableFuture.completedFuture(null);
 +    // Paper - rewrite chunk system
 +
 +    // Paper start - rewrite chunk system
@@ -23740,31 +23915,23 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - rewrite chunk system
  
-     public ChunkHolder(ChunkPos pos, int level, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.LevelChangeListener levelUpdateListener, ChunkHolder.PlayerProvider playersWatchingChunkProvider) {
+     public ChunkHolder(
+         ChunkPos pos,
+@@ -62,11 +147,9 @@ public class ChunkHolder extends GenerationChunkHolder {
          super(pos);
--        this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
--        this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
--        this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
+         this.levelHeightAccessor = levelHeightAccessor;
+         this.lightEngine = lightEngine;
+-        this.onLevelChange = onLevelChange;
 +        // Paper - rewrite chunk system
-         this.blockChangedLightSectionFilter = new BitSet();
-         this.skyChangedLightSectionFilter = new BitSet();
--        this.pendingFullStateConfirmation = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error
--        this.sendSync = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error
--        this.saveSync = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error
-+        // Paper - rewrite chunk system
-         this.levelHeightAccessor = world;
-         this.lightEngine = lightingProvider;
--        this.onLevelChange = levelUpdateListener;
-+        // Paper - rewrite chunk system
-         this.playerProvider = playersWatchingChunkProvider;
+         this.playerProvider = playerProvider;
 -        this.oldTicketLevel = ChunkLevel.MAX_LEVEL + 1;
 -        this.ticketLevel = this.oldTicketLevel;
 -        this.queueLevel = this.oldTicketLevel;
 +        // Paper - rewrite chunk system
-         this.setTicketLevel(level);
-         this.changedBlocksPerSection = new ShortSet[world.getSectionsCount()];
+         this.setTicketLevel(ticketLevel);
+         this.changedBlocksPerSection = new ShortSet[levelHeightAccessor.getSectionsCount()];
      }
-@@ -0,0 +0,0 @@ public class ChunkHolder extends GenerationChunkHolder {
+@@ -74,7 +157,7 @@ public class ChunkHolder extends GenerationChunkHolder {
      // CraftBukkit start
      public LevelChunk getFullChunkNow() {
          // Note: We use the oldTicketLevel for isLoaded checks.
@@ -23773,7 +23940,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          return this.getFullChunkNowUnchecked();
      }
  
-@@ -0,0 +0,0 @@ public class ChunkHolder extends GenerationChunkHolder {
+@@ -84,58 +167,63 @@ public class ChunkHolder extends GenerationChunkHolder {
      // CraftBukkit end
  
      public CompletableFuture<ChunkResult<LevelChunk>> getTickingChunkFuture() {
@@ -23793,7 +23960,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      @Nullable
      public final LevelChunk getTickingChunk() { // Paper - final for inline
--        return (LevelChunk) ((ChunkResult) this.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).orElse(null); // CraftBukkit - decompile error
+-        return this.getTickingChunkFuture().getNow(UNLOADED_LEVEL_CHUNK).orElse(null);
 +        // Paper start - rewrite chunk system
 +        if (this.newChunkHolder.isTickingReady()) {
 +            if (this.newChunkHolder.getCurrentChunk() instanceof LevelChunk levelChunk) {
@@ -23821,16 +23988,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
-     public void addSendDependency(CompletableFuture<?> postProcessingFuture) {
+     public void addSendDependency(CompletableFuture<?> dependency) {
 -        if (this.sendSync.isDone()) {
--            this.sendSync = postProcessingFuture;
+-            this.sendSync = dependency;
 -        } else {
--            this.sendSync = this.sendSync.thenCombine(postProcessingFuture, (object, object1) -> {
--                return null;
--            });
+-            this.sendSync = this.sendSync.thenCombine((CompletionStage<? extends Object>)dependency, (object, object1) -> null);
 -        }
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- 
      }
  
      public CompletableFuture<?> getSaveSyncFuture() {
@@ -23844,52 +24008,49 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      @Override
-     protected void addSaveDependency(CompletableFuture<?> savingFuture) {
+     protected void addSaveDependency(CompletableFuture<?> dependency) {
 -        if (this.saveSync.isDone()) {
--            this.saveSync = savingFuture;
+-            this.saveSync = dependency;
 -        } else {
--            this.saveSync = this.saveSync.thenCombine(savingFuture, (object, object1) -> {
--                return null;
--            });
+-            this.saveSync = this.saveSync.thenCombine((CompletionStage<? extends Object>)dependency, (object, object1) -> null);
 -        }
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- 
      }
  
      public boolean blockChanged(BlockPos pos) {
--        LevelChunk chunk = this.getTickingChunk();
-+        LevelChunk chunk = this.playersSentChunkTo.size() == 0 ? null : this.getChunkToSend(); // Paper - rewrite chunk system
- 
-         if (chunk == null) {
-             return false;
-@@ -0,0 +0,0 @@ public class ChunkHolder extends GenerationChunkHolder {
+-        LevelChunk tickingChunk = this.getTickingChunk();
++        LevelChunk tickingChunk = this.playersSentChunkTo.size() == 0 ? null : this.getChunkToSend(); // Paper - rewrite chunk system
+         if (tickingChunk == null) {
              return false;
          } else {
-             ichunkaccess.markUnsaved();
--            LevelChunk chunk = this.getTickingChunk();
-+            LevelChunk chunk = this.playersSentChunkTo.size() == 0 ? null : this.getChunkToSend(); // Paper - rewrite chunk system
- 
-             if (chunk == null) {
+@@ -158,7 +246,7 @@ public class ChunkHolder extends GenerationChunkHolder {
+             return false;
+         } else {
+             chunkIfPresent.markUnsaved();
+-            LevelChunk tickingChunk = this.getTickingChunk();
++            LevelChunk tickingChunk = this.playersSentChunkTo.size() == 0 ? null : this.getChunkToSend(); // Paper - rewrite chunk system
+             if (tickingChunk == null) {
                  return false;
-@@ -0,0 +0,0 @@ public class ChunkHolder extends GenerationChunkHolder {
-             List list;
- 
+             } else {
+@@ -188,7 +276,7 @@ public class ChunkHolder extends GenerationChunkHolder {
+         if (this.hasChangesToBroadcast()) {
+             Level level = chunk.getLevel();
              if (!this.skyChangedLightSectionFilter.isEmpty() || !this.blockChangedLightSectionFilter.isEmpty()) {
--                list = this.playerProvider.getPlayers(this.pos, true);
-+                list = this.moonrise$getPlayers(true); // Paper - rewrite chunk system
-                 if (!list.isEmpty()) {
-                     ClientboundLightUpdatePacket packetplayoutlightupdate = new ClientboundLightUpdatePacket(chunk.getPos(), this.lightEngine, this.skyChangedLightSectionFilter, this.blockChangedLightSectionFilter);
- 
-@@ -0,0 +0,0 @@ public class ChunkHolder extends GenerationChunkHolder {
+-                List<ServerPlayer> players = this.playerProvider.getPlayers(this.pos, true);
++                List<ServerPlayer> players = this.moonrise$getPlayers(true); // Paper - rewrite chunk system
+                 if (!players.isEmpty()) {
+                     ClientboundLightUpdatePacket clientboundLightUpdatePacket = new ClientboundLightUpdatePacket(
+                         chunk.getPos(), this.lightEngine, this.skyChangedLightSectionFilter, this.blockChangedLightSectionFilter
+@@ -201,7 +289,7 @@ public class ChunkHolder extends GenerationChunkHolder {
              }
  
              if (this.hasChangedSections) {
--                list = this.playerProvider.getPlayers(this.pos, false);
-+                list = this.moonrise$getPlayers(false); // Paper - rewrite chunk system
+-                List<ServerPlayer> players = this.playerProvider.getPlayers(this.pos, false);
++                List<ServerPlayer> players = this.moonrise$getPlayers(false); // Paper - rewrite chunk system
  
-                 for (int i = 0; i < this.changedBlocksPerSection.length; ++i) {
-                     ShortSet shortset = this.changedBlocksPerSection[i];
-@@ -0,0 +0,0 @@ public class ChunkHolder extends GenerationChunkHolder {
+                 for (int i = 0; i < this.changedBlocksPerSection.length; i++) {
+                     ShortSet set = this.changedBlocksPerSection[i];
+@@ -256,193 +344,50 @@ public class ChunkHolder extends GenerationChunkHolder {
  
      @Override
      public int getTicketLevel() {
@@ -23903,8 +24064,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
-     private void setQueueLevel(int level) {
--        this.queueLevel = level;
+     private void setQueueLevel(int queueLevel) {
+-        this.queueLevel = queueLevel;
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
@@ -23913,41 +24074,36 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper - rewrite chunk system
      }
  
-     private void scheduleFullChunkPromotion(ChunkMap chunkLoadingManager, CompletableFuture<ChunkResult<LevelChunk>> chunkFuture, Executor executor, FullChunkStatus target) {
+     private void scheduleFullChunkPromotion(
+         ChunkMap chunkMap, CompletableFuture<ChunkResult<LevelChunk>> future, Executor executor, FullChunkStatus fullChunkStatus
+     ) {
 -        this.pendingFullStateConfirmation.cancel(false);
--        CompletableFuture<Void> completablefuture1 = new CompletableFuture();
--
--        completablefuture1.thenRunAsync(() -> {
--            chunkLoadingManager.onFullChunkStatusChange(this.pos, target);
--        }, executor);
--        this.pendingFullStateConfirmation = completablefuture1;
--        chunkFuture.thenAccept((chunkresult) -> {
--            chunkresult.ifSuccess((chunk) -> {
--                completablefuture1.complete(null); // CraftBukkit - decompile error
--            });
--        });
+-        CompletableFuture<Void> completableFuture = new CompletableFuture<>();
+-        completableFuture.thenRunAsync(() -> chunkMap.onFullChunkStatusChange(this.pos, fullChunkStatus), executor);
+-        this.pendingFullStateConfirmation = completableFuture;
+-        future.thenAccept(chunkResult -> chunkResult.ifSuccess(levelChunk -> completableFuture.complete(null)));
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
-     private void demoteFullChunk(ChunkMap chunkLoadingManager, FullChunkStatus target) {
+     private void demoteFullChunk(ChunkMap chunkMap, FullChunkStatus fullChunkStatus) {
 -        this.pendingFullStateConfirmation.cancel(false);
--        chunkLoadingManager.onFullChunkStatusChange(this.pos, target);
+-        chunkMap.onFullChunkStatusChange(this.pos, fullChunkStatus);
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
      // CraftBukkit start
      // ChunkUnloadEvent: Called before the chunk is unloaded: isChunkLoaded is still true and chunk can still be modified by plugins.
      // SPIGOT-7780: Moved out of updateFutures to call all chunk unload events before calling updateHighestAllowedStatus for all chunks
-     protected void callEventIfUnloading(ChunkMap playerchunkmap) {
+     protected void callEventIfUnloading(ChunkMap chunkMap) {
 -        FullChunkStatus oldFullChunkStatus = ChunkLevel.fullStatus(this.oldTicketLevel);
 -        FullChunkStatus newFullChunkStatus = ChunkLevel.fullStatus(this.ticketLevel);
 -        boolean oldIsFull = oldFullChunkStatus.isOrAfter(FullChunkStatus.FULL);
 -        boolean newIsFull = newFullChunkStatus.isOrAfter(FullChunkStatus.FULL);
 -        if (oldIsFull && !newIsFull) {
 -            this.getFullChunkFuture().thenAccept((either) -> {
--                LevelChunk chunk = (LevelChunk) either.orElse(null);
+-                LevelChunk chunk = either.orElse(null);
 -                if (chunk != null) {
--                    playerchunkmap.callbackExecutor.execute(() -> {
+-                    chunkMap.callbackExecutor.execute(() -> {
 -                        // Minecraft will apply the chunks tick lists to the world once the chunk got loaded, and then store the tick
 -                        // lists again inside the chunk once the chunk becomes inaccessible and set the chunk's needsSaving flag.
 -                        // These actions may however happen deferred, so we manually set the needsSaving flag already here.
@@ -23957,34 +24113,33 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -                }
 -            }).exceptionally((throwable) -> {
 -                // ensure exceptions are printed, by default this is not the case
--                MinecraftServer.LOGGER.error("Failed to schedule unload callback for chunk " + ChunkHolder.this.pos, throwable);
+-                net.minecraft.server.MinecraftServer.LOGGER.error("Failed to schedule unload callback for chunk " + ChunkHolder.this.pos, throwable);
 -                return null;
 -            });
 -
 -            // Run callback right away if the future was already done
--            playerchunkmap.callbackExecutor.run();
+-            chunkMap.callbackExecutor.run();
 -        }
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
      // CraftBukkit end
  
-     protected void updateFutures(ChunkMap chunkLoadingManager, Executor executor) {
--        FullChunkStatus fullchunkstatus = ChunkLevel.fullStatus(this.oldTicketLevel);
--        FullChunkStatus fullchunkstatus1 = ChunkLevel.fullStatus(this.ticketLevel);
--        boolean flag = fullchunkstatus.isOrAfter(FullChunkStatus.FULL);
--        boolean flag1 = fullchunkstatus1.isOrAfter(FullChunkStatus.FULL);
--
--        this.wasAccessibleSinceLastSave |= flag1;
--        if (!flag && flag1) {
+     protected void updateFutures(ChunkMap chunkMap, Executor executor) {
+-        FullChunkStatus fullChunkStatus = ChunkLevel.fullStatus(this.oldTicketLevel);
+-        FullChunkStatus fullChunkStatus1 = ChunkLevel.fullStatus(this.ticketLevel);
+-        boolean isOrAfter = fullChunkStatus.isOrAfter(FullChunkStatus.FULL);
+-        boolean isOrAfter1 = fullChunkStatus1.isOrAfter(FullChunkStatus.FULL);
+-        this.wasAccessibleSinceLastSave |= isOrAfter1;
+-        if (!isOrAfter && isOrAfter1) {
 -            int expectCreateCount = ++this.fullChunkCreateCount; // Paper
--            this.fullChunkFuture = chunkLoadingManager.prepareAccessibleChunk(this);
--            this.scheduleFullChunkPromotion(chunkLoadingManager, this.fullChunkFuture, executor, FullChunkStatus.FULL);
+-            this.fullChunkFuture = chunkMap.prepareAccessibleChunk(this);
+-            this.scheduleFullChunkPromotion(chunkMap, this.fullChunkFuture, executor, FullChunkStatus.FULL);
 -            // Paper start - cache ticking ready status
 -            this.fullChunkFuture.thenAccept(chunkResult -> {
 -                chunkResult.ifSuccess(chunk -> {
 -                    if (ChunkHolder.this.fullChunkCreateCount == expectCreateCount) {
 -                        ChunkHolder.this.isFullChunkReady = true;
--                        ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkBorder(chunk, this);
+-                        ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkBorder(chunk, this);
 -                    }
 -                });
 -            });
@@ -23992,99 +24147,97 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -            this.addSaveDependency(this.fullChunkFuture);
 -        }
 -
--        if (flag && !flag1) {
+-        if (isOrAfter && !isOrAfter1) {
 -            // Paper start
 -            if (this.isFullChunkReady) {
--                ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotBorder(this.fullChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
+-                ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkNotBorder(this.fullChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
 -            }
 -            // Paper end
--            this.fullChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
--            this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
+-            this.fullChunkFuture.complete(UNLOADED_LEVEL_CHUNK);
+-            this.fullChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE;
 -        }
 -
--        boolean flag2 = fullchunkstatus.isOrAfter(FullChunkStatus.BLOCK_TICKING);
--        boolean flag3 = fullchunkstatus1.isOrAfter(FullChunkStatus.BLOCK_TICKING);
--
--        if (!flag2 && flag3) {
--            this.tickingChunkFuture = chunkLoadingManager.prepareTickingChunk(this);
--            this.scheduleFullChunkPromotion(chunkLoadingManager, this.tickingChunkFuture, executor, FullChunkStatus.BLOCK_TICKING);
+-        boolean isOrAfter2 = fullChunkStatus.isOrAfter(FullChunkStatus.BLOCK_TICKING);
+-        boolean isOrAfter3 = fullChunkStatus1.isOrAfter(FullChunkStatus.BLOCK_TICKING);
+-        if (!isOrAfter2 && isOrAfter3) {
+-            this.tickingChunkFuture = chunkMap.prepareTickingChunk(this);
+-            this.scheduleFullChunkPromotion(chunkMap, this.tickingChunkFuture, executor, FullChunkStatus.BLOCK_TICKING);
 -            // Paper start - cache ticking ready status
 -            this.tickingChunkFuture.thenAccept(chunkResult -> {
 -                chunkResult.ifSuccess(chunk -> {
 -                    // note: Here is a very good place to add callbacks to logic waiting on this.
 -                    ChunkHolder.this.isTickingReady = true;
--                    ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkTicking(chunk, this);
+-                    ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkTicking(chunk, this);
 -                });
 -            });
 -            // Paper end
 -            this.addSaveDependency(this.tickingChunkFuture);
 -        }
 -
--        if (flag2 && !flag3) {
+-        if (isOrAfter2 && !isOrAfter3) {
 -            // Paper start
 -            if (this.isTickingReady) {
--                ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotTicking(this.tickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
+-                ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkNotTicking(this.tickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
 -            }
 -            // Paper end
 -            this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isTickingReady = false; // Paper - cache chunk ticking stage
--            this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
+-            this.tickingChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE;
 -        }
 -
--        boolean flag4 = fullchunkstatus.isOrAfter(FullChunkStatus.ENTITY_TICKING);
--        boolean flag5 = fullchunkstatus1.isOrAfter(FullChunkStatus.ENTITY_TICKING);
--
--        if (!flag4 && flag5) {
--            if (this.entityTickingChunkFuture != ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE) {
--                throw (IllegalStateException) Util.pauseInIde(new IllegalStateException());
+-        boolean isOrAfter4 = fullChunkStatus.isOrAfter(FullChunkStatus.ENTITY_TICKING);
+-        boolean isOrAfter5 = fullChunkStatus1.isOrAfter(FullChunkStatus.ENTITY_TICKING);
+-        if (!isOrAfter4 && isOrAfter5) {
+-            if (this.entityTickingChunkFuture != UNLOADED_LEVEL_CHUNK_FUTURE) {
+-                throw (IllegalStateException)Util.pauseInIde(new IllegalStateException());
 -            }
 -
--            this.entityTickingChunkFuture = chunkLoadingManager.prepareEntityTickingChunk(this);
--            this.scheduleFullChunkPromotion(chunkLoadingManager, this.entityTickingChunkFuture, executor, FullChunkStatus.ENTITY_TICKING);
+-            this.entityTickingChunkFuture = chunkMap.prepareEntityTickingChunk(this);
+-            this.scheduleFullChunkPromotion(chunkMap, this.entityTickingChunkFuture, executor, FullChunkStatus.ENTITY_TICKING);
 -            // Paper start - cache ticking ready status
 -            this.entityTickingChunkFuture.thenAccept(chunkResult -> {
 -                chunkResult.ifSuccess(chunk -> {
 -                    ChunkHolder.this.isEntityTickingReady = true;
--                    ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkEntityTicking(chunk, this);
+-                    ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkEntityTicking(chunk, this);
 -                });
 -            });
 -            // Paper end
 -            this.addSaveDependency(this.entityTickingChunkFuture);
 -        }
 -
--        if (flag4 && !flag5) {
+-        if (isOrAfter4 && !isOrAfter5) {
 -            // Paper start
 -            if (this.isEntityTickingReady) {
--                ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotEntityTicking(this.entityTickingChunkFuture.join().orElseThrow(IllegalStateException::new), this);
+-                ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkNotEntityTicking(this.entityTickingChunkFuture.join().orElseThrow(IllegalStateException::new), this);
 -            }
 -            // Paper end
 -            this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isEntityTickingReady = false; // Paper - cache chunk ticking stage
--            this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
+-            this.entityTickingChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE;
 -        }
 -
--        if (!fullchunkstatus1.isOrAfter(fullchunkstatus)) {
--            this.demoteFullChunk(chunkLoadingManager, fullchunkstatus1);
+-        if (!fullChunkStatus1.isOrAfter(fullChunkStatus)) {
+-            this.demoteFullChunk(chunkMap, fullChunkStatus1);
 -        }
 -
 -        this.onLevelChange.onLevelChange(this.pos, this::getQueueLevel, this.ticketLevel, this::setQueueLevel);
 -        this.oldTicketLevel = this.ticketLevel;
 -        // CraftBukkit start
 -        // ChunkLoadEvent: Called after the chunk is loaded: isChunkLoaded returns true and chunk is ready to be modified by plugins.
--        if (!fullchunkstatus.isOrAfter(FullChunkStatus.FULL) && fullchunkstatus1.isOrAfter(FullChunkStatus.FULL)) {
+-        if (!fullChunkStatus.isOrAfter(FullChunkStatus.FULL) && fullChunkStatus1.isOrAfter(FullChunkStatus.FULL)) {
 -            this.getFullChunkFuture().thenAccept((either) -> {
 -                LevelChunk chunk = (LevelChunk) either.orElse(null);
 -                if (chunk != null) {
--                    chunkLoadingManager.callbackExecutor.execute(() -> {
+-                    chunkMap.callbackExecutor.execute(() -> {
 -                        chunk.loadCallback();
 -                    });
 -                }
 -            }).exceptionally((throwable) -> {
 -                // ensure exceptions are printed, by default this is not the case
--                MinecraftServer.LOGGER.error("Failed to schedule load callback for chunk " + ChunkHolder.this.pos, throwable);
+-                net.minecraft.server.MinecraftServer.LOGGER.error("Failed to schedule load callback for chunk " + ChunkHolder.this.pos, throwable);
 -                return null;
 -            });
 -
 -            // Run callback right away if the future was already done
--            chunkLoadingManager.callbackExecutor.run();
+-            chunkMap.callbackExecutor.run();
 -        }
 -        // CraftBukkit end
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
@@ -24101,11 +24254,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      @FunctionalInterface
-diff --git a/src/main/java/net/minecraft/server/level/ChunkLevel.java b/src/main/java/net/minecraft/server/level/ChunkLevel.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ChunkLevel.java
-+++ b/src/main/java/net/minecraft/server/level/ChunkLevel.java
-@@ -0,0 +0,0 @@ import net.minecraft.world.level.chunk.status.ChunkStep;
+diff --git a/net/minecraft/server/level/ChunkLevel.java b/net/minecraft/server/level/ChunkLevel.java
+index e823b8aac00158892538083bc877ccf99895909a..7d871318065f19540748363809de82652613e733 100644
+--- a/net/minecraft/server/level/ChunkLevel.java
++++ b/net/minecraft/server/level/ChunkLevel.java
+@@ -7,8 +7,8 @@ import net.minecraft.world.level.chunk.status.ChunkStep;
  import org.jetbrains.annotations.Contract;
  
  public class ChunkLevel {
@@ -24116,51 +24269,51 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      public static final int ENTITY_TICKING_LEVEL = 31;
      private static final ChunkStep FULL_CHUNK_STEP = ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FULL);
      public static final int RADIUS_AROUND_FULL_CHUNK = FULL_CHUNK_STEP.accumulatedDependencies().getRadius();
-diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ChunkMap.java
-+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
-@@ -0,0 +0,0 @@ import org.slf4j.Logger;
- import org.bukkit.craftbukkit.generator.CustomChunkGenerator;
- // CraftBukkit end
+diff --git a/net/minecraft/server/level/ChunkMap.java b/net/minecraft/server/level/ChunkMap.java
+index ad665c7535c615d2b03a3e7864be435f933235dd..3dff97f13586be3b52bbe786852c185f6753a019 100644
+--- a/net/minecraft/server/level/ChunkMap.java
++++ b/net/minecraft/server/level/ChunkMap.java
+@@ -96,7 +96,7 @@ import net.minecraft.world.level.storage.LevelStorageSource;
+ import org.apache.commons.lang3.mutable.MutableBoolean;
+ import org.slf4j.Logger;
  
 -public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider, GeneratingChunkMap {
 +public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider, GeneratingChunkMap, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemChunkMap { // Paper - rewrite chunk system
- 
      private static final ChunkResult<List<ChunkAccess>> UNLOADED_CHUNK_LIST_RESULT = ChunkResult.error("Unloaded chunks found in range");
-     private static final CompletableFuture<ChunkResult<List<ChunkAccess>>> UNLOADED_CHUNK_LIST_FUTURE = CompletableFuture.completedFuture(ChunkMap.UNLOADED_CHUNK_LIST_RESULT);
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+     private static final CompletableFuture<ChunkResult<List<ChunkAccess>>> UNLOADED_CHUNK_LIST_FUTURE = CompletableFuture.completedFuture(
+         UNLOADED_CHUNK_LIST_RESULT
+@@ -112,10 +112,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
      public static final int MIN_VIEW_DISTANCE = 2;
      public static final int MAX_VIEW_DISTANCE = 32;
      public static final int FORCED_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING);
--    public final Long2ObjectLinkedOpenHashMap<ChunkHolder> updatingChunkMap = new Long2ObjectLinkedOpenHashMap();
--    public volatile Long2ObjectLinkedOpenHashMap<ChunkHolder> visibleChunkMap;
--    private final Long2ObjectLinkedOpenHashMap<ChunkHolder> pendingUnloads;
--    private final List<ChunkGenerationTask> pendingGenerationTasks;
+-    public final Long2ObjectLinkedOpenHashMap<ChunkHolder> updatingChunkMap = new Long2ObjectLinkedOpenHashMap<>();
+-    public volatile Long2ObjectLinkedOpenHashMap<ChunkHolder> visibleChunkMap = this.updatingChunkMap.clone();
+-    private final Long2ObjectLinkedOpenHashMap<ChunkHolder> pendingUnloads = new Long2ObjectLinkedOpenHashMap<>();
+-    private final List<ChunkGenerationTask> pendingGenerationTasks = new ArrayList<>();
 +    // Paper - rewrite chunk system
      public final ServerLevel level;
      private final ThreadedLevelLightEngine lightEngine;
      private final BlockableEventLoop<Runnable> mainThreadExecutor;
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -125,22 +122,18 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
      private final PoiManager poiManager;
-     public final LongSet toDrop;
+     public final LongSet toDrop = new LongOpenHashSet();
      private boolean modified;
 -    private final ChunkTaskDispatcher worldgenTaskDispatcher;
 -    private final ChunkTaskDispatcher lightTaskDispatcher;
 +    // Paper - rewrite chunk system
      public final ChunkProgressListener progressListener;
      private final ChunkStatusUpdateListener chunkStatusListener;
-     public final ChunkMap.ChunkDistanceManager distanceManager;
--    private final AtomicInteger tickingGenerated;
-+    public final AtomicInteger tickingGenerated; // Paper - public
+     public final ChunkMap.DistanceManager distanceManager;
+-    private final AtomicInteger tickingGenerated = new AtomicInteger();
++    public final AtomicInteger tickingGenerated = new AtomicInteger();  // Paper - public
      private final String storageName;
-     private final PlayerMap playerMap;
-     public final Int2ObjectMap<ChunkMap.TrackedEntity> entityMap;
-     private final Long2ByteMap chunkTypeCache;
--    private final Long2LongMap nextChunkSaveTime;
--    private final LongSet chunksToEagerlySave;
--    private final Queue<Runnable> unloadQueue;
--    private final AtomicInteger activeChunkWrites;
+     private final PlayerMap playerMap = new PlayerMap();
+     public final Int2ObjectMap<ChunkMap.TrackedEntity> entityMap = new Int2ObjectOpenHashMap<>();
+     private final Long2ByteMap chunkTypeCache = new Long2ByteOpenHashMap();
+-    private final Long2LongMap nextChunkSaveTime = new Long2LongOpenHashMap();
+-    private final LongSet chunksToEagerlySave = new LongLinkedOpenHashSet();
+-    private final Queue<Runnable> unloadQueue = Queues.newConcurrentLinkedQueue();
+-    private final AtomicInteger activeChunkWrites = new AtomicInteger();
 +    // Paper - rewrite chunk system
      public int serverViewDistance;
 -    private final WorldGenContext worldGenContext;
@@ -24168,7 +24321,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      // CraftBukkit start - recursion-safe executor for Chunk loadCallback() and unloadCallback()
      public final CallbackExecutor callbackExecutor = new CallbackExecutor();
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -165,9 +158,16 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
  
      // Paper start
      public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) {
@@ -24184,62 +24337,50 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - rewrite chunk system
  
-     public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop<Runnable> mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory, int viewDistance, boolean dsync) {
-         super(new RegionStorageInfo(session.getLevelId(), world.dimension(), "chunk"), session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync);
--        this.visibleChunkMap = this.updatingChunkMap.clone();
--        this.pendingUnloads = new Long2ObjectLinkedOpenHashMap();
--        this.pendingGenerationTasks = new ArrayList();
+     public ChunkMap(
+         ServerLevel level,
+@@ -213,10 +213,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+         this.progressListener = progressListener;
+         this.chunkStatusListener = chunkStatusListener;
+         ConsecutiveExecutor consecutiveExecutor1 = new ConsecutiveExecutor(dispatcher, "light");
+-        this.worldgenTaskDispatcher = new ChunkTaskDispatcher(consecutiveExecutor, dispatcher);
+-        this.lightTaskDispatcher = new ChunkTaskDispatcher(consecutiveExecutor1, dispatcher);
 +        // Paper - rewrite chunk system
-         this.toDrop = new LongOpenHashSet();
-         this.tickingGenerated = new AtomicInteger();
-         this.playerMap = new PlayerMap();
-         this.entityMap = new Int2ObjectOpenHashMap();
-         this.chunkTypeCache = new Long2ByteOpenHashMap();
--        this.nextChunkSaveTime = new Long2LongOpenHashMap();
--        this.chunksToEagerlySave = new LongLinkedOpenHashSet();
--        this.unloadQueue = Queues.newConcurrentLinkedQueue();
--        this.activeChunkWrites = new AtomicInteger();
-+        // Paper - rewrite chunk system
-         Path path = session.getDimensionPath(world.dimension());
- 
-         this.storageName = path.getFileName().toString();
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-         this.chunkStatusListener = chunkStatusChangeListener;
-         ConsecutiveExecutor consecutiveexecutor1 = new ConsecutiveExecutor(executor, "light");
- 
--        this.worldgenTaskDispatcher = new ChunkTaskDispatcher(consecutiveexecutor, executor);
--        this.lightTaskDispatcher = new ChunkTaskDispatcher(consecutiveexecutor1, executor);
--        this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), consecutiveexecutor1, this.lightTaskDispatcher);
-+        this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), consecutiveexecutor1, null); // Paper - rewrite chunk system
-         this.distanceManager = new ChunkMap.ChunkDistanceManager(executor, mainThreadExecutor);
-         this.overworldDataStorage = persistentStateManagerFactory;
-         this.poiManager = new PoiManager(new RegionStorageInfo(session.getLevelId(), world.dimension(), "poi"), path.resolve("poi"), dataFixer, dsync, iregistrycustom, world.getServer(), world);
+         this.lightEngine = new ThreadedLevelLightEngine(
+-            lightChunk, this, this.level.dimensionType().hasSkyLight(), consecutiveExecutor1, this.lightTaskDispatcher
++            lightChunk, this, this.level.dimensionType().hasSkyLight(), consecutiveExecutor1, null // Paper - rewrite chunk system
+         );
+         this.distanceManager = new ChunkMap.DistanceManager(dispatcher, mainThreadExecutor);
+         this.overworldDataStorage = overworldDataStorage;
+@@ -230,11 +229,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+             level
+         );
          this.setServerViewDistance(viewDistance);
--        this.worldGenContext = new WorldGenContext(world, chunkGenerator, structureTemplateManager, this.lightEngine, mainThreadExecutor, this::setChunkUnsaved);
-+        this.worldGenContext = new WorldGenContext(world, chunkGenerator, structureTemplateManager, this.lightEngine, null, this::setChunkUnsaved); // Paper - rewrite chunk system
+-        this.worldGenContext = new WorldGenContext(level, generator, structureManager, this.lightEngine, mainThreadExecutor, this::setChunkUnsaved);
++        this.worldGenContext = new WorldGenContext(level, generator, structureManager, this.lightEngine, null, this::setChunkUnsaved); // Paper - rewrite chunk system
      }
  
-     private void setChunkUnsaved(ChunkPos pos) {
--        this.chunksToEagerlySave.add(pos.toLong());
+     private void setChunkUnsaved(ChunkPos chunkPos) {
+-        this.chunksToEagerlySave.add(chunkPos.toLong());
 +        // Paper - rewrite chunk system
      }
  
      // Paper start
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -264,23 +263,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
      }
  
-     boolean isChunkTracked(ServerPlayer player, int chunkX, int chunkZ) {
--        return player.getChunkTrackingView().contains(chunkX, chunkZ) && !player.connection.chunkSender.isPending(ChunkPos.asLong(chunkX, chunkZ));
-+        return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().isChunkSent(player, chunkX, chunkZ); // Paper - rewrite chunk system
+     boolean isChunkTracked(ServerPlayer player, int x, int z) {
+-        return player.getChunkTrackingView().contains(x, z) && !player.connection.chunkSender.isPending(ChunkPos.asLong(x, z));
++        return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().isChunkSent(player, x, z); // Paper - rewrite chunk system
      }
  
-     private boolean isChunkOnTrackedBorder(ServerPlayer player, int chunkX, int chunkZ) {
--        if (!this.isChunkTracked(player, chunkX, chunkZ)) {
+     private boolean isChunkOnTrackedBorder(ServerPlayer player, int x, int z) {
+-        if (!this.isChunkTracked(player, x, z)) {
 -            return false;
 -        } else {
--            for (int k = -1; k <= 1; ++k) {
--                for (int l = -1; l <= 1; ++l) {
--                    if ((k != 0 || l != 0) && !this.isChunkTracked(player, chunkX + k, chunkZ + l)) {
+-            for (int i = -1; i <= 1; i++) {
+-                for (int i1 = -1; i1 <= 1; i1++) {
+-                    if ((i != 0 || i1 != 0) && !this.isChunkTracked(player, x + i, z + i1)) {
 -                        return true;
 -                    }
 -                }
@@ -24247,89 +24388,81 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -
 -            return false;
 -        }
-+        return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().isChunkSent(player, chunkX, chunkZ, true); // Paper - rewrite chunk system
++        return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().isChunkSent(player, x, z, true); // Paper - rewrite chunk system
      }
  
      protected ThreadedLevelLightEngine getLightEngine() {
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -289,21 +276,22 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
  
      @Nullable
-     protected ChunkHolder getUpdatingChunkIfPresent(long pos) {
--        return (ChunkHolder) this.updatingChunkMap.get(pos);
+     protected ChunkHolder getUpdatingChunkIfPresent(long chunkPos) {
+-        return this.updatingChunkMap.get(chunkPos);
 +        // Paper start - rewrite chunk system
-+        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(pos);
++        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkPos);
 +        return holder == null ? null : holder.vanillaChunkHolder;
 +        // Paper end - rewrite chunk system
      }
  
      @Nullable
-     public ChunkHolder getVisibleChunkIfPresent(long pos) {
--        return (ChunkHolder) this.visibleChunkMap.get(pos);
+     public ChunkHolder getVisibleChunkIfPresent(long chunkPos) {
+-        return this.visibleChunkMap.get(chunkPos);
 +        // Paper start - rewrite chunk system
-+        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(pos);
++        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkPos);
 +        return holder == null ? null : holder.vanillaChunkHolder;
 +        // Paper end - rewrite chunk system
      }
  
-     protected IntSupplier getChunkQueueLevel(long pos) {
+     protected IntSupplier getChunkQueueLevel(long chunkPos) {
 -        return () -> {
--            ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos);
--
--            return playerchunk == null ? ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1 : Math.min(playerchunk.getQueueLevel(), ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1);
+-            ChunkHolder visibleChunkIfPresent = this.getVisibleChunkIfPresent(chunkPos);
+-            return visibleChunkIfPresent == null
+-                ? ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1
+-                : Math.min(visibleChunkIfPresent.getQueueLevel(), ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1);
 -        };
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
-     public String getChunkDebugData(ChunkPos chunkPos) {
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+     public String getChunkDebugData(ChunkPos pos) {
+@@ -329,47 +317,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
      }
  
-     private CompletableFuture<ChunkResult<List<ChunkAccess>>> getChunkRangeFuture(ChunkHolder centerChunk, int margin, IntFunction<ChunkStatus> distanceToStatus) {
--        if (margin == 0) {
--            ChunkStatus chunkstatus = (ChunkStatus) distanceToStatus.apply(0);
--
--            return centerChunk.scheduleChunkGenerationTask(chunkstatus, this).thenApply((chunkresult) -> {
--                return chunkresult.map(List::of);
--            });
+     private CompletableFuture<ChunkResult<List<ChunkAccess>>> getChunkRangeFuture(ChunkHolder chunkHolder, int range, IntFunction<ChunkStatus> statusGetter) {
+-        if (range == 0) {
+-            ChunkStatus chunkStatus = statusGetter.apply(0);
+-            return chunkHolder.scheduleChunkGenerationTask(chunkStatus, this).thenApply(chunkResult -> chunkResult.map(List::of));
 -        } else {
--            int j = Mth.square(margin * 2 + 1);
--            List<CompletableFuture<ChunkResult<ChunkAccess>>> list = new ArrayList(j);
--            ChunkPos chunkcoordintpair = centerChunk.getPos();
+-            int squared = Mth.square(range * 2 + 1);
+-            List<CompletableFuture<ChunkResult<ChunkAccess>>> list = new ArrayList<>(squared);
+-            ChunkPos pos = chunkHolder.getPos();
 -
--            for (int k = -margin; k <= margin; ++k) {
--                for (int l = -margin; l <= margin; ++l) {
--                    int i1 = Math.max(Math.abs(l), Math.abs(k));
--                    long j1 = ChunkPos.asLong(chunkcoordintpair.x + l, chunkcoordintpair.z + k);
--                    ChunkHolder playerchunk1 = this.getUpdatingChunkIfPresent(j1);
--
--                    if (playerchunk1 == null) {
--                        return ChunkMap.UNLOADED_CHUNK_LIST_FUTURE;
+-            for (int i = -range; i <= range; i++) {
+-                for (int i1 = -range; i1 <= range; i1++) {
+-                    int max = Math.max(Math.abs(i1), Math.abs(i));
+-                    long packedChunkPos = ChunkPos.asLong(pos.x + i1, pos.z + i);
+-                    ChunkHolder updatingChunkIfPresent = this.getUpdatingChunkIfPresent(packedChunkPos);
+-                    if (updatingChunkIfPresent == null) {
+-                        return UNLOADED_CHUNK_LIST_FUTURE;
 -                    }
 -
--                    ChunkStatus chunkstatus1 = (ChunkStatus) distanceToStatus.apply(i1);
--
--                    list.add(playerchunk1.scheduleChunkGenerationTask(chunkstatus1, this));
+-                    ChunkStatus chunkStatus1 = statusGetter.apply(max);
+-                    list.add(updatingChunkIfPresent.scheduleChunkGenerationTask(chunkStatus1, this));
 -                }
 -            }
 -
--            return Util.sequence(list).thenApply((list1) -> {
--                List<ChunkAccess> list2 = new ArrayList(list1.size());
--                Iterator iterator = list1.iterator();
+-            return Util.sequence(list).thenApply(list1 -> {
+-                List<ChunkAccess> list2 = new ArrayList<>(list1.size());
 -
--                while (iterator.hasNext()) {
--                    ChunkResult<ChunkAccess> chunkresult = (ChunkResult) iterator.next();
--
--                    if (chunkresult == null) {
+-                for (ChunkResult<ChunkAccess> chunkResult : list1) {
+-                    if (chunkResult == null) {
 -                        throw this.debugFuturesAndCreateReportedException(new IllegalStateException("At least one of the chunk futures were null"), "n/a");
 -                    }
 -
--                    ChunkAccess ichunkaccess = (ChunkAccess) chunkresult.orElse(null); // CraftBukkit - decompile error
--
--                    if (ichunkaccess == null) {
--                        return ChunkMap.UNLOADED_CHUNK_LIST_RESULT;
+-                    ChunkAccess chunkAccess = chunkResult.orElse(null);
+-                    if (chunkAccess == null) {
+-                        return UNLOADED_CHUNK_LIST_RESULT;
 -                    }
 -
--                    list2.add(ichunkaccess);
+-                    list2.add(chunkAccess);
 -                }
 -
 -                return ChunkResult.of(list2);
@@ -24339,49 +24472,44 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      public ReportedException debugFuturesAndCreateReportedException(IllegalStateException exception, String details) {
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -401,95 +349,29 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
      }
  
-     public CompletableFuture<ChunkResult<LevelChunk>> prepareEntityTickingChunk(ChunkHolder holder) {
--        return this.getChunkRangeFuture(holder, 2, (i) -> {
--            return ChunkStatus.FULL;
--        }).thenApply((chunkresult) -> {
--            return chunkresult.map((list) -> {
--                return (LevelChunk) list.get(list.size() / 2);
--            });
--        });
+     public CompletableFuture<ChunkResult<LevelChunk>> prepareEntityTickingChunk(ChunkHolder chunk) {
+-        return this.getChunkRangeFuture(chunk, 2, i -> ChunkStatus.FULL)
+-            .thenApply(chunkResult -> chunkResult.map(list -> (LevelChunk)list.get(list.size() / 2)));
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
      @Nullable
-     ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k) {
--        if (!ChunkLevel.isLoaded(k) && !ChunkLevel.isLoaded(level)) {
+     ChunkHolder updateChunkScheduling(long chunkPos, int newLevel, @Nullable ChunkHolder holder, int oldLevel) {
+-        if (!ChunkLevel.isLoaded(oldLevel) && !ChunkLevel.isLoaded(newLevel)) {
 -            return holder;
 -        } else {
 -            if (holder != null) {
--                holder.setTicketLevel(level);
+-                holder.setTicketLevel(newLevel);
 -            }
 -
 -            if (holder != null) {
--                if (!ChunkLevel.isLoaded(level)) {
--                    this.toDrop.add(pos);
+-                if (!ChunkLevel.isLoaded(newLevel)) {
+-                    this.toDrop.add(chunkPos);
 -                } else {
--                    this.toDrop.remove(pos);
+-                    this.toDrop.remove(chunkPos);
 -                }
 -            }
 -
--            if (ChunkLevel.isLoaded(level) && holder == null) {
--                holder = (ChunkHolder) this.pendingUnloads.remove(pos);
+-            if (ChunkLevel.isLoaded(newLevel) && holder == null) {
+-                holder = this.pendingUnloads.remove(chunkPos);
 -                if (holder != null) {
--                    holder.setTicketLevel(level);
+-                    holder.setTicketLevel(newLevel);
 -                } else {
--                    holder = new ChunkHolder(new ChunkPos(pos), level, this.level, this.lightEngine, this::onLevelChange, this);
+-                    holder = new ChunkHolder(new ChunkPos(chunkPos), newLevel, this.level, this.lightEngine, this::onLevelChange, this);
 -                    // Paper start
--                    ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderCreate(this.level, holder);
+-                    ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkHolderCreate(this.level, holder);
 -                    // Paper end
 -                }
 -
--                this.updatingChunkMap.put(pos, holder);
+-                this.updatingChunkMap.put(chunkPos, holder);
 -                this.modified = true;
 -            }
 -
@@ -24390,9 +24518,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
-     private void onLevelChange(ChunkPos pos, IntSupplier levelGetter, int targetLevel, IntConsumer levelSetter) {
--        this.worldgenTaskDispatcher.onLevelChange(pos, levelGetter, targetLevel, levelSetter);
--        this.lightTaskDispatcher.onLevelChange(pos, levelGetter, targetLevel, levelSetter);
+     private void onLevelChange(ChunkPos chunkPos, IntSupplier queueLevelGetter, int ticketLevel, IntConsumer queueLevelSetter) {
+-        this.worldgenTaskDispatcher.onLevelChange(chunkPos, queueLevelGetter, ticketLevel, queueLevelSetter);
+-        this.lightTaskDispatcher.onLevelChange(chunkPos, queueLevelGetter, ticketLevel, queueLevelSetter);
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
@@ -24406,43 +24534,39 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -            super.close();
 -        }
 +        throw new UnsupportedOperationException("Use ServerChunkCache#close"); // Paper - rewrite chunk system
- 
      }
  
      protected void saveAllChunks(boolean flush) {
 -        if (flush) {
--            List<ChunkHolder> list = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).toList(); // Paper
--            MutableBoolean mutableboolean = new MutableBoolean();
+-            List<ChunkHolder> list = ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.level) // Paper - moonrise
+-                //.values() // Paper - moonrise
+-                .stream()
+-                .filter(ChunkHolder::wasAccessibleSinceLastSave)
+-                .peek(ChunkHolder::refreshAccessibility)
+-                .toList();
+-            MutableBoolean mutableBoolean = new MutableBoolean();
 -
 -            do {
--                mutableboolean.setFalse();
--                list.stream().map((playerchunk) -> {
--                    BlockableEventLoop iasynctaskhandler = this.mainThreadExecutor;
--
--                    Objects.requireNonNull(playerchunk);
--                    iasynctaskhandler.managedBlock(playerchunk::isReadyForSaving);
--                    return playerchunk.getLatestChunk();
--                }).filter((ichunkaccess) -> {
--                    return ichunkaccess instanceof ImposterProtoChunk || ichunkaccess instanceof LevelChunk;
--                }).filter(this::save).forEach((ichunkaccess) -> {
--                    mutableboolean.setTrue();
--                });
--            } while (mutableboolean.isTrue());
+-                mutableBoolean.setFalse();
+-                list.stream()
+-                    .map(chunk -> {
+-                        this.mainThreadExecutor.managedBlock(chunk::isReadyForSaving);
+-                        return chunk.getLatestChunk();
+-                    })
+-                    .filter(chunk -> chunk instanceof ImposterProtoChunk || chunk instanceof LevelChunk)
+-                    .filter(this::save)
+-                    .forEach(chunk -> mutableBoolean.setTrue());
+-            } while (mutableBoolean.isTrue());
 -
 -            this.poiManager.flushAll();
--            this.processUnloads(() -> {
--                return true;
--            });
+-            this.processUnloads(() -> true);
 -            this.flushWorker();
 -        } else {
 -            this.nextChunkSaveTime.clear();
--            long i = Util.getMillis();
--            Iterator<ChunkHolder> objectiterator = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper
+-            long millis = Util.getMillis();
 -
--            while (objectiterator.hasNext()) {
--                ChunkHolder playerchunk = (ChunkHolder) objectiterator.next();
--
--                this.saveChunkIfNeeded(playerchunk, i);
+-            for (ChunkHolder chunkHolder : ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.level)) { // Paper
+-                this.saveChunkIfNeeded(chunkHolder, millis);
 -            }
 -        }
 +        // Paper start - rewrite chunk system
@@ -24450,112 +24574,104 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            flush, false, false
 +        );
 +        // Paper end - rewrite chunk system
- 
      }
  
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+     protected void tick(BooleanSupplier hasMoreTime) {
+@@ -505,130 +387,28 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
      }
  
      public boolean hasWork() {
--        return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || ca.spottedleaf.moonrise.common.util.ChunkSystem.hasAnyChunkHolders(this.level) || !this.updatingChunkMap.isEmpty() || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.worldgenTaskDispatcher.hasWork() || this.lightTaskDispatcher.hasWork() || this.distanceManager.hasTickets();
+-        return this.lightEngine.hasLightWork()
+-            || !this.pendingUnloads.isEmpty()
+-            || ca.spottedleaf.moonrise.common.PlatformHooks.get().hasAnyChunkHolders(this.level) // Paper - moonrise
+-            || !this.updatingChunkMap.isEmpty()
+-            || this.poiManager.hasWork()
+-            || !this.toDrop.isEmpty()
+-            || !this.unloadQueue.isEmpty()
+-            || this.worldgenTaskDispatcher.hasWork()
+-            || this.lightTaskDispatcher.hasWork()
+-            || this.distanceManager.hasTickets();
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
-     private void processUnloads(BooleanSupplier shouldKeepTicking) {
--        for (LongIterator longiterator = this.toDrop.iterator(); longiterator.hasNext(); longiterator.remove()) {
--            long i = longiterator.nextLong();
--            ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.get(i);
--
--            if (playerchunk != null) {
--                this.updatingChunkMap.remove(i);
--                this.pendingUnloads.put(i, playerchunk);
+     private void processUnloads(BooleanSupplier hasMoreTime) {
+-        for (LongIterator longIterator = this.toDrop.iterator(); longIterator.hasNext(); longIterator.remove()) {
+-            long l = longIterator.nextLong();
+-            ChunkHolder chunkHolder = this.updatingChunkMap.get(l);
+-            if (chunkHolder != null) {
+-                this.updatingChunkMap.remove(l);
+-                this.pendingUnloads.put(l, chunkHolder);
 -                this.modified = true;
--                this.scheduleUnload(i, playerchunk);
+-                this.scheduleUnload(l, chunkHolder);
 -            }
 -        }
 -
--        int j = Math.max(0, this.unloadQueue.size() - 2000);
+-        int max = Math.max(0, this.unloadQueue.size() - 2000);
 -
 -        Runnable runnable;
--
--        while ((j > 0 || shouldKeepTicking.getAsBoolean()) && (runnable = (Runnable) this.unloadQueue.poll()) != null) {
--            --j;
+-        while ((max > 0 || hasMoreTime.getAsBoolean()) && (runnable = this.unloadQueue.poll()) != null) {
+-            max--;
 -            runnable.run();
 -        }
 -
--        this.saveChunksEagerly(shouldKeepTicking);
+-        this.saveChunksEagerly(hasMoreTime);
 +        ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.processUnloads(); // Paper - rewrite chunk system
 +        ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.autoSave(); // Paper - rewrite chunk system
      }
  
-     private void saveChunksEagerly(BooleanSupplier shouldKeepTicking) {
--        long i = Util.getMillis();
--        int j = 0;
--        LongIterator longiterator = this.chunksToEagerlySave.iterator();
+     private void saveChunksEagerly(BooleanSupplier hasMoreTime) {
+-        long millis = Util.getMillis();
+-        int i = 0;
+-        LongIterator longIterator = this.chunksToEagerlySave.iterator();
 -
--        while (j < 20 && this.activeChunkWrites.get() < 128 && shouldKeepTicking.getAsBoolean() && longiterator.hasNext()) {
--            long k = longiterator.nextLong();
--            ChunkHolder playerchunk = (ChunkHolder) this.visibleChunkMap.get(k);
--            ChunkAccess ichunkaccess = playerchunk != null ? playerchunk.getLatestChunk() : null;
--
--            if (ichunkaccess != null && ichunkaccess.isUnsaved()) {
--                if (this.saveChunkIfNeeded(playerchunk, i)) {
--                    ++j;
--                    longiterator.remove();
--                }
--            } else {
--                longiterator.remove();
+-        while (i < 20 && this.activeChunkWrites.get() < 128 && hasMoreTime.getAsBoolean() && longIterator.hasNext()) {
+-            long l = longIterator.nextLong();
+-            ChunkHolder chunkHolder = this.visibleChunkMap.get(l);
+-            ChunkAccess chunkAccess = chunkHolder != null ? chunkHolder.getLatestChunk() : null;
+-            if (chunkAccess == null || !chunkAccess.isUnsaved()) {
+-                longIterator.remove();
+-            } else if (this.saveChunkIfNeeded(chunkHolder, millis)) {
+-                i++;
+-                longIterator.remove();
 -            }
 -        }
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- 
      }
  
-     private void scheduleUnload(long pos, ChunkHolder chunk) {
--        CompletableFuture<?> completablefuture = chunk.getSaveSyncFuture();
--        Runnable runnable = () -> {
--            CompletableFuture<?> completablefuture1 = chunk.getSaveSyncFuture();
--
--            if (completablefuture1 != completablefuture) {
--                this.scheduleUnload(pos, chunk);
+     private void scheduleUnload(long chunkPos, ChunkHolder chunkHolder) {
+-        CompletableFuture<?> saveSyncFuture = chunkHolder.getSaveSyncFuture();
+-        saveSyncFuture.thenRunAsync(() -> {
+-            CompletableFuture<?> saveSyncFuture1 = chunkHolder.getSaveSyncFuture();
+-            if (saveSyncFuture1 != saveSyncFuture) {
+-                this.scheduleUnload(chunkPos, chunkHolder);
 -            } else {
--                ChunkAccess ichunkaccess = chunk.getLatestChunk();
+-                ChunkAccess latestChunk = chunkHolder.getLatestChunk();
 -                // Paper start
 -                boolean removed;
--                if ((removed = this.pendingUnloads.remove(pos, chunk)) && ichunkaccess != null) {
--                    ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, chunk);
+-                if ((removed = this.pendingUnloads.remove(chunkPos, chunkHolder)) && latestChunk != null) {
+-                    ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkHolderDelete(this.level, chunkHolder);
 -                    // Paper end
--                    LevelChunk chunk1;
--
--                    if (ichunkaccess instanceof LevelChunk) {
--                        chunk1 = (LevelChunk) ichunkaccess;
--                        chunk1.setLoaded(false);
+-                    if (latestChunk instanceof LevelChunk levelChunk) {
+-                        levelChunk.setLoaded(false);
 -                    }
 -
--                    this.save(ichunkaccess);
--                    if (ichunkaccess instanceof LevelChunk) {
--                        chunk1 = (LevelChunk) ichunkaccess;
--                        this.level.unload(chunk1);
+-                    this.save(latestChunk);
+-                    if (latestChunk instanceof LevelChunk levelChunk) {
+-                        this.level.unload(levelChunk);
 -                    }
 -
--                    this.lightEngine.updateChunkStatus(ichunkaccess.getPos());
+-                    this.lightEngine.updateChunkStatus(latestChunk.getPos());
 -                    this.lightEngine.tryScheduleUpdate();
--                    this.progressListener.onStatusChange(ichunkaccess.getPos(), (ChunkStatus) null);
--                    this.nextChunkSaveTime.remove(ichunkaccess.getPos().toLong());
+-                    this.progressListener.onStatusChange(latestChunk.getPos(), null);
+-                    this.nextChunkSaveTime.remove(latestChunk.getPos().toLong());
 -                } else if (removed) { // Paper start
--                    ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, chunk);
+-                    ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkHolderDelete(this.level, chunkHolder);
 -                } // Paper end
--
 -            }
--        };
--        Queue queue = this.unloadQueue;
--
--        Objects.requireNonNull(this.unloadQueue);
--        completablefuture.thenRunAsync(runnable, queue::add).whenComplete((ovoid, throwable) -> {
--            if (throwable != null) {
--                ChunkMap.LOGGER.error("Failed to save chunk {}", chunk.getPos(), throwable);
+-        }, this.unloadQueue::add).whenComplete((_void, error) -> {
+-            if (error != null) {
+-                LOGGER.error("Failed to save chunk {}", chunkHolder.getPos(), error);
 -            }
--
 -        });
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
@@ -24571,120 +24687,99 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
-     private CompletableFuture<ChunkAccess> scheduleChunkLoad(ChunkPos pos) {
--        CompletableFuture<Optional<SerializableChunkData>> completablefuture = this.readChunk(pos).thenApplyAsync((optional) -> {
--            return optional.map((nbttagcompound) -> {
--                SerializableChunkData serializablechunkdata = SerializableChunkData.parse(this.level, this.level.registryAccess(), nbttagcompound);
--
--                if (serializablechunkdata == null) {
--                    ChunkMap.LOGGER.error("Chunk file at {} is missing level data, skipping", pos);
--                }
--
--                return serializablechunkdata;
--            });
--        }, Util.backgroundExecutor().forName("parseChunk"));
--        CompletableFuture<?> completablefuture1 = this.poiManager.prefetch(pos);
--
--        return completablefuture.thenCombine(completablefuture1, (optional, object) -> {
--            return optional;
--        }).thenApplyAsync((optional) -> {
--            Profiler.get().incrementCounter("chunkLoad");
--            if (optional.isPresent()) {
--                ProtoChunk protochunk = ((SerializableChunkData) optional.get()).read(this.level, this.poiManager, this.storageInfo(), pos);
--
--                this.markPosition(pos, protochunk.getPersistedStatus().getChunkType());
--                return protochunk;
--            } else {
--                return this.createEmptyChunk(pos);
+     private CompletableFuture<ChunkAccess> scheduleChunkLoad(ChunkPos chunkPos) {
+-        CompletableFuture<Optional<SerializableChunkData>> completableFuture = this.readChunk(chunkPos).thenApplyAsync(optional -> optional.map(tag -> {
+-            SerializableChunkData serializableChunkData = SerializableChunkData.parse(this.level, this.level.registryAccess(), tag);
+-            if (serializableChunkData == null) {
+-                LOGGER.error("Chunk file at {} is missing level data, skipping", chunkPos);
 -            }
--        }, this.mainThreadExecutor).exceptionallyAsync((throwable) -> {
--            return this.handleChunkLoadFailure(throwable, pos);
--        }, this.mainThreadExecutor);
-+        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
-     }
- 
-     private ChunkAccess handleChunkLoadFailure(Throwable throwable, ChunkPos chunkPos) {
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- 
-     @Override
-     public GenerationChunkHolder acquireGeneration(long pos) {
--        ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.get(pos);
 -
--        playerchunk.increaseGenerationRefCount();
--        return playerchunk;
+-            return serializableChunkData;
+-        }), Util.backgroundExecutor().forName("parseChunk"));
+-        CompletableFuture<?> completableFuture1 = this.poiManager.prefetch(chunkPos);
+-        return completableFuture.<Object, Optional<SerializableChunkData>>thenCombine(
+-                (CompletionStage<? extends Object>)completableFuture1, (optional, object) -> optional
+-            )
+-            .thenApplyAsync(optional -> {
+-                Profiler.get().incrementCounter("chunkLoad");
+-                if (optional.isPresent()) {
+-                    ChunkAccess chunkAccess = optional.get().read(this.level, this.poiManager, this.storageInfo(), chunkPos);
+-                    this.markPosition(chunkPos, chunkAccess.getPersistedStatus().getChunkType());
+-                    return chunkAccess;
+-                } else {
+-                    return this.createEmptyChunk(chunkPos);
+-                }
+-            }, this.mainThreadExecutor)
+-            .exceptionallyAsync(throwable -> this.handleChunkLoadFailure(throwable, chunkPos), this.mainThreadExecutor);
++        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+     }
+ 
+     private ChunkAccess handleChunkLoadFailure(Throwable exception, ChunkPos chunkPos) {
+@@ -666,108 +446,43 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ 
+     @Override
+     public GenerationChunkHolder acquireGeneration(long chunkPos) {
+-        ChunkHolder chunkHolder = this.updatingChunkMap.get(chunkPos);
+-        chunkHolder.increaseGenerationRefCount();
+-        return chunkHolder;
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
      @Override
-     public void releaseGeneration(GenerationChunkHolder chunkHolder) {
--        chunkHolder.decreaseGenerationRefCount();
+     public void releaseGeneration(GenerationChunkHolder chunk) {
+-        chunk.decreaseGenerationRefCount();
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
      @Override
-     public CompletableFuture<ChunkAccess> applyStep(GenerationChunkHolder chunkHolder, ChunkStep step, StaticCache2D<GenerationChunkHolder> chunks) {
--        ChunkPos chunkcoordintpair = chunkHolder.getPos();
--
+     public CompletableFuture<ChunkAccess> applyStep(GenerationChunkHolder chunk, ChunkStep step, StaticCache2D<GenerationChunkHolder> cache) {
+-        ChunkPos pos = chunk.getPos();
 -        if (step.targetStatus() == ChunkStatus.EMPTY) {
--            return this.scheduleChunkLoad(chunkcoordintpair);
+-            return this.scheduleChunkLoad(pos);
 -        } else {
 -            try {
--                GenerationChunkHolder generationchunkholder1 = (GenerationChunkHolder) chunks.get(chunkcoordintpair.x, chunkcoordintpair.z);
--                ChunkAccess ichunkaccess = generationchunkholder1.getChunkIfPresentUnchecked(step.targetStatus().getParent());
--
--                if (ichunkaccess == null) {
+-                GenerationChunkHolder generationChunkHolder = cache.get(pos.x, pos.z);
+-                ChunkAccess chunkIfPresentUnchecked = generationChunkHolder.getChunkIfPresentUnchecked(step.targetStatus().getParent());
+-                if (chunkIfPresentUnchecked == null) {
 -                    throw new IllegalStateException("Parent chunk missing");
 -                } else {
--                    CompletableFuture<ChunkAccess> completablefuture = step.apply(this.worldGenContext, chunks, ichunkaccess);
--
--                    this.progressListener.onStatusChange(chunkcoordintpair, step.targetStatus());
--                    return completablefuture;
+-                    CompletableFuture<ChunkAccess> completableFuture = step.apply(this.worldGenContext, cache, chunkIfPresentUnchecked);
+-                    this.progressListener.onStatusChange(pos, step.targetStatus());
+-                    return completableFuture;
 -                }
--            } catch (Exception exception) {
--                exception.getStackTrace();
--                CrashReport crashreport = CrashReport.forThrowable(exception, "Exception generating new chunk");
--                CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Chunk to be generated");
--
--                crashreportsystemdetails.setDetail("Status being generated", () -> {
--                    return step.targetStatus().getName();
--                });
--                crashreportsystemdetails.setDetail("Location", (Object) String.format(Locale.ROOT, "%d,%d", chunkcoordintpair.x, chunkcoordintpair.z));
--                crashreportsystemdetails.setDetail("Position hash", (Object) ChunkPos.asLong(chunkcoordintpair.x, chunkcoordintpair.z));
--                crashreportsystemdetails.setDetail("Generator", (Object) this.generator());
+-            } catch (Exception var8) {
+-                var8.getStackTrace();
+-                CrashReport crashReport = CrashReport.forThrowable(var8, "Exception generating new chunk");
+-                CrashReportCategory crashReportCategory = crashReport.addCategory("Chunk to be generated");
+-                crashReportCategory.setDetail("Status being generated", () -> step.targetStatus().getName());
+-                crashReportCategory.setDetail("Location", String.format(Locale.ROOT, "%d,%d", pos.x, pos.z));
+-                crashReportCategory.setDetail("Position hash", ChunkPos.asLong(pos.x, pos.z));
+-                crashReportCategory.setDetail("Generator", this.generator());
 -                this.mainThreadExecutor.execute(() -> {
--                    throw new ReportedException(crashreport);
+-                    throw new ReportedException(crashReport);
 -                });
--                throw new ReportedException(crashreport);
+-                throw new ReportedException(crashReport);
 -            }
 -        }
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
      @Override
-     public ChunkGenerationTask scheduleGenerationTask(ChunkStatus requestedStatus, ChunkPos pos) {
--        ChunkGenerationTask chunkgenerationtask = ChunkGenerationTask.create(this, requestedStatus, pos);
--
--        this.pendingGenerationTasks.add(chunkgenerationtask);
--        return chunkgenerationtask;
+     public ChunkGenerationTask scheduleGenerationTask(ChunkStatus targetStatus, ChunkPos pos) {
+-        ChunkGenerationTask chunkGenerationTask = ChunkGenerationTask.create(this, targetStatus, pos);
+-        this.pendingGenerationTasks.add(chunkGenerationTask);
+-        return chunkGenerationTask;
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
-     private void runGenerationTask(ChunkGenerationTask loader) {
--        GenerationChunkHolder generationchunkholder = loader.getCenter();
--        ChunkTaskDispatcher chunktaskdispatcher = this.worldgenTaskDispatcher;
--        Runnable runnable = () -> {
--            CompletableFuture<?> completablefuture = loader.runUntilWait();
--
--            if (completablefuture != null) {
--                completablefuture.thenRun(() -> {
--                    this.runGenerationTask(loader);
--                });
+     private void runGenerationTask(ChunkGenerationTask task) {
+-        GenerationChunkHolder center = task.getCenter();
+-        this.worldgenTaskDispatcher.submit(() -> {
+-            CompletableFuture<?> completableFuture = task.runUntilWait();
+-            if (completableFuture != null) {
+-                completableFuture.thenRun(() -> this.runGenerationTask(task));
 -            }
--        };
--        long i = generationchunkholder.getPos().toLong();
--
--        Objects.requireNonNull(generationchunkholder);
--        chunktaskdispatcher.submit(runnable, i, generationchunkholder::getQueueLevel);
+-        }, center.getPos().toLong(), center::getQueueLevel);
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
@@ -24696,46 +24791,34 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      public CompletableFuture<ChunkResult<LevelChunk>> prepareTickingChunk(ChunkHolder holder) {
--        CompletableFuture<ChunkResult<List<ChunkAccess>>> completablefuture = this.getChunkRangeFuture(holder, 1, (i) -> {
--            return ChunkStatus.FULL;
--        });
--        CompletableFuture<ChunkResult<LevelChunk>> completablefuture1 = completablefuture.thenApplyAsync((chunkresult) -> {
--            return chunkresult.map((list) -> {
--                LevelChunk chunk = (LevelChunk) list.get(list.size() / 2);
+-        CompletableFuture<ChunkResult<List<ChunkAccess>>> chunkRangeFuture = this.getChunkRangeFuture(holder, 1, i -> ChunkStatus.FULL);
+-        CompletableFuture<ChunkResult<LevelChunk>> completableFuture = chunkRangeFuture.thenApplyAsync(chunk -> chunk.map(list -> {
+-            LevelChunk levelChunk = (LevelChunk)list.get(list.size() / 2);
+-            levelChunk.postProcessGeneration(this.level);
+-            this.level.startTickingChunk(levelChunk);
+-            CompletableFuture<?> sendSyncFuture = holder.getSendSyncFuture();
+-            if (sendSyncFuture.isDone()) {
+-                this.onChunkReadyToSend(holder, levelChunk);
+-            } else {
+-                sendSyncFuture.thenAcceptAsync(object -> this.onChunkReadyToSend(holder, levelChunk), this.mainThreadExecutor);
+-            }
 -
--                chunk.postProcessGeneration(this.level);
--                this.level.startTickingChunk(chunk);
--                CompletableFuture<?> completablefuture2 = holder.getSendSyncFuture();
--
--                if (completablefuture2.isDone()) {
--                    this.onChunkReadyToSend(holder, chunk);
--                } else {
--                    completablefuture2.thenAcceptAsync((object) -> {
--                        this.onChunkReadyToSend(holder, chunk);
--                    }, this.mainThreadExecutor);
--                }
--
--                return chunk;
--            });
--        }, this.mainThreadExecutor);
--
--        completablefuture1.handle((chunkresult, throwable) -> {
+-            return levelChunk;
+-        }), this.mainThreadExecutor);
+-        completableFuture.handle((chunk, exception) -> {
 -            this.tickingGenerated.getAndIncrement();
 -            return null;
 -        });
--        return completablefuture1;
+-        return completableFuture;
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
      private void onChunkReadyToSend(ChunkHolder chunkHolder, LevelChunk chunk) {
--        ChunkPos chunkcoordintpair = chunk.getPos();
--        Iterator iterator = this.playerMap.getAllPlayers().iterator();
+-        ChunkPos pos = chunk.getPos();
 -
--        while (iterator.hasNext()) {
--            ServerPlayer entityplayer = (ServerPlayer) iterator.next();
--
--            if (entityplayer.getChunkTrackingView().contains(chunkcoordintpair)) {
--                ChunkMap.markChunkPendingToSend(entityplayer, chunk);
+-        for (ServerPlayer serverPlayer : this.playerMap.getAllPlayers()) {
+-            if (serverPlayer.getChunkTrackingView().contains(pos)) {
+-                markChunkPendingToSend(serverPlayer, chunk);
 -            }
 -        }
 -
@@ -24743,39 +24826,33 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
-     public CompletableFuture<ChunkResult<LevelChunk>> prepareAccessibleChunk(ChunkHolder holder) {
--        return this.getChunkRangeFuture(holder, 1, ChunkLevel::getStatusAroundFullChunk).thenApply((chunkresult) -> {
--            return chunkresult.map((list) -> {
--                return (LevelChunk) list.get(list.size() / 2);
--            });
--        });
+     public CompletableFuture<ChunkResult<LevelChunk>> prepareAccessibleChunk(ChunkHolder chunk) {
+-        return this.getChunkRangeFuture(chunk, 1, ChunkLevel::getStatusAroundFullChunk)
+-            .thenApply(chunkResult -> chunkResult.map(list -> (LevelChunk)list.get(list.size() / 2)));
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
      public int getTickingGenerated() {
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -775,125 +490,78 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
      }
  
-     private boolean saveChunkIfNeeded(ChunkHolder chunkHolder, long currentTime) {
--        if (chunkHolder.wasAccessibleSinceLastSave() && chunkHolder.isReadyForSaving()) {
--            ChunkAccess ichunkaccess = chunkHolder.getLatestChunk();
--
--            if (!(ichunkaccess instanceof ImposterProtoChunk) && !(ichunkaccess instanceof LevelChunk)) {
+     private boolean saveChunkIfNeeded(ChunkHolder chunk, long gametime) {
+-        if (chunk.wasAccessibleSinceLastSave() && chunk.isReadyForSaving()) {
+-            ChunkAccess latestChunk = chunk.getLatestChunk();
+-            if (!(latestChunk instanceof ImposterProtoChunk) && !(latestChunk instanceof LevelChunk)) {
 -                return false;
--            } else if (!ichunkaccess.isUnsaved()) {
+-            } else if (!latestChunk.isUnsaved()) {
 -                return false;
 -            } else {
--                long j = ichunkaccess.getPos().toLong();
--                long k = this.nextChunkSaveTime.getOrDefault(j, -1L);
--
--                if (currentTime < k) {
+-                long packedChunkPos = latestChunk.getPos().toLong();
+-                long orDefault = this.nextChunkSaveTime.getOrDefault(packedChunkPos, -1L);
+-                if (gametime < orDefault) {
 -                    return false;
 -                } else {
--                    boolean flag = this.save(ichunkaccess);
--
--                    chunkHolder.refreshAccessibility();
+-                    boolean flag = this.save(latestChunk);
+-                    chunk.refreshAccessibility();
 -                    if (flag) {
--                        this.nextChunkSaveTime.put(j, currentTime + 10000L);
+-                        this.nextChunkSaveTime.put(packedChunkPos, gametime + 10000L);
 -                    }
 -
 -                    return flag;
@@ -24792,111 +24869,97 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -        if (!chunk.tryMarkSaved()) {
 -            return false;
 -        } else {
--            ChunkPos chunkcoordintpair = chunk.getPos();
+-            ChunkPos pos = chunk.getPos();
 -
 -            try {
--                ChunkStatus chunkstatus = chunk.getPersistedStatus();
--
--                if (chunkstatus.getChunkType() != ChunkType.LEVELCHUNK) {
--                    if (this.isExistingChunkFull(chunkcoordintpair)) {
+-                ChunkStatus persistedStatus = chunk.getPersistedStatus();
+-                if (persistedStatus.getChunkType() != ChunkType.LEVELCHUNK) {
+-                    if (this.isExistingChunkFull(pos)) {
 -                        return false;
 -                    }
 -
--                    if (chunkstatus == ChunkStatus.EMPTY && chunk.getAllStarts().values().stream().noneMatch(StructureStart::isValid)) {
+-                    if (persistedStatus == ChunkStatus.EMPTY && chunk.getAllStarts().values().stream().noneMatch(StructureStart::isValid)) {
 -                        return false;
 -                    }
 -                }
 -
 -                Profiler.get().incrementCounter("chunkSave");
 -                this.activeChunkWrites.incrementAndGet();
--                SerializableChunkData serializablechunkdata = SerializableChunkData.copyOf(this.level, chunk);
--
--                Objects.requireNonNull(serializablechunkdata);
--                CompletableFuture<CompoundTag> completablefuture = CompletableFuture.supplyAsync(serializablechunkdata::write, Util.backgroundExecutor());
--
--                Objects.requireNonNull(completablefuture);
--                this.write(chunkcoordintpair, completablefuture::join).handle((ovoid, throwable) -> {
--                    if (throwable != null) {
--                        this.level.getServer().reportChunkSaveFailure(throwable, this.storageInfo(), chunkcoordintpair);
+-                SerializableChunkData serializableChunkData = SerializableChunkData.copyOf(this.level, chunk);
+-                CompletableFuture<CompoundTag> completableFuture = CompletableFuture.supplyAsync(serializableChunkData::write, Util.backgroundExecutor());
+-                this.write(pos, completableFuture::join).handle((_void, exception1) -> {
+-                    if (exception1 != null) {
+-                        this.level.getServer().reportChunkSaveFailure(exception1, this.storageInfo(), pos);
 -                    }
 -
 -                    this.activeChunkWrites.decrementAndGet();
 -                    return null;
 -                });
--                this.markPosition(chunkcoordintpair, chunkstatus.getChunkType());
+-                this.markPosition(pos, persistedStatus.getChunkType());
 -                return true;
--            } catch (Exception exception) {
--                this.level.getServer().reportChunkSaveFailure(exception, this.storageInfo(), chunkcoordintpair);
+-            } catch (Exception var6) {
+-                this.level.getServer().reportChunkSaveFailure(var6, this.storageInfo(), pos);
 -                return false;
 -            }
 -        }
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
-     private boolean isExistingChunkFull(ChunkPos pos) {
--        byte b0 = this.chunkTypeCache.get(pos.toLong());
--
--        if (b0 != 0) {
--            return b0 == 1;
+     private boolean isExistingChunkFull(ChunkPos chunkPos) {
+-        byte b = this.chunkTypeCache.get(chunkPos.toLong());
+-        if (b != 0) {
+-            return b == 1;
 -        } else {
--            CompoundTag nbttagcompound;
--
+-            CompoundTag compoundTag;
 -            try {
--                nbttagcompound = (CompoundTag) ((Optional) this.readChunk(pos).join()).orElse((Object) null);
--                if (nbttagcompound == null) {
--                    this.markPositionReplaceable(pos);
+-                compoundTag = this.readChunk(chunkPos).join().orElse(null);
+-                if (compoundTag == null) {
+-                    this.markPositionReplaceable(chunkPos);
 -                    return false;
 -                }
--            } catch (Exception exception) {
--                ChunkMap.LOGGER.error("Failed to read chunk {}", pos, exception);
--                this.markPositionReplaceable(pos);
+-            } catch (Exception var5) {
+-                LOGGER.error("Failed to read chunk {}", chunkPos, var5);
+-                this.markPositionReplaceable(chunkPos);
 -                return false;
 -            }
 -
--            ChunkType chunktype = SerializableChunkData.getChunkTypeFromTag(nbttagcompound);
--
--            return this.markPosition(pos, chunktype) == 1;
+-            ChunkType chunkTypeFromTag = SerializableChunkData.getChunkTypeFromTag(compoundTag);
+-            return this.markPosition(chunkPos, chunkTypeFromTag) == 1;
 -        }
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
-     public void setServerViewDistance(int watchDistance) { // Paper - public
--        int j = Mth.clamp(watchDistance, 2, 32);
--
--        if (j != this.serverViewDistance) {
--            this.serverViewDistance = j;
+     public void setServerViewDistance(int viewDistance) {
+-        int i = Mth.clamp(viewDistance, 2, 32);
+-        if (i != this.serverViewDistance) {
+-            this.serverViewDistance = i;
 -            this.distanceManager.updatePlayerTickets(this.serverViewDistance);
--            Iterator iterator = this.playerMap.getAllPlayers().iterator();
 -
--            while (iterator.hasNext()) {
--                ServerPlayer entityplayer = (ServerPlayer) iterator.next();
--
--                this.updateChunkTracking(entityplayer);
+-            for (ServerPlayer serverPlayer : this.playerMap.getAllPlayers()) {
+-                this.updateChunkTracking(serverPlayer);
 -            }
 +        // Paper start - rewrite chunk system
-+        final int clamped = Mth.clamp(watchDistance, 2, ca.spottedleaf.moonrise.common.util.MoonriseConstants.MAX_VIEW_DISTANCE);
++        final int clamped = Mth.clamp(viewDistance, 2, ca.spottedleaf.moonrise.common.util.MoonriseConstants.MAX_VIEW_DISTANCE);
 +        if (clamped == this.serverViewDistance) {
 +            return;
          }
- 
++
 +        this.serverViewDistance = clamped;
 +        ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().setLoadDistance(this.serverViewDistance + 1);
 +        // Paper end - rewrite chunk system
      }
  
-     public int getPlayerViewDistance(ServerPlayer player) { // Paper - public
+     int getPlayerViewDistance(ServerPlayer player) {
 -        return Mth.clamp(player.requestedViewDistance(), 2, this.serverViewDistance);
-+        return ca.spottedleaf.moonrise.common.util.ChunkSystem.getSendViewDistance(player); // Paper - rewrite chunk system
++        return ca.spottedleaf.moonrise.common.PlatformHooks.get().getSendViewDistance(player); // Paper - rewrite chunk system
      }
  
-     private void markChunkPendingToSend(ServerPlayer player, ChunkPos pos) {
--        LevelChunk chunk = this.getChunkToSend(pos.toLong());
--
--        if (chunk != null) {
--            ChunkMap.markChunkPendingToSend(player, chunk);
+     private void markChunkPendingToSend(ServerPlayer player, ChunkPos chunkPos) {
+-        LevelChunk chunkToSend = this.getChunkToSend(chunkPos.toLong());
+-        if (chunkToSend != null) {
+-            markChunkPendingToSend(player, chunkToSend);
 -        }
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- 
      }
  
      private static void markChunkPendingToSend(ServerPlayer player, LevelChunk chunk) {
@@ -24904,8 +24967,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
-     private static void dropChunk(ServerPlayer player, ChunkPos pos) {
--        player.connection.chunkSender.dropChunk(player, pos);
+     private static void dropChunk(ServerPlayer player, ChunkPos chunkPos) {
+-        player.connection.chunkSender.dropChunk(player, chunkPos);
 +        // Paper - rewrite chunk system
 +    }
 +
@@ -24935,116 +24998,98 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionFileType.CHUNK_DATA
 +        );
 +        return null;
-     }
- 
++    }
++
 +    @Override
 +    public void flushWorker() {
 +        ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.flush(this.level);
-+    }
+     }
 +    // Paper end - rewrite chunk system
-+
+ 
      @Nullable
-     public LevelChunk getChunkToSend(long pos) {
-         ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos);
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+     public LevelChunk getChunkToSend(long chunkPos) {
+@@ -981,7 +649,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
      }
  
      // CraftBukkit start
--    private CompoundTag upgradeChunkTag(CompoundTag nbttagcompound, ChunkPos chunkcoordintpair) {
-+    public CompoundTag upgradeChunkTag(CompoundTag nbttagcompound, ChunkPos chunkcoordintpair) { // Paper - public
-         return this.upgradeChunkTag(this.level.getTypeKey(), this.overworldDataStorage, nbttagcompound, this.generator().getTypeNameForDataFixer(), chunkcoordintpair, this.level);
-         // CraftBukkit end
+-    private CompoundTag upgradeChunkTag(CompoundTag tag, ChunkPos pos) {
++    public CompoundTag upgradeChunkTag(CompoundTag tag, ChunkPos pos) { // Paper - public
+         return this.upgradeChunkTag(this.level.getTypeKey(), this.overworldDataStorage, tag, this.generator().getTypeNameForDataFixer(), pos, this.level);
+     // CraftBukkit end
      }
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -991,7 +659,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
  
-         while (longiterator.hasNext()) {
-             long i = longiterator.nextLong();
--            ChunkHolder playerchunk = (ChunkHolder) this.visibleChunkMap.get(i);
-+            ChunkHolder playerchunk = (ChunkHolder) this.getVisibleChunkIfPresent(i); // Paper - rewrite chunk system
- 
-             if (playerchunk != null && this.anyPlayerCloseEnoughForSpawningInternal(playerchunk.getPos())) {
-                 callback.accept(playerchunk);
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+         while (spawnCandidateChunks.hasNext()) {
+             long l = spawnCandidateChunks.nextLong();
+-            ChunkHolder chunkHolder = this.visibleChunkMap.get(l);
++            ChunkHolder chunkHolder = this.getVisibleChunkIfPresent(l); // Paper - rewrite chunk system
+             if (chunkHolder != null && this.anyPlayerCloseEnoughForSpawningInternal(chunkHolder.getPos())) {
+                 action.accept(chunkHolder);
+             }
+@@ -1004,7 +672,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
      }
  
-     boolean anyPlayerCloseEnoughForSpawning(ChunkPos chunkcoordintpair, boolean reducedRange) {
--        return !this.distanceManager.hasPlayersNearby(chunkcoordintpair.toLong()) ? false : this.anyPlayerCloseEnoughForSpawningInternal(chunkcoordintpair, reducedRange);
-+        return this.anyPlayerCloseEnoughForSpawningInternal(chunkcoordintpair, reducedRange); // Paper - chunk tick iteration optimisation
+     boolean anyPlayerCloseEnoughForSpawning(ChunkPos chunkPos, boolean reducedRange) {
+-        return this.distanceManager.hasPlayersNearby(chunkPos.toLong()) && this.anyPlayerCloseEnoughForSpawningInternal(chunkPos, reducedRange);
++        return this.anyPlayerCloseEnoughForSpawningInternal(chunkPos, reducedRange); // Paper - chunk tick iteration optimisation
          // Spigot end
      }
  
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-     private boolean anyPlayerCloseEnoughForSpawningInternal(ChunkPos chunkcoordintpair, boolean reducedRange) {
+@@ -1016,7 +684,20 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+     private boolean anyPlayerCloseEnoughForSpawningInternal(ChunkPos chunkPos, boolean reducedRange) {
          double blockRange; // Paper - use from event
          // Spigot end
--        Iterator iterator = this.playerMap.getAllPlayers().iterator();
--
--        ServerPlayer entityplayer;
+-        for (ServerPlayer serverPlayer : this.playerMap.getAllPlayers()) {
 +        // Paper start - chunk tick iteration optimisation
 +        final ca.spottedleaf.moonrise.common.list.ReferenceList<ServerPlayer> players = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getNearbyPlayers().getPlayers(
-+            chunkcoordintpair, ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.SPAWN_RANGE
++            chunkPos, ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.SPAWN_RANGE
 +        );
 +        if (players == null) {
 +            return false;
 +        }
- 
--        do {
--            if (!iterator.hasNext()) {
--                return false;
--            }
++
 +        final ServerPlayer[] raw = players.getRawDataUnchecked();
 +        final int len = players.size();
- 
--            entityplayer = (ServerPlayer) iterator.next();
++
 +        Objects.checkFromIndexSize(0, len, raw.length);
 +        for (int i = 0; i < len; ++i) {
-+            final ServerPlayer entityplayer = raw[i];
++            final ServerPlayer serverPlayer = raw[i];
              // Paper start - PlayerNaturallySpawnCreaturesEvent
              com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent event;
              blockRange = 16384.0D;
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-                 blockRange = (double) ((event.getSpawnRadius() << 4) * (event.getSpawnRadius() << 4));
-             }
-             // Paper end - PlayerNaturallySpawnCreaturesEvent
--        } while (!this.playerIsCloseEnoughForSpawning(entityplayer, chunkcoordintpair, blockRange)); // Spigot
-+            if (this.playerIsCloseEnoughForSpawning(entityplayer, chunkcoordintpair, blockRange)) {
-+                return true;
-+            }
-+        }
+@@ -1032,26 +713,41 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+         }
  
--        return true;
-+        return false;
+         return false;
 +        // Paper end - chunk tick iteration optimisation
      }
  
-     public List<ServerPlayer> getPlayersCloseForSpawning(ChunkPos pos) {
--        long i = pos.toLong();
+     public List<ServerPlayer> getPlayersCloseForSpawning(ChunkPos chunkPos) {
+-        long packedChunkPos = chunkPos.toLong();
+-        if (!this.distanceManager.hasPlayersNearby(packedChunkPos)) {
+-            return List.of();
+-        } else {
+-            Builder<ServerPlayer> builder = ImmutableList.builder();
 +        // Paper start - chunk tick iteration optimisation
 +        final ca.spottedleaf.moonrise.common.list.ReferenceList<ServerPlayer> players = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getNearbyPlayers().getPlayers(
-+            pos, ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.SPAWN_RANGE
++            chunkPos, ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.SPAWN_RANGE
 +        );
 +        if (players == null) {
 +            return new ArrayList<>();
 +        }
- 
--        if (!this.distanceManager.hasPlayersNearby(i)) {
--            return List.of();
--        } else {
--            Builder<ServerPlayer> builder = ImmutableList.builder();
--            Iterator iterator = this.playerMap.getAllPlayers().iterator();
++
 +        List<ServerPlayer> ret = null;
- 
--            while (iterator.hasNext()) {
--                ServerPlayer entityplayer = (ServerPlayer) iterator.next();
++
 +        final ServerPlayer[] raw = players.getRawDataUnchecked();
 +        final int len = players.size();
  
--                if (this.playerIsCloseEnoughForSpawning(entityplayer, pos, 16384.0D)) { // Spigot
--                    builder.add(entityplayer);
+-            for (ServerPlayer serverPlayer : this.playerMap.getAllPlayers()) {
+-                if (this.playerIsCloseEnoughForSpawning(serverPlayer, chunkPos, 16384.0D)) { // Spigot
+-                    builder.add(serverPlayer);
 +        Objects.checkFromIndexSize(0, len, raw.length);
 +        for (int i = 0; i < len; ++i) {
 +            final ServerPlayer player = raw[i];
-+            if (this.playerIsCloseEnoughForSpawning(player, pos, 16384.0D)) { // Spigot
++            if (this.playerIsCloseEnoughForSpawning(player, chunkPos, 16384.0D)) { // Spigot
 +                if (ret == null) {
 +                    ret = new ArrayList<>(len - i);
 +                    ret.add(player);
@@ -25060,137 +25105,111 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - chunk tick iteration optimisation
      }
  
--    private boolean playerIsCloseEnoughForSpawning(ServerPlayer entityplayer, ChunkPos chunkcoordintpair, double range) { // Spigot
-+    public boolean playerIsCloseEnoughForSpawning(ServerPlayer entityplayer, ChunkPos chunkcoordintpair, double range) { // Spigot // Paper - chunk tick iteration optimisation - public
-         if (entityplayer.isSpectator()) {
+-    private boolean playerIsCloseEnoughForSpawning(ServerPlayer player, ChunkPos chunkPos, double range) { // Spigot
++    public boolean playerIsCloseEnoughForSpawning(ServerPlayer player, ChunkPos chunkPos, double range) { // Spigot // Paper - chunk tick iteration optimisation - public
+         if (player.isSpectator()) {
              return false;
          } else {
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -1072,18 +768,20 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
              this.updatePlayerPos(player);
-             if (!flag1) {
-                 this.distanceManager.addPlayer(SectionPos.of((EntityAccess) player), player);
+             if (!flag) {
+                 this.distanceManager.addPlayer(SectionPos.of(player), player);
 +                ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$addPlayer(player, SectionPos.of(player)); // Paper - chunk tick iteration optimisation
              }
  
              player.setChunkTrackingView(ChunkTrackingView.EMPTY);
 -            this.updateChunkTracking(player);
-+            ca.spottedleaf.moonrise.common.util.ChunkSystem.addPlayerToDistanceMaps(this.level, player); // Paper - rewrite chunk system
++            ca.spottedleaf.moonrise.common.PlatformHooks.get().addPlayerToDistanceMaps(this.level, player); // Paper - rewrite chunk system
          } else {
-             SectionPos sectionposition = player.getLastSectionPos();
- 
+             SectionPos lastSectionPos = player.getLastSectionPos();
              this.playerMap.removePlayer(player);
-             if (!flag2) {
-                 this.distanceManager.removePlayer(sectionposition, player);
+             if (!flag1) {
+                 this.distanceManager.removePlayer(lastSectionPos, player);
 +                ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$removePlayer(player, SectionPos.of(player)); // Paper - chunk tick iteration optimisation
              }
  
 -            this.applyChunkTrackingView(player, ChunkTrackingView.EMPTY);
-+            ca.spottedleaf.moonrise.common.util.ChunkSystem.removePlayerFromDistanceMaps(this.level, player); // Paper - rewrite chunk system
++            ca.spottedleaf.moonrise.common.PlatformHooks.get().removePlayerFromDistanceMaps(this.level, player); // Paper - rewrite chunk system
          }
- 
      }
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ 
+@@ -1093,13 +791,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
      }
  
      public void move(ServerPlayer player) {
--        ObjectIterator objectiterator = this.entityMap.values().iterator();
--
--        while (objectiterator.hasNext()) {
--            ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next();
--
--            if (playerchunkmap_entitytracker.entity == player) {
--                playerchunkmap_entitytracker.updatePlayers(this.level.players());
+-        for (ChunkMap.TrackedEntity trackedEntity : this.entityMap.values()) {
+-            if (trackedEntity.entity == player) {
+-                trackedEntity.updatePlayers(this.level.players());
 -            } else {
--                playerchunkmap_entitytracker.updatePlayer(player);
+-                trackedEntity.updatePlayer(player);
 -            }
 -        }
 +        // Paper - optimise entity tracker
  
-         SectionPos sectionposition = player.getLastSectionPos();
-         SectionPos sectionposition1 = SectionPos.of((EntityAccess) player);
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- 
+         SectionPos lastSectionPos = player.getLastSectionPos();
+         SectionPos sectionPos = SectionPos.of(player);
+@@ -1108,6 +800,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+         boolean flag2 = lastSectionPos.asLong() != sectionPos.asLong();
          if (flag2 || flag != flag1) {
              this.updatePlayerPos(player);
-+            ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$updatePlayer(player, sectionposition, sectionposition1, flag, flag1); // Paper - chunk tick iteration optimisation
++            ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$updatePlayer(player, lastSectionPos, sectionPos, flag, flag1); // Paper - chunk tick iteration optimisation
              if (!flag) {
-                 this.distanceManager.removePlayer(sectionposition, player);
+                 this.distanceManager.removePlayer(lastSectionPos, player);
              }
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -1124,49 +817,29 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
                  this.playerMap.unIgnorePlayer(player);
              }
  
 -            this.updateChunkTracking(player);
 +            // Paper - rewrite chunk system
          }
- 
-+        ca.spottedleaf.moonrise.common.util.ChunkSystem.updateMaps(this.level, player); // Paper - rewrite chunk system
++        ca.spottedleaf.moonrise.common.PlatformHooks.get().updateMaps(this.level, player); // Paper - rewrite chunk system
      }
  
      private void updateChunkTracking(ServerPlayer player) {
--        ChunkPos chunkcoordintpair = player.chunkPosition();
--        int i = this.getPlayerViewDistance(player);
--        ChunkTrackingView chunktrackingview = player.getChunkTrackingView();
--
--        if (chunktrackingview instanceof ChunkTrackingView.Positioned chunktrackingview_a) {
--            if (chunktrackingview_a.center().equals(chunkcoordintpair) && chunktrackingview_a.viewDistance() == i) {
--                return;
--            }
+-        ChunkPos chunkPos = player.chunkPosition();
+-        int playerViewDistance = this.getPlayerViewDistance(player);
+-        if (!(
+-            player.getChunkTrackingView() instanceof ChunkTrackingView.Positioned positioned
+-                && positioned.center().equals(chunkPos)
+-                && positioned.viewDistance() == playerViewDistance
+-        )) {
+-            this.applyChunkTrackingView(player, ChunkTrackingView.of(chunkPos, playerViewDistance));
 -        }
--
--        this.applyChunkTrackingView(player, ChunkTrackingView.of(chunkcoordintpair, i));
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
-     private void applyChunkTrackingView(ServerPlayer player, ChunkTrackingView chunkFilter) {
+     private void applyChunkTrackingView(ServerPlayer player, ChunkTrackingView chunkTrackingView) {
 -        if (player.level() == this.level) {
--            ChunkTrackingView chunktrackingview1 = player.getChunkTrackingView();
--
--            if (chunkFilter instanceof ChunkTrackingView.Positioned) {
--                label15:
--                {
--                    ChunkTrackingView.Positioned chunktrackingview_a = (ChunkTrackingView.Positioned) chunkFilter;
--
--                    if (chunktrackingview1 instanceof ChunkTrackingView.Positioned) {
--                        ChunkTrackingView.Positioned chunktrackingview_a1 = (ChunkTrackingView.Positioned) chunktrackingview1;
--
--                        if (chunktrackingview_a1.center().equals(chunktrackingview_a.center())) {
--                            break label15;
--                        }
--                    }
--
--                    player.connection.send(new ClientboundSetChunkCacheCenterPacket(chunktrackingview_a.center().x, chunktrackingview_a.center().z));
--                }
+-            ChunkTrackingView chunkTrackingView1 = player.getChunkTrackingView();
+-            if (chunkTrackingView instanceof ChunkTrackingView.Positioned positioned
+-                && !(chunkTrackingView1 instanceof ChunkTrackingView.Positioned positioned1 && positioned1.center().equals(positioned.center()))) {
+-                player.connection.send(new ClientboundSetChunkCacheCenterPacket(positioned.center().x, positioned.center().z));
 -            }
 -
--            ChunkTrackingView.difference(chunktrackingview1, chunkFilter, (chunkcoordintpair) -> {
--                this.markChunkPendingToSend(player, chunkcoordintpair);
--            }, (chunkcoordintpair) -> {
--                ChunkMap.dropChunk(player, chunkcoordintpair);
--            });
--            player.setChunkTrackingView(chunkFilter);
+-            ChunkTrackingView.difference(
+-                chunkTrackingView1, chunkTrackingView, chunkPos -> this.markChunkPendingToSend(player, chunkPos), chunkPos -> dropChunk(player, chunkPos)
+-            );
+-            player.setChunkTrackingView(chunkTrackingView);
 -        }
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
      @Override
-     public List<ServerPlayer> getPlayers(ChunkPos chunkPos, boolean onlyOnWatchDistanceEdge) {
--        Set<ServerPlayer> set = this.playerMap.getAllPlayers();
+     public List<ServerPlayer> getPlayers(ChunkPos pos, boolean boundaryOnly) {
+-        Set<ServerPlayer> allPlayers = this.playerMap.getAllPlayers();
 -        Builder<ServerPlayer> builder = ImmutableList.builder();
--        Iterator iterator = set.iterator();
 -
--        while (iterator.hasNext()) {
--            ServerPlayer entityplayer = (ServerPlayer) iterator.next();
--
--            if (onlyOnWatchDistanceEdge && this.isChunkOnTrackedBorder(entityplayer, chunkPos.x, chunkPos.z) || !onlyOnWatchDistanceEdge && this.isChunkTracked(entityplayer, chunkPos.x, chunkPos.z)) {
--                builder.add(entityplayer);
+-        for (ServerPlayer serverPlayer : allPlayers) {
+-            if (boundaryOnly && this.isChunkOnTrackedBorder(serverPlayer, pos.x, pos.z) || !boundaryOnly && this.isChunkTracked(serverPlayer, pos.x, pos.z)) {
+-                builder.add(serverPlayer);
 -            }
 +        // Paper start - rewrite chunk system
-+        final ChunkHolder holder = this.getVisibleChunkIfPresent(chunkPos.toLong());
++        final ChunkHolder holder = this.getVisibleChunkIfPresent(pos.toLong());
 +        if (holder == null) {
 +            return new ArrayList<>();
 +        } else {
-+            return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)holder).moonrise$getPlayers(onlyOnWatchDistanceEdge);
++            return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)holder).moonrise$getPlayers(boundaryOnly);
          }
 -
 -        return builder.build();
@@ -25198,34 +25217,30 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      public void addEntity(Entity entity) {
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-                     ChunkMap.TrackedEntity playerchunkmap_entitytracker = new ChunkMap.TrackedEntity(entity, i, j, entitytypes.trackDeltas());
- 
-                     this.entityMap.put(entity.getId(), playerchunkmap_entitytracker);
+@@ -1190,6 +863,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+                 } else {
+                     ChunkMap.TrackedEntity trackedEntity = new ChunkMap.TrackedEntity(entity, i, updateInterval, type.trackDeltas());
+                     this.entityMap.put(entity.getId(), trackedEntity);
 +                    // Paper start - optimise entity tracker
 +                    if (((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$getTrackedEntity() != null) {
 +                        throw new IllegalStateException("Entity is already tracked");
 +                    }
-+                    ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$setTrackedEntity(playerchunkmap_entitytracker);
++                    ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$setTrackedEntity(trackedEntity);
 +                    // Paper end - optimise entity tracker
-                     playerchunkmap_entitytracker.updatePlayers(this.level.players());
-                     if (entity instanceof ServerPlayer) {
-                         ServerPlayer entityplayer = (ServerPlayer) entity;
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-             playerchunkmap_entitytracker1.broadcastRemoved();
+                     trackedEntity.updatePlayers(this.level.players());
+                     if (entity instanceof ServerPlayer serverPlayer) {
+                         this.updatePlayerStatus(serverPlayer, true);
+@@ -1219,12 +898,38 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+         if (trackedEntity1 != null) {
+             trackedEntity1.broadcastRemoved();
          }
- 
 +        ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$setTrackedEntity(null); // Paper - optimise entity tracker
      }
  
--    protected void tick() {
--        Iterator iterator = this.playerMap.getAllPlayers().iterator();
 +    // Paper start - optimise entity tracker
 +    private void newTrackerTick() {
 +        final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup entityLookup = (ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup)((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getEntityLookup();;
- 
--        while (iterator.hasNext()) {
--            ServerPlayer entityplayer = (ServerPlayer) iterator.next();
++
 +        final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.world.entity.Entity> trackerEntities = entityLookup.trackerEntities;
 +        final Entity[] trackerEntitiesRaw = trackerEntities.getRawDataUnchecked();
 +        for (int i = 0, len = trackerEntities.size(); i < len; ++i) {
@@ -25242,9 +25257,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +    // Paper end - optimise entity tracker
- 
--            this.updateChunkTracking(entityplayer);
-+    protected void tick() {
++
+     protected void tick() {
+-        for (ServerPlayer serverPlayer : this.playerMap.getAllPlayers()) {
+-            this.updateChunkTracking(serverPlayer);
 +        // Paper start - optimise entity tracker
 +        if (true) {
 +            this.newTrackerTick();
@@ -25255,28 +25271,24 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
          List<ServerPlayer> list = Lists.newArrayList();
          List<ServerPlayer> list1 = this.level.players();
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -1302,23 +1007,24 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
      }
  
-     public void waitForLightBeforeSending(ChunkPos centerPos, int radius) {
--        int j = radius + 1;
--
--        ChunkPos.rangeClosed(centerPos, j).forEach((chunkcoordintpair1) -> {
--            ChunkHolder playerchunk = this.getVisibleChunkIfPresent(chunkcoordintpair1.toLong());
--
--            if (playerchunk != null) {
--                playerchunk.addSendDependency(this.lightEngine.waitForPendingTasks(chunkcoordintpair1.x, chunkcoordintpair1.z));
+     public void waitForLightBeforeSending(ChunkPos chunkPos, int range) {
+-        int i = range + 1;
+-        ChunkPos.rangeClosed(chunkPos, i).forEach(pos -> {
+-            ChunkHolder visibleChunkIfPresent = this.getVisibleChunkIfPresent(pos.toLong());
+-            if (visibleChunkIfPresent != null) {
+-                visibleChunkIfPresent.addSendDependency(this.lightEngine.waitForPendingTasks(pos.x, pos.z));
 -            }
--
 -        });
 +        // Paper - rewrite chunk system
      }
  
--    public class ChunkDistanceManager extends DistanceManager { // Paper - public
-+    public class ChunkDistanceManager extends DistanceManager implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemDistanceManager { // Paper - public // Paper - rewrite chunk system
- 
-         protected ChunkDistanceManager(final Executor workerExecutor, final Executor mainThreadExecutor) {
-             super(workerExecutor, mainThreadExecutor);
+-    public class DistanceManager extends net.minecraft.server.level.DistanceManager { // Paper - public
++    public class DistanceManager extends net.minecraft.server.level.DistanceManager implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemDistanceManager { // Paper - public // Paper - rewrite chunk system
+         protected DistanceManager(final Executor dispatcher, final Executor mainThreadExecutor) {
+             super(dispatcher, mainThreadExecutor);
          }
  
 +        // Paper start - rewrite chunk system
@@ -25287,22 +25299,21 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - rewrite chunk system
 +
          @Override
-         protected boolean isChunkToRemove(long pos) {
--            return ChunkMap.this.toDrop.contains(pos);
+         protected boolean isChunkToRemove(long chunkPos) {
+-            return ChunkMap.this.toDrop.contains(chunkPos);
 +            throw new UnsupportedOperationException(); // Paper - rewrite chunk system
          }
  
          @Nullable
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -1334,13 +1040,96 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
          }
      }
  
 -    public class TrackedEntity {
 +    public class TrackedEntity implements ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity { // Paper - optimise entity tracker
- 
          public final ServerEntity serverEntity;
          final Entity entity;
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+         private final int range;
          SectionPos lastSectionPos;
          public final Set<ServerPlayerConnection> seenBy = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); // Paper - Perf: optimise map impl
  
@@ -25389,30 +25400,28 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +        // Paper end - optimise entity tracker
 +
-         public TrackedEntity(final Entity entity, final int i, final int j, final boolean flag) {
-             this.serverEntity = new ServerEntity(ChunkMap.this.level, entity, j, flag, this::broadcast, this.seenBy); // CraftBukkit
+         public TrackedEntity(final Entity entity, final int range, final int updateInterval, final boolean trackDelta) {
+             this.serverEntity = new ServerEntity(ChunkMap.this.level, entity, updateInterval, trackDelta, this::broadcast, this.seenBy); // CraftBukkit
              this.entity = entity;
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -1431,17 +1220,24 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
          }
  
          private int getEffectiveRange() {
 -            int i = this.range;
--            Iterator iterator = this.entity.getIndirectPassengers().iterator();
 +            // Paper start - optimise entity tracker
 +            final Entity entity = this.entity;
 +            int range = this.range;
  
--            while (iterator.hasNext()) {
--                Entity entity = (Entity) iterator.next();
--                int j = entity.getType().clientTrackingRange() * 16;
--                j = org.spigotmc.TrackingRange.getEntityTrackingRange(entity, j); // Paper
+-            for (Entity entity : this.entity.getIndirectPassengers()) {
+-                int i1 = entity.getType().clientTrackingRange() * 16;
+-                i1 = org.spigotmc.TrackingRange.getEntityTrackingRange(entity, i1); // Paper
+-                if (i1 > i) {
+-                    i = i1;
+-                }
 +            if (entity.getPassengers() == ImmutableList.<Entity>of()) {
 +                return this.scaledRange(range);
 +            }
- 
--                if (j > i) {
--                    i = j;
--                }
++
 +            // note: we change to List
 +            final List<Entity> passengers = (List<Entity>)entity.getIndirectPassengers();
 +            for (int i = 0, len = passengers.size(); i < len; ++i) {
@@ -25426,28 +25435,27 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            // Paper end - optimise entity tracker
          }
  
-         public void updatePlayers(List<ServerPlayer> players) {
-diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/DistanceManager.java
-+++ b/src/main/java/net/minecraft/server/level/DistanceManager.java
-@@ -0,0 +0,0 @@ import net.minecraft.world.level.ChunkPos;
+         public void updatePlayers(List<ServerPlayer> playersList) {
+diff --git a/net/minecraft/server/level/DistanceManager.java b/net/minecraft/server/level/DistanceManager.java
+index 8ec20e372570d5eb720cdcdaed9c92946033be9b..5eab6179ce3913cb4e4d424f910ba423faf21c85 100644
+--- a/net/minecraft/server/level/DistanceManager.java
++++ b/net/minecraft/server/level/DistanceManager.java
+@@ -34,56 +34,56 @@ import net.minecraft.world.level.ChunkPos;
  import net.minecraft.world.level.chunk.LevelChunk;
  import org.slf4j.Logger;
  
 -public abstract class DistanceManager {
 +public abstract class DistanceManager implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemDistanceManager, ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager { // Paper - rewrite chunk system // Paper - chunk tick iteration optimisation
- 
      static final Logger LOGGER = LogUtils.getLogger();
      static final int PLAYER_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING);
      private static final int INITIAL_TICKET_LIST_CAPACITY = 4;
-     final Long2ObjectMap<ObjectSet<ServerPlayer>> playersPerChunk = new Long2ObjectOpenHashMap();
--    public final Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> tickets = new Long2ObjectOpenHashMap();
+     final Long2ObjectMap<ObjectSet<ServerPlayer>> playersPerChunk = new Long2ObjectOpenHashMap<>();
+-    public final Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> tickets = new Long2ObjectOpenHashMap<>();
 -    private final DistanceManager.ChunkTicketTracker ticketTracker = new DistanceManager.ChunkTicketTracker();
 -    private final DistanceManager.FixedPlayerDistanceChunkTracker naturalSpawnChunkCounter = new DistanceManager.FixedPlayerDistanceChunkTracker(8);
 -    private final TickingTracker tickingTicketsTracker = new TickingTracker();
 -    private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(32);
--    final Set<ChunkHolder> chunksToUpdateFutures = new ReferenceOpenHashSet();
+-    final Set<ChunkHolder> chunksToUpdateFutures = new ReferenceOpenHashSet<>();
 -    final ThrottlingChunkTaskDispatcher ticketDispatcher;
 -    final LongSet ticketsToRelease = new LongOpenHashSet();
 -    final Executor mainThreadExecutor;
@@ -25458,25 +25466,30 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -    public int simulationDistance = 10;
 +    // Paper - rewrite chunk system
  
-     protected DistanceManager(Executor workerExecutor, Executor mainThreadExecutor) {
-         TaskScheduler<Runnable> taskscheduler = TaskScheduler.wrapExecutor("player ticket throttler", mainThreadExecutor);
- 
--        this.ticketDispatcher = new ThrottlingChunkTaskDispatcher(taskscheduler, workerExecutor, 4);
+     protected DistanceManager(Executor dispatcher, Executor mainThreadExecutor) {
+         TaskScheduler<Runnable> taskScheduler = TaskScheduler.wrapExecutor("player ticket throttler", mainThreadExecutor);
+-        this.ticketDispatcher = new ThrottlingChunkTaskDispatcher(taskScheduler, dispatcher, 4);
 -        this.mainThreadExecutor = mainThreadExecutor;
 +        // Paper - rewrite chunk system
      }
  
 -    protected void purgeStaleTickets() {
--        ++this.ticketTickCounter;
--        ObjectIterator<Entry<SortedArraySet<Ticket<?>>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator();
+-        this.ticketTickCounter++;
+-        ObjectIterator<Entry<SortedArraySet<Ticket<?>>>> objectIterator = this.tickets.long2ObjectEntrySet().fastIterator();
 -
--        while (objectiterator.hasNext()) {
--            Entry<SortedArraySet<Ticket<?>>> entry = (Entry) objectiterator.next();
--            Iterator<Ticket<?>> iterator = ((SortedArraySet) entry.getValue()).iterator();
+-        while (objectIterator.hasNext()) {
+-            Entry<SortedArraySet<Ticket<?>>> entry = objectIterator.next();
+-            Iterator<Ticket<?>> iterator = entry.getValue().iterator();
 -            boolean flag = false;
 -
 -            while (iterator.hasNext()) {
--                Ticket<?> ticket = (Ticket) iterator.next();
+-                Ticket<?> ticket = iterator.next();
+-                if (ticket.timedOut(this.ticketTickCounter)) {
+-                    iterator.remove();
+-                    flag = true;
+-                    this.tickingTicketsTracker.removeTicket(entry.getLongKey(), ticket);
+-                }
+-            }
 +    // Paper start - rewrite chunk system
 +    @Override
 +    public final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager moonrise$getChunkHolderManager() {
@@ -25486,28 +25499,22 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    // Paper start - chunk tick iteration optimisation
 +    private final ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap<ServerPlayer> spawnChunkTracker = new ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap<>();
  
--                if (ticket.timedOut(this.ticketTickCounter)) {
--                    iterator.remove();
--                    flag = true;
--                    this.tickingTicketsTracker.removeTicket(entry.getLongKey(), ticket);
--                }
+-            if (flag) {
+-                this.ticketTracker.update(entry.getLongKey(), getTicketLevelAt(entry.getValue()), false);
 -            }
 +    @Override
 +    public final void moonrise$addPlayer(final ServerPlayer player, final SectionPos pos) {
 +        this.spawnChunkTracker.add(player, pos.x(), pos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE);
 +    }
  
--            if (flag) {
--                this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt((SortedArraySet) entry.getValue()), false);
+-            if (entry.getValue().isEmpty()) {
+-                objectIterator.remove();
 -            }
 +    @Override
 +    public final void moonrise$removePlayer(final ServerPlayer player, final SectionPos pos) {
 +        this.spawnChunkTracker.remove(player);
 +    }
- 
--            if (((SortedArraySet) entry.getValue()).isEmpty()) {
--                objectiterator.remove();
--            }
++
 +    @Override
 +    public final void moonrise$updatePlayer(final ServerPlayer player,
 +                                            final SectionPos oldPos, final SectionPos newPos,
@@ -25517,80 +25524,60 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        } else {
 +            this.spawnChunkTracker.addOrUpdate(player, newPos.x(), newPos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE);
          }
-+    }
+     }
 +    // Paper end - chunk tick iteration optimisation
 +
 +    protected void purgeStaleTickets() {
 +        this.moonrise$getChunkHolderManager().tick(); // Paper - rewrite chunk system
++    }
  
-     }
+     private static int getTicketLevelAt(SortedArraySet<Ticket<?>> tickets) {
+         return !tickets.isEmpty() ? tickets.first().getTicketLevel() : ChunkLevel.MAX_LEVEL + 1;
+@@ -98,77 +98,15 @@ public abstract class DistanceManager {
+     protected abstract ChunkHolder updateChunkScheduling(long chunkPos, int i, @Nullable ChunkHolder newLevel, int holder);
  
-@@ -0,0 +0,0 @@ public abstract class DistanceManager {
-     protected abstract ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k);
- 
-     public boolean runAllUpdates(ChunkMap chunkLoadingManager) {
+     public boolean runAllUpdates(ChunkMap chunkMap) {
 -        this.naturalSpawnChunkCounter.runAllUpdates();
 -        this.tickingTicketsTracker.runAllUpdates();
 -        this.playerTicketManager.runAllUpdates();
 -        int i = Integer.MAX_VALUE - this.ticketTracker.runDistanceUpdates(Integer.MAX_VALUE);
 -        boolean flag = i != 0;
--
 -        if (flag) {
--            ;
 -        }
 -
 -        if (!this.chunksToUpdateFutures.isEmpty()) {
--            Iterator iterator = this.chunksToUpdateFutures.iterator();
--
--            ChunkHolder playerchunk;
--
 -            // CraftBukkit start - SPIGOT-7780: Call chunk unload events before updateHighestAllowedStatus
--            while (iterator.hasNext()) {
--                playerchunk = (ChunkHolder) iterator.next();
--                playerchunk.callEventIfUnloading(chunkLoadingManager);
+-            for (final ChunkHolder chunkHolder : this.chunksToUpdateFutures) {
+-                chunkHolder.callEventIfUnloading(chunkMap);
+-            }
+-            // CraftBukkit end - SPIGOT-7780: Call chunk unload events before updateHighestAllowedStatus
+-
+-            for (ChunkHolder chunkHolder : this.chunksToUpdateFutures) {
+-                chunkHolder.updateHighestAllowedStatus(chunkMap);
 -            }
 -
--            iterator = this.chunksToUpdateFutures.iterator();
--            // CraftBukkit end
--
--            while (iterator.hasNext()) {
--                playerchunk = (ChunkHolder) iterator.next();
--                playerchunk.updateHighestAllowedStatus(chunkLoadingManager);
--            }
--
--            iterator = this.chunksToUpdateFutures.iterator();
--
--            while (iterator.hasNext()) {
--                playerchunk = (ChunkHolder) iterator.next();
--                playerchunk.updateFutures(chunkLoadingManager, this.mainThreadExecutor);
+-            for (ChunkHolder chunkHolder : this.chunksToUpdateFutures) {
+-                chunkHolder.updateFutures(chunkMap, this.mainThreadExecutor);
 -            }
 -
 -            this.chunksToUpdateFutures.clear();
 -            return true;
 -        } else {
 -            if (!this.ticketsToRelease.isEmpty()) {
--                LongIterator longiterator = this.ticketsToRelease.iterator();
+-                LongIterator longIterator = this.ticketsToRelease.iterator();
 -
--                while (longiterator.hasNext()) {
--                    long j = longiterator.nextLong();
--
--                    if (this.getTickets(j).stream().anyMatch((ticket) -> {
--                        return ticket.getType() == TicketType.PLAYER;
--                    })) {
--                        ChunkHolder playerchunk1 = chunkLoadingManager.getUpdatingChunkIfPresent(j);
--
--                        if (playerchunk1 == null) {
+-                while (longIterator.hasNext()) {
+-                    long l = longIterator.nextLong();
+-                    if (this.getTickets(l).stream().anyMatch(ticket -> ticket.getType() == TicketType.PLAYER)) {
+-                        ChunkHolder updatingChunkIfPresent = chunkMap.getUpdatingChunkIfPresent(l);
+-                        if (updatingChunkIfPresent == null) {
 -                            throw new IllegalStateException();
 -                        }
 -
--                        CompletableFuture<ChunkResult<LevelChunk>> completablefuture = playerchunk1.getEntityTickingChunkFuture();
--
--                        completablefuture.thenAccept((chunkresult) -> {
--                            this.mainThreadExecutor.execute(() -> {
--                                this.ticketDispatcher.release(j, () -> {
--                                }, false);
--                            });
--                        });
+-                        CompletableFuture<ChunkResult<LevelChunk>> entityTickingChunkFuture = updatingChunkIfPresent.getEntityTickingChunkFuture();
+-                        entityTickingChunkFuture.thenAccept(
+-                            chunkResult -> this.mainThreadExecutor.execute(() -> this.ticketDispatcher.release(l, () -> {}, false))
+-                        );
 -                    }
 -                }
 -
@@ -25602,115 +25589,121 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return this.moonrise$getChunkHolderManager().processTicketUpdates(); // Paper - rewrite chunk system
      }
  
-     boolean addTicket(long i, Ticket<?> ticket) { // CraftBukkit - void -> boolean
--        SortedArraySet<Ticket<?>> arraysetsorted = this.getTickets(i);
--        int j = DistanceManager.getTicketLevelAt(arraysetsorted);
--        Ticket<?> ticket1 = (Ticket) arraysetsorted.addOrGet(ticket);
--
+     void addTicket(long chunkPos, Ticket<?> ticket) {
+-        SortedArraySet<Ticket<?>> tickets = this.getTickets(chunkPos);
+-        int ticketLevelAt = getTicketLevelAt(tickets);
+-        Ticket<?> ticket1 = tickets.addOrGet(ticket);
 -        ticket1.setCreatedTick(this.ticketTickCounter);
--        if (ticket.getTicketLevel() < j) {
--            this.ticketTracker.update(i, ticket.getTicketLevel(), true);
+-        if (ticket.getTicketLevel() < ticketLevelAt) {
+-            this.ticketTracker.update(chunkPos, ticket.getTicketLevel(), true);
+-        }
++        this.moonrise$getChunkHolderManager().addTicketAtLevel((TicketType)ticket.getType(), chunkPos, ticket.getTicketLevel(), ticket.key); // Paper - rewrite chunk system
+     }
+ 
+     void removeTicket(long chunkPos, Ticket<?> ticket) {
+-        SortedArraySet<Ticket<?>> tickets = this.getTickets(chunkPos);
+-        if (tickets.remove(ticket)) {
 -        }
 -
--        return ticket == ticket1; // CraftBukkit
-+        return this.moonrise$getChunkHolderManager().addTicketAtLevel((TicketType)ticket.getType(), i, ticket.getTicketLevel(), ticket.key); // Paper - rewrite chunk system
-     }
- 
-     boolean removeTicket(long i, Ticket<?> ticket) { // CraftBukkit - void -> boolean
--        SortedArraySet<Ticket<?>> arraysetsorted = this.getTickets(i);
--
--        boolean removed = false; // CraftBukkit
--        if (arraysetsorted.remove(ticket)) {
--            removed = true; // CraftBukkit
+-        if (tickets.isEmpty()) {
+-            this.tickets.remove(chunkPos);
 -        }
 -
--        if (arraysetsorted.isEmpty()) {
--            this.tickets.remove(i);
+-        this.ticketTracker.update(chunkPos, getTicketLevelAt(tickets), false);
++        this.moonrise$getChunkHolderManager().removeTicketAtLevel((TicketType)ticket.getType(), chunkPos, ticket.getTicketLevel(), ticket.key); // Paper - rewrite chunk system
+     }
+ 
+     public <T> void addTicket(TicketType<T> type, ChunkPos pos, int level, T value) {
+@@ -181,68 +119,43 @@ public abstract class DistanceManager {
+     }
+ 
+     public <T> void addRegionTicket(TicketType<T> type, ChunkPos pos, int distance, T value) {
+-        Ticket<T> ticket = new Ticket<>(type, ChunkLevel.byStatus(FullChunkStatus.FULL) - distance, value);
+-        long packedChunkPos = pos.toLong();
+-        this.addTicket(packedChunkPos, ticket); // Paper - diff on change above
+-        this.tickingTicketsTracker.addTicket(packedChunkPos, ticket);
++        this.moonrise$getChunkHolderManager().addTicketAtLevel(type, pos, ChunkLevel.byStatus(FullChunkStatus.FULL) - distance, value); // Paper - rewrite chunk system
+     }
+ 
+     public <T> void removeRegionTicket(TicketType<T> type, ChunkPos pos, int distance, T value) {
+-        Ticket<T> ticket = new Ticket<>(type, ChunkLevel.byStatus(FullChunkStatus.FULL) - distance, value);
+-        long packedChunkPos = pos.toLong();
+-        this.removeTicket(packedChunkPos, ticket); // Paper - diff on change above
+-        this.tickingTicketsTracker.removeTicket(packedChunkPos, ticket);
++        this.moonrise$getChunkHolderManager().removeTicketAtLevel(type, pos, ChunkLevel.byStatus(FullChunkStatus.FULL) - distance, value); // Paper - rewrite chunk system
+     }
+ 
+     // Paper start
+     public boolean addPluginRegionTicket(final ChunkPos pos, final org.bukkit.plugin.Plugin value) {
+-        Ticket<org.bukkit.plugin.Plugin> ticket = new Ticket<>(TicketType.PLUGIN_TICKET, ChunkLevel.byStatus(FullChunkStatus.FULL) - 2, value); // Copied from below and keep in-line with force loading, add at level 31
+-        final long packedChunkPos = pos.toLong();
+-        final Set<Ticket<?>> tickets = this.getTickets(packedChunkPos);
+-        if (tickets.contains(ticket)) {
+-            return false;
 -        }
--
--        this.ticketTracker.update(i, DistanceManager.getTicketLevelAt(arraysetsorted), false);
--        return removed; // CraftBukkit
-+        return this.moonrise$getChunkHolderManager().removeTicketAtLevel((TicketType)ticket.getType(), i, ticket.getTicketLevel(), ticket.key); // Paper - rewrite chunk system
+-        this.addTicket(packedChunkPos, ticket);
+-        this.tickingTicketsTracker.addTicket(packedChunkPos, ticket);
+-        return true;
++        return this.moonrise$getChunkHolderManager().addTicketAtLevel(TicketType.PLUGIN_TICKET, pos, ChunkLevel.byStatus(FullChunkStatus.FULL) - 2, value); // Paper - rewrite chunk system
      }
  
-     public <T> void addTicket(TicketType<T> type, ChunkPos pos, int level, T argument) {
-@@ -0,0 +0,0 @@ public abstract class DistanceManager {
+     public boolean removePluginRegionTicket(final ChunkPos pos, final org.bukkit.plugin.Plugin value) {
+-        Ticket<org.bukkit.plugin.Plugin> ticket = new Ticket<>(TicketType.PLUGIN_TICKET, ChunkLevel.byStatus(FullChunkStatus.FULL) - 2, value); // Copied from below and keep in-line with force loading, add at level 31
+-        final long packedChunkPos = pos.toLong();
+-        final Set<Ticket<?>> tickets = this.tickets.get(packedChunkPos); // Don't use getTickets, we don't want to create a new set
+-        if (tickets == null || !tickets.contains(ticket)) {
+-            return false;
+-        }
+-        this.removeTicket(packedChunkPos, ticket);
+-        this.tickingTicketsTracker.removeTicket(packedChunkPos, ticket);
+-        return true;
++        return this.moonrise$getChunkHolderManager().removeTicketAtLevel(TicketType.PLUGIN_TICKET, pos, ChunkLevel.byStatus(FullChunkStatus.FULL) - 2, value); // Paper - rewrite chunk system
      }
+     // Paper end
  
-     public <T> boolean addRegionTicketAtDistance(TicketType<T> tickettype, ChunkPos chunkcoordintpair, int i, T t0) {
--        // CraftBukkit end
--        Ticket<T> ticket = new Ticket<>(tickettype, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0);
--        long j = chunkcoordintpair.toLong();
--
--        boolean added = this.addTicket(j, ticket); // CraftBukkit
--        this.tickingTicketsTracker.addTicket(j, ticket);
--        return added; // CraftBukkit
-+        return this.moonrise$getChunkHolderManager().addTicketAtLevel(tickettype, chunkcoordintpair, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0); // Paper - rewrite chunk system
-     }
- 
-     public <T> void removeRegionTicket(TicketType<T> type, ChunkPos pos, int radius, T argument) {
-@@ -0,0 +0,0 @@ public abstract class DistanceManager {
-     }
- 
-     public <T> boolean removeRegionTicketAtDistance(TicketType<T> tickettype, ChunkPos chunkcoordintpair, int i, T t0) {
--        // CraftBukkit end
--        Ticket<T> ticket = new Ticket<>(tickettype, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0);
--        long j = chunkcoordintpair.toLong();
--
--        boolean removed = this.removeTicket(j, ticket); // CraftBukkit
--        this.tickingTicketsTracker.removeTicket(j, ticket);
--        return removed; // CraftBukkit
-+        return this.moonrise$getChunkHolderManager().removeTicketAtLevel(tickettype, chunkcoordintpair, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0); // Paper - rewrite chunk system
-     }
- 
-     private SortedArraySet<Ticket<?>> getTickets(long position) {
--        return (SortedArraySet) this.tickets.computeIfAbsent(position, (j) -> {
--            return SortedArraySet.create(4);
--        });
+     private SortedArraySet<Ticket<?>> getTickets(long chunkPos) {
+-        return this.tickets.computeIfAbsent(chunkPos, l -> SortedArraySet.create(4));
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
-     protected void updateChunkForced(ChunkPos pos, boolean forced) {
+     protected void updateChunkForced(ChunkPos pos, boolean add) {
 -        Ticket<ChunkPos> ticket = new Ticket<>(TicketType.FORCED, ChunkMap.FORCED_TICKET_LEVEL, pos);
--        long i = pos.toLong();
--
+-        long packedChunkPos = pos.toLong();
 +        // Paper start - rewrite chunk system
-         if (forced) {
--            this.addTicket(i, ticket);
--            this.tickingTicketsTracker.addTicket(i, ticket);
+         if (add) {
+-            this.addTicket(packedChunkPos, ticket);
+-            this.tickingTicketsTracker.addTicket(packedChunkPos, ticket);
 +            this.moonrise$getChunkHolderManager().addTicketAtLevel(TicketType.FORCED, pos, ChunkMap.FORCED_TICKET_LEVEL, pos);
          } else {
--            this.removeTicket(i, ticket);
--            this.tickingTicketsTracker.removeTicket(i, ticket);
+-            this.removeTicket(packedChunkPos, ticket);
+-            this.tickingTicketsTracker.removeTicket(packedChunkPos, ticket);
 +            this.moonrise$getChunkHolderManager().removeTicketAtLevel(TicketType.FORCED, pos, ChunkMap.FORCED_TICKET_LEVEL, pos);
          }
 +        // Paper end - rewrite chunk system
- 
      }
  
-@@ -0,0 +0,0 @@ public abstract class DistanceManager {
-         ((ObjectSet) this.playersPerChunk.computeIfAbsent(i, (j) -> {
-             return new ObjectOpenHashSet();
-         })).add(player);
--        this.naturalSpawnChunkCounter.update(i, 0, true);
--        this.playerTicketManager.update(i, 0, true);
--        this.tickingTicketsTracker.addTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair);
+     public void addPlayer(SectionPos sectionPos, ServerPlayer player) {
+         ChunkPos chunkPos = sectionPos.chunk();
+         long packedChunkPos = chunkPos.toLong();
+         this.playersPerChunk.computeIfAbsent(packedChunkPos, l -> new ObjectOpenHashSet<>()).add(player);
+-        this.naturalSpawnChunkCounter.update(packedChunkPos, 0, true);
+-        this.playerTicketManager.update(packedChunkPos, 0, true);
+-        this.tickingTicketsTracker.addTicket(TicketType.PLAYER, chunkPos, this.getPlayerTicketLevel(), chunkPos);
 +        // Paper - chunk tick iteration optimisation
 +        // Paper - rewrite chunk system
      }
  
-     public void removePlayer(SectionPos pos, ServerPlayer player) {
-@@ -0,0 +0,0 @@ public abstract class DistanceManager {
-         if (objectset != null) objectset.remove(player); // Paper - some state corruption happens here, don't crash, clean up gracefully
-         if (objectset == null || objectset.isEmpty()) { // Paper
-             this.playersPerChunk.remove(i);
--            this.naturalSpawnChunkCounter.update(i, Integer.MAX_VALUE, false);
--            this.playerTicketManager.update(i, Integer.MAX_VALUE, false);
--            this.tickingTicketsTracker.removeTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair);
+     public void removePlayer(SectionPos sectionPos, ServerPlayer player) {
+@@ -254,136 +167,90 @@ public abstract class DistanceManager {
+         if (set == null || set.isEmpty()) {
+         // Paper end - some state corruption happens here, don't crash, clean up gracefully
+             this.playersPerChunk.remove(packedChunkPos);
+-            this.naturalSpawnChunkCounter.update(packedChunkPos, Integer.MAX_VALUE, false);
+-            this.playerTicketManager.update(packedChunkPos, Integer.MAX_VALUE, false);
+-            this.tickingTicketsTracker.removeTicket(TicketType.PLAYER, chunkPos, this.getPlayerTicketLevel(), chunkPos);
 +            // Paper - chunk tick iteration optimisation
 +            // Paper - rewrite chunk system
          }
- 
      }
  
      private int getPlayerTicketLevel() {
@@ -25734,11 +25727,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - rewrite chunk system
      }
  
-     protected String getTicketDebugString(long pos) {
--        SortedArraySet<Ticket<?>> arraysetsorted = (SortedArraySet) this.tickets.get(pos);
--
--        return arraysetsorted != null && !arraysetsorted.isEmpty() ? ((Ticket) arraysetsorted.first()).toString() : "no_ticket";
-+        return this.moonrise$getChunkHolderManager().getTicketDebugString(pos); // Paper - rewrite chunk system
+     protected String getTicketDebugString(long chunkPos) {
+-        SortedArraySet<Ticket<?>> set = this.tickets.get(chunkPos);
+-        return set != null && !set.isEmpty() ? set.first().toString() : "no_ticket";
++        return this.moonrise$getChunkHolderManager().getTicketDebugString(chunkPos); // Paper - rewrite chunk system
      }
  
      protected void updatePlayerTickets(int viewDistance) {
@@ -25754,7 +25746,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper start - rewrite chunk system
 +        // note: vanilla does not clamp to 0, but we do simply because we need a min of 0
 +        final int clamped = net.minecraft.util.Mth.clamp(simulationDistance, 0, ca.spottedleaf.moonrise.common.util.MoonriseConstants.MAX_VIEW_DISTANCE);
- 
++
 +        ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.moonrise$getChunkMap().level).moonrise$getPlayerChunkLoader().setTickDistance(clamped);
 +        // Paper end - rewrite chunk system
      }
@@ -25782,40 +25774,21 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return "No DistanceManager stats available"; // Paper - rewrite chunk system
      }
  
-     private void dumpTickets(String path) {
--        try {
--            FileOutputStream fileoutputstream = new FileOutputStream(new File(path));
+     private void dumpTickets(String filename) {
+-        try (FileOutputStream fileOutputStream = new FileOutputStream(new File(filename))) {
+-            for (Entry<SortedArraySet<Ticket<?>>> entry : this.tickets.long2ObjectEntrySet()) {
+-                ChunkPos chunkPos = new ChunkPos(entry.getLongKey());
 -
--            try {
--                ObjectIterator objectiterator = this.tickets.long2ObjectEntrySet().iterator();
--
--                while (objectiterator.hasNext()) {
--                    Entry<SortedArraySet<Ticket<?>>> entry = (Entry) objectiterator.next();
--                    ChunkPos chunkcoordintpair = new ChunkPos(entry.getLongKey());
--                    Iterator iterator = ((SortedArraySet) entry.getValue()).iterator();
--
--                    while (iterator.hasNext()) {
--                        Ticket<?> ticket = (Ticket) iterator.next();
--
--                        fileoutputstream.write((chunkcoordintpair.x + "\t" + chunkcoordintpair.z + "\t" + String.valueOf(ticket.getType()) + "\t" + ticket.getTicketLevel() + "\t\n").getBytes(StandardCharsets.UTF_8));
--                    }
+-                for (Ticket<?> ticket : entry.getValue()) {
+-                    fileOutputStream.write(
+-                        (chunkPos.x + "\t" + chunkPos.z + "\t" + ticket.getType() + "\t" + ticket.getTicketLevel() + "\t\n").getBytes(StandardCharsets.UTF_8)
+-                    );
 -                }
--            } catch (Throwable throwable) {
--                try {
--                    fileoutputstream.close();
--                } catch (Throwable throwable1) {
--                    throwable.addSuppressed(throwable1);
--                }
--
--                throw throwable;
 -            }
--
--            fileoutputstream.close();
--        } catch (IOException ioexception) {
--            DistanceManager.LOGGER.error("Failed to dump tickets to {}", path, ioexception);
+-        } catch (IOException var10) {
+-            LOGGER.error("Failed to dump tickets to {}", filename, var10);
 -        }
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- 
      }
  
      @VisibleForTesting
@@ -25830,18 +25803,17 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      public void removeTicketsOnClosing() {
--        ImmutableSet<TicketType<?>> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.FUTURE_AWAIT); // Paper - add additional tickets to preserve
--        ObjectIterator<Entry<SortedArraySet<Ticket<?>>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator();
+-        ImmutableSet<TicketType<?>> set = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.FUTURE_AWAIT); // Paper - add additional tickets to preserve
+-        ObjectIterator<Entry<SortedArraySet<Ticket<?>>>> objectIterator = this.tickets.long2ObjectEntrySet().fastIterator();
 -
--        while (objectiterator.hasNext()) {
--            Entry<SortedArraySet<Ticket<?>>> entry = (Entry) objectiterator.next();
--            Iterator<Ticket<?>> iterator = ((SortedArraySet) entry.getValue()).iterator();
+-        while (objectIterator.hasNext()) {
+-            Entry<SortedArraySet<Ticket<?>>> entry = objectIterator.next();
+-            Iterator<Ticket<?>> iterator = entry.getValue().iterator();
 -            boolean flag = false;
 -
 -            while (iterator.hasNext()) {
--                Ticket<?> ticket = (Ticket) iterator.next();
--
--                if (!immutableset.contains(ticket.getType())) {
+-                Ticket<?> ticket = iterator.next();
+-                if (!set.contains(ticket.getType())) {
 -                    iterator.remove();
 -                    flag = true;
 -                    this.tickingTicketsTracker.removeTicket(entry.getLongKey(), ticket);
@@ -25849,15 +25821,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -            }
 -
 -            if (flag) {
--                this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt((SortedArraySet) entry.getValue()), false);
+-                this.ticketTracker.update(entry.getLongKey(), getTicketLevelAt(entry.getValue()), false);
 -            }
 -
--            if (((SortedArraySet) entry.getValue()).isEmpty()) {
--                objectiterator.remove();
+-            if (entry.getValue().isEmpty()) {
+-                objectIterator.remove();
 -            }
 -        }
 +        // Paper - rewrite chunk system
- 
      }
  
      public boolean hasTickets() {
@@ -25886,39 +25857,39 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
      // CraftBukkit end
  
-+    /*  // Paper - rewrite chunk system
-     private class ChunkTicketTracker extends ChunkTracker {
- 
++/*  // Paper - rewrite chunk system
+     class ChunkTicketTracker extends ChunkTracker {
          private static final int MAX_LEVEL = ChunkLevel.MAX_LEVEL + 1;
-@@ -0,0 +0,0 @@ public abstract class DistanceManager {
-         public int runDistanceUpdates(int distance) {
-             return this.runUpdates(distance);
+ 
+@@ -428,7 +295,7 @@ public abstract class DistanceManager {
+         public int runDistanceUpdates(int toUpdateCount) {
+             return this.runUpdates(toUpdateCount);
          }
 -    }
 +    }*/  // Paper - rewrite chunk system
  
-     private class FixedPlayerDistanceChunkTracker extends ChunkTracker {
- 
-@@ -0,0 +0,0 @@ public abstract class DistanceManager {
+     class FixedPlayerDistanceChunkTracker extends ChunkTracker {
+         protected final Long2ByteMap chunks = new Long2ByteOpenHashMap();
+@@ -487,6 +354,7 @@ public abstract class DistanceManager {
          }
      }
  
-+    /*  // Paper - rewrite chunk system
-     private class PlayerTicketTracker extends DistanceManager.FixedPlayerDistanceChunkTracker {
- 
-         private int viewDistance = 0;
-@@ -0,0 +0,0 @@ public abstract class DistanceManager {
-         private boolean haveTicketFor(int distance) {
-             return distance <= this.viewDistance;
++/*  // Paper - rewrite chunk system
+     class PlayerTicketTracker extends DistanceManager.FixedPlayerDistanceChunkTracker {
+         private int viewDistance;
+         private final Long2IntMap queueLevels = Long2IntMaps.synchronize(new Long2IntOpenHashMap());
+@@ -563,5 +431,5 @@ public abstract class DistanceManager {
+         private boolean haveTicketFor(int level) {
+             return level <= this.viewDistance;
          }
 -    }
 +    }*/  // Paper - rewrite chunk system
  }
-diff --git a/src/main/java/net/minecraft/server/level/GenerationChunkHolder.java b/src/main/java/net/minecraft/server/level/GenerationChunkHolder.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/GenerationChunkHolder.java
-+++ b/src/main/java/net/minecraft/server/level/GenerationChunkHolder.java
-@@ -0,0 +0,0 @@ public abstract class GenerationChunkHolder {
+diff --git a/net/minecraft/server/level/GenerationChunkHolder.java b/net/minecraft/server/level/GenerationChunkHolder.java
+index cb66209c64b855dedf2e4e114a7716da13bc4587..da1366fdc4889d6a3befd43d81a19a816ed4cbe9 100644
+--- a/net/minecraft/server/level/GenerationChunkHolder.java
++++ b/net/minecraft/server/level/GenerationChunkHolder.java
+@@ -27,13 +27,7 @@ public abstract class GenerationChunkHolder {
      public static final ChunkResult<ChunkAccess> UNLOADED_CHUNK = ChunkResult.error("Unloaded chunk");
      public static final CompletableFuture<ChunkResult<ChunkAccess>> UNLOADED_CHUNK_FUTURE = CompletableFuture.completedFuture(UNLOADED_CHUNK);
      protected final ChunkPos pos;
@@ -25933,55 +25904,55 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      public GenerationChunkHolder(ChunkPos pos) {
          this.pos = pos;
-@@ -0,0 +0,0 @@ public abstract class GenerationChunkHolder {
+@@ -43,243 +37,96 @@ public abstract class GenerationChunkHolder {
      }
  
-     public CompletableFuture<ChunkResult<ChunkAccess>> scheduleChunkGenerationTask(ChunkStatus requestedStatus, ChunkMap chunkLoadingManager) {
--        if (this.isStatusDisallowed(requestedStatus)) {
+     public CompletableFuture<ChunkResult<ChunkAccess>> scheduleChunkGenerationTask(ChunkStatus targetStatus, ChunkMap chunkMap) {
+-        if (this.isStatusDisallowed(targetStatus)) {
 -            return UNLOADED_CHUNK_FUTURE;
 -        } else {
--            CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.getOrCreateFuture(requestedStatus);
--            if (completableFuture.isDone()) {
--                return completableFuture;
+-            CompletableFuture<ChunkResult<ChunkAccess>> future = this.getOrCreateFuture(targetStatus);
+-            if (future.isDone()) {
+-                return future;
 -            } else {
 -                ChunkGenerationTask chunkGenerationTask = this.task.get();
--                if (chunkGenerationTask == null || requestedStatus.isAfter(chunkGenerationTask.targetStatus)) {
--                    this.rescheduleChunkTask(chunkLoadingManager, requestedStatus);
+-                if (chunkGenerationTask == null || targetStatus.isAfter(chunkGenerationTask.targetStatus)) {
+-                    this.rescheduleChunkTask(chunkMap, targetStatus);
 -                }
 -
--                return completableFuture;
+-                return future;
 -            }
 -        }
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
-     CompletableFuture<ChunkResult<ChunkAccess>> applyStep(ChunkStep step, GeneratingChunkMap chunkLoadingManager, StaticCache2D<GenerationChunkHolder> chunks) {
+     CompletableFuture<ChunkResult<ChunkAccess>> applyStep(ChunkStep step, GeneratingChunkMap chunkMap, StaticCache2D<GenerationChunkHolder> cache) {
 -        if (this.isStatusDisallowed(step.targetStatus())) {
 -            return UNLOADED_CHUNK_FUTURE;
 -        } else {
--            return this.acquireStatusBump(step.targetStatus()) ? chunkLoadingManager.applyStep(this, step, chunks).handle((chunk, throwable) -> {
+-            return this.acquireStatusBump(step.targetStatus()) ? chunkMap.applyStep(this, step, cache).handle((chunkAccess, throwable) -> {
 -                if (throwable != null) {
 -                    CrashReport crashReport = CrashReport.forThrowable(throwable, "Exception chunk generation/loading");
 -                    MinecraftServer.setFatalException(new ReportedException(crashReport));
 -                } else {
--                    this.completeFuture(step.targetStatus(), chunk);
+-                    this.completeFuture(step.targetStatus(), chunkAccess);
 -                }
 -
--                return ChunkResult.of(chunk);
+-                return ChunkResult.of(chunkAccess);
 -            }) : this.getOrCreateFuture(step.targetStatus());
 -        }
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
-     protected void updateHighestAllowedStatus(ChunkMap chunkLoadingManager) {
+     protected void updateHighestAllowedStatus(ChunkMap chunkMap) {
 -        ChunkStatus chunkStatus = this.highestAllowedStatus;
--        ChunkStatus chunkStatus2 = ChunkLevel.generationStatus(this.getTicketLevel());
--        this.highestAllowedStatus = chunkStatus2;
--        boolean bl = chunkStatus != null && (chunkStatus2 == null || chunkStatus2.isBefore(chunkStatus));
--        if (bl) {
--            this.failAndClearPendingFuturesBetween(chunkStatus2, chunkStatus);
+-        ChunkStatus chunkStatus1 = ChunkLevel.generationStatus(this.getTicketLevel());
+-        this.highestAllowedStatus = chunkStatus1;
+-        boolean flag = chunkStatus != null && (chunkStatus1 == null || chunkStatus1.isBefore(chunkStatus));
+-        if (flag) {
+-            this.failAndClearPendingFuturesBetween(chunkStatus1, chunkStatus);
 -            if (this.task.get() != null) {
--                this.rescheduleChunkTask(chunkLoadingManager, this.findHighestStatusWithPendingFuture(chunkStatus2));
+-                this.rescheduleChunkTask(chunkMap, this.findHighestStatusWithPendingFuture(chunkStatus1));
 -            }
 -        }
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
@@ -25991,57 +25962,57 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -        CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = CompletableFuture.completedFuture(ChunkResult.of(chunk));
 -
 -        for (int i = 0; i < this.futures.length() - 1; i++) {
--            CompletableFuture<ChunkResult<ChunkAccess>> completableFuture2 = this.futures.get(i);
--            Objects.requireNonNull(completableFuture2);
--            ChunkAccess chunkAccess = completableFuture2.getNow(NOT_DONE_YET).orElse(null);
+-            CompletableFuture<ChunkResult<ChunkAccess>> completableFuture1 = this.futures.get(i);
+-            Objects.requireNonNull(completableFuture1);
+-            ChunkAccess chunkAccess = completableFuture1.getNow(NOT_DONE_YET).orElse(null);
 -            if (!(chunkAccess instanceof ProtoChunk)) {
 -                throw new IllegalStateException("Trying to replace a ProtoChunk, but found " + chunkAccess);
 -            }
 -
--            if (!this.futures.compareAndSet(i, completableFuture2, completableFuture)) {
+-            if (!this.futures.compareAndSet(i, completableFuture1, completableFuture)) {
 -                throw new IllegalStateException("Future changed by other thread while trying to replace it");
 -            }
 -        }
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
-     void removeTask(ChunkGenerationTask loader) {
--        this.task.compareAndSet(loader, null);
+     void removeTask(ChunkGenerationTask task) {
+-        this.task.compareAndSet(task, null);
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
-     private void rescheduleChunkTask(ChunkMap chunkLoadingManager, @Nullable ChunkStatus requestedStatus) {
+     private void rescheduleChunkTask(ChunkMap chunkMap, @Nullable ChunkStatus targetStatus) {
 -        ChunkGenerationTask chunkGenerationTask;
--        if (requestedStatus != null) {
--            chunkGenerationTask = chunkLoadingManager.scheduleGenerationTask(requestedStatus, this.getPos());
+-        if (targetStatus != null) {
+-            chunkGenerationTask = chunkMap.scheduleGenerationTask(targetStatus, this.getPos());
 -        } else {
 -            chunkGenerationTask = null;
 -        }
 -
--        ChunkGenerationTask chunkGenerationTask3 = this.task.getAndSet(chunkGenerationTask);
--        if (chunkGenerationTask3 != null) {
--            chunkGenerationTask3.markForCancellation();
+-        ChunkGenerationTask chunkGenerationTask1 = this.task.getAndSet(chunkGenerationTask);
+-        if (chunkGenerationTask1 != null) {
+-            chunkGenerationTask1.markForCancellation();
 -        }
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
-     private CompletableFuture<ChunkResult<ChunkAccess>> getOrCreateFuture(ChunkStatus status) {
--        if (this.isStatusDisallowed(status)) {
+     private CompletableFuture<ChunkResult<ChunkAccess>> getOrCreateFuture(ChunkStatus targetStatus) {
+-        if (this.isStatusDisallowed(targetStatus)) {
 -            return UNLOADED_CHUNK_FUTURE;
 -        } else {
--            int i = status.getIndex();
--            CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(i);
+-            int index = targetStatus.getIndex();
+-            CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(index);
 -
 -            while (completableFuture == null) {
--                CompletableFuture<ChunkResult<ChunkAccess>> completableFuture2 = new CompletableFuture<>();
--                completableFuture = this.futures.compareAndExchange(i, null, completableFuture2);
+-                CompletableFuture<ChunkResult<ChunkAccess>> completableFuture1 = new CompletableFuture<>();
+-                completableFuture = this.futures.compareAndExchange(index, null, completableFuture1);
 -                if (completableFuture == null) {
--                    if (this.isStatusDisallowed(status)) {
--                        this.failAndClearPendingFuture(i, completableFuture2);
+-                    if (this.isStatusDisallowed(targetStatus)) {
+-                        this.failAndClearPendingFuture(index, completableFuture1);
 -                        return UNLOADED_CHUNK_FUTURE;
 -                    }
 -
--                    return completableFuture2;
+-                    return completableFuture1;
 -                }
 -            }
 -
@@ -26050,34 +26021,34 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
-     private void failAndClearPendingFuturesBetween(@Nullable ChunkStatus from, ChunkStatus to) {
--        int i = from == null ? 0 : from.getIndex() + 1;
--        int j = to.getIndex();
+     private void failAndClearPendingFuturesBetween(@Nullable ChunkStatus highestAllowableStatus, ChunkStatus currentStatus) {
+-        int i = highestAllowableStatus == null ? 0 : highestAllowableStatus.getIndex() + 1;
+-        int index = currentStatus.getIndex();
 -
--        for (int k = i; k <= j; k++) {
--            CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(k);
+-        for (int i1 = i; i1 <= index; i1++) {
+-            CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(i1);
 -            if (completableFuture != null) {
--                this.failAndClearPendingFuture(k, completableFuture);
+-                this.failAndClearPendingFuture(i1, completableFuture);
 -            }
 -        }
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
-     private void failAndClearPendingFuture(int statusIndex, CompletableFuture<ChunkResult<ChunkAccess>> previousFuture) {
--        if (previousFuture.complete(UNLOADED_CHUNK) && !this.futures.compareAndSet(statusIndex, previousFuture, null)) {
+     private void failAndClearPendingFuture(int status, CompletableFuture<ChunkResult<ChunkAccess>> future) {
+-        if (future.complete(UNLOADED_CHUNK) && !this.futures.compareAndSet(status, future, null)) {
 -            throw new IllegalStateException("Nothing else should replace the future here");
 -        }
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
-     private void completeFuture(ChunkStatus status, ChunkAccess chunk) {
--        ChunkResult<ChunkAccess> chunkResult = ChunkResult.of(chunk);
--        int i = status.getIndex();
+     private void completeFuture(ChunkStatus targetStatus, ChunkAccess chunkAccess) {
+-        ChunkResult<ChunkAccess> chunkResult = ChunkResult.of(chunkAccess);
+-        int index = targetStatus.getIndex();
 -
 -        while (true) {
--            CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(i);
+-            CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(index);
 -            if (completableFuture == null) {
--                if (this.futures.compareAndSet(i, null, CompletableFuture.completedFuture(chunkResult))) {
+-                if (this.futures.compareAndSet(index, null, CompletableFuture.completedFuture(chunkResult))) {
 -                    return;
 -                }
 -            } else {
@@ -26096,14 +26067,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      @Nullable
-     private ChunkStatus findHighestStatusWithPendingFuture(@Nullable ChunkStatus checkUpperBound) {
--        if (checkUpperBound == null) {
+     private ChunkStatus findHighestStatusWithPendingFuture(@Nullable ChunkStatus generationStatus) {
+-        if (generationStatus == null) {
 -            return null;
 -        } else {
--            ChunkStatus chunkStatus = checkUpperBound;
+-            ChunkStatus chunkStatus = generationStatus;
 -
--            for (ChunkStatus chunkStatus2 = this.startedWork.get();
--                chunkStatus2 == null || chunkStatus.isAfter(chunkStatus2);
+-            for (ChunkStatus chunkStatus1 = this.startedWork.get();
+-                chunkStatus1 == null || chunkStatus.isAfter(chunkStatus1);
 -                chunkStatus = chunkStatus.getParent()
 -            ) {
 -                if (this.futures.get(chunkStatus.getIndex()) != null) {
@@ -26120,15 +26091,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
-     private boolean acquireStatusBump(ChunkStatus nextStatus) {
--        ChunkStatus chunkStatus = nextStatus == ChunkStatus.EMPTY ? null : nextStatus.getParent();
--        ChunkStatus chunkStatus2 = this.startedWork.compareAndExchange(chunkStatus, nextStatus);
--        if (chunkStatus2 == chunkStatus) {
+     private boolean acquireStatusBump(ChunkStatus status) {
+-        ChunkStatus chunkStatus = status == ChunkStatus.EMPTY ? null : status.getParent();
+-        ChunkStatus chunkStatus1 = this.startedWork.compareAndExchange(chunkStatus, status);
+-        if (chunkStatus1 == chunkStatus) {
 -            return true;
--        } else if (chunkStatus2 != null && !nextStatus.isAfter(chunkStatus2)) {
+-        } else if (chunkStatus1 != null && !status.isAfter(chunkStatus1)) {
 -            return false;
 -        } else {
--            throw new IllegalStateException("Unexpected last startedWork status: " + chunkStatus2 + " while trying to start: " + nextStatus);
+-            throw new IllegalStateException("Unexpected last startedWork status: " + chunkStatus1 + " while trying to start: " + status);
 -        }
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
@@ -26139,7 +26110,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
-     protected abstract void addSaveDependency(CompletableFuture<?> savingFuture);
+     protected abstract void addSaveDependency(CompletableFuture<?> saveDependency);
  
      public void increaseGenerationRefCount() {
 -        if (this.generationRefCount.getAndIncrement() == 0) {
@@ -26163,19 +26134,19 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      @Nullable
-     public ChunkAccess getChunkIfPresentUnchecked(ChunkStatus requestedStatus) {
--        CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(requestedStatus.getIndex());
+     public ChunkAccess getChunkIfPresentUnchecked(ChunkStatus status) {
+-        CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(status.getIndex());
 -        return completableFuture == null ? null : completableFuture.getNow(NOT_DONE_YET).orElse(null);
 +        // Paper start - rewrite chunk system
-+        return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getChunkIfPresentUnchecked(requestedStatus);
++        return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getChunkIfPresentUnchecked(status);
 +        // Paper end - rewrite chunk system
      }
  
      @Nullable
-     public ChunkAccess getChunkIfPresent(ChunkStatus requestedStatus) {
--        return this.isStatusDisallowed(requestedStatus) ? null : this.getChunkIfPresentUnchecked(requestedStatus);
+     public ChunkAccess getChunkIfPresent(ChunkStatus status) {
+-        return this.isStatusDisallowed(status) ? null : this.getChunkIfPresentUnchecked(status);
 +        // Paper start - rewrite chunk system
-+        return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getChunkIfPresent(requestedStatus);
++        return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getChunkIfPresent(status);
 +        // Paper end - rewrite chunk system
      }
  
@@ -26185,8 +26156,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -        if (chunkStatus == null) {
 -            return null;
 -        } else {
--            ChunkAccess chunkAccess = this.getChunkIfPresentUnchecked(chunkStatus);
--            return chunkAccess != null ? chunkAccess : this.getChunkIfPresentUnchecked(chunkStatus.getParent());
+-            ChunkAccess chunkIfPresentUnchecked = this.getChunkIfPresentUnchecked(chunkStatus);
+-            return chunkIfPresentUnchecked != null ? chunkIfPresentUnchecked : this.getChunkIfPresentUnchecked(chunkStatus.getParent());
 -        }
 +        // Paper start - rewrite chunk system
 +        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getLastChunkCompletion();
@@ -26206,7 +26177,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      public ChunkPos getPos() {
-@@ -0,0 +0,0 @@ public abstract class GenerationChunkHolder {
+@@ -287,7 +134,7 @@ public abstract class GenerationChunkHolder {
      }
  
      public FullChunkStatus getFullStatus() {
@@ -26215,7 +26186,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      public abstract int getTicketLevel();
-@@ -0,0 +0,0 @@ public abstract class GenerationChunkHolder {
+@@ -296,26 +143,15 @@ public abstract class GenerationChunkHolder {
  
      @VisibleForDebug
      public List<Pair<ChunkStatus, CompletableFuture<ChunkResult<ChunkAccess>>>> getAllFutures() {
@@ -26234,8 +26205,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      public ChunkStatus getLatestStatus() {
 -        for (int i = CHUNK_STATUSES.size() - 1; i >= 0; i--) {
 -            ChunkStatus chunkStatus = CHUNK_STATUSES.get(i);
--            ChunkAccess chunkAccess = this.getChunkIfPresentUnchecked(chunkStatus);
--            if (chunkAccess != null) {
+-            ChunkAccess chunkIfPresentUnchecked = this.getChunkIfPresentUnchecked(chunkStatus);
+-            if (chunkIfPresentUnchecked != null) {
 -                return chunkStatus;
 -            }
 -        }
@@ -26247,20 +26218,20 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - rewrite chunk system
      }
  }
-diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-@@ -0,0 +0,0 @@ import net.minecraft.world.level.storage.DimensionDataStorage;
+diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java
+index 2f49dbc919f7f5eea9abce6106723c72f5ae45fb..87d4291a3944f706a694536da6de0f28c548ab8d 100644
+--- a/net/minecraft/server/level/ServerChunkCache.java
++++ b/net/minecraft/server/level/ServerChunkCache.java
+@@ -52,7 +52,7 @@ import net.minecraft.world.level.storage.DimensionDataStorage;
  import net.minecraft.world.level.storage.LevelStorageSource;
  import org.slf4j.Logger;
  
 -public class ServerChunkCache extends ChunkSource {
 +public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemServerChunkCache { // Paper - rewrite chunk system
- 
      private static final Logger LOGGER = LogUtils.getLogger();
      private final DistanceManager distanceManager;
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
+     private final ServerLevel level;
+@@ -80,6 +80,107 @@ public class ServerChunkCache extends ChunkSource {
      }
      long chunkFutureAwaitCounter;
      // Paper end
@@ -26354,7 +26325,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        final ServerPlayer[] raw = players.getRawDataUnchecked();
 +        final int len = players.size();
 +
-+        Objects.checkFromIndexSize(0, len, raw.length);
++        java.util.Objects.checkFromIndexSize(0, len, raw.length);
 +        for (int i = 0; i < len; ++i) {
 +            if (chunkMap.playerIsCloseEnoughForSpawning(raw[i], chunkPos, 16384.0D)) { // Spigot (reducedRange = false)
 +                return true;
@@ -26366,9 +26337,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    // Paper end - chunk tick iteration optimisations
 +
  
-     public ServerChunkCache(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, int simulationDistance, boolean dsync, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory) {
-         this.level = world;
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
+     public ServerChunkCache(
+         ServerLevel level,
+@@ -138,13 +239,7 @@ public class ServerChunkCache extends ChunkSource {
      }
      // CraftBukkit end
      // Paper start
@@ -26383,61 +26354,54 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      @Nullable
      public ChunkAccess getChunkAtImmediately(int x, int z) {
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
+@@ -215,51 +310,42 @@ public class ServerChunkCache extends ChunkSource {
      @Nullable
      @Override
-     public ChunkAccess getChunk(int x, int z, ChunkStatus leastStatus, boolean create) {
+     public ChunkAccess getChunk(int x, int z, ChunkStatus chunkStatus, boolean requireChunk) {
 -        if (Thread.currentThread() != this.mainThread) {
--            return (ChunkAccess) CompletableFuture.supplyAsync(() -> {
--                return this.getChunk(x, z, leastStatus, create);
--            }, this.mainThreadProcessor).join();
+-            return CompletableFuture.<ChunkAccess>supplyAsync(() -> this.getChunk(x, z, chunkStatus, requireChunk), this.mainThreadProcessor).join();
 -        } else {
 -            // Paper start - Perf: Optimise getChunkAt calls for loaded chunks
--            LevelChunk ifLoaded = this.getChunkAtIfLoadedMainThread(x, z);
+-            LevelChunk ifLoaded = this.getChunkAtIfCachedImmediately(x, z);
 -            if (ifLoaded != null) {
 -                return ifLoaded;
 -            }
 -            // Paper end - Perf: Optimise getChunkAt calls for loaded chunks
--            ProfilerFiller gameprofilerfiller = Profiler.get();
-+        // Paper start - rewrite chunk system
-+        if (leastStatus == ChunkStatus.FULL) {
-+            final LevelChunk ret = this.fullChunks.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(x, z));
- 
--            gameprofilerfiller.incrementCounter("getChunk");
--            long k = ChunkPos.asLong(x, z);
+-            ProfilerFiller profilerFiller = Profiler.get();
+-            profilerFiller.incrementCounter("getChunk");
+-            long packedChunkPos = ChunkPos.asLong(x, z);
 -
--            for (int l = 0; l < 4; ++l) {
--                if (k == this.lastChunkPos[l] && leastStatus == this.lastChunkStatus[l]) {
--                    ChunkAccess ichunkaccess = this.lastChunk[l];
--
--                    if (ichunkaccess != null) { // CraftBukkit - the chunk can become accessible in the meantime TODO for non-null chunks it might also make sense to check that the chunk's state hasn't changed in the meantime
--                        return ichunkaccess;
+-            for (int i = 0; i < 4; i++) {
+-                if (packedChunkPos == this.lastChunkPos[i] && chunkStatus == this.lastChunkStatus[i]) {
+-                    ChunkAccess chunkAccess = this.lastChunk[i];
+-                    if (chunkAccess != null) { // CraftBukkit - the chunk can become accessible in the meantime TODO for non-null chunks it might also make sense to check that the chunk's state hasn't changed in the meantime
+-                        return chunkAccess;
 -                    }
 -                }
+-            }
++        // Paper start - rewrite chunk system
++        if (chunkStatus == ChunkStatus.FULL) {
++            final LevelChunk ret = this.fullChunks.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(x, z));
+ 
+-            profilerFiller.incrementCounter("getChunkCacheMiss");
+-            CompletableFuture<ChunkResult<ChunkAccess>> chunkFutureMainThread = this.getChunkFutureMainThread(x, z, chunkStatus, requireChunk);
+-            this.mainThreadProcessor.managedBlock(chunkFutureMainThread::isDone);
+-            // com.destroystokyo.paper.io.SyncLoadFinder.logSyncLoad(this.level, x, z); // Paper - Add debug for sync chunk loads
+-            ChunkResult<ChunkAccess> chunkResult = chunkFutureMainThread.join();
+-            ChunkAccess chunkAccess1 = chunkResult.orElse(null);
+-            if (chunkAccess1 == null && requireChunk) {
+-                throw (IllegalStateException)Util.pauseInIde(new IllegalStateException("Chunk not there when requested: " + chunkResult.getError()));
+-            } else {
+-                this.storeInCache(packedChunkPos, chunkAccess1, chunkStatus);
+-                return chunkAccess1;
 +            if (ret != null) {
 +                return ret;
              }
- 
--            gameprofilerfiller.incrementCounter("getChunkCacheMiss");
--            CompletableFuture<ChunkResult<ChunkAccess>> completablefuture = this.getChunkFutureMainThread(x, z, leastStatus, create);
--            ServerChunkCache.MainThreadExecutor chunkproviderserver_b = this.mainThreadProcessor;
--
--            Objects.requireNonNull(completablefuture);
--            chunkproviderserver_b.managedBlock(completablefuture::isDone);
--            // com.destroystokyo.paper.io.SyncLoadFinder.logSyncLoad(this.level, x, z); // Paper - Add debug for sync chunk loads
--            ChunkResult<ChunkAccess> chunkresult = (ChunkResult) completablefuture.join();
--            ChunkAccess ichunkaccess1 = (ChunkAccess) chunkresult.orElse(null); // CraftBukkit - decompile error
--
--            if (ichunkaccess1 == null && create) {
--                throw (IllegalStateException) Util.pauseInIde(new IllegalStateException("Chunk not there when requested: " + chunkresult.getError()));
--            } else {
--                this.storeInCache(k, ichunkaccess1, leastStatus);
--                return ichunkaccess1;
--            }
-+            return create ? this.getChunkFallback(x, z, leastStatus, create) : null;
++
++            return requireChunk ? this.getChunkFallback(x, z, chunkStatus, requireChunk) : null;
          }
 +
-+        return this.getChunkFallback(x, z, leastStatus, create);
++        return this.getChunkFallback(x, z, chunkStatus, requireChunk);
 +        // Paper end - rewrite chunk system
      }
  
@@ -26447,7 +26411,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -        if (Thread.currentThread() != this.mainThread) {
 -            return null;
 -        } else {
--            return this.getChunkAtIfLoadedMainThread(chunkX, chunkZ); // Paper - Perf: Optimise getChunkAt calls for loaded chunks
+-            return this.getChunkAtIfCachedImmediately(chunkX, chunkZ); // Paper - Perf: Optimise getChunkAt calls for loaded chunks
 +        // Paper start - rewrite chunk system
 +        final LevelChunk ret = this.fullChunks.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
 +        if (!ca.spottedleaf.moonrise.common.PlatformHooks.get().hasCurrentlyLoadingChunk()) {
@@ -26456,64 +26420,64 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +        if (ret != null || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThread()) {
 +            return ret;
-+        }
+         }
 +
 +        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler()
 +            .chunkHolderManager.getChunkHolder(chunkX, chunkZ);
 +        if (holder == null) {
 +            return ret;
-         }
++        }
 +
 +        return ca.spottedleaf.moonrise.common.PlatformHooks.get().getCurrentlyLoadingChunk(holder.vanillaChunkHolder);
 +        // Paper end - rewrite chunk system
      }
  
      private void clearCache() {
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
+@@ -285,54 +371,59 @@ public class ServerChunkCache extends ChunkSource {
      }
  
-     private CompletableFuture<ChunkResult<ChunkAccess>> getChunkFutureMainThread(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) {
--        ChunkPos chunkcoordintpair = new ChunkPos(chunkX, chunkZ);
--        long k = chunkcoordintpair.toLong();
--        int l = ChunkLevel.byStatus(leastStatus);
--        ChunkHolder playerchunk = this.getVisibleChunkIfPresent(k);
--
+     private CompletableFuture<ChunkResult<ChunkAccess>> getChunkFutureMainThread(int x, int z, ChunkStatus chunkStatus, boolean requireChunk) {
+-        ChunkPos chunkPos = new ChunkPos(x, z);
+-        long packedChunkPos = chunkPos.toLong();
+-        int i = ChunkLevel.byStatus(chunkStatus);
+-        ChunkHolder visibleChunkIfPresent = this.getVisibleChunkIfPresent(packedChunkPos);
 -        // CraftBukkit start - don't add new ticket for currently unloading chunk
 -        boolean currentlyUnloading = false;
--        if (playerchunk != null) {
--            FullChunkStatus oldChunkState = ChunkLevel.fullStatus(playerchunk.oldTicketLevel);
--            FullChunkStatus currentChunkState = ChunkLevel.fullStatus(playerchunk.getTicketLevel());
+-        if (visibleChunkIfPresent != null) {
+-            FullChunkStatus oldChunkState = ChunkLevel.fullStatus(visibleChunkIfPresent.oldTicketLevel);
+-            FullChunkStatus currentChunkState = ChunkLevel.fullStatus(visibleChunkIfPresent.getTicketLevel());
 -            currentlyUnloading = (oldChunkState.isOrAfter(FullChunkStatus.FULL) && !currentChunkState.isOrAfter(FullChunkStatus.FULL));
 -        }
--        if (create && !currentlyUnloading) {
--            // CraftBukkit end
--            this.distanceManager.addTicket(TicketType.UNKNOWN, chunkcoordintpair, l, chunkcoordintpair);
--            if (this.chunkAbsent(playerchunk, l)) {
--                ProfilerFiller gameprofilerfiller = Profiler.get();
--
--                gameprofilerfiller.push("chunkLoad");
+-        if (requireChunk && !currentlyUnloading) {
+-        // CraftBukkit end
+-            this.distanceManager.addTicket(TicketType.UNKNOWN, chunkPos, i, chunkPos);
+-            if (this.chunkAbsent(visibleChunkIfPresent, i)) {
+-                ProfilerFiller profilerFiller = Profiler.get();
+-                profilerFiller.push("chunkLoad");
 -                this.runDistanceManagerUpdates();
--                playerchunk = this.getVisibleChunkIfPresent(k);
--                gameprofilerfiller.pop();
--                if (this.chunkAbsent(playerchunk, l)) {
--                    throw (IllegalStateException) Util.pauseInIde(new IllegalStateException("No chunk holder after ticket has been added"));
+-                visibleChunkIfPresent = this.getVisibleChunkIfPresent(packedChunkPos);
+-                profilerFiller.pop();
+-                if (this.chunkAbsent(visibleChunkIfPresent, i)) {
+-                    throw (IllegalStateException)Util.pauseInIde(new IllegalStateException("No chunk holder after ticket has been added"));
 -                }
 -            }
 +        // Paper start - rewrite chunk system
-+        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.level, chunkX, chunkZ, "Scheduling chunk load off-main");
++        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.level, x, z, "Scheduling chunk load off-main");
 +
-+        final int minLevel = ChunkLevel.byStatus(leastStatus);
-+        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ);
++        final int minLevel = ChunkLevel.byStatus(chunkStatus);
++        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(x, z);
 +
-+        final boolean needsFullScheduling = leastStatus == ChunkStatus.FULL && (chunkHolder == null || !chunkHolder.getChunkStatus().isOrAfter(FullChunkStatus.FULL));
++        final boolean needsFullScheduling = chunkStatus == ChunkStatus.FULL && (chunkHolder == null || !chunkHolder.getChunkStatus().isOrAfter(FullChunkStatus.FULL));
 +
-+        if ((chunkHolder == null || chunkHolder.getTicketLevel() > minLevel || needsFullScheduling) && !create) {
++        if ((chunkHolder == null || chunkHolder.getTicketLevel() > minLevel || needsFullScheduling) && !requireChunk) {
 +            return ChunkHolder.UNLOADED_CHUNK_FUTURE;
          }
  
--        return this.chunkAbsent(playerchunk, l) ? GenerationChunkHolder.UNLOADED_CHUNK_FUTURE : playerchunk.scheduleChunkGenerationTask(leastStatus, this.chunkMap);
+-        return this.chunkAbsent(visibleChunkIfPresent, i)
+-            ? GenerationChunkHolder.UNLOADED_CHUNK_FUTURE
+-            : visibleChunkIfPresent.scheduleChunkGenerationTask(chunkStatus, this.chunkMap);
 -    }
-+        final ChunkAccess ifPresent = chunkHolder == null ? null : chunkHolder.getChunkIfPresent(leastStatus);
++        final ChunkAccess ifPresent = chunkHolder == null ? null : chunkHolder.getChunkIfPresent(chunkStatus);
 +        if (needsFullScheduling || ifPresent == null) {
 +            // schedule
 +            final CompletableFuture<ChunkResult<ChunkAccess>> ret = new CompletableFuture<>();
@@ -26525,10 +26489,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                }
 +            };
  
--    private boolean chunkAbsent(@Nullable ChunkHolder holder, int maxLevel) {
--        return holder == null || holder.oldTicketLevel > maxLevel; // CraftBukkit using oldTicketLevel for isLoaded checks
+-    private boolean chunkAbsent(@Nullable ChunkHolder chunkHolder, int status) {
+-        return chunkHolder == null || chunkHolder.oldTicketLevel > status; // CraftBukkit using oldTicketLevel for isLoaded checks
 +            ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(
-+                chunkX, chunkZ, leastStatus, true,
++                x, z, chunkStatus, true,
 +                ca.spottedleaf.concurrentutil.util.Priority.HIGHER,
 +                complete
 +            );
@@ -26543,20 +26507,18 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      @Override
      public boolean hasChunk(int x, int z) {
--        ChunkHolder playerchunk = this.getVisibleChunkIfPresent((new ChunkPos(x, z)).toLong());
--        int k = ChunkLevel.byStatus(ChunkStatus.FULL);
--
--        return !this.chunkAbsent(playerchunk, k);
+-        ChunkHolder visibleChunkIfPresent = this.getVisibleChunkIfPresent(new ChunkPos(x, z).toLong());
+-        int i = ChunkLevel.byStatus(ChunkStatus.FULL);
+-        return !this.chunkAbsent(visibleChunkIfPresent, i);
 +        return this.getChunkNow(x, z) != null; // Paper - rewrite chunk system
      }
  
      @Nullable
      @Override
      public LightChunk getChunkForLighting(int chunkX, int chunkZ) {
--        long k = ChunkPos.asLong(chunkX, chunkZ);
--        ChunkHolder playerchunk = this.getVisibleChunkIfPresent(k);
--
--        return playerchunk == null ? null : playerchunk.getChunkIfPresentUnchecked(ChunkStatus.INITIALIZE_LIGHT.getParent());
+-        long packedChunkPos = ChunkPos.asLong(chunkX, chunkZ);
+-        ChunkHolder visibleChunkIfPresent = this.getVisibleChunkIfPresent(packedChunkPos);
+-        return visibleChunkIfPresent == null ? null : visibleChunkIfPresent.getChunkIfPresentUnchecked(ChunkStatus.INITIALIZE_LIGHT.getParent());
 +        // Paper start - rewrite chunk system
 +        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ);
 +        if (newChunkHolder == null) {
@@ -26567,13 +26529,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      @Override
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
+@@ -345,28 +436,18 @@ public class ServerChunkCache extends ChunkSource {
      }
  
      public boolean runDistanceManagerUpdates() { // Paper - public
 -        boolean flag = this.distanceManager.runAllUpdates(this.chunkMap);
 -        boolean flag1 = this.chunkMap.promoteChunkMap();
--
 -        this.chunkMap.runGenerationTasks();
 -        if (!flag && !flag1) {
 -            return false;
@@ -26584,16 +26545,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(); // Paper - rewrite chunk system
      }
  
-     public boolean isPositionTicking(long pos) {
--        if (!this.level.shouldTickBlocksAt(pos)) {
+     public boolean isPositionTicking(long chunkPos) {
+-        if (!this.level.shouldTickBlocksAt(chunkPos)) {
 -            return false;
 -        } else {
--            ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos);
--
--            return playerchunk == null ? false : ((ChunkResult) playerchunk.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).isSuccess();
+-            ChunkHolder visibleChunkIfPresent = this.getVisibleChunkIfPresent(chunkPos);
+-            return visibleChunkIfPresent != null && visibleChunkIfPresent.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK).isSuccess();
 -        }
 +        // Paper start - rewrite chunk system
-+        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(pos);
++        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkPos);
 +        return newChunkHolder != null && newChunkHolder.isTickingReady();
 +        // Paper end - rewrite chunk system
      }
@@ -26604,7 +26564,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          this.chunkMap.saveAllChunks(flush);
      }
  
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
+@@ -377,17 +458,15 @@ public class ServerChunkCache extends ChunkSource {
      }
  
      public void close(boolean save) throws IOException {
@@ -26625,50 +26585,50 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          ProfilerFiller gameprofilerfiller = Profiler.get();
  
          gameprofilerfiller.push("purge");
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
+@@ -411,6 +490,7 @@ public class ServerChunkCache extends ChunkSource {
          this.runDistanceManagerUpdates();
-         gameprofilerfiller.popPush("chunks");
+         profilerFiller.popPush("chunks");
          if (tickChunks) {
 +            ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().tick(); // Paper - rewrite chunk system
              this.tickChunks();
              this.chunkMap.tick();
          }
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
-                     gameprofilerfiller.push("filteringTickingChunks");
+@@ -435,7 +515,10 @@ public class ServerChunkCache extends ChunkSource {
+                     profilerFiller.push("filteringTickingChunks");
                      this.collectTickingChunks(list);
-                     gameprofilerfiller.popPush("shuffleChunks");
+                     profilerFiller.popPush("shuffleChunks");
 -                    Util.shuffle(list, this.level.random);
 +                    // Paper start - chunk tick iteration optimisation
 +                    this.shuffleRandom.setSeed(this.level.random.nextLong());
 +                    Util.shuffle(list, this.shuffleRandom);
 +                    // Paper end - chunk tick iteration optimisation
-                     this.tickChunks(gameprofilerfiller, j, list);
-                     gameprofilerfiller.pop();
+                     this.tickChunks(profilerFiller, l, list);
+                     profilerFiller.pop();
                  } finally {
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
+@@ -452,7 +535,7 @@ public class ServerChunkCache extends ChunkSource {
+         profiler.push("broadcast");
  
-         while (iterator.hasNext()) {
-             ChunkHolder playerchunk = (ChunkHolder) iterator.next();
--            LevelChunk chunk = playerchunk.getTickingChunk();
-+            LevelChunk chunk = playerchunk.getChunkToSend(); // Paper - rewrite chunk system
- 
-             if (chunk != null) {
-                 playerchunk.broadcastChanges(chunk);
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
+         for (ChunkHolder chunkHolder : this.chunkHoldersToBroadcast) {
+-            LevelChunk tickingChunk = chunkHolder.getTickingChunk();
++            LevelChunk tickingChunk = chunkHolder.getChunkToSend(); // Paper - rewrite chunk system
+             if (tickingChunk != null) {
+                 chunkHolder.broadcastChanges(tickingChunk);
+             }
+@@ -463,12 +546,26 @@ public class ServerChunkCache extends ChunkSource {
      }
  
-     private void collectTickingChunks(List<LevelChunk> chunks) {
--        this.chunkMap.forEachSpawnCandidateChunk((playerchunk) -> {
--            LevelChunk chunk = playerchunk.getTickingChunk();
+     private void collectTickingChunks(List<LevelChunk> output) {
+-        this.chunkMap.forEachSpawnCandidateChunk(chunk -> {
+-            LevelChunk tickingChunk = chunk.getTickingChunk();
+-            if (tickingChunk != null && this.level.isNaturalSpawningAllowed(chunk.getPos())) {
+-                output.add(tickingChunk);
 +        // Paper start - chunk tick iteration optimisation
 +        final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> tickingChunks =
 +            ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel)this.level).moonrise$getPlayerTickingChunks();
 +
 +        final ServerChunkCache.ChunkAndHolder[] raw = tickingChunks.getRawDataUnchecked();
 +        final int size = tickingChunks.size();
- 
--            if (chunk != null && this.level.isNaturalSpawningAllowed(playerchunk.getPos())) {
--                chunks.add(chunk);
++
 +        final ChunkMap chunkMap = this.chunkMap;
 +
 +        for (int i = 0; i < size; ++i) {
@@ -26678,43 +26638,42 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            if (!this.isChunkNearPlayer(chunkMap, levelChunk.getPos(), levelChunk)) {
 +                continue;
              }
- 
 -        });
-+            chunks.add(levelChunk);
++
++            output.add(levelChunk);
 +        }
 +        // Paper end - chunk tick iteration optimisation
      }
  
-     private void tickChunks(ProfilerFiller profiler, long timeDelta, List<LevelChunk> chunks) {
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
-                 NaturalSpawner.spawnForChunk(this.level, chunk, spawnercreature_d, list1);
+     private void tickChunks(ProfilerFiller profiler, long timeInhabited, List<LevelChunk> chunks) {
+@@ -504,7 +601,7 @@ public class ServerChunkCache extends ChunkSource {
+                 NaturalSpawner.spawnForChunk(this.level, levelChunk, spawnState, filteredSpawningCategories);
              }
  
--            if (this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) {
+-            if (this.level.shouldTickBlocksAt(pos.toLong())) {
 +            if (true) { // Paper - rewrite chunk system
-                 this.level.tickChunk(chunk, k);
+                 this.level.tickChunk(levelChunk, _int);
              }
          }
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
+@@ -516,10 +613,13 @@ public class ServerChunkCache extends ChunkSource {
      }
  
-     private void getFullChunk(long pos, Consumer<LevelChunk> chunkConsumer) {
--        ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos);
--
--        if (playerchunk != null) {
--            ((ChunkResult) playerchunk.getFullChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).ifSuccess(chunkConsumer);
+     private void getFullChunk(long chunkPos, Consumer<LevelChunk> fullChunkGetter) {
+-        ChunkHolder visibleChunkIfPresent = this.getVisibleChunkIfPresent(chunkPos);
+-        if (visibleChunkIfPresent != null) {
+-            visibleChunkIfPresent.getFullChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK).ifSuccess(fullChunkGetter);
 +        // Paper start - rewrite chunk system
 +        // note: bypass currentlyLoaded from getChunkNow
-+        final LevelChunk fullChunk = this.fullChunks.get(pos);
++        final LevelChunk fullChunk = this.fullChunks.get(chunkPos);
 +        if (fullChunk != null) {
-+            chunkConsumer.accept(fullChunk);
++            fullChunkGetter.accept(fullChunk);
          }
 +        // Paper end - rewrite chunk system
- 
      }
  
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
-         this.chunkMap.setServerViewDistance(watchDistance);
+     @Override
+@@ -607,6 +707,12 @@ public class ServerChunkCache extends ChunkSource {
+         this.chunkMap.setServerViewDistance(viewDistance);
      }
  
 +    // Paper start - rewrite chunk system
@@ -26726,11 +26685,20 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      public void setSimulationDistance(int simulationDistance) {
          this.distanceManager.updateSimulationDistance(simulationDistance);
      }
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
+@@ -654,7 +760,7 @@ public class ServerChunkCache extends ChunkSource {
+         }
+     }
+ 
+-    record ChunkAndHolder(LevelChunk chunk, ChunkHolder holder) {
++    public record ChunkAndHolder(LevelChunk chunk, ChunkHolder holder) { // Paper - public
+     }
+ 
+     public final class MainThreadExecutor extends BlockableEventLoop<Runnable> {
+@@ -695,18 +801,14 @@ public class ServerChunkCache extends ChunkSource {
+ 
          @Override
-         // CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task
          public boolean pollTask() {
--        try {
+-            try { // CraftBukkit - process pending Chunk loadCallback() and unloadCallback() after each run task
 -            if (ServerChunkCache.this.runDistanceManagerUpdates()) {
 +            // Paper start - rewrite chunk system
 +            final ServerChunkCache serverChunkCache = ServerChunkCache.this;
@@ -26739,26 +26707,22 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
              } else {
 -                ServerChunkCache.this.lightEngine.tryScheduleUpdate();
 -                return super.pollTask();
+-            }
+-            // CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task
+-            } finally {
+-                ServerChunkCache.this.chunkMap.callbackExecutor.run();
 +                return super.pollTask() | ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)serverChunkCache.level).moonrise$getChunkTaskScheduler().executeMainThreadTask();
              }
--        } finally {
--            ServerChunkCache.this.chunkMap.callbackExecutor.run();
--        }
+-            // CraftBukkit end - process pending Chunk loadCallback() and unloadCallback() after each run task
 +            // Paper end - rewrite chunk system
-         // CraftBukkit end
          }
      }
- 
--    private static record ChunkAndHolder(LevelChunk chunk, ChunkHolder holder) {
-+    public static record ChunkAndHolder(LevelChunk chunk, ChunkHolder holder) { // Paper - rewrite chunk system - public
- 
-     }
  }
-diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerEntity.java
-+++ b/src/main/java/net/minecraft/server/level/ServerEntity.java
-@@ -0,0 +0,0 @@ public class ServerEntity {
+diff --git a/net/minecraft/server/level/ServerEntity.java b/net/minecraft/server/level/ServerEntity.java
+index 70f6d068b3f3665b282d9750310c883839120ab2..870b9efd445ddadb3725e88351555ad986ce7c72 100644
+--- a/net/minecraft/server/level/ServerEntity.java
++++ b/net/minecraft/server/level/ServerEntity.java
+@@ -91,6 +91,11 @@ public class ServerEntity {
      }
  
      public void sendChanges() {
@@ -26767,24 +26731,24 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            this.teleportDelay = 9999;
 +        }
 +        // Paper end - optimise collisions
-         List<Entity> list = this.entity.getPassengers();
- 
-         if (!list.equals(this.lastPassengers)) {
-diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerLevel.java
-+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
-@@ -0,0 +0,0 @@ import org.bukkit.event.weather.LightningStrikeEvent;
- import org.bukkit.event.world.TimeSkipEvent;
- // CraftBukkit end
+         List<Entity> passengers = this.entity.getPassengers();
+         if (!passengers.equals(this.lastPassengers)) {
+             this.broadcastAndSend(new ClientboundSetPassengersPacket(this.entity)); // CraftBukkit
+diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java
+index 192977dd661ee795ada13db895db770293e9b402..95a4e37a3c93f9b3c56c7a7376ed521cd46fbb6f 100644
+--- a/net/minecraft/server/level/ServerLevel.java
++++ b/net/minecraft/server/level/ServerLevel.java
+@@ -170,7 +170,7 @@ import net.minecraft.world.phys.shapes.VoxelShape;
+ import net.minecraft.world.ticks.LevelTicks;
+ import org.slf4j.Logger;
  
 -public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLevel {
-+public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLevel, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevelReader, ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel {  // Paper - rewrite chunk system // Paper - chunk tick iteration
- 
++public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLevel, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevelReader, ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel { // Paper - rewrite chunk system // Paper - chunk tick iteration
      public static final BlockPos END_SPAWN_POINT = new BlockPos(100, 50, 0);
      public static final IntProvider RAIN_DELAY = UniformInt.of(12000, 180000);
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
-     public final PrimaryLevelData serverLevelData; // CraftBukkit - type
+     public static final IntProvider RAIN_DURATION = UniformInt.of(12000, 24000);
+@@ -185,7 +185,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+     public final net.minecraft.world.level.storage.PrimaryLevelData serverLevelData; // CraftBukkit - type
      private int lastSpawnChunkRadius;
      final EntityTickList entityTickList = new EntityTickList();
 -    public final PersistentEntitySectionManager<Entity> entityManager;
@@ -26792,7 +26756,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      private final GameEventDispatcher gameEventDispatcher;
      public boolean noSave;
      private final SleepStatus sleepStatus;
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+@@ -256,12 +256,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
  
      public final void loadChunksForMoveAsync(AABB axisalignedbb, ca.spottedleaf.concurrentutil.util.Priority priority,
                                               java.util.function.Consumer<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
@@ -26806,7 +26770,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3;
          int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3;
  
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+@@ -280,32 +275,159 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
      public final void loadChunks(int minChunkX, int minChunkZ, int maxChunkX, int maxChunkZ,
                                   ca.spottedleaf.concurrentutil.util.Priority priority,
                                   java.util.function.Consumer<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
@@ -26854,9 +26818,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = newChunkHolder.getLastChunkCompletion();
 +        return lastCompletion == null ? null : lastCompletion.chunk();
 +    }
- 
--        int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1);
--        int[] loadedChunks = new int[1];
++
 +    @Override
 +    public final ChunkAccess moonrise$getSpecificChunkIfLoaded(final int chunkX, final int chunkZ, final net.minecraft.world.level.chunk.status.ChunkStatus leastStatus) {
 +        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ);
@@ -26875,14 +26837,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public final ChunkAccess moonrise$syncLoadNonFull(final int chunkX, final int chunkZ, final net.minecraft.world.level.chunk.status.ChunkStatus status) {
 +        return this.moonrise$getChunkTaskScheduler().syncLoadNonFull(chunkX, chunkZ, status);
 +    }
- 
--        Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter++);
++
 +    @Override
 +    public final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler moonrise$getChunkTaskScheduler() {
 +        return this.chunkTaskScheduler;
 +    }
- 
--        java.util.function.Consumer<net.minecraft.world.level.chunk.ChunkAccess> consumer = (net.minecraft.world.level.chunk.ChunkAccess chunk) -> {
++
 +    @Override
 +    public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController  moonrise$getChunkDataController() {
 +        return this.chunkDataController;
@@ -26947,14 +26907,18 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                                               final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
 +        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler = this.moonrise$getChunkTaskScheduler();
 +        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager chunkHolderManager = chunkTaskScheduler.chunkHolderManager;
-+
+ 
+-        int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1);
+-        int[] loadedChunks = new int[1];
 +        final int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1);
 +        final java.util.concurrent.atomic.AtomicInteger loadedChunks = new java.util.concurrent.atomic.AtomicInteger();
 +        final Long holderIdentifier = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getNextChunkLoadId();
 +        final int ticketLevel = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getTicketLevel(chunkStatus);
-+
+ 
+-        Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter++);
 +        final List<ChunkAccess> ret = new ArrayList<>(requiredChunks);
-+
+ 
+-        java.util.function.Consumer<net.minecraft.world.level.chunk.ChunkAccess> consumer = (net.minecraft.world.level.chunk.ChunkAccess chunk) -> {
 +        final java.util.function.Consumer<net.minecraft.world.level.chunk.ChunkAccess> consumer = (final ChunkAccess chunk) -> {
              if (chunk != null) {
 -                int ticketLevel = Math.max(33, chunkProvider.chunkMap.getUpdatingChunkIfPresent(chunk.getPos().toLong()).getTicketLevel());
@@ -26982,14 +26946,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
                      }
                  }
              }
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
- 
-         for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
-             for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
--                ca.spottedleaf.moonrise.common.util.ChunkSystem.scheduleChunkLoad(
--                    this, cx, cz, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true, priority, consumer
--                );
-+                chunkTaskScheduler.scheduleChunkLoad(cx, cz, chunkStatus, true, priority, consumer);
+@@ -319,16 +441,133 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
              }
          }
      }
@@ -27003,8 +26960,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -        return player != null && player.level() == this ? player : null;
 +    public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder() {
 +        return this.viewDistanceHolder;
-     }
--    // Paper end - optimise getPlayerByUUID
++    }
 +
 +    @Override
 +    public final long moonrise$getLastMidTickFailure() {
@@ -27019,7 +26975,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    @Override
 +    public final ca.spottedleaf.moonrise.common.misc.NearbyPlayers moonrise$getNearbyPlayers() {
 +        return this.nearbyPlayers;
-+    }
+     }
+-    // Paper end - optimise getPlayerByUUID
 +
 +    @Override
 +    public final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> moonrise$getLoadedChunks() {
@@ -27128,38 +27085,49 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - chunk tick iteration
  
-     // Add env and gen to constructor, IWorldDataServer -> WorldDataServer
-     public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey<Level> resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List<CustomSpawner> list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) {
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
-         DataFixer datafixer = minecraftserver.getFixerUpper();
-         EntityPersistentStorage<Entity> entitypersistentstorage = new EntityStorage(new SimpleRegionStorage(new RegionStorageInfo(convertable_conversionsession.getLevelId(), resourcekey, "entities"), convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"), datafixer, flag2, DataFixTypes.ENTITY_CHUNK), this, minecraftserver);
- 
--        this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entitypersistentstorage);
+     public ServerLevel(
+         MinecraftServer server,
+@@ -376,18 +615,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+         // CraftBukkit end
+         boolean flag = server.forceSynchronousWrites();
+         DataFixer fixerUpper = server.getFixerUpper();
+-        EntityPersistentStorage<Entity> entityPersistentStorage = new EntityStorage(
+-            new SimpleRegionStorage(
+-                new RegionStorageInfo(levelStorageAccess.getLevelId(), dimension, "entities"),
+-                levelStorageAccess.getDimensionPath(dimension).resolve("entities"),
+-                fixerUpper,
+-                flag,
+-                DataFixTypes.ENTITY_CHUNK
+-            ),
+-            this,
+-            server
+-        );
+-        this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entityPersistentStorage);
 +        // Paper - rewrite chunk system
-         StructureTemplateManager structuretemplatemanager = minecraftserver.getStructureManager();
-         int j = this.spigotConfig.viewDistance; // Spigot
-         int k = this.spigotConfig.simulationDistance; // Spigot
--        PersistentEntitySectionManager persistententitysectionmanager = this.entityManager;
-+        // Paper - rewrite chunk system
- 
--        Objects.requireNonNull(this.entityManager);
--        this.chunkSource = new ServerChunkCache(this, convertable_conversionsession, datafixer, structuretemplatemanager, executor, chunkgenerator, j, k, flag2, worldloadlistener, persistententitysectionmanager::updateChunkStatus, () -> {
-+        this.chunkSource = new ServerChunkCache(this, convertable_conversionsession, datafixer, structuretemplatemanager, executor, chunkgenerator, j, k, flag2, worldloadlistener, null, () -> { // Paper - rewrite chunk system
-             return minecraftserver.overworld().getDataStorage();
-         });
+         this.chunkSource = new ServerChunkCache(
+             this,
+             levelStorageAccess,
+@@ -399,7 +627,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+             this.spigotConfig.simulationDistance, // Spigot
+             flag,
+             progressListener,
+-            this.entityManager::updateChunkStatus,
++            null, // Paper - rewrite chunk system
+             () -> server.overworld().getDataStorage()
+         );
          this.chunkSource.getGeneratorState().ensureStructuresGenerated();
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
-         this.randomSequences = (RandomSequences) Objects.requireNonNullElseGet(randomsequences, () -> {
-             return (RandomSequences) this.getDataStorage().computeIfAbsent(RandomSequences.factory(l), "random_sequences");
-         });
+@@ -437,6 +665,20 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+         this.randomSequences = Objects.requireNonNullElseGet(
+             randomSequences, () -> this.getDataStorage().computeIfAbsent(RandomSequences.factory(seed), "random_sequences")
+         );
 +        // Paper start - rewrite chunk system
 +        this.moonrise$setEntityLookup(new ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup((ServerLevel)(Object)this, ((ServerLevel)(Object)this).new EntityCallbacks()));
 +        this.chunkTaskScheduler = new ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler((ServerLevel)(Object)this);
 +        this.entityDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.EntityDataController(
 +            new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.EntityDataController.EntityRegionFileStorage(
-+                new RegionStorageInfo(convertable_conversionsession.getLevelId(), resourcekey, "entities"),
-+                convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"),
-+                minecraftserver.forceSynchronousWrites()
++                new RegionStorageInfo(levelStorageAccess.getLevelId(), dimension, "entities"),
++                levelStorageAccess.getDimensionPath(dimension).resolve("entities"),
++                server.forceSynchronousWrites()
 +            ),
 +            this.chunkTaskScheduler
 +        );
@@ -27169,22 +27137,23 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          this.getCraftServer().addWorld(this.getWorld()); // CraftBukkit
      }
  
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
-                         gameprofilerfiller.push("checkDespawn");
-                         entity.checkDespawn();
-                         gameprofilerfiller.pop();
--                        if (entity instanceof ServerPlayer || this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) {
-+                        if (true) { // Paper - rewrite chunk system
-                             Entity entity1 = entity.getVehicle();
- 
-                             if (entity1 != null) {
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+@@ -560,8 +802,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+                                 profilerFiller.push("checkDespawn");
+                                 entity.checkDespawn();
+                                 profilerFiller.pop();
+-                                if (entity instanceof ServerPlayer
+-                                    || this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) {
++                                if (true) { // Paper - rewrite chunk system
+                                     Entity vehicle = entity.getVehicle();
+                                     if (vehicle != null) {
+                                         if (!vehicle.isRemoved() && vehicle.hasPassenger(entity)) {
+@@ -584,13 +825,16 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
          }
  
-         gameprofilerfiller.push("entityManagement");
+         profilerFiller.push("entityManagement");
 -        this.entityManager.tick();
 +        // Paper - rewrite chunk system
-         gameprofilerfiller.pop();
+         profilerFiller.pop();
      }
  
      @Override
@@ -27197,8 +27166,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      protected void tickTime() {
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
-         });
+@@ -621,14 +865,67 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+         this.players.stream().filter(LivingEntity::isSleeping).collect(Collectors.toList()).forEach(player -> player.stopSleepInBed(false, false));
      }
  
 +    // Paper start - optimise random ticking
@@ -27255,128 +27224,125 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
      public void tickChunk(LevelChunk chunk, int randomTickSpeed) {
 +        final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = this.simpleRandom; // Paper - optimise random ticking
-         ChunkPos chunkcoordintpair = chunk.getPos();
-         boolean flag = this.isRaining();
-         int j = chunkcoordintpair.getMinBlockX();
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
-         ProfilerFiller gameprofilerfiller = Profiler.get();
- 
-         gameprofilerfiller.push("thunder");
--        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
-+        if (!this.paperConfig().environment.disableThunder && flag && this.isThundering() && this.spigotConfig.thunderChance > 0 && simpleRandom.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot // Paper - Option to disable thunder // Paper - optimise random ticking
-             BlockPos blockposition = this.findLightningTargetAround(this.getBlockRandomPos(j, 0, k, 15));
- 
-             if (this.isRainingAt(blockposition)) {
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+         ChunkPos pos = chunk.getPos();
+         boolean isRaining = this.isRaining();
+         int minBlockX = pos.getMinBlockX();
+         int minBlockZ = pos.getMinBlockZ();
+         ProfilerFiller profilerFiller = Profiler.get();
+         profilerFiller.push("thunder");
+-        if (!this.paperConfig().environment.disableThunder && isRaining && this.isThundering() && this.spigotConfig.thunderChance > 0 && this.random.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot // Paper - Option to disable thunder
++        if (!this.paperConfig().environment.disableThunder && isRaining && this.isThundering() && this.spigotConfig.thunderChance > 0 && simpleRandom.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot // Paper - Option to disable thunder // Paper - optimise random ticking
+             BlockPos blockPos = this.findLightningTargetAround(this.getBlockRandomPos(minBlockX, 0, minBlockZ, 15));
+             if (this.isRainingAt(blockPos)) {
+                 DifficultyInstance currentDifficultyAt = this.getCurrentDifficultyAt(blockPos);
+@@ -658,7 +955,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
  
          if (!this.paperConfig().environment.disableIceAndSnow) { // Paper - Option to disable ice and snow
-         for (int l = 0; l < randomTickSpeed; ++l) {
+         for (int i = 0; i < randomTickSpeed; i++) {
 -            if (this.random.nextInt(48) == 0) {
-+            if (simpleRandom.nextInt(48) == 0) { // Paper - optimise random ticking
-                 this.tickPrecipitation(this.getBlockRandomPos(j, 0, k, 15));
++            if (simpleRandom.nextInt(48) == 0) {  // Paper - optimise random ticking
+                 this.tickPrecipitation(this.getBlockRandomPos(minBlockX, 0, minBlockZ, 15));
              }
          }
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+@@ -666,33 +963,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
  
-         gameprofilerfiller.popPush("tickBlocks");
+         profilerFiller.popPush("tickBlocks");
          if (randomTickSpeed > 0) {
--            LevelChunkSection[] achunksection = chunk.getSections();
+-            LevelChunkSection[] sections = chunk.getSections();
 -
--            for (int i1 = 0; i1 < achunksection.length; ++i1) {
--                LevelChunkSection chunksection = achunksection[i1];
+-            for (int i1 = 0; i1 < sections.length; i1++) {
+-                LevelChunkSection levelChunkSection = sections[i1];
+-                if (levelChunkSection.isRandomlyTicking()) {
+-                    int sectionYFromSectionIndex = chunk.getSectionYFromSectionIndex(i1);
+-                    int blockPosCoord = SectionPos.sectionToBlockCoord(sectionYFromSectionIndex);
 -
--                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);
+-                    for (int i2 = 0; i2 < randomTickSpeed; i2++) {
+-                        BlockPos blockRandomPos = this.getBlockRandomPos(minBlockX, blockPosCoord, minBlockZ, 15);
+-                        profilerFiller.push("randomTick");
+-                        BlockState blockState = levelChunkSection.getBlockState(
+-                            blockRandomPos.getX() - minBlockX, blockRandomPos.getY() - blockPosCoord, blockRandomPos.getZ() - minBlockZ
+-                        );
+-                        if (blockState.isRandomlyTicking()) {
+-                            blockState.randomTick(this, blockRandomPos, this.random);
 -                        }
 -
--                        FluidState fluid = iblockdata.getFluidState();
--
--                        if (fluid.isRandomlyTicking()) {
--                            fluid.randomTick(this, blockposition1, this.random);
+-                        FluidState fluidState = blockState.getFluidState();
+-                        if (fluidState.isRandomlyTicking()) {
+-                            fluidState.randomTick(this, blockRandomPos, this.random);
 -                        }
 -
--                        gameprofilerfiller.pop();
+-                        profilerFiller.pop();
 -                    }
 -                }
 -            }
 +            this.optimiseRandomTick(chunk, randomTickSpeed); // Paper - optimise random ticking
          }
  
-         gameprofilerfiller.pop();
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
-         if (fluid1.is(fluid)) {
-             fluid1.tick(this, pos, iblockdata);
+         profilerFiller.pop();
+@@ -946,6 +1217,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+         if (fluidState.is(fluid)) {
+             fluidState.tick(this, pos, blockState);
          }
 +        // Paper start - rewrite chunk system
 +        if ((++this.tickedBlocksOrFluids & 7L) != 0L) {
 +            ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks();
 +        }
 +        // Paper end - rewrite chunk system
- 
++
      }
  
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
-         if (iblockdata.is(block)) {
-             iblockdata.tick(this, pos, this.random);
+     private void tickBlock(BlockPos pos, Block block) {
+@@ -953,6 +1230,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+         if (blockState.is(block)) {
+             blockState.tick(this, pos, this.random);
          }
 +        // Paper start - rewrite chunk system
 +        if ((++this.tickedBlocksOrFluids & 7L) != 0L) {
 +            ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks();
 +        }
 +        // Paper end - rewrite chunk system
- 
++
      }
  
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+     // Paper start - log detailed entity tick information
+@@ -1033,6 +1316,11 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
      }
  
-     public void save(@Nullable ProgressListener progressListener, boolean flush, boolean savingDisabled) {
+     public void save(@Nullable ProgressListener progress, boolean flush, boolean skipSave) {
 +        // Paper start - add close param
-+        this.save(progressListener, flush, savingDisabled, false);
++        this.save(progress, flush, skipSave, false);
 +    }
-+    public void save(@Nullable ProgressListener progressListener, boolean flush, boolean savingDisabled, boolean close) {
++    public void save(@Nullable ProgressListener progress, boolean flush, boolean skipSave, boolean close) {
 +        // Paper end - add close param
-         ServerChunkCache chunkproviderserver = this.getChunkSource();
- 
-         if (!savingDisabled) {
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
-                 progressListener.progressStage(Component.translatable("menu.savingChunks"));
+         ServerChunkCache chunkSource = this.getChunkSource();
+         if (!skipSave) {
+             org.bukkit.Bukkit.getPluginManager().callEvent(new org.bukkit.event.world.WorldSaveEvent(this.getWorld())); // CraftBukkit
+@@ -1045,13 +1333,18 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+                 progress.progressStage(Component.translatable("menu.savingChunks"));
              }
  
--            chunkproviderserver.save(flush);
+-            chunkSource.save(flush);
 -            if (flush) {
 -                this.entityManager.saveAll();
 -            } else {
 -                this.entityManager.autoSave();
--            }
-+            if (!close) { chunkproviderserver.save(flush); } // Paper - add close param
++            if (!close) { chunkSource.save(flush); } // Paper - add close param
 +            // Paper - rewrite chunk system
- 
-         }
++        }
 +        // Paper start - add close param
 +        if (close) {
 +            try {
-+                chunkproviderserver.close(!savingDisabled);
++                chunkSource.close(!skipSave);
 +            } catch (IOException never) {
 +                throw new RuntimeException(never);
-+            }
-+        }
+             }
+         }
 +        // Paper end - add close param
  
          // CraftBukkit start - moved from MinecraftServer.saveChunks
          ServerLevel worldserver1 = this;
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
-             this.removePlayerImmediately((ServerPlayer) entity, Entity.RemovalReason.DISCARDED);
+@@ -1182,7 +1475,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+             this.removePlayerImmediately((ServerPlayer)entity, Entity.RemovalReason.DISCARDED);
          }
  
 -        this.entityManager.addNewEntity(player);
@@ -27384,7 +27350,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      // CraftBukkit start
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+@@ -1213,7 +1506,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
              }
              // CraftBukkit end
  
@@ -27393,56 +27359,52 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          }
      }
  
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+@@ -1224,7 +1517,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
  
      public boolean tryAddFreshEntityWithPassengers(Entity entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) {
          // CraftBukkit end
--        Stream<UUID> stream = entity.getSelfAndPassengers().map(Entity::getUUID); // CraftBukkit - decompile error
--        PersistentEntitySectionManager persistententitysectionmanager = this.entityManager;
--
--        Objects.requireNonNull(this.entityManager);
--        if (stream.anyMatch(persistententitysectionmanager::isLoaded)) {
+-        if (entity.getSelfAndPassengers().map(Entity::getUUID).anyMatch(this.entityManager::isLoaded)) {
 +        if (entity.getSelfAndPassengers().map(Entity::getUUID).anyMatch(this.moonrise$getEntityLookup()::hasEntity)) { // Paper - rewrite chunk system
              return false;
          } else {
              this.addFreshEntityWithPassengers(entity, reason); // CraftBukkit
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+@@ -1959,7 +2252,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
                  }
              }
  
--            bufferedwriter.write(String.format(Locale.ROOT, "entities: %s\n", this.entityManager.gatherStats()));
-+            bufferedwriter.write(String.format(Locale.ROOT, "entities: %s\n", this.moonrise$getEntityLookup().getDebugInfo())); // Paper - rewrite chunk system
-             bufferedwriter.write(String.format(Locale.ROOT, "block_entity_tickers: %d\n", this.blockEntityTickers.size()));
-             bufferedwriter.write(String.format(Locale.ROOT, "block_ticks: %d\n", this.getBlockTicks().count()));
-             bufferedwriter.write(String.format(Locale.ROOT, "fluid_ticks: %d\n", this.getFluidTicks().count()));
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
-         BufferedWriter bufferedwriter2 = Files.newBufferedWriter(path1);
+-            bufferedWriter.write(String.format(Locale.ROOT, "entities: %s\n", this.entityManager.gatherStats()));
++            bufferedWriter.write(String.format(Locale.ROOT, "entities: %s\n", this.moonrise$getEntityLookup().getDebugInfo()));  // Paper - rewrite chunk system
+             bufferedWriter.write(String.format(Locale.ROOT, "block_entity_tickers: %d\n", this.blockEntityTickers.size()));
+             bufferedWriter.write(String.format(Locale.ROOT, "block_ticks: %d\n", this.getBlockTicks().count()));
+             bufferedWriter.write(String.format(Locale.ROOT, "fluid_ticks: %d\n", this.getFluidTicks().count()));
+@@ -1977,13 +2270,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+         Path path1 = path.resolve("chunks.csv");
  
-         try {
--            playerchunkmap.dumpChunks(bufferedwriter2);
-+            //playerchunkmap.dumpChunks(bufferedwriter2); // Paper - rewrite chunk system
-         } catch (Throwable throwable4) {
-             if (bufferedwriter2 != null) {
-                 try {
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
-         BufferedWriter bufferedwriter3 = Files.newBufferedWriter(path2);
+         try (Writer bufferedWriter2 = Files.newBufferedWriter(path1)) {
+-            chunkMap.dumpChunks(bufferedWriter2);
++            //chunkMap.dumpChunks(bufferedWriter2); // Paper - rewrite chunk system
+         }
  
-         try {
--            this.entityManager.dumpSections(bufferedwriter3);
-+            //this.entityManager.dumpSections(bufferedwriter3); // Paper - rewrite chunk system
-         } catch (Throwable throwable6) {
-             if (bufferedwriter3 != null) {
-                 try {
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+         Path path2 = path.resolve("entity_chunks.csv");
  
-     @VisibleForTesting
-     public String getWatchdogStats() {
--        return String.format(Locale.ROOT, "players: %s, entities: %s [%s], block_entities: %d [%s], block_ticks: %d, fluid_ticks: %d, chunk_source: %s", this.players.size(), this.entityManager.gatherStats(), ServerLevel.getTypeCount(this.entityManager.getEntityGetter().getAll(), (entity) -> {
-+        return String.format(Locale.ROOT, "players: %s, entities: %s [%s], block_entities: %d [%s], block_ticks: %d, fluid_ticks: %d, chunk_source: %s", this.players.size(), this.moonrise$getEntityLookup().getDebugInfo(), ServerLevel.getTypeCount(this.moonrise$getEntityLookup().getAll(), (entity) -> { // Paper - rewrite chunk system
-             return BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString();
-         }), this.blockEntityTickers.size(), ServerLevel.getTypeCount(this.blockEntityTickers, TickingBlockEntity::getType), this.getBlockTicks().count(), this.getFluidTicks().count(), this.gatherChunkSourceStats());
-     }
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+         try (Writer bufferedWriter3 = Files.newBufferedWriter(path2)) {
+-            this.entityManager.dumpSections(bufferedWriter3);
++            //this.entityManager.dumpSections(bufferedWriter3); // Paper - rewrite chunk system
+         }
+ 
+         Path path3 = path.resolve("entities.csv");
+@@ -2092,8 +2385,8 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+             Locale.ROOT,
+             "players: %s, entities: %s [%s], block_entities: %d [%s], block_ticks: %d, fluid_ticks: %d, chunk_source: %s",
+             this.players.size(),
+-            this.entityManager.gatherStats(),
+-            getTypeCount(this.entityManager.getEntityGetter().getAll(), entity -> BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString()),
++            this.moonrise$getEntityLookup().getDebugInfo(), // Paper - rewrite chunk system
++            getTypeCount(this.moonrise$getEntityLookup().getAll(), entity -> BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString()), // Paper - rewrite chunk system
+             this.blockEntityTickers.size(),
+             getTypeCount(this.blockEntityTickers, TickingBlockEntity::getType),
+             this.getBlockTicks().count(),
+@@ -2125,15 +2418,25 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
      @Override
      public LevelEntityGetter<Entity> getEntities() {
          org.spigotmc.AsyncCatcher.catchOp("Chunk getEntities call"); // Spigot
@@ -27471,7 +27433,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      public void startTickingChunk(LevelChunk chunk) {
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+@@ -2151,32 +2454,45 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
      @Override
      public void close() throws IOException {
          super.close();
@@ -27481,10 +27443,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      @Override
      public String gatherChunkSourceStats() {
-         String s = this.chunkSource.gatherStats();
- 
--        return "Chunks[S] W: " + s + " E: " + this.entityManager.gatherStats();
-+        return "Chunks[S] W: " + s + " E: " + this.moonrise$getEntityLookup().getDebugInfo(); // Paper - rewrite chunk system
+-        return "Chunks[S] W: " + this.chunkSource.gatherStats() + " E: " + this.entityManager.gatherStats();
++        return "Chunks[S] W: " + this.chunkSource.gatherStats() + " E: " + this.moonrise$getEntityLookup().getDebugInfo(); // Paper - rewrite chunk system
      }
  
      public boolean areEntitiesLoaded(long chunkPos) {
@@ -27517,38 +27477,38 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - rewrite chunk system
      }
  
-     public boolean isNaturalSpawningAllowed(ChunkPos pos) {
--        return this.entityManager.canPositionTick(pos);
+     public boolean isNaturalSpawningAllowed(ChunkPos chunkPos) {
+-        return this.entityManager.canPositionTick(chunkPos);
 +        // Paper start - rewrite chunk system
-+        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(pos));
++        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkPos));
 +        return chunkHolder != null && chunkHolder.isEntityTickingReady();
 +        // Paper end - rewrite chunk system
      }
  
      @Override
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
-         CrashReportCategory crashreportsystemdetails = super.fillReportDetails(report);
- 
-         crashreportsystemdetails.setDetail("Loaded entity count", () -> {
--            return String.valueOf(this.entityManager.count());
-+            return String.valueOf(this.moonrise$getEntityLookup().getEntityCount()); // Paper - rewrite chunk system
-         });
-         return crashreportsystemdetails;
+@@ -2230,7 +2546,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+     @Override
+     public CrashReportCategory fillReportDetails(CrashReport report) {
+         CrashReportCategory crashReportCategory = super.fillReportDetails(report);
+-        crashReportCategory.setDetail("Loaded entity count", () -> String.valueOf(this.entityManager.count()));
++        crashReportCategory.setDetail("Loaded entity count", () -> String.valueOf(this.moonrise$getEntityLookup().getEntityCount())); // Paper - rewrite chunk system
+         return crashReportCategory;
      }
-diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
-+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
-@@ -0,0 +0,0 @@ import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
- import org.bukkit.inventory.MainHand;
- // CraftBukkit end
  
--public class ServerPlayer extends net.minecraft.world.entity.player.Player {
-+public class ServerPlayer extends net.minecraft.world.entity.player.Player implements ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer { // Paper - rewrite chunk system
+diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java
+index ff5889f8fed0707a6654d9d21862e32e2ebc866d..e61fe83479f095e8addbd3e8f1d5179c998ae1eb 100644
+--- a/net/minecraft/server/level/ServerPlayer.java
++++ b/net/minecraft/server/level/ServerPlayer.java
+@@ -178,7 +178,7 @@ import net.minecraft.world.scores.Team;
+ import net.minecraft.world.scores.criteria.ObjectiveCriteria;
+ import org.slf4j.Logger;
  
+-public class ServerPlayer extends Player {
++public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer { // Paper - rewrite chunk system
      private static final Logger LOGGER = LogUtils.getLogger();
      private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_XZ = 32;
-@@ -0,0 +0,0 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
+     private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_Y = 10;
+@@ -388,6 +388,36 @@ public class ServerPlayer extends Player {
      public @Nullable String clientBrandName = null; // Paper - Brand support
      public org.bukkit.event.player.PlayerQuitEvent.QuitReason quitReason = null; // Paper - Add API for quit reason; there are a lot of changes to do if we change all methods leading to the event
  
@@ -27582,14 +27542,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - rewrite chunk system
 +
-     public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile, ClientInformation clientOptions) {
-         super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile);
-         this.chatVisibility = ChatVisiblity.FULL;
-diff --git a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
-+++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
-@@ -0,0 +0,0 @@ import net.minecraft.world.level.chunk.LightChunkGetter;
+     public ServerPlayer(MinecraftServer server, ServerLevel level, GameProfile gameProfile, ClientInformation clientInformation) {
+         super(level, level.getSharedSpawnPos(), level.getSharedSpawnAngle(), gameProfile);
+         this.textFilter = server.createTextFilterForPlayer(this);
+diff --git a/net/minecraft/server/level/ThreadedLevelLightEngine.java b/net/minecraft/server/level/ThreadedLevelLightEngine.java
+index 11a264ef2f43c2b00741397c9c9ea5393afad6ab..5c9ac44a3b4bc8e047feaf61a94eb163761498a2 100644
+--- a/net/minecraft/server/level/ThreadedLevelLightEngine.java
++++ b/net/minecraft/server/level/ThreadedLevelLightEngine.java
+@@ -22,23 +22,134 @@ import net.minecraft.world.level.chunk.LightChunkGetter;
  import net.minecraft.world.level.lighting.LevelLightEngine;
  import org.slf4j.Logger;
  
@@ -27721,17 +27681,17 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    // Paper end - rewrite chunk system
  
      public ThreadedLevelLightEngine(
-         LightChunkGetter chunkProvider, ChunkMap chunkLoadingManager, boolean hasBlockLight, ConsecutiveExecutor processor, ChunkTaskDispatcher executor
+         LightChunkGetter lightChunkGetter, ChunkMap chunkMap, boolean skyLight, ConsecutiveExecutor consecutiveExecutor, ChunkTaskDispatcher taskDispatcher
      ) {
-         super(chunkProvider, true, hasBlockLight);
-         this.chunkMap = chunkLoadingManager;
--        this.taskDispatcher = executor;
--        this.consecutiveExecutor = processor;
-+        // Paper - rewrite chunk sytem
+         super(lightChunkGetter, true, skyLight);
+         this.chunkMap = chunkMap;
+-        this.taskDispatcher = taskDispatcher;
+-        this.consecutiveExecutor = consecutiveExecutor;
++        // Paper - rewrite chunk system
      }
  
      @Override
-@@ -0,0 +0,0 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
+@@ -52,163 +163,73 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
  
      @Override
      public void checkBlock(BlockPos pos) {
@@ -27750,35 +27710,35 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - rewrite chunk system
      }
  
-     protected void updateChunkStatus(ChunkPos pos) {
--        this.addTask(pos.x, pos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
--            super.retainData(pos, false);
--            super.setLightEnabled(pos, false);
+     protected void updateChunkStatus(ChunkPos chunkPos) {
+-        this.addTask(chunkPos.x, chunkPos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
+-            super.retainData(chunkPos, false);
+-            super.setLightEnabled(chunkPos, false);
 -
--            for (int i = this.getMinLightSection(); i < this.getMaxLightSection(); i++) {
--                super.queueSectionData(LightLayer.BLOCK, SectionPos.of(pos, i), null);
--                super.queueSectionData(LightLayer.SKY, SectionPos.of(pos, i), null);
+-            for (int lightSection = this.getMinLightSection(); lightSection < this.getMaxLightSection(); lightSection++) {
+-                super.queueSectionData(LightLayer.BLOCK, SectionPos.of(chunkPos, lightSection), null);
+-                super.queueSectionData(LightLayer.SKY, SectionPos.of(chunkPos, lightSection), null);
 -            }
 -
--            for (int j = this.levelHeightAccessor.getMinSectionY(); j <= this.levelHeightAccessor.getMaxSectionY(); j++) {
--                super.updateSectionStatus(SectionPos.of(pos, j), true);
+-            for (int lightSection = this.levelHeightAccessor.getMinSectionY(); lightSection <= this.levelHeightAccessor.getMaxSectionY(); lightSection++) {
+-                super.updateSectionStatus(SectionPos.of(chunkPos, lightSection), true);
 -            }
--        }, () -> "updateChunkStatus " + pos + " true"));
+-        }, () -> "updateChunkStatus " + chunkPos + " true"));
 +        // Paper - rewrite chunk system
      }
  
      @Override
-     public void updateSectionStatus(SectionPos pos, boolean notReady) {
+     public void updateSectionStatus(SectionPos pos, boolean isEmpty) {
 -        this.addTask(
 -            pos.x(),
 -            pos.z(),
 -            () -> 0,
 -            ThreadedLevelLightEngine.TaskType.PRE_UPDATE,
--            Util.name(() -> super.updateSectionStatus(pos, notReady), () -> "updateSectionStatus " + pos + " " + notReady)
+-            Util.name(() -> super.updateSectionStatus(pos, isEmpty), () -> "updateSectionStatus " + pos + " " + isEmpty)
 -        );
 +        // Paper start - rewrite chunk system
 +        this.queueTaskForSection(pos.getX(), pos.getY(), pos.getZ(), () -> {
-+            return ThreadedLevelLightEngine.this.starlight$getLightEngine().sectionChange(pos, notReady);
++            return ThreadedLevelLightEngine.this.starlight$getLightEngine().sectionChange(pos, isEmpty);
 +        });
 +        // Paper end - rewrite chunk system
      }
@@ -27795,84 +27755,84 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      @Override
-     public void setLightEnabled(ChunkPos pos, boolean retainData) {
+     public void setLightEnabled(ChunkPos chunkPos, boolean lightEnabled) {
 -        this.addTask(
--            pos.x,
--            pos.z,
+-            chunkPos.x,
+-            chunkPos.z,
 -            ThreadedLevelLightEngine.TaskType.PRE_UPDATE,
--            Util.name(() -> super.setLightEnabled(pos, retainData), () -> "enableLight " + pos + " " + retainData)
+-            Util.name(() -> super.setLightEnabled(chunkPos, lightEnabled), () -> "enableLight " + chunkPos + " " + lightEnabled)
 -        );
 +        // Paper start - rewrite chunk system
      }
  
      @Override
-     public void queueSectionData(LightLayer lightType, SectionPos pos, @Nullable DataLayer nibbles) {
+     public void queueSectionData(LightLayer lightLayer, SectionPos sectionPos, @Nullable DataLayer dataLayer) {
 -        this.addTask(
--            pos.x(),
--            pos.z(),
+-            sectionPos.x(),
+-            sectionPos.z(),
 -            () -> 0,
 -            ThreadedLevelLightEngine.TaskType.PRE_UPDATE,
--            Util.name(() -> super.queueSectionData(lightType, pos, nibbles), () -> "queueData " + pos)
+-            Util.name(() -> super.queueSectionData(lightLayer, sectionPos, dataLayer), () -> "queueData " + sectionPos)
 -        );
 +        // Paper start - rewrite chunk system
      }
  
-     private void addTask(int x, int z, ThreadedLevelLightEngine.TaskType stage, Runnable task) {
--        this.addTask(x, z, this.chunkMap.getChunkQueueLevel(ChunkPos.asLong(x, z)), stage, task);
+     private void addTask(int chunkX, int chunkZ, ThreadedLevelLightEngine.TaskType type, Runnable task) {
+-        this.addTask(chunkX, chunkZ, this.chunkMap.getChunkQueueLevel(ChunkPos.asLong(chunkX, chunkZ)), type, task);
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
-     private void addTask(int x, int z, IntSupplier completedLevelSupplier, ThreadedLevelLightEngine.TaskType stage, Runnable task) {
+     private void addTask(int chunkX, int chunkZ, IntSupplier queueLevelSupplier, ThreadedLevelLightEngine.TaskType type, Runnable task) {
 -        this.taskDispatcher.submit(() -> {
--            this.lightTasks.add(Pair.of(stage, task));
+-            this.lightTasks.add(Pair.of(type, task));
 -            if (this.lightTasks.size() >= 1000) {
 -                this.runUpdate();
 -            }
--        }, ChunkPos.asLong(x, z), completedLevelSupplier);
+-        }, ChunkPos.asLong(chunkX, chunkZ), queueLevelSupplier);
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
      @Override
-     public void retainData(ChunkPos pos, boolean retainData) {
+     public void retainData(ChunkPos pos, boolean retain) {
 -        this.addTask(
--            pos.x, pos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> super.retainData(pos, retainData), () -> "retainData " + pos)
+-            pos.x, pos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> super.retainData(pos, retain), () -> "retainData " + pos)
 -        );
 +        // Paper start - rewrite chunk system
      }
  
-     public CompletableFuture<ChunkAccess> initializeLight(ChunkAccess chunk, boolean bl) {
--        ChunkPos chunkPos = chunk.getPos();
--        this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
--            LevelChunkSection[] levelChunkSections = chunk.getSections();
+     public CompletableFuture<ChunkAccess> initializeLight(ChunkAccess chunk, boolean lightEnabled) {
+-        ChunkPos pos = chunk.getPos();
+-        this.addTask(pos.x, pos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
+-            LevelChunkSection[] sections = chunk.getSections();
 -
 -            for (int i = 0; i < chunk.getSectionsCount(); i++) {
--                LevelChunkSection levelChunkSection = levelChunkSections[i];
+-                LevelChunkSection levelChunkSection = sections[i];
 -                if (!levelChunkSection.hasOnlyAir()) {
--                    int j = this.levelHeightAccessor.getSectionYFromSectionIndex(i);
--                    super.updateSectionStatus(SectionPos.of(chunkPos, j), false);
+-                    int sectionYFromSectionIndex = this.levelHeightAccessor.getSectionYFromSectionIndex(i);
+-                    super.updateSectionStatus(SectionPos.of(pos, sectionYFromSectionIndex), false);
 -                }
 -            }
--        }, () -> "initializeLight: " + chunkPos));
+-        }, () -> "initializeLight: " + pos));
 -        return CompletableFuture.supplyAsync(() -> {
--            super.setLightEnabled(chunkPos, bl);
--            super.retainData(chunkPos, false);
+-            super.setLightEnabled(pos, lightEnabled);
+-            super.retainData(pos, false);
 -            return chunk;
--        }, task -> this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, task));
+-        }, task -> this.addTask(pos.x, pos.z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, task));
 +        return CompletableFuture.completedFuture(chunk); // Paper start - rewrite chunk system
      }
  
-     public CompletableFuture<ChunkAccess> lightChunk(ChunkAccess chunk, boolean excludeBlocks) {
--        ChunkPos chunkPos = chunk.getPos();
+     public CompletableFuture<ChunkAccess> lightChunk(ChunkAccess chunk, boolean isLighted) {
+-        ChunkPos pos = chunk.getPos();
 -        chunk.setLightCorrect(false);
--        this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
--            if (!excludeBlocks) {
--                super.propagateLightSources(chunkPos);
+-        this.addTask(pos.x, pos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
+-            if (!isLighted) {
+-                super.propagateLightSources(pos);
 -            }
--        }, () -> "lightChunk " + chunkPos + " " + excludeBlocks));
+-        }, () -> "lightChunk " + pos + " " + isLighted));
 -        return CompletableFuture.supplyAsync(() -> {
 -            chunk.setLightCorrect(true);
 -            return chunk;
--        }, task -> this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, task));
+-        }, task -> this.addTask(pos.x, pos.z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, task));
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
@@ -27887,24 +27847,24 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      private void runUpdate() {
--        int i = Math.min(this.lightTasks.size(), 1000);
+-        int min = Math.min(this.lightTasks.size(), 1000);
 -        ObjectListIterator<Pair<ThreadedLevelLightEngine.TaskType, Runnable>> objectListIterator = this.lightTasks.iterator();
 -
--        int j;
--        for (j = 0; objectListIterator.hasNext() && j < i; j++) {
+-        int i;
+-        for (i = 0; objectListIterator.hasNext() && i < min; i++) {
 -            Pair<ThreadedLevelLightEngine.TaskType, Runnable> pair = objectListIterator.next();
 -            if (pair.getFirst() == ThreadedLevelLightEngine.TaskType.PRE_UPDATE) {
 -                pair.getSecond().run();
 -            }
 -        }
 -
--        objectListIterator.back(j);
+-        objectListIterator.back(i);
 -        super.runLightUpdates();
 -
--        for (int var5 = 0; objectListIterator.hasNext() && var5 < i; var5++) {
--            Pair<ThreadedLevelLightEngine.TaskType, Runnable> pair2 = objectListIterator.next();
--            if (pair2.getFirst() == ThreadedLevelLightEngine.TaskType.POST_UPDATE) {
--                pair2.getSecond().run();
+-        for (int var5 = 0; objectListIterator.hasNext() && var5 < min; var5++) {
+-            Pair<ThreadedLevelLightEngine.TaskType, Runnable> pair = objectListIterator.next();
+-            if (pair.getFirst() == ThreadedLevelLightEngine.TaskType.POST_UPDATE) {
+-                pair.getSecond().run();
 -            }
 -
 -            objectListIterator.remove();
@@ -27913,17 +27873,16 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      public CompletableFuture<?> waitForPendingTasks(int x, int z) {
--        return CompletableFuture.runAsync(() -> {
--        }, callback -> this.addTask(x, z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, callback));
+-        return CompletableFuture.runAsync(() -> {}, task -> this.addTask(x, z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, task));
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
      static enum TaskType {
-diff --git a/src/main/java/net/minecraft/server/level/Ticket.java b/src/main/java/net/minecraft/server/level/Ticket.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/Ticket.java
-+++ b/src/main/java/net/minecraft/server/level/Ticket.java
-@@ -0,0 +0,0 @@ package net.minecraft.server.level;
+diff --git a/net/minecraft/server/level/Ticket.java b/net/minecraft/server/level/Ticket.java
+index e574f217386d677400b4c093d50261045df06d5c..ed8a3d5bd25909ee4648b1ec2ee66878198a1d8a 100644
+--- a/net/minecraft/server/level/Ticket.java
++++ b/net/minecraft/server/level/Ticket.java
+@@ -2,13 +2,25 @@ package net.minecraft.server.level;
  
  import java.util.Objects;
  
@@ -27936,7 +27895,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    // Paper start - rewrite chunk system
 +    private long removeDelay;
  
--    protected Ticket(TicketType<T> type, int level, T argument) {
+-    protected Ticket(TicketType<T> type, int ticketLevel, T key) {
 +    @Override
 +    public final long moonrise$getRemoveDelay() {
 +        return this.removeDelay;
@@ -27946,13 +27905,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public final void moonrise$setRemoveDelay(final long removeDelay) {
 +        this.removeDelay = removeDelay;
 +    }
-+    // Paper end - rewerite chunk system
++    // Paper end - rewrite chunk system
 +
-+    public Ticket(TicketType<T> type, int level, T argument) { // Paper - public
++    public Ticket(TicketType<T> type, int ticketLevel, T key) { // Paper - public
          this.type = type;
-         this.ticketLevel = level;
-         this.key = argument;
-@@ -0,0 +0,0 @@ public final class Ticket<T> implements Comparable<Ticket<?>> {
+         this.ticketLevel = ticketLevel;
+         this.key = key;
+@@ -41,7 +53,7 @@ public final class Ticket<T> implements Comparable<Ticket<?>> {
  
      @Override
      public String toString() {
@@ -27961,25 +27920,25 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      public TicketType<T> getType() {
-@@ -0,0 +0,0 @@ public final class Ticket<T> implements Comparable<Ticket<?>> {
+@@ -53,11 +65,10 @@ public final class Ticket<T> implements Comparable<Ticket<?>> {
      }
  
-     protected void setCreatedTick(long tickCreated) {
--        this.createdTick = tickCreated;
+     protected void setCreatedTick(long timestamp) {
+-        this.createdTick = timestamp;
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
-     protected boolean timedOut(long currentTick) {
--        long l = this.type.timeout();
--        return l != 0L && currentTick - this.createdTick > l;
+     protected boolean timedOut(long currentTime) {
+-        long timeout = this.type.timeout();
+-        return timeout != 0L && currentTime - this.createdTick > timeout;
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  }
-diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java
-+++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java
-@@ -0,0 +0,0 @@ public class WorldGenRegion implements WorldGenLevel {
+diff --git a/net/minecraft/server/level/WorldGenRegion.java b/net/minecraft/server/level/WorldGenRegion.java
+index 4eb040006f5d41b47e5ac9df5d9f19c4315d6343..7fa41dea184b01891f45d8e404bc1cba19cf1bcf 100644
+--- a/net/minecraft/server/level/WorldGenRegion.java
++++ b/net/minecraft/server/level/WorldGenRegion.java
+@@ -78,6 +78,36 @@ public class WorldGenRegion implements WorldGenLevel {
      private final AtomicLong subTickCount = new AtomicLong();
      private static final ResourceLocation WORLDGEN_REGION_RANDOM = ResourceLocation.withDefaultNamespace("worldgen_region_random");
  
@@ -28013,36 +27972,36 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - rewrite chunk system
 +
-     public WorldGenRegion(ServerLevel world, StaticCache2D<GenerationChunkHolder> chunks, ChunkStep generationStep, ChunkAccess centerPos) {
-         this.generatingStep = generationStep;
-         this.cache = chunks;
-diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/players/PlayerList.java
-+++ b/src/main/java/net/minecraft/server/players/PlayerList.java
-@@ -0,0 +0,0 @@ public abstract class PlayerList {
+     public WorldGenRegion(ServerLevel level, StaticCache2D<GenerationChunkHolder> cache, ChunkStep generatingStep, ChunkAccess center) {
+         this.generatingStep = generatingStep;
+         this.cache = cache;
+diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java
+index ff0315cffdb282fdc0a1ffd15e2954caa76835c9..5e94dd9e26aa4fd6545dbaae2ae0cb51cb6f13e0 100644
+--- a/net/minecraft/server/players/PlayerList.java
++++ b/net/minecraft/server/players/PlayerList.java
+@@ -1312,7 +1312,7 @@ public abstract class PlayerList {
  
      public void setViewDistance(int viewDistance) {
          this.viewDistance = viewDistance;
 -        this.broadcastAll(new ClientboundSetChunkCacheRadiusPacket(viewDistance));
 +        //this.broadcastAll(new ClientboundSetChunkCacheRadiusPacket(viewDistance)); // Paper - rewrite chunk system
-         Iterator iterator = this.server.getAllLevels().iterator();
  
-         while (iterator.hasNext()) {
-@@ -0,0 +0,0 @@ public abstract class PlayerList {
+         for (ServerLevel serverLevel : this.server.getAllLevels()) {
+             if (serverLevel != null) {
+@@ -1323,7 +1323,7 @@ public abstract class PlayerList {
  
      public void setSimulationDistance(int simulationDistance) {
          this.simulationDistance = simulationDistance;
 -        this.broadcastAll(new ClientboundSetSimulationDistancePacket(simulationDistance));
-+        //this.broadcastAll(new ClientboundSetSimulationDistancePacket(simulationDistance)); // Paper - rewrite chunk system
-         Iterator iterator = this.server.getAllLevels().iterator();
++        //this.broadcastAll(new ClientboundSetSimulationDistancePacket(simulationDistance));  // Paper - rewrite chunk system
  
-         while (iterator.hasNext()) {
-diff --git a/src/main/java/net/minecraft/util/BitStorage.java b/src/main/java/net/minecraft/util/BitStorage.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/util/BitStorage.java
-+++ b/src/main/java/net/minecraft/util/BitStorage.java
-@@ -0,0 +0,0 @@ package net.minecraft.util;
+         for (ServerLevel serverLevel : this.server.getAllLevels()) {
+             if (serverLevel != null) {
+diff --git a/net/minecraft/util/BitStorage.java b/net/minecraft/util/BitStorage.java
+index 32fe9b22e1d3a422dd80c64d61156dbc7241ba20..02502d50f0255f5bbcc0ecb965abb48cc1a112da 100644
+--- a/net/minecraft/util/BitStorage.java
++++ b/net/minecraft/util/BitStorage.java
+@@ -2,7 +2,7 @@ package net.minecraft.util;
  
  import java.util.function.IntConsumer;
  
@@ -28051,8 +28010,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      int getAndSet(int index, int value);
  
      void set(int index, int value);
-@@ -0,0 +0,0 @@ public interface BitStorage {
-     void unpack(int[] out);
+@@ -20,4 +20,22 @@ public interface BitStorage {
+     void unpack(int[] array);
  
      BitStorage copy();
 +
@@ -28074,11 +28033,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - block counting
  }
-diff --git a/src/main/java/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java b/src/main/java/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java
-+++ b/src/main/java/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java
-@@ -0,0 +0,0 @@ import java.util.Iterator;
+diff --git a/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java b/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java
+index 4a7c83c56dfbff59af71c3cd2fa4205c9a22bdc7..f28fbf81a417a678726d3f77b3999054676d522e 100644
+--- a/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java
++++ b/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java
+@@ -7,7 +7,7 @@ import java.util.Iterator;
  import javax.annotation.Nullable;
  import net.minecraft.core.IdMap;
  
@@ -28087,7 +28046,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      private static final int NOT_FOUND = -1;
      private static final Object EMPTY_SLOT = null;
      private static final float LOADFACTOR = 0.8F;
-@@ -0,0 +0,0 @@ public class CrudeIncrementalIntIdentityHashBiMap<K> implements IdMap<K> {
+@@ -17,6 +17,16 @@ public class CrudeIncrementalIntIdentityHashBiMap<K> implements IdMap<K> {
      private int nextId;
      private int size;
  
@@ -28104,7 +28063,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      private CrudeIncrementalIntIdentityHashBiMap(int size) {
          this.keys = (K[])(new Object[size]);
          this.values = new int[size];
-@@ -0,0 +0,0 @@ public class CrudeIncrementalIntIdentityHashBiMap<K> implements IdMap<K> {
+@@ -88,6 +98,12 @@ public class CrudeIncrementalIntIdentityHashBiMap<K> implements IdMap<K> {
          this.byId = crudeIncrementalIntIdentityHashBiMap.byId;
          this.nextId = crudeIncrementalIntIdentityHashBiMap.nextId;
          this.size = crudeIncrementalIntIdentityHashBiMap.size;
@@ -28116,12 +28075,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - optimise palette reads
      }
  
-     public void addMapping(K value, int id) {
-diff --git a/src/main/java/net/minecraft/util/SimpleBitStorage.java b/src/main/java/net/minecraft/util/SimpleBitStorage.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/util/SimpleBitStorage.java
-+++ b/src/main/java/net/minecraft/util/SimpleBitStorage.java
-@@ -0,0 +0,0 @@ public class SimpleBitStorage implements BitStorage {
+     public void addMapping(K object, int intKey) {
+diff --git a/net/minecraft/util/SimpleBitStorage.java b/net/minecraft/util/SimpleBitStorage.java
+index 6fb3a3f167d8cbaa78135af0c180b592661e2c1d..e6306a68c8652d4c5d22d5ecb1416f5f931f76ee 100644
+--- a/net/minecraft/util/SimpleBitStorage.java
++++ b/net/minecraft/util/SimpleBitStorage.java
+@@ -208,6 +208,20 @@ public class SimpleBitStorage implements BitStorage {
      private final int divideAdd; private final long divideAddUnsigned; // Paper - Perf: Optimize SimpleBitStorage
      private final int divideShift;
  
@@ -28139,12 +28098,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    private final int mulBits;
 +    // Paper end - optimise bitstorage read/write operations
 +
-     public SimpleBitStorage(int elementBits, int size, int[] data) {
-         this(elementBits, size);
+     public SimpleBitStorage(int bits, int size, int[] data) {
+         this(bits, size);
          int i = 0;
-@@ -0,0 +0,0 @@ public class SimpleBitStorage implements BitStorage {
+@@ -261,6 +275,13 @@ public class SimpleBitStorage implements BitStorage {
          } else {
-             this.data = new long[j];
+             this.data = new long[i1];
          }
 +        // Paper start - optimise bitstorage read/write operations
 +        this.magic = BETTER_MAGIC[this.bits];
@@ -28156,16 +28115,16 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      private int cellIndex(int index) {
-@@ -0,0 +0,0 @@ public class SimpleBitStorage implements BitStorage {
+@@ -269,28 +290,51 @@ public class SimpleBitStorage implements BitStorage {
+ 
+     @Override
      public final int getAndSet(int index, int value) { // Paper - Perf: Optimize SimpleBitStorage
-         //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage
-         //Validate.inclusiveBetween(0L, this.mask, (long)value); // Paper - Perf: Optimize SimpleBitStorage
 -        int i = this.cellIndex(index);
 -        long l = this.data[i];
--        int j = (index - i * this.valuesPerLong) * this.bits;
--        int k = (int)(l >> j & this.mask);
--        this.data[i] = l & ~(this.mask << j) | ((long)value & this.mask) << j;
--        return k;
+-        int i1 = (index - i * this.valuesPerLong) * this.bits;
+-        int i2 = (int)(l >> i1 & this.mask);
+-        this.data[i] = l & ~(this.mask << i1) | (value & this.mask) << i1;
+-        return i2;
 +        // Paper start - optimise bitstorage read/write operations
 +        final int full = this.magic * index; // 20 bits of magic + 12 bits of index = barely int
 +        final int divQ = full >>> 20;
@@ -28186,12 +28145,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      @Override
      public final void set(int index, int value) { // Paper - Perf: Optimize SimpleBitStorage
-         //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage
-         //Validate.inclusiveBetween(0L, this.mask, (long)value); // Paper - Perf: Optimize SimpleBitStorage
 -        int i = this.cellIndex(index);
 -        long l = this.data[i];
--        int j = (index - i * this.valuesPerLong) * this.bits;
--        this.data[i] = l & ~(this.mask << j) | ((long)value & this.mask) << j;
+-        int i1 = (index - i * this.valuesPerLong) * this.bits;
+-        this.data[i] = l & ~(this.mask << i1) | (value & this.mask) << i1;
 +        // Paper start - optimise bitstorage read/write operations
 +        final int full = this.magic * index; // 20 bits of magic + 12 bits of index = barely int
 +        final int divQ = full >>> 20;
@@ -28210,11 +28167,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      @Override
      public final int get(int index) { // Paper - Perf: Optimize SimpleBitStorage
-         //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage
 -        int i = this.cellIndex(index);
 -        long l = this.data[i];
--        int j = (index - i * this.valuesPerLong) * this.bits;
--        return (int)(l >> j & this.mask);
+-        int i1 = (index - i * this.valuesPerLong) * this.bits;
+-        return (int)(l >> i1 & this.mask);
 +        // Paper start - optimise bitstorage read/write operations
 +        final int full = this.magic * index; // 20 bits of magic + 12 bits of index = barely int
 +        final int divQ = full >>> 20;
@@ -28225,7 +28181,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      @Override
-@@ -0,0 +0,0 @@ public class SimpleBitStorage implements BitStorage {
+@@ -355,6 +399,67 @@ public class SimpleBitStorage implements BitStorage {
          return new SimpleBitStorage(this.bits, this.size, (long[])this.data.clone());
      }
  
@@ -28293,11 +28249,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      public static class InitializationException extends RuntimeException {
          InitializationException(String message) {
              super(message);
-diff --git a/src/main/java/net/minecraft/util/SortedArraySet.java b/src/main/java/net/minecraft/util/SortedArraySet.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/util/SortedArraySet.java
-+++ b/src/main/java/net/minecraft/util/SortedArraySet.java
-@@ -0,0 +0,0 @@ import java.util.Iterator;
+diff --git a/net/minecraft/util/SortedArraySet.java b/net/minecraft/util/SortedArraySet.java
+index 2c6b35b86eed9002016b8228c3195f8033d219ca..339b19e88567be382e550ed54477fabd58d51faa 100644
+--- a/net/minecraft/util/SortedArraySet.java
++++ b/net/minecraft/util/SortedArraySet.java
+@@ -8,12 +8,89 @@ import java.util.Iterator;
  import java.util.NoSuchElementException;
  import javax.annotation.Nullable;
  
@@ -28388,11 +28344,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      private SortedArraySet(int initialCapacity, Comparator<T> comparator) {
          this.comparator = comparator;
          if (initialCapacity < 0) {
-diff --git a/src/main/java/net/minecraft/util/ZeroBitStorage.java b/src/main/java/net/minecraft/util/ZeroBitStorage.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/util/ZeroBitStorage.java
-+++ b/src/main/java/net/minecraft/util/ZeroBitStorage.java
-@@ -0,0 +0,0 @@ public class ZeroBitStorage implements BitStorage {
+diff --git a/net/minecraft/util/ZeroBitStorage.java b/net/minecraft/util/ZeroBitStorage.java
+index 8cc5c0716392ba06501542ff5cbe71ee43979e5d..09fd99c9cbd23b5f3c899bfb00c9b89651948ed8 100644
+--- a/net/minecraft/util/ZeroBitStorage.java
++++ b/net/minecraft/util/ZeroBitStorage.java
+@@ -62,4 +62,22 @@ public class ZeroBitStorage implements BitStorage {
      public BitStorage copy() {
          return this;
      }
@@ -28415,20 +28371,20 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - block counting
  }
-diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/entity/Entity.java
-+++ b/src/main/java/net/minecraft/world/entity/Entity.java
-@@ -0,0 +0,0 @@ import org.bukkit.event.player.PlayerTeleportEvent;
- import org.bukkit.plugin.PluginManager;
- // CraftBukkit end
+diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java
+index 45f69a914d5a0565196c4105d61541047301470f..f42bdae2f80805e5069212bd16e3ab6814aef9ee 100644
+--- a/net/minecraft/world/entity/Entity.java
++++ b/net/minecraft/world/entity/Entity.java
+@@ -135,7 +135,7 @@ import net.minecraft.world.scores.ScoreHolder;
+ import net.minecraft.world.scores.Team;
+ import org.slf4j.Logger;
  
 -public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess, ScoreHolder {
 +public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess, ScoreHolder, ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity, ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity {  // Paper - rewrite chunk system // Paper - optimise entity tracker
  
      // CraftBukkit start
      private static final int CURRENT_LEVEL = 2;
-@@ -0,0 +0,0 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+@@ -146,7 +146,17 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
  
      // Paper start - Share random for entities to make them more random
      public static RandomSource SHARED_RANDOM = new RandomRandomSource();
@@ -28447,7 +28403,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          private boolean locked = false;
  
          @Override
-@@ -0,0 +0,0 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+@@ -159,61 +169,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
              }
          }
  
@@ -28510,7 +28466,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
      // Paper end - Share random for entities to make them more random
      public org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason spawnReason; // Paper - Entity#getEntitySpawnReason
-@@ -0,0 +0,0 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+@@ -419,6 +375,156 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
          return this.dimensions.makeBoundingBox(x, y, z);
      }
      // Paper end
@@ -28665,45 +28621,49 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - optimise entity tracker
  
-     public Entity(EntityType<?> type, Level world) {
-         this.id = Entity.ENTITY_COUNTER.incrementAndGet();
-@@ -0,0 +0,0 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+     public Entity(EntityType<?> entityType, Level level) {
+         this.type = entityType;
+@@ -1327,35 +1433,77 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+         return distance;
      }
  
-     private Vec3 collide(Vec3 movement) {
--        AABB axisalignedbb = this.getBoundingBox();
--        List<VoxelShape> list = this.level().getEntityCollisions(this, axisalignedbb.expandTowards(movement));
--        Vec3 vec3d1 = movement.lengthSqr() == 0.0D ? movement : Entity.collideBoundingBox(this, movement, axisalignedbb, this.level(), list);
--        boolean flag = movement.x != vec3d1.x;
--        boolean flag1 = movement.y != vec3d1.y;
--        boolean flag2 = movement.z != vec3d1.z;
--        boolean flag3 = flag1 && movement.y < 0.0D;
--
+-    private Vec3 collide(Vec3 vec) {
+-        AABB boundingBox = this.getBoundingBox();
+-        List<VoxelShape> entityCollisions = this.level().getEntityCollisions(this, boundingBox.expandTowards(vec));
+-        Vec3 vec3 = vec.lengthSqr() == 0.0 ? vec : collideBoundingBox(this, vec, boundingBox, this.level(), entityCollisions);
+-        boolean flag = vec.x != vec3.x;
+-        boolean flag1 = vec.y != vec3.y;
+-        boolean flag2 = vec.z != vec3.z;
+-        boolean flag3 = flag1 && vec.y < 0.0;
 -        if (this.maxUpStep() > 0.0F && (flag3 || this.onGround()) && (flag || flag2)) {
--            AABB axisalignedbb1 = flag3 ? axisalignedbb.move(0.0D, vec3d1.y, 0.0D) : axisalignedbb;
--            AABB axisalignedbb2 = axisalignedbb1.expandTowards(movement.x, (double) this.maxUpStep(), movement.z);
--
+-            AABB aabb = flag3 ? boundingBox.move(0.0, vec3.y, 0.0) : boundingBox;
+-            AABB aabb1 = aabb.expandTowards(vec.x, this.maxUpStep(), vec.z);
 -            if (!flag3) {
--                axisalignedbb2 = axisalignedbb2.expandTowards(0.0D, -9.999999747378752E-6D, 0.0D);
+-                aabb1 = aabb1.expandTowards(0.0, -1.0E-5F, 0.0);
 -            }
-+        // Paper start - optimise collisions
++    // Paper start - optimise collisions
++    private Vec3 collide(Vec3 movement) {
 +        final boolean xZero = movement.x == 0.0;
 +        final boolean yZero = movement.y == 0.0;
 +        final boolean zZero = movement.z == 0.0;
 +        if (xZero & yZero & zZero) {
 +            return movement;
 +        }
-+
+ 
+-            List<VoxelShape> list = collectColliders(this, this.level, entityCollisions, aabb1);
+-            float f = (float)vec3.y;
+-            float[] floats = collectCandidateStepUpHeights(aabb, list, this.maxUpStep(), f);
 +        final AABB currentBox = this.getBoundingBox();
-+
+ 
+-            for (float f1 : floats) {
+-                Vec3 vec31 = collideWithShapes(new Vec3(vec.x, f1, vec.z), aabb, list);
+-                if (vec31.horizontalDistanceSqr() > vec3.horizontalDistanceSqr()) {
+-                    double d = boundingBox.minY - aabb.minY;
+-                    return vec31.add(0.0, -d, 0.0);
+-                }
 +        final List<VoxelShape> potentialCollisionsVoxel = new ArrayList<>();
 +        final List<AABB> potentialCollisionsBB = new ArrayList<>();
- 
--            List<VoxelShape> list1 = Entity.collectColliders(this, this.level, list, axisalignedbb2);
--            float f = (float) vec3d1.y;
--            float[] afloat = Entity.collectCandidateStepUpHeights(axisalignedbb1, list1, this.maxUpStep(), f);
--            float[] afloat1 = afloat;
--            int i = afloat.length;
++
 +        final AABB initialCollisionBox;
 +        if (xZero & zZero) {
 +            // note: xZero & zZero -> collision on x/z == 0 -> no step height calculation
@@ -28713,26 +28673,19 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        } else {
 +            initialCollisionBox = currentBox.expandTowards(movement);
 +        }
- 
--            for (int j = 0; j < i; ++j) {
--                float f1 = afloat1[j];
--                Vec3 vec3d2 = Entity.collideWithShapes(new Vec3(movement.x, (double) f1, movement.z), axisalignedbb1, list1);
++
 +        final List<AABB> entityAABBs = new ArrayList<>();
 +        ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getEntityHardCollisions(
 +            this.level, (Entity)(Object)this, initialCollisionBox, entityAABBs, 0, null
 +        );
- 
--                if (vec3d2.horizontalDistanceSqr() > vec3d1.horizontalDistanceSqr()) {
--                    double d0 = axisalignedbb.minY - axisalignedbb1.minY;
++
 +        ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisionsForBlocksOrWorldBorder(
 +            this.level, (Entity)(Object)this, initialCollisionBox, potentialCollisionsVoxel, potentialCollisionsBB,
 +            ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, null
 +        );
 +        potentialCollisionsBB.addAll(entityAABBs);
 +        final Vec3 collided = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(movement, currentBox, potentialCollisionsVoxel, potentialCollisionsBB);
- 
--                    return vec3d2.add(0.0D, -d0, 0.0D);
--                }
++
 +        final boolean collidedX = collided.x != movement.x;
 +        final boolean collidedY = collided.y != movement.y;
 +        final boolean collidedZ = collided.z != movement.z;
@@ -28766,13 +28719,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
              }
          }
  
--        return vec3d1;
+-        return vec3;
 +        return collided;
 +        // Paper end - optimise collisions
      }
  
-     private static float[] collectCandidateStepUpHeights(AABB collisionBox, List<VoxelShape> collisions, float f, float stepHeight) {
-@@ -0,0 +0,0 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+     private static float[] collectCandidateStepUpHeights(AABB box, List<VoxelShape> colliders, float deltaY, float maxUpStep) {
+@@ -2664,23 +2812,110 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
      }
  
      public boolean isInWall() {
@@ -28781,20 +28734,25 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
              return false;
 -        } else {
 -            float f = this.dimensions.width() * 0.8F;
--            AABB axisalignedbb = AABB.ofSize(this.getEyePosition(), (double) f, 1.0E-6D, (double) f);
+-            AABB aabb = AABB.ofSize(this.getEyePosition(), f, 1.0E-6, f);
+-            return BlockPos.betweenClosedStream(aabb)
+-                .anyMatch(
+-                    pos -> {
+-                        BlockState blockState = this.level().getBlockState(pos);
+-                        return !blockState.isAir()
+-                            && blockState.isSuffocating(this.level(), pos)
+-                            && Shapes.joinIsNotEmpty(
+-                                blockState.getCollisionShape(this.level(), pos).move(pos.getX(), pos.getY(), pos.getZ()), Shapes.create(aabb), BooleanOp.AND
+-                            );
 +        }
- 
--            return BlockPos.betweenClosedStream(axisalignedbb).anyMatch((blockposition) -> {
--                BlockState iblockdata = this.level().getBlockState(blockposition);
++
 +        final double reducedWith = (double)(this.dimensions.width() * 0.8F);
 +        final AABB boundingBox = AABB.ofSize(this.getEyePosition(), reducedWith, 1.0E-6D, reducedWith);
 +        final Level world = this.level;
- 
--                return !iblockdata.isAir() && iblockdata.isSuffocating(this.level(), blockposition) && Shapes.joinIsNotEmpty(iblockdata.getCollisionShape(this.level(), blockposition).move((double) blockposition.getX(), (double) blockposition.getY(), (double) blockposition.getZ()), Shapes.create(axisalignedbb), BooleanOp.AND);
--            });
++
 +        if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(boundingBox)) {
 +            return false;
-         }
++        }
 +
 +        final int minBlockX = Mth.floor(boundingBox.minX);
 +        final int minBlockY = Mth.floor(boundingBox.minY);
@@ -28824,7 +28782,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                    final int sectionIdx = currChunkY - minSection;
 +                    if (sectionIdx < 0 || sectionIdx >= sections.length) {
 +                        continue;
-+                    }
+                     }
+-                );
 +                    final net.minecraft.world.level.chunk.LevelChunkSection section = sections[sectionIdx];
 +                    if (section.hasOnlyAir()) {
 +                        // empty
@@ -28883,14 +28842,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                    }
 +                }
 +            }
-+        }
+         }
 +
 +        return false;
 +        // Paper end - optimise collisions
      }
  
      public InteractionResult interact(Player player, InteractionHand hand) {
-@@ -0,0 +0,0 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+@@ -4104,15 +4339,17 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
      }
  
      public Iterable<Entity> getIndirectPassengers() {
@@ -28907,55 +28866,53 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            return ret;
          }
 -        return indirectPassengers.build();
+-        // Paper end - Optimize indirect passenger iteration
 +
 +        collectIndirectPassengers(ret, this.passengers);
 +
 +        return ret;
 +        // Paper end - optimise entity tracker
      }
-     private Iterable<Entity> getIndirectPassengers_old() {
-         // Paper end - Optimize indirect passenger iteration
-@@ -0,0 +0,0 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
-         return Mth.lerp(delta, this.yRotO, this.yRot);
+ 
+     public int countPlayerPassengers() {
+@@ -4250,77 +4487,136 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+         return Mth.lerp(partialTick, this.yRotO, this.yRot);
      }
  
--    public boolean updateFluidHeightAndDoFluidPushing(TagKey<Fluid> tag, double speed) {
+-    public boolean updateFluidHeightAndDoFluidPushing(TagKey<Fluid> fluidTag, double motionScale) {
 +    // Paper start - optimise collisions
 +    public boolean updateFluidHeightAndDoFluidPushing(final TagKey<Fluid> fluid, final double flowScale) {
          if (this.touchingUnloadedChunk()) {
              return false;
 -        } else {
--            AABB axisalignedbb = this.getBoundingBox().deflate(0.001D);
--            int i = Mth.floor(axisalignedbb.minX);
--            int j = Mth.ceil(axisalignedbb.maxX);
--            int k = Mth.floor(axisalignedbb.minY);
--            int l = Mth.ceil(axisalignedbb.maxY);
--            int i1 = Mth.floor(axisalignedbb.minZ);
--            int j1 = Mth.ceil(axisalignedbb.maxZ);
--            double d1 = 0.0D;
--            boolean flag = this.isPushedByFluid();
--            boolean flag1 = false;
--            Vec3 vec3d = Vec3.ZERO;
--            int k1 = 0;
--            BlockPos.MutableBlockPos blockposition_mutableblockposition = new BlockPos.MutableBlockPos();
+-            AABB aabb = this.getBoundingBox().deflate(0.001);
+-            int floor = Mth.floor(aabb.minX);
+-            int ceil = Mth.ceil(aabb.maxX);
+-            int floor1 = Mth.floor(aabb.minY);
+-            int ceil1 = Mth.ceil(aabb.maxY);
+-            int floor2 = Mth.floor(aabb.minZ);
+-            int ceil2 = Mth.ceil(aabb.maxZ);
+-            double d = 0.0;
+-            boolean isPushedByFluid = this.isPushedByFluid();
+-            boolean flag = false;
+-            Vec3 vec3 = Vec3.ZERO;
+-            int i = 0;
+-            BlockPos.MutableBlockPos mutableBlockPos = new BlockPos.MutableBlockPos();
 -
--            for (int l1 = i; l1 < j; ++l1) {
--                for (int i2 = k; i2 < l; ++i2) {
--                    for (int j2 = i1; j2 < j1; ++j2) {
--                        blockposition_mutableblockposition.set(l1, i2, j2);
--                        FluidState fluid = this.level().getFluidState(blockposition_mutableblockposition);
--
--                        if (fluid.is(tag)) {
--                            double d2 = (double) ((float) i2 + fluid.getHeight(this.level(), blockposition_mutableblockposition));
--
--                            if (d2 >= axisalignedbb.minY) {
--                                flag1 = true;
--                                d1 = Math.max(d2 - axisalignedbb.minY, d1);
--                                if (flag) {
--                                    Vec3 vec3d1 = fluid.getFlow(this.level(), blockposition_mutableblockposition);
--
--                                    if (d1 < 0.4D) {
--                                        vec3d1 = vec3d1.scale(d1);
+-            for (int i1 = floor; i1 < ceil; i1++) {
+-                for (int i2 = floor1; i2 < ceil1; i2++) {
+-                    for (int i3 = floor2; i3 < ceil2; i3++) {
+-                        mutableBlockPos.set(i1, i2, i3);
+-                        FluidState fluidState = this.level().getFluidState(mutableBlockPos);
+-                        if (fluidState.is(fluidTag)) {
+-                            double d1 = i2 + fluidState.getHeight(this.level(), mutableBlockPos);
+-                            if (d1 >= aabb.minY) {
+-                                flag = true;
+-                                d = Math.max(d1 - aabb.minY, d);
+-                                if (isPushedByFluid) {
+-                                    Vec3 flow = fluidState.getFlow(this.level(), mutableBlockPos);
+-                                    if (d < 0.4) {
+-                                        flow = flow.scale(d);
 -                                    }
 +        }
 +
@@ -29017,8 +28974,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                    final int minYIterate = currChunkY == minChunkY ? (minBlockY & 15) : 0;
 +                    final int maxYIterate = currChunkY == maxChunkY ? (maxBlockY & 15) : 15;
  
--                                    vec3d = vec3d.add(vec3d1);
--                                    ++k1;
+-                                    vec3 = vec3.add(flow);
+-                                    i++;
 +                    for (int currY = minYIterate; currY <= maxYIterate; ++currY) {
 +                        for (int currZ = minZIterate; currZ <= maxZIterate; ++currZ) {
 +                            for (int currX = minXIterate; currX <= maxXIterate; ++currX) {
@@ -29028,8 +28985,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                                    continue;
                                  }
 -                                // CraftBukkit start - store last lava contact location
--                                if (tag == FluidTags.LAVA) {
--                                    this.lastLavaContact = blockposition_mutableblockposition.immutable();
+-                                if (fluidTag == FluidTags.LAVA) {
+-                                    this.lastLavaContact = mutableBlockPos.immutable();
 +
 +                                mutablePos.set(currX | (currChunkX << 4), currY | (currChunkY << 4), currZ | (currChunkZ << 4));
 +
@@ -29064,53 +29021,52 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
              }
 +        }
  
--            if (vec3d.length() > 0.0D) {
--                if (k1 > 0) {
--                    vec3d = vec3d.scale(1.0D / (double) k1);
+-            if (vec3.length() > 0.0) {
+-                if (i > 0) {
+-                    vec3 = vec3.scale(1.0 / i);
 -                }
 +        this.fluidHeight.put(fluid, maxHeightDiff);
  
 -                if (!(this instanceof Player)) {
--                    vec3d = vec3d.normalize();
+-                    vec3 = vec3.normalize();
 -                }
 +        if (pushVector.lengthSqr() == 0.0) {
 +            return inFluid;
 +        }
  
--                Vec3 vec3d2 = this.getDeltaMovement();
+-                Vec3 deltaMovement = this.getDeltaMovement();
+-                vec3 = vec3.scale(motionScale);
+-                double d2 = 0.003;
+-                if (Math.abs(deltaMovement.x) < 0.003 && Math.abs(deltaMovement.z) < 0.003 && vec3.length() < 0.0045000000000000005) {
+-                    vec3 = vec3.normalize().scale(0.0045000000000000005);
+-                }
 +        // note: totalPushes != 0 as pushVector != 0
 +        pushVector = pushVector.scale(1.0 / totalPushes);
 +        final Vec3 currMovement = this.getDeltaMovement();
  
--                vec3d = vec3d.scale(speed);
--                double d3 = 0.003D;
+-                this.setDeltaMovement(this.getDeltaMovement().add(vec3));
+-            }
 +        if (!((Entity)(Object)this instanceof Player)) {
 +            pushVector = pushVector.normalize();
 +        }
  
--                if (Math.abs(vec3d2.x) < 0.003D && Math.abs(vec3d2.z) < 0.003D && vec3d.length() < 0.0045000000000000005D) {
--                    vec3d = vec3d.normalize().scale(0.0045000000000000005D);
--                }
+-            this.fluidHeight.put(fluidTag, d);
+-            return flag;
 +        pushVector = pushVector.scale(flowScale);
 +        if (Math.abs(currMovement.x) < 0.003 && Math.abs(currMovement.z) < 0.003 && pushVector.length() < 0.0045000000000000005) {
 +            pushVector = pushVector.normalize().scale(0.0045000000000000005);
-+        }
- 
--                this.setDeltaMovement(this.getDeltaMovement().add(vec3d));
--            }
+         }
++
 +        this.setDeltaMovement(currMovement.add(pushVector));
- 
--            this.fluidHeight.put(tag, d1);
--            return flag1;
--        }
++
 +        // note: inFluid = true here as pushVector != 0
 +        return true;
      }
 +    // Paper end - optimise collisions
  
      public boolean touchingUnloadedChunk() {
-         AABB axisalignedbb = this.getBoundingBox().inflate(1.0D);
-@@ -0,0 +0,0 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+         AABB aabb = this.getBoundingBox().inflate(1.0);
+@@ -4473,6 +4769,15 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
          this.setPosRaw(x, y, z, false);
      }
      public final void setPosRaw(double x, double y, double z, boolean forceBoundingBoxUpdate) {
@@ -29126,42 +29082,42 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          if (!checkPosition(this, x, y, z)) {
              return;
          }
-@@ -0,0 +0,0 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+@@ -4603,6 +4908,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
  
      @Override
-     public final void setRemoved(Entity.RemovalReason entity_removalreason, EntityRemoveEvent.Cause cause) {
+     public final void setRemoved(Entity.RemovalReason removalReason, org.bukkit.event.entity.EntityRemoveEvent.Cause cause) {
 +        // Paper start - rewrite chunk system
 +        if (!((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this.level).moonrise$getEntityLookup().canRemoveEntity((Entity)(Object)this)) {
 +            LOGGER.warn("Entity " + this + " is currently prevented from being removed from the world since it is processing section status updates", new Throwable());
 +            return;
 +        }
 +        // Paper end - rewrite chunk system
-         CraftEventFactory.callEntityRemoveEvent(this, cause);
+         org.bukkit.craftbukkit.event.CraftEventFactory.callEntityRemoveEvent(this, cause);
          // CraftBukkit end
          final boolean alreadyRemoved = this.removalReason != null; // Paper - Folia schedulers
-@@ -0,0 +0,0 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+@@ -4614,7 +4925,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
              this.stopRiding();
          }
  
 -        this.getPassengers().forEach(Entity::stopRiding);
 +        if (this.removalReason != Entity.RemovalReason.UNLOADED_TO_CHUNK) { this.getPassengers().forEach(Entity::stopRiding); } // Paper - rewrite chunk system
-         this.levelCallback.onRemove(entity_removalreason);
-         this.onRemoval(entity_removalreason);
+         this.levelCallback.onRemove(removalReason);
+         this.onRemoval(removalReason);
          // Paper start - Folia schedulers
-@@ -0,0 +0,0 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
- 
-     @Override
+@@ -4648,7 +4959,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
      public boolean shouldBeSaved() {
--        return this.removalReason != null && !this.removalReason.shouldSave() ? false : (this.isPassenger() ? false : !this.isVehicle() || !this.hasExactlyOnePlayerPassenger());
-+        return this.removalReason != null && !this.removalReason.shouldSave() ? false : (this.isPassenger() ? false : !this.isVehicle() || !((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)this).moonrise$hasAnyPlayerPassengers()); // Paper - rewrite chunk system
+         return (this.removalReason == null || this.removalReason.shouldSave())
+             && !this.isPassenger()
+-            && (!this.isVehicle() || !this.hasExactlyOnePlayerPassenger());
++            && (!this.isVehicle() || !((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)this).moonrise$hasAnyPlayerPassengers()); // Paper - rewrite chunk system
      }
  
      @Override
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
+diff --git a/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/net/minecraft/world/entity/ai/village/poi/PoiManager.java
+index 7d590dd06cc69c0925d22708425520c38e3cda25..5c5724f5e3ad640f55aecbc1d8f71d1f59ecdc62 100644
+--- a/net/minecraft/world/entity/ai/village/poi/PoiManager.java
++++ b/net/minecraft/world/entity/ai/village/poi/PoiManager.java
+@@ -38,12 +38,137 @@ import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
  import net.minecraft.world.level.chunk.storage.SectionStorage;
  import net.minecraft.world.level.chunk.storage.SimpleRegionStorage;
  
@@ -29298,76 +29254,76 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    // Paper end - rewrite chunk system
 +
      public PoiManager(
-         RegionStorageInfo storageKey,
-         Path directory,
-@@ -0,0 +0,0 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> {
-             world
+         RegionStorageInfo info,
+         Path folder,
+@@ -64,6 +189,7 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> {
+             levelHeightAccessor
          );
          this.distanceTracker = new PoiManager.DistanceTracker();
-+        this.world = (net.minecraft.server.level.ServerLevel)world; // Paper - rewrite chunk system
++        this.world = (net.minecraft.server.level.ServerLevel)levelHeightAccessor; // Paper - rewrite chunk system
      }
  
      public void add(BlockPos pos, Holder<PoiType> type) {
-@@ -0,0 +0,0 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> {
+@@ -197,8 +323,10 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> {
      }
  
-     public int sectionsToVillage(SectionPos pos) {
+     public int sectionsToVillage(SectionPos sectionPos) {
 -        this.distanceTracker.runAllUpdates();
--        return this.distanceTracker.getLevel(pos.asLong());
+-        return this.distanceTracker.getLevel(sectionPos.asLong());
 +        // Paper start - rewrite chunk system
 +        this.villageDistanceTracker.propagateUpdates();
-+        return convertBetweenLevels(this.villageDistanceTracker.getLevel(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionKey(pos)));
++        return convertBetweenLevels(this.villageDistanceTracker.getLevel(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionKey(sectionPos)));
 +        // Paper end - rewrite chunk system
      }
  
-     boolean isVillageCenter(long pos) {
-@@ -0,0 +0,0 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> {
+     boolean isVillageCenter(long chunkPos) {
+@@ -212,19 +340,26 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> {
  
      @Override
-     public void tick(BooleanSupplier shouldKeepTicking) {
--        super.tick(shouldKeepTicking);
+     public void tick(BooleanSupplier aheadOfTime) {
+-        super.tick(aheadOfTime);
 -        this.distanceTracker.runAllUpdates();
 +        this.villageDistanceTracker.propagateUpdates(); // Paper - rewrite chunk system
      }
  
      @Override
--    protected void setDirty(long pos) {
--        super.setDirty(pos);
--        this.distanceTracker.update(pos, this.distanceTracker.getLevelFromSource(pos), false);
-+    public void setDirty(long pos) { // Paper - public
+-    protected void setDirty(long sectionPos) {
+-        super.setDirty(sectionPos);
+-        this.distanceTracker.update(sectionPos, this.distanceTracker.getLevelFromSource(sectionPos), false);
++    public void setDirty(long sectionPos) { // Paper - public
 +        // Paper start - rewrite chunk system
-+        final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionX(pos);
-+        final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(pos);
++        final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionX(sectionPos);
++        final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(sectionPos);
 +        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager manager = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager;
 +        final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk chunk = manager.getPoiChunkIfLoaded(chunkX, chunkZ, false);
 +        if (chunk != null) {
 +            chunk.setDirty(true);
 +        }
-+        this.updateDistanceTracking(pos);
++        this.updateDistanceTracking(sectionPos);
 +        // Paper end - rewrite chunk system
      }
  
      @Override
-     protected void onSectionLoad(long pos) {
--        this.distanceTracker.update(pos, this.distanceTracker.getLevelFromSource(pos), false);
-+        this.updateDistanceTracking(pos); // Paper - rewrite chunk system
+     protected void onSectionLoad(long sectionKey) {
+-        this.distanceTracker.update(sectionKey, this.distanceTracker.getLevelFromSource(sectionKey), false);
++        this.updateDistanceTracking(sectionKey); // Paper - rewrite chunk system
      }
  
-     public void checkConsistencyWithBlocks(SectionPos sectionPos, LevelChunkSection chunkSection) {
-@@ -0,0 +0,0 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> {
+     public void checkConsistencyWithBlocks(SectionPos sectionPos, LevelChunkSection levelChunkSection) {
+@@ -263,7 +398,7 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> {
              .map(sectionPos -> Pair.of(sectionPos, this.getOrLoad(sectionPos.asLong())))
              .filter(pair -> !pair.getSecond().map(PoiSection::isValid).orElse(false))
              .map(pair -> pair.getFirst().chunk())
 -            .filter(chunkPos -> this.loadedChunks.add(chunkPos.toLong()))
 +            // Paper - rewrite chunk system
-             .forEach(chunkPos -> world.getChunk(chunkPos.x, chunkPos.z, ChunkStatus.EMPTY));
+             .forEach(chunkPos -> levelReader.getChunk(chunkPos.x, chunkPos.z, ChunkStatus.EMPTY));
      }
  
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ import net.minecraft.core.SectionPos;
+diff --git a/net/minecraft/world/entity/ai/village/poi/PoiSection.java b/net/minecraft/world/entity/ai/village/poi/PoiSection.java
+index 324cc0686f0f5b1371b2bbea5b8c8fdb1f363006..39cd1e3d8192d7077d6b7864d33933097cc6b986 100644
+--- a/net/minecraft/world/entity/ai/village/poi/PoiSection.java
++++ b/net/minecraft/world/entity/ai/village/poi/PoiSection.java
+@@ -23,13 +23,27 @@ import net.minecraft.core.SectionPos;
  import net.minecraft.util.VisibleForDebug;
  import org.slf4j.Logger;
  
@@ -29393,27 +29349,27 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - rewrite chunk system
 +
-     public PoiSection(Runnable updateListener) {
-         this(updateListener, true, ImmutableList.of());
+     public PoiSection(Runnable setDirty) {
+         this(setDirty, true, ImmutableList.of());
      }
-diff --git a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java
-+++ b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java
-@@ -0,0 +0,0 @@ public class ArmorStand extends LivingEntity {
+diff --git a/net/minecraft/world/entity/decoration/ArmorStand.java b/net/minecraft/world/entity/decoration/ArmorStand.java
+index 33f6d6862731d22d6d3eeb7cf39a4a42049afae3..a3cc0001a949597e345d7919c3f6109fa4a949ad 100644
+--- a/net/minecraft/world/entity/decoration/ArmorStand.java
++++ b/net/minecraft/world/entity/decoration/ArmorStand.java
+@@ -316,7 +316,7 @@ public class ArmorStand extends LivingEntity {
      @Override
      protected void pushEntities() {
          if (!this.level().paperConfig().entities.armorStands.doCollisionEntityLookups) return; // Paper - Option to prevent armor stands from doing entity lookups
--        List<Entity> list = this.level().getEntities((Entity) this, this.getBoundingBox(), ArmorStand.RIDABLE_MINECARTS);
-+        List<AbstractMinecart> list = this.level().getEntitiesOfClass(AbstractMinecart.class, this.getBoundingBox(), RIDABLE_MINECARTS); // Paper - optimise collisions
-         Iterator iterator = list.iterator();
- 
-         while (iterator.hasNext()) {
-diff --git a/src/main/java/net/minecraft/world/level/ClipContext.java b/src/main/java/net/minecraft/world/level/ClipContext.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/ClipContext.java
-+++ b/src/main/java/net/minecraft/world/level/ClipContext.java
-@@ -0,0 +0,0 @@ public class ClipContext {
+-        for (Entity entity : this.level().getEntities(this, this.getBoundingBox(), RIDABLE_MINECARTS)) {
++        for (Entity entity : this.level().getEntitiesOfClass(AbstractMinecart.class, this.getBoundingBox(), RIDABLE_MINECARTS)) { // Paper - optimise collisions
+             if (this.distanceToSqr(entity) <= 0.2) {
+                 entity.push(this);
+             }
+diff --git a/net/minecraft/world/level/ClipContext.java b/net/minecraft/world/level/ClipContext.java
+index 9f34fc4278860dd7bcfa1fd79b15e588b0cc3973..a7ebd624652cb6f0edc735bf6b9760e7b443594f 100644
+--- a/net/minecraft/world/level/ClipContext.java
++++ b/net/minecraft/world/level/ClipContext.java
+@@ -17,7 +17,7 @@ public class ClipContext {
      private final Vec3 from;
      private final Vec3 to;
      private final ClipContext.Block block;
@@ -29421,33 +29377,33 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public final ClipContext.Fluid fluid; // Paper - optimise collisions - public
      private final CollisionContext collisionContext;
  
-     public ClipContext(Vec3 start, Vec3 end, ClipContext.Block shapeType, ClipContext.Fluid fluidHandling, Entity entity) {
-diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/EntityGetter.java
-+++ b/src/main/java/net/minecraft/world/level/EntityGetter.java
-@@ -0,0 +0,0 @@ import net.minecraft.world.phys.shapes.BooleanOp;
+     public ClipContext(Vec3 from, Vec3 to, ClipContext.Block block, ClipContext.Fluid fluid, Entity entity) {
+diff --git a/net/minecraft/world/level/EntityGetter.java b/net/minecraft/world/level/EntityGetter.java
+index 300f3ed58109219d97846082941b860585f66fed..e81195df621159da67136f020fa7a6d39d1ee5ed 100644
+--- a/net/minecraft/world/level/EntityGetter.java
++++ b/net/minecraft/world/level/EntityGetter.java
+@@ -15,7 +15,7 @@ import net.minecraft.world.phys.shapes.BooleanOp;
  import net.minecraft.world.phys.shapes.Shapes;
  import net.minecraft.world.phys.shapes.VoxelShape;
  
 -public interface EntityGetter {
 +public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter { // Paper - rewrite chunk system
-     List<Entity> getEntities(@Nullable Entity except, AABB box, Predicate<? super Entity> predicate);
+     List<Entity> getEntities(@Nullable Entity entity, AABB area, Predicate<? super Entity> predicate);
  
-     <T extends Entity> List<T> getEntities(EntityTypeTest<Entity, T> filter, AABB box, Predicate<? super T> predicate);
-@@ -0,0 +0,0 @@ public interface EntityGetter {
-         return this.getEntities(except, box, EntitySelector.NO_SPECTATORS);
+     <T extends Entity> List<T> getEntities(EntityTypeTest<Entity, T> entityTypeTest, AABB bounds, Predicate<? super T> predicate);
+@@ -30,21 +30,44 @@ public interface EntityGetter {
+         return this.getEntities(entity, area, EntitySelector.NO_SPECTATORS);
      }
  
--    default boolean isUnobstructed(@Nullable Entity except, VoxelShape shape) {
+-    default boolean isUnobstructed(@Nullable Entity entity, VoxelShape shape) {
 -        if (shape.isEmpty()) {
 -            return true;
 -        } else {
--            for (Entity entity : this.getEntities(except, shape.bounds())) {
--                if (!entity.isRemoved()
--                    && entity.blocksBuilding
--                    && (except == null || !entity.isPassengerOfSameVehicle(except))
--                    && Shapes.joinIsNotEmpty(shape, Shapes.create(entity.getBoundingBox()), BooleanOp.AND)) {
+-            for (Entity entity1 : this.getEntities(entity, shape.bounds())) {
+-                if (!entity1.isRemoved()
+-                    && entity1.blocksBuilding
+-                    && (entity == null || !entity1.isPassengerOfSameVehicle(entity))
+-                    && Shapes.joinIsNotEmpty(shape, Shapes.create(entity1.getBoundingBox()), BooleanOp.AND)) {
 -                    return false;
 +    // Paper start - rewrite chunk system
 +    @Override
@@ -29490,16 +29446,16 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - optimise collisions
      }
  
-     default <T extends Entity> List<T> getEntitiesOfClass(Class<T> entityClass, AABB box) {
-@@ -0,0 +0,0 @@ public interface EntityGetter {
+     default <T extends Entity> List<T> getEntitiesOfClass(Class<T> entityClass, AABB area) {
+@@ -52,23 +75,41 @@ public interface EntityGetter {
      }
  
-     default List<VoxelShape> getEntityCollisions(@Nullable Entity entity, AABB box) {
--        if (box.getSize() < 1.0E-7) {
+     default List<VoxelShape> getEntityCollisions(@Nullable Entity entity, AABB collisionBox) {
+-        if (collisionBox.getSize() < 1.0E-7) {
 -            return List.of();
 +        // Paper start - optimise collisions
 +        // first behavior change is to correctly check for empty AABB
-+        if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(box)) {
++        if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(collisionBox)) {
 +            // reduce indirection by always returning type with same class
 +            return new java.util.ArrayList<>();
 +        }
@@ -29507,23 +29463,23 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // to comply with vanilla intersection rules, expand by -epsilon so that we only get stuff we definitely collide with.
 +        // Vanilla for hard collisions has this backwards, and they expand by +epsilon but this causes terrible problems
 +        // specifically with boat collisions.
-+        box = box.inflate(-ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON);
++        collisionBox = collisionBox.inflate(-ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON);
 +
 +        final List<Entity> entities;
-+        if (entity != null && ((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)entity).moonrise$isHardColliding()) {
-+            entities = this.getEntities(entity, box, null);
++        if (entity != null && ((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity) entity).moonrise$isHardColliding()) {
++            entities = this.getEntities(entity, collisionBox, null);
          } else {
 -            Predicate<Entity> predicate = entity == null ? EntitySelector.CAN_BE_COLLIDED_WITH : EntitySelector.NO_SPECTATORS.and(entity::canCollideWith);
--            List<Entity> list = this.getEntities(entity, box.inflate(1.0E-7), predicate);
--            if (list.isEmpty()) {
+-            List<Entity> entities = this.getEntities(entity, collisionBox.inflate(1.0E-7), predicate);
+-            if (entities.isEmpty()) {
 -                return List.of();
 -            } else {
--                Builder<VoxelShape> builder = ImmutableList.builderWithExpectedSize(list.size());
+-                Builder<VoxelShape> builder = ImmutableList.builderWithExpectedSize(entities.size());
 -
--                for (Entity entity2 : list) {
--                    builder.add(Shapes.create(entity2.getBoundingBox()));
+-                for (Entity entity1 : entities) {
+-                    builder.add(Shapes.create(entity1.getBoundingBox()));
 -                }
-+            entities = ((ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter)this).moonrise$getHardCollidingEntities(entity, box, null);
++            entities = ((ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter) this).moonrise$getHardCollidingEntities(entity, collisionBox, null);
 +        }
  
 -                return builder.build();
@@ -29546,11 +29502,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      // Paper start - Affects Spawning API
-diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/Level.java
-+++ b/src/main/java/net/minecraft/world/level/Level.java
-@@ -0,0 +0,0 @@ import net.minecraft.world.level.storage.LevelData;
+diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java
+index 771d6ed6a7c889c09efd4ff6e20298c851eaa79f..8331b49185500ab3b4307e9ae05126b4f83a318a 100644
+--- a/net/minecraft/world/level/Level.java
++++ b/net/minecraft/world/level/Level.java
+@@ -79,6 +79,7 @@ import net.minecraft.world.level.storage.LevelData;
  import net.minecraft.world.level.storage.WritableLevelData;
  import net.minecraft.world.phys.AABB;
  import net.minecraft.world.phys.Vec3;
@@ -29558,25 +29514,25 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  import net.minecraft.world.scores.Scoreboard;
  
  // CraftBukkit start
-@@ -0,0 +0,0 @@ import org.bukkit.entity.SpawnCategory;
+@@ -102,7 +103,7 @@ import org.bukkit.entity.SpawnCategory;
  import org.bukkit.event.block.BlockPhysicsEvent;
  // CraftBukkit end
  
 -public abstract class Level implements LevelAccessor, AutoCloseable {
 +public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel, ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter { // Paper - rewrite chunk system // Paper - optimise collisions
- 
      public static final Codec<ResourceKey<Level>> RESOURCE_KEY_CODEC = ResourceKey.codec(Registries.DIMENSION);
      public static final ResourceKey<Level> OVERWORLD = ResourceKey.create(Registries.DIMENSION, ResourceLocation.withDefaultNamespace("overworld"));
-@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+     public static final ResourceKey<Level> NETHER = ResourceKey.create(Registries.DIMENSION, ResourceLocation.withDefaultNamespace("the_nether"));
+@@ -127,7 +128,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
      public float rainLevel;
      protected float oThunderLevel;
      public float thunderLevel;
 -    public final RandomSource random = RandomSource.create();
 +    public final RandomSource random = new ca.spottedleaf.moonrise.common.util.ThreadUnsafeRandom(net.minecraft.world.level.levelgen.RandomSupport.generateUniqueSeed()); // Paper - replace random
-     /** @deprecated */
      @Deprecated
      private final RandomSource threadSafeRandom = RandomSource.createThreadSafe();
-@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+     private final Holder<DimensionType> dimensionTypeRegistration;
+@@ -202,6 +203,629 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
  
      public abstract ResourceKey<LevelStem> getTypeKey();
  
@@ -29670,7 +29626,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    @Override
 +    public boolean moonrise$areChunksLoaded(final int fromX, final int fromZ, final int toX, final int toZ) {
-+        final ChunkSource chunkSource = this.getChunkSource();
++        final net.minecraft.world.level.chunk.ChunkSource chunkSource = this.getChunkSource();
 +
 +        for (int currZ = fromZ; currZ <= toZ; ++currZ) {
 +            for (int currX = fromX; currX <= toX; ++currX) {
@@ -30010,7 +29966,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        final int minChunkZ = minBlockZ >> 4;
 +        final int maxChunkZ = maxBlockZ >> 4;
 +
-+        final ChunkSource chunkSource = this.getChunkSource();
++        final net.minecraft.world.level.chunk.ChunkSource chunkSource = this.getChunkSource();
 +
 +        for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) {
 +            for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) {
@@ -30203,9 +30159,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - optimise random ticking
 +
-     protected Level(WritableLevelData worlddatamutable, ResourceKey<Level> resourcekey, RegistryAccess iregistrycustom, Holder<DimensionType> holder, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function<org.spigotmc.SpigotWorldConfig, io.papermc.paper.configuration.WorldConfiguration> paperWorldConfigCreator, java.util.concurrent.Executor executor) { // Paper - create paper world config & Anti-Xray
+     protected Level(
+         WritableLevelData levelData,
+         ResourceKey<Level> dimension,
+@@ -218,6 +842,15 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+         io.papermc.paper.configuration.WorldConfiguration> paperWorldConfigCreator, // Paper - create paper world config
+         java.util.concurrent.Executor executor // Paper - Anti-Xray
+     ) {
 +        // Paper start - getblock optimisations - cache world height/sections
-+        final DimensionType dimType = holder.value();
++        final DimensionType dimType = dimensionTypeRegistration.value();
 +        this.minY = dimType.minY();
 +        this.height = dimType.height();
 +        this.maxY = this.minY + this.height - 1;
@@ -30213,18 +30175,18 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        this.maxSectionY = this.maxY >> 4;
 +        this.sectionsCount = this.maxSectionY - this.minSectionY + 1;
 +        // Paper end - getblock optimisations - cache world height/sections
-         this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot
+         this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) levelData).getLevelName()); // Spigot
          this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper - create paper world config
          this.generator = gen;
-@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+@@ -298,6 +931,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
          this.entityLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.entityMaxTickTime);
          this.tileLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.tileMaxTickTime);
-         this.chunkPacketBlockController = this.paperConfig().anticheat.antiXray.enabled ? new com.destroystokyo.paper.antixray.ChunkPacketBlockControllerAntiXray(this, executor) : com.destroystokyo.paper.antixray.ChunkPacketBlockController.NO_OPERATION_INSTANCE; // Paper - Anti-Xray
+         this.chunkPacketBlockController = this.paperConfig().anticheat.antiXray.enabled ? new io.papermc.paper.antixray.ChunkPacketBlockControllerAntiXray(this, executor) : io.papermc.paper.antixray.ChunkPacketBlockController.NO_OPERATION_INSTANCE; // Paper - Anti-Xray
 +        this.entityLookup = new ca.spottedleaf.moonrise.patches.chunk_system.level.entity.dfl.DefaultEntityLookup(this); // Paper - rewrite chunk system
      }
  
      // Paper start - Cancel hit for vanished players
-@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+@@ -567,7 +1201,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
                  this.setBlocksDirty(blockposition, iblockdata1, iblockdata2);
              }
  
@@ -30233,19 +30195,18 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
                  this.sendBlockUpdated(blockposition, iblockdata1, iblockdata, i);
              }
  
-@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
-         // Iterator<TickingBlockEntity> iterator = this.blockEntityTickers.iterator();
-         boolean flag = this.tickRateManager().runsNormally();
+@@ -835,6 +1469,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+         // Spigot start
+         boolean runsNormally = this.tickRateManager().runsNormally();
  
 +        int tickedEntities = 0; // Paper - rewrite chunk system
-+
-         int tilesThisCycle = 0;
          var toRemove = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<TickingBlockEntity>(); // Paper - Fix MC-117075; use removeAll
          toRemove.add(null); // Paper - Fix MC-117075
-@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
-                 // Spigot end
-             } else if (flag && this.shouldTickBlocksAt(tickingblockentity.getPos())) {
-                 tickingblockentity.tick();
+         for (tileTickPosition = 0; tileTickPosition < this.blockEntityTickers.size(); tileTickPosition++) { // Paper - Disable tick limiters
+@@ -845,6 +1480,11 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+                 toRemove.add(tickingBlockEntity); // Paper - Fix MC-117075; use removeAll
+             } else if (runsNormally && 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();
@@ -30254,16 +30215,18 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
              }
          }
          this.blockEntityTickers.removeAll(toRemove); // Paper - Fix MC-117075
-@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+@@ -865,6 +1505,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
          }
 +        this.moonrise$midTickTasks(); // Paper - rewrite chunk system
      }
+ 
      // Paper start - Option to prevent armor stands from doing entity lookups
-     @Override
+@@ -872,7 +1513,14 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
      public boolean noCollision(@Nullable Entity entity, AABB box) {
-         if (entity instanceof net.minecraft.world.entity.decoration.ArmorStand && !entity.level().paperConfig().entities.armorStands.doCollisionEntityLookups) return false;
+         if (entity instanceof net.minecraft.world.entity.decoration.ArmorStand && !entity.level().paperConfig().entities.armorStands.doCollisionEntityLookups)
+             return false;
 -        return LevelAccessor.super.noCollision(entity, box);
 +        // Paper start - optimise collisions
 +        final int flags = entity == null ? (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER | ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_ONLY) : ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_ONLY;
@@ -30276,59 +30239,62 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
      // Paper end - Option to prevent armor stands from doing entity lookups
  
-@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+@@ -1010,7 +1658,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+         if (this.isOutsideBuildHeight(pos)) {
+             return null;
+         } else {
+-            return !this.isClientSide && Thread.currentThread() != this.thread
++            return !this.isClientSide && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThread() // Paper - rewrite chunk system
+                 ? null
+                 : this.getChunkAt(pos).getBlockEntity(pos, LevelChunk.EntityCreationType.IMMEDIATE);
          }
-         // Paper end - Perf: Optimize capturedTileEntities lookup
-         // CraftBukkit end
--        return this.isOutsideBuildHeight(blockposition) ? null : (!this.isClientSide && Thread.currentThread() != this.thread ? null : this.getChunkAt(blockposition).getBlockEntity(blockposition, LevelChunk.EntityCreationType.IMMEDIATE));
-+        return this.isOutsideBuildHeight(blockposition) ? null : (!this.isClientSide && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThread() ? null : this.getChunkAt(blockposition).getBlockEntity(blockposition, LevelChunk.EntityCreationType.IMMEDIATE)); // Paper - rewrite chunk system
-     }
- 
-     public void setBlockEntity(BlockEntity blockEntity) {
-@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+@@ -1103,22 +1751,16 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+     public List<Entity> getEntities(@Nullable Entity entity, AABB boundingBox, Predicate<? super Entity> predicate) {
          Profiler.get().incrementCounter("getEntities");
          List<Entity> list = Lists.newArrayList();
- 
--        this.getEntities().get(box, (entity1) -> {
--            if (entity1 != except && predicate.test(entity1)) {
+-        this.getEntities().get(boundingBox, entity1 -> {
+-            if (entity1 != entity && predicate.test(entity1)) {
 -                list.add(entity1);
 -            }
--
 -        });
--        Iterator iterator = this.dragonParts().iterator();
+ 
+-        for (EnderDragonPart enderDragonPart : this.dragonParts()) {
+-            if (enderDragonPart != entity
+-                && enderDragonPart.parentMob != entity
+-                && predicate.test(enderDragonPart)
+-                && boundingBox.intersects(enderDragonPart.getBoundingBox())) {
+-                list.add(enderDragonPart);
+-            }
+-        }
 +        // Paper start - rewrite chunk system
 +        final List<Entity> ret = new java.util.ArrayList<>();
  
--        while (iterator.hasNext()) {
--            EnderDragonPart entitycomplexpart = (EnderDragonPart) iterator.next();
-+        ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(except, box, ret, predicate);
- 
--            if (entitycomplexpart != except && entitycomplexpart.parentMob != except && predicate.test(entitycomplexpart) && box.intersects(entitycomplexpart.getBoundingBox())) {
--                list.add(entitycomplexpart);
--            }
--        }
-+        ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, except, box, predicate, ret);
- 
 -        return list;
++        ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(entity, boundingBox, ret, predicate);
++
++        ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entity, boundingBox, predicate, ret);
++
 +        return ret;
 +        // Paper end - rewrite chunk system
      }
  
      @Override
-@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
-         this.getEntities(filter, box, predicate, result, Integer.MAX_VALUE);
+@@ -1132,33 +1774,94 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+         this.getEntities(entityTypeTest, bounds, predicate, output, Integer.MAX_VALUE);
      }
  
--    public <T extends Entity> void getEntities(EntityTypeTest<Entity, T> filter, AABB box, Predicate<? super T> predicate, List<? super T> result, int limit) {
+-    public <T extends Entity> void getEntities(
+-        EntityTypeTest<Entity, T> entityTypeTest, AABB bounds, Predicate<? super T> predicate, List<? super T> output, int maxResults
+-    ) {
 +    // Paper start - rewrite chunk system
 +    public <T extends Entity> void getEntities(final EntityTypeTest<Entity, T> entityTypeTest,
 +                                               final AABB boundingBox, final Predicate<? super T> predicate,
 +                                               final List<? super T> into, final int maxCount) {
          Profiler.get().incrementCounter("getEntities");
--        this.getEntities().get(filter, box, (entity) -> {
+-        this.getEntities().get(entityTypeTest, bounds, entity -> {
 -            if (predicate.test(entity)) {
--                result.add(entity);
--                if (result.size() >= limit) {
+-                output.add(entity);
+-                if (output.size() >= maxResults) {
 -                    return AbortableIterationConsumer.Continuation.ABORT;
 -                }
 +
@@ -30344,9 +30310,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
              }
 +        }
  
--            if (entity instanceof EnderDragon entityenderdragon) {
--                EnderDragonPart[] aentitycomplexpart = entityenderdragon.getSubEntities();
--                int j = aentitycomplexpart.length;
+-            if (entity instanceof EnderDragon enderDragon) {
+-                for (EnderDragonPart enderDragonPart : enderDragon.getSubEntities()) {
+-                    T entity1 = entityTypeTest.tryCast(enderDragonPart);
+-                    if (entity1 != null && predicate.test(entity1)) {
+-                        output.add(entity1);
+-                        if (output.size() >= maxResults) {
+-                            return AbortableIterationConsumer.Continuation.ABORT;
+-                        }
+-                    }
 +        if (entityTypeTest == null) {
 +            if (maxCount != Integer.MAX_VALUE) {
 +                ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)predicate, maxCount);
@@ -30358,18 +30330,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                return;
 +            }
 +        }
- 
--                for (int k = 0; k < j; ++k) {
--                    EnderDragonPart entitycomplexpart = aentitycomplexpart[k];
--                    T t0 = filter.tryCast(entitycomplexpart); // CraftBukkit - decompile error
++
 +        final Class<? extends Entity> base = entityTypeTest.getBaseClass();
- 
--                    if (t0 != null && predicate.test(t0)) {
--                        result.add(t0);
--                        if (result.size() >= limit) {
--                            return AbortableIterationConsumer.Continuation.ABORT;
--                        }
--                    }
++
 +        final Predicate<? super T> modifiedPredicate;
 +        if (predicate == null) {
 +            modifiedPredicate = (final T obj) -> {
@@ -30431,11 +30394,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      @Nullable
      public abstract Entity getEntity(int id);
-diff --git a/src/main/java/net/minecraft/world/level/LevelReader.java b/src/main/java/net/minecraft/world/level/LevelReader.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/LevelReader.java
-+++ b/src/main/java/net/minecraft/world/level/LevelReader.java
-@@ -0,0 +0,0 @@ import net.minecraft.world.level.dimension.DimensionType;
+diff --git a/net/minecraft/world/level/LevelReader.java b/net/minecraft/world/level/LevelReader.java
+index 2709803b9266ff4a2034d83321cd0ba4e30fc0aa..26c8c1e5598daf3550aef05b12218c47bda6618b 100644
+--- a/net/minecraft/world/level/LevelReader.java
++++ b/net/minecraft/world/level/LevelReader.java
+@@ -22,7 +22,18 @@ import net.minecraft.world.level.dimension.DimensionType;
  import net.minecraft.world.level.levelgen.Heightmap;
  import net.minecraft.world.phys.AABB;
  
@@ -30453,13 +30416,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    // Paper end - rewrite chunk system
 +
      @Nullable
-     ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create);
+     ChunkAccess getChunk(int x, int z, ChunkStatus chunkStatus, boolean requireChunk);
  
-diff --git a/src/main/java/net/minecraft/world/level/ServerExplosion.java b/src/main/java/net/minecraft/world/level/ServerExplosion.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/ServerExplosion.java
-+++ b/src/main/java/net/minecraft/world/level/ServerExplosion.java
-@@ -0,0 +0,0 @@ public class ServerExplosion implements Explosion {
+diff --git a/net/minecraft/world/level/ServerExplosion.java b/net/minecraft/world/level/ServerExplosion.java
+index 3619509d51ebd2e5e36fe4b67e76c94a8d272d1b..7b132c55caf9d3c3df3b0a123f4b5bfc7ae35984 100644
+--- a/net/minecraft/world/level/ServerExplosion.java
++++ b/net/minecraft/world/level/ServerExplosion.java
+@@ -63,6 +63,249 @@ public class ServerExplosion implements Explosion {
      public float yield;
      // CraftBukkit end
      public boolean excludeSourceFromDamage = true; // Paper - Allow explosions to damage source
@@ -30707,61 +30670,60 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - collisions optimisations
  
-     public ServerExplosion(ServerLevel world, @Nullable Entity entity, @Nullable DamageSource damageSource, @Nullable ExplosionDamageCalculator behavior, Vec3 pos, float power, boolean createFire, Explosion.BlockInteraction destructionType) {
-         this.level = world;
-@@ -0,0 +0,0 @@ public class ServerExplosion implements Explosion {
+     public ServerExplosion(
+         ServerLevel level,
+@@ -134,63 +377,102 @@ public class ServerExplosion implements Explosion {
      }
  
      private List<BlockPos> calculateExplodedPositions() {
--        Set<BlockPos> set = new HashSet();
--        boolean flag = true;
+-        Set<BlockPos> set = new HashSet<>();
+-        int i = 16;
 -
--        for (int i = 0; i < 16; ++i) {
--            for (int j = 0; j < 16; ++j) {
--                for (int k = 0; k < 16; ++k) {
--                    if (i == 0 || i == 15 || j == 0 || j == 15 || k == 0 || k == 15) {
--                        double d0 = (double) ((float) i / 15.0F * 2.0F - 1.0F);
--                        double d1 = (double) ((float) j / 15.0F * 2.0F - 1.0F);
--                        double d2 = (double) ((float) k / 15.0F * 2.0F - 1.0F);
--                        double d3 = Math.sqrt(d0 * d0 + d1 * d1 + d2 * d2);
--
--                        d0 /= d3;
--                        d1 /= d3;
--                        d2 /= d3;
+-        for (int i1 = 0; i1 < 16; i1++) {
+-            for (int i2 = 0; i2 < 16; i2++) {
+-                for (int i3 = 0; i3 < 16; i3++) {
+-                    if (i1 == 0 || i1 == 15 || i2 == 0 || i2 == 15 || i3 == 0 || i3 == 15) {
+-                        double d = i1 / 15.0F * 2.0F - 1.0F;
+-                        double d1 = i2 / 15.0F * 2.0F - 1.0F;
+-                        double d2 = i3 / 15.0F * 2.0F - 1.0F;
+-                        double squareRoot = Math.sqrt(d * d + d1 * d1 + d2 * d2);
+-                        d /= squareRoot;
+-                        d1 /= squareRoot;
+-                        d2 /= squareRoot;
 -                        float f = this.radius * (0.7F + this.level.random.nextFloat() * 0.6F);
--                        double d4 = this.center.x;
--                        double d5 = this.center.y;
--                        double d6 = this.center.z;
+-                        double d3 = this.center.x;
+-                        double d4 = this.center.y;
+-                        double d5 = this.center.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
--
--                            if (!this.level.isInWorldBounds(blockposition)) {
+-                            BlockPos blockPos = BlockPos.containing(d3, d4, d5);
+-                            BlockState blockState = this.level.getBlockState(blockPos);
+-                            if (!blockState.isDestroyable()) continue; // Paper - Protect Bedrock and End Portal/Frames from being destroyed
+-                            FluidState fluidState = blockState.getFluidState(); // Paper - Perf: Optimize call to getFluid for explosions
+-                            if (!this.level.isInWorldBounds(blockPos)) {
 -                                break;
 -                            }
 +        // Paper start - collision optimisations
 +        final ObjectArrayList<BlockPos> ret = new ObjectArrayList<>();
  
--                            Optional<Float> optional = this.damageCalculator.getBlockExplosionResistance(this, this.level, blockposition, iblockdata, fluid);
+-                            Optional<Float> blockExplosionResistance = this.damageCalculator
+-                                .getBlockExplosionResistance(this, this.level, blockPos, blockState, fluidState);
+-                            if (blockExplosionResistance.isPresent()) {
+-                                f -= (blockExplosionResistance.get() + 0.3F) * 0.3F;
+-                            }
 +        final Vec3 center = this.center;
  
--                            if (optional.isPresent()) {
--                                f -= ((Float) optional.get() + 0.3F) * 0.3F;
--                            }
-+        final ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[] blockCache = this.directMappedBlockCache;
- 
--                            if (f > 0.0F && this.damageCalculator.shouldBlockExplode(this, this.level, blockposition, iblockdata, f)) {
--                                set.add(blockposition);
+-                            if (f > 0.0F && this.damageCalculator.shouldBlockExplode(this, this.level, blockPos, blockState, f)) {
+-                                set.add(blockPos);
 -                                // Paper start - prevent headless pistons from forming
--                                if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowHeadlessPistons && iblockdata.getBlock() == Blocks.MOVING_PISTON) {
--                                    net.minecraft.world.level.block.entity.BlockEntity extension = this.level.getBlockEntity(blockposition);
+-                                if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowHeadlessPistons && blockState.is(Blocks.MOVING_PISTON)) {
+-                                    net.minecraft.world.level.block.entity.BlockEntity extension = this.level.getBlockEntity(blockPos);
 -                                    if (extension instanceof net.minecraft.world.level.block.piston.PistonMovingBlockEntity blockEntity && blockEntity.isSourcePiston()) {
--                                        net.minecraft.core.Direction direction = iblockdata.getValue(net.minecraft.world.level.block.piston.PistonHeadBlock.FACING);
--                                        set.add(blockposition.relative(direction.getOpposite()));
+-                                        net.minecraft.core.Direction direction = blockState.getValue(net.minecraft.world.level.block.piston.PistonHeadBlock.FACING);
+-                                        set.add(blockPos.relative(direction.getOpposite()));
 -                                    }
++        final ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[] blockCache = this.directMappedBlockCache;
++
 +        // use initial cache value that is most likely to be used: the source position
 +        final ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache initialCache;
 +        {
@@ -30837,10 +30799,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
                                  }
 -                                // Paper end - prevent headless pistons from forming
                              }
--
--                            d4 += d0 * 0.30000001192092896D;
--                            d5 += d1 * 0.30000001192092896D;
--                            d6 += d2 * 0.30000001192092896D;
+ 
+-                            d3 += d * 0.3F;
+-                            d4 += d1 * 0.3F;
+-                            d5 += d2 * 0.3F;
 +                            // Paper end - prevent headless pistons from forming
                          }
                      }
@@ -30854,13 +30816,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            } while (power > 0.0f);
          }
  
--        return new ObjectArrayList(set);
+-        return new ObjectArrayList<>(set);
 +        return ret;
 +        // Paper end - collision optimisations
      }
  
      private void hurtEntities() {
-@@ -0,0 +0,0 @@ public class ServerExplosion implements Explosion {
+@@ -371,6 +653,14 @@ public class ServerExplosion implements Explosion {
              return;
          }
          // CraftBukkit end
@@ -30872,10 +30834,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        this.directMappedBlockCache = new ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH];
 +        this.mutablePos = new BlockPos.MutableBlockPos();
 +        // Paper end - collision optimisations
-         this.level.gameEvent(this.source, (Holder) GameEvent.EXPLODE, this.center);
+         this.level.gameEvent(this.source, GameEvent.EXPLODE, this.center);
          List<BlockPos> list = this.calculateExplodedPositions();
- 
-@@ -0,0 +0,0 @@ public class ServerExplosion implements Explosion {
+         this.hurtEntities();
+@@ -384,6 +674,13 @@ public class ServerExplosion implements Explosion {
          if (this.fire) {
              this.createFire(list);
          }
@@ -30886,10 +30848,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        this.directMappedBlockCache = null;
 +        this.mutablePos = null;
 +        // Paper end - collision optimisations
- 
      }
  
-@@ -0,0 +0,0 @@ public class ServerExplosion implements Explosion {
+     private static void addOrAppendStack(List<ServerExplosion.StackCollector> stackCollectors, ItemStack stack, BlockPos pos) {
+@@ -474,12 +771,12 @@ public class ServerExplosion implements Explosion {
      // Paper start - Optimize explosions
      private float getBlockDensity(Vec3 vec3d, Entity entity) {
          if (!this.level.paperConfig().environment.optimizeExplosions) {
@@ -30904,73 +30866,73 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
              this.level.explosionDensityCache.put(key, blockDensity);
          }
  
-diff --git a/src/main/java/net/minecraft/world/level/biome/Biome.java b/src/main/java/net/minecraft/world/level/biome/Biome.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/biome/Biome.java
-+++ b/src/main/java/net/minecraft/world/level/biome/Biome.java
-@@ -0,0 +0,0 @@ public final class Biome {
+diff --git a/net/minecraft/world/level/biome/Biome.java b/net/minecraft/world/level/biome/Biome.java
+index ea521e1d636b8ffdeb22882dfc8875b2ddbd8da1..7b666bbeefe296e7fdbadcc72dbf9e602f73e925 100644
+--- a/net/minecraft/world/level/biome/Biome.java
++++ b/net/minecraft/world/level/biome/Biome.java
+@@ -117,20 +117,7 @@ public final class Biome {
  
      @Deprecated
-     public float getTemperature(BlockPos blockPos, int seaLevel) {
--        long l = blockPos.asLong();
--        Long2FloatLinkedOpenHashMap long2FloatLinkedOpenHashMap = this.temperatureCache.get();
--        float f = long2FloatLinkedOpenHashMap.get(l);
+     public float getTemperature(BlockPos pos, int seaLevel) {
+-        long packedBlockPos = pos.asLong();
+-        Long2FloatLinkedOpenHashMap map = this.temperatureCache.get();
+-        float f = map.get(packedBlockPos);
 -        if (!Float.isNaN(f)) {
 -            return f;
 -        } else {
--            float g = this.getHeightAdjustedTemperature(blockPos, seaLevel);
--            if (long2FloatLinkedOpenHashMap.size() == 1024) {
--                long2FloatLinkedOpenHashMap.removeFirstFloat();
+-            float heightAdjustedTemperature = this.getHeightAdjustedTemperature(pos, seaLevel);
+-            if (map.size() == 1024) {
+-                map.removeFirstFloat();
 -            }
 -
--            long2FloatLinkedOpenHashMap.put(l, g);
--            return g;
+-            map.put(packedBlockPos, heightAdjustedTemperature);
+-            return heightAdjustedTemperature;
 -        }
-+        return this.getHeightAdjustedTemperature(blockPos, seaLevel); // Paper - optimise random ticking
++        return this.getHeightAdjustedTemperature(pos, seaLevel); // Paper - optimise random ticking
      }
  
-     public boolean shouldFreeze(LevelReader world, BlockPos blockPos) {
-diff --git a/src/main/java/net/minecraft/world/level/biome/BiomeManager.java b/src/main/java/net/minecraft/world/level/biome/BiomeManager.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/biome/BiomeManager.java
-+++ b/src/main/java/net/minecraft/world/level/biome/BiomeManager.java
-@@ -0,0 +0,0 @@ public class BiomeManager {
+     public boolean shouldFreeze(LevelReader level, BlockPos pos) {
+diff --git a/net/minecraft/world/level/biome/BiomeManager.java b/net/minecraft/world/level/biome/BiomeManager.java
+index 8d98cba3830dc5dfb5cae9a6f5fedfffee0d2cd8..73962e79a0f3d892e3155443a1b84508b0f4042e 100644
+--- a/net/minecraft/world/level/biome/BiomeManager.java
++++ b/net/minecraft/world/level/biome/BiomeManager.java
+@@ -98,8 +98,7 @@ public class BiomeManager {
      }
  
-     private static double getFiddle(long l) {
--        double d = (double)Math.floorMod(l >> 24, 1024) / 1024.0;
+     private static double getFiddle(long seed) {
+-        double d = Math.floorMod(seed >> 24, 1024) / 1024.0;
 -        return (d - 0.5) * 0.9;
-+        return (double)(((l >> 24) & (1024 - 1)) - (1024/2)) * (0.9 / 1024.0); // Paper - avoid floorMod, fp division, and fp subtraction
++        return (double)(((seed >> 24) & (1024 - 1)) - (1024/2)) * (0.9 / 1024.0); // Paper - avoid floorMod, fp division, and fp subtraction
      }
  
      public interface NoiseBiomeSource {
-diff --git a/src/main/java/net/minecraft/world/level/block/Block.java b/src/main/java/net/minecraft/world/level/block/Block.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/block/Block.java
-+++ b/src/main/java/net/minecraft/world/level/block/Block.java
-@@ -0,0 +0,0 @@ public class Block extends BlockBehaviour implements ItemLike {
+diff --git a/net/minecraft/world/level/block/Block.java b/net/minecraft/world/level/block/Block.java
+index 91d7d250f7c3de8a71aef26e23c12764b06b322b..0d36b1ac7d54283af71f2494accded11c059dba5 100644
+--- a/net/minecraft/world/level/block/Block.java
++++ b/net/minecraft/world/level/block/Block.java
+@@ -259,7 +259,7 @@ public class Block extends BlockBehaviour implements ItemLike {
      }
  
      public static boolean isShapeFullBlock(VoxelShape shape) {
--        return (Boolean) Block.SHAPE_FULL_BLOCK_CACHE.getUnchecked(shape);
+-        return SHAPE_FULL_BLOCK_CACHE.getUnchecked(shape);
 +        return ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$isFullBlock(); // Paper - optimise collisions
      }
  
-     public void animateTick(BlockState state, Level world, BlockPos pos, RandomSource random) {}
-diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
-+++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
-@@ -0,0 +0,0 @@ public abstract class BlockBehaviour implements FeatureElement {
-         boolean test(BlockState state, BlockGetter world, BlockPos pos);
+     public void animateTick(BlockState state, Level level, BlockPos pos, RandomSource random) {
+diff --git a/net/minecraft/world/level/block/state/BlockBehaviour.java b/net/minecraft/world/level/block/state/BlockBehaviour.java
+index 25e49a24cedfa8ad04245d59fcac3231bcd62103..061d94a35d957ca72a01bae959d38aab59b1a64d 100644
+--- a/net/minecraft/world/level/block/state/BlockBehaviour.java
++++ b/net/minecraft/world/level/block/state/BlockBehaviour.java
+@@ -416,7 +416,7 @@ public abstract class BlockBehaviour implements FeatureElement {
+         return this.properties.destroyTime;
      }
  
 -    public abstract static class BlockStateBase extends StateHolder<Block, BlockState> {
 +    public abstract static class BlockStateBase extends StateHolder<Block, BlockState> implements ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState, ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState { // Paper - rewrite chunk system // Paper - optimise collisions
- 
          private static final Direction[] DIRECTIONS = Direction.values();
-         private static final VoxelShape[] EMPTY_OCCLUSION_SHAPES = (VoxelShape[]) Util.make(new VoxelShape[BlockBehaviour.BlockStateBase.DIRECTIONS.length], (avoxelshape) -> {
-@@ -0,0 +0,0 @@ public abstract class BlockBehaviour implements FeatureElement {
+         private static final VoxelShape[] EMPTY_OCCLUSION_SHAPES = Util.make(new VoxelShape[DIRECTIONS.length], shape -> Arrays.fill(shape, Shapes.empty()));
+         private static final VoxelShape[] FULL_BLOCK_OCCLUSION_SHAPES = Util.make(
+@@ -455,6 +455,76 @@ public abstract class BlockBehaviour implements FeatureElement {
          private boolean propagatesSkylightDown;
          private int lightBlock;
  
@@ -31044,13 +31006,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +        // Paper end - optimise collisions
 +
-         protected BlockStateBase(Block block, Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap, MapCodec<BlockState> codec) {
-             super(block, propertyMap, codec);
-             this.fluidState = Fluids.EMPTY.defaultFluidState();
-@@ -0,0 +0,0 @@ public abstract class BlockBehaviour implements FeatureElement {
+         protected BlockStateBase(Block owner, Reference2ObjectArrayMap<Property<?>, Comparable<?>> values, MapCodec<BlockState> propertiesCodec) {
+             super(owner, values, propertiesCodec);
+             BlockBehaviour.Properties properties = owner.properties;
+@@ -533,6 +603,41 @@ public abstract class BlockBehaviour implements FeatureElement {
  
-             this.propagatesSkylightDown = ((Block) this.owner).propagatesSkylightDown(this.asState());
-             this.lightBlock = ((Block) this.owner).getLightBlock(this.asState());
+             this.propagatesSkylightDown = this.owner.propagatesSkylightDown(this.asState());
+             this.lightBlock = this.owner.getLightBlock(this.asState());
 +            // Paper start - rewrite chunk system
 +            this.isConditionallyFullOpaque = this.canOcclude & this.useShapeForLightOcclusion;
 +            // Paper end - rewrite chunk system
@@ -31089,11 +31051,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          }
  
          public Block getBlock() {
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ import java.util.stream.Collectors;
+diff --git a/net/minecraft/world/level/block/state/StateHolder.java b/net/minecraft/world/level/block/state/StateHolder.java
+index 2f2dbf02a9732a7e640a6c730d4fc1443e723933..098518383d2c07491e047749ce3a834e98b85b1d 100644
+--- a/net/minecraft/world/level/block/state/StateHolder.java
++++ b/net/minecraft/world/level/block/state/StateHolder.java
+@@ -15,7 +15,7 @@ import java.util.stream.Collectors;
  import javax.annotation.Nullable;
  import net.minecraft.world.level.block.state.properties.Property;
  
@@ -31102,7 +31064,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      public static final String NAME_TAG = "Name";
      public static final String PROPERTIES_TAG = "Properties";
      public static final Function<Entry<Property<?>, Comparable<?>>, String> PROPERTY_ENTRY_TO_STRING_FUNCTION = new Function<Entry<Property<?>, Comparable<?>>, String>() {
-@@ -0,0 +0,0 @@ public abstract class StateHolder<O, S> {
+@@ -34,14 +34,28 @@ public abstract class StateHolder<O, S> {
          }
      };
      protected final O owner;
@@ -31121,10 +31083,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - optimise blockstate property access
 +
-     protected StateHolder(O owner, Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap, MapCodec<S> codec) {
+     protected StateHolder(O owner, Reference2ObjectArrayMap<Property<?>, Comparable<?>> values, MapCodec<S> propertiesCodec) {
          this.owner = owner;
-         this.values = propertyMap;
-         this.propertiesCodec = codec;
+         this.values = values;
+         this.propertiesCodec = propertiesCodec;
 +        // Paper start - optimise blockstate property access
 +        this.optimisedTable = new ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util.ZeroCollidingReferenceStateTable<>(this.values.keySet());
 +        this.tableIndex = this.optimisedTable.getIndex((StateHolder<O, S>)(Object)this);
@@ -31132,7 +31094,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      public <T extends Comparable<T>> S cycle(Property<T> property) {
-@@ -0,0 +0,0 @@ public abstract class StateHolder<O, S> {
+@@ -67,20 +81,21 @@ public abstract class StateHolder<O, S> {
      }
  
      public Collection<Property<?>> getProperties() {
@@ -31161,7 +31123,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      public <T extends Comparable<T>> Optional<T> getOptionalValue(Property<T> property) {
-@@ -0,0 +0,0 @@ public abstract class StateHolder<O, S> {
+@@ -93,22 +108,30 @@ public abstract class StateHolder<O, S> {
  
      @Nullable
      public <T extends Comparable<T>> T getNullableValue(Property<T> property) {
@@ -31200,45 +31162,49 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - optimise blockstate property access
      }
  
-     private <T extends Comparable<T>, V extends T> S setValueInternal(Property<T> property, V newValue, Comparable<?> oldValue) {
-@@ -0,0 +0,0 @@ public abstract class StateHolder<O, S> {
+     private <T extends Comparable<T>, V extends T> S setValueInternal(Property<T> property, V value, Comparable<?> comparable) {
+@@ -125,21 +148,27 @@ public abstract class StateHolder<O, S> {
      }
  
-     public void populateNeighbours(Map<Map<Property<?>, Comparable<?>>, S> states) {
+     public void populateNeighbours(Map<Map<Property<?>, Comparable<?>>, S> possibleStateMap) {
 -        if (this.neighbours != null) {
 -            throw new IllegalStateException();
 -        } else {
 -            Map<Property<?>, S[]> map = new Reference2ObjectArrayMap<>(this.values.size());
+-
+-            for (Entry<Property<?>, Comparable<?>> entry : this.values.entrySet()) {
+-                Property<?> property = entry.getKey();
+-                map.put(
+-                    property,
+-                    (S[]) property.getPossibleValues().stream().map(comparable -> possibleStateMap.get(this.makeNeighbourValues(property, comparable))).toArray()
+-                );
+-            }
 +        // Paper start - optimise blockstate property access
-+        final Map<Map<Property<?>, Comparable<?>>, S> map = states;
++        final Map<Map<Property<?>, Comparable<?>>, S> map = possibleStateMap;
 +        if (this.optimisedTable.isLoaded()) {
 +            return;
 +        }
 +        this.optimisedTable.loadInTable(map);
  
--            for (Entry<Property<?>, Comparable<?>> entry : this.values.entrySet()) {
--                Property<?> property = entry.getKey();
--                map.put(property, property.getPossibleValues().stream().map(value -> states.get(this.makeNeighbourValues(property, value))).toArray());
--            }
+-            this.neighbours = map;
 +        // de-duplicate the tables
 +        for (final Map.Entry<Map<Property<?>, Comparable<?>>, S> entry : map.entrySet()) {
 +            final S value = entry.getValue();
 +            ((StateHolder<O, S>)value).optimisedTable = this.optimisedTable;
-+        }
- 
--            this.neighbours = map;
+         }
++
 +        // remove values arrays
 +        for (final Map.Entry<Map<Property<?>, Comparable<?>>, S> entry : map.entrySet()) {
 +            final S value = entry.getValue();
 +            ((StateHolder<O, S>)value).values = null;
-         }
++        }
 +
 +        return;
 +        // Paper end  optimise blockstate property access
      }
  
      private Map<Property<?>, Comparable<?>> makeNeighbourValues(Property<?> property, Comparable<?> value) {
-@@ -0,0 +0,0 @@ public abstract class StateHolder<O, S> {
+@@ -149,7 +178,11 @@ public abstract class StateHolder<O, S> {
      }
  
      public Map<Property<?>, Comparable<?>> getValues() {
@@ -31250,12 +31216,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - optimise blockstate property access
      }
  
-     protected static <O, S extends StateHolder<O, S>> Codec<S> codec(Codec<O> codec, Function<O, S> ownerToStateFunction) {
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ package net.minecraft.world.level.block.state.properties;
+     protected static <O, S extends StateHolder<O, S>> Codec<S> codec(Codec<O> propertyMap, Function<O, S> holderFunction) {
+diff --git a/net/minecraft/world/level/block/state/properties/BooleanProperty.java b/net/minecraft/world/level/block/state/properties/BooleanProperty.java
+index 40c83ff614169be8ab988f3ab476eca93acee28d..654f14e0fd1920ec94300649719c2460918899e2 100644
+--- a/net/minecraft/world/level/block/state/properties/BooleanProperty.java
++++ b/net/minecraft/world/level/block/state/properties/BooleanProperty.java
+@@ -3,13 +3,23 @@ package net.minecraft.world.level.block.state.properties;
  import java.util.List;
  import java.util.Optional;
  
@@ -31280,11 +31246,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      @Override
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ import java.util.function.Predicate;
+diff --git a/net/minecraft/world/level/block/state/properties/EnumProperty.java b/net/minecraft/world/level/block/state/properties/EnumProperty.java
+index c56728863a084a5e1f6e6d9489d00bb0c83af168..1d785b7bb046ef291342efa3ede6cdeb460f12fb 100644
+--- a/net/minecraft/world/level/block/state/properties/EnumProperty.java
++++ b/net/minecraft/world/level/block/state/properties/EnumProperty.java
+@@ -10,11 +10,39 @@ import java.util.function.Predicate;
  import java.util.stream.Collectors;
  import net.minecraft.util.StringRepresentable;
  
@@ -31322,10 +31288,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - optimise blockstate property access
 +
-     private EnumProperty(String name, Class<T> type, List<T> values) {
-         super(name, type);
+     private EnumProperty(String name, Class<T> clazz, List<T> values) {
+         super(name, clazz);
          if (values.isEmpty()) {
-@@ -0,0 +0,0 @@ public final class EnumProperty<T extends Enum<T> & StringRepresentable> extends
+@@ -37,6 +65,7 @@ public final class EnumProperty<T extends Enum<T> & StringRepresentable> extends
  
              this.names = builder.buildOrThrow();
          }
@@ -31333,11 +31299,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      @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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ import java.util.List;
+diff --git a/net/minecraft/world/level/block/state/properties/IntegerProperty.java b/net/minecraft/world/level/block/state/properties/IntegerProperty.java
+index 28a15908420cb239c317d58f7e3a1df3c6278b33..b7543eb5a8f87bc7bd275ed9d46a68072c1e57b5 100644
+--- a/net/minecraft/world/level/block/state/properties/IntegerProperty.java
++++ b/net/minecraft/world/level/block/state/properties/IntegerProperty.java
+@@ -5,11 +5,33 @@ import java.util.List;
  import java.util.Optional;
  import java.util.stream.IntStream;
  
@@ -31372,7 +31338,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      private IntegerProperty(String name, int min, int max) {
          super(name, Integer.class);
          if (min < 0) {
-@@ -0,0 +0,0 @@ public final class IntegerProperty extends Property<Integer> {
+@@ -21,6 +43,7 @@ public final class IntegerProperty extends Property<Integer> {
              this.max = max;
              this.values = IntImmutableList.toList(IntStream.range(min, max + 1));
          }
@@ -31380,11 +31346,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      @Override
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ import java.util.stream.Stream;
+diff --git a/net/minecraft/world/level/block/state/properties/Property.java b/net/minecraft/world/level/block/state/properties/Property.java
+index 92350434746f06bbf4a161c6bc42602de7b45220..1c24f38d21da1be9740512981f219924c5d3cf76 100644
+--- a/net/minecraft/world/level/block/state/properties/Property.java
++++ b/net/minecraft/world/level/block/state/properties/Property.java
+@@ -10,7 +10,7 @@ import java.util.stream.Stream;
  import javax.annotation.Nullable;
  import net.minecraft.world.level.block.state.StateHolder;
  
@@ -31393,7 +31359,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      private final Class<T> clazz;
      private final String name;
      @Nullable
-@@ -0,0 +0,0 @@ public abstract class Property<T extends Comparable<T>> {
+@@ -24,9 +24,38 @@ public abstract class Property<T extends Comparable<T>> {
          );
      private final Codec<Property.Value<T>> valueCodec = this.codec.xmap(this::value, Property.Value::value);
  
@@ -31425,27 +31391,27 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public abstract int moonrise$getIdFor(final T value);
 +    // Paper end - optimise blockstate property access
 +
-     protected Property(String name, Class<T> type) {
-         this.clazz = type;
+     protected Property(String name, Class<T> clazz) {
+         this.clazz = clazz;
          this.name = name;
 +        this.id = ID_GENERATOR.getAndIncrement(); // Paper - optimise blockstate property access
      }
  
      public Property.Value<T> value(T value) {
-diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
-@@ -0,0 +0,0 @@ import net.minecraft.world.ticks.SavedTick;
+diff --git a/net/minecraft/world/level/chunk/ChunkAccess.java b/net/minecraft/world/level/chunk/ChunkAccess.java
+index 860d1c9729c4ee97ec6f40f7aa969829070b27c0..94de1217d18e1a7a0fb7b83f21436eaf0a5998c6 100644
+--- a/net/minecraft/world/level/chunk/ChunkAccess.java
++++ b/net/minecraft/world/level/chunk/ChunkAccess.java
+@@ -57,7 +57,7 @@ import net.minecraft.world.ticks.SavedTick;
  import net.minecraft.world.ticks.TickContainerAccess;
  import org.slf4j.Logger;
  
 -public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, LightChunk, StructureAccess {
 +public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, LightChunk, StructureAccess, ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk { // Paper - rewrite chunk system
- 
      public static final int NO_FILLED_SECTION = -1;
      private static final Logger LOGGER = LogUtils.getLogger();
-@@ -0,0 +0,0 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh
+     private static final LongSet EMPTY_REFERENCE_SET = new LongOpenHashSet();
+@@ -75,7 +75,7 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh
      @Nullable
      protected BlendingData blendingData;
      public final Map<Heightmap.Types, Heightmap> heightmaps = Maps.newEnumMap(Heightmap.Types.class);
@@ -31454,9 +31420,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      private final Map<Structure, StructureStart> structureStarts = Maps.newHashMap();
      private final Map<Structure, LongSet> structuresRefences = Maps.newHashMap();
      protected final Map<BlockPos, CompoundTag> pendingBlockEntities = Maps.newHashMap();
-@@ -0,0 +0,0 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh
-     public org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer persistentDataContainer = new org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer(ChunkAccess.DATA_TYPE_REGISTRY);
+@@ -88,6 +88,57 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh
      // CraftBukkit end
+     public final Registry<Biome> biomeRegistry; // CraftBukkit
  
 +    // Paper start - rewrite chunk system
 +    private volatile ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] blockNibbles;
@@ -31509,26 +31475,26 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    private final int maxSection;
 +    // Paper end - get block chunk optimisation
 +
-     public ChunkAccess(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor heightLimitView, Registry<Biome> biomeRegistry, long inhabitedTime, @Nullable LevelChunkSection[] sectionArray, @Nullable BlendingData blendingData) {
-         this.locX = pos.x; this.locZ = pos.z; // Paper - reduce need for field lookups
-         this.chunkPos = pos; this.coordinateKey = ChunkPos.asLong(locX, locZ); // Paper - cache long key
-@@ -0,0 +0,0 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh
+     public ChunkAccess(
+         ChunkPos chunkPos,
+         UpgradeData upgradeData,
+@@ -105,7 +156,7 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh
          this.inhabitedTime = inhabitedTime;
-         this.postProcessing = new ShortList[heightLimitView.getSectionsCount()];
+         this.postProcessing = new ShortList[levelHeightAccessor.getSectionsCount()];
          this.blendingData = blendingData;
--        this.skyLightSources = new ChunkSkyLightSources(heightLimitView);
+-        this.skyLightSources = new ChunkSkyLightSources(levelHeightAccessor);
 +        // Paper - rewrite chunk system
-         if (sectionArray != null) {
-             if (this.sections.length == sectionArray.length) {
-                 System.arraycopy(sectionArray, 0, this.sections, 0, this.sections.length);
-@@ -0,0 +0,0 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh
+         if (sections != null) {
+             if (this.sections.length == sections.length) {
+                 System.arraycopy(sections, 0, this.sections, 0, this.sections.length);
+@@ -116,6 +167,16 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh
+ 
          this.replaceMissingSections(biomeRegistry, this.sections); // Paper - Anti-Xray - make it a non-static method
-         // CraftBukkit start
-         this.biomeRegistry = biomeRegistry;
+         this.biomeRegistry = biomeRegistry; // CraftBukkit
 +        // Paper start - rewrite chunk system
 +        if (!((Object)this instanceof ImposterProtoChunk)) {
-+            this.starlight$setBlockNibbles(ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(heightLimitView));
-+            this.starlight$setSkyNibbles(ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(heightLimitView));
++            this.starlight$setBlockNibbles(ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(levelHeightAccessor));
++            this.starlight$setSkyNibbles(ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(levelHeightAccessor));
 +        }
 +        // Paper end - rewrite chunk system
 +        // Paper start - get block chunk optimisation
@@ -31536,30 +31502,26 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        this.maxSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(levelHeightAccessor);
 +        // Paper end - get block chunk optimisation
      }
-     public final Registry<Biome> biomeRegistry;
-     // CraftBukkit end
-@@ -0,0 +0,0 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh
+ 
+     private void replaceMissingSections(Registry<Biome> biomeRegistry, LevelChunkSection[] sections) { // Paper - Anti-Xray - make it a non-static method
+@@ -442,18 +503,22 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh
  
      @Override
-     public Holder<Biome> getNoiseBiome(int biomeX, int biomeY, int biomeZ) {
+     public Holder<Biome> getNoiseBiome(int x, int y, int z) {
 -        try {
--            int l = QuartPos.fromBlock(this.getMinY());
--            int i1 = l + QuartPos.fromBlock(this.getHeight()) - 1;
--            int j1 = Mth.clamp(biomeY, l, i1);
--            int k1 = this.getSectionIndex(QuartPos.toBlock(j1));
--
--            return this.sections[k1].getNoiseBiome(biomeX & 3, j1 & 3, biomeZ & 3);
--        } catch (Throwable throwable) {
--            CrashReport crashreport = CrashReport.forThrowable(throwable, "Getting biome");
--            CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Biome being got");
--
--            crashreportsystemdetails.setDetail("Location", () -> {
--                return CrashReportCategory.formatLocation(this, biomeX, biomeY, biomeZ);
--            });
--            throw new ReportedException(crashreport);
+-            int quartPosMinY = QuartPos.fromBlock(this.getMinY());
+-            int i = quartPosMinY + QuartPos.fromBlock(this.getHeight()) - 1;
+-            int i1 = Mth.clamp(y, quartPosMinY, i);
+-            int sectionIndex = this.getSectionIndex(QuartPos.toBlock(i1));
+-            return this.sections[sectionIndex].getNoiseBiome(x & 3, i1 & 3, z & 3);
+-        } catch (Throwable var8) {
+-            CrashReport crashReport = CrashReport.forThrowable(var8, "Getting biome");
+-            CrashReportCategory crashReportCategory = crashReport.addCategory("Biome being got");
+-            crashReportCategory.setDetail("Location", () -> CrashReportCategory.formatLocation(this, x, y, z));
+-            throw new ReportedException(crashReport);
 +        // Paper start - get block chunk optimisation
-+        int sectionY = (biomeY >> 2) - this.minSection;
-+        int rel = biomeY & 3;
++        int sectionY = (y >> 2) - this.minSection;
++        int rel = y & 3;
 +
 +        final LevelChunkSection[] sections = this.sections;
 +
@@ -31571,12 +31533,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            rel = 3;
          }
 +
-+        return sections[sectionY].getNoiseBiome(biomeX & 3, rel, biomeZ & 3);
++        return sections[sectionY].getNoiseBiome(x & 3, rel, z & 3);
 +        // Paper end - get block chunk optimisation
      }
- 
      // CraftBukkit start
-@@ -0,0 +0,0 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh
+     public void setBiome(int i, int j, int k, Holder<Biome> biome) {
+@@ -507,12 +572,12 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh
      }
  
      public void initializeLightSources() {
@@ -31590,34 +31552,34 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return null; // Paper - rewrite chunk system
      }
  
-     public static record PackedTicks(List<SavedTick<Block>> blocks, List<SavedTick<Fluid>> fluids) {
-diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java
-@@ -0,0 +0,0 @@ public abstract class ChunkGenerator {
+     public record PackedTicks(List<SavedTick<Block>> blocks, List<SavedTick<Fluid>> fluids) {
+diff --git a/net/minecraft/world/level/chunk/ChunkGenerator.java b/net/minecraft/world/level/chunk/ChunkGenerator.java
+index 65117a9cb9d1b8684cae8d36ea3b8e2050fb928c..a9d65e28b849c9660a14ef7c16ed17bd5182bd7e 100644
+--- a/net/minecraft/world/level/chunk/ChunkGenerator.java
++++ b/net/minecraft/world/level/chunk/ChunkGenerator.java
+@@ -116,7 +116,7 @@ public abstract class ChunkGenerator {
          return CompletableFuture.supplyAsync(() -> {
-             chunk.fillBiomesFromNoise(this.biomeSource, noiseConfig.sampler());
+             chunk.fillBiomesFromNoise(this.biomeSource, randomState.sampler());
              return chunk;
 -        }, Util.backgroundExecutor().forName("init_biomes"));
 +        }, Runnable::run);  // Paper - rewrite chunk system
      }
  
-     public abstract void applyCarvers(WorldGenRegion chunkRegion, long seed, RandomState noiseConfig, BiomeManager biomeAccess, StructureManager structureAccessor, ChunkAccess chunk);
-@@ -0,0 +0,0 @@ public abstract class ChunkGenerator {
-                         return Pair.of(placement.getLocatePos(pos), holder);
-                     }
+     public abstract void applyCarvers(
+@@ -315,7 +315,7 @@ public abstract class ChunkGenerator {
+                     return Pair.of(placement.getLocatePos(chunkPos), holder);
+                 }
  
--                    ChunkAccess ichunkaccess = world.getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS);
-+                    ChunkAccess ichunkaccess = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevelReader)world).moonrise$syncLoadNonFull(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS); // Paper - rewrite chunk system
- 
-                     structurestart = structureAccessor.getStartForStructure(SectionPos.bottomOf(ichunkaccess), (Structure) holder.value(), ichunkaccess);
-                 } while (structurestart == null);
-diff --git a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
-@@ -0,0 +0,0 @@ import net.minecraft.world.level.block.state.BlockState;
+-                ChunkAccess chunk = level.getChunk(chunkPos.x, chunkPos.z, ChunkStatus.STRUCTURE_STARTS);
++                ChunkAccess chunk = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevelReader)level).moonrise$syncLoadNonFull(chunkPos.x, chunkPos.z, ChunkStatus.STRUCTURE_STARTS); // Paper - rewrite chunk system
+                 StructureStart startForStructure = structureManager.getStartForStructure(SectionPos.bottomOf(chunk), holder.value(), chunk);
+                 if (startForStructure != null && startForStructure.isValid() && (!skipKnownStructures || tryAddReference(structureManager, startForStructure))) {
+                     return Pair.of(placement.getLocatePos(startForStructure.getChunkPos()), holder);
+diff --git a/net/minecraft/world/level/chunk/EmptyLevelChunk.java b/net/minecraft/world/level/chunk/EmptyLevelChunk.java
+index ec128412e4a0d3d21e3b6abea8cd06c03656f00c..07b7e82c7d24f52c0251e09195451841d47883c9 100644
+--- a/net/minecraft/world/level/chunk/EmptyLevelChunk.java
++++ b/net/minecraft/world/level/chunk/EmptyLevelChunk.java
+@@ -13,7 +13,7 @@ import net.minecraft.world.level.block.state.BlockState;
  import net.minecraft.world.level.material.FluidState;
  import net.minecraft.world.level.material.Fluids;
  
@@ -31625,9 +31587,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +public class EmptyLevelChunk extends LevelChunk implements ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk { // Paper - rewrite chunk system
      private final Holder<Biome> biome;
  
-     public EmptyLevelChunk(Level world, ChunkPos pos, Holder<Biome> biomeEntry) {
-@@ -0,0 +0,0 @@ public class EmptyLevelChunk extends LevelChunk {
-         this.biome = biomeEntry;
+     public EmptyLevelChunk(Level level, ChunkPos pos, Holder<Biome> biome) {
+@@ -21,6 +21,40 @@ public class EmptyLevelChunk extends LevelChunk {
+         this.biome = biome;
      }
  
 +    // Paper start - rewrite chunk system
@@ -31667,11 +31629,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      @Override
      public BlockState getBlockState(BlockPos pos) {
          return Blocks.VOID_AIR.defaultBlockState();
-diff --git a/src/main/java/net/minecraft/world/level/chunk/HashMapPalette.java b/src/main/java/net/minecraft/world/level/chunk/HashMapPalette.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/HashMapPalette.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/HashMapPalette.java
-@@ -0,0 +0,0 @@ import net.minecraft.network.FriendlyByteBuf;
+diff --git a/net/minecraft/world/level/chunk/HashMapPalette.java b/net/minecraft/world/level/chunk/HashMapPalette.java
+index 7cd5d42e0c28033ee80f18bd0031ed1241fb7aae..718d00a386f32423db9f6d6c95b4a20698b976f5 100644
+--- a/net/minecraft/world/level/chunk/HashMapPalette.java
++++ b/net/minecraft/world/level/chunk/HashMapPalette.java
+@@ -8,12 +8,19 @@ import net.minecraft.network.FriendlyByteBuf;
  import net.minecraft.network.VarInt;
  import net.minecraft.util.CrudeIncrementalIntIdentityHashBiMap;
  
@@ -31689,14 +31651,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - optimise palette reads
 +
-     public HashMapPalette(IdMap<T> idList, int bits, PaletteResize<T> listener, List<T> entries) {
-         this(idList, bits, listener);
-         entries.forEach(this.values::add);
-diff --git a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
-@@ -0,0 +0,0 @@ import net.minecraft.world.level.material.FluidState;
+     public HashMapPalette(IdMap<T> registry, int bits, PaletteResize<T> resizeHandler, List<T> values) {
+         this(registry, bits, resizeHandler);
+         values.forEach(this.values::add);
+diff --git a/net/minecraft/world/level/chunk/ImposterProtoChunk.java b/net/minecraft/world/level/chunk/ImposterProtoChunk.java
+index e7c0f4da8508fbca467326f475668d66454d7b77..41856c98d97e7eb0782f8e441b9a269a47ed1914 100644
+--- a/net/minecraft/world/level/chunk/ImposterProtoChunk.java
++++ b/net/minecraft/world/level/chunk/ImposterProtoChunk.java
+@@ -30,7 +30,7 @@ import net.minecraft.world.level.material.FluidState;
  import net.minecraft.world.ticks.BlackholeTickAccess;
  import net.minecraft.world.ticks.TickContainerAccess;
  
@@ -31705,8 +31667,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      private final LevelChunk wrapped;
      private final boolean allowWrites;
  
-@@ -0,0 +0,0 @@ public class ImposterProtoChunk extends ProtoChunk {
-         this.allowWrites = propagateToWrapped;
+@@ -46,6 +46,48 @@ public class ImposterProtoChunk extends ProtoChunk {
+         this.allowWrites = allowWrites;
      }
  
 +    // Paper start - rewrite chunk system
@@ -31754,35 +31716,20 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      @Nullable
      @Override
      public BlockEntity getBlockEntity(BlockPos pos) {
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
-@@ -0,0 +0,0 @@ import net.minecraft.world.ticks.LevelChunkTicks;
+diff --git a/net/minecraft/world/level/chunk/LevelChunk.java b/net/minecraft/world/level/chunk/LevelChunk.java
+index 96b0342ab7b922aa16d07b6c00542e6cb66c974a..c1ae7755e8d6fa8501d2210dab7605d993c55722 100644
+--- a/net/minecraft/world/level/chunk/LevelChunk.java
++++ b/net/minecraft/world/level/chunk/LevelChunk.java
+@@ -52,7 +52,7 @@ import net.minecraft.world.ticks.LevelChunkTicks;
  import net.minecraft.world.ticks.TickContainerAccess;
  import org.slf4j.Logger;
  
 -public class LevelChunk extends ChunkAccess {
 +public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk, ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk, ca.spottedleaf.moonrise.patches.getblock.GetBlockChunk { // Paper - rewrite chunk system // Paper - get block chunk optimisation
- 
      static final Logger LOGGER = LogUtils.getLogger();
      private static final TickingBlockEntity NULL_TICKER = new TickingBlockEntity() {
-@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess {
-         this.postLoad = entityLoader;
-         this.blockTicks = blockTickScheduler;
-         this.fluidTicks = fluidTickScheduler;
-+        // Paper start - get block chunk optimisation
-+        this.minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(level);
-+        this.maxSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(level);
-+
-+        final boolean empty = ((Object)this instanceof EmptyLevelChunk);
-+        this.debug = !empty && this.level.isDebug();
-+        this.defaultBlockState = empty ? VOID_AIR_BLOCKSTATE : AIR_BLOCKSTATE;
-+        // Paper end - get block chunk optimisation
-     }
- 
-     // CraftBukkit start
-@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess {
+         @Override
+@@ -93,6 +93,39 @@ public class LevelChunk extends ChunkAccess {
      // Paper start
      boolean loadedTicketLevel;
      // Paper end
@@ -31820,39 +31767,54 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - get block chunk optimisation
  
-     public LevelChunk(ServerLevel world, ProtoChunk protoChunk, @Nullable LevelChunk.PostLoadProcessor entityLoader) {
-         this(world, protoChunk.getPos(), protoChunk.getUpgradeData(), protoChunk.unpackBlockTicks(), protoChunk.unpackFluidTicks(), protoChunk.getInhabitedTime(), protoChunk.getSections(), entityLoader, protoChunk.getBlendingData());
-@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess {
+     public LevelChunk(Level level, ChunkPos pos) {
+         this(level, pos, UpgradeData.EMPTY, new LevelChunkTicks<>(), new LevelChunkTicks<>(), 0L, null, null, null);
+@@ -122,6 +155,14 @@ public class LevelChunk extends ChunkAccess {
+         this.postLoad = postLoad;
+         this.blockTicks = blockTicks;
+         this.fluidTicks = fluidTicks;
++        // Paper start - get block chunk optimisation
++        this.minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(level);
++        this.maxSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(level);
++
++        final boolean empty = ((Object)this instanceof EmptyLevelChunk);
++        this.debug = !empty && this.level.isDebug();
++        this.defaultBlockState = empty ? VOID_AIR_BLOCKSTATE : AIR_BLOCKSTATE;
++        // Paper end - get block chunk optimisation
+     }
+ 
+     public LevelChunk(ServerLevel level, ProtoChunk chunk, @Nullable LevelChunk.PostLoadProcessor postLoad) {
+@@ -159,13 +200,19 @@ public class LevelChunk extends ChunkAccess {
              }
          }
  
--        this.skyLightSources = protoChunk.skyLightSources;
+-        this.skyLightSources = chunk.skyLightSources;
 +        // Paper - rewrite chunk system
-         this.setLightCorrect(protoChunk.isLightCorrect());
+         this.setLightCorrect(chunk.isLightCorrect());
          this.markUnsaved();
          this.needsDecoration = true; // CraftBukkit
          // CraftBukkit start
-         this.persistentDataContainer = protoChunk.persistentDataContainer; // SPIGOT-6814: copy PDC to account for 1.17 to 1.18 chunk upgrading.
+         this.persistentDataContainer = chunk.persistentDataContainer; // SPIGOT-6814: copy PDC to account for 1.17 to 1.18 chunk upgrading.
          // CraftBukkit end
 +        // Paper start - rewrite chunk system
-+        this.starlight$setBlockNibbles(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)protoChunk).starlight$getBlockNibbles());
-+        this.starlight$setSkyNibbles(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)protoChunk).starlight$getSkyNibbles());
-+        this.starlight$setSkyEmptinessMap(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)protoChunk).starlight$getSkyEmptinessMap());
-+        this.starlight$setBlockEmptinessMap(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)protoChunk).starlight$getBlockEmptinessMap());
++        this.starlight$setBlockNibbles(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)chunk).starlight$getBlockNibbles());
++        this.starlight$setSkyNibbles(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)chunk).starlight$getSkyNibbles());
++        this.starlight$setSkyEmptinessMap(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)chunk).starlight$getSkyEmptinessMap());
++        this.starlight$setBlockEmptinessMap(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)chunk).starlight$getBlockEmptinessMap());
 +        // Paper end - rewrite chunk system
      }
  
      public void setUnsavedListener(LevelChunk.UnsavedListener unsavedListener) {
-@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess {
-                     ProfilerFiller gameprofilerfiller = Profiler.get();
- 
-                     gameprofilerfiller.push("updateSkyLightSources");
--                    this.skyLightSources.update(this, j, i, l);
+@@ -341,7 +388,7 @@ public class LevelChunk extends ChunkAccess {
+                 if (LightEngine.hasDifferentLightProperties(blockState, state)) {
+                     ProfilerFiller profilerFiller = Profiler.get();
+                     profilerFiller.push("updateSkyLightSources");
+-                    this.skyLightSources.update(this, i, y, i2);
 +                    // Paper - rewrite chunk system
-                     gameprofilerfiller.popPush("queueCheckLight");
-                     this.level.getChunkSource().getLightEngine().checkBlock(blockposition);
-                     gameprofilerfiller.pop();
-@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess {
+                     profilerFiller.popPush("queueCheckLight");
+                     this.level.getChunkSource().getLightEngine().checkBlock(pos);
+                     profilerFiller.pop();
+@@ -573,11 +620,12 @@ public class LevelChunk extends ChunkAccess {
  
      // CraftBukkit start
      public void loadCallback() {
@@ -31866,7 +31828,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          if (server != null) {
              /*
               * If it's a new world, the first few chunks are generated inside
-@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess {
+@@ -586,6 +634,7 @@ public class LevelChunk extends ChunkAccess {
               */
              org.bukkit.Chunk bukkitChunk = new org.bukkit.craftbukkit.CraftChunk(this);
              server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkLoadEvent(bukkitChunk, this.needsDecoration));
@@ -31874,7 +31836,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
              if (this.needsDecoration) {
                  this.needsDecoration = false;
-@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess {
+@@ -612,13 +661,15 @@ public class LevelChunk extends ChunkAccess {
      }
  
      public void unloadCallback() {
@@ -31892,7 +31854,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          // Paper start
          this.loadedTicketLevel = false;
          // Paper end
-@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess {
+@@ -626,8 +677,31 @@ public class LevelChunk extends ChunkAccess {
  
      @Override
      public boolean isUnsaved() {
@@ -31925,7 +31887,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      // CraftBukkit end
  
      public boolean isEmpty() {
-@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess {
+@@ -706,6 +780,7 @@ public class LevelChunk extends ChunkAccess {
  
          this.pendingBlockEntities.clear();
          this.upgradeData.upgrade(this);
@@ -31933,20 +31895,20 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      @Nullable
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
-@@ -0,0 +0,0 @@ import net.minecraft.world.level.block.Blocks;
+diff --git a/net/minecraft/world/level/chunk/LevelChunkSection.java b/net/minecraft/world/level/chunk/LevelChunkSection.java
+index fc21c3268c4b4fda2933d71f0913db28e3796653..2ff691bcd32f587e8add2d8eda7e7339ccbde6e8 100644
+--- a/net/minecraft/world/level/chunk/LevelChunkSection.java
++++ b/net/minecraft/world/level/chunk/LevelChunkSection.java
+@@ -13,7 +13,7 @@ import net.minecraft.world.level.block.Blocks;
  import net.minecraft.world.level.block.state.BlockState;
  import net.minecraft.world.level.material.FluidState;
  
 -public class LevelChunkSection {
 +public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection { // Paper - block counting
- 
      public static final int SECTION_WIDTH = 16;
      public static final int SECTION_HEIGHT = 16;
-@@ -0,0 +0,0 @@ public class LevelChunkSection {
+     public static final int SECTION_SIZE = 4096;
+@@ -24,6 +24,30 @@ public class LevelChunkSection {
      public final PalettedContainer<BlockState> states;
      private PalettedContainer<Holder<Biome>> biomes; // CraftBukkit - read/write
  
@@ -31977,7 +31939,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      private LevelChunkSection(LevelChunkSection section) {
          this.nonEmptyBlockCount = section.nonEmptyBlockCount;
          this.tickingBlockCount = section.tickingBlockCount;
-@@ -0,0 +0,0 @@ public class LevelChunkSection {
+@@ -69,6 +93,45 @@ public class LevelChunkSection {
          return this.setBlockState(x, y, z, state, true);
      }
  
@@ -32020,37 +31982,48 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - block counting
 +
-     public BlockState setBlockState(int x, int y, int z, BlockState state, boolean lock) {
-         BlockState iblockdata1;
- 
-@@ -0,0 +0,0 @@ public class LevelChunkSection {
+     public BlockState setBlockState(int x, int y, int z, BlockState state, boolean useLocks) {
+         BlockState blockState;
+         if (useLocks) {
+@@ -86,7 +149,7 @@ public class LevelChunkSection {
              }
          }
  
--        if (!fluid.isEmpty()) {
-+        if (!!fluid.isRandomlyTicking()) { // Paper - block counting
-             --this.tickingFluidCount;
+-        if (!fluidState.isEmpty()) {
++        if (!!fluidState.isRandomlyTicking()) { // Paper - block counting
+             this.tickingFluidCount--;
          }
  
-@@ -0,0 +0,0 @@ public class LevelChunkSection {
+@@ -97,10 +160,12 @@ public class LevelChunkSection {
              }
          }
  
--        if (!fluid1.isEmpty()) {
-+        if (!!fluid1.isRandomlyTicking()) { // Paper - block counting
-             ++this.tickingFluidCount;
+-        if (!fluidState1.isEmpty()) {
++        if (!!fluidState1.isRandomlyTicking()) { // Paper - block counting
+             this.tickingFluidCount++;
          }
  
-+        this.updateBlockCallback(x, y, z, state, iblockdata1); // Paper - block counting
++        this.updateBlockCallback(x, y, z, state, blockState); // Paper - block counting
 +
-         return iblockdata1;
+         return blockState;
      }
  
-@@ -0,0 +0,0 @@ public class LevelChunkSection {
+@@ -121,35 +186,70 @@ public class LevelChunkSection {
      }
  
      public void recalcBlockCounts() {
--        class a implements PalettedContainer.CountConsumer<BlockState> {
+-        class BlockCounter implements PalettedContainer.CountConsumer<BlockState> {
+-            public int nonEmptyBlockCount;
+-            public int tickingBlockCount;
+-            public int tickingFluidCount;
+-
+-            @Override
+-            public void accept(BlockState state, int count) {
+-                FluidState fluidState = state.getFluidState();
+-                if (!state.isAir()) {
+-                    this.nonEmptyBlockCount += count;
+-                    if (state.isRandomlyTicking()) {
+-                        this.tickingBlockCount += count;
 +        // Paper start - block counting
 +        // reset, then recalculate
 +        this.nonEmptyBlockCount = (short)0;
@@ -32078,13 +32051,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                final int paletteIdx = entry.getIntKey();
 +                final it.unimi.dsi.fastutil.shorts.ShortArrayList coordinates = entry.getValue();
 +                final int paletteCount = coordinates.size();
- 
--            public int nonEmptyBlockCount;
--            public int tickingBlockCount;
--            public int tickingFluidCount;
++
 +                final BlockState state = palette.valueFor(paletteIdx);
- 
--            a(final LevelChunkSection chunksection) {}
++
 +                if (state.isAir()) {
 +                    continue;
 +                }
@@ -32099,49 +32068,43 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                    final int rawLen = raw.length;
 +
 +                    final ca.spottedleaf.moonrise.common.list.ShortList tickingBlocks = this.tickingBlocks;
- 
--            public void accept(BlockState iblockdata, int i) {
--                FluidState fluid = iblockdata.getFluidState();
++
 +                    tickingBlocks.setMinCapacity(Math.min((rawLen + tickingBlocks.size()) * 3 / 2, 16*16*16));
- 
--                if (!iblockdata.isAir()) {
--                    this.nonEmptyBlockCount += i;
--                    if (iblockdata.isRandomlyTicking()) {
--                        this.tickingBlockCount += i;
++
 +                    java.util.Objects.checkFromToIndex(0, paletteCount, raw.length);
 +                    for (int i = 0; i < paletteCount; ++i) {
 +                        tickingBlocks.add(raw[i]);
                      }
                  }
  
+-                if (!fluidState.isEmpty()) {
+-                    this.nonEmptyBlockCount += count;
+-                    if (fluidState.isRandomlyTicking()) {
+-                        this.tickingFluidCount += count;
 +                final FluidState fluid = state.getFluidState();
 +
-                 if (!fluid.isEmpty()) {
--                    this.nonEmptyBlockCount += i;
++                if (!fluid.isEmpty()) {
 +                    //this.nonEmptyBlockCount += count; // fix vanilla bug: make non-empty block count correct
-                     if (fluid.isRandomlyTicking()) {
--                        this.tickingFluidCount += i;
++                    if (fluid.isRandomlyTicking()) {
 +                        this.tickingFluidCount += (short)paletteCount;
                      }
                  }
--
              }
          }
 -
--        a a0 = new a(this);
--
--        this.states.count(a0);
--        this.nonEmptyBlockCount = (short) a0.nonEmptyBlockCount;
--        this.tickingBlockCount = (short) a0.tickingBlockCount;
--        this.tickingFluidCount = (short) a0.tickingFluidCount;
+-        BlockCounter blockCounter = new BlockCounter();
+-        this.states.count(blockCounter);
+-        this.nonEmptyBlockCount = (short)blockCounter.nonEmptyBlockCount;
+-        this.tickingBlockCount = (short)blockCounter.tickingBlockCount;
+-        this.tickingFluidCount = (short)blockCounter.tickingFluidCount;
 +        // Paper end - block counting
      }
  
      public PalettedContainer<BlockState> getStates() {
-@@ -0,0 +0,0 @@ public class LevelChunkSection {
- 
-         datapaletteblock.read(buf);
-         this.biomes = datapaletteblock;
+@@ -166,6 +266,11 @@ public class LevelChunkSection {
+         PalettedContainer<Holder<Biome>> palettedContainer = this.biomes.recreate();
+         palettedContainer.read(buffer);
+         this.biomes = palettedContainer;
 +        // Paper start - block counting
 +        this.isClient = true;
 +        // force has special colliding blocks to be true
@@ -32149,12 +32112,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - block counting
      }
  
-     public void readBiomes(FriendlyByteBuf buf) {
-diff --git a/src/main/java/net/minecraft/world/level/chunk/LinearPalette.java b/src/main/java/net/minecraft/world/level/chunk/LinearPalette.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/LinearPalette.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/LinearPalette.java
-@@ -0,0 +0,0 @@ import net.minecraft.network.FriendlyByteBuf;
+     public void readBiomes(FriendlyByteBuf buffer) {
+diff --git a/net/minecraft/world/level/chunk/LinearPalette.java b/net/minecraft/world/level/chunk/LinearPalette.java
+index 5ae2f38dc613ac6129af49084980d064f14ff153..2073f6ff41aa570102621d183ee890b076267d54 100644
+--- a/net/minecraft/world/level/chunk/LinearPalette.java
++++ b/net/minecraft/world/level/chunk/LinearPalette.java
+@@ -7,13 +7,20 @@ import net.minecraft.network.FriendlyByteBuf;
  import net.minecraft.network.VarInt;
  import org.apache.commons.lang3.Validate;
  
@@ -32173,36 +32136,36 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - optimise palette reads
 +
-     private LinearPalette(IdMap<T> idList, int bits, PaletteResize<T> listener, List<T> list) {
-         this.registry = idList;
+     private LinearPalette(IdMap<T> registry, int bits, PaletteResize<T> resizeHandler, List<T> values) {
+         this.registry = registry;
          this.values = (T[])(new Object[1 << bits]);
-diff --git a/src/main/java/net/minecraft/world/level/chunk/Palette.java b/src/main/java/net/minecraft/world/level/chunk/Palette.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/Palette.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/Palette.java
-@@ -0,0 +0,0 @@ import java.util.function.Predicate;
+diff --git a/net/minecraft/world/level/chunk/Palette.java b/net/minecraft/world/level/chunk/Palette.java
+index b4b973e453a093dcc04a6b7257883aa0065e2a89..a80b2e9dceea423180a9c390d1970317dff4f1b0 100644
+--- a/net/minecraft/world/level/chunk/Palette.java
++++ b/net/minecraft/world/level/chunk/Palette.java
+@@ -5,7 +5,7 @@ import java.util.function.Predicate;
  import net.minecraft.core.IdMap;
  import net.minecraft.network.FriendlyByteBuf;
  
 -public interface Palette<T> {
 +public interface Palette<T> extends ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<T> { // Paper - optimise palette reads
-     int idFor(T object);
+     int idFor(T state);
  
-     boolean maybeHas(Predicate<T> predicate);
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
-@@ -0,0 +0,0 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
-     private final PaletteResize<T> dummyPaletteResize = (newSize, added) -> 0;
+     boolean maybeHas(Predicate<T> filter);
+diff --git a/net/minecraft/world/level/chunk/PalettedContainer.java b/net/minecraft/world/level/chunk/PalettedContainer.java
+index a6028a54c75de068515e95913b21160ab4326985..f5da433050fd3060e0335d4002d520ebe8cd691f 100644
+--- a/net/minecraft/world/level/chunk/PalettedContainer.java
++++ b/net/minecraft/world/level/chunk/PalettedContainer.java
+@@ -29,7 +29,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
+     private final PaletteResize<T> dummyPaletteResize = (bits, objectAdded) -> 0;
      public final IdMap<T> registry;
      private final T @org.jetbrains.annotations.Nullable [] presetValues; // Paper - Anti-Xray - Add preset values
 -    private volatile PalettedContainer.Data<T> data;
 +    public volatile PalettedContainer.Data<T> data; // Paper - optimise collisions - public
      private final PalettedContainer.Strategy strategy;
-     // private final ThreadingDetector threadingDetector = new ThreadingDetector("PalettedContainer"); // Paper - unused
+     //private final ThreadingDetector threadingDetector = new ThreadingDetector("PalettedContainer"); // Paper - unused
  
-@@ -0,0 +0,0 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
+@@ -75,6 +75,33 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
              );
      }
  
@@ -32234,9 +32197,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    // Paper end - optimise palette reads
 +
      // Paper start - Anti-Xray - Add preset values
-     @Deprecated @io.papermc.paper.annotation.DoNotUse public PalettedContainer(IdMap<T> idList, PalettedContainer.Strategy paletteProvider, PalettedContainer.Configuration<T> dataProvider, BitStorage storage, List<T> paletteEntries) { this(idList, paletteProvider, dataProvider, storage, paletteEntries, null, null); }
-     public PalettedContainer(
-@@ -0,0 +0,0 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
+     @Deprecated @io.papermc.paper.annotation.DoNotUse
+     public PalettedContainer(IdMap<T> registry, PalettedContainer.Strategy strategy, PalettedContainer.Configuration<T> configuration, BitStorage storage, List<T> values) {
+@@ -109,6 +136,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
              }
          }
          // Paper end
@@ -32244,47 +32207,47 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      // Paper start - Anti-Xray - Add preset values
-@@ -0,0 +0,0 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
-         this.registry = idList;
-         this.strategy = paletteProvider;
+@@ -118,6 +146,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
+         this.registry = registry;
+         this.strategy = strategy;
          this.data = data;
 +        this.updateData(this.data); // Paper - optimise palette reads
      }
  
-     private PalettedContainer(PalettedContainer<T> container, T @org.jetbrains.annotations.Nullable [] presetValues) { // Paper - Anti-Xray - Add preset values
-@@ -0,0 +0,0 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
-         this.registry = idList;
+     private PalettedContainer(PalettedContainer<T> other, T @org.jetbrains.annotations.Nullable [] presetValues) { // Paper - Anti-Xray - Add preset values
+@@ -139,6 +168,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
+         this.registry = registry;
          this.data = this.createOrReuseData(null, 0);
-         this.data.palette.idFor(object);
+         this.data.palette.idFor(palette);
 +        this.updateData(this.data); // Paper - optimise palette reads
      }
  
-     private PalettedContainer.Data<T> createOrReuseData(@Nullable PalettedContainer.Data<T> previousData, int bits) {
-@@ -0,0 +0,0 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
-         data2.copyFrom(data.palette, data.storage);
-         this.data = data2;
+     private PalettedContainer.Data<T> createOrReuseData(@Nullable PalettedContainer.Data<T> data, int id) {
+@@ -163,6 +193,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
+         this.data = data1;
+         // Paper start - Anti-Xray
          this.addPresetValues();
 +        this.updateData(this.data); // Paper - optimise palette reads
-         return object == null ? -1 : data2.palette.idFor(object);
-         // Paper end
+         return objectAdded == null ? -1 : data1.palette.idFor(objectAdded);
      }
-@@ -0,0 +0,0 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
+     private void addPresetValues() {
+@@ -192,9 +223,12 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
      }
  
-     private synchronized T getAndSet(int index, T value) { // Paper - synchronize
--        int i = this.data.palette.idFor(value);
--        int j = this.data.storage.getAndSet(index, i);
--        return this.data.palette.valueFor(j);
+     private T getAndSet(int index, T state) {
+-        int i = this.data.palette.idFor(state);
+-        int andSet = this.data.storage.getAndSet(index, i);
+-        return this.data.palette.valueFor(andSet);
 +        // Paper start - optimise palette reads
-+        final int paletteIdx = this.data.palette.idFor(value);
++        final int paletteIdx = this.data.palette.idFor(state);
 +        final PalettedContainer.Data<T> data = this.data;
 +        final int prev = data.storage.getAndSet(index, paletteIdx);
 +        return this.readPalette(data, prev);
 +        // Paper end - optimise palette reads
      }
  
-     public void set(int x, int y, int z, T value) {
-@@ -0,0 +0,0 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
+     public synchronized void set(int x, int y, int z, T state) { // Paper - synchronize
+@@ -217,9 +251,11 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
          return this.get(this.strategy.getIndex(x, y, z));
      }
  
@@ -32299,19 +32262,19 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      @Override
-@@ -0,0 +0,0 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
-             buf.readLongArray(data.storage.getRaw());
+@@ -240,6 +276,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
+             buffer.readLongArray(data.storage.getRaw());
              this.data = data;
              this.addPresetValues(); // Paper - Anti-Xray - Add preset values (inefficient, but this isn't used by the server)
 +            this.updateData(this.data); // Paper - optimise palette reads
          } finally {
              this.release();
          }
-@@ -0,0 +0,0 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
-         void accept(T object, int count);
+@@ -390,7 +427,44 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
+         void accept(T state, int count);
      }
  
--    static record Data<T>(PalettedContainer.Configuration<T> configuration, BitStorage storage, Palette<T> palette) {
+-    record Data<T>(PalettedContainer.Configuration<T> configuration, BitStorage storage, Palette<T> palette) {
 +    // Paper start - optimise palette reads
 +    public static final class Data<T> implements ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T> {
 +
@@ -32350,27 +32313,27 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +        // Paper end - optimise palette reads
 +
-         public void copyFrom(Palette<T> palette, BitStorage storage) {
-             for (int i = 0; i < storage.getSize(); i++) {
-                 T object = palette.valueFor(storage.get(i));
-diff --git a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
-@@ -0,0 +0,0 @@ public class ProtoChunk extends ChunkAccess {
+         public void copyFrom(Palette<T> palette, BitStorage bitStorage) {
+             for (int i = 0; i < bitStorage.getSize(); i++) {
+                 T object = palette.valueFor(bitStorage.get(i));
+diff --git a/net/minecraft/world/level/chunk/ProtoChunk.java b/net/minecraft/world/level/chunk/ProtoChunk.java
+index 8c333d7f390d823a7c7f303e2f444f52ec16f799..e66239e2da91bd3ddf358d239be796719c0da327 100644
+--- a/net/minecraft/world/level/chunk/ProtoChunk.java
++++ b/net/minecraft/world/level/chunk/ProtoChunk.java
+@@ -151,7 +151,7 @@ public class ProtoChunk extends ChunkAccess {
                      }
  
                      if (LightEngine.hasDifferentLightProperties(blockState, state)) {
--                        this.skyLightSources.update(this, m, j, o);
+-                        this.skyLightSources.update(this, relativeBlockPosX, y, relativeBlockPosZ);
 +                        // Paper - rewrite chunk system
                          this.lightEngine.checkBlock(pos);
                      }
                  }
-diff --git a/src/main/java/net/minecraft/world/level/chunk/SingleValuePalette.java b/src/main/java/net/minecraft/world/level/chunk/SingleValuePalette.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/SingleValuePalette.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/SingleValuePalette.java
-@@ -0,0 +0,0 @@ import net.minecraft.network.FriendlyByteBuf;
+diff --git a/net/minecraft/world/level/chunk/SingleValuePalette.java b/net/minecraft/world/level/chunk/SingleValuePalette.java
+index bc84910dbc688331efaea76972a6625014ff76f5..2ffae24b0cb1a20c7d5a8520f1b5197c2cedea11 100644
+--- a/net/minecraft/world/level/chunk/SingleValuePalette.java
++++ b/net/minecraft/world/level/chunk/SingleValuePalette.java
+@@ -8,12 +8,24 @@ import net.minecraft.network.FriendlyByteBuf;
  import net.minecraft.network.VarInt;
  import org.apache.commons.lang3.Validate;
  
@@ -32393,25 +32356,25 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - optimise palette reads
 +
-     public SingleValuePalette(IdMap<T> idList, PaletteResize<T> listener, List<T> entries) {
-         this.registry = idList;
-         this.resizeHandler = listener;
-@@ -0,0 +0,0 @@ public class SingleValuePalette<T> implements Palette<T> {
-             return this.resizeHandler.onResize(1, object);
+     public SingleValuePalette(IdMap<T> registry, PaletteResize<T> resizeHandler, List<T> value) {
+         this.registry = registry;
+         this.resizeHandler = resizeHandler;
+@@ -33,6 +45,11 @@ public class SingleValuePalette<T> implements Palette<T> {
+             return this.resizeHandler.onResize(1, state);
          } else {
-             this.value = object;
+             this.value = state;
 +            // Paper start - optimise palette reads
 +            if (this.rawPalette != null) {
-+                this.rawPalette[0] = object;
++                this.rawPalette[0] = state;
 +            }
 +            // Paper end - optimise palette reads
              return 0;
          }
      }
-@@ -0,0 +0,0 @@ public class SingleValuePalette<T> implements Palette<T> {
+@@ -58,6 +75,11 @@ public class SingleValuePalette<T> implements Palette<T> {
      @Override
-     public void read(FriendlyByteBuf buf) {
-         this.value = this.registry.byIdOrThrow(buf.readVarInt());
+     public void read(FriendlyByteBuf buffer) {
+         this.value = this.registry.byIdOrThrow(buffer.readVarInt());
 +        // Paper start - optimise palette reads
 +        if (this.rawPalette != null) {
 +            this.rawPalette[0] = this.value;
@@ -32420,11 +32383,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      @Override
-diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkPyramid.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkPyramid.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkPyramid.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/status/ChunkPyramid.java
-@@ -0,0 +0,0 @@ public record ChunkPyramid(ImmutableList<ChunkStep> steps) {
+diff --git a/net/minecraft/world/level/chunk/status/ChunkPyramid.java b/net/minecraft/world/level/chunk/status/ChunkPyramid.java
+index 9c6f4aa173fa25f9c8a3852d91a4585e069236b6..b14001afe0bf841dac7d0a1d1568fd10f6086237 100644
+--- a/net/minecraft/world/level/chunk/status/ChunkPyramid.java
++++ b/net/minecraft/world/level/chunk/status/ChunkPyramid.java
+@@ -54,7 +54,7 @@ public record ChunkPyramid(ImmutableList<ChunkStep> steps) {
          .step(ChunkStatus.CARVERS, builder -> builder)
          .step(ChunkStatus.FEATURES, builder -> builder)
          .step(ChunkStatus.INITIALIZE_LIGHT, builder -> builder.setTask(ChunkStatusTasks::initializeLight))
@@ -32433,11 +32396,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          .step(ChunkStatus.SPAWN, builder -> builder)
          .step(ChunkStatus.FULL, builder -> builder.setTask(ChunkStatusTasks::full))
          .build();
-diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java
-@@ -0,0 +0,0 @@ import net.minecraft.resources.ResourceLocation;
+diff --git a/net/minecraft/world/level/chunk/status/ChunkStatus.java b/net/minecraft/world/level/chunk/status/ChunkStatus.java
+index 7a64b00ff31d1273d0b0b9a3cfd43808c88ef46a..c9d8a1c0a75c34ccd9f5cead02cccd776276f3cb 100644
+--- a/net/minecraft/world/level/chunk/status/ChunkStatus.java
++++ b/net/minecraft/world/level/chunk/status/ChunkStatus.java
+@@ -11,7 +11,7 @@ import net.minecraft.resources.ResourceLocation;
  import net.minecraft.world.level.levelgen.Heightmap;
  import org.jetbrains.annotations.VisibleForTesting;
  
@@ -32446,7 +32409,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      public static final int MAX_STRUCTURE_DISTANCE = 8;
      private static final EnumSet<Heightmap.Types> WORLDGEN_HEIGHTMAPS = EnumSet.of(Heightmap.Types.OCEAN_FLOOR_WG, Heightmap.Types.WORLD_SURFACE_WG);
      public static final EnumSet<Heightmap.Types> FINAL_HEIGHTMAPS = EnumSet.of(
-@@ -0,0 +0,0 @@ public class ChunkStatus {
+@@ -51,8 +51,70 @@ public class ChunkStatus {
          return list;
      }
  
@@ -32504,53 +32467,55 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    // Paper end - rewrite chunk system
 +
      @VisibleForTesting
-     protected ChunkStatus(@Nullable ChunkStatus previous, EnumSet<Heightmap.Types> heightMapTypes, ChunkType chunkType) {
+     protected ChunkStatus(@Nullable ChunkStatus parent, EnumSet<Heightmap.Types> heightmapsAfter, ChunkType chunkType) {
++        // Paper start - rewrite chunk system
 +        this.isParallelCapable = false;
 +        this.writeRadius = -1;
 +        this.nextStatus = (ChunkStatus)(Object)this;
-+        if (previous != null) {
-+            previous.nextStatus = (ChunkStatus)(Object)this;
++        if (parent != null) {
++            parent.nextStatus = (ChunkStatus)(Object)this;
 +        }
 +        this.warnedAboutNoImmediateComplete = new java.util.concurrent.atomic.AtomicBoolean();
-         this.parent = previous == null ? this : previous;
++        // Paper end - rewrite chunk system
+         this.parent = parent == null ? this : parent;
          this.chunkType = chunkType;
-         this.heightmapsAfter = heightMapTypes;
-diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
-@@ -0,0 +0,0 @@ public class ChunkStatusTasks {
-                 chunk1 = protochunkextension.getWrapped();
+         this.heightmapsAfter = heightmapsAfter;
+diff --git a/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java b/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
+index c953bc93de8a42bcc12b7e8f46b3ae804e54964e..2ccbdfdcf81556306e098277ecf119d5fd02138c 100644
+--- a/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
++++ b/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
+@@ -182,7 +182,7 @@ public class ChunkStatusTasks {
+             if (protoChunk instanceof ImposterProtoChunk imposterProtoChunk) {
+                 wrapped = imposterProtoChunk.getWrapped();
              } else {
-                 chunk1 = new LevelChunk(worldserver, protochunk, ($) -> { // Paper - decompile fix
--                    ChunkStatusTasks.postLoadProtoChunk(worldserver, protochunk.getEntities());
-+                    ChunkStatusTasks.postLoadProtoChunk(worldserver, protochunk.getEntities(), protochunk.getPos()); // Paper - rewrite chunk system
-                 });
-                 generationchunkholder.replaceProtoChunk(new ImposterProtoChunk(chunk1, false));
+-                wrapped = new LevelChunk(serverLevel, protoChunk, chunk1 -> postLoadProtoChunk(serverLevel, protoChunk.getEntities()));
++                wrapped = new LevelChunk(serverLevel, protoChunk, chunk1 -> postLoadProtoChunk(serverLevel, protoChunk.getEntities(), protoChunk.getPos())); // Paper - rewrite chunk system
+                 generationChunkHolder.replaceProtoChunk(new ImposterProtoChunk(wrapped, false));
              }
-@@ -0,0 +0,0 @@ public class ChunkStatusTasks {
-         }, context.mainThreadExecutor());
+ 
+@@ -196,7 +196,7 @@ public class ChunkStatusTasks {
+         }, worldGenContext.mainThreadExecutor());
      }
  
--    public static void postLoadProtoChunk(ServerLevel world, List<CompoundTag> entities) { // Paper - public
-+    public static void postLoadProtoChunk(ServerLevel world, List<CompoundTag> entities, ChunkPos pos) { // Paper - public // Paper - rewrite chunk system - add ChunkPos param
-         if (!entities.isEmpty()) {
+-    public static void postLoadProtoChunk(ServerLevel level, List<CompoundTag> entityTags) { // Paper - public
++    public static void postLoadProtoChunk(ServerLevel level, List<CompoundTag> entityTags, ChunkPos pos) { // Paper - public // Paper - rewrite chunk system - add ChunkPos param
+         if (!entityTags.isEmpty()) {
              // CraftBukkit start - these are spawned serialized (DefinedStructure) and we don't call an add event below at the moment due to ordering complexities
-             world.addWorldGenChunkEntities(EntityType.loadEntitiesRecursive(entities, world, EntitySpawnReason.LOAD).filter((entity) -> {
-@@ -0,0 +0,0 @@ public class ChunkStatusTasks {
+             level.addWorldGenChunkEntities(EntityType.loadEntitiesRecursive(entityTags, level, EntitySpawnReason.LOAD).filter((entity) -> {
+@@ -208,7 +208,7 @@ public class ChunkStatusTasks {
                  }
-                 checkDupeUUID(world, entity); // Paper - duplicate uuid resolving
+                 checkDupeUUID(level, entity); // Paper - duplicate uuid resolving
                  return !needsRemoval;
 -            }));
 +            }), pos); // Paper - rewrite chunk system
              // CraftBukkit end
          }
- 
-diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStep.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStep.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStep.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStep.java
-@@ -0,0 +0,0 @@ import net.minecraft.util.profiling.jfr.callback.ProfiledDuration;
+     }
+diff --git a/net/minecraft/world/level/chunk/status/ChunkStep.java b/net/minecraft/world/level/chunk/status/ChunkStep.java
+index 7a4d299d2ce36982204e30de9278ddfd5b37c3df..b8348976e80578d9eff64eea68c04c603fed49ad 100644
+--- a/net/minecraft/world/level/chunk/status/ChunkStep.java
++++ b/net/minecraft/world/level/chunk/status/ChunkStep.java
+@@ -11,9 +11,50 @@ import net.minecraft.util.profiling.jfr.callback.ProfiledDuration;
  import net.minecraft.world.level.chunk.ChunkAccess;
  import net.minecraft.world.level.chunk.ProtoChunk;
  
@@ -32604,7 +32569,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      public int getAccumulatedRadiusOf(ChunkStatus status) {
          return status == this.targetStatus ? 0 : this.accumulatedDependencies.getRadiusOf(status);
      }
-@@ -0,0 +0,0 @@ public record ChunkStep(
+@@ -40,6 +81,56 @@ public record ChunkStep(
          return chunk;
      }
  
@@ -32661,17 +32626,16 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      public static class Builder {
          private final ChunkStatus status;
          @Nullable
-diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java
-@@ -0,0 +0,0 @@ import net.minecraft.world.level.dimension.LevelStem;
+diff --git a/net/minecraft/world/level/chunk/storage/ChunkStorage.java b/net/minecraft/world/level/chunk/storage/ChunkStorage.java
+index 80bc7ad9ad076968d06279dedd845d5946cf2501..433feab7f7c1931f79836164a0b8c4a1c3b75ba6 100644
+--- a/net/minecraft/world/level/chunk/storage/ChunkStorage.java
++++ b/net/minecraft/world/level/chunk/storage/ChunkStorage.java
+@@ -22,20 +22,30 @@ import net.minecraft.world.level.chunk.ChunkGenerator;
  import net.minecraft.world.level.levelgen.structure.LegacyStructureDataHandler;
  import net.minecraft.world.level.storage.DimensionDataStorage;
  
 -public class ChunkStorage implements AutoCloseable {
 +public class ChunkStorage implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkStorage { // Paper - rewrite chunk system
- 
      public static final int LAST_MONOLYTH_STRUCTURE_DATA_VERSION = 1493;
 -    private final IOWorker worker;
 +    // Paper - rewrite chunk system
@@ -32689,29 +32653,29 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - rewrite chunk system
 +
-     public ChunkStorage(RegionStorageInfo storageKey, Path directory, DataFixer dataFixer, boolean dsync) {
-         this.fixerUpper = dataFixer;
--        this.worker = new IOWorker(storageKey, directory, dsync);
-+        this.storage = new IOWorker(storageKey, directory, dsync).storage; // Paper - rewrite chunk system
+     public ChunkStorage(RegionStorageInfo info, Path folder, DataFixer fixerUpper, boolean sync) {
+         this.fixerUpper = fixerUpper;
+-        this.worker = new IOWorker(info, folder, sync);
++        this.storage = new IOWorker(info, folder, sync).storage; // Paper - rewrite chunk system
      }
  
-     public boolean isOldChunkAround(ChunkPos chunkPos, int checkRadius) {
--        return this.worker.isOldChunkAround(chunkPos, checkRadius);
+     public boolean isOldChunkAround(ChunkPos pos, int radius) {
+-        return this.worker.isOldChunkAround(pos, radius);
 +        return true; // Paper - rewrite chunk system
      }
  
      // CraftBukkit start
-@@ -0,0 +0,0 @@ public class ChunkStorage implements AutoCloseable {
-                     if (nbttagcompound.getCompound("Level").getBoolean("hasLegacyStructureData")) {
-                         LegacyStructureDataHandler persistentstructurelegacy = this.getLegacyStructureHandler(resourcekey, supplier);
- 
-+                        synchronized (persistentstructurelegacy) { // Paper - rewrite chunk system
-                         nbttagcompound = persistentstructurelegacy.updateFromLegacy(nbttagcompound);
-+                        } // Paper - rewrite chunk system
+@@ -99,7 +109,9 @@ public class ChunkStorage implements AutoCloseable {
+                     chunkData = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.CHUNK, chunkData, version, 1493); // Paper - replace chunk converter
+                     if (chunkData.getCompound("Level").getBoolean("hasLegacyStructureData")) {
+                         LegacyStructureDataHandler legacyStructureHandler = this.getLegacyStructureHandler(levelKey, storage);
++                        synchronized (legacyStructureHandler) { // Paper - rewrite chunk system
+                         chunkData = legacyStructureHandler.updateFromLegacy(chunkData);
++                        }
                      }
                  }
  
-@@ -0,0 +0,0 @@ public class ChunkStorage implements AutoCloseable {
+@@ -163,7 +175,13 @@ public class ChunkStorage implements AutoCloseable {
      }
  
      public CompletableFuture<Optional<CompoundTag>> read(ChunkPos chunkPos) {
@@ -32725,15 +32689,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - rewrite chunk system
      }
  
-     public CompletableFuture<Void> write(ChunkPos chunkPos, Supplier<CompoundTag> nbtSupplier) {
-@@ -0,0 +0,0 @@ public class ChunkStorage implements AutoCloseable {
+     public CompletableFuture<Void> write(ChunkPos pos, Supplier<CompoundTag> tagSupplier) {
+@@ -179,29 +197,54 @@ public class ChunkStorage implements AutoCloseable {
          };
          // Paper end - guard against possible chunk pos desync
-         this.handleLegacyStructureIndex(chunkPos);
--        return this.worker.store(chunkPos, guardedPosCheck); // Paper - guard against possible chunk pos desync
+         this.handleLegacyStructureIndex(pos);
+-        return this.worker.store(pos, guardedPosCheck); // Paper - guard against possible chunk pos desync
 +        // Paper start - rewrite chunk system
 +        try {
-+            this.storage.write(chunkPos, guardedPosCheck.get());
++            this.storage.write(pos, guardedPosCheck.get());
 +            return CompletableFuture.completedFuture(null);
 +        } catch (final Throwable throwable) {
 +            return CompletableFuture.failedFuture(throwable);
@@ -32747,7 +32711,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
              this.legacyStructureHandler.removeIndex(chunkPos.toLong());
 +            } // Paper - rewrite chunk system
          }
- 
      }
  
      public void flushWorker() {
@@ -32761,6 +32724,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - rewrite chunk system
      }
  
+     @Override
      public void close() throws IOException {
 -        this.worker.close();
 +        this.storage.close(); // Paper - rewrite chunk system
@@ -32787,30 +32751,30 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return this.storage.info(); // Paper - rewrite chunk system
      }
  }
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ public class EntityStorage implements EntityPersistentStorage<Entity> {
+diff --git a/net/minecraft/world/level/chunk/storage/EntityStorage.java b/net/minecraft/world/level/chunk/storage/EntityStorage.java
+index c3c9771138cb1712ea429d8c45596220830314eb..da05fb780c55381a7a08ced51d01764a645740b2 100644
+--- a/net/minecraft/world/level/chunk/storage/EntityStorage.java
++++ b/net/minecraft/world/level/chunk/storage/EntityStorage.java
+@@ -71,12 +71,12 @@ public class EntityStorage implements EntityPersistentStorage<Entity> {
          }
      }
  
--    private static ChunkPos readChunkPos(CompoundTag chunkNbt) {
-+    public static ChunkPos readChunkPos(CompoundTag chunkNbt) { // Paper - public
-         int[] is = chunkNbt.getIntArray("Position");
-         return new ChunkPos(is[0], is[1]);
+-    private static ChunkPos readChunkPos(CompoundTag tag) {
++    public static ChunkPos readChunkPos(CompoundTag tag) { // Paper - public
+         int[] intArray = tag.getIntArray("Position");
+         return new ChunkPos(intArray[0], intArray[1]);
      }
  
--    private static void writeChunkPos(CompoundTag chunkNbt, ChunkPos pos) {
-+    public static void writeChunkPos(CompoundTag chunkNbt, ChunkPos pos) { // Paper - public
-         chunkNbt.put("Position", new IntArrayTag(new int[]{pos.x, pos.z}));
+-    private static void writeChunkPos(CompoundTag tag, ChunkPos pos) {
++    public static void writeChunkPos(CompoundTag tag, ChunkPos pos) { // Paper - public
+         tag.put("Position", new IntArrayTag(new int[]{pos.x, pos.z}));
      }
  
-diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/IOWorker.java b/src/main/java/net/minecraft/world/level/chunk/storage/IOWorker.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/storage/IOWorker.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/storage/IOWorker.java
-@@ -0,0 +0,0 @@ public class IOWorker implements ChunkScanAccess, AutoCloseable {
+diff --git a/net/minecraft/world/level/chunk/storage/IOWorker.java b/net/minecraft/world/level/chunk/storage/IOWorker.java
+index 889e188e920edb284f04b264bcdd06146f54a4cb..2199a9e2a0141c646d108f2687a27f1d165453c5 100644
+--- a/net/minecraft/world/level/chunk/storage/IOWorker.java
++++ b/net/minecraft/world/level/chunk/storage/IOWorker.java
+@@ -30,7 +30,7 @@ public class IOWorker implements ChunkScanAccess, AutoCloseable {
      private static final Logger LOGGER = LogUtils.getLogger();
      private final AtomicBoolean shutdownRequested = new AtomicBoolean();
      private final PriorityConsecutiveExecutor consecutiveExecutor;
@@ -32819,22 +32783,22 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      private final SequencedMap<ChunkPos, IOWorker.PendingStore> pendingWrites = new LinkedHashMap<>();
      private final Long2ObjectLinkedOpenHashMap<CompletableFuture<BitSet>> regionCacheForBlender = new Long2ObjectLinkedOpenHashMap<>();
      private static final int REGION_CACHE_SIZE = 1024;
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ import net.minecraft.nbt.NbtIo; // Paper
+diff --git a/net/minecraft/world/level/chunk/storage/RegionFile.java b/net/minecraft/world/level/chunk/storage/RegionFile.java
+index 783a2d80f6197dd0af0dc81909f0353a8ea2ecf4..7da388ffab162c282cad0f297bb7304f3c2abbaf 100644
+--- a/net/minecraft/world/level/chunk/storage/RegionFile.java
++++ b/net/minecraft/world/level/chunk/storage/RegionFile.java
+@@ -22,7 +22,7 @@ import net.minecraft.util.profiling.jfr.JvmProfiler;
  import net.minecraft.world.level.ChunkPos;
  import org.slf4j.Logger;
  
 -public class RegionFile implements AutoCloseable {
 +public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemRegionFile { // Paper - rewrite chunk system
- 
      private static final Logger LOGGER = LogUtils.getLogger();
      private static final int SECTOR_BYTES = 4096;
-@@ -0,0 +0,0 @@ public class RegionFile implements AutoCloseable {
      @VisibleForTesting
-     protected final RegionBitmap usedSectors;
+@@ -45,6 +45,21 @@ public class RegionFile implements AutoCloseable {
+     @VisibleForTesting
+     protected final RegionBitmap usedSectors = new RegionBitmap();
  
 +    // Paper start - rewrite chunk system
 +    @Override
@@ -32851,33 +32815,32 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - rewrite chunk system
 +
-     public RegionFile(RegionStorageInfo storageKey, Path directory, Path path, boolean dsync) throws IOException {
-         this(storageKey, directory, path, RegionFileVersion.getCompressionFormat(), dsync); // Paper - Configurable region compression format
+     public RegionFile(RegionStorageInfo info, Path path, Path externalFileDir, boolean sync) throws IOException {
+         this(info, path, externalFileDir, RegionFileVersion.getCompressionFormat(), sync); // Paper - Configurable region compression format
      }
-@@ -0,0 +0,0 @@ public class RegionFile implements AutoCloseable {
+@@ -204,6 +219,16 @@ public class RegionFile implements AutoCloseable {
  
      @Nullable
-     private DataInputStream createExternalChunkInputStream(ChunkPos pos, byte flags) throws IOException {
+     private DataInputStream createExternalChunkInputStream(ChunkPos chunkPos, byte versionByte) throws IOException {
 +        // Paper start - rewrite chunk system
-+        final DataInputStream is = this.createExternalChunkInputStream0(pos, flags);
++        final DataInputStream is = this.createExternalChunkInputStream0(chunkPos, versionByte);
 +        if (is == null) {
 +            return is;
 +        }
 +        return new ca.spottedleaf.moonrise.patches.chunk_system.util.stream.ExternalChunkStreamMarker(is);
 +    }
 +    @Nullable
-+    private DataInputStream createExternalChunkInputStream0(ChunkPos pos, byte flags) throws IOException {
++    private DataInputStream createExternalChunkInputStream0(ChunkPos chunkPos, byte versionByte) throws IOException {
 +        // Paper end - rewrite chunk system
-         Path path = this.getExternalChunkPath(pos);
- 
-         if (!Files.isRegularFile(path, new LinkOption[0])) {
-@@ -0,0 +0,0 @@ public class RegionFile implements AutoCloseable {
- 
+         Path externalChunkPath = this.getExternalChunkPath(chunkPos);
+         if (!Files.isRegularFile(externalChunkPath)) {
+             LOGGER.error("External chunk path {} is not file", externalChunkPath);
+@@ -398,9 +423,28 @@ public class RegionFile implements AutoCloseable {
+         }
      }
-     // Paper end
--    private class ChunkBuffer extends ByteArrayOutputStream {
-+    private class ChunkBuffer extends ByteArrayOutputStream implements ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkBuffer { // Paper - rewrite chunk system
  
+-    class ChunkBuffer extends ByteArrayOutputStream {
++    class ChunkBuffer extends ByteArrayOutputStream implements ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkBuffer { // Paper - rewrite chunk system
          private final ChunkPos pos;
  
 +        // Paper start - rewrite chunk system
@@ -32899,36 +32862,36 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +        // Paper end - rewrite chunk system
 +
-         public ChunkBuffer(final ChunkPos chunkcoordintpair) {
+         public ChunkBuffer(final ChunkPos pos) {
              super(8096);
              super.write(0);
-@@ -0,0 +0,0 @@ public class RegionFile implements AutoCloseable {
- 
+@@ -417,7 +461,7 @@ public class RegionFile implements AutoCloseable {
+             int i = this.count - 5 + 1;
              JvmProfiler.INSTANCE.onRegionFileWrite(RegionFile.this.info, this.pos, RegionFile.this.version, i);
-             bytebuffer.putInt(0, i);
--            RegionFile.this.write(this.pos, bytebuffer);
-+            if (this.writeOnClose) { RegionFile.this.write(this.pos, bytebuffer); } // Paper - rewrite chunk system
+             byteBuffer.putInt(0, i);
+-            RegionFile.this.write(this.pos, byteBuffer);
++            if (this.writeOnClose) { RegionFile.this.write(this.pos, byteBuffer); } // Paper - rewrite chunk system
          }
      }
  
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ import net.minecraft.nbt.StreamTagVisitor;
+diff --git a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+index 51bf310423013d0ae9d3202d66e36a053a767197..e35bb5534e2fbd2e30154a15ff6d39baa121608f 100644
+--- a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
++++ b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+@@ -14,7 +14,7 @@ import net.minecraft.nbt.StreamTagVisitor;
  import net.minecraft.util.ExceptionCollector;
  import net.minecraft.world.level.ChunkPos;
  
 -public final class RegionFileStorage implements AutoCloseable {
 +public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.io.ChunkSystemRegionFileStorage { // Paper - rewrite chunk system
- 
      public static final String ANVIL_EXTENSION = ".mca";
      private static final int MAX_CACHE_SIZE = 256;
-@@ -0,0 +0,0 @@ public final class RegionFileStorage implements AutoCloseable {
+     public final Long2ObjectLinkedOpenHashMap<RegionFile> regionCache = new Long2ObjectLinkedOpenHashMap<>();
+@@ -22,29 +22,218 @@ public final class RegionFileStorage implements AutoCloseable {
      private final Path folder;
      private final boolean sync;
  
--    RegionFileStorage(RegionStorageInfo storageKey, Path directory, boolean dsync) {
+-    RegionFileStorage(RegionStorageInfo info, Path folder, boolean sync) {
 +    // Paper start - rewrite chunk system
 +    private static final int REGION_SHIFT = 5;
 +    private static final int MAX_NON_EXISTING_CACHE = 1024 * 4;
@@ -33099,34 +33062,31 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +    // Paper end - rewrite chunk system
-+
-+    protected RegionFileStorage(RegionStorageInfo storageKey, Path directory, boolean dsync) { // Paper - protected
-         this.folder = directory;
-         this.sync = dsync;
-         this.info = storageKey;
-     }
- 
--    private RegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit
--        long i = ChunkPos.asLong(chunkcoordintpair.getRegionX(), chunkcoordintpair.getRegionZ());
--        RegionFile regionfile = (RegionFile) this.regionCache.getAndMoveToFirst(i);
 +    // Paper start - rewrite chunk system
 +    public RegionFile getRegionFile(ChunkPos chunkcoordintpair) throws IOException {
 +        return this.getRegionFile(chunkcoordintpair, false);
 +    }
 +    // Paper end - rewrite chunk system
++
++    protected RegionFileStorage(RegionStorageInfo info, Path folder, boolean sync) { // Paper - protected
+         this.folder = folder;
+         this.sync = sync;
+         this.info = info;
+     }
  
--        if (regionfile != null) {
--            return regionfile;
+     @org.jetbrains.annotations.Contract("_, false -> !null") @Nullable private RegionFile getRegionFile(ChunkPos chunkPos, boolean existingOnly) throws IOException { // CraftBukkit
+-        long packedChunkPos = ChunkPos.asLong(chunkPos.getRegionX(), chunkPos.getRegionZ());
+-        RegionFile regionFile = this.regionCache.getAndMoveToFirst(packedChunkPos);
+-        if (regionFile != null) {
+-            return regionFile;
 -        } else {
 -            if (this.regionCache.size() >= io.papermc.paper.configuration.GlobalConfiguration.get().misc.regionFileCacheSize) { // Paper - Sanitise RegionFileCache and make configurable
--                ((RegionFile) this.regionCache.removeLast()).close();
-+    public RegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit // Paper - public
 +        // Paper start - rewrite chunk system
 +        if (existingOnly) {
-+            return this.moonrise$getRegionFileIfExists(chunkcoordintpair.x, chunkcoordintpair.z);
++            return this.moonrise$getRegionFileIfExists(chunkPos.x, chunkPos.z);
 +        }
 +        synchronized (this) {
-+            final long key = ChunkPos.asLong(chunkcoordintpair.x >> REGION_SHIFT, chunkcoordintpair.z >> REGION_SHIFT);
++            final long key = ChunkPos.asLong(chunkPos.x >> REGION_SHIFT, chunkPos.z >> REGION_SHIFT);
 +
 +            RegionFile ret = this.regionCache.getAndMoveToFirst(key);
 +            if (ret != null) {
@@ -33134,22 +33094,20 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            }
 +
 +            if (this.regionCache.size() >= io.papermc.paper.configuration.GlobalConfiguration.get().misc.regionFileCacheSize) { // Paper
-+                this.regionCache.removeLast().close();
+                 this.regionCache.removeLast().close();
              }
  
-+            final Path regionPath = this.folder.resolve(getRegionFileName(chunkcoordintpair.x, chunkcoordintpair.z));
++            final Path regionPath = this.folder.resolve(getRegionFileName(chunkPos.x, chunkPos.z));
 +
 +            this.createRegionFile(key);
 +
              FileUtil.createDirectoriesSafe(this.folder);
--            Path path = this.folder;
--            int j = chunkcoordintpair.getRegionX();
--            Path path1 = path.resolve("r." + j + "." + chunkcoordintpair.getRegionZ() + ".mca");
--            if (existingOnly && !java.nio.file.Files.exists(path1)) return null; // CraftBukkit
--            RegionFile regionfile1 = new RegionFile(this.info, path1, this.folder, this.sync);
- 
--            this.regionCache.putAndMoveToFirst(i, regionfile1);
--            return regionfile1;
+-            Path path = this.folder.resolve("r." + chunkPos.getRegionX() + "." + chunkPos.getRegionZ() + ".mca");
+-            if (existingOnly && !java.nio.file.Files.exists(path)) return null; // CraftBukkit
+-            RegionFile regionFile1 = new RegionFile(this.info, path, this.folder, this.sync);
+-            this.regionCache.putAndMoveToFirst(packedChunkPos, regionFile1);
+-            return regionFile1;
++
 +            ret = new RegionFile(this.info, regionPath, this.folder, this.sync);
 +
 +            this.regionCache.putAndMoveToFirst(key, ret);
@@ -33160,37 +33118,34 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      // Paper start
-@@ -0,0 +0,0 @@ public final class RegionFileStorage implements AutoCloseable {
- 
+@@ -126,8 +315,14 @@ public final class RegionFileStorage implements AutoCloseable {
+         }
      }
  
--    protected void write(ChunkPos pos, @Nullable CompoundTag nbt) throws IOException {
--        RegionFile regionfile = this.getRegionFile(pos, false); // CraftBukkit
-+    public void write(ChunkPos pos, @Nullable CompoundTag nbt) throws IOException { // Paper - rewrite chunk system - public
-+        RegionFile regionfile = this.getRegionFile(pos, nbt == null); // CraftBukkit // Paper - rewrite chunk system
+-    protected void write(ChunkPos chunkPos, @Nullable CompoundTag chunkData) throws IOException {
+-        RegionFile regionFile = this.getRegionFile(chunkPos, false); // CraftBukkit
++    public void write(ChunkPos chunkPos, @Nullable CompoundTag chunkData) throws IOException { // Paper - rewrite chunk system - public
++        RegionFile regionFile = this.getRegionFile(chunkPos, chunkData == null); // CraftBukkit // Paper - rewrite chunk system
 +        // Paper start - rewrite chunk system
-+        if (regionfile == null) {
++        if (regionFile == null) {
 +            // if the RegionFile doesn't exist, no point in deleting from it
 +            return;
 +        }
 +        // Paper end - rewrite chunk system
+         if (chunkData == null) {
+             regionFile.clear(chunkPos);
+         } else {
+@@ -140,23 +335,36 @@ public final class RegionFileStorage implements AutoCloseable {
  
-         if (nbt == null) {
-             regionfile.clear(pos);
-@@ -0,0 +0,0 @@ public final class RegionFileStorage implements AutoCloseable {
-     }
- 
+     @Override
      public void close() throws IOException {
--        ExceptionCollector<IOException> exceptionsuppressor = new ExceptionCollector<>();
--        ObjectIterator objectiterator = this.regionCache.values().iterator();
--
--        while (objectiterator.hasNext()) {
--            RegionFile regionfile = (RegionFile) objectiterator.next();
+-        ExceptionCollector<IOException> exceptionCollector = new ExceptionCollector<>();
 -
+-        for (RegionFile regionFile : this.regionCache.values()) {
 -            try {
--                regionfile.close();
--            } catch (IOException ioexception) {
--                exceptionsuppressor.add(ioexception);
+-                regionFile.close();
+-            } catch (IOException var5) {
+-                exceptionCollector.add(var5);
 +        // Paper start - rewrite chunk system
 +        synchronized (this) {
 +            final ExceptionCollector<IOException> exceptionCollector = new ExceptionCollector<>();
@@ -33201,19 +33156,16 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                    exceptionCollector.add(ex);
 +                }
              }
--        }
- 
--        exceptionsuppressor.throwIfPresent();
 +            exceptionCollector.throwIfPresent();
-+        }
+         }
+-
+-        exceptionCollector.throwIfPresent();
 +        // Paper end - rewrite chunk system
      }
  
      public void flush() throws IOException {
--        ObjectIterator objectiterator = this.regionCache.values().iterator();
--
--        while (objectiterator.hasNext()) {
--            RegionFile regionfile = (RegionFile) objectiterator.next();
+-        for (RegionFile regionFile : this.regionCache.values()) {
+-            regionFile.flush();
 +        // Paper start - rewrite chunk system
 +        synchronized (this) {
 +            final ExceptionCollector<IOException> exceptionCollector = new ExceptionCollector<>();
@@ -33224,19 +33176,18 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                    exceptionCollector.add(ex);
 +                }
 +            }
- 
--            regionfile.flush();
++
 +            exceptionCollector.throwIfPresent();
          }
 +        // Paper end - 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ import net.minecraft.world.level.ChunkPos;
+     public RegionStorageInfo info() {
+diff --git a/net/minecraft/world/level/chunk/storage/SectionStorage.java b/net/minecraft/world/level/chunk/storage/SectionStorage.java
+index 7dc1ffffd9d0fec54dbc254c154ee85ee750174d..778bd73a938c94ecb85ca0f8b686ff4e1baee040 100644
+--- a/net/minecraft/world/level/chunk/storage/SectionStorage.java
++++ b/net/minecraft/world/level/chunk/storage/SectionStorage.java
+@@ -40,10 +40,10 @@ import net.minecraft.world.level.ChunkPos;
  import net.minecraft.world.level.LevelHeightAccessor;
  import org.slf4j.Logger;
  
@@ -33249,7 +33200,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      private final Long2ObjectMap<Optional<R>> storage = new Long2ObjectOpenHashMap<>();
      private final LongLinkedOpenHashSet dirtyChunks = new LongLinkedOpenHashSet();
      private final Codec<P> codec;
-@@ -0,0 +0,0 @@ public class SectionStorage<R, P> implements AutoCloseable {
+@@ -57,6 +57,18 @@ public class SectionStorage<R, P> implements AutoCloseable {
      private final Long2ObjectMap<CompletableFuture<Optional<SectionStorage.PackedChunk<P>>>> pendingLoads = new Long2ObjectOpenHashMap<>();
      private final Object loadLock = new Object();
  
@@ -33266,26 +33217,26 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    // Paper end - rewrite chunk system
 +
      public SectionStorage(
-         SimpleRegionStorage storageAccess,
+         SimpleRegionStorage simpleRegionStorage,
          Codec<P> codec,
-@@ -0,0 +0,0 @@ public class SectionStorage<R, P> implements AutoCloseable {
-         ChunkIOErrorReporter errorHandler,
-         LevelHeightAccessor world
+@@ -67,7 +79,7 @@ public class SectionStorage<R, P> implements AutoCloseable {
+         ChunkIOErrorReporter errorReporter,
+         LevelHeightAccessor levelHeightAccessor
      ) {
--        this.simpleRegionStorage = storageAccess;
+-        this.simpleRegionStorage = simpleRegionStorage;
 +        // Paper - rewrite chunk system
          this.codec = codec;
-         this.packer = serializer;
-         this.unpacker = deserializer;
-@@ -0,0 +0,0 @@ public class SectionStorage<R, P> implements AutoCloseable {
-         this.registryAccess = registryManager;
-         this.errorReporter = errorHandler;
-         this.levelHeightAccessor = world;
-+        this.regionStorage = storageAccess.worker.storage; // Paper - rewrite chunk system
+         this.packer = packer;
+         this.unpacker = unpacker;
+@@ -75,6 +87,7 @@ public class SectionStorage<R, P> implements AutoCloseable {
+         this.registryAccess = registryAccess;
+         this.errorReporter = errorReporter;
+         this.levelHeightAccessor = levelHeightAccessor;
++        this.regionStorage = simpleRegionStorage.worker.storage; // Paper - rewrite chunk system
      }
  
-     protected void tick(BooleanSupplier shouldKeepTicking) {
-@@ -0,0 +0,0 @@ public class SectionStorage<R, P> implements AutoCloseable {
+     protected void tick(BooleanSupplier aheadOfTime) {
+@@ -188,65 +201,15 @@ public class SectionStorage<R, P> implements AutoCloseable {
      }
  
      private CompletableFuture<Optional<SectionStorage.PackedChunk<P>>> tryRead(ChunkPos chunkPos) {
@@ -33293,43 +33244,44 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -        return this.simpleRegionStorage
 -            .read(chunkPos)
 -            .thenApplyAsync(
--                chunkNbt -> chunkNbt.map(
--                        nbt -> SectionStorage.PackedChunk.parse(this.codec, registryOps, nbt, this.simpleRegionStorage, this.levelHeightAccessor)
--                    ),
+-                optional -> optional.map(
+-                    compoundTag -> SectionStorage.PackedChunk.parse(this.codec, registryOps, compoundTag, this.simpleRegionStorage, this.levelHeightAccessor)
+-                ),
 -                Util.backgroundExecutor().forName("parseSection")
 -            )
--            .exceptionally(throwable -> {
--                if (throwable instanceof CompletionException) {
--                    throwable = throwable.getCause();
+-            .exceptionally(cause -> {
+-                if (cause instanceof CompletionException) {
+-                    cause = cause.getCause();
 -                }
 -
--                if (throwable instanceof IOException iOException) {
--                    LOGGER.error("Error reading chunk {} data from disk", chunkPos, iOException);
--                    this.errorReporter.reportChunkLoadFailure(iOException, this.simpleRegionStorage.storageInfo(), chunkPos);
+-                if (cause instanceof IOException ioException) {
+-                    LOGGER.error("Error reading chunk {} data from disk", chunkPos, ioException);
+-                    this.errorReporter.reportChunkLoadFailure(ioException, this.simpleRegionStorage.storageInfo(), chunkPos);
 -                    return Optional.empty();
 -                } else {
--                    throw new CompletionException(throwable);
+-                    throw new CompletionException(cause);
 -                }
 -            });
 +        throw new IllegalStateException("Only chunk system can write state, offending class:" + this.getClass().getName()); // Paper - rewrite chunk system
      }
  
-     private void unpackChunk(ChunkPos chunkPos, @Nullable SectionStorage.PackedChunk<P> result) {
--        if (result == null) {
--            for (int i = this.levelHeightAccessor.getMinSectionY(); i <= this.levelHeightAccessor.getMaxSectionY(); i++) {
--                this.storage.put(getKey(chunkPos, i), Optional.empty());
+     private void unpackChunk(ChunkPos pos, @Nullable SectionStorage.PackedChunk<P> packedChunk) {
+-        if (packedChunk == null) {
+-            for (int sectionY = this.levelHeightAccessor.getMinSectionY(); sectionY <= this.levelHeightAccessor.getMaxSectionY(); sectionY++) {
+-                this.storage.put(getKey(pos, sectionY), Optional.empty());
 -            }
 -        } else {
--            boolean bl = result.versionChanged();
+-            boolean versionChanged = packedChunk.versionChanged();
 -
--            for (int j = this.levelHeightAccessor.getMinSectionY(); j <= this.levelHeightAccessor.getMaxSectionY(); j++) {
--                long l = getKey(chunkPos, j);
--                Optional<R> optional = Optional.ofNullable(result.sectionsByY.get(j)).map(section -> this.unpacker.apply((P)section, () -> this.setDirty(l)));
--                this.storage.put(l, optional);
+-            for (int sectionY1 = this.levelHeightAccessor.getMinSectionY(); sectionY1 <= this.levelHeightAccessor.getMaxSectionY(); sectionY1++) {
+-                long key = getKey(pos, sectionY1);
+-                Optional<R> optional = Optional.ofNullable(packedChunk.sectionsByY.get(sectionY1))
+-                    .map(object -> this.unpacker.apply((P)object, () -> this.setDirty(key)));
+-                this.storage.put(key, optional);
 -                optional.ifPresent(object -> {
--                    this.onSectionLoad(l);
--                    if (bl) {
--                        this.setDirty(l);
+-                    this.onSectionLoad(key);
+-                    if (versionChanged) {
+-                        this.setDirty(key);
 -                    }
 -                });
 -            }
@@ -33352,17 +33304,17 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        throw new IllegalStateException("Only chunk system can write state, offending class:" + this.getClass().getName()); // Paper - rewrite chunk system
      }
  
-     private <T> Dynamic<T> writeChunk(ChunkPos chunkPos, DynamicOps<T> ops) {
-@@ -0,0 +0,0 @@ public class SectionStorage<R, P> implements AutoCloseable {
-     protected void onSectionLoad(long pos) {
+     private <T> Dynamic<T> writeChunk(ChunkPos pos, DynamicOps<T> ops) {
+@@ -282,7 +245,7 @@ public class SectionStorage<R, P> implements AutoCloseable {
+     protected void onSectionLoad(long sectionKey) {
      }
  
--    protected void setDirty(long pos) {
-+    public void setDirty(long pos) { // Paper - public
-         Optional<R> optional = this.storage.get(pos);
+-    protected void setDirty(long sectionPos) {
++    public void setDirty(long sectionPos) { // Paper - public
+         Optional<R> optional = this.storage.get(sectionPos);
          if (optional != null && !optional.isEmpty()) {
-             this.dirtyChunks.add(ChunkPos.asLong(SectionPos.x(pos), SectionPos.z(pos)));
-@@ -0,0 +0,0 @@ public class SectionStorage<R, P> implements AutoCloseable {
+             this.dirtyChunks.add(ChunkPos.asLong(SectionPos.x(sectionPos), SectionPos.z(sectionPos)));
+@@ -303,7 +266,7 @@ public class SectionStorage<R, P> implements AutoCloseable {
  
      @Override
      public void close() throws IOException {
@@ -33370,40 +33322,40 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        this.moonrise$close(); // Paper - rewrite chunk system
      }
  
-     static record PackedChunk<T>(Int2ObjectMap<T> sectionsByY, boolean versionChanged) {
-diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java b/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
-@@ -0,0 +0,0 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
-             long j = nbt.getLong("InhabitedTime");
-             ChunkStatus chunkstatus = ChunkStatus.byName(nbt.getString("Status"));
-             UpgradeData chunkconverter = nbt.contains("UpgradeData", 10) ? new UpgradeData(nbt.getCompound("UpgradeData"), world) : UpgradeData.EMPTY;
--            boolean flag = nbt.getBoolean("isLightOn");
-+            boolean flag = chunkstatus.isOrAfter(ChunkStatus.LIGHT) && (nbt.get("isLightOn") != null && nbt.getInt(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.STARLIGHT_VERSION_TAG) == ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.STARLIGHT_LIGHT_VERSION); // Paper - starlight
-             DataResult dataresult;
-             Logger logger;
-             BlendingData.Packed blendingdata_d;
-@@ -0,0 +0,0 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
-                 DataLayer nibblearray = nbttagcompound3.contains("BlockLight", 7) ? new DataLayer(nbttagcompound3.getByteArray("BlockLight")) : null;
-                 DataLayer nibblearray1 = nbttagcompound3.contains("SkyLight", 7) ? new DataLayer(nbttagcompound3.getByteArray("SkyLight")) : null;
+     record PackedChunk<T>(Int2ObjectMap<T> sectionsByY, boolean versionChanged) {
+diff --git a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
+index cf6e2053d81f7b0f8c8e58b9c0fad3285ebc047d..70a9972252576e039ac126f6057a6ed66b80cdfc 100644
+--- a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
++++ b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
+@@ -148,7 +148,7 @@ public record SerializableChunkData(
+             UpgradeData upgradeData = tag.contains("UpgradeData", 10)
+                 ? new UpgradeData(tag.getCompound("UpgradeData"), levelHeightAccessor)
+                 : UpgradeData.EMPTY;
+-            boolean _boolean = tag.getBoolean("isLightOn");
++            boolean _boolean = chunkStatus.isOrAfter(ChunkStatus.LIGHT) && (tag.get("isLightOn") != null && tag.getInt(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.STARLIGHT_VERSION_TAG) == ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.STARLIGHT_LIGHT_VERSION); // Paper - starlight
+             BlendingData.Packed packed;
+             if (tag.contains("blending_data", 10)) {
+                 packed = BlendingData.Packed.CODEC.parse(NbtOps.INSTANCE, tag.getCompound("blending_data")).resultOrPartial(LOGGER::error).orElse(null);
+@@ -249,7 +249,17 @@ public record SerializableChunkData(
  
--                list4.add(new SerializableChunkData.SectionData(b0, chunksection, nibblearray, nibblearray1));
+                 DataLayer dataLayer = compound2.contains("BlockLight", 7) ? new DataLayer(compound2.getByteArray("BlockLight")) : null;
+                 DataLayer dataLayer1 = compound2.contains("SkyLight", 7) ? new DataLayer(compound2.getByteArray("SkyLight")) : null;
+-                list8.add(new SerializableChunkData.SectionData(_byte, levelChunkSection, dataLayer, dataLayer1));
 +                // Paper start - starlight
-+                SerializableChunkData.SectionData serializableChunkData = new SerializableChunkData.SectionData(b0, chunksection, nibblearray, nibblearray1);
-+                if (sectionData.contains(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.BLOCKLIGHT_STATE_TAG, Tag.TAG_ANY_NUMERIC)) {
++                SerializableChunkData.SectionData serializableChunkData = new SerializableChunkData.SectionData(_byte, levelChunkSection, dataLayer, dataLayer1);
++                if (sectionData.contains(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.BLOCKLIGHT_STATE_TAG, net.minecraft.nbt.Tag.TAG_ANY_NUMERIC)) {
 +                    ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)serializableChunkData).starlight$setBlockLightState(sectionData.getInt(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.BLOCKLIGHT_STATE_TAG));
 +                }
 +
-+                if (sectionData.contains(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.SKYLIGHT_STATE_TAG, Tag.TAG_ANY_NUMERIC)) {
++                if (sectionData.contains(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.SKYLIGHT_STATE_TAG, net.minecraft.nbt.Tag.TAG_ANY_NUMERIC)) {
 +                    ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)serializableChunkData).starlight$setSkyLightState(sectionData.getInt(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.SKYLIGHT_STATE_TAG));
 +                }
-+                list4.add(serializableChunkData);
++                list8.add(serializableChunkData);
 +                // Paper end - starlight
              }
  
-             // CraftBukkit - ChunkBukkitValues
-@@ -0,0 +0,0 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
+             return new SerializableChunkData(
+@@ -276,6 +286,59 @@ public record SerializableChunkData(
          }
      }
  
@@ -33460,56 +33412,59 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - starlight
 +
-     public ProtoChunk read(ServerLevel world, PoiManager poiStorage, RegionStorageInfo key, ChunkPos expectedPos) {
-         if (!Objects.equals(expectedPos, this.chunkPos)) {
-             SerializableChunkData.LOGGER.error("Chunk file at {} is in the wrong location; relocating. (Expected {}, got {})", new Object[]{expectedPos, expectedPos, this.chunkPos});
-@@ -0,0 +0,0 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
- 
-             if (serializablechunkdata_b.chunkSection != null) {
-                 achunksection[world.getSectionIndexFromSectionY(serializablechunkdata_b.y)] = serializablechunkdata_b.chunkSection;
--                poiStorage.checkConsistencyWithBlocks(sectionposition, serializablechunkdata_b.chunkSection);
-+                //poiStorage.checkConsistencyWithBlocks(sectionposition, serializablechunkdata_b.chunkSection); // Paper - rewrite chunk system
+     public ProtoChunk read(ServerLevel level, PoiManager poiManager, RegionStorageInfo regionStorageInfo, ChunkPos pos) {
+         if (!Objects.equals(pos, this.chunkPos)) {
+             LOGGER.error("Chunk file at {} is in the wrong location; relocating. (Expected {}, got {})", pos, pos, this.chunkPos);
+@@ -294,7 +357,7 @@ public record SerializableChunkData(
+             SectionPos sectionPos = SectionPos.of(pos, sectionData.y);
+             if (sectionData.chunkSection != null) {
+                 levelChunkSections[level.getSectionIndexFromSectionY(sectionData.y)] = sectionData.chunkSection;
+-                poiManager.checkConsistencyWithBlocks(sectionPos, sectionData.chunkSection);
++                //poiManager.checkConsistencyWithBlocks(sectionPos, sectionData.chunkSection); // Paper - rewrite chunk system
              }
  
-             boolean flag2 = serializablechunkdata_b.blockLight != null;
-@@ -0,0 +0,0 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
+             boolean flag1 = sectionData.blockLight != null;
+@@ -376,7 +439,7 @@ public record SerializableChunkData(
          }
  
-         if (chunktype == ChunkType.LEVELCHUNK) {
--            return new ImposterProtoChunk((LevelChunk) object, false);
-+            return this.loadStarlightLightData(world, new ImposterProtoChunk((LevelChunk) object, false)); // Paper - starlight
+         if (chunkType == ChunkType.LEVELCHUNK) {
+-            return new ImposterProtoChunk((LevelChunk)chunkAccess, false);
++            return this.loadStarlightLightData(level, new ImposterProtoChunk((LevelChunk)chunkAccess, false)); // Paper - starlight
          } else {
-             ProtoChunk protochunk1 = (ProtoChunk) object;
-             Iterator iterator2 = this.entities.iterator();
-@@ -0,0 +0,0 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
-                 protochunk1.setCarvingMask(new CarvingMask(this.carvingMask, ((ChunkAccess) object).getMinY()));
+             ProtoChunk protoChunk1 = (ProtoChunk)chunkAccess;
+ 
+@@ -399,7 +462,7 @@ public record SerializableChunkData(
+                 protoChunk1.setCarvingMask(new CarvingMask(this.carvingMask, chunkAccess.getMinY()));
              }
  
--            return protochunk1;
-+            return this.loadStarlightLightData(world, protochunk1); // Paper - starlight
+-            return protoChunk1;
++            return this.loadStarlightLightData(level, protoChunk1); // Paper - starlight
          }
      }
  
-@@ -0,0 +0,0 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
-             throw new IllegalArgumentException("Chunk can't be serialized: " + String.valueOf(chunk));
+@@ -427,22 +490,48 @@ public record SerializableChunkData(
+             throw new IllegalArgumentException("Chunk can't be serialized: " + chunk);
          } else {
-             ChunkPos chunkcoordintpair = chunk.getPos();
--            List<SerializableChunkData.SectionData> list = new ArrayList();
-+            List<SerializableChunkData.SectionData> list = new ArrayList(); final List<SerializableChunkData.SectionData> sections = list; // Paper - starlight - OBFHELPER
-             LevelChunkSection[] achunksection = chunk.getSections();
-             ThreadedLevelLightEngine lightenginethreaded = world.getChunkSource().getLightEngine();
+             ChunkPos pos = chunk.getPos();
+-            List<SerializableChunkData.SectionData> list = new ArrayList<>();
++            List<SerializableChunkData.SectionData> list = new ArrayList<>(); final List<SerializableChunkData.SectionData> sectionsList = list; // Paper - starlight - OBFHELPER
+             LevelChunkSection[] sections = chunk.getSections();
+             LevelLightEngine lightEngine = level.getChunkSource().getLightEngine();
  
--            for (int i = lightenginethreaded.getMinLightSection(); i < lightenginethreaded.getMaxLightSection(); ++i) {
--                int j = chunk.getSectionIndexFromSectionY(i);
--                boolean flag = j >= 0 && j < achunksection.length;
--                DataLayer nibblearray = lightenginethreaded.getLayerListener(LightLayer.BLOCK).getDataLayerData(SectionPos.of(chunkcoordintpair, i));
--                DataLayer nibblearray1 = lightenginethreaded.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(chunkcoordintpair, i));
--                DataLayer nibblearray2 = nibblearray != null && !nibblearray.isEmpty() ? nibblearray.copy() : null;
--                DataLayer nibblearray3 = nibblearray1 != null && !nibblearray1.isEmpty() ? nibblearray1.copy() : null;
+-            for (int lightSection = lightEngine.getMinLightSection(); lightSection < lightEngine.getMaxLightSection(); lightSection++) {
+-                int sectionIndexFromSectionY = chunk.getSectionIndexFromSectionY(lightSection);
+-                boolean flag = sectionIndexFromSectionY >= 0 && sectionIndexFromSectionY < sections.length;
+-                DataLayer dataLayerData = lightEngine.getLayerListener(LightLayer.BLOCK).getDataLayerData(SectionPos.of(pos, lightSection));
+-                DataLayer dataLayerData1 = lightEngine.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(pos, lightSection));
+-                DataLayer dataLayer = dataLayerData != null && !dataLayerData.isEmpty() ? dataLayerData.copy() : null;
+-                DataLayer dataLayer1 = dataLayerData1 != null && !dataLayerData1.isEmpty() ? dataLayerData1.copy() : null;
+-                if (flag || dataLayer != null || dataLayer1 != null) {
+-                    LevelChunkSection levelChunkSection = flag ? sections[sectionIndexFromSectionY].copy() : null;
+-                    list.add(new SerializableChunkData.SectionData(lightSection, levelChunkSection, dataLayer, dataLayer1));
 +            // Paper start - starlight
-+            final int minLightSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinLightSection(world);
-+            final int maxLightSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxLightSection(world);
-+            final int minBlockSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(world);
++            final int minLightSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinLightSection(level);
++            final int maxLightSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxLightSection(level);
++            final int minBlockSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(level);
 +
 +            final LevelChunkSection[] chunkSections = chunk.getSections();
 +            final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] blockNibbles = ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)chunk).starlight$getBlockNibbles();
@@ -33518,17 +33473,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            for (int lightSection = minLightSection; lightSection <= maxLightSection; ++lightSection) {
 +                final int lightSectionIdx = lightSection - minLightSection;
 +                final int blockSectionIdx = lightSection - minBlockSection;
- 
--                if (flag || nibblearray2 != null || nibblearray3 != null) {
--                    LevelChunkSection chunksection = flag ? achunksection[j].copy() : null;
++
 +                final LevelChunkSection chunkSection = (blockSectionIdx >= 0 && blockSectionIdx < chunkSections.length) ? chunkSections[blockSectionIdx].copy() : null;
 +                final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray.SaveState blockNibble = blockNibbles[lightSectionIdx].getSaveState();
 +                final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray.SaveState skyNibble = skyNibbles[lightSectionIdx].getSaveState();
- 
--                    list.add(new SerializableChunkData.SectionData(i, chunksection, nibblearray2, nibblearray3));
++
 +                if (chunkSection == null && blockNibble == null && skyNibble == null) {
 +                    continue;
-                 }
++                }
 +
 +                final SerializableChunkData.SectionData sectionData = new SerializableChunkData.SectionData(
 +                    lightSection, chunkSection,
@@ -33542,27 +33494,25 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +                if (skyNibble != null) {
 +                    ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)sectionData).starlight$setSkyLightState(skyNibble.state);
-+                }
+                 }
 +
-+                sections.add(sectionData);
++                sectionsList.add(sectionData);
              }
 +            // Paper end - starlight
  
-             List<CompoundTag> list1 = new ArrayList(chunk.getBlockEntitiesPos().size());
-             Iterator iterator = chunk.getBlockEntitiesPos().iterator();
-@@ -0,0 +0,0 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
-         Iterator iterator = this.sectionData.iterator();
+             List<CompoundTag> list1 = new ArrayList<>(chunk.getBlockEntitiesPos().size());
  
-         while (iterator.hasNext()) {
--            SerializableChunkData.SectionData serializablechunkdata_b = (SerializableChunkData.SectionData) iterator.next();
--            CompoundTag nbttagcompound1 = new CompoundTag();
-+            SerializableChunkData.SectionData serializablechunkdata_b = (SerializableChunkData.SectionData) iterator.next(); final SerializableChunkData.SectionData sectionData = serializablechunkdata_b; // Paper - starlight - OBFHELPER
-+            CompoundTag nbttagcompound1 = new CompoundTag(); final CompoundTag sectionNBT = nbttagcompound1; // Paper - starlight - OBFHELPER
-             LevelChunkSection chunksection = serializablechunkdata_b.chunkSection;
+@@ -540,7 +629,7 @@ public record SerializableChunkData(
+         Codec<PalettedContainerRO<Holder<Biome>>> codec = makeBiomeCodec(this.biomeRegistry);
  
-             if (chunksection != null) {
-@@ -0,0 +0,0 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
-                 nbttagcompound1.putByteArray("SkyLight", serializablechunkdata_b.skyLight.getData());
+         for (SerializableChunkData.SectionData sectionData : this.sectionData) {
+-            CompoundTag compoundTag1 = new CompoundTag();
++            CompoundTag compoundTag1 = new CompoundTag(); final CompoundTag sectionNBT = compoundTag1; // Paper - starlight - OBFHELPER
+             LevelChunkSection levelChunkSection = sectionData.chunkSection;
+             if (levelChunkSection != null) {
+                 compoundTag1.put("block_states", BLOCK_STATE_CODEC.encodeStart(NbtOps.INSTANCE, levelChunkSection.getStates()).getOrThrow());
+@@ -555,6 +644,19 @@ public record SerializableChunkData(
+                 compoundTag1.putByteArray("SkyLight", sectionData.skyLight.getData());
              }
  
 +            // Paper start - starlight
@@ -33578,29 +33528,29 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            }
 +            // Paper end - starlight
 +
-             if (!nbttagcompound1.isEmpty()) {
-                 nbttagcompound1.putByte("Y", (byte) serializablechunkdata_b.y);
-                 nbttaglist.add(nbttagcompound1);
-@@ -0,0 +0,0 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
-             nbttagcompound.put("ChunkBukkitValues", this.persistentDataContainer);
+             if (!compoundTag1.isEmpty()) {
+                 compoundTag1.putByte("Y", (byte)sectionData.y);
+                 listTag.add(compoundTag1);
+@@ -589,6 +691,14 @@ public record SerializableChunkData(
+             compoundTag.put("ChunkBukkitValues", this.persistentDataContainer);
          }
          // CraftBukkit end
 +        // Paper start - starlight
 +        if (this.lightCorrect && !this.chunkStatus.isBefore(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) {
 +            // clobber vanilla value to force vanilla to relight
-+            nbttagcompound.putBoolean("isLightOn", false);
++            compoundTag.putBoolean("isLightOn", false);
 +            // store our light version
-+            nbttagcompound.putInt(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.STARLIGHT_VERSION_TAG, ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.STARLIGHT_LIGHT_VERSION);
++            compoundTag.putInt(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.STARLIGHT_VERSION_TAG, ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.STARLIGHT_LIGHT_VERSION);
 +        }
 +        // Paper end - starlight
-         return nbttagcompound;
+         return compoundTag;
      }
  
-@@ -0,0 +0,0 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
-         return nbttaglist;
+@@ -747,6 +857,66 @@ public record SerializableChunkData(
+         }
      }
  
--    public static record SectionData(int y, @Nullable LevelChunkSection chunkSection, @Nullable DataLayer blockLight, @Nullable DataLayer skyLight) {
+-    public record SectionData(int y, @Nullable LevelChunkSection chunkSection, @Nullable DataLayer blockLight, @Nullable DataLayer skyLight) {
 +    // Paper start - starlight - convert from record
 +    public static final class SectionData implements ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData { // Paper - starlight - our diff
 +        private final int y;
@@ -33662,14 +33612,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            return skyLight;
 +        }
 +        // Paper end - starlight - convert from record
- 
      }
- 
-diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
-@@ -0,0 +0,0 @@ import net.minecraft.util.datafix.DataFixTypes;
+ }
+diff --git a/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java b/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
+index 41ddaceb7485626b1f2ee258c2142eb3114c106e..f883c6400281788982403d0af3ee28613e9a29b1 100644
+--- a/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
++++ b/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
+@@ -14,7 +14,7 @@ import net.minecraft.util.datafix.DataFixTypes;
  import net.minecraft.world.level.ChunkPos;
  
  public class SimpleRegionStorage implements AutoCloseable {
@@ -33678,11 +33627,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      private final DataFixer fixerUpper;
      private final DataFixTypes dataFixType;
  
-diff --git a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java
-+++ b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java
-@@ -0,0 +0,0 @@ import javax.annotation.Nullable;
+diff --git a/net/minecraft/world/level/entity/EntityTickList.java b/net/minecraft/world/level/entity/EntityTickList.java
+index 342c83309b19c64d86e0dd97c1756c96be52772b..423779a2b690f387a4f0bd07b97b50e0baefda76 100644
+--- a/net/minecraft/world/level/entity/EntityTickList.java
++++ b/net/minecraft/world/level/entity/EntityTickList.java
+@@ -9,52 +9,38 @@ import javax.annotation.Nullable;
  import net.minecraft.world.entity.Entity;
  
  public class EntityTickList {
@@ -33700,9 +33649,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -                this.passive.put(entry.getIntKey(), entry.getValue());
 -            }
 -
--            Int2ObjectMap<Entity> int2ObjectMap = this.active;
+-            Int2ObjectMap<Entity> map = this.active;
 -            this.active = this.passive;
--            this.passive = int2ObjectMap;
+-            this.passive = map;
 -        }
 +        // Paper - rewrite chunk system
      }
@@ -33724,15 +33673,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return this.entities.contains(entity); // Paper - rewrite chunk system
      }
  
-     public void forEach(Consumer<Entity> action) {
+     public void forEach(Consumer<Entity> entity) {
 -        if (this.iterated != null) {
 -            throw new UnsupportedOperationException("Only one concurrent iteration supported");
 -        } else {
 -            this.iterated = this.active;
 -
 -            try {
--                for (Entity entity : this.active.values()) {
--                    action.accept(entity);
+-                for (Entity entity1 : this.active.values()) {
+-                    entity.accept(entity1);
 -                }
 -            } finally {
 -                this.iterated = null;
@@ -33742,7 +33691,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        final ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet.Iterator<Entity> iterator = this.entities.iterator();
 +        try {
 +            while (iterator.hasNext()) {
-+                action.accept(iterator.next());
++                entity.accept(iterator.next());
              }
 +        } finally {
 +            iterator.finishedIterating();
@@ -33750,33 +33699,33 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - rewrite chunk system
      }
  }
-diff --git a/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java b/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
-+++ b/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
-@@ -0,0 +0,0 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator {
+diff --git a/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java b/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
+index 29d9f6e54421c539e9e55ab9f51b4c872da3fbb8..d77016287f5f9a0964d56f05d2d5256ef2e6e86c 100644
+--- a/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
++++ b/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
+@@ -78,7 +78,7 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator {
          return CompletableFuture.supplyAsync(() -> {
-             this.doCreateBiomes(blender, noiseConfig, structureAccessor, chunk);
+             this.doCreateBiomes(blender, randomState, structureManager, chunk);
              return chunk;
 -        }, Util.backgroundExecutor().forName("init_biomes"));
 +        }, Runnable::run); // Paper - rewrite chunk system
      }
  
-     private void doCreateBiomes(Blender blender, RandomState noiseConfig, StructureManager structureAccessor, ChunkAccess chunk) {
-@@ -0,0 +0,0 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator {
+     private void doCreateBiomes(Blender blender, RandomState random, StructureManager structureManager, ChunkAccess chunk) {
+@@ -318,7 +318,7 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator {
              }
  
-             return ichunkaccess1;
+             return var20;
 -        }, Util.backgroundExecutor().forName("wgen_fill_noise"));
 +        }, Runnable::run); // Paper - rewrite chunk system
      }
  
-     private ChunkAccess doFill(Blender blender, StructureManager structureAccessor, RandomState noiseConfig, ChunkAccess chunk, int minimumCellY, int cellHeight) {
-diff --git a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java
-+++ b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java
-@@ -0,0 +0,0 @@ public class StructureCheck {
+     private ChunkAccess doFill(Blender blender, StructureManager structureManager, RandomState random, ChunkAccess chunk, int minCellY, int cellCountY) {
+diff --git a/net/minecraft/world/level/levelgen/structure/StructureCheck.java b/net/minecraft/world/level/levelgen/structure/StructureCheck.java
+index 06b54c0bec4031689d5c2da5cfea4ef28dbd16bc..f7dc4957b38878ddd3bfc7546be8a4e0af65c807 100644
+--- a/net/minecraft/world/level/levelgen/structure/StructureCheck.java
++++ b/net/minecraft/world/level/levelgen/structure/StructureCheck.java
+@@ -47,8 +47,13 @@ public class StructureCheck {
      private final BiomeSource biomeSource;
      private final long seed;
      private final DataFixer fixerUpper;
@@ -33791,66 +33740,66 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    // Paper end - rewrite chunk system
  
      public StructureCheck(
-         ChunkScanAccess chunkIoWorker,
-@@ -0,0 +0,0 @@ public class StructureCheck {
+         ChunkScanAccess storageAccess,
+@@ -90,7 +95,7 @@ public class StructureCheck {
  
-     public StructureCheckResult checkStart(ChunkPos pos, Structure type, StructurePlacement placement, boolean skipReferencedStructures) {
-         long l = pos.toLong();
--        Object2IntMap<Structure> object2IntMap = this.loadedChunks.get(l);
-+        Object2IntMap<Structure> object2IntMap = this.loadedChunksSafe.get(l); // Paper - rewrite chunk system
-         if (object2IntMap != null) {
-             return this.checkStructureInfo(object2IntMap, type, skipReferencedStructures);
+     public StructureCheckResult checkStart(ChunkPos chunkPos, Structure structure, StructurePlacement placement, boolean skipKnownStructures) {
+         long packedChunkPos = chunkPos.toLong();
+-        Object2IntMap<Structure> map = this.loadedChunks.get(packedChunkPos);
++        Object2IntMap<Structure> map = this.loadedChunksSafe.get(packedChunkPos); // Paper - rewrite chunk system
+         if (map != null) {
+             return this.checkStructureInfo(map, structure, skipKnownStructures);
          } else {
-@@ -0,0 +0,0 @@ public class StructureCheck {
-             } else if (!placement.applyAdditionalChunkRestrictions(pos.x, pos.z, this.seed, this.getSaltOverride(type))) { // Paper - add missing structure seed configs
+@@ -100,9 +105,11 @@ public class StructureCheck {
+             } else if (!placement.applyAdditionalChunkRestrictions(chunkPos.x, chunkPos.z, this.seed, this.getSaltOverride(structure))) { // Paper - add missing structure seed configs
                  return StructureCheckResult.START_NOT_PRESENT;
              } else {
--                boolean bl = this.featureChecks
--                    .computeIfAbsent(type, structure2 -> new Long2BooleanOpenHashMap())
--                    .computeIfAbsent(l, chunkPos -> this.canCreateStructure(pos, type));
+-                boolean flag = this.featureChecks
+-                    .computeIfAbsent(structure, structure1 -> new Long2BooleanOpenHashMap())
+-                    .computeIfAbsent(packedChunkPos, l -> this.canCreateStructure(chunkPos, structure));
 +                // Paper start - rewrite chunk system
-+                boolean bl = this.featureChecksSafe
-+                    .computeIfAbsent(type, structure2 -> new ca.spottedleaf.moonrise.common.map.SynchronisedLong2BooleanMap(PER_FEATURE_CHECK_LIMIT))
-+                    .getOrCompute(l, chunkPos -> this.canCreateStructure(pos, type));
++                boolean flag = this.featureChecksSafe
++                    .computeIfAbsent(structure, structure1 -> new ca.spottedleaf.moonrise.common.map.SynchronisedLong2BooleanMap(PER_FEATURE_CHECK_LIMIT))
++                    .getOrCompute(packedChunkPos, l -> this.canCreateStructure(chunkPos, structure));
 +                // Paper end - rewrite chunk system
-                 return !bl ? StructureCheckResult.START_NOT_PRESENT : StructureCheckResult.CHUNK_LOAD_NEEDED;
+                 return !flag ? StructureCheckResult.START_NOT_PRESENT : StructureCheckResult.CHUNK_LOAD_NEEDED;
              }
          }
-@@ -0,0 +0,0 @@ public class StructureCheck {
+@@ -228,15 +235,25 @@ public class StructureCheck {
      }
  
-     private void storeFullResults(long pos, Object2IntMap<Structure> referencesByStructure) {
--        this.loadedChunks.put(pos, deduplicateEmptyMap(referencesByStructure));
--        this.featureChecks.values().forEach(generationPossibilityByChunkPos -> generationPossibilityByChunkPos.remove(pos));
+     private void storeFullResults(long chunkPos, Object2IntMap<Structure> structureChunks) {
+-        this.loadedChunks.put(chunkPos, deduplicateEmptyMap(structureChunks));
+-        this.featureChecks.values().forEach(map -> map.remove(chunkPos));
 +        // Paper start - rewrite chunk system
-+        this.loadedChunksSafe.put(pos, deduplicateEmptyMap(referencesByStructure));
++        this.loadedChunksSafe.put(chunkPos, deduplicateEmptyMap(structureChunks));
 +        // once we insert into loadedChunks, we don't really need to be very careful about removing everything
 +        // from this map, as everything that checks this map uses loadedChunks first
 +        // so, one way or another it's a race condition that doesn't matter
 +        for (ca.spottedleaf.moonrise.common.map.SynchronisedLong2BooleanMap value : this.featureChecksSafe.values()) {
-+            value.remove(pos);
++            value.remove(chunkPos);
 +        }
 +        // Paper end - rewrite chunk system
      }
  
      public void incrementReference(ChunkPos pos, Structure structure) {
--        this.loadedChunks.compute(pos.toLong(), (posx, referencesByStructure) -> {
--            if (referencesByStructure == null || referencesByStructure.isEmpty()) {
-+        this.loadedChunksSafe.compute(pos.toLong(), (posx, referencesByStructure) -> { // Paper start - rewrite chunk system
-+            if (referencesByStructure == null) {
-                 referencesByStructure = new Object2IntOpenHashMap<>();
+-        this.loadedChunks.compute(pos.toLong(), (_long, map) -> {
+-            if (map == null || map.isEmpty()) {
++        this.loadedChunksSafe.compute(pos.toLong(), (_long, map) -> { // Paper start - rewrite chunk system
++            if (map == null) {
+                 map = new Object2IntOpenHashMap<>();
 +            } else {
-+                referencesByStructure = referencesByStructure instanceof Object2IntOpenHashMap<Structure> fastClone ? fastClone.clone() : new Object2IntOpenHashMap<>(referencesByStructure);
++                map = map instanceof Object2IntOpenHashMap<Structure> fastClone ? fastClone.clone() : new Object2IntOpenHashMap<>(map);
              }
 +            // Paper end - rewrite chunk system
  
-             referencesByStructure.computeInt(structure, (feature, references) -> references == null ? 1 : references + 1);
-             return referencesByStructure;
-diff --git a/src/main/java/net/minecraft/world/level/lighting/LevelLightEngine.java b/src/main/java/net/minecraft/world/level/lighting/LevelLightEngine.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/lighting/LevelLightEngine.java
-+++ b/src/main/java/net/minecraft/world/level/lighting/LevelLightEngine.java
-@@ -0,0 +0,0 @@ import net.minecraft.world.level.LightLayer;
+             map.computeInt(structure, (structure1, integer) -> integer == null ? 1 : integer + 1);
+             return map;
+diff --git a/net/minecraft/world/level/lighting/LevelLightEngine.java b/net/minecraft/world/level/lighting/LevelLightEngine.java
+index ca23af013967b50420ebee178878ea79333de53b..d41b9266625ca6c5e32c5126f35a1f7733159cfc 100644
+--- a/net/minecraft/world/level/lighting/LevelLightEngine.java
++++ b/net/minecraft/world/level/lighting/LevelLightEngine.java
+@@ -9,151 +9,111 @@ import net.minecraft.world.level.LightLayer;
  import net.minecraft.world.level.chunk.DataLayer;
  import net.minecraft.world.level.chunk.LightChunkGetter;
  
@@ -33888,15 +33837,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - rewrite chunk system
  
-     public LevelLightEngine(LightChunkGetter chunkProvider, boolean hasBlockLight, boolean hasSkyLight) {
-         this.levelHeightAccessor = chunkProvider.getLevel();
--        this.blockEngine = hasBlockLight ? new BlockLightEngine(chunkProvider) : null;
--        this.skyEngine = hasSkyLight ? new SkyLightEngine(chunkProvider) : null;
+     public LevelLightEngine(LightChunkGetter lightChunkGetter, boolean blockLight, boolean skyLight) {
+         this.levelHeightAccessor = lightChunkGetter.getLevel();
+-        this.blockEngine = blockLight ? new BlockLightEngine(lightChunkGetter) : null;
+-        this.skyEngine = skyLight ? new SkyLightEngine(lightChunkGetter) : null;
 +        // Paper start - rewrite chunk system
-+        if (chunkProvider.getLevel() instanceof net.minecraft.world.level.Level) {
-+            this.lightEngine = new ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface(chunkProvider, hasSkyLight, hasBlockLight, (LevelLightEngine)(Object)this);
++        if (lightChunkGetter.getLevel() instanceof net.minecraft.world.level.Level) {
++            this.lightEngine = new ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface(lightChunkGetter, skyLight, blockLight, (LevelLightEngine)(Object)this);
 +        } else {
-+            this.lightEngine = new ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface(null, hasSkyLight, hasBlockLight, (LevelLightEngine)(Object)this);
++            this.lightEngine = new ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface(null, skyLight, blockLight, (LevelLightEngine)(Object)this);
 +        }
 +        // Paper end - rewrite chunk system
      }
@@ -33946,25 +33895,25 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      @Override
-     public void updateSectionStatus(SectionPos pos, boolean notReady) {
+     public void updateSectionStatus(SectionPos pos, boolean isEmpty) {
 -        if (this.blockEngine != null) {
--            this.blockEngine.updateSectionStatus(pos, notReady);
+-            this.blockEngine.updateSectionStatus(pos, isEmpty);
 -        }
 -
 -        if (this.skyEngine != null) {
--            this.skyEngine.updateSectionStatus(pos, notReady);
+-            this.skyEngine.updateSectionStatus(pos, isEmpty);
 -        }
-+        this.lightEngine.sectionChange(pos, notReady); // Paper - rewrite chunk system
++        this.lightEngine.sectionChange(pos, isEmpty); // Paper - rewrite chunk system
      }
  
      @Override
-     public void setLightEnabled(ChunkPos pos, boolean retainData) {
+     public void setLightEnabled(ChunkPos chunkPos, boolean lightEnabled) {
 -        if (this.blockEngine != null) {
--            this.blockEngine.setLightEnabled(pos, retainData);
+-            this.blockEngine.setLightEnabled(chunkPos, lightEnabled);
 -        }
 -
 -        if (this.skyEngine != null) {
--            this.skyEngine.setLightEnabled(pos, retainData);
+-            this.skyEngine.setLightEnabled(chunkPos, lightEnabled);
 -        }
 +        // Paper - rewrite chunk system
      }
@@ -33981,82 +33930,82 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper - rewrite chunk system
      }
  
-     public LayerLightEventListener getLayerListener(LightLayer lightType) {
--        if (lightType == LightLayer.BLOCK) {
+     public LayerLightEventListener getLayerListener(LightLayer type) {
+-        if (type == LightLayer.BLOCK) {
 -            return (LayerLightEventListener)(this.blockEngine == null ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : this.blockEngine);
 -        } else {
 -            return (LayerLightEventListener)(this.skyEngine == null ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : this.skyEngine);
 -        }
-+        return lightType == LightLayer.BLOCK ? this.lightEngine.getBlockReader() : this.lightEngine.getSkyReader(); // Paper - rewrite chunk system
++        return type == LightLayer.BLOCK ? this.lightEngine.getBlockReader() : this.lightEngine.getSkyReader(); // Paper - rewrite chunk system
      }
  
-     public String getDebugData(LightLayer lightType, SectionPos pos) {
--        if (lightType == LightLayer.BLOCK) {
+     public String getDebugData(LightLayer lightLayer, SectionPos sectionPos) {
+-        if (lightLayer == LightLayer.BLOCK) {
 -            if (this.blockEngine != null) {
--                return this.blockEngine.getDebugData(pos.asLong());
+-                return this.blockEngine.getDebugData(sectionPos.asLong());
 -            }
 -        } else if (this.skyEngine != null) {
--            return this.skyEngine.getDebugData(pos.asLong());
+-            return this.skyEngine.getDebugData(sectionPos.asLong());
 -        }
 -
 -        return "n/a";
 +        return "n/a"; // Paper - rewrite chunk system
      }
  
-     public LayerLightSectionStorage.SectionType getDebugSectionType(LightLayer lightType, SectionPos pos) {
--        if (lightType == LightLayer.BLOCK) {
+     public LayerLightSectionStorage.SectionType getDebugSectionType(LightLayer lightLayer, SectionPos sectionPos) {
+-        if (lightLayer == LightLayer.BLOCK) {
 -            if (this.blockEngine != null) {
--                return this.blockEngine.getDebugSectionType(pos.asLong());
+-                return this.blockEngine.getDebugSectionType(sectionPos.asLong());
 -            }
 -        } else if (this.skyEngine != null) {
--            return this.skyEngine.getDebugSectionType(pos.asLong());
+-            return this.skyEngine.getDebugSectionType(sectionPos.asLong());
 -        }
 -
 -        return LayerLightSectionStorage.SectionType.EMPTY;
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
-     public void queueSectionData(LightLayer lightType, SectionPos pos, @Nullable DataLayer nibbles) {
--        if (lightType == LightLayer.BLOCK) {
+     public void queueSectionData(LightLayer lightLayer, SectionPos sectionPos, @Nullable DataLayer dataLayer) {
+-        if (lightLayer == LightLayer.BLOCK) {
 -            if (this.blockEngine != null) {
--                this.blockEngine.queueSectionData(pos.asLong(), nibbles);
+-                this.blockEngine.queueSectionData(sectionPos.asLong(), dataLayer);
 -            }
 -        } else if (this.skyEngine != null) {
--            this.skyEngine.queueSectionData(pos.asLong(), nibbles);
+-            this.skyEngine.queueSectionData(sectionPos.asLong(), dataLayer);
 -        }
 +        // Paper - rewrite chunk system
      }
  
-     public void retainData(ChunkPos pos, boolean retainData) {
+     public void retainData(ChunkPos pos, boolean retain) {
 -        if (this.blockEngine != null) {
--            this.blockEngine.retainData(pos, retainData);
+-            this.blockEngine.retainData(pos, retain);
 -        }
 -
 -        if (this.skyEngine != null) {
--            this.skyEngine.retainData(pos, retainData);
+-            this.skyEngine.retainData(pos, retain);
 -        }
 +        // Paper - rewrite chunk system
      }
  
-     public int getRawBrightness(BlockPos pos, int ambientDarkness) {
--        int i = this.skyEngine == null ? 0 : this.skyEngine.getLightValue(pos) - ambientDarkness;
--        int j = this.blockEngine == null ? 0 : this.blockEngine.getLightValue(pos);
--        return Math.max(j, i);
-+        return this.lightEngine.getRawBrightness(pos, ambientDarkness); // Paper - rewrite chunk system
+     public int getRawBrightness(BlockPos blockPos, int amount) {
+-        int i = this.skyEngine == null ? 0 : this.skyEngine.getLightValue(blockPos) - amount;
+-        int i1 = this.blockEngine == null ? 0 : this.blockEngine.getLightValue(blockPos);
+-        return Math.max(i1, i);
++        return this.lightEngine.getRawBrightness(blockPos, amount); // Paper - rewrite chunk system
      }
  
-     public boolean lightOnInColumn(long sectionPos) {
+     public boolean lightOnInColumn(long columnPos) {
 -        return this.blockEngine == null
--            || this.blockEngine.storage.lightOnInColumn(sectionPos) && (this.skyEngine == null || this.skyEngine.storage.lightOnInColumn(sectionPos));
+-            || this.blockEngine.storage.lightOnInColumn(columnPos) && (this.skyEngine == null || this.skyEngine.storage.lightOnInColumn(columnPos));
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system // Paper - not implemented on server
      }
  
      public int getLightSectionCount() {
-diff --git a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java
-+++ b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java
-@@ -0,0 +0,0 @@ public abstract class FlowingFluid extends Fluid {
+diff --git a/net/minecraft/world/level/material/FlowingFluid.java b/net/minecraft/world/level/material/FlowingFluid.java
+index 130ef38a50f1df1faa26b433b0c605a4507f71af..f6daca279788c3d983a9ee213df85d5d93fc6eed 100644
+--- a/net/minecraft/world/level/material/FlowingFluid.java
++++ b/net/minecraft/world/level/material/FlowingFluid.java
+@@ -45,6 +45,48 @@ public abstract class FlowingFluid extends Fluid {
      });
      private final Map<FluidState, VoxelShape> shapes = Maps.newIdentityHashMap();
  
@@ -34102,97 +34051,97 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - fluid method optimisations
 +
-     public FlowingFluid() {}
- 
      @Override
-@@ -0,0 +0,0 @@ public abstract class FlowingFluid extends Fluid {
+     protected void createFluidStateDefinition(StateDefinition.Builder<Fluid, FluidState> builder) {
+         builder.add(FALLING);
+@@ -209,61 +251,71 @@ public abstract class FlowingFluid extends Fluid {
          }
      }
  
--    private static boolean canPassThroughWall(Direction face, BlockGetter world, BlockPos pos, BlockState state, BlockPos fromPos, BlockState fromState) {
--        VoxelShape voxelshape = fromState.getCollisionShape(world, fromPos);
+-    private static boolean canPassThroughWall(
+-        Direction direction, BlockGetter level, BlockPos pos, BlockState state, BlockPos spreadPos, BlockState spreadState
+-    ) {
+-        VoxelShape collisionShape = spreadState.getCollisionShape(level, spreadPos);
+-        if (collisionShape == Shapes.block()) {
+-            return false;
+-        } else {
+-            VoxelShape collisionShape1 = state.getCollisionShape(level, pos);
+-            if (collisionShape1 == Shapes.block()) {
+-                return false;
+-            } else if (collisionShape1 == Shapes.empty() && collisionShape == Shapes.empty()) {
+-                return true;
+-            } else {
+-                Object2ByteLinkedOpenHashMap<FlowingFluid.BlockStatePairKey> map;
+-                if (!state.getBlock().hasDynamicShape() && !spreadState.getBlock().hasDynamicShape()) {
+-                    map = OCCLUSION_CACHE.get();
+-                } else {
+-                    map = null;
+-                }
 +    // Paper start - fluid method optimisations
 +    private static boolean canPassThroughWall(final Direction direction, final BlockGetter level,
-+                                             final BlockPos fromPos, final BlockState fromState,
-+                                             final BlockPos toPos, final BlockState toState) {
++                                              final BlockPos fromPos, final BlockState fromState,
++                                              final BlockPos toPos, final BlockState toState) {
 +        if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)fromState).moonrise$emptyCollisionShape() & ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)toState).moonrise$emptyCollisionShape()) {
 +            // don't even try to cache simple cases
 +            return true;
 +        }
  
--        if (voxelshape == Shapes.block()) {
+-                FlowingFluid.BlockStatePairKey blockStatePairKey;
+-                if (map != null) {
+-                    blockStatePairKey = new FlowingFluid.BlockStatePairKey(state, spreadState, direction);
+-                    byte andMoveToFirst = map.getAndMoveToFirst(blockStatePairKey);
+-                    if (andMoveToFirst != 127) {
+-                        return andMoveToFirst != 0;
+-                    }
+-                } else {
+-                    blockStatePairKey = null;
+-                }
 +        if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)fromState).moonrise$occludesFullBlock() | ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)toState).moonrise$occludesFullBlock()) {
 +            // don't even try to cache simple cases
-             return false;
--        } else {
--            VoxelShape voxelshape1 = state.getCollisionShape(world, pos);
--
--            if (voxelshape1 == Shapes.block()) {
--                return false;
--            } else if (voxelshape1 == Shapes.empty() && voxelshape == Shapes.empty()) {
--                return true;
--            } else {
--                Object2ByteLinkedOpenHashMap object2bytelinkedopenhashmap;
--
--                if (!state.getBlock().hasDynamicShape() && !fromState.getBlock().hasDynamicShape()) {
--                    object2bytelinkedopenhashmap = (Object2ByteLinkedOpenHashMap) FlowingFluid.OCCLUSION_CACHE.get();
--                } else {
--                    object2bytelinkedopenhashmap = null;
--                }
++            return false;
 +        }
  
--                FlowingFluid.BlockStatePairKey fluidtypeflowing_a;
+-                boolean flag = !Shapes.mergedFaceOccludes(collisionShape1, collisionShape, direction);
+-                if (map != null) {
+-                    if (map.size() == 200) {
+-                        map.removeLastByte();
+-                    }
 +        final ca.spottedleaf.moonrise.patches.collisions.util.FluidOcclusionCacheKey[] cache = ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)fromState).moonrise$hasCache() & ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)toState).moonrise$hasCache() ?
 +            COLLISION_OCCLUSION_CACHE.get() : null;
  
--                if (object2bytelinkedopenhashmap != null) {
--                    fluidtypeflowing_a = new FlowingFluid.BlockStatePairKey(state, fromState, face);
--                    byte b0 = object2bytelinkedopenhashmap.getAndMoveToFirst(fluidtypeflowing_a);
+-                    map.putAndMoveToFirst(blockStatePairKey, (byte)(flag ? 1 : 0));
+-                }
 +        final int keyIndex
 +            = (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)fromState).moonrise$uniqueId1() ^ ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)toState).moonrise$uniqueId2() ^ ((ca.spottedleaf.moonrise.patches.collisions.util.CollisionDirection)(Object)direction).moonrise$uniqueId())
 +            & (COLLISION_OCCLUSION_CACHE_SIZE - 1);
  
--                    if (b0 != 127) {
--                        return b0 != 0;
--                    }
--                } else {
--                    fluidtypeflowing_a = null;
--                }
--
--                boolean flag = !Shapes.mergedFaceOccludes(voxelshape1, voxelshape, face);
+-                return flag;
 +        if (cache != null) {
 +            final ca.spottedleaf.moonrise.patches.collisions.util.FluidOcclusionCacheKey cached = cache[keyIndex];
 +            if (cached != null && cached.first() == fromState && cached.second() == toState && cached.direction() == direction) {
 +                return cached.result();
-+            }
-+        }
- 
--                if (object2bytelinkedopenhashmap != null) {
--                    if (object2bytelinkedopenhashmap.size() == 200) {
--                        object2bytelinkedopenhashmap.removeLastByte();
--                    }
+             }
+         }
++
 +        final VoxelShape shape1 = fromState.getCollisionShape(level, fromPos);
 +        final VoxelShape shape2 = toState.getCollisionShape(level, toPos);
- 
--                    object2bytelinkedopenhashmap.putAndMoveToFirst(fluidtypeflowing_a, (byte) (flag ? 1 : 0));
--                }
++
 +        final boolean result = !Shapes.mergedFaceOccludes(shape1, shape2, direction);
- 
--                return flag;
--            }
++
 +        if (cache != null) {
 +            // we can afford to replace in-use keys more often due to the excessive caching the collision patch does in mergedFaceOccludes
 +            cache[keyIndex] = new ca.spottedleaf.moonrise.patches.collisions.util.FluidOcclusionCacheKey(fromState, toState, direction, result);
-         }
++        }
 +
 +        return result;
      }
 +    // Paper end - fluid method optimisations
++
  
      public abstract Fluid getFlowing();
  
      public FluidState getFlowing(int level, boolean falling) {
--        return (FluidState) ((FluidState) this.getFlowing().defaultFluidState().setValue(FlowingFluid.LEVEL, level)).setValue(FlowingFluid.FALLING, falling);
+-        return this.getFlowing().defaultFluidState().setValue(LEVEL, Integer.valueOf(level)).setValue(FALLING, Boolean.valueOf(falling));
 +        // Paper start - fluid method optimisations
 +        final int amount = level;
 +        if (!this.init) {
@@ -34206,7 +34155,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      public abstract Fluid getSource();
  
      public FluidState getSource(boolean falling) {
--        return (FluidState) this.getSource().defaultFluidState().setValue(FlowingFluid.FALLING, falling);
+-        return this.getSource().defaultFluidState().setValue(FALLING, Boolean.valueOf(falling));
 +        // Paper start - fluid method optimisations
 +        if (!this.init) {
 +            this.init();
@@ -34215,12 +34164,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - fluid method optimisations
      }
  
-     protected abstract boolean canConvertToSource(ServerLevel world);
-diff --git a/src/main/java/net/minecraft/world/level/material/FluidState.java b/src/main/java/net/minecraft/world/level/material/FluidState.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/material/FluidState.java
-+++ b/src/main/java/net/minecraft/world/level/material/FluidState.java
-@@ -0,0 +0,0 @@ import net.minecraft.world.level.block.state.properties.Property;
+     protected abstract boolean canConvertToSource(ServerLevel level);
+diff --git a/net/minecraft/world/level/material/FluidState.java b/net/minecraft/world/level/material/FluidState.java
+index d2d71b22666639c003d86a6b6403fcbd2912c5af..481cb46973acb9785fdee5732e98aac560c6ec08 100644
+--- a/net/minecraft/world/level/material/FluidState.java
++++ b/net/minecraft/world/level/material/FluidState.java
+@@ -22,12 +22,30 @@ import net.minecraft.world.level.block.state.properties.Property;
  import net.minecraft.world.phys.Vec3;
  import net.minecraft.world.phys.shapes.VoxelShape;
  
@@ -34249,10 +34198,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - fluid method optimisations
 +
-     public FluidState(Fluid fluid, Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap, MapCodec<FluidState> codec) {
-         super(fluid, propertyMap, codec);
-         this.isEmpty = fluid.isEmpty(); // Paper - Perf: moved from isEmpty()
-@@ -0,0 +0,0 @@ public final class FluidState extends StateHolder<Fluid, FluidState> {
+     public FluidState(Fluid owner, Reference2ObjectArrayMap<Property<?>, Comparable<?>> values, MapCodec<FluidState> propertiesCodec) {
+         super(owner, values, propertiesCodec);
+         this.isEmpty = owner.isEmpty(); // Paper - Perf: moved from isEmpty()
+@@ -38,11 +56,11 @@ public final class FluidState extends StateHolder<Fluid, FluidState> {
      }
  
      public boolean isSource() {
@@ -34266,7 +34215,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      public boolean isEmpty() {
-@@ -0,0 +0,0 @@ public final class FluidState extends StateHolder<Fluid, FluidState> {
+@@ -54,11 +72,11 @@ public final class FluidState extends StateHolder<Fluid, FluidState> {
      }
  
      public float getOwnHeight() {
@@ -34279,8 +34228,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return this.amount; // Paper - fluid method optimisations
      }
  
-     public boolean shouldRenderBackwardUpFace(BlockGetter world, BlockPos pos) {
-@@ -0,0 +0,0 @@ public final class FluidState extends StateHolder<Fluid, FluidState> {
+     public boolean shouldRenderBackwardUpFace(BlockGetter level, BlockPos pos) {
+@@ -84,7 +102,7 @@ public final class FluidState extends StateHolder<Fluid, FluidState> {
      }
  
      public boolean isRandomlyTicking() {
@@ -34288,8 +34237,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return this.isRandomlyTicking; // Paper - fluid method optimisations
      }
  
-     public void randomTick(ServerLevel world, BlockPos pos, RandomSource random) {
-@@ -0,0 +0,0 @@ public final class FluidState extends StateHolder<Fluid, FluidState> {
+     public void randomTick(ServerLevel level, BlockPos pos, RandomSource random) {
+@@ -96,7 +114,12 @@ public final class FluidState extends StateHolder<Fluid, FluidState> {
      }
  
      public BlockState createLegacyBlock() {
@@ -34303,33 +34252,33 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      @Nullable
-diff --git a/src/main/java/net/minecraft/world/phys/AABB.java b/src/main/java/net/minecraft/world/phys/AABB.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/phys/AABB.java
-+++ b/src/main/java/net/minecraft/world/phys/AABB.java
-@@ -0,0 +0,0 @@ public class AABB {
+diff --git a/net/minecraft/world/phys/AABB.java b/net/minecraft/world/phys/AABB.java
+index 047e1fd078d7f49a2547daeca9eec31306d25dd0..85148858db1fd5e9da8bbdde4b0d84110d80e373 100644
+--- a/net/minecraft/world/phys/AABB.java
++++ b/net/minecraft/world/phys/AABB.java
+@@ -314,7 +314,7 @@ public class AABB {
      }
  
      @Nullable
--    private static Direction getDirection(
-+    public static Direction getDirection( // Paper - optimise collisions - public
-         AABB box, Vec3 intersectingVector, double[] traceDistanceResult, @Nullable Direction approachDirection, double deltaX, double deltaY, double deltaZ
-     ) {
-         return getDirection(
-diff --git a/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java
-+++ b/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java
-@@ -0,0 +0,0 @@ public class ArrayVoxelShape extends VoxelShape {
+-    private static Direction getDirection(AABB aabb, Vec3 start, double[] minDistance, @Nullable Direction facing, double deltaX, double deltaY, double deltaZ) {
++    public static Direction getDirection(AABB aabb, Vec3 start, double[] minDistance, @Nullable Direction facing, double deltaX, double deltaY, double deltaZ) { // Paper - optimise collisions - public
+         return getDirection(aabb.minX, aabb.minY, aabb.minZ, aabb.maxX, aabb.maxY, aabb.maxZ, start, minDistance, facing, deltaX, deltaY, deltaZ);
+     }
+ 
+diff --git a/net/minecraft/world/phys/shapes/ArrayVoxelShape.java b/net/minecraft/world/phys/shapes/ArrayVoxelShape.java
+index adb5f1be35d3a712499076719a1bb819ef52b9a8..39a634e1392239e17818a11750ba869ea7d195ce 100644
+--- a/net/minecraft/world/phys/shapes/ArrayVoxelShape.java
++++ b/net/minecraft/world/phys/shapes/ArrayVoxelShape.java
+@@ -20,7 +20,7 @@ public class ArrayVoxelShape extends VoxelShape {
          );
      }
  
--    ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xPoints, DoubleList yPoints, DoubleList zPoints) {
-+    public ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xPoints, DoubleList yPoints, DoubleList zPoints) { // Paper - optimise collisions - public
+-    ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xs, DoubleList ys, DoubleList zs) {
++    public ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xs, DoubleList ys, DoubleList zs) { // Paper - optimise collisions - public
          super(shape);
          int i = shape.getXSize() + 1;
-         int j = shape.getYSize() + 1;
-@@ -0,0 +0,0 @@ public class ArrayVoxelShape extends VoxelShape {
+         int i1 = shape.getYSize() + 1;
+@@ -34,6 +34,7 @@ public class ArrayVoxelShape extends VoxelShape {
                  new IllegalArgumentException("Lengths of point arrays must be consistent with the size of the VoxelShape.")
              );
          }
@@ -34337,11 +34286,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      @Override
-diff --git a/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java
-+++ b/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java
-@@ -0,0 +0,0 @@ import java.util.BitSet;
+diff --git a/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java b/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java
+index 14a12bdaa428556fa7b0c43e37b79699ae2fcb92..3a56e4ad9b3cba0cdf4bc373f7d0457d8643fdc4 100644
+--- a/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java
++++ b/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java
+@@ -4,13 +4,13 @@ import java.util.BitSet;
  import net.minecraft.core.Direction;
  
  public final class BitSetDiscreteVoxelShape extends DiscreteVoxelShape {
@@ -34360,14 +34309,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public int yMax; // Paper - optimise collisions - public
 +    public int zMax; // Paper - optimise collisions - public
  
-     public BitSetDiscreteVoxelShape(int sizeX, int sizeY, int sizeZ) {
-         super(sizeX, sizeY, sizeZ);
-@@ -0,0 +0,0 @@ public final class BitSetDiscreteVoxelShape extends DiscreteVoxelShape {
+     public BitSetDiscreteVoxelShape(int xSize, int ySize, int zSize) {
+         super(xSize, ySize, zSize);
+@@ -150,47 +150,109 @@ public final class BitSetDiscreteVoxelShape extends DiscreteVoxelShape {
          return bitSetDiscreteVoxelShape;
      }
  
--    protected static void forAllBoxes(DiscreteVoxelShape voxelSet, DiscreteVoxelShape.IntLineConsumer callback, boolean coalesce) {
--        BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = new BitSetDiscreteVoxelShape(voxelSet);
+-    protected static void forAllBoxes(DiscreteVoxelShape shape, DiscreteVoxelShape.IntLineConsumer consumer, boolean combine) {
+-        BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = new BitSetDiscreteVoxelShape(shape);
 +    // Paper start - optimise collisions
 +    public static void forAllBoxes(final DiscreteVoxelShape shape, final DiscreteVoxelShape.IntLineConsumer consumer, final boolean mergeAdjacent) {
 +        // Paper - remove debug
@@ -34409,19 +34358,19 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            // this branch is actually important to optimise, as it affects uncached toAabbs() (which affects optimize())
  
 -        for (int i = 0; i < bitSetDiscreteVoxelShape.ySize; i++) {
--            for (int j = 0; j < bitSetDiscreteVoxelShape.xSize; j++) {
--                int k = -1;
+-            for (int i1 = 0; i1 < bitSetDiscreteVoxelShape.xSize; i1++) {
+-                int i2 = -1;
 +            // only clone when we may write to it
 +            bitset = ca.spottedleaf.moonrise.common.util.MixinWorkarounds.clone(bitset);
  
--                for (int l = 0; l <= bitSetDiscreteVoxelShape.zSize; l++) {
--                    if (bitSetDiscreteVoxelShape.isFullWide(j, i, l)) {
--                        if (coalesce) {
--                            if (k == -1) {
--                                k = l;
+-                for (int i3 = 0; i3 <= bitSetDiscreteVoxelShape.zSize; i3++) {
+-                    if (bitSetDiscreteVoxelShape.isFullWide(i1, i, i3)) {
+-                        if (combine) {
+-                            if (i2 == -1) {
+-                                i2 = i3;
 -                            }
 -                        } else {
--                            callback.consume(j, i, l, j + 1, i + 1, l + 1);
+-                            consumer.consume(i1, i, i3, i1 + 1, i + 1, i3 + 1);
 +            for (int y = 0; y < sizeY; ++y, indexY += incY) {
 +                indexX = indexY;
 +                for (int x = 0; x < sizeX; ++x, indexX += incX) {
@@ -34436,14 +34385,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                        if (lastSetZ == -1) {
 +                            lastSetZ = endIndex;
                          }
--                    } else if (k != -1) {
--                        int m = j;
--                        int n = i;
--                        bitSetDiscreteVoxelShape.clearZStrip(k, l, j, i);
+-                    } else if (i2 != -1) {
+-                        int i4 = i1;
+-                        int i5 = i;
+-                        bitSetDiscreteVoxelShape.clearZStrip(i2, i3, i1, i);
 -
--                        while (bitSetDiscreteVoxelShape.isZStripFull(k, l, m + 1, i)) {
--                            bitSetDiscreteVoxelShape.clearZStrip(k, l, m + 1, i);
--                            m++;
+-                        while (bitSetDiscreteVoxelShape.isZStripFull(i2, i3, i4 + 1, i)) {
+-                            bitSetDiscreteVoxelShape.clearZStrip(i2, i3, i4 + 1, i);
+-                            i4++;
 +
 +                        ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.clearRange(bitset, firstSetZ, lastSetZ);
 +
@@ -34457,9 +34406,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                            ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.clearRange(bitset, neighbourIdxStart, neighbourIdxEnd);
                          }
  
--                        while (bitSetDiscreteVoxelShape.isXZRectangleFull(j, m + 1, k, l, n + 1)) {
--                            for (int o = j; o <= m; o++) {
--                                bitSetDiscreteVoxelShape.clearZStrip(k, l, o, n + 1);
+-                        while (bitSetDiscreteVoxelShape.isXZRectangleFull(i1, i4 + 1, i2, i3, i5 + 1)) {
+-                            for (int i6 = i1; i6 <= i4; i6++) {
+-                                bitSetDiscreteVoxelShape.clearZStrip(i2, i3, i6, i5 + 1);
 +                        // try to merge neighbouring on the Y axis
 +
 +                        int endY; // exclusive
@@ -34476,7 +34425,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                                }
                              }
  
--                            n++;
+-                            i5++;
 +                            ++endY;
 +
 +                            // passed, so we can clear it
@@ -34486,8 +34435,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                            }
                          }
  
--                        callback.consume(j, i, k, m + 1, n + 1, l);
--                        k = -1;
+-                        consumer.consume(i1, i, i2, i4 + 1, i5 + 1, i3);
+-                        i2 = -1;
 +                        consumer.consume(x, y, firstSetZ - indexX, endX, endY, lastSetZ - indexX);
 +                        zIdx = lastSetZ;
                      }
@@ -34497,25 +34446,25 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
 +    // Paper end - optimise collisions
  
-     private boolean isZStripFull(int z1, int z2, int x, int y) {
-         return x < this.xSize && y < this.ySize && this.storage.nextClearBit(this.getIndex(x, y, z1)) >= this.getIndex(x, y, z2);
-diff --git a/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java
-+++ b/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java
-@@ -0,0 +0,0 @@ import net.minecraft.util.Mth;
+     private boolean isZStripFull(int zMin, int zMax, int x, int y) {
+         return x < this.xSize && y < this.ySize && this.storage.nextClearBit(this.getIndex(x, y, zMin)) >= this.getIndex(x, y, zMax);
+diff --git a/net/minecraft/world/phys/shapes/CubeVoxelShape.java b/net/minecraft/world/phys/shapes/CubeVoxelShape.java
+index f6b6481591e009de80f6b6318d35f193aabb7df3..e9b5069dcd572966b2f5aa220cef30e7a328fa2c 100644
+--- a/net/minecraft/world/phys/shapes/CubeVoxelShape.java
++++ b/net/minecraft/world/phys/shapes/CubeVoxelShape.java
+@@ -7,6 +7,7 @@ import net.minecraft.util.Mth;
  public final class CubeVoxelShape extends VoxelShape {
-     protected CubeVoxelShape(DiscreteVoxelShape voxels) {
-         super(voxels);
+     protected CubeVoxelShape(DiscreteVoxelShape shape) {
+         super(shape);
 +        ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)this).moonrise$initCache(); // Paper - optimise collisions
      }
  
      @Override
-diff --git a/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java
-+++ b/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java
-@@ -0,0 +0,0 @@ package net.minecraft.world.phys.shapes;
+diff --git a/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java b/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java
+index 4fc61b329ccb7c9aeb6105dc53d71545a3baea89..309a34f192f7737204ce7a5c3b4004bdd83842f2 100644
+--- a/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java
++++ b/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java
+@@ -3,12 +3,79 @@ package net.minecraft.world.phys.shapes;
  import net.minecraft.core.AxisCycle;
  import net.minecraft.core.Direction;
  
@@ -34593,14 +34542,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - optimise collisions
 +
-     protected DiscreteVoxelShape(int sizeX, int sizeY, int sizeZ) {
-         if (sizeX >= 0 && sizeY >= 0 && sizeZ >= 0) {
-             this.xSize = sizeX;
-diff --git a/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java b/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java
-+++ b/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java
-@@ -0,0 +0,0 @@ import it.unimi.dsi.fastutil.doubles.AbstractDoubleList;
+     protected DiscreteVoxelShape(int xSize, int ySize, int zSize) {
+         if (xSize >= 0 && ySize >= 0 && zSize >= 0) {
+             this.xSize = xSize;
+diff --git a/net/minecraft/world/phys/shapes/OffsetDoubleList.java b/net/minecraft/world/phys/shapes/OffsetDoubleList.java
+index ac1488875537421b74f0c491c9b7a40e75539c92..9eb27eb8d6dcaad6ce02f8ce4546acc224c4196f 100644
+--- a/net/minecraft/world/phys/shapes/OffsetDoubleList.java
++++ b/net/minecraft/world/phys/shapes/OffsetDoubleList.java
+@@ -4,8 +4,8 @@ import it.unimi.dsi.fastutil.doubles.AbstractDoubleList;
  import it.unimi.dsi.fastutil.doubles.DoubleList;
  
  public class OffsetDoubleList extends AbstractDoubleList {
@@ -34609,13 +34558,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public final DoubleList delegate; // Paper - optimise collisions - public
 +    public final double offset; // Paper - optimise collisions - public
  
-     public OffsetDoubleList(DoubleList oldList, double offset) {
-         this.delegate = oldList;
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java
-+++ b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java
-@@ -0,0 +0,0 @@ public final class Shapes {
+     public OffsetDoubleList(DoubleList delegate, double offset) {
+         this.delegate = delegate;
+diff --git a/net/minecraft/world/phys/shapes/Shapes.java b/net/minecraft/world/phys/shapes/Shapes.java
+index e759221fb54aa510d2d8bbba47e1d794367aec6d..5665cfaae4fc9e72b77fd41e16e7f64460b099b0 100644
+--- a/net/minecraft/world/phys/shapes/Shapes.java
++++ b/net/minecraft/world/phys/shapes/Shapes.java
+@@ -16,9 +16,15 @@ public final class Shapes {
      public static final double EPSILON = 1.0E-7;
      public static final double BIG_EPSILON = 1.0E-6;
      private static final VoxelShape BLOCK = Util.make(() -> {
@@ -34634,7 +34583,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      });
      public static final VoxelShape INFINITY = box(
          Double.NEGATIVE_INFINITY,
-@@ -0,0 +0,0 @@ public final class Shapes {
+@@ -43,6 +49,30 @@ public final class Shapes {
          return BLOCK;
      }
  
@@ -34665,16 +34614,16 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      public static VoxelShape box(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) {
          if (!(minX > maxX) && !(minY > maxY) && !(minZ > maxZ)) {
              return create(minX, minY, minZ, maxX, maxY, maxZ);
-@@ -0,0 +0,0 @@ public final class Shapes {
+@@ -52,39 +82,42 @@ public final class Shapes {
      }
  
      public static VoxelShape create(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) {
 +        // Paper start - optimise collisions
          if (!(maxX - minX < 1.0E-7) && !(maxY - minY < 1.0E-7) && !(maxZ - minZ < 1.0E-7)) {
 -            int i = findBits(minX, maxX);
--            int j = findBits(minY, maxY);
--            int k = findBits(minZ, maxZ);
--            if (i < 0 || j < 0 || k < 0) {
+-            int i1 = findBits(minY, maxY);
+-            int i2 = findBits(minZ, maxZ);
+-            if (i < 0 || i1 < 0 || i2 < 0) {
 +            final int bitsX = findBits(minX, maxX);
 +            final int bitsY = findBits(minY, maxY);
 +            final int bitsZ = findBits(minZ, maxZ);
@@ -34707,22 +34656,22 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                    minY == 0.0 && maxY == 1.0 ? ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.ZERO_ONE : DoubleArrayList.wrap(new double[] { minY, maxY }),
 +                    minZ == 0.0 && maxZ == 1.0 ? ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.ZERO_ONE : DoubleArrayList.wrap(new double[] { minZ, maxZ })
                  );
--            } else if (i == 0 && j == 0 && k == 0) {
+-            } else if (i == 0 && i1 == 0 && i2 == 0) {
 -                return block();
 -            } else {
--                int l = 1 << i;
--                int m = 1 << j;
--                int n = 1 << k;
+-                int i3 = 1 << i;
+-                int i4 = 1 << i1;
+-                int i5 = 1 << i2;
 -                BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = BitSetDiscreteVoxelShape.withFilledBounds(
--                    l,
--                    m,
--                    n,
--                    (int)Math.round(minX * (double)l),
--                    (int)Math.round(minY * (double)m),
--                    (int)Math.round(minZ * (double)n),
--                    (int)Math.round(maxX * (double)l),
--                    (int)Math.round(maxY * (double)m),
--                    (int)Math.round(maxZ * (double)n)
+-                    i3,
+-                    i4,
+-                    i5,
+-                    (int)Math.round(minX * i3),
+-                    (int)Math.round(minY * i4),
+-                    (int)Math.round(minZ * i5),
+-                    (int)Math.round(maxX * i3),
+-                    (int)Math.round(maxY * i4),
+-                    (int)Math.round(maxZ * i5)
 -                );
 -                return new CubeVoxelShape(bitSetDiscreteVoxelShape);
              }
@@ -34733,18 +34682,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - optimise collisions
      }
  
-     public static VoxelShape create(AABB box) {
-@@ -0,0 +0,0 @@ public final class Shapes {
-         return join(first, second, BooleanOp.OR);
+     public static VoxelShape create(AABB aabb) {
+@@ -120,85 +153,52 @@ public final class Shapes {
      }
  
--    public static VoxelShape or(VoxelShape first, VoxelShape... others) {
--        return Arrays.stream(others).reduce(first, Shapes::or);
-+    // Paper start - optimise collisions
-+    public static VoxelShape or(VoxelShape shape, VoxelShape... others) {
+     public static VoxelShape or(VoxelShape shape1, VoxelShape... others) {
+-        return Arrays.stream(others).reduce(shape1, Shapes::or);
 +        int size = others.length;
 +        if (size == 0) {
-+            return shape;
++            return shape1;
 +        }
 +
 +        // reduce complexity of joins by splitting the merges
@@ -34753,7 +34699,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        ++size;
 +        final VoxelShape[] tmp = Arrays.copyOf(others, size);
 +        // insert first shape
-+        tmp[size - 1] = shape;
++        tmp[size - 1] = shape1;
 +
 +        while (size > 1) {
 +            int newSize = 0;
@@ -34778,88 +34724,94 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - optimise collisions
      }
  
-     public static VoxelShape join(VoxelShape first, VoxelShape second, BooleanOp function) {
--        return joinUnoptimized(first, second, function).optimize();
-+        return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.joinOptimized(first, second, function); // Paper - optimise collisions
+     public static VoxelShape join(VoxelShape shape1, VoxelShape shape2, BooleanOp function) {
+-        return joinUnoptimized(shape1, shape2, function).optimize();
++        return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.joinOptimized(shape1, shape2, function); // Paper - optimise collisions
      }
  
-     public static VoxelShape joinUnoptimized(VoxelShape one, VoxelShape two, BooleanOp function) {
+     public static VoxelShape joinUnoptimized(VoxelShape shape1, VoxelShape shape2, BooleanOp function) {
 -        if (function.apply(false, false)) {
 -            throw (IllegalArgumentException)Util.pauseInIde(new IllegalArgumentException());
--        } else if (one == two) {
--            return function.apply(true, true) ? one : empty();
+-        } else if (shape1 == shape2) {
+-            return function.apply(true, true) ? shape1 : empty();
 -        } else {
--            boolean bl = function.apply(true, false);
--            boolean bl2 = function.apply(false, true);
--            if (one.isEmpty()) {
--                return bl2 ? two : empty();
--            } else if (two.isEmpty()) {
--                return bl ? one : empty();
+-            boolean flag = function.apply(true, false);
+-            boolean flag1 = function.apply(false, true);
+-            if (shape1.isEmpty()) {
+-                return flag1 ? shape2 : empty();
+-            } else if (shape2.isEmpty()) {
+-                return flag ? shape1 : empty();
 -            } else {
--                IndexMerger indexMerger = createIndexMerger(1, one.getCoords(Direction.Axis.X), two.getCoords(Direction.Axis.X), bl, bl2);
--                IndexMerger indexMerger2 = createIndexMerger(indexMerger.size() - 1, one.getCoords(Direction.Axis.Y), two.getCoords(Direction.Axis.Y), bl, bl2);
--                IndexMerger indexMerger3 = createIndexMerger(
--                    (indexMerger.size() - 1) * (indexMerger2.size() - 1), one.getCoords(Direction.Axis.Z), two.getCoords(Direction.Axis.Z), bl, bl2
+-                IndexMerger indexMerger = createIndexMerger(1, shape1.getCoords(Direction.Axis.X), shape2.getCoords(Direction.Axis.X), flag, flag1);
+-                IndexMerger indexMerger1 = createIndexMerger(
+-                    indexMerger.size() - 1, shape1.getCoords(Direction.Axis.Y), shape2.getCoords(Direction.Axis.Y), flag, flag1
+-                );
+-                IndexMerger indexMerger2 = createIndexMerger(
+-                    (indexMerger.size() - 1) * (indexMerger1.size() - 1), shape1.getCoords(Direction.Axis.Z), shape2.getCoords(Direction.Axis.Z), flag, flag1
 -                );
 -                BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = BitSetDiscreteVoxelShape.join(
--                    one.shape, two.shape, indexMerger, indexMerger2, indexMerger3, function
+-                    shape1.shape, shape2.shape, indexMerger, indexMerger1, indexMerger2, function
 -                );
 -                return (VoxelShape)(indexMerger instanceof DiscreteCubeMerger
+-                        && indexMerger1 instanceof DiscreteCubeMerger
 -                        && indexMerger2 instanceof DiscreteCubeMerger
--                        && indexMerger3 instanceof DiscreteCubeMerger
 -                    ? new CubeVoxelShape(bitSetDiscreteVoxelShape)
--                    : new ArrayVoxelShape(bitSetDiscreteVoxelShape, indexMerger.getList(), indexMerger2.getList(), indexMerger3.getList()));
+-                    : new ArrayVoxelShape(bitSetDiscreteVoxelShape, indexMerger.getList(), indexMerger1.getList(), indexMerger2.getList()));
 -            }
 -        }
-+        return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.joinUnoptimized(one, two, function); // Paper - optimise collisions
++        return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.joinUnoptimized(shape1, shape2, function); // Paper - optimise collisions
      }
  
-     public static boolean joinIsNotEmpty(VoxelShape shape1, VoxelShape shape2, BooleanOp predicate) {
--        if (predicate.apply(false, false)) {
+     public static boolean joinIsNotEmpty(VoxelShape shape1, VoxelShape shape2, BooleanOp resultOperator) {
+-        if (resultOperator.apply(false, false)) {
 -            throw (IllegalArgumentException)Util.pauseInIde(new IllegalArgumentException());
 -        } else {
--            boolean bl = shape1.isEmpty();
--            boolean bl2 = shape2.isEmpty();
--            if (!bl && !bl2) {
+-            boolean isEmpty = shape1.isEmpty();
+-            boolean isEmpty1 = shape2.isEmpty();
+-            if (!isEmpty && !isEmpty1) {
 -                if (shape1 == shape2) {
--                    return predicate.apply(true, true);
+-                    return resultOperator.apply(true, true);
 -                } else {
--                    boolean bl3 = predicate.apply(true, false);
--                    boolean bl4 = predicate.apply(false, true);
+-                    boolean flag = resultOperator.apply(true, false);
+-                    boolean flag1 = resultOperator.apply(false, true);
 -
 -                    for (Direction.Axis axis : AxisCycle.AXIS_VALUES) {
 -                        if (shape1.max(axis) < shape2.min(axis) - 1.0E-7) {
--                            return bl3 || bl4;
+-                            return flag || flag1;
 -                        }
 -
 -                        if (shape2.max(axis) < shape1.min(axis) - 1.0E-7) {
--                            return bl3 || bl4;
+-                            return flag || flag1;
 -                        }
 -                    }
 -
--                    IndexMerger indexMerger = createIndexMerger(1, shape1.getCoords(Direction.Axis.X), shape2.getCoords(Direction.Axis.X), bl3, bl4);
+-                    IndexMerger indexMerger = createIndexMerger(1, shape1.getCoords(Direction.Axis.X), shape2.getCoords(Direction.Axis.X), flag, flag1);
+-                    IndexMerger indexMerger1 = createIndexMerger(
+-                        indexMerger.size() - 1, shape1.getCoords(Direction.Axis.Y), shape2.getCoords(Direction.Axis.Y), flag, flag1
+-                    );
 -                    IndexMerger indexMerger2 = createIndexMerger(
--                        indexMerger.size() - 1, shape1.getCoords(Direction.Axis.Y), shape2.getCoords(Direction.Axis.Y), bl3, bl4
+-                        (indexMerger.size() - 1) * (indexMerger1.size() - 1),
+-                        shape1.getCoords(Direction.Axis.Z),
+-                        shape2.getCoords(Direction.Axis.Z),
+-                        flag,
+-                        flag1
 -                    );
--                    IndexMerger indexMerger3 = createIndexMerger(
--                        (indexMerger.size() - 1) * (indexMerger2.size() - 1), shape1.getCoords(Direction.Axis.Z), shape2.getCoords(Direction.Axis.Z), bl3, bl4
--                    );
--                    return joinIsNotEmpty(indexMerger, indexMerger2, indexMerger3, shape1.shape, shape2.shape, predicate);
+-                    return joinIsNotEmpty(indexMerger, indexMerger1, indexMerger2, shape1.shape, shape2.shape, resultOperator);
 -                }
 -            } else {
--                return predicate.apply(!bl, !bl2);
+-                return resultOperator.apply(!isEmpty, !isEmpty1);
 -            }
 -        }
-+        return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isJoinNonEmpty(shape1, shape2, predicate); // Paper - optimise collisions
++        return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isJoinNonEmpty(shape1, shape2, resultOperator); // Paper - optimise collisions
      }
  
      private static boolean joinIsNotEmpty(
-@@ -0,0 +0,0 @@ public final class Shapes {
-         return maxDist;
+@@ -230,52 +230,116 @@ public final class Shapes {
+         return desiredOffset;
      }
  
--    public static boolean blockOccudes(VoxelShape shape, VoxelShape neighbor, Direction direction) {
--        if (shape == block() && neighbor == block()) {
+-    public static boolean blockOccudes(VoxelShape shape, VoxelShape adjacentShape, Direction side) {
+-        if (shape == block() && adjacentShape == block()) {
 +    // Paper start - optimise collisions
 +    public static boolean blockOccudes(final VoxelShape first, final VoxelShape second, final Direction direction) {
 +        final boolean firstBlock = first == BLOCK;
@@ -34867,7 +34819,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +        if (firstBlock & secondBlock) {
              return true;
--        } else if (neighbor.isEmpty()) {
+-        } else if (adjacentShape.isEmpty()) {
 +        }
 +
 +        if (first.isEmpty() | second.isEmpty()) {
@@ -34880,14 +34832,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        if (newFirst.isEmpty()) {
              return false;
 -        } else {
--            Direction.Axis axis = direction.getAxis();
--            Direction.AxisDirection axisDirection = direction.getAxisDirection();
--            VoxelShape voxelShape = axisDirection == Direction.AxisDirection.POSITIVE ? shape : neighbor;
--            VoxelShape voxelShape2 = axisDirection == Direction.AxisDirection.POSITIVE ? neighbor : shape;
+-            Direction.Axis axis = side.getAxis();
+-            Direction.AxisDirection axisDirection = side.getAxisDirection();
+-            VoxelShape voxelShape = axisDirection == Direction.AxisDirection.POSITIVE ? shape : adjacentShape;
+-            VoxelShape voxelShape1 = axisDirection == Direction.AxisDirection.POSITIVE ? adjacentShape : shape;
 -            BooleanOp booleanOp = axisDirection == Direction.AxisDirection.POSITIVE ? BooleanOp.ONLY_FIRST : BooleanOp.ONLY_SECOND;
 -            return DoubleMath.fuzzyEquals(voxelShape.max(axis), 1.0, 1.0E-7)
--                && DoubleMath.fuzzyEquals(voxelShape2.min(axis), 0.0, 1.0E-7)
--                && !joinIsNotEmpty(new SliceShape(voxelShape, axis, voxelShape.shape.getSize(axis) - 1), new SliceShape(voxelShape2, axis, 0), booleanOp);
+-                && DoubleMath.fuzzyEquals(voxelShape1.min(axis), 0.0, 1.0E-7)
+-                && !joinIsNotEmpty(new SliceShape(voxelShape, axis, voxelShape.shape.getSize(axis) - 1), new SliceShape(voxelShape1, axis, 0), booleanOp);
          }
 +        final VoxelShape newSecond = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$getFaceShapeClamped(direction.getOpposite());
 +        if (newSecond.isEmpty()) {
@@ -34898,12 +34850,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - optimise collisions
      }
  
--    public static boolean mergedFaceOccludes(VoxelShape one, VoxelShape two, Direction direction) {
--        if (one != block() && two != block()) {
--            Direction.Axis axis = direction.getAxis();
--            Direction.AxisDirection axisDirection = direction.getAxisDirection();
--            VoxelShape voxelShape = axisDirection == Direction.AxisDirection.POSITIVE ? one : two;
--            VoxelShape voxelShape2 = axisDirection == Direction.AxisDirection.POSITIVE ? two : one;
+-    public static boolean mergedFaceOccludes(VoxelShape shape, VoxelShape adjacentShape, Direction side) {
+-        if (shape != block() && adjacentShape != block()) {
+-            Direction.Axis axis = side.getAxis();
+-            Direction.AxisDirection axisDirection = side.getAxisDirection();
+-            VoxelShape voxelShape = axisDirection == Direction.AxisDirection.POSITIVE ? shape : adjacentShape;
+-            VoxelShape voxelShape1 = axisDirection == Direction.AxisDirection.POSITIVE ? adjacentShape : shape;
 -            if (!DoubleMath.fuzzyEquals(voxelShape.max(axis), 1.0, 1.0E-7)) {
 -                voxelShape = empty();
 -            }
@@ -34913,8 +34865,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        final AABB bounds1 = shape1.bounds();
 +        final AABB bounds2 = shape2.bounds();
  
--            if (!DoubleMath.fuzzyEquals(voxelShape2.min(axis), 0.0, 1.0E-7)) {
--                voxelShape2 = empty();
+-            if (!DoubleMath.fuzzyEquals(voxelShape1.min(axis), 0.0, 1.0E-7)) {
+-                voxelShape1 = empty();
 -            }
 +        final double minX = Math.min(bounds1.minX, bounds2.minX);
 +        final double minY = Math.min(bounds1.minY, bounds2.minY);
@@ -34922,7 +34874,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
 -            return !joinIsNotEmpty(
 -                block(),
--                joinUnoptimized(new SliceShape(voxelShape, axis, voxelShape.shape.getSize(axis) - 1), new SliceShape(voxelShape2, axis, 0), BooleanOp.OR),
+-                joinUnoptimized(new SliceShape(voxelShape, axis, voxelShape.shape.getSize(axis) - 1), new SliceShape(voxelShape1, axis, 0), BooleanOp.OR),
 -                BooleanOp.ONLY_FIRST
 -            );
 -        } else {
@@ -34940,8 +34892,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public static boolean mergedFaceOccludes(final VoxelShape first, final VoxelShape second, final Direction direction) {
 +        // see if any of the shapes on their own occludes, only if cached
 +        if (((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$occludesFullBlockIfCached() || ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$occludesFullBlockIfCached()) {
-+            return true;
-+        }
+             return true;
+         }
 +
 +        if (first.isEmpty() & second.isEmpty()) {
 +            return false;
@@ -34954,8 +34906,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +        // see if any of the shapes on their own occludes, only if cached
 +        if (((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)newFirst).moonrise$occludesFullBlockIfCached() || ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)newSecond).moonrise$occludesFullBlockIfCached()) {
-             return true;
-         }
++            return true;
++        }
 +
 +        final boolean firstEmpty = newFirst.isEmpty();
 +        final boolean secondEmpty = newSecond.isEmpty();
@@ -34996,32 +34948,33 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            return ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape1).moonrise$occludesFullBlock();
 +        }
  
--    public static boolean faceShapeOccludes(VoxelShape one, VoxelShape two) {
--        return one == block()
--            || two == block()
--            || (!one.isEmpty() || !two.isEmpty()) && !joinIsNotEmpty(block(), joinUnoptimized(one, two, BooleanOp.OR), BooleanOp.ONLY_FIRST);
+-    public static boolean faceShapeOccludes(VoxelShape voxelShape1, VoxelShape voxelShape2) {
+-        return voxelShape1 == block()
+-            || voxelShape2 == block()
+-            || (!voxelShape1.isEmpty() || !voxelShape2.isEmpty())
+-                && !joinIsNotEmpty(block(), joinUnoptimized(voxelShape1, voxelShape2, BooleanOp.OR), BooleanOp.ONLY_FIRST);
 +        return mergedMayOccludeBlock(shape1, shape2) && ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape1).moonrise$orUnoptimized(shape2)).moonrise$occludesFullBlock();
 +        // Paper end - optimise collisions
      }
  
      @VisibleForTesting
-diff --git a/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java b/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java
-+++ b/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java
-@@ -0,0 +0,0 @@ public class SliceShape extends VoxelShape {
-         super(makeSlice(shape.shape, axis, sliceWidth));
-         this.delegate = shape;
+diff --git a/net/minecraft/world/phys/shapes/SliceShape.java b/net/minecraft/world/phys/shapes/SliceShape.java
+index 79f7f04207891dd98cc0b2d93ecb2e07c8baa7b6..7ca12213c10f962ff597a8d51413a17b1827bbb4 100644
+--- a/net/minecraft/world/phys/shapes/SliceShape.java
++++ b/net/minecraft/world/phys/shapes/SliceShape.java
+@@ -12,6 +12,7 @@ public class SliceShape extends VoxelShape {
+         super(makeSlice(delegate.shape, axis, index));
+         this.delegate = delegate;
          this.axis = axis;
 +        ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)this).moonrise$initCache(); // Paper - optimise collisions
      }
  
-     private static DiscreteVoxelShape makeSlice(DiscreteVoxelShape voxelSet, Direction.Axis axis, int sliceWidth) {
-diff --git a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java
-+++ b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java
-@@ -0,0 +0,0 @@ import net.minecraft.world.phys.AABB;
+     private static DiscreteVoxelShape makeSlice(DiscreteVoxelShape shape, Direction.Axis axis, int index) {
+diff --git a/net/minecraft/world/phys/shapes/VoxelShape.java b/net/minecraft/world/phys/shapes/VoxelShape.java
+index 006065c32baf3b1ddc5647196cb9f863c7969064..2c7e70675b62cb753447d2acebf2f36cdac74973 100644
+--- a/net/minecraft/world/phys/shapes/VoxelShape.java
++++ b/net/minecraft/world/phys/shapes/VoxelShape.java
+@@ -15,61 +15,546 @@ import net.minecraft.world.phys.AABB;
  import net.minecraft.world.phys.BlockHitResult;
  import net.minecraft.world.phys.Vec3;
  
@@ -35446,8 +35399,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - optimise collisions
 +
-     protected VoxelShape(DiscreteVoxelShape voxels) {
-         this.shape = voxels;
+     protected VoxelShape(DiscreteVoxelShape shape) {
+         this.shape = shape;
      }
  
      public double min(Direction.Axis axis) {
@@ -35517,11 +35470,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper start - optimise collisions
 +        if (this.isEmpty) {
 +            throw Util.pauseInIde(new UnsupportedOperationException("No bounds for empty shape."));
-+        }
+         }
 +        AABB cached = this.cachedBounds;
 +        if (cached != null) {
 +            return cached;
-         }
++        }
 +
 +        final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeData = this.cachedShapeData;
 +
@@ -35596,18 +35549,18 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return this.isEmpty; // Paper - optimise collisions
      }
  
-     public VoxelShape move(Vec3 vec3d) {
-@@ -0,0 +0,0 @@ public abstract class VoxelShape {
+     public VoxelShape move(Vec3 offset) {
+@@ -77,20 +562,96 @@ public abstract class VoxelShape {
      }
  
-     public VoxelShape move(double x, double y, double z) {
+     public VoxelShape move(double xOffset, double yOffset, double zOffset) {
 -        return (VoxelShape)(this.isEmpty()
 -            ? Shapes.empty()
 -            : new ArrayVoxelShape(
 -                this.shape,
--                new OffsetDoubleList(this.getCoords(Direction.Axis.X), x),
--                new OffsetDoubleList(this.getCoords(Direction.Axis.Y), y),
--                new OffsetDoubleList(this.getCoords(Direction.Axis.Z), z)
+-                new OffsetDoubleList(this.getCoords(Direction.Axis.X), xOffset),
+-                new OffsetDoubleList(this.getCoords(Direction.Axis.Y), yOffset),
+-                new OffsetDoubleList(this.getCoords(Direction.Axis.Z), zOffset)
 -            ));
 +        // Paper start - optimise collisions
 +        if (this.isEmpty) {
@@ -35616,14 +35569,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +        final ArrayVoxelShape ret = new ArrayVoxelShape(
 +            this.shape,
-+            offsetList(this.rootCoordinatesX, this.offsetX + x),
-+            offsetList(this.rootCoordinatesY, this.offsetY + y),
-+            offsetList(this.rootCoordinatesZ, this.offsetZ + z)
++            offsetList(this.rootCoordinatesX, this.offsetX + xOffset),
++            offsetList(this.rootCoordinatesY, this.offsetY + yOffset),
++            offsetList(this.rootCoordinatesZ, this.offsetZ + zOffset)
 +        );
 +
 +        final ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs cachedToAABBs = this.cachedToAABBs;
 +        if (cachedToAABBs != null) {
-+            ((VoxelShape)(Object)ret).cachedToAABBs = ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs.offset(cachedToAABBs, x, y, z);
++            ((VoxelShape)(Object)ret).cachedToAABBs = ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs.offset(cachedToAABBs, xOffset, yOffset, zOffset);
 +        }
 +
 +        return ret;
@@ -35632,11 +35585,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      public VoxelShape optimize() {
 -        VoxelShape[] voxelShapes = new VoxelShape[]{Shapes.empty()};
--        this.forAllBoxes(
--            (minX, minY, minZ, maxX, maxY, maxZ) -> voxelShapes[0] = Shapes.joinUnoptimized(
--                    voxelShapes[0], Shapes.box(minX, minY, minZ, maxX, maxY, maxZ), BooleanOp.OR
--                )
--        );
+-        this.forAllBoxes((x1, y1, z1, x2, y2, z2) -> voxelShapes[0] = Shapes.joinUnoptimized(voxelShapes[0], Shapes.box(x1, y1, z1, x2, y2, z2), BooleanOp.OR));
 -        return voxelShapes[0];
 +        // Paper start - optimise collisions
 +        if (this.isEmpty) {
@@ -35708,8 +35657,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - optimise collisions
      }
  
-     public void forAllEdges(Shapes.DoubleLineConsumer consumer) {
-@@ -0,0 +0,0 @@ public abstract class VoxelShape {
+     public void forAllEdges(Shapes.DoubleLineConsumer action) {
+@@ -122,9 +683,24 @@ public abstract class VoxelShape {
      }
  
      public List<AABB> toAabbs() {
@@ -35736,14 +35685,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - optimise collisions
      }
  
-     public double min(Direction.Axis axis, double from, double to) {
-@@ -0,0 +0,0 @@ public abstract class VoxelShape {
+     public double min(Direction.Axis axis, double primaryPosition, double secondaryPosition) {
+@@ -146,46 +722,92 @@ public abstract class VoxelShape {
      }
  
-     protected int findIndex(Direction.Axis axis, double coord) {
--        return Mth.binarySearch(0, this.shape.getSize(axis) + 1, i -> coord < this.get(axis, i)) - 1;
+     protected int findIndex(Direction.Axis axis, double position) {
+-        return Mth.binarySearch(0, this.shape.getSize(axis) + 1, value -> position < this.get(axis, value)) - 1;
 +        // Paper start - optimise collisions
-+        final double value = coord;
++        final double value = position;
 +        switch (axis) {
 +            case X: {
 +                final double[] values = this.rootCoordinatesX;
@@ -35771,68 +35720,66 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      @Nullable
--    public BlockHitResult clip(Vec3 start, Vec3 end, BlockPos pos) {
+     public BlockHitResult clip(Vec3 startVec, Vec3 endVec, BlockPos pos) {
 -        if (this.isEmpty()) {
-+    // Paper start - optimise collisions
-+    public BlockHitResult clip(final Vec3 from, final Vec3 to, final BlockPos offset) {
++        // Paper start - optimise collisions
 +        if (this.isEmpty) {
              return null;
 -        } else {
--            Vec3 vec3 = end.subtract(start);
+-            Vec3 vec3 = endVec.subtract(startVec);
 -            if (vec3.lengthSqr() < 1.0E-7) {
 -                return null;
 -            } else {
--                Vec3 vec32 = start.add(vec3.scale(0.001));
+-                Vec3 vec31 = startVec.add(vec3.scale(0.001));
 -                return this.shape
 -                        .isFullWide(
--                            this.findIndex(Direction.Axis.X, vec32.x - (double)pos.getX()),
--                            this.findIndex(Direction.Axis.Y, vec32.y - (double)pos.getY()),
--                            this.findIndex(Direction.Axis.Z, vec32.z - (double)pos.getZ())
+-                            this.findIndex(Direction.Axis.X, vec31.x - pos.getX()),
+-                            this.findIndex(Direction.Axis.Y, vec31.y - pos.getY()),
+-                            this.findIndex(Direction.Axis.Z, vec31.z - pos.getZ())
 -                        )
--                    ? new BlockHitResult(vec32, Direction.getApproximateNearest(vec3.x, vec3.y, vec3.z).getOpposite(), pos, true)
--                    : AABB.clip(this.toAabbs(), start, end, pos);
+-                    ? new BlockHitResult(vec31, Direction.getApproximateNearest(vec3.x, vec3.y, vec3.z).getOpposite(), pos, true)
+-                    : AABB.clip(this.toAabbs(), startVec, endVec, pos);
 +        }
 +
-+        final Vec3 directionOpposite = to.subtract(from);
++        final Vec3 directionOpposite = endVec.subtract(startVec);
 +        if (directionOpposite.lengthSqr() < ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) {
 +            return null;
 +        }
 +
-+        final Vec3 fromBehind = from.add(directionOpposite.scale(0.001));
-+        final double fromBehindOffsetX = fromBehind.x - (double)offset.getX();
-+        final double fromBehindOffsetY = fromBehind.y - (double)offset.getY();
-+        final double fromBehindOffsetZ = fromBehind.z - (double)offset.getZ();
++        final Vec3 fromBehind = startVec.add(directionOpposite.scale(0.001));
++        final double fromBehindOffsetX = fromBehind.x - (double) pos.getX();
++        final double fromBehindOffsetY = fromBehind.y - (double) pos.getY();
++        final double fromBehindOffsetZ = fromBehind.z - (double) pos.getZ();
 +
 +        final AABB singleAABB = this.singleAABBRepresentation;
 +        if (singleAABB != null) {
 +            if (singleAABB.contains(fromBehindOffsetX, fromBehindOffsetY, fromBehindOffsetZ)) {
-+                return new BlockHitResult(fromBehind, Direction.getApproximateNearest(directionOpposite.x, directionOpposite.y, directionOpposite.z).getOpposite(), offset, true);
++                return new BlockHitResult(fromBehind, Direction.getApproximateNearest(directionOpposite.x, directionOpposite.y, directionOpposite.z).getOpposite(), pos, true);
              }
-+            return clip(singleAABB, from, to, offset);
++            return clip(singleAABB, startVec, endVec, pos);
 +        }
 +
-+        if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.strictlyContains((VoxelShape)(Object)this, fromBehindOffsetX, fromBehindOffsetY, fromBehindOffsetZ)) {
-+            return new BlockHitResult(fromBehind, Direction.getApproximateNearest(directionOpposite.x, directionOpposite.y, directionOpposite.z).getOpposite(), offset, true);
++        if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.strictlyContains((VoxelShape) (Object) this, fromBehindOffsetX, fromBehindOffsetY, fromBehindOffsetZ)) {
++            return new BlockHitResult(fromBehind, Direction.getApproximateNearest(directionOpposite.x, directionOpposite.y, directionOpposite.z).getOpposite(), pos, true);
          }
 +
-+        return AABB.clip(((VoxelShape)(Object)this).toAabbs(), from, to, offset);
++        return AABB.clip(((VoxelShape) (Object) this).toAabbs(), startVec, endVec, pos);
 +        // Paper end - optimise collisions
      }
  
--    public Optional<Vec3> closestPointTo(Vec3 target) {
--        if (this.isEmpty()) {
 +    // Paper start - optimise collisions
-+    public Optional<Vec3> closestPointTo(Vec3 point) {
+     public Optional<Vec3> closestPointTo(Vec3 point) {
+-        if (this.isEmpty()) {
 +        if (this.isEmpty) {
              return Optional.empty();
 -        } else {
 -            Vec3[] vec3s = new Vec3[1];
--            this.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> {
--                double d = Mth.clamp(target.x(), minX, maxX);
--                double e = Mth.clamp(target.y(), minY, maxY);
--                double f = Mth.clamp(target.z(), minZ, maxZ);
--                if (vec3s[0] == null || target.distanceToSqr(d, e, f) < target.distanceToSqr(vec3s[0])) {
--                    vec3s[0] = new Vec3(d, e, f);
+-            this.forAllBoxes((x1, y1, z1, x2, y2, z2) -> {
+-                double d = Mth.clamp(point.x(), x1, x2);
+-                double d1 = Mth.clamp(point.y(), y1, y2);
+-                double d2 = Mth.clamp(point.z(), z1, z2);
+-                if (vec3s[0] == null || point.distanceToSqr(d, d1, d2) < point.distanceToSqr(vec3s[0])) {
+-                    vec3s[0] = new Vec3(d, d1, d2);
 -                }
 -            });
 -            return Optional.of(vec3s[0]);
@@ -35859,35 +35806,33 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - optimise collisions
      }
  
-     public VoxelShape getFaceShape(Direction facing) {
-@@ -0,0 +0,0 @@ public abstract class VoxelShape {
-         }
+     public VoxelShape getFaceShape(Direction side) {
+@@ -208,19 +830,23 @@ public abstract class VoxelShape {
      }
  
--    private VoxelShape calculateFace(Direction facing) {
--        Direction.Axis axis = facing.getAxis();
+     private VoxelShape calculateFace(Direction side) {
+-        Direction.Axis axis = side.getAxis();
 -        if (this.isCubeLikeAlong(axis)) {
 -            return this;
 -        } else {
--            Direction.AxisDirection axisDirection = facing.getAxisDirection();
+-            Direction.AxisDirection axisDirection = side.getAxisDirection();
 -            int i = this.findIndex(axis, axisDirection == Direction.AxisDirection.POSITIVE ? 0.9999999 : 1.0E-7);
 -            SliceShape sliceShape = new SliceShape(this, axis, i);
 -            if (sliceShape.isEmpty()) {
 -                return Shapes.empty();
 -            } else {
 -                return (VoxelShape)(sliceShape.isCubeLike() ? Shapes.block() : sliceShape);
-+    private VoxelShape calculateFace(Direction direction) {
 +        // Paper start - optimise collisions
-+        final Direction.Axis axis = direction.getAxis();
++        final Direction.Axis axis = side.getAxis();
 +        switch (axis) {
 +            case X: {
-+                return this.calculateFaceDirect(direction, axis, this.rootCoordinatesX, this.offsetX);
++                return this.calculateFaceDirect(side, axis, this.rootCoordinatesX, this.offsetX);
 +            }
 +            case Y: {
-+                return this.calculateFaceDirect(direction, axis, this.rootCoordinatesY, this.offsetY);
++                return this.calculateFaceDirect(side, axis, this.rootCoordinatesY, this.offsetY);
 +            }
 +            case Z: {
-+                return this.calculateFaceDirect(direction, axis, this.rootCoordinatesZ, this.offsetZ);
++                return this.calculateFaceDirect(side, axis, this.rootCoordinatesZ, this.offsetZ);
 +            }
 +            default: {
 +                throw new IllegalStateException("Unknown axis: " + axis);
@@ -35897,12 +35842,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      protected boolean isCubeLike() {
-@@ -0,0 +0,0 @@ public abstract class VoxelShape {
-             && DoubleMath.fuzzyEquals(doubleList.getDouble(1), 1.0, 1.0E-7);
+@@ -238,9 +864,30 @@ public abstract class VoxelShape {
+         return coords.size() == 2 && DoubleMath.fuzzyEquals(coords.getDouble(0), 0.0, 1.0E-7) && DoubleMath.fuzzyEquals(coords.getDouble(1), 1.0, 1.0E-7);
      }
  
--    public double collide(Direction.Axis axis, AABB box, double maxDist) {
--        return this.collideX(AxisCycle.between(axis, Direction.Axis.X), box, maxDist);
+-    public double collide(Direction.Axis movementAxis, AABB collisionBox, double desiredOffset) {
+-        return this.collideX(AxisCycle.between(movementAxis, Direction.Axis.X), collisionBox, desiredOffset);
 +    // Paper start - optimise collisions
 +    public double collide(final Direction.Axis axis, final AABB source, final double source_move) {
 +        if (this.isEmpty) {
@@ -35928,13 +35873,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
 +    // Paper end - optimise collisions
  
-     protected double collideX(AxisCycle axisCycle, AABB box, double maxDist) {
+     protected double collideX(AxisCycle movementAxis, AABB collisionBox, double desiredOffset) {
          if (this.isEmpty()) {
-diff --git a/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java b/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java
-+++ b/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java
-@@ -0,0 +0,0 @@ import net.minecraft.core.BlockPos;
+diff --git a/net/minecraft/world/ticks/LevelChunkTicks.java b/net/minecraft/world/ticks/LevelChunkTicks.java
+index 5b6bd88a5bbbce6cce351938418eba4326e41002..faf45ac459f7c25309d6ef6dce371d484a0dae7b 100644
+--- a/net/minecraft/world/ticks/LevelChunkTicks.java
++++ b/net/minecraft/world/ticks/LevelChunkTicks.java
+@@ -17,7 +17,7 @@ import net.minecraft.core.BlockPos;
  import net.minecraft.nbt.ListTag;
  import net.minecraft.world.level.ChunkPos;
  
@@ -35943,7 +35888,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      private final Queue<ScheduledTick<T>> tickQueue = new PriorityQueue<>(ScheduledTick.DRAIN_ORDER);
      @Nullable
      private List<SavedTick<T>> pendingTicks;
-@@ -0,0 +0,0 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
+@@ -25,6 +25,30 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
      @Nullable
      private BiConsumer<LevelChunkTicks<T>, ScheduledTick<T>> onTickAdded;
  
@@ -35974,7 +35919,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      public LevelChunkTicks() {
      }
  
-@@ -0,0 +0,0 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
+@@ -49,7 +73,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
      public ScheduledTick<T> poll() {
          ScheduledTick<T> scheduledTick = this.tickQueue.poll();
          if (scheduledTick != null) {
@@ -35983,16 +35928,16 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          }
  
          return scheduledTick;
-@@ -0,0 +0,0 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
+@@ -58,7 +82,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
      @Override
-     public void schedule(ScheduledTick<T> orderedTick) {
-         if (this.ticksPerPosition.add(orderedTick)) {
--            this.scheduleUnchecked(orderedTick);
-+            this.scheduleUnchecked(orderedTick); this.dirty = true; // Paper - rewrite chunk system
+     public void schedule(ScheduledTick<T> tick) {
+         if (this.ticksPerPosition.add(tick)) {
+-            this.scheduleUnchecked(tick);
++            this.scheduleUnchecked(tick); this.dirty = true; // Paper - rewrite chunk system
          }
      }
  
-@@ -0,0 +0,0 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
+@@ -80,7 +104,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
          while (iterator.hasNext()) {
              ScheduledTick<T> scheduledTick = iterator.next();
              if (predicate.test(scheduledTick)) {
@@ -36001,330 +35946,19 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
                  this.ticksPerPosition.remove(scheduledTick);
              }
          }
-@@ -0,0 +0,0 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
+@@ -110,6 +134,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
      }
  
-     public ListTag save(long time, Function<T, String> typeToNameFunction) {
-+        this.lastSaved = time; // Paper - rewrite chunk system
+     public ListTag save(long gametime, Function<T, String> idGetter) {
++        this.lastSaved = gametime; // Paper - rewrite chunk system
          ListTag listTag = new ListTag();
  
-         for (SavedTick<T> savedTick : this.pack(time)) {
-@@ -0,0 +0,0 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
+         for (SavedTick<T> savedTick : this.pack(gametime)) {
+@@ -121,6 +146,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
  
-     public void unpack(long time) {
+     public void unpack(long gameTime) {
          if (this.pendingTicks != null) {
-+            this.lastSaved = time; // Paper - rewrite chunk system
++            this.lastSaved = gameTime; // Paper - rewrite chunk system
              int i = -this.pendingTicks.size();
  
              for (SavedTick<T> savedTick : this.pendingTicks) {
-diff --git a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
-+++ b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
-@@ -0,0 +0,0 @@ public class CraftChunk implements Chunk {
-     }
- 
-     public ChunkAccess getHandle(ChunkStatus chunkStatus) {
-+        // Paper start - rewrite chunk system
-+        net.minecraft.world.level.chunk.LevelChunk full = this.worldServer.getChunkIfLoaded(this.x, this.z);
-+        if (full != null) {
-+            return full;
-+        }
-+        // Paper end - rewrite chunk system
-         ChunkAccess chunkAccess = this.worldServer.getChunk(this.x, this.z, chunkStatus);
- 
-         // SPIGOT-7332: Get unwrapped extension
-@@ -0,0 +0,0 @@ public class CraftChunk implements Chunk {
- 
-     @Override
-     public boolean isEntitiesLoaded() {
--        return this.getCraftWorld().getHandle().entityManager.areEntitiesLoaded(ChunkPos.asLong(this.x, this.z));
-+        return this.getCraftWorld().getHandle().areEntitiesLoaded(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(this.x, this.z)); // Paper - rewrite chunk system
-     }
- 
-     @Override
-     public Entity[] getEntities() {
--        if (!this.isLoaded()) {
--            this.getWorld().getChunkAt(this.x, this.z); // Transient load for this tick
--        }
--
--        PersistentEntitySectionManager<net.minecraft.world.entity.Entity> entityManager = this.getCraftWorld().getHandle().entityManager;
--        long pair = ChunkPos.asLong(this.x, this.z);
--
--        if (entityManager.areEntitiesLoaded(pair)) {
--            return entityManager.getEntities(new ChunkPos(this.x, this.z)).stream()
--                    .map(net.minecraft.world.entity.Entity::getBukkitEntity)
--                    .filter(Objects::nonNull).toArray(Entity[]::new);
--        }
--
--        entityManager.ensureChunkQueuedForLoad(pair); // Start entity loading
--
--        // SPIGOT-6772: Use entity mailbox and re-schedule entities if they get unloaded
--        ConsecutiveExecutor mailbox = ((EntityStorage) entityManager.permanentStorage).entityDeserializerQueue;
--        BooleanSupplier supplier = () -> {
--            // only execute inbox if our entities are not present
--            if (entityManager.areEntitiesLoaded(pair)) {
--                return true;
--            }
--
--            if (!entityManager.isPending(pair)) {
--                // Our entities got unloaded, this should normally not happen.
--                entityManager.ensureChunkQueuedForLoad(pair); // Re-start entity loading
--            }
--
--            // tick loading inbox, which loads the created entities to the world
--            // (if present)
--            entityManager.tick();
--            // check if our entities are loaded
--            return entityManager.areEntitiesLoaded(pair);
--        };
--
--        // now we wait until the entities are loaded,
--        // the converting from NBT to entity object is done on the main Thread which is why we wait
--        while (!supplier.getAsBoolean()) {
--            if (mailbox.size() != 0) {
--                mailbox.run();
--            } else {
--                Thread.yield();
--                LockSupport.parkNanos("waiting for entity loading", 100000L);
--            }
--        }
--
--        return entityManager.getEntities(new ChunkPos(this.x, this.z)).stream()
--                .map(net.minecraft.world.entity.Entity::getBukkitEntity)
--                .filter(Objects::nonNull).toArray(Entity[]::new);
-+        return this.getCraftWorld().getHandle().getChunkEntities(this.x, this.z); // Paper - rewrite chunk system
-     }
- 
-     @Override
-diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
-+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
-@@ -0,0 +0,0 @@ public final class CraftServer implements Server {
-         // Paper - Put world into worldlist before initing the world; move up
- 
-         this.getServer().prepareLevels(internal.getChunkSource().chunkMap.progressListener, internal);
--        internal.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API
-+        // Paper - rewrite chunk system
- 
-         this.pluginManager.callEvent(new WorldLoadEvent(internal.getWorld()));
-         return internal.getWorld();
-@@ -0,0 +0,0 @@ public final class CraftServer implements Server {
-             }
- 
-             handle.getChunkSource().close(save);
--            handle.entityManager.close(save); // SPIGOT-6722: close entityManager
-+            // Paper - rewrite chunk system
-             handle.convertable.close();
-         } catch (Exception ex) {
-             this.getLogger().log(Level.SEVERE, null, ex);
-@@ -0,0 +0,0 @@ public final class CraftServer implements Server {
- 
-     @Override
-     public boolean isPrimaryThread() {
--        return Thread.currentThread().equals(this.console.serverThread) || this.console.hasStopped() || !org.spigotmc.AsyncCatcher.enabled; // All bets are off if we have shut down (e.g. due to watchdog)
-+        return ca.spottedleaf.moonrise.common.util.TickThread.isTickThread(); // Paper - rewrite chunk system
-     }
- 
-     // Paper start - Adventure
-diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
-+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
-@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World {
-         ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z));
-         if (playerChunk == null) return false;
- 
--        playerChunk.getTickingChunkFuture().thenAccept(either -> {
--            either.ifSuccess(chunk -> {
-+        // Paper start - chunk system
-+        net.minecraft.world.level.chunk.LevelChunk chunk = playerChunk.getChunkToSend();
-+        if (chunk == null) {
-+            return false;
-+        }
-+        // Paper end - chunk system
-                 List<ServerPlayer> playersInRange = playerChunk.playerProvider.getPlayers(playerChunk.getPos(), false);
--                if (playersInRange.isEmpty()) return;
-+                if (playersInRange.isEmpty()) return true; // Paper - chunk system
- 
-                 FeatureHooks.sendChunkRefreshPackets(playersInRange, chunk);
--            });
--        });
--
-+        // Paper - chunk system
-         return true;
-     }
- 
-@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World {
-     @Override
-     public Collection<Plugin> getPluginChunkTickets(int x, int z) {
-         DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager;
--        SortedArraySet<Ticket<?>> tickets = chunkDistanceManager.tickets.get(ChunkPos.asLong(x, z));
--
--        if (tickets == null) {
--            return Collections.emptyList();
--        }
- 
--        ImmutableList.Builder<Plugin> ret = ImmutableList.builder();
--        for (Ticket<?> ticket : tickets) {
--            if (ticket.getType() == TicketType.PLUGIN_TICKET) {
--                ret.add((Plugin) ticket.key);
--            }
--        }
--
--        return ret.build();
-+        return chunkDistanceManager.moonrise$getChunkHolderManager().getPluginChunkTickets(x, z); // Paper - rewrite chunk system
-     }
- 
-     @Override
-@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World {
-         Map<Plugin, ImmutableList.Builder<Chunk>> ret = new HashMap<>();
-         DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager;
- 
--        for (Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> chunkTickets : chunkDistanceManager.tickets.long2ObjectEntrySet()) {
-+        for (Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> chunkTickets : chunkDistanceManager.moonrise$getChunkHolderManager().getTicketsCopy().long2ObjectEntrySet()) {  // Paper - rewrite chunk system
-             long chunkKey = chunkTickets.getLongKey();
-             SortedArraySet<Ticket<?>> tickets = chunkTickets.getValue();
- 
-@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World {
- 
-     @Override
-     public int getViewDistance() {
--        return this.world.getChunkSource().chunkMap.serverViewDistance;
-+        return this.getHandle().moonrise$getPlayerChunkLoader().getAPIViewDistance(); // Paper - rewrite chunk system
-     }
- 
-     @Override
-     public int getSimulationDistance() {
--        return this.world.getChunkSource().chunkMap.getDistanceManager().simulationDistance;
-+        return this.getHandle().moonrise$getPlayerChunkLoader().getAPITickDistance(); // Paper - rewrite chunk system
-     }
- 
-     public BlockMetadataStore getBlockMetadata() {
-@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World {
- 
-     @Override
-     public void setSimulationDistance(final int simulationDistance) {
--        throw new UnsupportedOperationException("Not implemented yet");
-+        if (simulationDistance < 2 || simulationDistance > 32) {
-+            throw new IllegalArgumentException("Simulation distance " + simulationDistance + " is out of range of [2, 32]");
-+        }
-+        this.getHandle().chunkSource.setSimulationDistance(simulationDistance); // Paper - rewrite chunk system
-     }
- 
-     @Override
-     public int getSendViewDistance() {
--        return this.getViewDistance();
-+        return this.getHandle().moonrise$getPlayerChunkLoader().getAPISendViewDistance(); // Paper - rewrite chunk system
-     }
- 
-     @Override
-     public void setSendViewDistance(final int viewDistance) {
--        throw new UnsupportedOperationException("Not implemented yet");
-+        this.getHandle().chunkSource.setSendViewDistance(viewDistance); // Paper - rewrite chunk system
-     }
- 
-     // Paper start - implement pointers
-diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
-+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
-@@ -0,0 +0,0 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
- 
-     @Override
-     public void setViewDistance(final int viewDistance) {
--        throw new UnsupportedOperationException("Not implemented yet");
-+        // Paper - rewrite chunk system - TODO do this better
-+        ((ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer)this.getHandle())
-+            .moonrise$getViewDistanceHolder().setLoadViewDistance(viewDistance + 1);
-     }
- 
-     @Override
-@@ -0,0 +0,0 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
- 
-     @Override
-     public void setSimulationDistance(final int simulationDistance) {
--        throw new UnsupportedOperationException("Not implemented yet");
-+        // Paper - rewrite chunk system - TODO do this better
-+        ((ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer)this.getHandle())
-+            .moonrise$getViewDistanceHolder().setTickViewDistance(simulationDistance);
-     }
- 
-     @Override
-@@ -0,0 +0,0 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
- 
-     @Override
-     public void setSendViewDistance(final int viewDistance) {
--        throw new UnsupportedOperationException("Not implemented yet");
-+        // Paper - rewrite chunk system - TODO do this better
-+        ((ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer)this.getHandle())
-+            .moonrise$getViewDistanceHolder().setSendViewDistance(viewDistance);
-     }
- 
-     // Paper start - entity effect API
-diff --git a/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java b/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java
-+++ b/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java
-@@ -0,0 +0,0 @@ public class CustomChunkGenerator extends InternalChunkGenerator {
-             return ichunkaccess1;
-         };
- 
--        return future == null ? CompletableFuture.supplyAsync(() -> function.apply(chunk), net.minecraft.Util.backgroundExecutor()) : future.thenApply(function);
-+        return future == null ? CompletableFuture.supplyAsync(() -> function.apply(chunk), Runnable::run) : future.thenApply(function); // Paper - rewrite chunk system
-     }
- 
-     @Override
-diff --git a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
-+++ b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
-@@ -0,0 +0,0 @@ public abstract class DelegatedGeneratorAccess implements WorldGenLevel {
-     public ChunkAccess getChunkIfLoadedImmediately(final int x, final int z) {
-         return this.handle.getChunkIfLoadedImmediately(x, z);
-     }
-+
-+    // Paper start - rewrite chunk system
-+    @Override
-+    public java.util.List<net.minecraft.world.entity.Entity> moonrise$getHardCollidingEntities(final net.minecraft.world.entity.Entity entity, final net.minecraft.world.phys.AABB box, final java.util.function.Predicate<? super net.minecraft.world.entity.Entity> predicate) {
-+        return this.handle.moonrise$getHardCollidingEntities(entity, box, predicate);
-+    }
-+    // Paper end - rewrite chunk system
-     // Paper end
- }
- 
-diff --git a/src/main/java/org/spigotmc/AsyncCatcher.java b/src/main/java/org/spigotmc/AsyncCatcher.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/spigotmc/AsyncCatcher.java
-+++ b/src/main/java/org/spigotmc/AsyncCatcher.java
-@@ -0,0 +0,0 @@ public class AsyncCatcher
- 
-     public static void catchOp(String reason)
-     {
--        if ( AsyncCatcher.enabled && Thread.currentThread() != MinecraftServer.getServer().serverThread )
-+        if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThread()) // Paper // Paper - rewrite chunk system
-         {
-             MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); // Paper
-             throw new IllegalStateException( "Asynchronous " + reason + "!" );
-diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/spigotmc/WatchdogThread.java
-+++ b/src/main/java/org/spigotmc/WatchdogThread.java
-@@ -0,0 +0,0 @@ import java.util.logging.Logger;
- import net.minecraft.server.MinecraftServer;
- import org.bukkit.Bukkit;
- 
--public class WatchdogThread extends Thread
-+public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThread // Paper - rewrite chunk system
- {
- 
-     private static WatchdogThread instance;
-@@ -0,0 +0,0 @@ public class WatchdogThread extends Thread
-                 // Paper end - Different message for short timeout
-                 log.log( Level.SEVERE, "------------------------------" );
-                 log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper
-+                ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(MinecraftServer.getServer(), isLongTimeout); // Paper - rewrite chunk system
-                 WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log );
-                 log.log( Level.SEVERE, "------------------------------" );
-                 //
diff --git a/feature-patches/1068-Fix-entity-tracker-desync-when-new-players-are-added.patch b/paper-server/patches/features/0018-Fix-entity-tracker-desync-when-new-players-are-added.patch
similarity index 54%
rename from feature-patches/1068-Fix-entity-tracker-desync-when-new-players-are-added.patch
rename to paper-server/patches/features/0018-Fix-entity-tracker-desync-when-new-players-are-added.patch
index e80eb74101..1059713e9f 100644
--- a/feature-patches/1068-Fix-entity-tracker-desync-when-new-players-are-added.patch
+++ b/paper-server/patches/features/0018-Fix-entity-tracker-desync-when-new-players-are-added.patch
@@ -28,30 +28,30 @@ which is most likely in an unloaded chunk - which means that the
 client will not tick the entity and thus not lerp the entity
 from its old position to its new position.
 
-diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundAddEntityPacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundAddEntityPacket.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/network/protocol/game/ClientboundAddEntityPacket.java
-+++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundAddEntityPacket.java
-@@ -0,0 +0,0 @@ public class ClientboundAddEntityPacket implements Packet<ClientGamePacketListen
+diff --git a/net/minecraft/network/protocol/game/ClientboundAddEntityPacket.java b/net/minecraft/network/protocol/game/ClientboundAddEntityPacket.java
+index db31989ebe3d7021cfd2311439e9a00f819b0841..1373977b339405ef59bb3ea03d195285c96dd3fe 100644
+--- a/net/minecraft/network/protocol/game/ClientboundAddEntityPacket.java
++++ b/net/minecraft/network/protocol/game/ClientboundAddEntityPacket.java
+@@ -42,9 +42,11 @@ public class ClientboundAddEntityPacket implements Packet<ClientGamePacketListen
          this(
              entity.getId(),
              entity.getUUID(),
--            entityTrackerEntry.getPositionBase().x(),
--            entityTrackerEntry.getPositionBase().y(),
--            entityTrackerEntry.getPositionBase().z(),
+-            serverEntity.getPositionBase().x(),
+-            serverEntity.getPositionBase().y(),
+-            serverEntity.getPositionBase().z(),
 +            // Paper start - fix entity tracker desync
 +            entity.trackingPosition().x(),
 +            entity.trackingPosition().y(),
 +            entity.trackingPosition().z(),
 +            // Paper end - fix entity tracker desync
-             entityTrackerEntry.getLastSentXRot(),
-             entityTrackerEntry.getLastSentYRot(),
+             serverEntity.getLastSentXRot(),
+             serverEntity.getLastSentYRot(),
              entity.getType(),
-diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ChunkMap.java
-+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+diff --git a/net/minecraft/server/level/ChunkMap.java b/net/minecraft/server/level/ChunkMap.java
+index 3dff97f13586be3b52bbe786852c185f6753a019..ff6503bf8eb88d1264c3d848a89d0255b4b3ae68 100644
+--- a/net/minecraft/server/level/ChunkMap.java
++++ b/net/minecraft/server/level/ChunkMap.java
+@@ -1208,6 +1208,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
                          this.serverEntity.addPairing(player);
                          }
                          // Paper end - entity tracking events
@@ -59,11 +59,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
                      }
                  } else if (this.seenBy.remove(player.connection)) {
                      this.serverEntity.removePairing(player);
-diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerEntity.java
-+++ b/src/main/java/net/minecraft/server/level/ServerEntity.java
-@@ -0,0 +0,0 @@ public class ServerEntity {
+diff --git a/net/minecraft/server/level/ServerEntity.java b/net/minecraft/server/level/ServerEntity.java
+index 870b9efd445ddadb3725e88351555ad986ce7c72..a4da36060ca75968f5831adfc3f7117281649b7a 100644
+--- a/net/minecraft/server/level/ServerEntity.java
++++ b/net/minecraft/server/level/ServerEntity.java
+@@ -90,6 +90,13 @@ public class ServerEntity {
          this.trackedDataValues = entity.getEntityData().getNonDefaultValues();
      }
  
@@ -77,29 +77,29 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      public void sendChanges() {
          // Paper start - optimise collisions
          if (((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)this.entity).moonrise$isHardColliding()) {
-@@ -0,0 +0,0 @@ public class ServerEntity {
-             }
+@@ -130,7 +137,7 @@ public class ServerEntity {
+             this.sendDirtyEntityData();
          }
  
 -        if (this.tickCount % this.updateInterval == 0 || this.entity.hasImpulse || this.entity.getEntityData().isDirty()) {
 +        if (this.forceStateResync || this.tickCount % this.updateInterval == 0 || this.entity.hasImpulse || this.entity.getEntityData().isDirty()) { // Paper - fix desync when a player is added to the tracker
-             byte b0 = Mth.packDegrees(this.entity.getYRot());
+             byte b = Mth.packDegrees(this.entity.getYRot());
              byte b1 = Mth.packDegrees(this.entity.getXRot());
-             boolean flag = Math.abs(b0 - this.lastSentYRot) >= 1 || Math.abs(b1 - this.lastSentXRot) >= 1;
-@@ -0,0 +0,0 @@ public class ServerEntity {
-                     long k = this.positionCodec.encodeZ(vec3d);
-                     boolean flag5 = i < -32768L || i > 32767L || j < -32768L || j > 32767L || k < -32768L || k > 32767L;
- 
--                    if (!flag5 && this.teleportDelay <= 400 && !this.wasRiding && this.wasOnGround == this.entity.onGround()) {
-+                    if (!this.forceStateResync && !flag5 && this.teleportDelay <= 400 && !this.wasRiding && this.wasOnGround == this.entity.onGround()) { // Paper - fix desync when a player is added to the tracker
-                         if ((!flag2 || !flag) && !(this.entity instanceof AbstractArrow)) {
-                             if (flag2) {
-                                 packet1 = new ClientboundMoveEntityPacket.Pos(this.entity.getId(), (short) ((int) i), (short) ((int) j), (short) ((int) k), this.entity.onGround());
-@@ -0,0 +0,0 @@ public class ServerEntity {
+             boolean flag = Math.abs(b - this.lastSentYRot) >= 1 || Math.abs(b1 - this.lastSentXRot) >= 1;
+@@ -165,7 +172,7 @@ public class ServerEntity {
+                 long l1 = this.positionCodec.encodeY(vec3);
+                 long l2 = this.positionCodec.encodeZ(vec3);
+                 boolean flag5 = l < -32768L || l > 32767L || l1 < -32768L || l1 > 32767L || l2 < -32768L || l2 > 32767L;
+-                if (flag5 || this.teleportDelay > 400 || this.wasRiding || this.wasOnGround != this.entity.onGround()) {
++                if (this.forceStateResync || flag5 || this.teleportDelay > 400 || this.wasRiding || this.wasOnGround != this.entity.onGround()) { // Paper - fix desync when a player is added to the tracker
+                     this.wasOnGround = this.entity.onGround();
+                     this.teleportDelay = 0;
+                     packet = ClientboundEntityPositionSyncPacket.of(this.entity);
+@@ -230,6 +237,7 @@ public class ServerEntity {
              }
  
              this.entity.hasImpulse = false;
 +            this.forceStateResync = false; // Paper - fix desync when a player is added to the tracker
          }
  
-         ++this.tickCount;
+         this.tickCount++;
diff --git a/feature-patches/1073-Eigencraft-redstone-implementation.patch b/paper-server/patches/features/0019-Eigencraft-redstone-implementation.patch
similarity index 92%
rename from feature-patches/1073-Eigencraft-redstone-implementation.patch
rename to paper-server/patches/features/0019-Eigencraft-redstone-implementation.patch
index 87c9d93cc3..9b2677a63c 100644
--- a/feature-patches/1073-Eigencraft-redstone-implementation.patch
+++ b/paper-server/patches/features/0019-Eigencraft-redstone-implementation.patch
@@ -3,8 +3,6 @@ From: theosib <millerti@172.16.221.1>
 Date: Thu, 27 Sep 2018 01:43:35 -0600
 Subject: [PATCH] Eigencraft redstone implementation
 
-Author: theosib <millerti@172.16.221.1>
-
 Original license: MIT
 
 This patch implements theosib's redstone algorithms to completely overhaul the way redstone works.
@@ -17,19 +15,15 @@ A lot of this code is self-contained in a helper class.
 Aside from making the obvious class/function renames and obfhelpers I didn't need to modify much.
 Just added Bukkit's event system and took a few liberties with dead code and comment misspellings.
 
-== AT ==
-public net.minecraft.world.level.block.RedStoneWireBlock shouldSignal
-public net.minecraft.world.level.block.RedStoneWireBlock canSurvive(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/LevelReader;Lnet/minecraft/core/BlockPos;)Z
-
 Co-authored-by: egg82 <eggys82@gmail.com>
 
-diff --git a/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java b/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java
+diff --git a/io/papermc/paper/redstone/RedstoneWireTurbo.java b/io/papermc/paper/redstone/RedstoneWireTurbo.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..ff747a1ecdf3c888bca0d69de4f85dcd810b6139
 --- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java
-@@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.util;
++++ b/io/papermc/paper/redstone/RedstoneWireTurbo.java
+@@ -0,0 +1,954 @@
++package io.papermc.paper.redstone;
 +
 +import java.util.List;
 +import java.util.Map;
@@ -48,14 +42,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import com.google.common.collect.Lists;
 +import com.google.common.collect.Maps;
 +
-+/**
-+ * Used for the faster redstone algorithm.
-+ * Original author: theosib
-+ * Original license: MIT
-+ *
-+ * Ported to Paper and updated to 1.13 by egg82
-+ */
-+public class RedstoneWireTurbo {
++public final class RedstoneWireTurbo {
 +    /*
 +     * This is Helper class for BlockRedstoneWire.  It implements a minimally-invasive
 +     * bolt-on accelerator that performs a breadth-first search through redstone wire blocks
@@ -862,7 +849,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     */
 +    private BlockState calculateCurrentChanges(final Level worldIn, final UpdateNode upd) {
 +        BlockState state = upd.currentState;
-+        final int i = state.getValue(RedStoneWireBlock.POWER).intValue();
++        final int i = state.getValue(RedStoneWireBlock.POWER);
 +        int j = 0;
 +        j = getMaxCurrentStrength(upd, j);
 +        int l = 0;
@@ -986,21 +973,21 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     */
 +    private static int getMaxCurrentStrength(final UpdateNode upd, final int strength) {
 +        if (upd.type != UpdateNode.Type.REDSTONE) return strength;
-+        final int i = upd.currentState.getValue(RedStoneWireBlock.POWER).intValue();
++        final int i = upd.currentState.getValue(RedStoneWireBlock.POWER);
 +        return i > strength ? i : strength;
 +    }
 +}
-diff --git a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java
-+++ b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java
-@@ -0,0 +0,0 @@ public class RedStoneWireBlock extends Block {
-         return floor.isFaceSturdy(world, pos, Direction.UP) || floor.is(Blocks.HOPPER);
+diff --git a/net/minecraft/world/level/block/RedStoneWireBlock.java b/net/minecraft/world/level/block/RedStoneWireBlock.java
+index 84e6c986917128d4488afa23d29c689cadb4f55d..f02232ce97779db0d12a5d5da1d767326d78ea4c 100644
+--- a/net/minecraft/world/level/block/RedStoneWireBlock.java
++++ b/net/minecraft/world/level/block/RedStoneWireBlock.java
+@@ -290,6 +290,60 @@ public class RedStoneWireBlock extends Block {
+         return state.isFaceSturdy(level, pos, Direction.UP) || state.is(Blocks.HOPPER);
      }
  
 +    // Paper start - Optimize redstone
 +    // The bulk of the new functionality is found in RedstoneWireTurbo.java
-+    com.destroystokyo.paper.util.RedstoneWireTurbo turbo = new com.destroystokyo.paper.util.RedstoneWireTurbo(this);
++    io.papermc.paper.redstone.RedstoneWireTurbo turbo = new io.papermc.paper.redstone.RedstoneWireTurbo(this);
 +
 +    /*
 +     * Modified version of pre-existing updateSurroundingRedstone, which is called from
@@ -1052,46 +1039,46 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end
 +
-     private void updatePowerStrength(Level world, BlockPos pos, BlockState state, @Nullable Orientation orientation, boolean blockAdded) {
-         if (useExperimentalEvaluator(world)) {
-             new ExperimentalRedstoneWireEvaluator(this).updatePowerStrength(world, pos, state, orientation, blockAdded);
-@@ -0,0 +0,0 @@ public class RedStoneWireBlock extends Block {
+     private void updatePowerStrength(Level level, BlockPos pos, BlockState state, @Nullable Orientation orientation, boolean updateShape) {
+         if (useExperimentalEvaluator(level)) {
+             new ExperimentalRedstoneWireEvaluator(this).updatePowerStrength(level, pos, state, orientation, updateShape);
+@@ -318,7 +372,7 @@ public class RedStoneWireBlock extends Block {
      @Override
-     protected void onPlace(BlockState state, Level world, BlockPos pos, BlockState oldState, boolean notify) {
-         if (!oldState.is(state.getBlock()) && !world.isClientSide) {
--            this.updatePowerStrength(world, pos, state, null, true);
-+            this.updateSurroundingRedstone(world, pos, state, null, true); // Paper - Optimize redstone
+     protected void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean isMoving) {
+         if (!oldState.is(state.getBlock()) && !level.isClientSide) {
+-            this.updatePowerStrength(level, pos, state, null, true);
++            this.updateSurroundingRedstone(level, pos, state, null, true); // Paper - Optimize redstone
  
              for (Direction direction : Direction.Plane.VERTICAL) {
-                 world.updateNeighborsAt(pos.relative(direction), this);
-@@ -0,0 +0,0 @@ public class RedStoneWireBlock extends Block {
-                     world.updateNeighborsAt(pos.relative(direction), this);
+                 level.updateNeighborsAt(pos.relative(direction), this);
+@@ -337,7 +391,7 @@ public class RedStoneWireBlock extends Block {
+                     level.updateNeighborsAt(pos.relative(direction), this);
                  }
  
--                this.updatePowerStrength(world, pos, state, null, false);
-+                this.updateSurroundingRedstone(world, pos, state, null, false); // Paper - Optimize redstone
-                 this.updateNeighborsOfNeighboringWires(world, pos);
+-                this.updatePowerStrength(level, pos, state, null, false);
++                this.updateSurroundingRedstone(level, pos, state, null, false); // Paper - Optimize redstone
+                 this.updateNeighborsOfNeighboringWires(level, pos);
              }
          }
-@@ -0,0 +0,0 @@ public class RedStoneWireBlock extends Block {
-         if (!world.isClientSide) {
-             if (sourceBlock != this || !useExperimentalEvaluator(world)) {
-                 if (state.canSurvive(world, pos)) {
--                    this.updatePowerStrength(world, pos, state, wireOrientation, false);
-+                    this.updateSurroundingRedstone(world, pos, state, wireOrientation, false); // Paper - Optimize redstone
+@@ -363,7 +417,7 @@ public class RedStoneWireBlock extends Block {
+         if (!level.isClientSide) {
+             if (neighborBlock != this || !useExperimentalEvaluator(level)) {
+                 if (state.canSurvive(level, pos)) {
+-                    this.updatePowerStrength(level, pos, state, orientation, false);
++                    this.updateSurroundingRedstone(level, pos, state, orientation, false); // Paper - Optimize redstone
                  } else {
-                     dropResources(state, world, pos);
-                     world.removeBlock(pos, false);
-diff --git a/src/main/java/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java b/src/main/java/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java
-+++ b/src/main/java/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java
-@@ -0,0 +0,0 @@ public class DefaultRedstoneWireEvaluator extends RedstoneWireEvaluator {
- 
+                     dropResources(state, level, pos);
+                     level.removeBlock(pos, false);
+diff --git a/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java b/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java
+index 380fc51a252022195e178daccd8aa53dd1d71a2e..2d77780b6727f82ffc3cb216ca5f2d6483496cfd 100644
+--- a/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java
++++ b/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java
+@@ -44,7 +44,7 @@ public class DefaultRedstoneWireEvaluator extends RedstoneWireEvaluator {
+         }
      }
  
--    private int calculateTargetStrength(Level world, BlockPos pos) {
-+    public int calculateTargetStrength(Level world, BlockPos pos) { // Paper - Optimize redstone
-         int i = this.getBlockSignal(world, pos);
- 
-         return i == 15 ? i : Math.max(i, this.getIncomingWireSignal(world, pos));
+-    private int calculateTargetStrength(Level level, BlockPos pos) {
++    public int calculateTargetStrength(Level level, BlockPos pos) { // Paper - Optimize redstone
+         int blockSignal = this.getBlockSignal(level, pos);
+         return blockSignal == 15 ? blockSignal : Math.max(blockSignal, this.getIncomingWireSignal(level, pos));
+     }
diff --git a/feature-patches/1074-Add-Alternate-Current-redstone-implementation.patch b/paper-server/patches/features/0020-Add-Alternate-Current-redstone-implementation.patch
similarity index 92%
rename from feature-patches/1074-Add-Alternate-Current-redstone-implementation.patch
rename to paper-server/patches/features/0020-Add-Alternate-Current-redstone-implementation.patch
index 50c8d18711..98d1bff65a 100644
--- a/feature-patches/1074-Add-Alternate-Current-redstone-implementation.patch
+++ b/paper-server/patches/features/0020-Add-Alternate-Current-redstone-implementation.patch
@@ -20,12 +20,12 @@ Alternate Current needs the following modifications:
 * RedStoneWireBlock: Replace calls to vanilla's or Eigencraft's methods for handling power changes with calls to
 Alternate Current's wire handler.
 
-diff --git a/src/main/java/alternate/current/wire/LevelHelper.java b/src/main/java/alternate/current/wire/LevelHelper.java
+diff --git a/alternate/current/wire/LevelHelper.java b/alternate/current/wire/LevelHelper.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..eda108e2df9bf7d1ddd89287b8d2c2d7f1637c96
 --- /dev/null
-+++ b/src/main/java/alternate/current/wire/LevelHelper.java
-@@ -0,0 +0,0 @@
++++ b/alternate/current/wire/LevelHelper.java
+@@ -0,0 +1,66 @@
 +package alternate.current.wire;
 +
 +import org.bukkit.craftbukkit.block.CraftBlock;
@@ -92,12 +92,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return true;
 +    }
 +}
-diff --git a/src/main/java/alternate/current/wire/Node.java b/src/main/java/alternate/current/wire/Node.java
+diff --git a/alternate/current/wire/Node.java b/alternate/current/wire/Node.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..8af6c69098e64945361d116b5fd6ac21e97fcd8d
 --- /dev/null
-+++ b/src/main/java/alternate/current/wire/Node.java
-@@ -0,0 +0,0 @@
++++ b/alternate/current/wire/Node.java
+@@ -0,0 +1,113 @@
 +package alternate.current.wire;
 +
 +import java.util.Arrays;
@@ -211,12 +211,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        throw new UnsupportedOperationException("Not a WireNode!");
 +    }
 +}
-diff --git a/src/main/java/alternate/current/wire/PriorityQueue.java b/src/main/java/alternate/current/wire/PriorityQueue.java
+diff --git a/alternate/current/wire/PriorityQueue.java b/alternate/current/wire/PriorityQueue.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..d71b4d0e4c44a2620b41b89475412db53bea20ed
 --- /dev/null
-+++ b/src/main/java/alternate/current/wire/PriorityQueue.java
-@@ -0,0 +0,0 @@
++++ b/alternate/current/wire/PriorityQueue.java
+@@ -0,0 +1,211 @@
 +package alternate.current.wire;
 +
 +import java.util.AbstractQueue;
@@ -428,12 +428,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return prev;
 +    }
 +}
-diff --git a/src/main/java/alternate/current/wire/SimpleQueue.java b/src/main/java/alternate/current/wire/SimpleQueue.java
+diff --git a/alternate/current/wire/SimpleQueue.java b/alternate/current/wire/SimpleQueue.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..2b30074252551e1dc55d5be17d26fb4a2d8eb2e4
 --- /dev/null
-+++ b/src/main/java/alternate/current/wire/SimpleQueue.java
-@@ -0,0 +0,0 @@
++++ b/alternate/current/wire/SimpleQueue.java
+@@ -0,0 +1,112 @@
 +package alternate.current.wire;
 +
 +import java.util.AbstractQueue;
@@ -546,12 +546,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
-diff --git a/src/main/java/alternate/current/wire/UpdateOrder.java b/src/main/java/alternate/current/wire/UpdateOrder.java
+diff --git a/alternate/current/wire/UpdateOrder.java b/alternate/current/wire/UpdateOrder.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..29338efd16cf62bb49e81cce09fbafd9b4319e7c
 --- /dev/null
-+++ b/src/main/java/alternate/current/wire/UpdateOrder.java
-@@ -0,0 +0,0 @@
++++ b/alternate/current/wire/UpdateOrder.java
+@@ -0,0 +1,390 @@
 +package alternate.current.wire;
 +
 +import java.util.Locale;
@@ -942,12 +942,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public abstract void forEachNeighbor(NodeProvider nodes, Node source, int forward, Consumer<Node> action);
 +
 +}
-diff --git a/src/main/java/alternate/current/wire/WireConnection.java b/src/main/java/alternate/current/wire/WireConnection.java
+diff --git a/alternate/current/wire/WireConnection.java b/alternate/current/wire/WireConnection.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..4fd8cb29024330397cfe4cbc1f237d285bfb7b3e
 --- /dev/null
-+++ b/src/main/java/alternate/current/wire/WireConnection.java
-@@ -0,0 +0,0 @@
++++ b/alternate/current/wire/WireConnection.java
+@@ -0,0 +1,30 @@
 +package alternate.current.wire;
 +
 +/**
@@ -978,12 +978,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        this.accept = accept;
 +    }
 +}
-diff --git a/src/main/java/alternate/current/wire/WireConnectionManager.java b/src/main/java/alternate/current/wire/WireConnectionManager.java
+diff --git a/alternate/current/wire/WireConnectionManager.java b/alternate/current/wire/WireConnectionManager.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f03b313e58385d626490a9e64c9616fd08aa951e
 --- /dev/null
-+++ b/src/main/java/alternate/current/wire/WireConnectionManager.java
-@@ -0,0 +0,0 @@
++++ b/alternate/current/wire/WireConnectionManager.java
+@@ -0,0 +1,134 @@
 +package alternate.current.wire;
 +
 +import java.util.Arrays;
@@ -1118,12 +1118,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
-diff --git a/src/main/java/alternate/current/wire/WireHandler.java b/src/main/java/alternate/current/wire/WireHandler.java
+diff --git a/alternate/current/wire/WireHandler.java b/alternate/current/wire/WireHandler.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..259b301b2c8b64cb7974a235afb260e0e991af54
 --- /dev/null
-+++ b/src/main/java/alternate/current/wire/WireHandler.java
-@@ -0,0 +0,0 @@
++++ b/alternate/current/wire/WireHandler.java
+@@ -0,0 +1,1073 @@
 +package alternate.current.wire;
 +
 +import java.util.Iterator;
@@ -2197,12 +2197,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    }
 +}
-diff --git a/src/main/java/alternate/current/wire/WireNode.java b/src/main/java/alternate/current/wire/WireNode.java
+diff --git a/alternate/current/wire/WireNode.java b/alternate/current/wire/WireNode.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..298076a0db4e6ee6e4775ac43bf749d9f5689bdb
 --- /dev/null
-+++ b/src/main/java/alternate/current/wire/WireNode.java
-@@ -0,0 +0,0 @@
++++ b/alternate/current/wire/WireNode.java
+@@ -0,0 +1,122 @@
 +package alternate.current.wire;
 +
 +import net.minecraft.core.BlockPos;
@@ -2325,11 +2325,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return LevelHelper.setWireState(level, pos, state, added);
 +    }
 +}
-diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerLevel.java
-+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java
+index 95a4e37a3c93f9b3c56c7a7376ed521cd46fbb6f..46dfaed12c998c219a20c711a06531aed2c68012 100644
+--- a/net/minecraft/server/level/ServerLevel.java
++++ b/net/minecraft/server/level/ServerLevel.java
+@@ -214,6 +214,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
      public final UUID uuid;
      public boolean hasPhysicsEvent = true; // Paper - BlockPhysicsEvent
      public boolean hasEntityMoveEvent; // Paper - Add EntityMoveEvent
@@ -2337,7 +2337,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      public LevelChunk getChunkIfLoaded(int x, int z) {
          return this.chunkSource.getChunkAtIfLoadedImmediately(x, z); // Paper - Use getChunkIfLoadedImmediately
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+@@ -2555,6 +2556,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
          return this.chunkSource.getGenerator().getSeaLevel();
      }
  
@@ -2348,14 +2348,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - optimize redstone (Alternate Current)
 +
-     private final class EntityCallbacks implements LevelCallback<Entity> {
- 
-         EntityCallbacks() {}
-diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/Level.java
-+++ b/src/main/java/net/minecraft/world/level/Level.java
-@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
+     final class EntityCallbacks implements LevelCallback<Entity> {
+         @Override
+         public void onCreated(Entity entity) {
+diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java
+index 8331b49185500ab3b4307e9ae05126b4f83a318a..2bbebb4335d927f240abcac67a5b423e38dc33d7 100644
+--- a/net/minecraft/world/level/Level.java
++++ b/net/minecraft/world/level/Level.java
+@@ -2099,6 +2099,17 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
  
      public abstract FuelValues fuelValues();
  
@@ -2371,77 +2371,78 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    // Paper end - optimize redstone (Alternate Current)
 +
      public static enum ExplosionInteraction implements StringRepresentable {
- 
-         NONE("none"), BLOCK("block"), MOB("mob"), TNT("tnt"), TRIGGER("trigger"), STANDARD("standard"); // CraftBukkit - Add STANDARD which will always use Explosion.Effect.DESTROY
-diff --git a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java
-+++ b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java
-@@ -0,0 +0,0 @@ public class RedStoneWireBlock extends Block {
-         return floor.isFaceSturdy(world, pos, Direction.UP) || floor.is(Blocks.HOPPER);
+         NONE("none"),
+         BLOCK("block"),
+diff --git a/net/minecraft/world/level/block/RedStoneWireBlock.java b/net/minecraft/world/level/block/RedStoneWireBlock.java
+index f02232ce97779db0d12a5d5da1d767326d78ea4c..12c9d60314c99fb65e640d255a2d0c6b7790ad4d 100644
+--- a/net/minecraft/world/level/block/RedStoneWireBlock.java
++++ b/net/minecraft/world/level/block/RedStoneWireBlock.java
+@@ -290,7 +290,7 @@ public class RedStoneWireBlock extends Block {
+         return state.isFaceSturdy(level, pos, Direction.UP) || state.is(Blocks.HOPPER);
      }
  
 -    // Paper start - Optimize redstone
 +    // Paper start - Optimize redstone (Eigencraft)
      // The bulk of the new functionality is found in RedstoneWireTurbo.java
-     com.destroystokyo.paper.util.RedstoneWireTurbo turbo = new com.destroystokyo.paper.util.RedstoneWireTurbo(this);
+     io.papermc.paper.redstone.RedstoneWireTurbo turbo = new io.papermc.paper.redstone.RedstoneWireTurbo(this);
  
-@@ -0,0 +0,0 @@ public class RedStoneWireBlock extends Block {
+@@ -372,7 +372,13 @@ public class RedStoneWireBlock extends Block {
      @Override
-     protected void onPlace(BlockState state, Level world, BlockPos pos, BlockState oldState, boolean notify) {
-         if (!oldState.is(state.getBlock()) && !world.isClientSide) {
--            this.updateSurroundingRedstone(world, pos, state, null, true); // Paper - Optimize redstone
+     protected void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean isMoving) {
+         if (!oldState.is(state.getBlock()) && !level.isClientSide) {
+-            this.updateSurroundingRedstone(level, pos, state, null, true); // Paper - Optimize redstone
 +            // Paper start - optimize redstone - replace call to updatePowerStrength
-+            if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) {
-+                world.getWireHandler().onWireAdded(pos, state); // Alternate Current
++            if (level.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) {
++                level.getWireHandler().onWireAdded(pos, state); // Alternate Current
 +            } else {
-+                this.updateSurroundingRedstone(world, pos, state, null, true); // Vanilla/Eigencraft
++                this.updateSurroundingRedstone(level, pos, state, null, true); // Vanilla/Eigencraft
 +            }
-+            // Paper end
++            // Paper end - optimize redstone
  
              for (Direction direction : Direction.Plane.VERTICAL) {
-                 world.updateNeighborsAt(pos.relative(direction), this);
-@@ -0,0 +0,0 @@ public class RedStoneWireBlock extends Block {
-                     world.updateNeighborsAt(pos.relative(direction), this);
+                 level.updateNeighborsAt(pos.relative(direction), this);
+@@ -391,7 +397,13 @@ public class RedStoneWireBlock extends Block {
+                     level.updateNeighborsAt(pos.relative(direction), this);
                  }
  
--                this.updateSurroundingRedstone(world, pos, state, null, false); // Paper - Optimize redstone
+-                this.updateSurroundingRedstone(level, pos, state, null, false); // Paper - Optimize redstone
 +                // Paper start - optimize redstone - replace call to updatePowerStrength
-+                if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) {
-+                    world.getWireHandler().onWireRemoved(pos, state); // Alternate Current
++                if (level.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) {
++                    level.getWireHandler().onWireRemoved(pos, state); // Alternate Current
 +                } else {
-+                    this.updateSurroundingRedstone(world, pos, state, null, false); // Vanilla/Eigencraft
++                    this.updateSurroundingRedstone(level, pos, state, null, false); // Vanilla/Eigencraft
 +                }
-                 this.updateNeighborsOfNeighboringWires(world, pos);
++                // Paper end - optimize redstone
+                 this.updateNeighborsOfNeighboringWires(level, pos);
              }
          }
-@@ -0,0 +0,0 @@ public class RedStoneWireBlock extends Block {
+@@ -415,9 +427,15 @@ public class RedStoneWireBlock extends Block {
      @Override
-     protected void neighborChanged(BlockState state, Level world, BlockPos pos, Block sourceBlock, @Nullable Orientation wireOrientation, boolean notify) {
-         if (!world.isClientSide) {
+     protected void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighborBlock, @Nullable Orientation orientation, boolean movedByPiston) {
+         if (!level.isClientSide) {
 +            // Paper start - optimize redstone (Alternate Current)
 +            // Alternate Current handles breaking of redstone wires in the WireHandler.
-+            if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) {
-+                world.getWireHandler().onWireUpdated(pos, state, wireOrientation);
++            if (level.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) {
++                level.getWireHandler().onWireUpdated(pos, state, orientation);
 +            } else
-+            // Paper end - optimize redstone (Alternate Current)
-             if (sourceBlock != this || !useExperimentalEvaluator(world)) {
-                 if (state.canSurvive(world, pos)) {
--                    this.updateSurroundingRedstone(world, pos, state, wireOrientation, false); // Paper - Optimize redstone
-+                    this.updateSurroundingRedstone(world, pos, state, wireOrientation, false); // Paper - Optimize redstone (Eigencraft)
++                // Paper end - optimize redstone (Alternate Current)
+             if (neighborBlock != this || !useExperimentalEvaluator(level)) {
+                 if (state.canSurvive(level, pos)) {
+-                    this.updateSurroundingRedstone(level, pos, state, orientation, false); // Paper - Optimize redstone
++                    this.updateSurroundingRedstone(level, pos, state, orientation, false); // Paper - Optimize redstone (Eigencraft)
                  } else {
-                     dropResources(state, world, pos);
-                     world.removeBlock(pos, false);
-diff --git a/src/main/java/net/minecraft/world/level/redstone/ExperimentalRedstoneUtils.java b/src/main/java/net/minecraft/world/level/redstone/ExperimentalRedstoneUtils.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/redstone/ExperimentalRedstoneUtils.java
-+++ b/src/main/java/net/minecraft/world/level/redstone/ExperimentalRedstoneUtils.java
-@@ -0,0 +0,0 @@ public class ExperimentalRedstoneUtils {
-             if (up != null) {
-                 orientation = orientation.withFront(up);
+                     dropResources(state, level, pos);
+                     level.removeBlock(pos, false);
+diff --git a/net/minecraft/world/level/redstone/ExperimentalRedstoneUtils.java b/net/minecraft/world/level/redstone/ExperimentalRedstoneUtils.java
+index 83f5da3e24834882193b9d7e3a788517e4b12b55..0c50a0bbbef1229230712b7c04364fea06859674 100644
+--- a/net/minecraft/world/level/redstone/ExperimentalRedstoneUtils.java
++++ b/net/minecraft/world/level/redstone/ExperimentalRedstoneUtils.java
+@@ -17,6 +17,11 @@ public class ExperimentalRedstoneUtils {
+             if (front != null) {
+                 orientation = orientation.withFront(front);
              }
 +            // Paper start - Optimize redstone (Alternate Current) - use default front instead of random
-+            else if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) {
++            else if (level.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) {
 +                orientation = orientation.withFront(Direction.WEST);
 +            }
 +            // Paper end - Optimize redstone (Alternate Current)
diff --git a/feature-patches/1075-Improve-exact-choice-recipe-ingredients.patch b/paper-server/patches/features/0021-Improve-exact-choice-recipe-ingredients.patch
similarity index 57%
rename from feature-patches/1075-Improve-exact-choice-recipe-ingredients.patch
rename to paper-server/patches/features/0021-Improve-exact-choice-recipe-ingredients.patch
index be741b29e9..19c90b0017 100644
--- a/feature-patches/1075-Improve-exact-choice-recipe-ingredients.patch
+++ b/paper-server/patches/features/0021-Improve-exact-choice-recipe-ingredients.patch
@@ -6,15 +6,12 @@ Subject: [PATCH] Improve exact choice recipe ingredients
 Fixes exact choices not working with recipe book clicks
 and shapeless recipes.
 
-== AT ==
-public net.minecraft.world.item.ItemStackLinkedSet TYPE_AND_TAG
-
-diff --git a/src/main/java/io/papermc/paper/inventory/recipe/ItemOrExact.java b/src/main/java/io/papermc/paper/inventory/recipe/ItemOrExact.java
+diff --git a/io/papermc/paper/inventory/recipe/ItemOrExact.java b/io/papermc/paper/inventory/recipe/ItemOrExact.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..ce745e49cd54fe3ae187785563a1bd311d14b5b2
 --- /dev/null
-+++ b/src/main/java/io/papermc/paper/inventory/recipe/ItemOrExact.java
-@@ -0,0 +0,0 @@
++++ b/io/papermc/paper/inventory/recipe/ItemOrExact.java
+@@ -0,0 +1,63 @@
 +package io.papermc.paper.inventory.recipe;
 +
 +import net.minecraft.core.Holder;
@@ -78,12 +75,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
-diff --git a/src/main/java/io/papermc/paper/inventory/recipe/StackedContentsExtrasMap.java b/src/main/java/io/papermc/paper/inventory/recipe/StackedContentsExtrasMap.java
+diff --git a/io/papermc/paper/inventory/recipe/StackedContentsExtrasMap.java b/io/papermc/paper/inventory/recipe/StackedContentsExtrasMap.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f47c12e9dd6cfa857ca07a764edc22de372e25b6
 --- /dev/null
-+++ b/src/main/java/io/papermc/paper/inventory/recipe/StackedContentsExtrasMap.java
-@@ -0,0 +0,0 @@
++++ b/io/papermc/paper/inventory/recipe/StackedContentsExtrasMap.java
+@@ -0,0 +1,68 @@
 +package io.papermc.paper.inventory.recipe;
 +
 +import it.unimi.dsi.fastutil.objects.Object2IntMap;
@@ -152,92 +149,94 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return false;
 +    }
 +}
-diff --git a/src/main/java/net/minecraft/recipebook/ServerPlaceRecipe.java b/src/main/java/net/minecraft/recipebook/ServerPlaceRecipe.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/recipebook/ServerPlaceRecipe.java
-+++ b/src/main/java/net/minecraft/recipebook/ServerPlaceRecipe.java
-@@ -0,0 +0,0 @@ public class ServerPlaceRecipe<R extends Recipe<?>> {
+diff --git a/net/minecraft/recipebook/ServerPlaceRecipe.java b/net/minecraft/recipebook/ServerPlaceRecipe.java
+index 6475509689439636275b06b9eac33f0922d8fcfd..6c398c91909f42e352e80d0781549df9d834a060 100644
+--- a/net/minecraft/recipebook/ServerPlaceRecipe.java
++++ b/net/minecraft/recipebook/ServerPlaceRecipe.java
+@@ -40,6 +40,7 @@ public class ServerPlaceRecipe<R extends Recipe<?>> {
              return RecipeBookMenu.PostPlaceAction.NOTHING;
          } else {
              StackedItemContents stackedItemContents = new StackedItemContents();
 +            stackedItemContents.initializeExtras(recipe.value(), null); // Paper - Improve exact choice recipe ingredients
              inventory.fillStackedContents(stackedItemContents);
-             handler.fillCraftSlotsStackedContents(stackedItemContents);
+             menu.fillCraftSlotsStackedContents(stackedItemContents);
              return serverPlaceRecipe.tryPlaceRecipe(recipe, stackedItemContents);
-@@ -0,0 +0,0 @@ public class ServerPlaceRecipe<R extends Recipe<?>> {
+@@ -99,7 +100,7 @@ public class ServerPlaceRecipe<R extends Recipe<?>> {
          }
  
-         int j = this.calculateAmountToCraft(i, bl);
+         int i = this.calculateAmountToCraft(biggestCraftableStack, flag);
 -        List<Holder<Item>> list = new ArrayList<>();
 +        List<io.papermc.paper.inventory.recipe.ItemOrExact> list = new ArrayList<>(); // Paper - Improve exact choice recipe ingredients
-         if (finder.canCraft(recipe.value(), j, list::add)) {
-             int k = clampToMaxStackSize(j, list);
-             if (k != j) {
-@@ -0,0 +0,0 @@ public class ServerPlaceRecipe<R extends Recipe<?>> {
-                 this.gridWidth, this.gridHeight, recipe.value(), recipe.value().placementInfo().slotsToIngredientIndex(), (slotx, index, x, y) -> {
-                     if (slotx != -1) {
-                         Slot slot2 = this.inputGridSlots.get(index);
--                        Holder<Item> holder = list.get(slotx);
-+                        io.papermc.paper.inventory.recipe.ItemOrExact holder = list.get(slotx); // Paper - Improve exact choice recipe ingredients
-                         int jx = k;
+         if (stackedItemContents.canCraft(recipe.value(), i, list::add)) {
+             int i1 = clampToMaxStackSize(i, list);
+             if (i1 != i) {
+@@ -114,7 +115,7 @@ public class ServerPlaceRecipe<R extends Recipe<?>> {
+                 this.gridWidth, this.gridHeight, recipe.value(), recipe.value().placementInfo().slotsToIngredientIndex(), (item1, slot1, x, y) -> {
+                     if (item1 != -1) {
+                         Slot slot2 = this.inputGridSlots.get(slot1);
+-                        Holder<Item> holder = list.get(item1);
++                        io.papermc.paper.inventory.recipe.ItemOrExact holder = list.get(item1); // Paper - Improve exact choice recipe ingredients
+                         int i2 = i1;
  
-                         while (jx > 0) {
-@@ -0,0 +0,0 @@ public class ServerPlaceRecipe<R extends Recipe<?>> {
+                         while (i2 > 0) {
+@@ -129,9 +130,11 @@ public class ServerPlaceRecipe<R extends Recipe<?>> {
          }
      }
  
--    private static int clampToMaxStackSize(int count, List<Holder<Item>> entries) {
--        for (Holder<Item> holder : entries) {
--            count = Math.min(count, holder.value().getDefaultMaxStackSize());
+-    private static int clampToMaxStackSize(int amount, List<Holder<Item>> items) {
+-        for (Holder<Item> holder : items) {
+-            amount = Math.min(amount, holder.value().getDefaultMaxStackSize());
 +    // Paper start - Improve exact choice recipe ingredients
-+    private static int clampToMaxStackSize(int count, List<io.papermc.paper.inventory.recipe.ItemOrExact> entries) {
-+        for (io.papermc.paper.inventory.recipe.ItemOrExact holder : entries) {
-+            count = Math.min(count, holder.getMaxStackSize());
++    private static int clampToMaxStackSize(int amount, List<io.papermc.paper.inventory.recipe.ItemOrExact> items) {
++        for (io.papermc.paper.inventory.recipe.ItemOrExact holder : items) {
++            amount = Math.min(amount, holder.getMaxStackSize());
 +            // Paper end - Improve exact choice recipe ingredients
          }
  
-         return count;
-@@ -0,0 +0,0 @@ public class ServerPlaceRecipe<R extends Recipe<?>> {
+         return amount;
+@@ -160,7 +163,7 @@ public class ServerPlaceRecipe<R extends Recipe<?>> {
          }
      }
  
 -    private int moveItemToGrid(Slot slot, Holder<Item> item, int count) {
 +    private int moveItemToGrid(Slot slot, io.papermc.paper.inventory.recipe.ItemOrExact item, int count) { // Paper - Improve exact choice recipe ingredients
-         ItemStack itemStack = slot.getItem();
-         int i = this.inventory.findSlotMatchingCraftingIngredient(item, itemStack);
+         ItemStack item1 = slot.getItem();
+         int i = this.inventory.findSlotMatchingCraftingIngredient(item, item1);
          if (i == -1) {
-diff --git a/src/main/java/net/minecraft/world/entity/player/Inventory.java b/src/main/java/net/minecraft/world/entity/player/Inventory.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/entity/player/Inventory.java
-+++ b/src/main/java/net/minecraft/world/entity/player/Inventory.java
-@@ -0,0 +0,0 @@ public class Inventory implements Container, Nameable {
+diff --git a/net/minecraft/world/entity/player/Inventory.java b/net/minecraft/world/entity/player/Inventory.java
+index 8a29d771046667db22fba166fa5337de1896cd0d..839cbb67d3d38960d9114a4db5bab911b66a573c 100644
+--- a/net/minecraft/world/entity/player/Inventory.java
++++ b/net/minecraft/world/entity/player/Inventory.java
+@@ -185,12 +185,12 @@ public class Inventory implements Container, Nameable {
          return !stack.isDamaged() && !stack.isEnchanted() && !stack.has(DataComponents.CUSTOM_NAME);
      }
  
 -    public int findSlotMatchingCraftingIngredient(Holder<Item> item, ItemStack stack) {
 +    public int findSlotMatchingCraftingIngredient(io.papermc.paper.inventory.recipe.ItemOrExact item, ItemStack stack) { // Paper - Improve exact choice recipe ingredients
-         for (int i = 0; i < this.items.size(); ++i) {
-             ItemStack itemstack1 = (ItemStack) this.items.get(i);
- 
--            if (!itemstack1.isEmpty() && itemstack1.is(item) && Inventory.isUsableForCrafting(itemstack1) && (stack.isEmpty() || ItemStack.isSameItemSameComponents(stack, itemstack1))) {
-+            if (!itemstack1.isEmpty() && item.matches(itemstack1) && (!(item instanceof io.papermc.paper.inventory.recipe.ItemOrExact.Item) || Inventory.isUsableForCrafting(itemstack1)) && (stack.isEmpty() || ItemStack.isSameItemSameComponents(stack, itemstack1))) { // Paper - Improve exact choice recipe ingredients
+         for (int i = 0; i < this.items.size(); i++) {
+             ItemStack itemStack = this.items.get(i);
+             if (!itemStack.isEmpty()
+-                && itemStack.is(item)
+-                && isUsableForCrafting(itemStack)
++                && item.matches(itemStack) // Paper - Improve exact choice recipe ingredients
++                && (!(item instanceof io.papermc.paper.inventory.recipe.ItemOrExact.Item) || Inventory.isUsableForCrafting(itemStack)) // Paper - Improve exact choice recipe ingredients
+                 && (stack.isEmpty() || ItemStack.isSameItemSameComponents(stack, itemStack))) {
                  return i;
              }
-         }
-diff --git a/src/main/java/net/minecraft/world/entity/player/StackedContents.java b/src/main/java/net/minecraft/world/entity/player/StackedContents.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/entity/player/StackedContents.java
-+++ b/src/main/java/net/minecraft/world/entity/player/StackedContents.java
-@@ -0,0 +0,0 @@ import java.util.List;
+diff --git a/net/minecraft/world/entity/player/StackedContents.java b/net/minecraft/world/entity/player/StackedContents.java
+index a4b528574ab371af94b0e07819e471cec94da244..a3fea6c8397046596afe3c8b5589f2ed37fcdfc3 100644
+--- a/net/minecraft/world/entity/player/StackedContents.java
++++ b/net/minecraft/world/entity/player/StackedContents.java
+@@ -13,7 +13,7 @@ import java.util.List;
  import javax.annotation.Nullable;
  
  public class StackedContents<T> {
 -    public final Reference2IntOpenHashMap<T> amounts = new Reference2IntOpenHashMap<>();
 +    public final it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap<T> amounts = new it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap<>(); // Paper - Improve exact choice recipe ingredients (don't use "reference" map)
  
-     boolean hasAtLeast(T input, int minimum) {
-         return this.amounts.getInt(input) >= minimum;
-@@ -0,0 +0,0 @@ public class StackedContents<T> {
+     boolean hasAtLeast(T item, int amount) {
+         return this.amounts.getInt(item) >= amount;
+@@ -49,7 +49,7 @@ public class StackedContents<T> {
      List<T> getUniqueAvailableIngredientItems(Iterable<? extends StackedContents.IngredientInfo<T>> ingredients) {
          List<T> list = new ArrayList<>();
  
@@ -246,7 +245,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
              if (entry.getIntValue() > 0 && anyIngredientMatches(ingredients, entry.getKey())) {
                  list.add(entry.getKey());
              }
-@@ -0,0 +0,0 @@ public class StackedContents<T> {
+@@ -71,13 +71,13 @@ public class StackedContents<T> {
      @VisibleForTesting
      public int getResultUpperBound(List<? extends StackedContents.IngredientInfo<T>> ingredients) {
          int i = Integer.MAX_VALUE;
@@ -255,18 +254,18 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
          label31:
          for (StackedContents.IngredientInfo<T> ingredientInfo : ingredients) {
-             int j = 0;
+             int i1 = 0;
  
 -            for (Entry<T> entry : objectIterable) {
 +            for (it.unimi.dsi.fastutil.objects.Object2IntMap.Entry<T> entry : objectIterable) { // Paper - Improve exact choice recipe ingredients (don't use "reference" map)
-                 int k = entry.getIntValue();
-                 if (k > j) {
+                 int intValue = entry.getIntValue();
+                 if (intValue > i1) {
                      if (ingredientInfo.acceptsItem(entry.getKey())) {
-diff --git a/src/main/java/net/minecraft/world/entity/player/StackedItemContents.java b/src/main/java/net/minecraft/world/entity/player/StackedItemContents.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/entity/player/StackedItemContents.java
-+++ b/src/main/java/net/minecraft/world/entity/player/StackedItemContents.java
-@@ -0,0 +0,0 @@ import net.minecraft.world.item.crafting.PlacementInfo;
+diff --git a/net/minecraft/world/entity/player/StackedItemContents.java b/net/minecraft/world/entity/player/StackedItemContents.java
+index 6bbe2e51ef71d193e0a5d3cace2b0ad1760ce759..83ccde54c625d40dc595e000c533f60aa929bd5a 100644
+--- a/net/minecraft/world/entity/player/StackedItemContents.java
++++ b/net/minecraft/world/entity/player/StackedItemContents.java
+@@ -9,9 +9,14 @@ import net.minecraft.world.item.crafting.PlacementInfo;
  import net.minecraft.world.item.crafting.Recipe;
  
  public class StackedItemContents {
@@ -274,25 +273,30 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    // Paper start - Improve exact choice recipe ingredients
 +    private final StackedContents<io.papermc.paper.inventory.recipe.ItemOrExact> raw = new StackedContents<>();
 +    @Nullable
-+    private io.papermc.paper.inventory.recipe.StackedContentsExtrasMap extrasMap = null;
++    private io.papermc.paper.inventory.recipe.StackedContentsExtrasMap extrasMap;
 +    // Paper start - Improve exact choice recipe ingredients
  
-     public void accountSimpleStack(ItemStack item) {
-+        if (this.extrasMap != null && this.extrasMap.accountStack(item, Math.min(64, item.getCount()))) return; // Paper - Improve exact choice recipe ingredients; max of 64 due to accountStack method below
-         if (Inventory.isUsableForCrafting(item)) {
-             this.accountStack(item);
+     public void accountSimpleStack(ItemStack stack) {
++        if (this.extrasMap != null && this.extrasMap.accountStack(stack, Math.min(64, stack.getCount()))) return; // Paper - Improve exact choice recipe ingredients; max of 64 due to accountStack method below
+         if (Inventory.isUsableForCrafting(stack)) {
+             this.accountStack(stack);
          }
-@@ -0,0 +0,0 @@ public class StackedItemContents {
-     public void accountStack(ItemStack item, int maxCount) {
-         if (!item.isEmpty()) {
-             int i = Math.min(maxCount, item.getCount());
--            this.raw.account(item.getItemHolder(), i);
-+            if (this.extrasMap != null && !item.getComponentsPatch().isEmpty() && this.extrasMap.accountStack(item, i)) return; // Paper - Improve exact choice recipe ingredients; if an exact ingredient, don't include it
-+            this.raw.account(new io.papermc.paper.inventory.recipe.ItemOrExact.Item(item.getItemHolder()), i);
+@@ -24,34 +29,51 @@ public class StackedItemContents {
+     public void accountStack(ItemStack stack, int maxStackSize) {
+         if (!stack.isEmpty()) {
+             int min = Math.min(maxStackSize, stack.getCount());
+-            this.raw.account(stack.getItemHolder(), min);
++            if (this.extrasMap != null && !stack.getComponentsPatch().isEmpty() && this.extrasMap.accountStack(stack, min)) return; // Paper - Improve exact choice recipe ingredients; if an exact ingredient, don't include it
++            this.raw.account(new io.papermc.paper.inventory.recipe.ItemOrExact.Item(stack.getItemHolder()), min);
          }
      }
  
--    public boolean canCraft(Recipe<?> recipe, @Nullable StackedContents.Output<Holder<Item>> itemCallback) {
+-    public boolean canCraft(Recipe<?> recipe, @Nullable StackedContents.Output<Holder<Item>> output) {
++    public boolean canCraft(Recipe<?> recipe, @Nullable StackedContents.Output<io.papermc.paper.inventory.recipe.ItemOrExact> output) { // Paper - Improve exact choice recipe ingredients
+         return this.canCraft(recipe, 1, output);
+     }
+ 
+-    public boolean canCraft(Recipe<?> recipe, int maxCount, @Nullable StackedContents.Output<Holder<Item>> output) {
 +    // Paper start - Improve exact choice recipe ingredients
 +    public void initializeExtras(final Recipe<?> recipe, @Nullable final net.minecraft.world.item.crafting.CraftingInput input) {
 +        if (this.extrasMap == null) {
@@ -309,70 +313,64 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - Improve exact choice recipe ingredients
 +
-+    public boolean canCraft(Recipe<?> recipe, @Nullable StackedContents.Output<io.papermc.paper.inventory.recipe.ItemOrExact> itemCallback) { // Paper - Improve exact choice recipe ingredients
-         return this.canCraft(recipe, 1, itemCallback);
-     }
- 
--    public boolean canCraft(Recipe<?> recipe, int quantity, @Nullable StackedContents.Output<Holder<Item>> itemCallback) {
-+    public boolean canCraft(Recipe<?> recipe, int quantity, @Nullable StackedContents.Output<io.papermc.paper.inventory.recipe.ItemOrExact> itemCallback) { // Paper - Improve exact choice recipe ingredients
++    public boolean canCraft(Recipe<?> recipe, int maxCount, @Nullable StackedContents.Output<io.papermc.paper.inventory.recipe.ItemOrExact> output) { // Paper - Improve exact choice recipe ingredients
          PlacementInfo placementInfo = recipe.placementInfo();
-         return !placementInfo.isImpossibleToPlace() && this.canCraft(placementInfo.ingredients(), quantity, itemCallback);
+         return !placementInfo.isImpossibleToPlace() && this.canCraft(placementInfo.ingredients(), maxCount, output);
      }
  
-     public boolean canCraft(
--        List<? extends StackedContents.IngredientInfo<Holder<Item>>> rawIngredients, @Nullable StackedContents.Output<Holder<Item>> itemCallback
-+        List<? extends StackedContents.IngredientInfo<io.papermc.paper.inventory.recipe.ItemOrExact>> rawIngredients, @Nullable StackedContents.Output<io.papermc.paper.inventory.recipe.ItemOrExact> itemCallback // Paper - Improve exact choice recipe ingredients
-     ) {
-         return this.canCraft(rawIngredients, 1, itemCallback);
+-    public boolean canCraft(List<? extends StackedContents.IngredientInfo<Holder<Item>>> ingredients, @Nullable StackedContents.Output<Holder<Item>> output) {
++    public boolean canCraft(List<? extends StackedContents.IngredientInfo<io.papermc.paper.inventory.recipe.ItemOrExact>> ingredients, @Nullable StackedContents.Output<io.papermc.paper.inventory.recipe.ItemOrExact> output) { // Paper - Improve exact choice recipe ingredients
+         return this.canCraft(ingredients, 1, output);
      }
  
      private boolean canCraft(
--        List<? extends StackedContents.IngredientInfo<Holder<Item>>> rawIngredients, int quantity, @Nullable StackedContents.Output<Holder<Item>> itemCallback
-+        List<? extends StackedContents.IngredientInfo<io.papermc.paper.inventory.recipe.ItemOrExact>> rawIngredients, int quantity, @Nullable StackedContents.Output<io.papermc.paper.inventory.recipe.ItemOrExact> itemCallback // Paper - Improve exact choice recipe ingredients
+-        List<? extends StackedContents.IngredientInfo<Holder<Item>>> ingredients, int maxCount, @Nullable StackedContents.Output<Holder<Item>> output
++        List<? extends StackedContents.IngredientInfo<io.papermc.paper.inventory.recipe.ItemOrExact>> ingredients, int maxCount, @Nullable StackedContents.Output<io.papermc.paper.inventory.recipe.ItemOrExact> output // Paper - Improve exact choice recipe ingredients
      ) {
-         return this.raw.tryPick(rawIngredients, quantity, itemCallback);
+         return this.raw.tryPick(ingredients, maxCount, output);
      }
  
--    public int getBiggestCraftableStack(Recipe<?> recipe, @Nullable StackedContents.Output<Holder<Item>> itemCallback) {
-+    public int getBiggestCraftableStack(Recipe<?> recipe, @Nullable StackedContents.Output<io.papermc.paper.inventory.recipe.ItemOrExact> itemCallback) { // Paper - Improve exact choice recipe ingredients
-         return this.getBiggestCraftableStack(recipe, Integer.MAX_VALUE, itemCallback);
+-    public int getBiggestCraftableStack(Recipe<?> recipe, @Nullable StackedContents.Output<Holder<Item>> output) {
++    public int getBiggestCraftableStack(Recipe<?> recipe, @Nullable StackedContents.Output<io.papermc.paper.inventory.recipe.ItemOrExact> output) { // Paper - Improve exact choice recipe ingredients
+         return this.getBiggestCraftableStack(recipe, Integer.MAX_VALUE, output);
      }
  
--    public int getBiggestCraftableStack(Recipe<?> recipe, int max, @Nullable StackedContents.Output<Holder<Item>> itemCallback) {
-+    public int getBiggestCraftableStack(Recipe<?> recipe, int max, @Nullable StackedContents.Output<io.papermc.paper.inventory.recipe.ItemOrExact> itemCallback) { // Paper - Improve exact choice recipe ingredients
-         return this.raw.tryPickAll(recipe.placementInfo().ingredients(), max, itemCallback);
+-    public int getBiggestCraftableStack(Recipe<?> recipe, int maxCount, @Nullable StackedContents.Output<Holder<Item>> output) {
++    public int getBiggestCraftableStack(Recipe<?> recipe, int maxCount, @Nullable StackedContents.Output<io.papermc.paper.inventory.recipe.ItemOrExact> output) { // Paper - Improve exact choice recipe ingredients
+         return this.raw.tryPickAll(recipe.placementInfo().ingredients(), maxCount, output);
      }
  
-diff --git a/src/main/java/net/minecraft/world/item/crafting/Ingredient.java b/src/main/java/net/minecraft/world/item/crafting/Ingredient.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/item/crafting/Ingredient.java
-+++ b/src/main/java/net/minecraft/world/item/crafting/Ingredient.java
-@@ -0,0 +0,0 @@ import java.util.List;
- import javax.annotation.Nullable;
- // CraftBukkit end
+diff --git a/net/minecraft/world/item/crafting/Ingredient.java b/net/minecraft/world/item/crafting/Ingredient.java
+index e43641650d66a62b5b7b58c43833ce504970ab1e..879c8fe1f20decc793cfa39e686b61d521bd76ba 100644
+--- a/net/minecraft/world/item/crafting/Ingredient.java
++++ b/net/minecraft/world/item/crafting/Ingredient.java
+@@ -21,7 +21,7 @@ import net.minecraft.world.item.Items;
+ import net.minecraft.world.item.crafting.display.SlotDisplay;
+ import net.minecraft.world.level.ItemLike;
  
 -public final class Ingredient implements StackedContents.IngredientInfo<Holder<Item>>, Predicate<ItemStack> {
 +public final class Ingredient implements StackedContents.IngredientInfo<io.papermc.paper.inventory.recipe.ItemOrExact>, Predicate<ItemStack> { // Paper - Improve exact choice recipe ingredients
- 
-     public static final StreamCodec<RegistryFriendlyByteBuf, Ingredient> CONTENTS_STREAM_CODEC = ByteBufCodecs.holderSet(Registries.ITEM).map(Ingredient::new, (recipeitemstack) -> {
-         return recipeitemstack.values;
-@@ -0,0 +0,0 @@ public final class Ingredient implements StackedContents.IngredientInfo<Holder<I
+     public static final StreamCodec<RegistryFriendlyByteBuf, Ingredient> CONTENTS_STREAM_CODEC = ByteBufCodecs.holderSet(Registries.ITEM)
+         .map(Ingredient::new, ingredient -> ingredient.values);
+     public static final StreamCodec<RegistryFriendlyByteBuf, Optional<Ingredient>> OPTIONAL_CONTENTS_STREAM_CODEC = ByteBufCodecs.holderSet(Registries.ITEM)
+@@ -35,20 +35,24 @@ public final class Ingredient implements StackedContents.IngredientInfo<Holder<I
      private final HolderSet<Item> values;
      // CraftBukkit start
-     @Nullable
--    private List<ItemStack> itemStacks;
+     @javax.annotation.Nullable
+-    private java.util.List<ItemStack> itemStacks;
 +    private java.util.Set<ItemStack> itemStacks; // Paper - Improve exact choice recipe ingredients
  
      public boolean isExact() {
          return this.itemStacks != null;
      }
  
--    public List<ItemStack> itemStacks() {
+     @javax.annotation.Nullable
+-    public java.util.List<ItemStack> itemStacks() {
 +    public java.util.Set<ItemStack> itemStacks() { // Paper - Improve exact choice recipe ingredients
          return this.itemStacks;
      }
  
-     public static Ingredient ofStacks(List<ItemStack> stacks) {
+     public static Ingredient ofStacks(java.util.List<ItemStack> stacks) {
          Ingredient recipe = Ingredient.of(stacks.stream().map(ItemStack::getItem));
 -        recipe.itemStacks = stacks;
 +        // Paper start - Improve exact choice recipe ingredients
@@ -383,29 +381,29 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          return recipe;
      }
      // CraftBukkit end
-@@ -0,0 +0,0 @@ public final class Ingredient implements StackedContents.IngredientInfo<Holder<I
-     public boolean test(ItemStack itemstack) {
+@@ -81,21 +85,22 @@ public final class Ingredient implements StackedContents.IngredientInfo<Holder<I
+     public boolean test(ItemStack stack) {
          // CraftBukkit start
          if (this.isExact()) {
 -            for (ItemStack itemstack1 : this.itemStacks()) {
--                if (itemstack1.getItem() == itemstack.getItem() && ItemStack.isSameItemSameComponents(itemstack, itemstack1)) {
+-                if (itemstack1.getItem() == stack.getItem() && ItemStack.isSameItemSameComponents(stack, itemstack1)) {
 -                    return true;
 -                }
 -            }
 -
 -            return false;
-+            return this.itemStacks.contains(itemstack); // Paper - Improve exact choice recipe ingredients (hashing FTW!)
++            return this.itemStacks.contains(stack); // Paper - Improve exact choice recipe ingredients (hashing FTW!)
          }
          // CraftBukkit end
-         return itemstack.is(this.values);
+         return stack.is(this.values);
      }
  
--    public boolean acceptsItem(Holder<Item> holder) {
--        return this.values.contains(holder);
 +    // Paper start - Improve exact choice recipe ingredients
-+    @Override
-+    public boolean acceptsItem(final io.papermc.paper.inventory.recipe.ItemOrExact holder) {
-+        return switch (holder) {
+     @Override
+-    public boolean acceptsItem(Holder<Item> item) {
+-        return this.values.contains(item);
++    public boolean acceptsItem(final io.papermc.paper.inventory.recipe.ItemOrExact itemOrExact) {
++        return switch (itemOrExact) {
 +            case io.papermc.paper.inventory.recipe.ItemOrExact.Item(final Holder<Item> item) ->
 +                !this.isExact() && this.values.contains(item);
 +            case io.papermc.paper.inventory.recipe.ItemOrExact.Exact(final ItemStack exact) ->
@@ -414,8 +412,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - Improve exact choice recipe ingredients
      }
  
-     public boolean equals(Object object) {
-@@ -0,0 +0,0 @@ public final class Ingredient implements StackedContents.IngredientInfo<Holder<I
+     @Override
+@@ -120,6 +125,11 @@ public final class Ingredient implements StackedContents.IngredientInfo<Holder<I
      }
  
      public SlotDisplay display() {
@@ -424,22 +422,25 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            return new SlotDisplay.Composite(this.itemStacks().stream().<SlotDisplay>map(SlotDisplay.ItemStackSlotDisplay::new).toList());
 +        }
 +        // Paper end - show exact ingredients in recipe book
-         return (SlotDisplay) this.values.unwrap().map(SlotDisplay.TagSlotDisplay::new, (list) -> {
-             return new SlotDisplay.Composite(list.stream().map(Ingredient::displayForSingleItem).toList());
-         });
-diff --git a/src/main/java/net/minecraft/world/item/crafting/ShapelessRecipe.java b/src/main/java/net/minecraft/world/item/crafting/ShapelessRecipe.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/item/crafting/ShapelessRecipe.java
-+++ b/src/main/java/net/minecraft/world/item/crafting/ShapelessRecipe.java
-@@ -0,0 +0,0 @@ public class ShapelessRecipe implements CraftingRecipe {
-     }
+         return (SlotDisplay)this.values
+             .unwrap()
+             .map(SlotDisplay.TagSlotDisplay::new, list -> new SlotDisplay.Composite(list.stream().map(Ingredient::displayForSingleItem).toList()));
+diff --git a/net/minecraft/world/item/crafting/ShapelessRecipe.java b/net/minecraft/world/item/crafting/ShapelessRecipe.java
+index fb317eafeed39adff793bffa8f6b21c37a32086c..d601b54b1de2f2ae44fe2b20c8116c71a6340e45 100644
+--- a/net/minecraft/world/item/crafting/ShapelessRecipe.java
++++ b/net/minecraft/world/item/crafting/ShapelessRecipe.java
+@@ -72,13 +72,18 @@ public class ShapelessRecipe implements CraftingRecipe {
  
-     public boolean matches(CraftingInput input, Level world) {
--        return input.ingredientCount() != this.ingredients.size() ? false : (input.size() == 1 && this.ingredients.size() == 1 ? ((Ingredient) this.ingredients.getFirst()).test(input.getItem(0)) : input.stackedContents().canCraft((Recipe) this, (StackedContents.Output) null));
+     @Override
+     public boolean matches(CraftingInput input, Level level) {
 +        // Paper start - Improve exact choice recipe ingredients & unwrap ternary
-+        if (input.ingredientCount() != this.ingredients.size()) {
-+            return false;
-+        }
+         if (input.ingredientCount() != this.ingredients.size()) {
+             return false;
+-        } else {
+-            return input.size() == 1 && this.ingredients.size() == 1
+-                ? this.ingredients.getFirst().test(input.getItem(0))
+-                : input.stackedContents().canCraft(this, null);
+         }
 +        if (input.size() == 1 && this.ingredients.size() == 1) {
 +            return this.ingredients.getFirst().test(input.getItem(0));
 +        }
@@ -450,4 +451,4 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - Improve exact choice recipe ingredients & unwrap ternary
      }
  
-     public ItemStack assemble(CraftingInput input, HolderLookup.Provider registries) {
+     @Override
diff --git a/feature-patches/1060-Only-write-chunk-data-to-disk-if-it-serializes-witho.patch b/paper-server/patches/features/0022-Only-write-chunk-data-to-disk-if-it-serializes-witho.patch
similarity index 54%
rename from feature-patches/1060-Only-write-chunk-data-to-disk-if-it-serializes-witho.patch
rename to paper-server/patches/features/0022-Only-write-chunk-data-to-disk-if-it-serializes-witho.patch
index d336b76564..454bf1d95d 100644
--- a/feature-patches/1060-Only-write-chunk-data-to-disk-if-it-serializes-witho.patch
+++ b/paper-server/patches/features/0022-Only-write-chunk-data-to-disk-if-it-serializes-witho.patch
@@ -7,22 +7,22 @@ Subject: [PATCH] Only write chunk data to disk if it serializes without
 This ensures at least a valid version of the chunk exists
 on disk, even if outdated
 
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
+diff --git a/net/minecraft/world/level/chunk/storage/RegionFile.java b/net/minecraft/world/level/chunk/storage/RegionFile.java
+index 7da388ffab162c282cad0f297bb7304f3c2abbaf..ff4fc280409f680f3879a495e37cf1925b1a38f1 100644
+--- a/net/minecraft/world/level/chunk/storage/RegionFile.java
++++ b/net/minecraft/world/level/chunk/storage/RegionFile.java
+@@ -24,6 +24,7 @@ import org.slf4j.Logger;
  
-     }
-     // Paper end
+ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemRegionFile { // Paper - rewrite chunk system
+     private static final Logger LOGGER = LogUtils.getLogger();
 +    public static final int MAX_CHUNK_SIZE = 500 * 1024 * 1024; // Paper - don't write garbage data to disk if writing serialization fails
-     private class ChunkBuffer extends ByteArrayOutputStream implements ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkBuffer { // Paper - rewrite chunk system
- 
-         private final ChunkPos pos;
-@@ -0,0 +0,0 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
-             super.write(RegionFile.this.version.getId());
-             this.pos = chunkcoordintpair;
+     private static final int SECTOR_BYTES = 4096;
+     @VisibleForTesting
+     protected static final int SECTOR_INTS = 1024;
+@@ -455,6 +456,24 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
+             this.pos = pos;
          }
+ 
 +        // Paper start - don't write garbage data to disk if writing serialization fails
 +        @Override
 +        public void write(final int b) {
@@ -40,23 +40,23 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            super.write(b, off, len);
 +        }
 +        // Paper end - don't write garbage data to disk if writing serialization fails
- 
++
+         @Override
          public void close() throws IOException {
-             ByteBuffer bytebuffer = ByteBuffer.wrap(this.buf, 0, this.count);
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ import net.minecraft.world.level.ChunkPos;
+             ByteBuffer byteBuffer = ByteBuffer.wrap(this.buf, 0, this.count);
+diff --git a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+index e35bb5534e2fbd2e30154a15ff6d39baa121608f..d263f78fa610ce6f6fb5a0f5e064e3d8335c2199 100644
+--- a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
++++ b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+@@ -15,6 +15,7 @@ import net.minecraft.util.ExceptionCollector;
+ import net.minecraft.world.level.ChunkPos;
  
  public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.io.ChunkSystemRegionFileStorage { // Paper - rewrite chunk system
- 
 +    private static final org.slf4j.Logger LOGGER = com.mojang.logging.LogUtils.getLogger(); // Paper
-+
      public static final String ANVIL_EXTENSION = ".mca";
      private static final int MAX_CACHE_SIZE = 256;
-     public final Long2ObjectLinkedOpenHashMap<RegionFile> regionCache = new Long2ObjectLinkedOpenHashMap();
-@@ -0,0 +0,0 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
+     public final Long2ObjectLinkedOpenHashMap<RegionFile> regionCache = new Long2ObjectLinkedOpenHashMap<>();
+@@ -119,11 +120,24 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
          // (and, the regionfile parameter is unused for writing until the write call)
          final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData writeData = ((ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemRegionFile)regionFile).moonrise$startWrite(compound, pos);
  
@@ -81,39 +81,26 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
          return writeData;
      }
-@@ -0,0 +0,0 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
-             try {
-                 NbtIo.write(nbt, (DataOutput) dataoutputstream);
-                 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
+@@ -326,9 +340,17 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
+         if (chunkData == null) {
+             regionFile.clear(chunkPos);
+         } else {
+-            try (DataOutputStream chunkDataOutputStream = regionFile.getChunkDataOutputStream(chunkPos)) {
++            DataOutputStream chunkDataOutputStream = regionFile.getChunkDataOutputStream(chunkPos); // Paper - Only write if successful
++            try { // Paper - Only write if successful
+                 NbtIo.write(chunkData, chunkDataOutputStream);
+                 regionFile.setOversized(chunkPos.x, chunkPos.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
++                chunkDataOutputStream.close();
 +            } catch (final RegionFileSizeException ex) {
-+                regionfile.clear(pos);
++                regionFile.clear(chunkPos);
 +                final int maxSize = RegionFile.MAX_CHUNK_SIZE / (1024 * 1024);
-+                LOGGER.error("Chunk at (" + pos.x + "," + pos.z + ") in regionfile '" + regionfile.getPath().toString() + "' exceeds max size of " + maxSize + "MiB, it has been deleted from disk.");
-+                return;
++                LOGGER.error("Chunk at (" + chunkPos.x + "," + chunkPos.z + ") in regionfile '" + regionFile.getPath().toString() + "' exceeds max size of " + maxSize + "MiB, it has been deleted from disk.");
 +                // Paper end - don't write garbage data to disk if writing serialization fails
-             } catch (Throwable throwable) {
-                 if (dataoutputstream != null) {
-                     try {
--                        dataoutputstream.close();
-+                        //dataoutputstream.close(); // Paper - don't write garbage data to disk if writing serialization fails
-                     } catch (Throwable throwable1) {
-                         throwable.addSuppressed(throwable1);
-                     }
-@@ -0,0 +0,0 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
- 
-                 throw throwable;
              }
--
--            if (dataoutputstream != null) {
--                dataoutputstream.close();
--            }
-+            // Paper - don't write garbage data to disk if writing serialization fails; move into try block to only write if successfully serialized
          }
- 
      }
-@@ -0,0 +0,0 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
+@@ -370,4 +392,13 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
      public RegionStorageInfo info() {
          return this.info;
      }
@@ -121,7 +108,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    // Paper start - don't write garbage data to disk if writing serialization fails
 +    public static final class RegionFileSizeException extends RuntimeException {
 +
-+        public RegionFileSizeException(String message) {
++        public RegionFileSizeException(final String message) {
 +            super(message);
 +        }
 +    }
diff --git a/feature-patches/1063-Entity-load-save-limit-per-chunk.patch b/paper-server/patches/features/0023-Entity-load-save-limit-per-chunk.patch
similarity index 61%
rename from feature-patches/1063-Entity-load-save-limit-per-chunk.patch
rename to paper-server/patches/features/0023-Entity-load-save-limit-per-chunk.patch
index 78788ef3c8..7f3828e1a2 100644
--- a/feature-patches/1063-Entity-load-save-limit-per-chunk.patch
+++ b/paper-server/patches/features/0023-Entity-load-save-limit-per-chunk.patch
@@ -8,11 +8,11 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ public final class ChunkEntitySlices {
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java
+index 7aea4e343581b977d11af90f9f65eac3532eade1..d21ce54ebb5724c04eadf56a2cde701d5eeb5db2 100644
+--- a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java
+@@ -104,7 +104,18 @@ public final class ChunkEntitySlices {
          }
  
          final ListTag entitiesTag = new ListTag();
@@ -31,21 +31,21 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
              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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/entity/EntityType.java
-+++ b/src/main/java/net/minecraft/world/entity/EntityType.java
-@@ -0,0 +0,0 @@ public class EntityType<T extends Entity> implements FeatureElement, EntityTypeT
-         final Spliterator<? extends Tag> spliterator = entityNbtList.spliterator();
- 
+diff --git a/net/minecraft/world/entity/EntityType.java b/net/minecraft/world/entity/EntityType.java
+index 73cdfa5a315ed259b38dfa946a0b7955d9ac9f50..49201d6664656ebe34c84c1c84b5ea4878729062 100644
+--- a/net/minecraft/world/entity/EntityType.java
++++ b/net/minecraft/world/entity/EntityType.java
+@@ -1420,9 +1420,20 @@ public class EntityType<T extends Entity> implements FeatureElement, EntityTypeT
+     public static Stream<Entity> loadEntitiesRecursive(final List<? extends Tag> entityTags, final Level level, final EntitySpawnReason spawnReason) {
+         final Spliterator<? extends Tag> spliterator = entityTags.spliterator();
          return StreamSupport.stream(new Spliterator<Entity>() {
 +            final java.util.Map<EntityType<?>, Integer> loadedEntityCounts = new java.util.HashMap<>(); // Paper - Entity load/save limit per chunk
+             @Override
              public boolean tryAdvance(Consumer<? super Entity> consumer) {
-                 return spliterator.tryAdvance((nbtbase) -> {
-                     EntityType.loadEntityRecursive((CompoundTag) nbtbase, world, reason, (entity) -> {
+                 return spliterator.tryAdvance(tag -> EntityType.loadEntityRecursive((CompoundTag)tag, level, spawnReason, entity -> {
 +                        // Paper start - Entity load/save limit per chunk
 +                        final EntityType<?> entityType = entity.getType();
-+                        final int saveLimit = world.paperConfig().chunks.entityPerChunkSaveLimit.getOrDefault(entityType, -1);
++                        final int saveLimit = level.paperConfig().chunks.entityPerChunkSaveLimit.getOrDefault(entityType, -1);
 +                        if (saveLimit > -1) {
 +                            if (this.loadedEntityCounts.getOrDefault(entityType, 0) >= saveLimit) {
 +                                return null;
@@ -53,19 +53,19 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                            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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ public class EntityStorage implements EntityPersistentStorage<Entity> {
+                     consumer.accept(entity);
+                     return entity;
+                 }));
+diff --git a/net/minecraft/world/level/chunk/storage/EntityStorage.java b/net/minecraft/world/level/chunk/storage/EntityStorage.java
+index da05fb780c55381a7a08ced51d01764a645740b2..2856206eafddfcbcc1b65408deda40357f43a6f8 100644
+--- a/net/minecraft/world/level/chunk/storage/EntityStorage.java
++++ b/net/minecraft/world/level/chunk/storage/EntityStorage.java
+@@ -93,7 +93,18 @@ public class EntityStorage implements EntityPersistentStorage<Entity> {
              }
          } else {
              ListTag listTag = new ListTag();
 +            final java.util.Map<net.minecraft.world.entity.EntityType<?>, Integer> savedEntityCounts = new java.util.HashMap<>(); // Paper - Entity load/save limit per chunk
-             dataList.getEntities().forEach(entity -> {
+             entities.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);
@@ -76,6 +76,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                    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);
+                 CompoundTag compoundTag1 = new CompoundTag();
+                 if (entity.save(compoundTag1)) {
+                     listTag.add(compoundTag1);
diff --git a/feature-patches/1064-Attempt-to-recalculate-regionfile-header-if-it-is-co.patch b/paper-server/patches/features/0024-Attempt-to-recalculate-regionfile-header-if-it-is-co.patch
similarity index 70%
rename from feature-patches/1064-Attempt-to-recalculate-regionfile-header-if-it-is-co.patch
rename to paper-server/patches/features/0024-Attempt-to-recalculate-regionfile-header-if-it-is-co.patch
index 0aa11765a3..e35d6a31a9 100644
--- a/feature-patches/1064-Attempt-to-recalculate-regionfile-header-if-it-is-co.patch
+++ b/paper-server/patches/features/0024-Attempt-to-recalculate-regionfile-header-if-it-is-co.patch
@@ -9,11 +9,11 @@ we instead drop the current regionfile header and recalculate -
 hoping that at least then we don't swap chunks, and maybe recover
 them all.
 
-diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java
-@@ -0,0 +0,0 @@ import java.util.BitSet;
+diff --git a/net/minecraft/world/level/chunk/storage/RegionBitmap.java b/net/minecraft/world/level/chunk/storage/RegionBitmap.java
+index 64a718c98f799c62a5bb28e1e8e5f66cc96c915d..666f2e967c99f78422c83fb20e1a3bf3efa7845e 100644
+--- a/net/minecraft/world/level/chunk/storage/RegionBitmap.java
++++ b/net/minecraft/world/level/chunk/storage/RegionBitmap.java
+@@ -9,6 +9,27 @@ import java.util.BitSet;
  public class RegionBitmap {
      private final BitSet used = new BitSet();
  
@@ -38,17 +38,17 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - Attempt to recalculate regionfile header if it is corrupt
 +
-     public void force(int start, int size) {
-         this.used.set(start, start + size);
+     public void force(int sectorOffset, int sectorCount) {
+         this.used.set(sectorOffset, sectorOffset + sectorCount);
      }
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
-     private final IntBuffer timestamps;
+diff --git a/net/minecraft/world/level/chunk/storage/RegionFile.java b/net/minecraft/world/level/chunk/storage/RegionFile.java
+index ff4fc280409f680f3879a495e37cf1925b1a38f1..a4621c96fd456c5cdd1b6847931806e677b26b30 100644
+--- a/net/minecraft/world/level/chunk/storage/RegionFile.java
++++ b/net/minecraft/world/level/chunk/storage/RegionFile.java
+@@ -46,6 +46,355 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
      @VisibleForTesting
-     protected final RegionBitmap usedSectors;
+     protected final RegionBitmap usedSectors = new RegionBitmap();
+ 
 +    // Paper start - Attempt to recalculate regionfile header if it is corrupt
 +    private static long roundToSectors(long bytes) {
 +        long sectors = bytes >>> 12; // 4096 = 2^12
@@ -57,9 +57,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return sectors + (sign >>> 63);
 +    }
 +
-+    private static final CompoundTag OVERSIZED_COMPOUND = new CompoundTag();
++    private static final net.minecraft.nbt.CompoundTag OVERSIZED_COMPOUND = new net.minecraft.nbt.CompoundTag();
 +
-+    private CompoundTag attemptRead(long sector, int chunkDataLength, long fileLength) throws IOException {
++    private @Nullable net.minecraft.nbt.CompoundTag attemptRead(long sector, int chunkDataLength, long fileLength) throws IOException {
 +        try {
 +            if (chunkDataLength < 0) {
 +                return null;
@@ -91,7 +91,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +            InputStream input = compression.wrap(new ByteArrayInputStream(chunkData.array(), chunkData.position(), chunkDataLength - chunkData.position()));
 +
-+            return NbtIo.read(new DataInputStream(input));
++            return net.minecraft.nbt.NbtIo.read(new DataInputStream(input));
 +        } catch (Exception ex) {
 +            return null;
 +        }
@@ -142,7 +142,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            // try to backup file so maybe it could be sent to us for further investigation
 +
 +            this.backupRegionFile();
-+            CompoundTag[] compounds = new CompoundTag[32 * 32]; // only in the regionfile (i.e exclude mojang/aikar oversized data)
++            net.minecraft.nbt.CompoundTag[] compounds = new net.minecraft.nbt.CompoundTag[32 * 32]; // only in the regionfile (i.e exclude mojang/aikar oversized data)
 +            int[] rawLengths = new int[32 * 32]; // length of chunk data including 4 byte length field, bytes
 +            int[] sectorOffsets = new int[32 * 32]; // in sectors
 +            boolean[] hasAikarOversized = new boolean[32 * 32];
@@ -154,7 +154,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +            for (long i = 2, maxSector = Math.min((long)(Integer.MAX_VALUE >>> 8), totalSectors); i < maxSector; ++i) { // first two sectors are header, skip
 +                int chunkDataLength = this.getLength(i);
-+                CompoundTag compound = this.attemptRead(i, chunkDataLength, fileLength);
++                net.minecraft.nbt.CompoundTag compound = this.attemptRead(i, chunkDataLength, fileLength);
 +                if (compound == null || compound == OVERSIZED_COMPOUND) {
 +                    continue;
 +                }
@@ -166,7 +166,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                }
 +                int location = (chunkPos.x & 31) | ((chunkPos.z & 31) << 5);
 +
-+                CompoundTag otherCompound = compounds[location];
++                net.minecraft.nbt.CompoundTag otherCompound = compounds[location];
 +
 +                if (otherCompound != null && SerializableChunkData.getLastWorldSaveTime(otherCompound) > SerializableChunkData.getLastWorldSaveTime(compound)) {
 +                    continue; // don't overwrite newer data.
@@ -177,7 +177,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                boolean isAikarOversized = false;
 +                if (Files.exists(aikarOversizedFile)) {
 +                    try {
-+                        CompoundTag aikarOversizedCompound = this.getOversizedData(chunkPos.x, chunkPos.z);
++                        net.minecraft.nbt.CompoundTag aikarOversizedCompound = this.getOversizedData(chunkPos.x, chunkPos.z);
 +                        if (SerializableChunkData.getLastWorldSaveTime(compound) == SerializableChunkData.getLastWorldSaveTime(aikarOversizedCompound)) {
 +                            // best we got for an id. hope it's good enough
 +                            isAikarOversized = true;
@@ -236,14 +236,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                        continue;
 +                    }
 +
-+                    CompoundTag compound = null;
++                    net.minecraft.nbt.CompoundTag compound = null;
 +
 +                    // We do not know the compression type, as it's stored in the regionfile. So we need to try all of them
 +                    RegionFileVersion compression = null;
 +                    for (RegionFileVersion compressionType : RegionFileVersion.VERSIONS.values()) {
 +                        try {
 +                            DataInputStream in = new DataInputStream(compressionType.wrap(new ByteArrayInputStream(chunkData))); // typical java
-+                            compound = NbtIo.read((java.io.DataInput)in);
++                            compound = net.minecraft.nbt.NbtIo.read((java.io.DataInput)in);
 +                            compression = compressionType;
 +                            break; // reaches here iff readNBT does not throw
 +                        } catch (Exception ex) {
@@ -397,63 +397,55 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    final boolean canRecalcHeader; // final forces compile fail on new constructor
 +    // Paper end - Attempt to recalculate regionfile header if it is corrupt
- 
++
      // Paper start - rewrite chunk system
      @Override
-@@ -0,0 +0,0 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
-             throw new IllegalArgumentException("Expected directory, got " + String.valueOf(directory.toAbsolutePath()));
+     public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite(final net.minecraft.nbt.CompoundTag data, final ChunkPos pos) throws IOException {
+@@ -74,6 +423,7 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
+             throw new IllegalArgumentException("Expected directory, got " + externalFileDir.toAbsolutePath());
          } else {
-             this.externalFileDir = directory;
+             this.externalFileDir = externalFileDir;
 +            this.canRecalcHeader = RegionFileStorage.isChunkDataFolder(this.externalFileDir); // Paper - add can recalc flag
              this.offsets = this.header.asIntBuffer();
-             ((java.nio.Buffer) this.offsets).limit(1024); // CraftBukkit - decompile error
-             ((java.nio.Buffer) this.header).position(4096); // CraftBukkit - decompile error
-@@ -0,0 +0,0 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
-                     RegionFile.LOGGER.warn("Region file {} has truncated header: {}", path, i);
-                 }
+             this.offsets.limit(1024);
+             this.header.position(4096);
+@@ -94,11 +444,13 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
  
--                long j = Files.size(path);
-+                final long j = Files.size(path); final long regionFileSize = j; // Paper - recalculate header on header corruption
+                 long size = Files.size(path);
  
--                for (int k = 0; k < 1024; ++k) {
--                    int l = this.offsets.get(k);
+-                for (int i1 = 0; i1 < 1024; i1++) {
 +                boolean needsHeaderRecalc = false; // Paper - recalculate header on header corruption
 +                boolean hasBackedUp = false; // Paper - recalculate header on header corruption
-+                for (int k = 0; k < 1024; ++k) { final int headerLocation = k; // Paper - we expect this to be the header location
-+                    final int l = this.offsets.get(k);
- 
-                     if (l != 0) {
--                        int i1 = RegionFile.getSectorNumber(l);
--                        int j1 = RegionFile.getNumSectors(l);
-+                        final int i1 = RegionFile.getSectorNumber(l); final int offset = i1; // Paper - we expect this to be offset in file in sectors
-+                        int j1 = RegionFile.getNumSectors(l); final int sectorLength; // Paper - diff on change, we expect this to be sector length of region - watch out for reassignments
++                for (int i1 = 0; i1 < 1024; i1++) { final int headerLocation = i1; // Paper - we expect this to be the header location
+                     int i2 = this.offsets.get(i1);
+                     if (i2 != 0) {
+-                        int sectorNumber = getSectorNumber(i2);
+-                        int numSectors = getNumSectors(i2);
++                        final int sectorNumber = getSectorNumber(i2); // Paper - we expect this to be offset in file in sectors
++                        int numSectors = getNumSectors(i2); // Paper - diff on change, we expect this to be sector length of region - watch out for reassignments
                          // Spigot start
-                         if (j1 == 255) {
+                         if (numSectors == 255) {
                              // We're maxed out, so we need to read the proper length from the section
-@@ -0,0 +0,0 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
-                             j1 = (realLen.getInt(0) + 4) / 4096 + 1;
-                         }
+@@ -109,18 +461,62 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
                          // Spigot end
-+                        sectorLength = j1; // Paper - diff on change, we expect this to be sector length of region
- 
-                         if (i1 < 2) {
-                             RegionFile.LOGGER.warn("Region file {} has invalid sector at index: {}; sector {} overlaps with header", new Object[]{path, k, i1});
--                            this.offsets.put(k, 0);
-+                            //this.offsets.put(k, 0); // Paper - we catch this, but need it in the header for the summary change
-                         } else if (j1 == 0) {
-                             RegionFile.LOGGER.warn("Region file {} has an invalid sector at index: {}; size has to be > 0", path, k);
--                            this.offsets.put(k, 0);
-+                            //this.offsets.put(k, 0); // Paper - we catch this, but need it in the header for the summary change
-                         } else if ((long) i1 * 4096L > j) {
-                             RegionFile.LOGGER.warn("Region file {} has an invalid sector at index: {}; sector {} is out of bounds", new Object[]{path, k, i1});
--                            this.offsets.put(k, 0);
-+                            //this.offsets.put(k, 0); // Paper - we catch this, but need it in the header for the summary change
+                         if (sectorNumber < 2) {
+                             LOGGER.warn("Region file {} has invalid sector at index: {}; sector {} overlaps with header", path, i1, sectorNumber);
+-                            this.offsets.put(i1, 0);
++                            //this.offsets.put(i1, 0); // Paper - we catch this, but need it in the header for the summary change
+                         } else if (numSectors == 0) {
+                             LOGGER.warn("Region file {} has an invalid sector at index: {}; size has to be > 0", path, i1);
+-                            this.offsets.put(i1, 0);
++                            //this.offsets.put(i1, 0); // Paper - we catch this, but need it in the header for the summary change
+                         } else if (sectorNumber * 4096L > size) {
+                             LOGGER.warn("Region file {} has an invalid sector at index: {}; sector {} is out of bounds", path, i1, sectorNumber);
+-                            this.offsets.put(i1, 0);
++                            //this.offsets.put(i1, 0); // Paper - we catch this, but need it in the header for the summary change
                          } else {
--                            this.usedSectors.force(i1, j1);
-+                            //this.usedSectors.force(i1, j1); // Paper - move this down so we can check if it fails to allocate
+-                            this.usedSectors.force(sectorNumber, numSectors);
++                            //this.usedSectors.force(sectorNumber, numSectors); // Paper - move this down so we can check if it fails to allocate
 +                        }
 +                        // Paper start - recalculate header on header corruption
-+                        if (offset < 2 || sectorLength <= 0 || ((long)offset * 4096L) > regionFileSize) {
++                        if (sectorNumber < 2 || numSectors <= 0 || ((long)sectorNumber * 4096L) > size) {
 +                            if (canRecalcHeader) {
 +                                LOGGER.error("Detected invalid header for regionfile " + this.path.toAbsolutePath() + "! Recalculating header...");
 +                                needsHeaderRecalc = true;
@@ -471,7 +463,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                                continue;
 +                            }
 +                        }
-+                        boolean failedToAllocate = !this.usedSectors.tryAllocate(offset, sectorLength);
++                        boolean failedToAllocate = !this.usedSectors.tryAllocate(sectorNumber, numSectors);
 +                        if (failedToAllocate) {
 +                            LOGGER.error("Overlapping allocation by local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") in regionfile " + this.path.toAbsolutePath());
                          }
@@ -499,20 +491,19 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                }
 +                // Paper end
              }
- 
          }
-@@ -0,0 +0,0 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
+     }
+@@ -130,10 +526,35 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
      }
  
      private Path getExternalChunkPath(ChunkPos chunkPos) {
--        String s = "c." + chunkPos.x + "." + chunkPos.z + ".mcc";
-+        String s = "c." + chunkPos.x + "." + chunkPos.z + ".mcc"; // Paper - diff on change
- 
-         return this.externalFileDir.resolve(s);
+-        String string = "c." + chunkPos.x + "." + chunkPos.z + ".mcc";
++        String string = "c." + chunkPos.x + "." + chunkPos.z + ".mcc"; // Paper - diff on change
+         return this.externalFileDir.resolve(string);
      }
  
 +    // Paper start
-+    private static ChunkPos getOversizedChunkPair(Path file) {
++    private static @Nullable ChunkPos getOversizedChunkPair(Path file) {
 +        String fileName = file.getFileName().toString();
 +
 +        if (!fileName.startsWith("c.") || !fileName.endsWith(".mcc")) {
@@ -537,81 +528,79 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    // Paper end
 +
      @Nullable
-     public synchronized DataInputStream getChunkDataInputStream(ChunkPos pos) throws IOException {
-         int i = this.getOffset(pos);
-@@ -0,0 +0,0 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
-             ((java.nio.Buffer) bytebuffer).flip(); // CraftBukkit - decompile error
-             if (bytebuffer.remaining() < 5) {
-                 RegionFile.LOGGER.error("Chunk {} header is truncated: expected {} but read {}", new Object[]{pos, l, bytebuffer.remaining()});
+     public synchronized DataInputStream getChunkDataInputStream(ChunkPos chunkPos) throws IOException {
+         int offset = this.getOffset(chunkPos);
+@@ -155,30 +576,67 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
+             byteBuffer.flip();
+             if (byteBuffer.remaining() < 5) {
+                 LOGGER.error("Chunk {} header is truncated: expected {} but read {}", chunkPos, i, byteBuffer.remaining());
 +                // Paper start - recalculate header on regionfile corruption
 +                if (this.canRecalcHeader && this.recalculateHeader()) {
-+                    return this.getChunkDataInputStream(pos);
++                    return this.getChunkDataInputStream(chunkPos);
 +                }
 +                // Paper end - recalculate header on regionfile corruption
                  return null;
              } else {
-                 int i1 = bytebuffer.getInt();
-@@ -0,0 +0,0 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
- 
-                 if (i1 == 0) {
-                     RegionFile.LOGGER.warn("Chunk {} is allocated, but stream is missing", pos);
+                 int _int = byteBuffer.getInt();
+                 byte b = byteBuffer.get();
+                 if (_int == 0) {
+                     LOGGER.warn("Chunk {} is allocated, but stream is missing", chunkPos);
 +                    // Paper start - recalculate header on regionfile corruption
 +                    if (this.canRecalcHeader && this.recalculateHeader()) {
-+                        return this.getChunkDataInputStream(pos);
++                        return this.getChunkDataInputStream(chunkPos);
 +                    }
 +                    // Paper end - recalculate header on regionfile corruption
                      return null;
                  } else {
-                     int j1 = i1 - 1;
-@@ -0,0 +0,0 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
-                     if (RegionFile.isExternalStreamChunk(b0)) {
-                         if (j1 != 0) {
-                             RegionFile.LOGGER.warn("Chunk has both internal and external streams");
+                     int i1 = _int - 1;
+                     if (isExternalStreamChunk(b)) {
+                         if (i1 != 0) {
+                             LOGGER.warn("Chunk has both internal and external streams");
 +                            // Paper start - recalculate header on regionfile corruption
 +                            if (this.canRecalcHeader && this.recalculateHeader()) {
-+                                return this.getChunkDataInputStream(pos);
++                                return this.getChunkDataInputStream(chunkPos);
 +                            }
 +                            // Paper end - recalculate header on regionfile corruption
                          }
  
--                        return this.createExternalChunkInputStream(pos, RegionFile.getExternalChunkVersion(b0));
+-                        return this.createExternalChunkInputStream(chunkPos, getExternalChunkVersion(b));
 +                        // Paper start - recalculate header on regionfile corruption
-+                        final DataInputStream ret = this.createExternalChunkInputStream(pos, RegionFile.getExternalChunkVersion(b0));
++                        final DataInputStream ret = this.createExternalChunkInputStream(chunkPos, getExternalChunkVersion(b));
 +                        if (ret == null && this.canRecalcHeader && this.recalculateHeader()) {
-+                            return this.getChunkDataInputStream(pos);
++                            return this.getChunkDataInputStream(chunkPos);
 +                        }
 +                        return ret;
 +                        // Paper end - recalculate header on regionfile corruption
-                     } else if (j1 > bytebuffer.remaining()) {
-                         RegionFile.LOGGER.error("Chunk {} stream is truncated: expected {} but read {}", new Object[]{pos, j1, bytebuffer.remaining()});
+                     } else if (i1 > byteBuffer.remaining()) {
+                         LOGGER.error("Chunk {} stream is truncated: expected {} but read {}", chunkPos, i1, byteBuffer.remaining());
 +                        // Paper start - recalculate header on regionfile corruption
 +                        if (this.canRecalcHeader && this.recalculateHeader()) {
-+                            return this.getChunkDataInputStream(pos);
++                            return this.getChunkDataInputStream(chunkPos);
 +                        }
 +                        // Paper end - recalculate header on regionfile corruption
                          return null;
-                     } else if (j1 < 0) {
-                         RegionFile.LOGGER.error("Declared size {} of chunk {} is negative", i1, pos);
+                     } else if (i1 < 0) {
+                         LOGGER.error("Declared size {} of chunk {} is negative", _int, chunkPos);
 +                        // Paper start - recalculate header on regionfile corruption
 +                        if (this.canRecalcHeader && this.recalculateHeader()) {
-+                            return this.getChunkDataInputStream(pos);
++                            return this.getChunkDataInputStream(chunkPos);
 +                        }
 +                        // Paper end - recalculate header on regionfile corruption
                          return null;
                      } else {
-                         JvmProfiler.INSTANCE.onRegionFileRead(this.info, pos, this.version, j1);
--                        return this.createChunkInputStream(pos, b0, RegionFile.createStream(bytebuffer, j1));
+                         JvmProfiler.INSTANCE.onRegionFileRead(this.info, chunkPos, this.version, i1);
+-                        return this.createChunkInputStream(chunkPos, b, createStream(byteBuffer, i1));
 +                        // Paper start - recalculate header on regionfile corruption
-+                        final DataInputStream ret = this.createChunkInputStream(pos, b0, RegionFile.createStream(bytebuffer, j1));
++                        final DataInputStream ret = this.createChunkInputStream(chunkPos, b, createStream(byteBuffer, i1));
 +                        if (ret == null && this.canRecalcHeader && this.recalculateHeader()) {
-+                            return this.getChunkDataInputStream(pos);
++                            return this.getChunkDataInputStream(chunkPos);
 +                        }
 +                        return ret;
 +                        // Paper end - recalculate header on regionfile corruption
                      }
                  }
              }
-@@ -0,0 +0,0 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
+@@ -361,9 +819,14 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
      }
  
      private ByteBuffer createExternalStub() {
@@ -620,22 +609,21 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    private ByteBuffer createExternalStub(RegionFileVersion version) {
 +        // Paper end - add version param
-         ByteBuffer bytebuffer = ByteBuffer.allocate(5);
+         ByteBuffer byteBuffer = ByteBuffer.allocate(5);
+         byteBuffer.putInt(1);
+-        byteBuffer.put((byte)(this.version.getId() | 128));
++        byteBuffer.put((byte)(version.getId() | 128));
+         byteBuffer.flip();
+         return byteBuffer;
+     }
+diff --git a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+index d263f78fa610ce6f6fb5a0f5e064e3d8335c2199..dad7f94b611cf0fc68b1a3878c458233f6bb6d61 100644
+--- a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
++++ b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+@@ -23,6 +23,36 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
+     private final Path folder;
+     private final boolean sync;
  
-         bytebuffer.putInt(1);
--        bytebuffer.put((byte) (this.version.getId() | 128));
-+        bytebuffer.put((byte) (version.getId() | 128)); // Paper - replace with version param
-         ((java.nio.Buffer) bytebuffer).flip(); // CraftBukkit - decompile error
-         return bytebuffer;
-     }
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
-         }
-     }
-     // Paper end - rewrite chunk system
 +    // Paper start - recalculate region file headers
 +    private final boolean isChunkData;
 +
@@ -666,40 +654,42 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +    // Paper end
- 
-     protected RegionFileStorage(RegionStorageInfo storageKey, Path directory, boolean dsync) { // Paper - protected
-         this.folder = directory;
-         this.sync = dsync;
-         this.info = storageKey;
+     // Paper start - rewrite chunk system
+     private static final int REGION_SHIFT = 5;
+     private static final int MAX_NON_EXISTING_CACHE = 1024 * 4;
+@@ -216,6 +246,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
+         this.folder = folder;
+         this.sync = sync;
+         this.info = info;
 +        this.isChunkData = isChunkDataFolder(this.folder); // Paper - recalculate region file headers
      }
  
-     // Paper start - rewrite chunk system
-@@ -0,0 +0,0 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
-             try {
-                 if (datainputstream != null) {
-                     nbttagcompound = NbtIo.read((DataInput) datainputstream);
-+                    // Paper start - recover from corrupt regionfile header
-+                    if (this.isChunkData) {
-+                        ChunkPos chunkPos = SerializableChunkData.getChunkCoordinate(nbttagcompound);
-+                        if (!chunkPos.equals(pos)) {
-+                            net.minecraft.server.MinecraftServer.LOGGER.error("Attempting to read chunk data at " + pos + " but got chunk data for " + chunkPos + " instead! Attempting regionfile recalculation for regionfile " + regionfile.getPath().toAbsolutePath());
-+                            if (regionfile.recalculateHeader()) {
-+                                return this.read(pos);
-+                            }
-+                            net.minecraft.server.MinecraftServer.LOGGER.error("Can't recalculate regionfile header, regenerating chunk " + pos + " for " + regionfile.getPath().toAbsolutePath());
-+                            return null;
-+                        }
-+                    }
-+                    // Paper end - recover from corrupt regionfile header
-                     break label43;
-                 }
+     @org.jetbrains.annotations.Contract("_, false -> !null") @Nullable private RegionFile getRegionFile(ChunkPos chunkPos, boolean existingOnly) throws IOException { // CraftBukkit
+@@ -309,6 +340,19 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
+             }
  
-diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java
-@@ -0,0 +0,0 @@ import org.slf4j.Logger;
+             var4 = NbtIo.read(chunkDataInputStream);
++            // Paper start - recover from corrupt regionfile header
++            if (this.isChunkData) {
++                ChunkPos headerChunkPos = SerializableChunkData.getChunkCoordinate(var4);
++                if (!headerChunkPos.equals(chunkPos)) {
++                    net.minecraft.server.MinecraftServer.LOGGER.error("Attempting to read chunk data at " + chunkPos + " but got chunk data for " + headerChunkPos + " instead! Attempting regionfile recalculation for regionfile " + regionFile.getPath().toAbsolutePath());
++                    if (regionFile.recalculateHeader()) {
++                        return this.read(chunkPos);
++                    }
++                    net.minecraft.server.MinecraftServer.LOGGER.error("Can't recalculate regionfile header, regenerating chunk " + chunkPos + " for " + regionFile.getPath().toAbsolutePath());
++                    return null;
++                }
++            }
++            // Paper end - recover from corrupt regionfile header
+         }
+ 
+         return var4;
+diff --git a/net/minecraft/world/level/chunk/storage/RegionFileVersion.java b/net/minecraft/world/level/chunk/storage/RegionFileVersion.java
+index 0c739ca5b01ac0ec35a11fd01c5fc65de97c2852..de7deee4b79c969a7797bd57b657a16404c15303 100644
+--- a/net/minecraft/world/level/chunk/storage/RegionFileVersion.java
++++ b/net/minecraft/world/level/chunk/storage/RegionFileVersion.java
+@@ -21,7 +21,7 @@ import org.slf4j.Logger;
  
  public class RegionFileVersion {
      private static final Logger LOGGER = LogUtils.getLogger();
@@ -708,11 +698,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      private static final Object2ObjectMap<String, RegionFileVersion> VERSIONS_BY_NAME = new Object2ObjectOpenHashMap<>();
      public static final RegionFileVersion VERSION_GZIP = register(
          new RegionFileVersion(
-diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java b/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
-@@ -0,0 +0,0 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
+diff --git a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
+index 70a9972252576e039ac126f6057a6ed66b80cdfc..d783c3580ea274a0a9cb07449eb8037bc5a04d76 100644
+--- a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
++++ b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
+@@ -120,6 +120,18 @@ public record SerializableChunkData(
          }
      }
      // Paper end - guard against serializing mismatching coordinates
@@ -731,12 +721,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      // Paper start - Do not let the server load chunks from newer versions
      private static final int CURRENT_DATA_VERSION = net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion();
-@@ -0,0 +0,0 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
-         nbttagcompound.putInt("xPos", this.chunkPos.x);
-         nbttagcompound.putInt("yPos", this.minSectionY);
-         nbttagcompound.putInt("zPos", this.chunkPos.z);
--        nbttagcompound.putLong("LastUpdate", this.lastUpdateTime);
-+        nbttagcompound.putLong("LastUpdate", this.lastUpdateTime); // Paper - Diff on change
-         nbttagcompound.putLong("InhabitedTime", this.inhabitedTime);
-         nbttagcompound.putString("Status", BuiltInRegistries.CHUNK_STATUS.getKey(this.chunkStatus).toString());
-         DataResult<Tag> dataresult; // CraftBukkit - decompile error
+@@ -604,7 +616,7 @@ public record SerializableChunkData(
+         compoundTag.putInt("xPos", this.chunkPos.x);
+         compoundTag.putInt("yPos", this.minSectionY);
+         compoundTag.putInt("zPos", this.chunkPos.z);
+-        compoundTag.putLong("LastUpdate", this.lastUpdateTime);
++        compoundTag.putLong("LastUpdate", this.lastUpdateTime); // Paper - Diff on change
+         compoundTag.putLong("InhabitedTime", this.inhabitedTime);
+         compoundTag.putString("Status", BuiltInRegistries.CHUNK_STATUS.getKey(this.chunkStatus).toString());
+         if (this.blendingData != null) {
diff --git a/feature-patches/1066-Incremental-chunk-and-player-saving.patch b/paper-server/patches/features/0025-Incremental-chunk-and-player-saving.patch
similarity index 55%
rename from feature-patches/1066-Incremental-chunk-and-player-saving.patch
rename to paper-server/patches/features/0025-Incremental-chunk-and-player-saving.patch
index 9e117f351b..4ac088fbda 100644
--- a/feature-patches/1066-Incremental-chunk-and-player-saving.patch
+++ b/paper-server/patches/features/0025-Incremental-chunk-and-player-saving.patch
@@ -4,23 +4,23 @@ Date: Sun, 9 Jun 2019 03:53:22 +0100
 Subject: [PATCH] Incremental chunk and player saving
 
 
-diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/MinecraftServer.java
-+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
- 
+diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
+index d967d605c2e4227ae980c30f1c8b86edbc680d6d..6dbae12bbfd47cd4e75bc3089561e8e226e9e604 100644
+--- a/net/minecraft/server/MinecraftServer.java
++++ b/net/minecraft/server/MinecraftServer.java
+@@ -960,7 +960,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+         boolean var4;
          try {
              this.isSaving = true;
 -            this.getPlayerList().saveAll();
 +            this.getPlayerList().saveAll(); // Paper - Incremental chunk and player saving; diff on change
-             flag3 = this.saveAllChunks(suppressLogs, flush, force);
+             var4 = this.saveAllChunks(suppressLog, flush, forced);
          } finally {
              this.isSaving = false;
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+@@ -1533,9 +1533,29 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
          }
  
-         --this.ticksUntilAutosave;
+         this.ticksUntilAutosave--;
 -        if (this.autosavePeriod > 0 && this.ticksUntilAutosave <= 0) { // CraftBukkit
 -            this.autoSave();
 +        // Paper start - Incremental chunk and player saving
@@ -47,22 +47,20 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        profiler.pop();
 +        // Paper end - Incremental chunk and player saving
  
-         ProfilerFiller gameprofilerfiller = Profiler.get();
- 
-diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerLevel.java
-+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+         ProfilerFiller profilerFiller = Profiler.get();
+         this.runAllTasks(); // Paper - move runAllTasks() into full server tick (previously for timings)
+diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java
+index 46dfaed12c998c219a20c711a06531aed2c68012..ebeeb63c3dca505a3ce8b88feaa5d2ca20ec24a2 100644
+--- a/net/minecraft/server/level/ServerLevel.java
++++ b/net/minecraft/server/level/ServerLevel.java
+@@ -1316,6 +1316,28 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
          return !this.server.isUnderSpawnProtection(this, pos, player) && this.getWorldBorder().isWithinBounds(pos);
      }
  
 +    // Paper start - Incremental chunk and player saving
 +    public void saveIncrementally(boolean doFull) {
-+        ServerChunkCache chunkproviderserver = this.getChunkSource();
-+
 +        if (doFull) {
-+            org.bukkit.Bukkit.getPluginManager().callEvent(new org.bukkit.event.world.WorldSaveEvent(getWorld()));
++            org.bukkit.Bukkit.getPluginManager().callEvent(new org.bukkit.event.world.WorldSaveEvent(this.getWorld()));
 +        }
 +
 +        if (doFull) {
@@ -72,43 +70,43 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Copied from save()
 +        // CraftBukkit start - moved from MinecraftServer.saveChunks
 +        if (doFull) { // Paper
-+            ServerLevel worldserver1 = this;
-+            this.serverLevelData.setWorldBorder(worldserver1.getWorldBorder().createSettings());
++            ServerLevel serverLevel1 = this;
++            this.serverLevelData.setWorldBorder(serverLevel1.getWorldBorder().createSettings());
 +            this.serverLevelData.setCustomBossEvents(this.server.getCustomBossEvents().save(this.registryAccess()));
-+            this.convertable.saveDataTag(this.server.registryAccess(), this.serverLevelData, this.server.getPlayerList().getSingleplayerData());
++            this.levelStorageAccess.saveDataTag(this.server.registryAccess(), this.serverLevelData, this.server.getPlayerList().getSingleplayerData());
 +        }
 +        // CraftBukkit end
 +    }
 +    // Paper end - Incremental chunk and player saving
 +
-     public void save(@Nullable ProgressListener progressListener, boolean flush, boolean savingDisabled) {
+     public void save(@Nullable ProgressListener progress, boolean flush, boolean skipSave) {
          // Paper start - add close param
-         this.save(progressListener, flush, savingDisabled, false);
-diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
-+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
-@@ -0,0 +0,0 @@ import org.bukkit.inventory.MainHand;
- public class ServerPlayer extends net.minecraft.world.entity.player.Player implements ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer { // Paper - rewrite chunk system
+         this.save(progress, flush, skipSave, false);
+diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java
+index e61fe83479f095e8addbd3e8f1d5179c998ae1eb..0a7e5106a1d39150326e7c323030df5d32ecef1e 100644
+--- a/net/minecraft/server/level/ServerPlayer.java
++++ b/net/minecraft/server/level/ServerPlayer.java
+@@ -180,6 +180,7 @@ import org.slf4j.Logger;
  
+ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer { // Paper - rewrite chunk system
      private static final Logger LOGGER = LogUtils.getLogger();
 +    public long lastSave = MinecraftServer.currentTick; // Paper - Incremental chunk and player saving
      private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_XZ = 32;
      private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_Y = 10;
      private static final int FLY_STAT_RECORDING_SPEED = 25;
-diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/players/PlayerList.java
-+++ b/src/main/java/net/minecraft/server/players/PlayerList.java
-@@ -0,0 +0,0 @@ public abstract class PlayerList {
+diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java
+index 5e94dd9e26aa4fd6545dbaae2ae0cb51cb6f13e0..03feaf0adb8ee87e33744a4615dc2507a02f92d7 100644
+--- a/net/minecraft/server/players/PlayerList.java
++++ b/net/minecraft/server/players/PlayerList.java
+@@ -482,6 +482,7 @@ public abstract class PlayerList {
  
      protected void save(ServerPlayer player) {
          if (!player.getBukkitEntity().isPersistent()) return; // CraftBukkit
 +        player.lastSave = MinecraftServer.currentTick; // Paper - Incremental chunk and player saving
          this.playerIo.save(player);
-         ServerStatsCounter serverstatisticmanager = (ServerStatsCounter) player.getStats(); // CraftBukkit
- 
-@@ -0,0 +0,0 @@ public abstract class PlayerList {
+         ServerStatsCounter serverStatsCounter = player.getStats(); // CraftBukkit
+         if (serverStatsCounter != null) {
+@@ -1064,9 +1065,23 @@ public abstract class PlayerList {
      }
  
      public void saveAll() {
@@ -116,18 +114,20 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        this.saveAll(-1);
 +    }
 +
-+    public void saveAll(int interval) {
++    public void saveAll(final int interval) {
          io.papermc.paper.util.MCUtil.ensureMain("Save Players" , () -> { // Paper - Ensure main
 +        int numSaved = 0;
-+        long now = MinecraftServer.currentTick;
-         for (int i = 0; i < this.players.size(); ++i) {
--            this.save((ServerPlayer) this.players.get(i));
++        final long now = MinecraftServer.currentTick;
+         for (int i = 0; i < this.players.size(); i++) {
+-            this.save(this.players.get(i));
 +            final ServerPlayer player = this.players.get(i);
 +            if (interval == -1 || now - player.lastSave >= interval) {
 +                this.save(player);
-+                if (interval != -1 && ++numSaved >= io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.maxPerTick()) { break; }
++                if (interval != -1 && ++numSaved >= io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.maxPerTick()) {
++                    break;
++                }
 +            }
 +            // Paper end - Incremental chunk and player saving
          }
- 
          return null; }); // Paper - ensure main
+     }
diff --git a/feature-patches/1067-Optimise-general-POI-access.patch b/paper-server/patches/features/0026-Optimise-general-POI-access.patch
similarity index 84%
rename from feature-patches/1067-Optimise-general-POI-access.patch
rename to paper-server/patches/features/0026-Optimise-general-POI-access.patch
index 92dfac7dd4..7387de347a 100644
--- a/feature-patches/1067-Optimise-general-POI-access.patch
+++ b/paper-server/patches/features/0026-Optimise-general-POI-access.patch
@@ -30,12 +30,12 @@ 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
+diff --git a/io/papermc/paper/util/PoiAccess.java b/io/papermc/paper/util/PoiAccess.java
 new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+index 0000000000000000000000000000000000000000..f39294b1f83c4022be5ced4da781103a1eee2daf
 --- /dev/null
-+++ b/src/main/java/io/papermc/paper/util/PoiAccess.java
-@@ -0,0 +0,0 @@
++++ b/io/papermc/paper/util/PoiAccess.java
+@@ -0,0 +1,806 @@
 +package io.papermc.paper.util;
 +
 +import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
@@ -842,38 +842,38 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ public class AcquirePoi {
-                                             return true;
-                                         }
-                                     };
--                                    Set<Pair<Holder<PoiType>, BlockPos>> set = poiManager.findAllClosestFirstWithType(
--                                            poiPredicate, predicate2, entity.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE
--                                        )
--                                        .limit(5L)
--                                        .filter(pairx -> worldPosBiPredicate.test(world, (BlockPos)pairx.getSecond()))
--                                        .collect(Collectors.toSet());
-+                                    // Paper start - optimise POI access
-+                                    final java.util.List<Pair<Holder<PoiType>, 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);
-+                                    final Set<Pair<Holder<PoiType>, BlockPos>> set = new java.util.HashSet<>(poiposes.size());
-+                                    for (final Pair<Holder<PoiType>, BlockPos> poiPose : poiposes) {
-+                                        if (worldPosBiPredicate.test(world, poiPose.getSecond())) {
-+                                            set.add(poiPose);
-+                                        }
-+                                    }
-+                                    // 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ public class NearestBedSensor extends Sensor<Mob> {
+diff --git a/net/minecraft/world/entity/ai/behavior/AcquirePoi.java b/net/minecraft/world/entity/ai/behavior/AcquirePoi.java
+index 9de13a78b2a8be181c02ab330bfa9abb936a83db..b9174ae7e3a3e2de2d570b95ab5012ac3c3a2eda 100644
+--- a/net/minecraft/world/entity/ai/behavior/AcquirePoi.java
++++ b/net/minecraft/world/entity/ai/behavior/AcquirePoi.java
+@@ -84,12 +84,16 @@ public class AcquirePoi {
+                                     return true;
+                                 }
+                             };
+-                            Set<Pair<Holder<PoiType>, BlockPos>> set = poiManager.findAllClosestFirstWithType(
+-                                    acquirablePois, predicate1, mob.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE
+-                                )
+-                                .limit(5L)
+-                                .filter(pair1 -> predicate.test(level, pair1.getSecond()))
+-                                .collect(Collectors.toSet());
++                            // Paper start - optimise POI access
++                            final java.util.List<Pair<Holder<PoiType>, BlockPos>> poiposes = new java.util.ArrayList<>();
++                            io.papermc.paper.util.PoiAccess.findNearestPoiPositions(poiManager, acquirablePois, predicate1, mob.blockPosition(), 48, 48*48, PoiManager.Occupancy.HAS_SPACE, false, 5, poiposes);
++                            final Set<Pair<Holder<PoiType>, BlockPos>> set = new java.util.HashSet<>(poiposes.size());
++                            for (final Pair<Holder<PoiType>, BlockPos> poiPose : poiposes) {
++                                if (predicate.test(level, poiPose.getSecond())) {
++                                    set.add(poiPose);
++                                }
++                            }
++                            // Paper end - optimise POI access
+                             Path path = findPathToPois(mob, set);
+                             if (path != null && path.canReach()) {
+                                 BlockPos target = path.getTarget();
+diff --git a/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java b/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java
+index 6e9325f0800a35637fdec5edb8a514ea03741762..066faa704338c573472381e1ebd063e0d52aaaa4 100644
+--- a/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java
++++ b/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java
+@@ -53,11 +53,12 @@ public class NearestBedSensor extends Sensor<Mob> {
                      return true;
                  }
              };
@@ -889,82 +889,82 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            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<Holder<PoiType>> 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> im
+                 BlockPos target = path.getTarget();
+                 Optional<Holder<PoiType>> type = poiManager.getType(target);
+diff --git a/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/net/minecraft/world/entity/ai/village/poi/PoiManager.java
+index 5c5724f5e3ad640f55aecbc1d8f71d1f59ecdc62..618fc0eb4fe70e46e55f3aa28e8eac1d2d01b6d9 100644
+--- a/net/minecraft/world/entity/ai/village/poi/PoiManager.java
++++ b/net/minecraft/world/entity/ai/village/poi/PoiManager.java
+@@ -254,36 +254,47 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> im
      public Optional<BlockPos> find(
-         Predicate<Holder<PoiType>> typePredicate, Predicate<BlockPos> posPredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus
+         Predicate<Holder<PoiType>> typePredicate, Predicate<BlockPos> posPredicate, BlockPos pos, int distance, PoiManager.Occupancy status
      ) {
--        return this.findAll(typePredicate, posPredicate, pos, radius, occupationStatus).findFirst();
+-        return this.findAll(typePredicate, posPredicate, pos, distance, status).findFirst();
 +        // Paper start - re-route to faster logic
-+        BlockPos ret = io.papermc.paper.util.PoiAccess.findAnyPoiPosition(this, typePredicate, posPredicate, pos, radius, occupationStatus, false);
++        BlockPos ret = io.papermc.paper.util.PoiAccess.findAnyPoiPosition(this, typePredicate, posPredicate, pos, distance, status, false);
 +        return Optional.ofNullable(ret);
 +        // Paper end
      }
  
-     public Optional<BlockPos> findClosest(Predicate<Holder<PoiType>> typePredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus) {
--        return this.getInRange(typePredicate, pos, radius, occupationStatus)
--            .map(PoiRecord::getPos)
--            .min(Comparator.comparingDouble(poiPos -> poiPos.distSqr(pos)));
+     public Optional<BlockPos> findClosest(Predicate<Holder<PoiType>> typePredicate, BlockPos pos, int distance, PoiManager.Occupancy status) {
+-        return this.getInRange(typePredicate, pos, distance, status).map(PoiRecord::getPos).min(Comparator.comparingDouble(blockPos -> blockPos.distSqr(pos)));
 +        // Paper start - re-route to faster logic
-+        BlockPos closestPos = io.papermc.paper.util.PoiAccess.findClosestPoiDataPosition(this, typePredicate, null, pos, radius, radius * radius, occupationStatus, false);
++        BlockPos closestPos = io.papermc.paper.util.PoiAccess.findClosestPoiDataPosition(this, typePredicate, null, pos, distance, distance * distance, status, false);
 +        return Optional.ofNullable(closestPos);
 +        // Paper end - re-route to faster logic
      }
  
      public Optional<Pair<Holder<PoiType>, BlockPos>> findClosestWithType(
-         Predicate<Holder<PoiType>> typePredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus
+         Predicate<Holder<PoiType>> typePredicate, BlockPos pos, int distance, PoiManager.Occupancy status
      ) {
--        return this.getInRange(typePredicate, pos, radius, occupationStatus)
--            .min(Comparator.comparingDouble(poi -> poi.getPos().distSqr(pos)))
--            .map(poi -> Pair.of(poi.getPoiType(), poi.getPos()));
+-        return this.getInRange(typePredicate, pos, distance, status)
+-            .min(Comparator.comparingDouble(poiRecord -> poiRecord.getPos().distSqr(pos)))
+-            .map(poiRecord -> Pair.of(poiRecord.getPoiType(), poiRecord.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
++            this, typePredicate, null, pos, distance, distance * distance, status, false
 +        ));
 +        // Paper end - re-route to faster logic
      }
  
      public Optional<BlockPos> findClosest(
-         Predicate<Holder<PoiType>> typePredicate, Predicate<BlockPos> posPredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus
+         Predicate<Holder<PoiType>> typePredicate, Predicate<BlockPos> posPredicate, BlockPos pos, int distance, PoiManager.Occupancy status
      ) {
--        return this.getInRange(typePredicate, pos, radius, occupationStatus)
+-        return this.getInRange(typePredicate, pos, distance, status)
 -            .map(PoiRecord::getPos)
 -            .filter(posPredicate)
--            .min(Comparator.comparingDouble(poiPos -> poiPos.distSqr(pos)));
+-            .min(Comparator.comparingDouble(blockPos -> blockPos.distSqr(pos)));
 +        // Paper start - re-route to faster logic
-+        BlockPos closestPos = io.papermc.paper.util.PoiAccess.findClosestPoiDataPosition(this, typePredicate, posPredicate, pos, radius, radius * radius, occupationStatus, false);
++        BlockPos closestPos = io.papermc.paper.util.PoiAccess.findClosestPoiDataPosition(this, typePredicate, posPredicate, pos, distance, distance * distance, status, false);
 +        return Optional.ofNullable(closestPos);
 +        // Paper end - re-route to faster logic
      }
  
-     public Optional<BlockPos> take(Predicate<Holder<PoiType>> typePredicate, BiPredicate<Holder<PoiType>, BlockPos> posPredicate, BlockPos pos, int radius) {
--        return this.getInRange(typePredicate, pos, radius, PoiManager.Occupancy.HAS_SPACE)
--            .filter(poi -> posPredicate.test(poi.getPoiType(), poi.getPos()))
+     public Optional<BlockPos> take(
+         Predicate<Holder<PoiType>> typePredicate, BiPredicate<Holder<PoiType>, BlockPos> combinedTypePosPredicate, BlockPos pos, int distance
+     ) {
+-        return this.getInRange(typePredicate, pos, distance, PoiManager.Occupancy.HAS_SPACE)
+-            .filter(poiRecord -> combinedTypePosPredicate.test(poiRecord.getPoiType(), poiRecord.getPos()))
 -            .findFirst()
 +        // Paper start - re-route to faster logic
 +        final @javax.annotation.Nullable PoiRecord closest = io.papermc.paper.util.PoiAccess.findClosestPoiDataRecord(
-+            this, typePredicate, posPredicate, pos, radius, radius * radius, Occupancy.HAS_SPACE, false
++            this, typePredicate, combinedTypePosPredicate, pos, distance, distance * distance, Occupancy.HAS_SPACE, false
 +        );
 +        return Optional.ofNullable(closest)
 +            // Paper end - re-route to faster logic
-             .map(poi -> {
-                 poi.acquireTicket();
-                 return poi.getPos();
-@@ -0,0 +0,0 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> im
-         int radius,
+             .map(poiRecord -> {
+                 poiRecord.acquireTicket();
+                 return poiRecord.getPos();
+@@ -298,8 +309,21 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> im
+         int distance,
          RandomSource random
      ) {
--        List<PoiRecord> list = Util.toShuffledList(this.getInRange(typePredicate, pos, radius, occupationStatus), random);
--        return list.stream().filter(poi -> positionPredicate.test(poi.getPos())).findFirst().map(PoiRecord::getPos);
+-        List<PoiRecord> list = Util.toShuffledList(this.getInRange(typePredicate, pos, distance, status), random);
+-        return list.stream().filter(poiRecord -> posPredicate.test(poiRecord.getPos())).findFirst().map(PoiRecord::getPos);
 +        // Paper start - re-route to faster logic
 +        List<PoiRecord> list = new java.util.ArrayList<>();
 +        io.papermc.paper.util.PoiAccess.findAnyPoiRecords(
-+            this, typePredicate, positionPredicate, pos, radius, occupationStatus, false, Integer.MAX_VALUE, list
++            this, typePredicate, posPredicate, pos, distance, status, false, Integer.MAX_VALUE, list
 +        );
 +
 +        // the old method shuffled the list and then tried to find the first element in it that
@@ -979,11 +979,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ import org.slf4j.Logger;
+diff --git a/net/minecraft/world/entity/ai/village/poi/PoiSection.java b/net/minecraft/world/entity/ai/village/poi/PoiSection.java
+index 39cd1e3d8192d7077d6b7864d33933097cc6b986..b92ba4d194fd3af94c7af5d8e150fc4297c73ab8 100644
+--- a/net/minecraft/world/entity/ai/village/poi/PoiSection.java
++++ b/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<PoiRecord> records = new Short2ObjectOpenHashMap<>();
@@ -992,48 +992,43 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
-@@ -0,0 +0,0 @@ public class SectionStorage<R, P> implements AutoCloseable, ca.spottedleaf.moonr
+diff --git a/net/minecraft/world/level/chunk/storage/SectionStorage.java b/net/minecraft/world/level/chunk/storage/SectionStorage.java
+index 778bd73a938c94ecb85ca0f8b686ff4e1baee040..79d4ce7712f16995b0de3be86477fb43ab3961d7 100644
+--- a/net/minecraft/world/level/chunk/storage/SectionStorage.java
++++ b/net/minecraft/world/level/chunk/storage/SectionStorage.java
+@@ -131,11 +131,11 @@ public class SectionStorage<R, P> implements AutoCloseable, ca.spottedleaf.moonr
      }
  
      @Nullable
--    protected Optional<R> get(long pos) {
-+    public Optional<R> get(long pos) { // Paper - public
-         return this.storage.get(pos);
+-    protected Optional<R> get(long sectionKey) {
++    public Optional<R> get(long sectionKey) { // Paper - public
+         return this.storage.get(sectionKey);
      }
  
--    protected Optional<R> getOrLoad(long pos) {
-+    public Optional<R> getOrLoad(long pos) { // Paper - public
-         if (this.outsideStoredRange(pos)) {
+-    protected Optional<R> getOrLoad(long sectionKey) {
++    public Optional<R> getOrLoad(long sectionKey) { // Paper - public
+         if (this.outsideStoredRange(sectionKey)) {
              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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java
-+++ b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java
-@@ -0,0 +0,0 @@ public class PortalForcer {
-         // int i = flag ? 16 : 128;
+diff --git a/net/minecraft/world/level/portal/PortalForcer.java b/net/minecraft/world/level/portal/PortalForcer.java
+index ada2da62d3a40d67e64f5f8d7299f78b5c6f53cb..90ae71eb8cc7f925eb212f39731d70f3bff5ef0a 100644
+--- a/net/minecraft/world/level/portal/PortalForcer.java
++++ b/net/minecraft/world/level/portal/PortalForcer.java
+@@ -48,13 +48,38 @@ public class PortalForcer {
+         PoiManager poiManager = this.level.getPoiManager();
+         // int i = isNether ? 16 : 128;
          // CraftBukkit end
- 
--        villageplace.ensureLoadedAndValid(this.level, blockposition, i);
--        Stream<BlockPos> 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));
+-        poiManager.ensureLoadedAndValid(this.level, exitPos, i);
+-        return poiManager.getInSquare(holder -> holder.is(PoiTypes.NETHER_PORTAL), exitPos, i, PoiManager.Occupancy.ANY)
+-            .map(PoiRecord::getPos)
+-            .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))) // Paper - Configurable nether ceiling damage
+-            .filter(blockPos -> this.level.getBlockState(blockPos).hasProperty(BlockStateProperties.HORIZONTAL_AXIS))
+-            .min(Comparator.<BlockPos>comparingDouble(blockPos -> blockPos.distSqr(exitPos)).thenComparingInt(Vec3i::getY));
 +        // Paper start - optimise portals
-+        Optional<PoiRecord> optional;
 +        java.util.List<PoiRecord> records = new java.util.ArrayList<>();
 +        io.papermc.paper.util.PoiAccess.findClosestPoiDataRecords(
-+            villageplace,
++            poiManager,
 +            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);
@@ -1042,12 +1037,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                    // 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
++                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
++            exitPos, i, Double.MAX_VALUE, PoiManager.Occupancy.ANY, true, records
 +        );
 +
 +        // this gets us most of the way there, but we bias towards lower y values.
diff --git a/paper-server/patches/features/0027-Optional-per-player-mob-spawns.patch b/paper-server/patches/features/0027-Optional-per-player-mob-spawns.patch
new file mode 100644
index 0000000000..500d8debd1
--- /dev/null
+++ b/paper-server/patches/features/0027-Optional-per-player-mob-spawns.patch
@@ -0,0 +1,236 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: kickash32 <kickash32@gmail.com>
+Date: Mon, 19 Aug 2019 01:27:58 +0500
+Subject: [PATCH] Optional per player mob spawns
+
+
+diff --git a/net/minecraft/server/level/ChunkMap.java b/net/minecraft/server/level/ChunkMap.java
+index ff6503bf8eb88d1264c3d848a89d0255b4b3ae68..9eed24939fc09f00a9dbce1be2ab9c34d024fd29 100644
+--- a/net/minecraft/server/level/ChunkMap.java
++++ b/net/minecraft/server/level/ChunkMap.java
+@@ -236,11 +236,29 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+         // Paper - rewrite chunk system
+     }
+ 
+-    // Paper start
+-    public int getMobCountNear(final ServerPlayer player, final net.minecraft.world.entity.MobCategory mobCategory) {
+-        return -1;
++    // Paper start - Optional per player mob spawns
++    public void updatePlayerMobTypeMap(final Entity entity) {
++        if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) {
++            return;
++        }
++
++        final int index = entity.getType().getCategory().ordinal();
++        final ca.spottedleaf.moonrise.common.list.ReferenceList<ServerPlayer> inRange =
++            this.level.moonrise$getNearbyPlayers().getPlayers(entity.chunkPosition(), ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.TICK_VIEW_DISTANCE);
++        if (inRange == null) {
++            return;
++        }
++
++        final ServerPlayer[] backingSet = inRange.getRawDataUnchecked();
++        for (int i = 0, len = inRange.size(); i < len; i++) {
++            ++(backingSet[i].mobCounts[index]);
++        }
+     }
+-    // Paper end
++
++    public int getMobCountNear(final ServerPlayer player, final net.minecraft.world.entity.MobCategory mobCategory) {
++        return player.mobCounts[mobCategory.ordinal()];
++     }
++    // Paper end - Optional per player mob spawns
+ 
+     protected ChunkGenerator generator() {
+         return this.worldGenContext.generator();
+diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java
+index 87d4291a3944f706a694536da6de0f28c548ab8d..5576bf1d1d70ab7a010653d3207909b5de867e70 100644
+--- a/net/minecraft/server/level/ServerChunkCache.java
++++ b/net/minecraft/server/level/ServerChunkCache.java
+@@ -517,7 +517,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
+                     profilerFiller.popPush("shuffleChunks");
+                     // Paper start - chunk tick iteration optimisation
+                     this.shuffleRandom.setSeed(this.level.random.nextLong());
+-                    Util.shuffle(list, this.shuffleRandom);
++                    if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) Util.shuffle(list, this.shuffleRandom); // Paper - Optional per player mob spawns; do not need this when per-player is enabled
+                     // Paper end - chunk tick iteration optimisation
+                     this.tickChunks(profilerFiller, l, list);
+                     profilerFiller.pop();
+@@ -571,9 +571,18 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
+     private void tickChunks(ProfilerFiller profiler, long timeInhabited, List<LevelChunk> chunks) {
+         profiler.popPush("naturalSpawnCount");
+         int naturalSpawnChunkCount = this.distanceManager.getNaturalSpawnChunkCount();
+-        NaturalSpawner.SpawnState spawnState = NaturalSpawner.createState(
+-            naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, new LocalMobCapCalculator(this.chunkMap)
+-        );
++        // Paper start - Optional per player mob spawns
++        NaturalSpawner.SpawnState spawnState;
++        if ((this.spawnFriendlies || this.spawnEnemies) && this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { // don't count mobs when animals and monsters are disabled
++            // re-set mob counts
++            for (ServerPlayer player : this.level.players) {
++                Arrays.fill(player.mobCounts, 0);
++            }
++            spawnState = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, null, true);
++        } else {
++            spawnState = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false);
++        }
++        // Paper end - Optional per player mob spawns
+         this.lastSpawnState = spawnState;
+         profiler.popPush("spawnAndTick");
+         boolean _boolean = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.players().isEmpty(); // CraftBukkit
+diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java
+index 0a7e5106a1d39150326e7c323030df5d32ecef1e..a63702dd7e86fc8b9f78c2ae23e23b65b6b2ee24 100644
+--- a/net/minecraft/server/level/ServerPlayer.java
++++ b/net/minecraft/server/level/ServerPlayer.java
+@@ -368,6 +368,10 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc
+     public boolean queueHealthUpdatePacket;
+     public net.minecraft.network.protocol.game.ClientboundSetHealthPacket queuedHealthUpdatePacket;
+     // Paper end - cancellable death event
++    // Paper start - Optional per player mob spawns
++    public static final int MOBCATEGORY_TOTAL_ENUMS = net.minecraft.world.entity.MobCategory.values().length;
++    public final int[] mobCounts = new int[MOBCATEGORY_TOTAL_ENUMS];
++    // Paper end - Optional per player mob spawns
+     // CraftBukkit start
+     public org.bukkit.craftbukkit.entity.CraftPlayer.TransferCookieConnection transferCookieConnection;
+     public String displayName;
+diff --git a/net/minecraft/world/level/NaturalSpawner.java b/net/minecraft/world/level/NaturalSpawner.java
+index 913ea92ace9d610c25bf28f703a3b227044aea63..ef8bacbbb43a9b80281a313ca43b7efff5a93e03 100644
+--- a/net/minecraft/world/level/NaturalSpawner.java
++++ b/net/minecraft/world/level/NaturalSpawner.java
+@@ -72,6 +72,14 @@ public final class NaturalSpawner {
+     public static NaturalSpawner.SpawnState createState(
+         int spawnableChunkCount, Iterable<Entity> entities, NaturalSpawner.ChunkGetter chunkGetter, LocalMobCapCalculator calculator
+     ) {
++        // Paper start - Optional per player mob spawns
++        return createState(spawnableChunkCount, entities, chunkGetter, calculator, false);
++    }
++
++    public static NaturalSpawner.SpawnState createState(
++        int spawnableChunkCount, Iterable<Entity> entities, NaturalSpawner.ChunkGetter chunkGetter, LocalMobCapCalculator calculator, final boolean countMobs
++    ) {
++        // Paper end - Optional per player mob spawns
+         PotentialCalculator potentialCalculator = new PotentialCalculator();
+         Object2IntOpenHashMap<MobCategory> map = new Object2IntOpenHashMap<>();
+ 
+@@ -93,11 +101,16 @@ public final class NaturalSpawner {
+                             potentialCalculator.addCharge(entity.blockPosition(), mobSpawnCost.charge());
+                         }
+ 
+-                        if (entity instanceof Mob) {
++                        if (calculator != null && entity instanceof Mob) { // Paper - Optional per player mob spawns
+                             calculator.addMob(chunk.getPos(), category);
+                         }
+ 
+                         map.addTo(category, 1);
++                        // Paper start - Optional per player mob spawns
++                        if (countMobs) {
++                            chunk.level.getChunkSource().chunkMap.updatePlayerMobTypeMap(entity);
++                        }
++                        // Paper end - Optional per player mob spawns
+                     });
+                 }
+             }
+@@ -135,7 +148,7 @@ public final class NaturalSpawner {
+             if ((spawnFriendlies || !mobCategory.isFriendly())
+                 && (spawnEnemies || mobCategory.isFriendly())
+                 && (spawnPassives || !mobCategory.isPersistent())
+-                && spawnState.canSpawnForCategoryGlobal(mobCategory, limit)) { // Paper - Optional per player mob spawns; remove global check, check later during the local one
++                && (level.paperConfig().entities.spawning.perPlayerMobSpawns || spawnState.canSpawnForCategoryGlobal(mobCategory, limit))) { // Paper - Optional per player mob spawns; remove global check, check later during the local one
+                 list.add(mobCategory);
+                 // CraftBukkit end
+             }
+@@ -149,8 +162,37 @@ public final class NaturalSpawner {
+         profilerFiller.push("spawner");
+ 
+         for (MobCategory mobCategory : categories) {
+-            if (spawnState.canSpawnForCategoryLocal(mobCategory, chunk.getPos())) {
+-                spawnCategoryForChunk(mobCategory, level, chunk, spawnState::canSpawn, spawnState::afterSpawn);
++            // Paper start - Optional per player mob spawns
++            final boolean canSpawn;
++            int maxSpawns = Integer.MAX_VALUE;
++            if (level.paperConfig().entities.spawning.perPlayerMobSpawns) {
++                // Copied from getFilteredSpawningCategories
++                int limit = mobCategory.getMaxInstancesPerChunk();
++                SpawnCategory spawnCategory = CraftSpawnCategory.toBukkit(mobCategory);
++                if (CraftSpawnCategory.isValidForLimits(spawnCategory)) {
++                    limit = level.getWorld().getSpawnLimit(spawnCategory);
++                }
++
++                // Apply per-player limit
++                int minDiff = Integer.MAX_VALUE;
++                final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerPlayer> inRange =
++                    level.moonrise$getNearbyPlayers().getPlayers(chunk.getPos(), ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.TICK_VIEW_DISTANCE);
++                if (inRange != null) {
++                    final net.minecraft.server.level.ServerPlayer[] backingSet = inRange.getRawDataUnchecked();
++                    for (int k = 0, len = inRange.size(); k < len; k++) {
++                        minDiff = Math.min(limit - level.getChunkSource().chunkMap.getMobCountNear(backingSet[k], mobCategory), minDiff);
++                    }
++                }
++
++                maxSpawns = (minDiff == Integer.MAX_VALUE) ? 0 : minDiff;
++                canSpawn = maxSpawns > 0;
++            } else {
++                canSpawn = spawnState.canSpawnForCategoryLocal(mobCategory, chunk.getPos());
++            }
++            if (canSpawn) {
++                spawnCategoryForChunk(mobCategory, level, chunk, spawnState::canSpawn, spawnState::afterSpawn,
++                    maxSpawns, level.paperConfig().entities.spawning.perPlayerMobSpawns ? level.getChunkSource().chunkMap::updatePlayerMobTypeMap : null);
++                // Paper end - Optional per player mob spawns
+             }
+         }
+ 
+@@ -170,9 +212,16 @@ public final class NaturalSpawner {
+     public static void spawnCategoryForChunk(
+         MobCategory category, ServerLevel level, LevelChunk chunk, NaturalSpawner.SpawnPredicate filter, NaturalSpawner.AfterSpawnCallback callback
+     ) {
++        // Paper start - Optional per player mob spawns
++        spawnCategoryForChunk(category, level, chunk, filter, callback, Integer.MAX_VALUE, null);
++    }
++    public static void spawnCategoryForChunk(
++        MobCategory category, ServerLevel level, LevelChunk chunk, NaturalSpawner.SpawnPredicate filter, NaturalSpawner.AfterSpawnCallback callback, final int maxSpawns, final Consumer<Entity> trackEntity
++    ) {
++        // Paper end - Optional per player mob spawns
+         BlockPos randomPosWithin = getRandomPosWithin(level, chunk);
+         if (randomPosWithin.getY() >= level.getMinY() + 1) {
+-            spawnCategoryForPosition(category, level, chunk, randomPosWithin, filter, callback);
++            spawnCategoryForPosition(category, level, chunk, randomPosWithin, filter, callback, maxSpawns, trackEntity); // Paper - Optional per player mob spawns
+         }
+     }
+ 
+@@ -189,6 +238,12 @@ public final class NaturalSpawner {
+         NaturalSpawner.SpawnPredicate filter,
+         NaturalSpawner.AfterSpawnCallback callback
+     ) {
++        spawnCategoryForPosition(category, level, chunk, pos, filter, callback, Integer.MAX_VALUE, null);
++    }
++    public static void spawnCategoryForPosition(
++        MobCategory category, ServerLevel level, ChunkAccess chunk, BlockPos pos, NaturalSpawner.SpawnPredicate filter, NaturalSpawner.AfterSpawnCallback callback, final int maxSpawns, final @Nullable Consumer<Entity> trackEntity
++    ) {
++        // Paper end - Optional per player mob spawns
+         StructureManager structureManager = level.structureManager();
+         ChunkGenerator generator = level.getChunkSource().getGenerator();
+         int y = pos.getY();
+@@ -252,9 +307,14 @@ public final class NaturalSpawner {
+                                         ++i;
+                                         ++i3;
+                                         callback.run(mobForSpawn, chunk);
++                                        // Paper start - Optional per player mob spawns
++                                        if (trackEntity != null) {
++                                            trackEntity.accept(mobForSpawn);
++                                        }
++                                        // Paper end - Optional per player mob spawns
+                                     }
+                                     // CraftBukkit end
+-                                    if (i >= mobForSpawn.getMaxSpawnClusterSize()) {
++                                    if (i >= mobForSpawn.getMaxSpawnClusterSize() || i >= maxSpawns) { // Paper - Optional per player mob spawns
+                                         return;
+                                     }
+ 
+@@ -565,7 +625,7 @@ public final class NaturalSpawner {
+             this.spawnPotential.addCharge(blockPos, d);
+             MobCategory category = type.getCategory();
+             this.mobCategoryCounts.addTo(category, 1);
+-            this.localMobCapCalculator.addMob(new ChunkPos(blockPos), category);
++            if (this.localMobCapCalculator != null) this.localMobCapCalculator.addMob(new ChunkPos(blockPos), category); // Paper - Optional per player mob spawns
+         }
+ 
+         public int getSpawnableChunkCount() {
diff --git a/paper-server/patches/features/0028-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch b/paper-server/patches/features/0028-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch
new file mode 100644
index 0000000000..2da69ced08
--- /dev/null
+++ b/paper-server/patches/features/0028-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch
@@ -0,0 +1,89 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: kickash32 <kickash32@gmail.com>
+Date: Mon, 5 Apr 2021 01:42:35 -0400
+Subject: [PATCH] Improve cancelling PreCreatureSpawnEvent with per player mob
+ spawns
+
+
+diff --git a/net/minecraft/server/level/ChunkMap.java b/net/minecraft/server/level/ChunkMap.java
+index 9eed24939fc09f00a9dbce1be2ab9c34d024fd29..b3f498558614243cf633dcd71e3c49c2c55e6e0f 100644
+--- a/net/minecraft/server/level/ChunkMap.java
++++ b/net/minecraft/server/level/ChunkMap.java
+@@ -255,8 +255,25 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+         }
+     }
+ 
++    // Paper start - per player mob count backoff
++    public void updateFailurePlayerMobTypeMap(int chunkX, int chunkZ, net.minecraft.world.entity.MobCategory mobCategory) {
++        if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) {
++            return;
++        }
++        int idx = mobCategory.ordinal();
++        final ca.spottedleaf.moonrise.common.list.ReferenceList<ServerPlayer> inRange =
++            this.level.moonrise$getNearbyPlayers().getPlayersByChunk(chunkX, chunkZ, ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.TICK_VIEW_DISTANCE);
++        if (inRange == null) {
++            return;
++        }
++        final ServerPlayer[] backingSet = inRange.getRawDataUnchecked();
++        for (int i = 0, len = inRange.size(); i < len; i++) {
++            ++(backingSet[i].mobBackoffCounts[idx]);
++        }
++    }
++    // Paper end - per player mob count backoff
+     public int getMobCountNear(final ServerPlayer player, final net.minecraft.world.entity.MobCategory mobCategory) {
+-        return player.mobCounts[mobCategory.ordinal()];
++        return player.mobCounts[mobCategory.ordinal()] + player.mobBackoffCounts[mobCategory.ordinal()]; // Paper - per player mob count backoff
+      }
+     // Paper end - Optional per player mob spawns
+ 
+diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java
+index 5576bf1d1d70ab7a010653d3207909b5de867e70..6540b2d6a1062d883811ce240c49d30d1925b291 100644
+--- a/net/minecraft/server/level/ServerChunkCache.java
++++ b/net/minecraft/server/level/ServerChunkCache.java
+@@ -576,7 +576,17 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
+         if ((this.spawnFriendlies || this.spawnEnemies) && this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { // don't count mobs when animals and monsters are disabled
+             // re-set mob counts
+             for (ServerPlayer player : this.level.players) {
+-                Arrays.fill(player.mobCounts, 0);
++                // Paper start - per player mob spawning backoff
++                for (int ii = 0; ii < ServerPlayer.MOBCATEGORY_TOTAL_ENUMS; ii++) {
++                    player.mobCounts[ii] = 0;
++
++                    int newBackoff = player.mobBackoffCounts[ii] - 1; // TODO make configurable bleed // TODO use nonlinear algorithm?
++                    if (newBackoff < 0) {
++                        newBackoff = 0;
++                    }
++                    player.mobBackoffCounts[ii] = newBackoff;
++                }
++                // Paper end - per player mob spawning backoff
+             }
+             spawnState = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, null, true);
+         } else {
+diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java
+index a63702dd7e86fc8b9f78c2ae23e23b65b6b2ee24..6d75a641431c7deb7e8ddbf02cdc919015a3a7dc 100644
+--- a/net/minecraft/server/level/ServerPlayer.java
++++ b/net/minecraft/server/level/ServerPlayer.java
+@@ -372,6 +372,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc
+     public static final int MOBCATEGORY_TOTAL_ENUMS = net.minecraft.world.entity.MobCategory.values().length;
+     public final int[] mobCounts = new int[MOBCATEGORY_TOTAL_ENUMS];
+     // Paper end - Optional per player mob spawns
++    public final int[] mobBackoffCounts = new int[MOBCATEGORY_TOTAL_ENUMS]; // Paper - per player mob count backoff
+     // CraftBukkit start
+     public org.bukkit.craftbukkit.entity.CraftPlayer.TransferCookieConnection transferCookieConnection;
+     public String displayName;
+diff --git a/net/minecraft/world/level/NaturalSpawner.java b/net/minecraft/world/level/NaturalSpawner.java
+index ef8bacbbb43a9b80281a313ca43b7efff5a93e03..17ce115e887cbbb06ad02ab7ddb488e27342c0e4 100644
+--- a/net/minecraft/world/level/NaturalSpawner.java
++++ b/net/minecraft/world/level/NaturalSpawner.java
+@@ -285,6 +285,11 @@ public final class NaturalSpawner {
+ 
+                             // Paper start - PreCreatureSpawnEvent
+                             PreSpawnStatus doSpawning = isValidSpawnPostitionForType(level, category, structureManager, generator, spawnerData, mutableBlockPos, d2);
++                            // Paper start - per player mob count backoff
++                            if (doSpawning == PreSpawnStatus.ABORT || doSpawning == PreSpawnStatus.CANCELLED) {
++                                level.getChunkSource().chunkMap.updateFailurePlayerMobTypeMap(mutableBlockPos.getX() >> 4, mutableBlockPos.getZ() >> 4, category);
++                            }
++                            // Paper end - per player mob count backoff
+                             if (doSpawning == PreSpawnStatus.ABORT) {
+                                 return;
+                             }
diff --git a/paper-server/patches/features/0029-Optimize-Hoppers.patch b/paper-server/patches/features/0029-Optimize-Hoppers.patch
new file mode 100644
index 0000000000..c81544a817
--- /dev/null
+++ b/paper-server/patches/features/0029-Optimize-Hoppers.patch
@@ -0,0 +1,683 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <aikar@aikar.co>
+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/io/papermc/paper/event/inventory/PaperInventoryMoveItemEvent.java b/io/papermc/paper/event/inventory/PaperInventoryMoveItemEvent.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..24a2090e068ad3c0d08705050944abdfe19136a2
+--- /dev/null
++++ b/io/papermc/paper/event/inventory/PaperInventoryMoveItemEvent.java
+@@ -0,0 +1,29 @@
++package io.papermc.paper.event.inventory;
++
++import org.bukkit.event.inventory.InventoryMoveItemEvent;
++import org.bukkit.inventory.Inventory;
++import org.bukkit.inventory.ItemStack;
++import org.jspecify.annotations.NullMarked;
++
++@NullMarked
++public class PaperInventoryMoveItemEvent extends InventoryMoveItemEvent {
++
++    public boolean calledSetItem;
++    public boolean calledGetItem;
++
++    public PaperInventoryMoveItemEvent(final Inventory sourceInventory, final ItemStack itemStack, final 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/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
+index 6dbae12bbfd47cd4e75bc3089561e8e226e9e604..9c859025302ddb2c20cf6457fa4e4eaf7fbafdd7 100644
+--- a/net/minecraft/server/MinecraftServer.java
++++ b/net/minecraft/server/MinecraftServer.java
+@@ -1707,6 +1707,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+             serverLevel.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - BlockPhysicsEvent
+             serverLevel.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent
+             serverLevel.updateLagCompensationTick(); // Paper - lag compensation
++            net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = serverLevel.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper - Perf: Optimize Hoppers
+             profilerFiller.push(() -> serverLevel + " " + serverLevel.dimension().location());
+             /* Drop global time updates
+             if (this.tickCount % 20 == 0) {
+diff --git a/net/minecraft/world/item/ItemStack.java b/net/minecraft/world/item/ItemStack.java
+index c255e11cb0981bd7e0456d4fd401beb5257be597..d6361863d6a1e364de262d6199373cbd68d1c699 100644
+--- a/net/minecraft/world/item/ItemStack.java
++++ b/net/minecraft/world/item/ItemStack.java
+@@ -808,10 +808,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(final boolean originalItem) {
++        if (!originalItem && this.isEmpty()) {
++            // Paper end - Perf: Optimize Hoppers
+             return 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/net/minecraft/world/level/block/entity/BlockEntity.java b/net/minecraft/world/level/block/entity/BlockEntity.java
+index 2ebdf1ad323bb53dfe9eed319e25856b35a1443c..77618757c0e678532dbab814aceed83f7f1cd892 100644
+--- a/net/minecraft/world/level/block/entity/BlockEntity.java
++++ b/net/minecraft/world/level/block/entity/BlockEntity.java
+@@ -26,6 +26,7 @@ import net.minecraft.world.level.block.state.BlockState;
+ import org.slf4j.Logger;
+ 
+ public abstract class BlockEntity {
++    static boolean ignoreBlockEntityUpdates; // Paper - Perf: Optimize Hoppers
+     // CraftBukkit start - data containers
+     private static final org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry();
+     public org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer persistentDataContainer;
+@@ -196,6 +197,7 @@ public abstract class BlockEntity {
+ 
+     public void setChanged() {
+         if (this.level != null) {
++            if (ignoreBlockEntityUpdates) return; // Paper - Perf: Optimize Hoppers
+             setChanged(this.level, this.worldPosition, this.blockState);
+         }
+     }
+diff --git a/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/net/minecraft/world/level/block/entity/HopperBlockEntity.java
+index e58a32593e8b42bfc534d13457240860293dd3f4..5cd1326ad5d046c88b2b3449d610a78fa880b4cd 100644
+--- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java
+@@ -139,18 +139,56 @@ 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 hopper) {
++        hopper.unpackLootTable(null);
++
++        final List<ItemStack> hopperItems = hopper.items;
++
++        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 level, BlockPos pos, BlockState state, HopperBlockEntity blockEntity, BooleanSupplier validator) {
+         if (level.isClientSide) {
+             return false;
+         } else {
+             if (!blockEntity.isOnCooldown() && 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 = ejectItems(level, pos, blockEntity);
+                 }
+ 
+-                if (!blockEntity.inventoryFull()) {
+-                    flag |= validator.getAsBoolean();
++                if (fullState != HOPPER_IS_FULL || flag) { // Paper - Perf: Optimize Hoppers
++                    flag |= validator.getAsBoolean(); // Paper - note: this is not a validator, it's what adds/sucks in items
+                 }
+ 
+                 if (flag) {
+@@ -174,6 +212,206 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
+         return true;
+     }
+ 
++    // Paper start - Perf: Optimize Hoppers
++    public static boolean skipHopperEvents;
++    private static boolean skipPullModeEventFire;
++    private static boolean skipPushModeEventFire;
++
++    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 have 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);
++            }
++
++            ignoreBlockEntityUpdates = true;
++            container.setItem(i, origItemStack);
++            ignoreBlockEntityUpdates = false;
++            container.setChanged();
++            return true;
++        }
++        origItemStack.setCount(originalItemCount);
++
++        if (level.paperConfig().hopper.cooldownWhenFull) {
++            applyCooldown(hopper);
++        }
++
++        return false;
++    }
++
++    @Nullable
++    private static ItemStack callPushMoveEvent(Container destination, ItemStack itemStack, HopperBlockEntity hopper) {
++        final org.bukkit.inventory.Inventory destinationInventory = getInventory(destination);
++        final io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent event = new io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent(
++            hopper.getOwner(false).getInventory(),
++            org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack),
++            destinationInventory,
++            true
++        );
++        final boolean result = event.callEvent();
++        if (!event.calledGetItem && !event.calledSetItem) {
++            skipPushModeEventFire = true;
++        }
++        if (!result) {
++            applyCooldown(hopper);
++            return null;
++        }
++
++        if (event.calledSetItem) {
++            return org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem());
++        } else {
++            return itemStack;
++        }
++    }
++
++    @Nullable
++    private static ItemStack callPullMoveEvent(final Hopper hopper, final Container container, final ItemStack itemstack) {
++        final org.bukkit.inventory.Inventory sourceInventory = getInventory(container);
++        final org.bukkit.inventory.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, org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack), destination, false);
++        final boolean result = event.callEvent();
++        if (!event.calledGetItem && !event.calledSetItem) {
++            skipPullModeEventFire = true;
++        }
++        if (!result) {
++            applyCooldown(hopper);
++            return null;
++        }
++
++        if (event.calledSetItem) {
++            return org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem());
++        } else {
++            return itemstack;
++        }
++    }
++
++    private static org.bukkit.inventory.Inventory getInventory(final Container container) {
++        final org.bukkit.inventory.Inventory sourceInventory;
++        if (container instanceof net.minecraft.world.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 org.bukkit.craftbukkit.inventory.CraftInventory(container);
++        }
++        return sourceInventory;
++    }
++
++    private static void applyCooldown(final Hopper hopper) {
++        if (hopper instanceof HopperBlockEntity blockEntity && blockEntity.getLevel() != null) {
++            blockEntity.setCooldown(blockEntity.getLevel().spigotConfig.hopperTransfer);
++        }
++    }
++
++    private static boolean allMatch(Container container, Direction direction, java.util.function.BiPredicate<ItemStack, Integer> test) {
++        if (container instanceof WorldlyContainer) {
++            for (int slot : ((WorldlyContainer) container).getSlotsForFace(direction)) {
++                if (!test.test(container.getItem(slot), slot)) {
++                    return false;
++                }
++            }
++        } else {
++            int size = container.getContainerSize();
++            for (int slot = 0; slot < size; slot++) {
++                if (!test.test(container.getItem(slot), slot)) {
++                    return false;
++                }
++            }
++        }
++        return true;
++    }
++
++    private static boolean anyMatch(Container container, Direction direction, java.util.function.BiPredicate<ItemStack, Integer> test) {
++        if (container instanceof WorldlyContainer) {
++            for (int slot : ((WorldlyContainer) container).getSlotsForFace(direction)) {
++                if (test.test(container.getItem(slot), slot)) {
++                    return true;
++                }
++            }
++        } else {
++            int size = container.getContainerSize();
++            for (int slot = 0; slot < size; slot++) {
++                if (test.test(container.getItem(slot), slot)) {
++                    return true;
++                }
++            }
++        }
++        return true;
++    }
++    private static final java.util.function.BiPredicate<ItemStack, Integer> STACK_SIZE_TEST = (itemStack, i) -> itemStack.getCount() >= itemStack.getMaxStackSize();
++    private static final java.util.function.BiPredicate<ItemStack, Integer> IS_EMPTY_TEST = (itemStack, i) -> itemStack.isEmpty();
++    // Paper end - Perf: Optimize Hoppers
++
+     private static boolean ejectItems(Level level, BlockPos pos, HopperBlockEntity blockEntity) {
+         Container attachedContainer = getAttachedContainer(level, pos, blockEntity);
+         if (attachedContainer == null) {
+@@ -183,57 +421,60 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
+             if (isFullContainer(attachedContainer, opposite)) {
+                 return false;
+             } else {
+-                for (int i = 0; i < blockEntity.getContainerSize(); i++) {
+-                    ItemStack item = blockEntity.getItem(i);
+-                    if (!item.isEmpty()) {
+-                        int count = item.getCount();
+-                        // CraftBukkit start - Call event when pushing items into other inventories
+-                        ItemStack original = item.copy();
+-                        org.bukkit.craftbukkit.inventory.CraftItemStack oitemstack = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(
+-                            blockEntity.removeItem(i, level.spigotConfig.hopperAmount)
+-                        ); // Spigot
+-
+-                        org.bukkit.inventory.Inventory destinationInventory;
+-                        // Have to special case large chests as they work oddly
+-                        if (attachedContainer instanceof final net.minecraft.world.CompoundContainer compoundContainer) {
+-                            destinationInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest(compoundContainer);
+-                        } else if (attachedContainer.getOwner() != null) {
+-                            destinationInventory = attachedContainer.getOwner().getInventory();
+-                        } else {
+-                            destinationInventory = new org.bukkit.craftbukkit.inventory.CraftInventory(attachedContainer);
+-                        }
+-
+-                        org.bukkit.event.inventory.InventoryMoveItemEvent event = new org.bukkit.event.inventory.InventoryMoveItemEvent(
+-                            blockEntity.getOwner().getInventory(),
+-                            oitemstack,
+-                            destinationInventory,
+-                            true
+-                        );
+-                        if (!event.callEvent()) {
+-                            blockEntity.setItem(i, original);
+-                            blockEntity.setCooldown(level.spigotConfig.hopperTransfer); // Delay hopper checks // Spigot
+-                            return false;
+-                        }
+-                        int origCount = event.getItem().getAmount(); // Spigot
+-                        ItemStack itemStack = HopperBlockEntity.addItem(blockEntity, attachedContainer, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem()), opposite);
+-                        // CraftBukkit end
+-
+-                        if (itemStack.isEmpty()) {
+-                            attachedContainer.setChanged();
+-                            return true;
+-                        }
+-
+-                        item.setCount(count);
+-                        // Spigot start
+-                        item.shrink(origCount - itemStack.getCount());
+-                        if (count <= level.spigotConfig.hopperAmount) {
+-                            // Spigot end
+-                            blockEntity.setItem(i, item);
+-                        }
+-                    }
+-                }
+-
+-                return false;
++                // Paper start - Perf: Optimize Hoppers
++                return hopperPush(level, attachedContainer, opposite, blockEntity);
++                //for (int i = 0; i < blockEntity.getContainerSize(); i++) {
++                //    ItemStack item = blockEntity.getItem(i);
++                //    if (!item.isEmpty()) {
++                //        int count = item.getCount();
++                //        // CraftBukkit start - Call event when pushing items into other inventories
++                //        ItemStack original = item.copy();
++                //        org.bukkit.craftbukkit.inventory.CraftItemStack oitemstack = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(
++                //            blockEntity.removeItem(i, level.spigotConfig.hopperAmount)
++                //        ); // Spigot
++
++                //        org.bukkit.inventory.Inventory destinationInventory;
++                //        // Have to special case large chests as they work oddly
++                //        if (attachedContainer instanceof final net.minecraft.world.CompoundContainer compoundContainer) {
++                //            destinationInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest(compoundContainer);
++                //        } else if (attachedContainer.getOwner() != null) {
++                //            destinationInventory = attachedContainer.getOwner().getInventory();
++                //        } else {
++                //            destinationInventory = new org.bukkit.craftbukkit.inventory.CraftInventory(attachedContainer);
++                //        }
++
++                //        org.bukkit.event.inventory.InventoryMoveItemEvent event = new org.bukkit.event.inventory.InventoryMoveItemEvent(
++                //            blockEntity.getOwner().getInventory(),
++                //            oitemstack,
++                //            destinationInventory,
++                //            true
++                //        );
++                //        if (!event.callEvent()) {
++                //            blockEntity.setItem(i, original);
++                //            blockEntity.setCooldown(level.spigotConfig.hopperTransfer); // Delay hopper checks // Spigot
++                //            return false;
++                //        }
++                //        int origCount = event.getItem().getAmount(); // Spigot
++                //        ItemStack itemStack = HopperBlockEntity.addItem(blockEntity, attachedContainer, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem()), opposite);
++                //        // CraftBukkit end
++
++                //        if (itemStack.isEmpty()) {
++                //            attachedContainer.setChanged();
++                //            return true;
++                //        }
++
++                //        item.setCount(count);
++                //        // Spigot start
++                //        item.shrink(origCount - itemStack.getCount());
++                //        if (count <= level.spigotConfig.hopperAmount) {
++                //            // Spigot end
++                //            blockEntity.setItem(i, item);
++                //        }
++                //    }
++                //}
++
++                //return false;
++                // Paper end - Perf: Optimize Hoppers
+             }
+         }
+     }
+@@ -288,6 +529,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
+         Container sourceContainer = getSourceContainer(level, hopper, blockPos, blockState);
+         if (sourceContainer != null) {
+             Direction direction = Direction.DOWN;
++            skipPullModeEventFire = skipHopperEvents; // Paper - Perf: Optimize Hoppers
+ 
+             for (int i : getSlots(sourceContainer, direction)) {
+                 if (tryTakeInItemFromSlot(hopper, sourceContainer, i, direction, level)) { // Spigot
+@@ -313,55 +555,58 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
+     private static boolean tryTakeInItemFromSlot(Hopper hopper, Container container, int slot, Direction direction, Level level) { // Spigot
+         ItemStack item = container.getItem(slot);
+         if (!item.isEmpty() && canTakeItemFromContainer(hopper, container, item, slot, direction)) {
+-            int count = item.getCount();
+-            // CraftBukkit start - Call event on collection of items from inventories into the hopper
+-            ItemStack original = item.copy();
+-            org.bukkit.craftbukkit.inventory.CraftItemStack oitemstack = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(
+-                container.removeItem(slot, level.spigotConfig.hopperAmount) // Spigot
+-            );
+-
+-            org.bukkit.inventory.Inventory sourceInventory;
+-            // Have to special case large chests as they work oddly
+-            if (container instanceof final net.minecraft.world.CompoundContainer compoundContainer) {
+-                sourceInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest(compoundContainer);
+-            } else if (container.getOwner() != null) {
+-                sourceInventory = container.getOwner().getInventory();
+-            } else {
+-                sourceInventory = new org.bukkit.craftbukkit.inventory.CraftInventory(container);
+-            }
+-
+-            org.bukkit.event.inventory.InventoryMoveItemEvent event = new org.bukkit.event.inventory.InventoryMoveItemEvent(
+-                sourceInventory,
+-                oitemstack,
+-                hopper.getOwner().getInventory(),
+-                false
+-            );
+-
+-            if (!event.callEvent()) {
+-                container.setItem(slot, original);
+-
+-                if (hopper instanceof final HopperBlockEntity hopperBlockEntity) {
+-                    hopperBlockEntity.setCooldown(level.spigotConfig.hopperTransfer); // Spigot
+-                }
+-
+-                return false;
+-            }
+-            int origCount = event.getItem().getAmount(); // Spigot
+-            ItemStack itemStack = HopperBlockEntity.addItem(container, hopper, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem()), null);
+-            // CraftBukkit end
+-
+-            if (itemStack.isEmpty()) {
+-                container.setChanged();
+-                return true;
+-            }
+-
+-            item.setCount(count);
+-            // Spigot start
+-            item.shrink(origCount - itemStack.getCount());
+-            if (count <= level.spigotConfig.hopperAmount) {
+-                // Spigot end
+-                container.setItem(slot, item);
+-            }
++            // Paper start - Perf: Optimize Hoppers
++            return hopperPull(level, hopper, container, item, slot);
++            //int count = item.getCount();
++            //// CraftBukkit start - Call event on collection of items from inventories into the hopper
++            //ItemStack original = item.copy();
++            //org.bukkit.craftbukkit.inventory.CraftItemStack oitemstack = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(
++            //    container.removeItem(slot, level.spigotConfig.hopperAmount) // Spigot
++            //);
++
++            //org.bukkit.inventory.Inventory sourceInventory;
++            //// Have to special case large chests as they work oddly
++            //if (container instanceof final net.minecraft.world.CompoundContainer compoundContainer) {
++            //    sourceInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest(compoundContainer);
++            //} else if (container.getOwner() != null) {
++            //    sourceInventory = container.getOwner().getInventory();
++            //} else {
++            //    sourceInventory = new org.bukkit.craftbukkit.inventory.CraftInventory(container);
++            //}
++
++            //org.bukkit.event.inventory.InventoryMoveItemEvent event = new org.bukkit.event.inventory.InventoryMoveItemEvent(
++            //    sourceInventory,
++            //    oitemstack,
++            //    hopper.getOwner().getInventory(),
++            //    false
++            //);
++
++            //if (!event.callEvent()) {
++            //    container.setItem(slot, original);
++
++            //    if (hopper instanceof final HopperBlockEntity hopperBlockEntity) {
++            //        hopperBlockEntity.setCooldown(level.spigotConfig.hopperTransfer); // Spigot
++            //    }
++
++            //    return false;
++            //}
++            //int origCount = event.getItem().getAmount(); // Spigot
++            //ItemStack itemStack = HopperBlockEntity.addItem(container, hopper, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem()), null);
++            //// CraftBukkit end
++
++            //if (itemStack.isEmpty()) {
++            //    container.setChanged();
++            //    return true;
++            //}
++
++            //item.setCount(count);
++            //// Spigot start
++            //item.shrink(origCount - itemStack.getCount());
++            //if (count <= level.spigotConfig.hopperAmount) {
++            //    // Spigot end
++            //    container.setItem(slot, item);
++            //}
++            // Paper end - Perf: Optimize Hoppers
+         }
+ 
+         return false;
+@@ -370,13 +615,15 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
+     public static boolean addItem(Container container, ItemEntity item) {
+         boolean flag = false;
+         // CraftBukkit start
++        if (org.bukkit.event.inventory.InventoryPickupItemEvent.getHandlerList().getRegisteredListeners().length > 0) { // Paper - optimize hoppers
+         org.bukkit.event.inventory.InventoryPickupItemEvent event = new org.bukkit.event.inventory.InventoryPickupItemEvent(
+-            container.getOwner().getInventory(), (org.bukkit.entity.Item) item.getBukkitEntity()
++            getInventory(container), (org.bukkit.entity.Item) item.getBukkitEntity() // Paper - Perf: Optimize Hoppers; use getInventory() to avoid snapshot creation
+         );
+         if (!event.callEvent()) {
+             return false;
+         }
+         // CraftBukkit end
++        } // Paper - Perf: Optimize Hoppers
+         ItemStack itemStack = item.getItem().copy();
+         ItemStack itemStack1 = addItem(null, container, itemStack, null);
+         if (itemStack1.isEmpty()) {
+@@ -431,7 +678,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
+                     stack = stack.split(destination.getMaxStackSize());
+                 }
+                 // Spigot end
++                ignoreBlockEntityUpdates = true; // Paper - Perf: Optimize Hoppers
+                 destination.setItem(slot, stack);
++                ignoreBlockEntityUpdates = false; // Paper - Perf: Optimize Hoppers
+                 stack = leftover; // Paper - Make hoppers respect inventory max stack size
+                 flag = true;
+             } else if (canMergeItems(item, stack)) {
+@@ -519,13 +768,19 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
+ 
+     @Nullable
+     public static Container getContainerAt(Level level, BlockPos pos) {
+-        return getContainerAt(level, pos, level.getBlockState(pos), pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5);
++        return getContainerAt(level, pos, level.getBlockState(pos), pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, true); // Paper - Optimize hoppers
+     }
+ 
+     @Nullable
+     private static Container getContainerAt(Level level, BlockPos pos, BlockState state, double x, double y, double z) {
++        // Paper start - Perf: Optimize Hoppers
++        return HopperBlockEntity.getContainerAt(level, pos, state, x, y, z, false);
++    }
++    @Nullable
++    private static Container getContainerAt(Level level, BlockPos pos, BlockState state, double x, double y, double z, final boolean optimizeEntities) {
++        // Paper end - Perf: Optimize Hoppers
+         Container blockContainer = getBlockContainer(level, pos, state);
+-        if (blockContainer == null) {
++        if (blockContainer == null && (!optimizeEntities || !level.paperConfig().hopper.ignoreOccludingBlocks || !state.getBukkitMaterial().isOccluding())) { // Paper - Perf: Optimize Hoppers
+             blockContainer = getEntityContainer(level, x, y, z);
+         }
+ 
+@@ -551,14 +806,14 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
+ 
+     @Nullable
+     private static Container getEntityContainer(Level level, double x, double y, double z) {
+-        List<Entity> entities = level.getEntities(
+-            (Entity)null, new AABB(x - 0.5, y - 0.5, z - 0.5, x + 0.5, y + 0.5, z + 0.5), EntitySelector.CONTAINER_ENTITY_SELECTOR
++        List<Entity> entities = level.getEntitiesOfClass(
++            (Class) Container.class, new AABB(x - 0.5, y - 0.5, z - 0.5, x + 0.5, y + 0.5, z + 0.5), EntitySelector.CONTAINER_ENTITY_SELECTOR // Paper - Perf: Optimize hoppers
+         );
+         return !entities.isEmpty() ? (Container)entities.get(level.random.nextInt(entities.size())) : null;
+     }
+ 
+     private static boolean canMergeItems(ItemStack stack1, ItemStack stack2) {
+-        return stack1.getCount() <= stack1.getMaxStackSize() && ItemStack.isSameItemSameComponents(stack1, stack2);
++        return stack1.getCount() < stack1.getMaxStackSize() && ItemStack.isSameItemSameComponents(stack1, stack2); // Paper - Perf: Optimize Hoppers; used to return true for full itemstacks?!
+     }
+ 
+     @Override
+diff --git a/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java b/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java
+index 73b3ddb120d6b6f89e478960e78bed415baea205..f9c31da81d84033abfc1179fc643bceffe35da17 100644
+--- a/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java
+@@ -53,7 +53,7 @@ public abstract class RandomizableContainerBlockEntity extends BaseContainerBloc
+ 
+     @Override
+     public ItemStack getItem(int index) {
+-        this.unpackLootTable(null);
++        if (index == 0) this.unpackLootTable(null); // Paper - Perf: Optimize Hoppers
+         return super.getItem(index);
+     }
+ 
diff --git a/feature-patches/1070-Optimise-collision-checking-in-player-move-packet-ha.patch b/paper-server/patches/features/0030-Optimise-collision-checking-in-player-move-packet-ha.patch
similarity index 55%
rename from feature-patches/1070-Optimise-collision-checking-in-player-move-packet-ha.patch
rename to paper-server/patches/features/0030-Optimise-collision-checking-in-player-move-packet-ha.patch
index 3f4cd40208..dbc8cdd071 100644
--- a/feature-patches/1070-Optimise-collision-checking-in-player-move-packet-ha.patch
+++ b/paper-server/patches/features/0030-Optimise-collision-checking-in-player-move-packet-ha.patch
@@ -5,61 +5,62 @@ Subject: [PATCH] Optimise collision checking in player move packet handling
 
 Move collision logic to just the hasNewCollision call instead of getCubes + hasNewCollision
 
-diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
-+++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
-@@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+index 650126e6445c0458c6a6649c235908bfeea428cd..d248671b2e1c6256fc4d74320bdb29ca078bad0b 100644
+--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java
++++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+@@ -561,7 +561,7 @@ public class ServerGamePacketListenerImpl
                      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; // Paper - diff on change, used for checking large move vectors above
-@@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+-                boolean flag = serverLevel.noCollision(rootVehicle, rootVehicle.getBoundingBox().deflate(0.0625));
++                AABB oldBox = rootVehicle.getBoundingBox(); // Paper - copy from player movement packet
+                 d3 = d - this.vehicleLastGoodX; // Paper - diff on change, used for checking large move vectors above
+                 d4 = d1 - this.vehicleLastGoodY; // Paper - diff on change, used for checking large move vectors above
+                 d5 = d2 - this.vehicleLastGoodZ; // Paper - diff on change, used for checking large move vectors above
+@@ -571,6 +571,7 @@ public class ServerGamePacketListenerImpl
                  }
  
-                 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();
-@@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+                 rootVehicle.move(MoverType.PLAYER, new Vec3(d3, d4, d5));
++                boolean didCollide = toX != rootVehicle.getX() || toY != rootVehicle.getY() || toZ != rootVehicle.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 verticalDelta = d4; // Paper - Decompile fix, was named d11 previously, is now gone in the source
+                 d3 = d - rootVehicle.getX();
+                 d4 = d1 - rootVehicle.getY();
+@@ -582,14 +583,22 @@ public class ServerGamePacketListenerImpl
+                 d7 = d3 * d3 + d4 * d4 + d5 * d5;
                  boolean flag2 = false;
- 
-                 if (d10 > org.spigotmc.SpigotConfig.movedWronglyThreshold) { // Spigot
+                 if (d7 > 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)});
+                     LOGGER.warn("{} (vehicle of {}) moved wrongly! {}", rootVehicle.getName().getString(), this.player.getName().getString(), Math.sqrt(d7));
                  }
  
-                 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));
- 
+                 rootVehicle.absMoveTo(d, d1, d2, f, f1);
+                 this.player.absMoveTo(d, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit
+-                boolean flag3 = serverLevel.noCollision(rootVehicle, rootVehicle.getBoundingBox().deflate(0.0625));
 -                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();
++                    AABB newBox = rootVehicle.getBoundingBox();
 +                    if (didCollide || !oldBox.equals(newBox)) {
-+                        teleportBack = this.hasNewCollision(worldserver, entity, oldBox, newBox);
++                        teleportBack = this.hasNewCollision(serverLevel, rootVehicle, 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(ClientboundMoveVehiclePacket.fromEntity(entity));
-@@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+                     rootVehicle.absMoveTo(x, y, z, f, f1);
+                     this.player.absMoveTo(x, y, z, this.player.getYRot(), this.player.getXRot()); // CraftBukkit
+                     this.send(ClientboundMoveVehiclePacket.fromEntity(rootVehicle));
+@@ -667,9 +676,32 @@ public class ServerGamePacketListenerImpl
      }
  
      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);
+-        return entity.level()
+-            .getBlockStates(entity.getBoundingBox().inflate(0.0625).expandTowards(0.0, -0.55, 0.0))
+-            .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);
++        AABB box = entity.getBoundingBox().inflate(0.0625).expandTowards(0.0, -0.55, 0.0);
 +        int minX = Mth.floor(box.minX);
 +        int minY = Mth.floor(box.minY);
 +        int minZ = Mth.floor(box.minZ);
@@ -67,15 +68,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        int maxY = Mth.floor(box.maxY);
 +        int maxZ = Mth.floor(box.maxZ);
 +
-+        Level world = entity.level();
++        Level level = 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()) {
++                    BlockState blockState = level.getBlockStateIfLoaded(pos);
++                    if (blockState != null && !blockState.isAir()) {
 +                        return false;
 +                    }
 +                }
@@ -87,61 +88,61 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      @Override
-@@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+@@ -1361,7 +1393,7 @@ public class ServerGamePacketListenerImpl
                                  }
                              }
  
--                            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
-@@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
- 
-                             this.player.move(MoverType.PLAYER, new Vec3(d6, d7, d8));
+-                            AABB boundingBox = this.player.getBoundingBox();
++                            AABB boundingBox = this.player.getBoundingBox(); // Paper - diff on change, should be old AABB
+                             d3 = d - this.lastGoodX; // Paper - diff on change, used for checking large move vectors above
+                             d4 = d1 - this.lastGoodY; // Paper - diff on change, used for checking large move vectors above
+                             d5 = d2 - this.lastGoodZ; // Paper - diff on change, used for checking large move vectors above
+@@ -1400,6 +1432,7 @@ public class ServerGamePacketListenerImpl
+                             boolean flag1 = this.player.verticalCollisionBelow;
+                             this.player.move(MoverType.PLAYER, new Vec3(d3, d4, d5));
                              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.
-@@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+@@ -1432,7 +1465,17 @@ public class ServerGamePacketListenerImpl
                              }
  
                              // 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));
+-                            boolean teleportBack = !this.player.noPhysics && !this.player.isSleeping() && (movedWrongly && serverLevel.noCollision(this.player, boundingBox) || this.isPlayerCollidingWithAnythingNew(serverLevel, boundingBox, d, 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
++                            this.player.absMoveTo(d, 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)) {
++                                if (didCollide || !boundingBox.equals(newBox)) {
 +                                    // note: only call after setLocation, or else getBoundingBox is wrong
-+                                    teleportBack = this.hasNewCollision(worldserver, this.player, axisalignedbb, newBox);
++                                    teleportBack = this.hasNewCollision(serverLevel, this.player, boundingBox, 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);
-@@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+@@ -1568,7 +1611,7 @@ public class ServerGamePacketListenerImpl
  
      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());
-             }
-@@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+                 this.teleport(
+                     this.awaitingPositionFromClient.x,
+@@ -1587,6 +1630,33 @@ public class ServerGamePacketListenerImpl
          }
      }
  
 +    // Paper start - optimise out extra getCubes
-+    private boolean hasNewCollision(final ServerLevel world, final Entity entity, final AABB oldBox, final AABB newBox) {
++    private boolean hasNewCollision(final ServerLevel level, final Entity entity, final AABB oldBox, final AABB newBox) {
 +        final List<AABB> collisionsBB = new java.util.ArrayList<>();
 +        final List<VoxelShape> collisionsVoxel = new java.util.ArrayList<>();
 +        ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisions(
-+            world, entity, newBox, collisionsVoxel, collisionsBB,
++            level, entity, newBox, collisionsVoxel, collisionsBB,
 +            ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS | ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER,
 +            null, null
 +        );
@@ -163,6 +164,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        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<VoxelShape> iterable = world.getCollisions(this.player, axisalignedbb1.deflate(9.999999747378752E-6D));
+     private boolean isPlayerCollidingWithAnythingNew(LevelReader level, AABB box, double x, double y, double z) {
+         AABB aabb = this.player.getBoundingBox().move(x - this.player.getX(), y - this.player.getY(), z - this.player.getZ());
+         Iterable<VoxelShape> collisions = level.getCollisions(this.player, aabb.deflate(1.0E-5F));
diff --git a/paper-server/patches/resources/data/minecraft/loot_table/chests/trial_chambers/intersection_barrel.json.patch b/paper-server/patches/resources/data/minecraft/loot_table/chests/trial_chambers/intersection_barrel.json.patch
index 572f83673b..7eb4e3632c 100644
--- a/paper-server/patches/resources/data/minecraft/loot_table/chests/trial_chambers/intersection_barrel.json.patch
+++ b/paper-server/patches/resources/data/minecraft/loot_table/chests/trial_chambers/intersection_barrel.json.patch
@@ -1,6 +1,6 @@
 --- a/data/minecraft/loot_table/chests/trial_chambers/intersection_barrel.json
 +++ b/data/minecraft/loot_table/chests/trial_chambers/intersection_barrel.json
-@@ -70,15 +70,6 @@
+@@ -70,15 +_,6 @@
                "add": false,
                "count": 1.0,
                "function": "minecraft:set_count"
diff --git a/paper-server/patches/resources/data/minecraft/worldgen/noise_settings/amplified.json.patch b/paper-server/patches/resources/data/minecraft/worldgen/noise_settings/amplified.json.patch
new file mode 100644
index 0000000000..356d44998f
--- /dev/null
+++ b/paper-server/patches/resources/data/minecraft/worldgen/noise_settings/amplified.json.patch
@@ -0,0 +1,12 @@
+--- a/data/minecraft/worldgen/noise_settings/amplified.json
++++ b/data/minecraft/worldgen/noise_settings/amplified.json
+@@ -389,7 +_,8 @@
+       {
+         "type": "minecraft:condition",
+         "if_true": {
+-          "type": "minecraft:vertical_gradient",
++          "type": "paper:optionally_flat_bedrock_condition_source",
++          "is_roof": false,
+           "false_at_and_above": {
+             "above_bottom": 5
+           },
diff --git a/paper-server/patches/resources/data/minecraft/worldgen/noise_settings/caves.json.patch b/paper-server/patches/resources/data/minecraft/worldgen/noise_settings/caves.json.patch
new file mode 100644
index 0000000000..64ecd45575
--- /dev/null
+++ b/paper-server/patches/resources/data/minecraft/worldgen/noise_settings/caves.json.patch
@@ -0,0 +1,22 @@
+--- a/data/minecraft/worldgen/noise_settings/caves.json
++++ b/data/minecraft/worldgen/noise_settings/caves.json
+@@ -110,7 +_,8 @@
+         "if_true": {
+           "type": "minecraft:not",
+           "invert": {
+-            "type": "minecraft:vertical_gradient",
++            "type": "paper:optionally_flat_bedrock_condition_source",
++            "is_roof": true,
+             "false_at_and_above": {
+               "below_top": 0
+             },
+@@ -130,7 +_,8 @@
+       {
+         "type": "minecraft:condition",
+         "if_true": {
+-          "type": "minecraft:vertical_gradient",
++          "type": "paper:optionally_flat_bedrock_condition_source",
++          "is_roof": false,
+           "false_at_and_above": {
+             "above_bottom": 5
+           },
diff --git a/paper-server/patches/resources/data/minecraft/worldgen/noise_settings/large_biomes.json.patch b/paper-server/patches/resources/data/minecraft/worldgen/noise_settings/large_biomes.json.patch
new file mode 100644
index 0000000000..e82d43382c
--- /dev/null
+++ b/paper-server/patches/resources/data/minecraft/worldgen/noise_settings/large_biomes.json.patch
@@ -0,0 +1,12 @@
+--- a/data/minecraft/worldgen/noise_settings/large_biomes.json
++++ b/data/minecraft/worldgen/noise_settings/large_biomes.json
+@@ -389,7 +_,8 @@
+       {
+         "type": "minecraft:condition",
+         "if_true": {
+-          "type": "minecraft:vertical_gradient",
++          "type": "paper:optionally_flat_bedrock_condition_source",
++          "is_roof": false,
+           "false_at_and_above": {
+             "above_bottom": 5
+           },
diff --git a/paper-server/patches/resources/data/minecraft/worldgen/noise_settings/nether.json.patch b/paper-server/patches/resources/data/minecraft/worldgen/noise_settings/nether.json.patch
new file mode 100644
index 0000000000..ad053aec9a
--- /dev/null
+++ b/paper-server/patches/resources/data/minecraft/worldgen/noise_settings/nether.json.patch
@@ -0,0 +1,22 @@
+--- a/data/minecraft/worldgen/noise_settings/nether.json
++++ b/data/minecraft/worldgen/noise_settings/nether.json
+@@ -108,7 +_,8 @@
+       {
+         "type": "minecraft:condition",
+         "if_true": {
+-          "type": "minecraft:vertical_gradient",
++          "type": "paper:optionally_flat_bedrock_condition_source",
++          "is_roof": false,
+           "false_at_and_above": {
+             "above_bottom": 5
+           },
+@@ -129,7 +_,8 @@
+         "if_true": {
+           "type": "minecraft:not",
+           "invert": {
+-            "type": "minecraft:vertical_gradient",
++            "type": "paper:optionally_flat_bedrock_condition_source",
++            "is_roof": true,
+             "false_at_and_above": {
+               "below_top": 0
+             },
diff --git a/paper-server/patches/resources/data/minecraft/worldgen/noise_settings/overworld.json.patch b/paper-server/patches/resources/data/minecraft/worldgen/noise_settings/overworld.json.patch
new file mode 100644
index 0000000000..868ffa6476
--- /dev/null
+++ b/paper-server/patches/resources/data/minecraft/worldgen/noise_settings/overworld.json.patch
@@ -0,0 +1,12 @@
+--- a/data/minecraft/worldgen/noise_settings/overworld.json
++++ b/data/minecraft/worldgen/noise_settings/overworld.json
+@@ -389,7 +_,8 @@
+       {
+         "type": "minecraft:condition",
+         "if_true": {
+-          "type": "minecraft:vertical_gradient",
++          "type": "paper:optionally_flat_bedrock_condition_source",
++          "is_roof": false,
+           "false_at_and_above": {
+             "above_bottom": 5
+           },
diff --git a/paper-server/patches/sources/ca/spottedleaf/moonrise/paper/PaperHooks.java.patch b/paper-server/patches/sources/ca/spottedleaf/moonrise/paper/PaperHooks.java.patch
new file mode 100644
index 0000000000..d736409b62
--- /dev/null
+++ b/paper-server/patches/sources/ca/spottedleaf/moonrise/paper/PaperHooks.java.patch
@@ -0,0 +1,244 @@
+--- /dev/null
++++ b/ca/spottedleaf/moonrise/paper/PaperHooks.java
+@@ -1,0 +_,241 @@
++package ca.spottedleaf.moonrise.paper;
++
++import ca.spottedleaf.moonrise.common.PlatformHooks;
++import ca.spottedleaf.moonrise.paper.util.BaseChunkSystemHooks;
++import com.mojang.datafixers.DSL;
++import com.mojang.datafixers.DataFixer;
++import com.mojang.serialization.Dynamic;
++import java.util.Collection;
++import net.minecraft.core.BlockPos;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.nbt.NbtOps;
++import net.minecraft.server.level.ChunkHolder;
++import net.minecraft.server.level.GenerationChunkHolder;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.server.level.ServerPlayer;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.entity.boss.EnderDragonPart;
++import net.minecraft.world.level.BlockGetter;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.level.Level;
++import net.minecraft.world.level.block.state.BlockState;
++import net.minecraft.world.level.chunk.ChunkAccess;
++import net.minecraft.world.level.chunk.LevelChunk;
++import net.minecraft.world.level.chunk.ProtoChunk;
++import net.minecraft.world.level.chunk.storage.SerializableChunkData;
++import net.minecraft.world.level.entity.EntityTypeTest;
++import net.minecraft.world.phys.AABB;
++import java.util.List;
++import java.util.function.Predicate;
++
++public final class PaperHooks extends BaseChunkSystemHooks implements PlatformHooks {
++
++    @Override
++    public String getBrand() {
++        return "Paper";
++    }
++
++    @Override
++    public int getLightEmission(final BlockState blockState, final BlockGetter world, final BlockPos pos) {
++        return blockState.getLightEmission();
++    }
++
++    @Override
++    public Predicate<BlockState> maybeHasLightEmission() {
++        return (final BlockState state) -> {
++            return state.getLightEmission() != 0;
++        };
++    }
++
++    @Override
++    public boolean hasCurrentlyLoadingChunk() {
++        return false;
++    }
++
++    @Override
++    public LevelChunk getCurrentlyLoadingChunk(final GenerationChunkHolder holder) {
++        return null;
++    }
++
++    @Override
++    public void setCurrentlyLoading(final GenerationChunkHolder holder, final LevelChunk levelChunk) {
++
++    }
++
++    @Override
++    public void chunkFullStatusComplete(final LevelChunk newChunk, final ProtoChunk original) {
++
++    }
++
++    @Override
++    public boolean allowAsyncTicketUpdates() {
++        return true;
++    }
++
++    @Override
++    public void onChunkHolderTicketChange(final ServerLevel world, final ChunkHolder holder, final int oldLevel, final int newLevel) {
++
++    }
++
++    @Override
++    public void chunkUnloadFromWorld(final LevelChunk chunk) {
++
++    }
++
++    @Override
++    public void chunkSyncSave(final ServerLevel world, final ChunkAccess chunk, final SerializableChunkData data) {
++
++    }
++
++    @Override
++    public void onChunkWatch(final ServerLevel world, final LevelChunk chunk, final ServerPlayer player) {
++
++    }
++
++    @Override
++    public void onChunkUnWatch(final ServerLevel world, final ChunkPos chunk, final ServerPlayer player) {
++
++    }
++
++    @Override
++    public void addToGetEntities(final Level world, final Entity entity, final AABB boundingBox, final Predicate<? super Entity> predicate, final List<Entity> into) {
++        final Collection<EnderDragonPart> parts = world.dragonParts();
++        if (parts.isEmpty()) {
++            return;
++        }
++
++        for (final EnderDragonPart part : parts) {
++            if (part != entity && part.getBoundingBox().intersects(boundingBox) && (predicate == null || predicate.test(part))) {
++                into.add(part);
++            }
++        }
++    }
++
++    @Override
++    public <T extends Entity> void addToGetEntities(final Level world, final EntityTypeTest<Entity, T> entityTypeTest, final AABB boundingBox, final Predicate<? super T> predicate, final List<? super T> into, final int maxCount) {
++        if (into.size() >= maxCount) {
++            // fix neoforge issue: do not add if list is already full
++            return;
++        }
++
++        final Collection<EnderDragonPart> parts = world.dragonParts();
++        if (parts.isEmpty()) {
++            return;
++        }
++        for (final EnderDragonPart part : parts) {
++            if (!part.getBoundingBox().intersects(boundingBox)) {
++                continue;
++            }
++            final T casted = (T)entityTypeTest.tryCast(part);
++            if (casted != null && (predicate == null || predicate.test(casted))) {
++                into.add(casted);
++                if (into.size() >= maxCount) {
++                    break;
++                }
++            }
++        }
++    }
++
++    @Override
++    public void entityMove(final Entity entity, final long oldSection, final long newSection) {
++
++    }
++
++    @Override
++    public boolean screenEntity(final ServerLevel world, final Entity entity, final boolean fromDisk, final boolean event) {
++        return true;
++    }
++
++    @Override
++    public boolean configFixMC224294() {
++        return true;
++    }
++
++    @Override
++    public boolean configAutoConfigSendDistance() {
++        return io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.autoConfigSendDistance;
++    }
++
++    @Override
++    public double configPlayerMaxLoadRate() {
++        return io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkLoadRate;
++    }
++
++    @Override
++    public double configPlayerMaxGenRate() {
++        return io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkGenerateRate;
++    }
++
++    @Override
++    public double configPlayerMaxSendRate() {
++        return io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkSendRate;
++    }
++
++    @Override
++    public int configPlayerMaxConcurrentLoads() {
++        return io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkLoads;
++    }
++
++    @Override
++    public int configPlayerMaxConcurrentGens() {
++        return io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkGenerates;
++    }
++
++    @Override
++    public long configAutoSaveInterval(final ServerLevel world) {
++        return world.paperConfig().chunks.autoSaveInterval.value();
++    }
++
++    @Override
++    public int configMaxAutoSavePerTick(final ServerLevel world) {
++        return world.paperConfig().chunks.maxAutoSaveChunksPerTick;
++    }
++
++    @Override
++    public boolean configFixMC159283() {
++        return true;
++    }
++
++    @Override
++    public boolean forceNoSave(final ChunkAccess chunk) {
++        return chunk instanceof LevelChunk levelChunk && levelChunk.mustNotSave;
++    }
++
++    @Override
++    public CompoundTag convertNBT(final DSL.TypeReference type, final DataFixer dataFixer, final CompoundTag nbt,
++                                  final int fromVersion, final int toVersion) {
++        return (CompoundTag)dataFixer.update(
++            type, new Dynamic<>(NbtOps.INSTANCE, nbt), fromVersion, toVersion
++        ).getValue();
++    }
++
++    @Override
++    public boolean hasMainChunkLoadHook() {
++        return false;
++    }
++
++    @Override
++    public void mainChunkLoad(final ChunkAccess chunk, final SerializableChunkData chunkData) {
++
++    }
++
++    @Override
++    public List<Entity> modifySavedEntities(final ServerLevel world, final int chunkX, final int chunkZ, final List<Entity> entities) {
++        return entities;
++    }
++
++    @Override
++    public void unloadEntity(final Entity entity) {
++        entity.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK, org.bukkit.event.entity.EntityRemoveEvent.Cause.UNLOAD);
++    }
++
++    @Override
++    public void postLoadProtoChunk(final ServerLevel world, final ProtoChunk chunk) {
++        net.minecraft.world.level.chunk.status.ChunkStatusTasks.postLoadProtoChunk(world, chunk.getEntities());
++    }
++
++    @Override
++    public int modifyEntityTrackingRange(final Entity entity, final int currentRange) {
++        return org.spigotmc.TrackingRange.getEntityTrackingRange(entity, currentRange);
++    }
++}
diff --git a/paper-server/patches/sources/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java.patch b/paper-server/patches/sources/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java.patch
new file mode 100644
index 0000000000..1835941481
--- /dev/null
+++ b/paper-server/patches/sources/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java.patch
@@ -0,0 +1,335 @@
+--- /dev/null
++++ b/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java
+@@ -1,0 +_,332 @@
++package ca.spottedleaf.moonrise.paper.util;
++
++import ca.spottedleaf.concurrentutil.util.Priority;
++import com.mojang.logging.LogUtils;
++import net.minecraft.server.level.ChunkHolder;
++import net.minecraft.server.level.ChunkResult;
++import net.minecraft.server.level.FullChunkStatus;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.server.level.ServerPlayer;
++import net.minecraft.server.level.TicketType;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.level.chunk.ChunkAccess;
++import net.minecraft.world.level.chunk.LevelChunk;
++import net.minecraft.world.level.chunk.status.ChunkPyramid;
++import net.minecraft.world.level.chunk.status.ChunkStatus;
++import net.minecraft.world.level.chunk.status.ChunkStep;
++import org.bukkit.Bukkit;
++import org.slf4j.Logger;
++import java.util.ArrayList;
++import java.util.List;
++import java.util.concurrent.CompletableFuture;
++import java.util.function.Consumer;
++
++public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.common.util.ChunkSystemHooks {
++
++    private static final Logger LOGGER = LogUtils.getLogger();
++    private static final ChunkStep FULL_CHUNK_STEP = ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FULL);
++    private static final TicketType<Long> CHUNK_LOAD = TicketType.create("chunk_load", Long::compareTo);
++
++    private long chunkLoadCounter = 0L;
++
++    private static int getDistance(final ChunkStatus status) {
++        return FULL_CHUNK_STEP.getAccumulatedRadiusOf(status);
++    }
++
++    @Override
++    public void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) {
++        this.scheduleChunkTask(level, chunkX, chunkZ, run, Priority.NORMAL);
++    }
++
++    @Override
++    public void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final Priority priority) {
++        level.chunkSource.mainThreadProcessor.execute(run);
++    }
++
++    @Override
++    public void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen,
++                                  final ChunkStatus toStatus, final boolean addTicket, final Priority priority,
++                                  final Consumer<ChunkAccess> onComplete) {
++        if (gen) {
++            this.scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
++            return;
++        }
++        this.scheduleChunkLoad(level, chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> {
++            if (chunk == null) {
++                if (onComplete != null) {
++                    onComplete.accept(null);
++                }
++            } else {
++                if (chunk.getPersistedStatus().isOrAfter(toStatus)) {
++                    BaseChunkSystemHooks.this.scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
++                } else {
++                    if (onComplete != null) {
++                        onComplete.accept(null);
++                    }
++                }
++            }
++        });
++    }
++
++    @Override
++    public void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus,
++                                  final boolean addTicket, final Priority priority, final Consumer<ChunkAccess> onComplete) {
++        if (!Bukkit.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) {
++            this.scheduleChunkTask(level, chunkX, chunkZ, () -> {
++                BaseChunkSystemHooks.this.scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
++            }, priority);
++            return;
++        }
++
++        final int minLevel = 33 + getDistance(toStatus);
++        final Long chunkReference = addTicket ? Long.valueOf(++this.chunkLoadCounter) : null;
++        final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
++
++        if (addTicket) {
++            level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
++        }
++        level.chunkSource.runDistanceManagerUpdates();
++
++        final Consumer<ChunkAccess> loadCallback = (final ChunkAccess chunk) -> {
++            try {
++                if (onComplete != null) {
++                    onComplete.accept(chunk);
++                }
++            } catch (final Throwable thr) {
++                LOGGER.error("Exception handling chunk load callback", thr);
++                com.destroystokyo.paper.util.SneakyThrow.sneaky(thr);
++            } finally {
++                if (addTicket) {
++                    level.chunkSource.addTicketAtLevel(net.minecraft.server.level.TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
++                    level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
++                }
++            }
++        };
++
++        final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
++
++        if (holder == null || holder.getTicketLevel() > minLevel) {
++            loadCallback.accept(null);
++            return;
++        }
++
++        final CompletableFuture<ChunkResult<ChunkAccess>> loadFuture = holder.scheduleChunkGenerationTask(toStatus, level.chunkSource.chunkMap);
++
++        if (loadFuture.isDone()) {
++            loadCallback.accept(loadFuture.join().orElse(null));
++            return;
++        }
++
++        loadFuture.whenCompleteAsync((final ChunkResult<ChunkAccess> result, final Throwable thr) -> {
++            if (thr != null) {
++                loadCallback.accept(null);
++                return;
++            }
++            loadCallback.accept(result.orElse(null));
++        }, (final Runnable r) -> {
++            BaseChunkSystemHooks.this.scheduleChunkTask(level, chunkX, chunkZ, r, Priority.HIGHEST);
++        });
++    }
++
++    @Override
++    public void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ,
++                                     final FullChunkStatus toStatus, final boolean addTicket,
++                                     final Priority priority, final Consumer<LevelChunk> onComplete) {
++        // This method goes unused until the chunk system rewrite
++        if (toStatus == FullChunkStatus.INACCESSIBLE) {
++            throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status");
++        }
++
++        if (!Bukkit.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) {
++            this.scheduleChunkTask(level, chunkX, chunkZ, () -> {
++                BaseChunkSystemHooks.this.scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
++            }, priority);
++            return;
++        }
++
++        final int minLevel = 33 - (toStatus.ordinal() - 1);
++        final int radius = toStatus.ordinal() - 1;
++        final Long chunkReference = addTicket ? Long.valueOf(++this.chunkLoadCounter) : null;
++        final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
++
++        if (addTicket) {
++            level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
++        }
++        level.chunkSource.runDistanceManagerUpdates();
++
++        final Consumer<LevelChunk> loadCallback = (final LevelChunk chunk) -> {
++            try {
++                if (onComplete != null) {
++                    onComplete.accept(chunk);
++                }
++            } catch (final Throwable thr) {
++                LOGGER.error("Exception handling chunk load callback", thr);
++                com.destroystokyo.paper.util.SneakyThrow.sneaky(thr);
++            } finally {
++                if (addTicket) {
++                    level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
++                    level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
++                }
++            }
++        };
++
++        final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
++
++        if (holder == null || holder.getTicketLevel() > minLevel) {
++            loadCallback.accept(null);
++            return;
++        }
++
++        final CompletableFuture<ChunkResult<LevelChunk>> tickingState;
++        switch (toStatus) {
++            case FULL: {
++                tickingState = holder.getFullChunkFuture();
++                break;
++            }
++            case BLOCK_TICKING: {
++                tickingState = holder.getTickingChunkFuture();
++                break;
++            }
++            case ENTITY_TICKING: {
++                tickingState = holder.getEntityTickingChunkFuture();
++                break;
++            }
++            default: {
++                throw new IllegalStateException("Cannot reach here");
++            }
++        }
++
++        if (tickingState.isDone()) {
++            loadCallback.accept(tickingState.join().orElse(null));
++            return;
++        }
++
++        tickingState.whenCompleteAsync((final ChunkResult<LevelChunk> result, final Throwable thr) -> {
++            if (thr != null) {
++                loadCallback.accept(null);
++                return;
++            }
++            loadCallback.accept(result.orElse(null));
++        }, (final Runnable r) -> {
++            BaseChunkSystemHooks.this.scheduleChunkTask(level, chunkX, chunkZ, r, Priority.HIGHEST);
++        });
++    }
++
++    @Override
++    public List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) {
++        return new ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values());
++    }
++
++    @Override
++    public List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level) {
++        return new ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values());
++    }
++
++    @Override
++    public int getVisibleChunkHolderCount(final ServerLevel level) {
++        return level.chunkSource.chunkMap.visibleChunkMap.size();
++    }
++
++    @Override
++    public int getUpdatingChunkHolderCount(final ServerLevel level) {
++        return level.chunkSource.chunkMap.updatingChunkMap.size();
++    }
++
++    @Override
++    public boolean hasAnyChunkHolders(final ServerLevel level) {
++        return this.getUpdatingChunkHolderCount(level) != 0;
++    }
++
++    @Override
++    public void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) {
++
++    }
++
++    @Override
++    public void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) {
++
++    }
++
++    @Override
++    public void onChunkPreBorder(final LevelChunk chunk, final ChunkHolder holder) {
++
++    }
++
++    @Override
++    public void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) {
++
++    }
++
++    @Override
++    public void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
++
++    }
++
++    @Override
++    public void onChunkPostNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
++
++    }
++
++    @Override
++    public void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) {
++
++    }
++
++    @Override
++    public void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) {
++
++    }
++
++    @Override
++    public void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
++
++    }
++
++    @Override
++    public void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
++
++    }
++
++    @Override
++    public ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) {
++        return level.chunkSource.chunkMap.getUnloadingChunkHolder(chunkX, chunkZ);
++    }
++
++    @Override
++    public int getSendViewDistance(final ServerPlayer player) {
++        return this.getViewDistance(player);
++    }
++
++    @Override
++    public int getViewDistance(final ServerPlayer player) {
++        final ServerLevel level = player.serverLevel();
++        if (level == null) {
++            return Bukkit.getViewDistance();
++        }
++        return level.chunkSource.chunkMap.serverViewDistance;
++    }
++
++    @Override
++    public int getTickViewDistance(final ServerPlayer player) {
++        final ServerLevel level = player.serverLevel();
++        if (level == null) {
++            return Bukkit.getSimulationDistance();
++        }
++        return level.chunkSource.chunkMap.distanceManager.simulationDistance;
++    }
++
++    @Override
++    public void addPlayerToDistanceMaps(final ServerLevel world, final ServerPlayer player) {
++
++    }
++
++    @Override
++    public void removePlayerFromDistanceMaps(final ServerLevel world, final ServerPlayer player) {
++
++    }
++
++    @Override
++    public void updateMaps(final ServerLevel world, final ServerPlayer player) {
++
++    }
++}
diff --git a/paper-server/patches/unapplied/com/mojang/authlib/yggdrasil/YggdrasilGameProfileRepository.java.patch b/paper-server/patches/sources/com/mojang/authlib/yggdrasil/YggdrasilGameProfileRepository.java.patch
similarity index 96%
rename from paper-server/patches/unapplied/com/mojang/authlib/yggdrasil/YggdrasilGameProfileRepository.java.patch
rename to paper-server/patches/sources/com/mojang/authlib/yggdrasil/YggdrasilGameProfileRepository.java.patch
index 30f05bd5aa..3d5dbbd314 100644
--- a/paper-server/patches/unapplied/com/mojang/authlib/yggdrasil/YggdrasilGameProfileRepository.java.patch
+++ b/paper-server/patches/sources/com/mojang/authlib/yggdrasil/YggdrasilGameProfileRepository.java.patch
@@ -1,6 +1,6 @@
 --- a/com/mojang/authlib/yggdrasil/YggdrasilGameProfileRepository.java
 +++ b/com/mojang/authlib/yggdrasil/YggdrasilGameProfileRepository.java
-@@ -44,6 +44,7 @@
+@@ -44,6 +_,7 @@
              .collect(Collectors.toSet());
  
          final int page = 0;
@@ -8,7 +8,7 @@
  
          for (final List<String> request : Iterables.partition(criteria, ENTRIES_PER_PAGE)) {
              final List<String> normalizedRequest = request.stream().map(YggdrasilGameProfileRepository::normalizeName).toList();
-@@ -75,6 +76,12 @@
+@@ -75,6 +_,12 @@
                          LOGGER.debug("Couldn't find profile {}", name);
                          callback.onProfileLookupFailed(name, new ProfileNotFoundException("Server did not find the requested profile"));
                      }
diff --git a/paper-server/patches/unapplied/com/mojang/brigadier/CommandDispatcher.java.patch b/paper-server/patches/sources/com/mojang/brigadier/CommandDispatcher.java.patch
similarity index 82%
rename from paper-server/patches/unapplied/com/mojang/brigadier/CommandDispatcher.java.patch
rename to paper-server/patches/sources/com/mojang/brigadier/CommandDispatcher.java.patch
index ba95e196e5..ffd3df5ae9 100644
--- a/paper-server/patches/unapplied/com/mojang/brigadier/CommandDispatcher.java.patch
+++ b/paper-server/patches/sources/com/mojang/brigadier/CommandDispatcher.java.patch
@@ -1,14 +1,6 @@
 --- a/com/mojang/brigadier/CommandDispatcher.java
 +++ b/com/mojang/brigadier/CommandDispatcher.java
-@@ -3,6 +3,7 @@
- 
- package com.mojang.brigadier;
- 
-+// CHECKSTYLE:OFF
- import com.mojang.brigadier.builder.LiteralArgumentBuilder;
- import com.mojang.brigadier.context.CommandContext;
- import com.mojang.brigadier.context.CommandContextBuilder;
-@@ -297,15 +298,21 @@
+@@ -297,15 +_,21 @@
          List<ParseResults<S>> potentials = null;
          final int cursor = originalReader.getCursor();
  
@@ -31,7 +23,7 @@
                  } catch (final RuntimeException ex) {
                      throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherParseException().createWithContext(reader, ex.getMessage());
                  }
-@@ -320,6 +327,7 @@
+@@ -320,6 +_,7 @@
                  }
                  errors.put(child, ex);
                  reader.setCursor(cursor);
@@ -39,7 +31,7 @@
                  continue;
              }
  
-@@ -451,7 +459,7 @@
+@@ -451,7 +_,7 @@
      }
  
      private String getSmartUsage(final CommandNode<S> node, final S source, final boolean optional, final boolean deep) {
@@ -48,16 +40,16 @@
              return null;
          }
  
-@@ -465,7 +473,7 @@
-                 final String redirect = node.getRedirect() == this.root ? "..." : "-> " + node.getRedirect().getUsageText();
-                 return self + CommandDispatcher.ARGUMENT_SEPARATOR + redirect;
+@@ -465,7 +_,7 @@
+                 final String redirect = node.getRedirect() == root ? "..." : "-> " + node.getRedirect().getUsageText();
+                 return self + ARGUMENT_SEPARATOR + redirect;
              } else {
 -                final Collection<CommandNode<S>> children = node.getChildren().stream().filter(c -> c.canUse(source)).collect(Collectors.toList());
 +                final Collection<CommandNode<S>> children = node.getChildren().stream().filter(c -> source == null || c.canUse(source)).collect(Collectors.toList()); // Paper
                  if (children.size() == 1) {
-                     final String usage = this.getSmartUsage(children.iterator().next(), source, childOptional, childOptional);
+                     final String usage = getSmartUsage(children.iterator().next(), source, childOptional, childOptional);
                      if (usage != null) {
-@@ -537,10 +545,14 @@
+@@ -537,10 +_,14 @@
          int i = 0;
          for (final CommandNode<S> node : parent.getChildren()) {
              CompletableFuture<Suggestions> future = Suggestions.empty();
diff --git a/paper-server/patches/unapplied/com/mojang/brigadier/builder/ArgumentBuilder.java.patch b/paper-server/patches/sources/com/mojang/brigadier/builder/ArgumentBuilder.java.patch
similarity index 98%
rename from paper-server/patches/unapplied/com/mojang/brigadier/builder/ArgumentBuilder.java.patch
rename to paper-server/patches/sources/com/mojang/brigadier/builder/ArgumentBuilder.java.patch
index 16daaa3a3c..e778d183d1 100644
--- a/paper-server/patches/unapplied/com/mojang/brigadier/builder/ArgumentBuilder.java.patch
+++ b/paper-server/patches/sources/com/mojang/brigadier/builder/ArgumentBuilder.java.patch
@@ -1,6 +1,6 @@
 --- a/com/mojang/brigadier/builder/ArgumentBuilder.java
 +++ b/com/mojang/brigadier/builder/ArgumentBuilder.java
-@@ -14,9 +14,17 @@
+@@ -14,9 +_,17 @@
  import java.util.function.Predicate;
  
  public abstract class ArgumentBuilder<S, T extends ArgumentBuilder<S, T>> {
diff --git a/paper-server/patches/sources/com/mojang/brigadier/exceptions/CommandSyntaxException.java.patch b/paper-server/patches/sources/com/mojang/brigadier/exceptions/CommandSyntaxException.java.patch
index b3bbcc46e5..5a9ff8c8d0 100644
--- a/paper-server/patches/sources/com/mojang/brigadier/exceptions/CommandSyntaxException.java.patch
+++ b/paper-server/patches/sources/com/mojang/brigadier/exceptions/CommandSyntaxException.java.patch
@@ -1,6 +1,6 @@
 --- a/com/mojang/brigadier/exceptions/CommandSyntaxException.java
 +++ b/com/mojang/brigadier/exceptions/CommandSyntaxException.java
-@@ -5,7 +5,7 @@
+@@ -5,7 +_,7 @@
  
  import com.mojang.brigadier.Message;
  
@@ -9,7 +9,7 @@
      public static final int CONTEXT_AMOUNT = 10;
      public static boolean ENABLE_COMMAND_STACK_TRACES = true;
      public static BuiltInExceptionProvider BUILT_IN_EXCEPTIONS = new BuiltInExceptions();
-@@ -73,4 +73,11 @@
+@@ -73,4 +_,11 @@
      public int getCursor() {
          return cursor;
      }
diff --git a/paper-server/patches/unapplied/com/mojang/brigadier/tree/CommandNode.java.patch b/paper-server/patches/sources/com/mojang/brigadier/tree/CommandNode.java.patch
similarity index 64%
rename from paper-server/patches/unapplied/com/mojang/brigadier/tree/CommandNode.java.patch
rename to paper-server/patches/sources/com/mojang/brigadier/tree/CommandNode.java.patch
index 179686de6f..bb97c0d52c 100644
--- a/paper-server/patches/unapplied/com/mojang/brigadier/tree/CommandNode.java.patch
+++ b/paper-server/patches/sources/com/mojang/brigadier/tree/CommandNode.java.patch
@@ -1,26 +1,16 @@
 --- a/com/mojang/brigadier/tree/CommandNode.java
 +++ b/com/mojang/brigadier/tree/CommandNode.java
-@@ -3,6 +3,7 @@
- 
- package com.mojang.brigadier.tree;
- 
-+// CHECKSTYLE:OFF
- import com.mojang.brigadier.AmbiguityConsumer;
- import com.mojang.brigadier.Command;
- import com.mojang.brigadier.RedirectModifier;
-@@ -22,6 +23,7 @@
- import java.util.Set;
- import java.util.concurrent.CompletableFuture;
- import java.util.function.Predicate;
-+import net.minecraft.commands.CommandSourceStack;
- 
- public abstract class CommandNode<S> implements Comparable<CommandNode<S>> {
+@@ -27,11 +_,21 @@
      private final Map<String, CommandNode<S>> children = new LinkedHashMap<>();
-@@ -32,6 +34,16 @@
+     private final Map<String, LiteralCommandNode<S>> literals = new LinkedHashMap<>();
+     private final Map<String, ArgumentCommandNode<S, ?>> arguments = new LinkedHashMap<>();
+-    private final Predicate<S> requirement;
++    public Predicate<S> requirement; // Paper - public-f
+     private final CommandNode<S> redirect;
      private final RedirectModifier<S> modifier;
      private final boolean forks;
      private Command<S> command;
-+    public CommandNode<CommandSourceStack> clientNode; // Paper - Brigadier API
++    public CommandNode<net.minecraft.commands.CommandSourceStack> clientNode; // Paper - Brigadier API
 +    public CommandNode<io.papermc.paper.command.brigadier.CommandSourceStack> unwrappedCached = null; // Paper - Brigadier Command API
 +    public CommandNode<io.papermc.paper.command.brigadier.CommandSourceStack> wrappedCached = null; // Paper - Brigadier Command API
 +    // CraftBukkit start
@@ -33,50 +23,50 @@
  
      protected CommandNode(final Command<S> command, final Predicate<S> requirement, final CommandNode<S> redirect, final RedirectModifier<S> modifier, final boolean forks) {
          this.command = command;
-@@ -61,7 +73,17 @@
-         return this.modifier;
+@@ -61,7 +_,17 @@
+         return modifier;
      }
  
 -    public boolean canUse(final S source) {
 +    // CraftBukkit start
 +    public synchronized boolean canUse(final S source) {
-+        if (source instanceof CommandSourceStack) {
++        if (source instanceof final net.minecraft.commands.CommandSourceStack css) {
 +            try {
-+                ((CommandSourceStack) source).currentCommand.put(Thread.currentThread(), this); // Paper - Thread Safe Vanilla Command permission checking
++                css.currentCommand.put(Thread.currentThread(), this); // Paper - Thread Safe Vanilla Command permission checking
 +                return this.requirement.test(source);
 +            } finally {
-+                ((CommandSourceStack) source).currentCommand.remove(Thread.currentThread()); // Paper - Thread Safe Vanilla Command permission checking
++                css.currentCommand.remove(Thread.currentThread()); // Paper - Thread Safe Vanilla Command permission checking
 +            }
 +        }
 +        // CraftBukkit end
-         return this.requirement.test(source);
+         return requirement.test(source);
      }
  
-@@ -151,6 +173,12 @@
+@@ -151,6 +_,12 @@
      protected abstract String getSortedKey();
  
      public Collection<? extends CommandNode<S>> getRelevantNodes(final StringReader input) {
-+        // Paper start - prioritize mc commands in function parsing
++    // Paper start - prioritize mc commands in function parsing
 +        return this.getRelevantNodes(input, null);
 +    }
 +    @org.jetbrains.annotations.ApiStatus.Internal
 +    public Collection<? extends CommandNode<S>> getRelevantNodes(final StringReader input, final Object source) {
-+        // Paper end - prioritize mc commands in function parsing
-         if (this.literals.size() > 0) {
++     // Paper end - prioritize mc commands in function parsing
+         if (literals.size() > 0) {
              final int cursor = input.getCursor();
              while (input.canRead() && input.peek() != ' ') {
-@@ -158,7 +186,21 @@
+@@ -158,7 +_,21 @@
              }
              final String text = input.getString().substring(cursor, input.getCursor());
              input.setCursor(cursor);
--            final LiteralCommandNode<S> literal = this.literals.get(text);
+-            final LiteralCommandNode<S> literal = literals.get(text);
 +            // Paper start - prioritize mc commands in function parsing
 +            LiteralCommandNode<S> literal = null;
-+            if (source instanceof CommandSourceStack css && css.source == net.minecraft.commands.CommandSource.NULL) {
++            if (source instanceof net.minecraft.commands.CommandSourceStack css && css.source == net.minecraft.commands.CommandSource.NULL) {
 +                if (!text.contains(":")) {
 +                    literal = this.literals.get("minecraft:" + text);
 +                }
-+            } else if (source instanceof CommandSourceStack css && css.source instanceof net.minecraft.world.level.BaseCommandBlock) {
++            } else if (source instanceof net.minecraft.commands.CommandSourceStack css && css.source instanceof net.minecraft.world.level.BaseCommandBlock) {
 +                if (css.getServer().server.getCommandBlockOverride(text) && !text.contains(":")) {
 +                    literal = this.literals.get("minecraft:" + text);
 +                }
@@ -88,7 +78,7 @@
              if (literal != null) {
                  return Collections.singleton(literal);
              } else {
-@@ -183,4 +225,11 @@
+@@ -183,4 +_,11 @@
      }
  
      public abstract Collection<String> getExamples();
diff --git a/paper-server/patches/unapplied/com/mojang/brigadier/tree/LiteralCommandNode.java.patch b/paper-server/patches/sources/com/mojang/brigadier/tree/LiteralCommandNode.java.patch
similarity index 97%
rename from paper-server/patches/unapplied/com/mojang/brigadier/tree/LiteralCommandNode.java.patch
rename to paper-server/patches/sources/com/mojang/brigadier/tree/LiteralCommandNode.java.patch
index 15d0b7b976..f08f887a30 100644
--- a/paper-server/patches/unapplied/com/mojang/brigadier/tree/LiteralCommandNode.java.patch
+++ b/paper-server/patches/sources/com/mojang/brigadier/tree/LiteralCommandNode.java.patch
@@ -1,6 +1,6 @@
 --- a/com/mojang/brigadier/tree/LiteralCommandNode.java
 +++ b/com/mojang/brigadier/tree/LiteralCommandNode.java
-@@ -23,11 +23,19 @@
+@@ -23,11 +_,19 @@
  public class LiteralCommandNode<S> extends CommandNode<S> {
      private final String literal;
      private final String literalLowerCase;
@@ -20,7 +20,7 @@
      }
  
      public String getLiteral() {
-@@ -42,7 +50,12 @@
+@@ -42,7 +_,12 @@
      @Override
      public void parse(final StringReader reader, final CommandContextBuilder<S> contextBuilder) throws CommandSyntaxException {
          final int start = reader.getCursor();
@@ -34,7 +34,7 @@
          if (end > -1) {
              contextBuilder.withNode(this, StringRange.between(start, end));
              return;
-@@ -51,7 +64,10 @@
+@@ -51,7 +_,10 @@
          throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.literalIncorrect().createWithContext(reader, literal);
      }
  
@@ -46,7 +46,7 @@
          final int start = reader.getCursor();
          if (reader.canRead(literal.length())) {
              final int end = start + literal.length();
-@@ -78,7 +94,7 @@
+@@ -78,7 +_,7 @@
  
      @Override
      public boolean isValidInput(final String input) {
diff --git a/paper-server/patches/unapplied/com/mojang/datafixers/DataFixerBuilder.java.patch b/paper-server/patches/sources/com/mojang/datafixers/DataFixerBuilder.java.patch
similarity index 80%
rename from paper-server/patches/unapplied/com/mojang/datafixers/DataFixerBuilder.java.patch
rename to paper-server/patches/sources/com/mojang/datafixers/DataFixerBuilder.java.patch
index 6d1be183b0..020d6ed859 100644
--- a/paper-server/patches/unapplied/com/mojang/datafixers/DataFixerBuilder.java.patch
+++ b/paper-server/patches/sources/com/mojang/datafixers/DataFixerBuilder.java.patch
@@ -1,17 +1,17 @@
 --- a/com/mojang/datafixers/DataFixerBuilder.java
 +++ b/com/mojang/datafixers/DataFixerBuilder.java
-@@ -29,8 +29,10 @@
+@@ -29,8 +_,10 @@
      private final Int2ObjectSortedMap<Schema> schemas = new Int2ObjectAVLTreeMap<>();
      private final List<DataFix> globalList = new ArrayList<>();
      private final IntSortedSet fixerVersions = new IntAVLTreeSet();
 +    private final int minDataFixPrecacheVersion; // Paper - Perf: Cache DataFixerUpper Rewrite Rules on demand
  
      public DataFixerBuilder(final int dataVersion) {
-+        minDataFixPrecacheVersion = Integer.getInteger("Paper.minPrecachedDatafixVersion", dataVersion+1) * 10; // Paper - Perf: default to precache nothing - mojang stores versions * 10 to allow for 'sub versions'
++        this.minDataFixPrecacheVersion = Integer.getInteger("Paper.minPrecachedDatafixVersion", dataVersion + 1) * 10; // Paper - Perf: default to precache nothing - Mojang stores versions * 10 to allow for 'sub versions'
          this.dataVersion = dataVersion;
      }
  
-@@ -88,6 +90,7 @@
+@@ -88,6 +_,7 @@
              final IntIterator iterator = fixerUpper.fixerVersions().iterator();
              while (iterator.hasNext()) {
                  final int versionKey = iterator.nextInt();
diff --git a/paper-server/patches/unapplied/com/mojang/datafixers/util/Either.java.patch b/paper-server/patches/sources/com/mojang/datafixers/util/Either.java.patch
similarity index 94%
rename from paper-server/patches/unapplied/com/mojang/datafixers/util/Either.java.patch
rename to paper-server/patches/sources/com/mojang/datafixers/util/Either.java.patch
index 810603f8a3..7f682f9002 100644
--- a/paper-server/patches/unapplied/com/mojang/datafixers/util/Either.java.patch
+++ b/paper-server/patches/sources/com/mojang/datafixers/util/Either.java.patch
@@ -1,6 +1,6 @@
 --- a/com/mojang/datafixers/util/Either.java
 +++ b/com/mojang/datafixers/util/Either.java
-@@ -22,7 +22,7 @@
+@@ -22,7 +_,7 @@
      }
  
      private static final class Left<L, R> extends Either<L, R> {
@@ -9,7 +9,7 @@
  
          public Left(final L value) {
              this.value = value;
-@@ -51,7 +51,7 @@
+@@ -51,7 +_,7 @@
  
          @Override
          public Optional<L> left() {
@@ -18,7 +18,7 @@
          }
  
          @Override
-@@ -83,7 +83,7 @@
+@@ -83,7 +_,7 @@
      }
  
      private static final class Right<L, R> extends Either<L, R> {
@@ -27,7 +27,7 @@
  
          public Right(final R value) {
              this.value = value;
-@@ -117,7 +117,7 @@
+@@ -117,7 +_,7 @@
  
          @Override
          public Optional<R> right() {
diff --git a/paper-server/patches/unapplied/com/mojang/logging/LogUtils.java.patch b/paper-server/patches/sources/com/mojang/logging/LogUtils.java.patch
similarity index 95%
rename from paper-server/patches/unapplied/com/mojang/logging/LogUtils.java.patch
rename to paper-server/patches/sources/com/mojang/logging/LogUtils.java.patch
index 1cabed3cc3..b05ea2c1ed 100644
--- a/paper-server/patches/unapplied/com/mojang/logging/LogUtils.java.patch
+++ b/paper-server/patches/sources/com/mojang/logging/LogUtils.java.patch
@@ -1,6 +1,6 @@
 --- a/com/mojang/logging/LogUtils.java
 +++ b/com/mojang/logging/LogUtils.java
-@@ -61,4 +61,9 @@
+@@ -61,4 +_,9 @@
      public static Logger getLogger() {
          return LoggerFactory.getLogger(STACK_WALKER.getCallerClass());
      }
diff --git a/paper-server/patches/unapplied/com/mojang/math/OctahedralGroup.java.patch b/paper-server/patches/sources/com/mojang/math/OctahedralGroup.java.patch
similarity index 74%
rename from paper-server/patches/unapplied/com/mojang/math/OctahedralGroup.java.patch
rename to paper-server/patches/sources/com/mojang/math/OctahedralGroup.java.patch
index 64c7d5d058..671c030fac 100644
--- a/paper-server/patches/unapplied/com/mojang/math/OctahedralGroup.java.patch
+++ b/paper-server/patches/sources/com/mojang/math/OctahedralGroup.java.patch
@@ -1,14 +1,14 @@
 --- a/com/mojang/math/OctahedralGroup.java
 +++ b/com/mojang/math/OctahedralGroup.java
-@@ -110,6 +110,7 @@
-         this.permutation = axisTransformation;
-         this.transformation = new Matrix3f().scaling(flipX ? -1.0F : 1.0F, flipY ? -1.0F : 1.0F, flipZ ? -1.0F : 1.0F);
-         this.transformation.mul(axisTransformation.transformation());
+@@ -111,6 +_,7 @@
+         this.permutation = permutation;
+         this.transformation = new Matrix3f().scaling(invertX ? -1.0F : 1.0F, invertY ? -1.0F : 1.0F, invertZ ? -1.0F : 1.0F);
+         this.transformation.mul(permutation.transformation());
 +        this.initializeRotationDirections(); // Paper - Avoid Lazy Initialization for Enum Fields
      }
  
      private BooleanList packInversions() {
-@@ -138,7 +139,7 @@
+@@ -139,7 +_,7 @@
          return this.name;
      }
  
@@ -17,7 +17,7 @@
          if (this.rotatedDirections == null) {
              this.rotatedDirections = Maps.newEnumMap(Direction.class);
              Direction.Axis[] axiss = Direction.Axis.values();
-@@ -153,6 +154,10 @@
+@@ -154,6 +_,10 @@
              }
          }
  
diff --git a/paper-server/patches/unapplied/com/mojang/serialization/Dynamic.java.patch b/paper-server/patches/sources/com/mojang/serialization/Dynamic.java.patch
similarity index 96%
rename from paper-server/patches/unapplied/com/mojang/serialization/Dynamic.java.patch
rename to paper-server/patches/sources/com/mojang/serialization/Dynamic.java.patch
index cdd24c2f21..98fe692497 100644
--- a/paper-server/patches/unapplied/com/mojang/serialization/Dynamic.java.patch
+++ b/paper-server/patches/sources/com/mojang/serialization/Dynamic.java.patch
@@ -1,6 +1,6 @@
 --- a/com/mojang/serialization/Dynamic.java
 +++ b/com/mojang/serialization/Dynamic.java
-@@ -19,6 +19,7 @@
+@@ -19,6 +_,7 @@
  
  @SuppressWarnings("unused")
  public class Dynamic<T> extends DynamicLike<T> {
@@ -8,7 +8,7 @@
      private final T value;
  
      public Dynamic(final DynamicOps<T> ops) {
-@@ -120,7 +121,7 @@
+@@ -120,7 +_,7 @@
          return new OptionalDynamic<>(ops, ops.getMap(value).flatMap(m -> {
              final T value = m.get(key);
              if (value == null) {
diff --git a/paper-server/patches/sources/io/papermc/paper/FeatureHooks.java.patch b/paper-server/patches/sources/io/papermc/paper/FeatureHooks.java.patch
new file mode 100644
index 0000000000..f9337524bb
--- /dev/null
+++ b/paper-server/patches/sources/io/papermc/paper/FeatureHooks.java.patch
@@ -0,0 +1,236 @@
+--- /dev/null
++++ b/io/papermc/paper/FeatureHooks.java
+@@ -1,0 +_,233 @@
++package io.papermc.paper;
++
++import io.papermc.paper.command.PaperSubcommand;
++import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
++import it.unimi.dsi.fastutil.longs.LongSet;
++import it.unimi.dsi.fastutil.longs.LongSets;
++import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
++import it.unimi.dsi.fastutil.objects.ObjectSet;
++import it.unimi.dsi.fastutil.objects.ObjectSets;
++import java.util.List;
++import java.util.Map;
++import java.util.Set;
++import net.minecraft.core.Registry;
++import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket;
++import net.minecraft.server.level.ServerPlayer;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.entity.monster.Spider;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.level.Level;
++import net.minecraft.world.level.biome.Biome;
++import net.minecraft.world.level.block.Block;
++import net.minecraft.world.level.block.Blocks;
++import net.minecraft.world.level.block.state.BlockState;
++import net.minecraft.world.level.chunk.LevelChunk;
++import net.minecraft.world.level.chunk.LevelChunkSection;
++import net.minecraft.world.level.chunk.PalettedContainer;
++import org.bukkit.Chunk;
++import org.bukkit.World;
++
++public final class FeatureHooks {
++
++    public static void initChunkTaskScheduler(final boolean useParallelGen) {
++    }
++
++    public static void registerPaperCommands(final Map<Set<String>, PaperSubcommand> commands) {
++    }
++
++    public static LevelChunkSection createSection(final Registry<Biome> biomeRegistry, final Level level, final ChunkPos chunkPos, final int chunkSection) {
++        return new LevelChunkSection(biomeRegistry);
++    }
++
++    public static void sendChunkRefreshPackets(final List<ServerPlayer> playersInRange, final LevelChunk chunk) {
++        final ClientboundLevelChunkWithLightPacket refreshPacket = new ClientboundLevelChunkWithLightPacket(chunk, chunk.level.getLightEngine(), null, null);
++        for (final ServerPlayer player : playersInRange) {
++            if (player.connection == null) continue;
++
++            player.connection.send(refreshPacket);
++        }
++    }
++
++    public static PalettedContainer<BlockState> emptyPalettedBlockContainer() {
++        return new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES);
++    }
++
++    public static Set<Long> getSentChunkKeys(final ServerPlayer player) {
++        final LongSet keys = new LongOpenHashSet();
++        player.getChunkTrackingView().forEach(pos -> keys.add(pos.longKey));
++        return LongSets.unmodifiable(keys);
++    }
++
++    public static Set<Chunk> getSentChunks(final ServerPlayer player) {
++        final ObjectSet<Chunk> chunks = new ObjectOpenHashSet<>();
++        final World world = player.serverLevel().getWorld();
++        player.getChunkTrackingView().forEach(pos -> {
++            final org.bukkit.Chunk chunk = world.getChunkAt(pos.longKey);
++            chunks.add(chunk);
++        });
++        return ObjectSets.unmodifiable(chunks);
++    }
++
++    public static boolean isChunkSent(final ServerPlayer player, final long chunkKey) {
++        return player.getChunkTrackingView().contains(new ChunkPos(chunkKey));
++    }
++
++    public static boolean isSpiderCollidingWithWorldBorder(final Spider spider) {
++        return true; // ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isCollidingWithBorder(spider.level().getWorldBorder(), spider.getBoundingBox().inflate(ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON))
++    }
++
++    public static void dumpAllChunkLoadInfo(net.minecraft.server.MinecraftServer server, boolean isLongTimeout) {
++    }
++
++    private static void dumpEntity(final Entity entity) {
++    }
++
++    public static org.bukkit.entity.Entity[] getChunkEntities(net.minecraft.server.level.ServerLevel world, int chunkX, int chunkZ) {
++        world.getChunk(chunkX, chunkZ); // ensure full loaded
++
++        net.minecraft.world.level.entity.PersistentEntitySectionManager<net.minecraft.world.entity.Entity> entityManager = world.entityManager;
++        long pair = ChunkPos.asLong(chunkX, chunkZ);
++
++        if (entityManager.areEntitiesLoaded(pair)) {
++            return entityManager.getEntities(new ChunkPos(chunkX, chunkZ)).stream()
++                .map(net.minecraft.world.entity.Entity::getBukkitEntity)
++                .filter(java.util.Objects::nonNull).toArray(org.bukkit.entity.Entity[]::new);
++        }
++
++        entityManager.ensureChunkQueuedForLoad(pair); // Start entity loading
++
++        // SPIGOT-6772: Use entity mailbox and re-schedule entities if they get unloaded
++        net.minecraft.util.thread.ConsecutiveExecutor mailbox = ((net.minecraft.world.level.chunk.storage.EntityStorage) entityManager.permanentStorage).entityDeserializerQueue;
++        java.util.function.BooleanSupplier supplier = () -> {
++            // only execute inbox if our entities are not present
++            if (entityManager.areEntitiesLoaded(pair)) {
++                return true;
++            }
++
++            if (!entityManager.isPending(pair)) {
++                // Our entities got unloaded, this should normally not happen.
++                entityManager.ensureChunkQueuedForLoad(pair); // Re-start entity loading
++            }
++
++            // tick loading inbox, which loads the created entities to the world
++            // (if present)
++            entityManager.tick();
++            // check if our entities are loaded
++            return entityManager.areEntitiesLoaded(pair);
++        };
++
++        // now we wait until the entities are loaded,
++        // the converting from NBT to entity object is done on the main Thread which is why we wait
++        while (!supplier.getAsBoolean()) {
++            if (mailbox.size() != 0) {
++                mailbox.run();
++            } else {
++                Thread.yield();
++                java.util.concurrent.locks.LockSupport.parkNanos("waiting for entity loading", 100000L);
++            }
++        }
++
++        return entityManager.getEntities(new ChunkPos(chunkX, chunkZ)).stream()
++            .map(net.minecraft.world.entity.Entity::getBukkitEntity)
++            .filter(java.util.Objects::nonNull).toArray(org.bukkit.entity.Entity[]::new);
++    }
++
++    public static java.util.Collection<org.bukkit.plugin.Plugin> getPluginChunkTickets(net.minecraft.server.level.ServerLevel world,
++                                                                                       int x, int z) {
++        net.minecraft.server.level.DistanceManager chunkDistanceManager = world.getChunkSource().chunkMap.distanceManager;
++        net.minecraft.util.SortedArraySet<net.minecraft.server.level.Ticket<?>> tickets = chunkDistanceManager.tickets.get(ChunkPos.asLong(x, z));
++
++        if (tickets == null) {
++            return java.util.Collections.emptyList();
++        }
++
++        com.google.common.collect.ImmutableList.Builder<org.bukkit.plugin.Plugin> ret = com.google.common.collect.ImmutableList.builder();
++        for (net.minecraft.server.level.Ticket<?> ticket : tickets) {
++            if (ticket.getType() == net.minecraft.server.level.TicketType.PLUGIN_TICKET) {
++                ret.add((org.bukkit.plugin.Plugin) ticket.key);
++            }
++        }
++
++        return ret.build();
++    }
++
++    public static Map<org.bukkit.plugin.Plugin, java.util.Collection<org.bukkit.Chunk>> getPluginChunkTickets(net.minecraft.server.level.ServerLevel world) {
++        Map<org.bukkit.plugin.Plugin, com.google.common.collect.ImmutableList.Builder<Chunk>> ret = new HashMap<>();
++        net.minecraft.server.level.DistanceManager chunkDistanceManager = world.getChunkSource().chunkMap.distanceManager;
++
++        for (it.unimi.dsi.fastutil.longs.Long2ObjectMap.Entry<net.minecraft.util.SortedArraySet<net.minecraft.server.level.Ticket<?>>> chunkTickets : chunkDistanceManager.tickets.long2ObjectEntrySet()) {
++            long chunkKey = chunkTickets.getLongKey();
++            net.minecraft.util.SortedArraySet<net.minecraft.server.level.Ticket<?>> tickets = chunkTickets.getValue();
++
++            org.bukkit.Chunk chunk = null;
++            for (net.minecraft.server.level.Ticket<?> ticket : tickets) {
++                if (ticket.getType() != net.minecraft.server.level.TicketType.PLUGIN_TICKET) {
++                    continue;
++                }
++
++                if (chunk == null) {
++                    chunk = world.getWorld().getChunkAt(ChunkPos.getX(chunkKey), ChunkPos.getZ(chunkKey));
++                }
++
++                ret.computeIfAbsent((org.bukkit.plugin.Plugin) ticket.key, (key) -> com.google.common.collect.ImmutableList.builder()).add(chunk);
++            }
++        }
++
++        return ret.entrySet().stream().collect(com.google.common.collect.ImmutableMap.toImmutableMap(Map.Entry::getKey, (entry) -> entry.getValue().build()));
++    }
++
++    public static int getViewDistance(net.minecraft.server.level.ServerLevel world) {
++        return world.getChunkSource().chunkMap.serverViewDistance;
++    }
++
++    public static int getSimulationDistance(net.minecraft.server.level.ServerLevel world) {
++        return world.getChunkSource().chunkMap.getDistanceManager().simulationDistance;
++    }
++
++    public static int getSendViewDistance(net.minecraft.server.level.ServerLevel world) {
++        return getViewDistance(world);
++    }
++
++    public static void setViewDistance(net.minecraft.server.level.ServerLevel world, int distance) {
++        if (distance < 2 || distance > 32) {
++            throw new IllegalArgumentException("View distance " + distance + " is out of range of [2, 32]");
++        }
++        world.chunkSource.chunkMap.setServerViewDistance(distance);
++    }
++
++    public static void setSimulationDistance(net.minecraft.server.level.ServerLevel world, int distance) {
++        if (distance < 2 || distance > 32) {
++            throw new IllegalArgumentException("Simulation distance " + distance + " is out of range of [2, 32]");
++        }
++        world.chunkSource.chunkMap.distanceManager.updateSimulationDistance(distance);
++    }
++
++    public static void setSendViewDistance(net.minecraft.server.level.ServerLevel world, int distance) {
++        throw new UnsupportedOperationException("Not implemented yet");
++    }
++
++    public static void tickEntityManager(net.minecraft.server.level.ServerLevel world) {
++        world.entityManager.tick();
++    }
++
++    public static void closeEntityManager(net.minecraft.server.level.ServerLevel world, boolean save) {
++        world.entityManager.close(save);
++    }
++
++    public static java.util.concurrent.Executor getWorldgenExecutor() {
++        return net.minecraft.Util.backgroundExecutor();
++    }
++
++    public static void setViewDistance(ServerPlayer player, int distance) {
++        throw new UnsupportedOperationException("Not implemented yet");
++    }
++
++    public static void setSimulationDistance(ServerPlayer player, int distance) {
++        throw new UnsupportedOperationException("Not implemented yet");
++    }
++
++    public static void setSendViewDistance(ServerPlayer player, int distance) {
++        throw new UnsupportedOperationException("Not implemented yet");
++    }
++
++}
diff --git a/paper-server/patches/unapplied/net/minecraft/ChatFormatting.java.patch b/paper-server/patches/sources/net/minecraft/ChatFormatting.java.patch
similarity index 60%
rename from paper-server/patches/unapplied/net/minecraft/ChatFormatting.java.patch
rename to paper-server/patches/sources/net/minecraft/ChatFormatting.java.patch
index eb700c93b5..658c5707e3 100644
--- a/paper-server/patches/unapplied/net/minecraft/ChatFormatting.java.patch
+++ b/paper-server/patches/sources/net/minecraft/ChatFormatting.java.patch
@@ -1,11 +1,12 @@
 --- a/net/minecraft/ChatFormatting.java
 +++ b/net/minecraft/ChatFormatting.java
-@@ -112,6 +112,18 @@
-         return name == null ? null : FORMATTING_BY_NAME.get(cleanName(name));
+@@ -112,6 +_,19 @@
+         return friendlyName == null ? null : FORMATTING_BY_NAME.get(cleanName(friendlyName));
      }
  
 +    // Paper start - add method to get by hex value
-+    @Nullable public static ChatFormatting getByHexValue(int i) {
++    @Nullable
++    public static ChatFormatting getByHexValue(int i) {
 +        for (ChatFormatting value : values()) {
 +            if (value.getColor() != null && value.getColor() == i) {
 +                return value;
@@ -17,5 +18,5 @@
 +    // Paper end - add method to get by hex value
 +
      @Nullable
-     public static ChatFormatting getById(int colorIndex) {
-         if (colorIndex < 0) {
+     public static ChatFormatting getById(int index) {
+         if (index < 0) {
diff --git a/paper-server/patches/unapplied/net/minecraft/CrashReport.java.patch b/paper-server/patches/sources/net/minecraft/CrashReport.java.patch
similarity index 54%
rename from paper-server/patches/unapplied/net/minecraft/CrashReport.java.patch
rename to paper-server/patches/sources/net/minecraft/CrashReport.java.patch
index 0b8863cd41..d9c00ba981 100644
--- a/paper-server/patches/unapplied/net/minecraft/CrashReport.java.patch
+++ b/paper-server/patches/sources/net/minecraft/CrashReport.java.patch
@@ -1,22 +1,22 @@
 --- a/net/minecraft/CrashReport.java
 +++ b/net/minecraft/CrashReport.java
-@@ -34,8 +34,10 @@
+@@ -32,8 +_,10 @@
      private final SystemReport systemReport = new SystemReport();
  
-     public CrashReport(String message, Throwable cause) {
-+        io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateThrowable(cause); // Paper
-         this.title = message;
-         this.exception = cause;
+     public CrashReport(String title, Throwable exception) {
++        io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateThrowable(exception); // Paper
+         this.title = title;
+         this.exception = exception;
 +        this.systemReport.setDetail("CraftBukkit Information", new org.bukkit.craftbukkit.CraftCrashReport()); // CraftBukkit
      }
  
      public String getTitle() {
-@@ -251,7 +253,7 @@
+@@ -218,7 +_,7 @@
      }
  
      public static void preload() {
 -        MemoryReserve.allocate();
-+        // MemoryReserve.allocate(); // Paper - Disable memory reserve allocating
-         (new CrashReport("Don't panic!", new Throwable())).getFriendlyReport(ReportType.CRASH);
++        //MemoryReserve.allocate(); // Paper - Disable memory reserve allocating
+         new CrashReport("Don't panic!", new Throwable()).getFriendlyReport(ReportType.CRASH);
      }
  }
diff --git a/paper-server/patches/unapplied/net/minecraft/CrashReportCategory.java.patch b/paper-server/patches/sources/net/minecraft/CrashReportCategory.java.patch
similarity index 55%
rename from paper-server/patches/unapplied/net/minecraft/CrashReportCategory.java.patch
rename to paper-server/patches/sources/net/minecraft/CrashReportCategory.java.patch
index 23f5297d2e..4120655a29 100644
--- a/paper-server/patches/unapplied/net/minecraft/CrashReportCategory.java.patch
+++ b/paper-server/patches/sources/net/minecraft/CrashReportCategory.java.patch
@@ -1,9 +1,9 @@
 --- a/net/minecraft/CrashReportCategory.java
 +++ b/net/minecraft/CrashReportCategory.java
-@@ -110,6 +110,7 @@
+@@ -138,6 +_,7 @@
          } else {
-             this.stackTrace = new StackTraceElement[stackTraceElements.length - 3 - ignoredCallCount];
-             System.arraycopy(stackTraceElements, 3 + ignoredCallCount, this.stackTrace, 0, this.stackTrace.length);
+             this.stackTrace = new StackTraceElement[stackTrace.length - 3 - size];
+             System.arraycopy(stackTrace, 3 + size, this.stackTrace, 0, this.stackTrace.length);
 +            this.stackTrace = io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateStacktrace(this.stackTrace); // Paper
              return this.stackTrace.length;
          }
diff --git a/paper-server/patches/unapplied/net/minecraft/Util.java.patch b/paper-server/patches/sources/net/minecraft/Util.java.patch
similarity index 73%
rename from paper-server/patches/unapplied/net/minecraft/Util.java.patch
rename to paper-server/patches/sources/net/minecraft/Util.java.patch
index 378b82b378..ebf86ad6c2 100644
--- a/paper-server/patches/unapplied/net/minecraft/Util.java.patch
+++ b/paper-server/patches/sources/net/minecraft/Util.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/Util.java
 +++ b/net/minecraft/Util.java
-@@ -92,9 +92,26 @@
+@@ -92,9 +_,26 @@
      private static final int DEFAULT_MAX_THREADS = 255;
      private static final int DEFAULT_SAFE_FILE_OPERATION_RETRIES = 10;
      private static final String MAX_THREADS_SYSTEM_PROPERTY = "max.bg.threads";
@@ -28,7 +28,15 @@
      private static final DateTimeFormatter FILENAME_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss", Locale.ROOT);
      public static final int LINEAR_LOOKUP_THRESHOLD = 8;
      private static final Set<String> ALLOWED_UNTRUSTED_LINK_PROTOCOLS = Set.of("http", "https");
-@@ -136,7 +153,7 @@
+@@ -113,6 +_,7 @@
+         .findFirst()
+         .orElseThrow(() -> new IllegalStateException("No jar file system provider found"));
+     private static Consumer<String> thePauser = string -> {};
++    public static final double COLLISION_EPSILON = 1.0E-7; // Paper - Check distance in entity interactions
+ 
+     public static <K, V> Collector<Entry<? extends K, ? extends V>, ?, Map<K, V>> toMap() {
+         return Collectors.toMap(Entry::getKey, Entry::getValue);
+@@ -135,7 +_,7 @@
      }
  
      public static long getNanos() {
@@ -37,36 +45,31 @@
      }
  
      public static long getEpochMillis() {
-@@ -147,15 +164,16 @@
+@@ -146,9 +_,10 @@
          return FILENAME_DATE_TIME_FORMATTER.format(ZonedDateTime.now());
      }
  
 -    private static TracingExecutor makeExecutor(String name) {
 +    private static TracingExecutor makeExecutor(String name, final int priorityModifier) { // Paper - Perf: add priority
          int i = maxAllowedExecutorThreads();
--        ExecutorService executorService;
+-        ExecutorService directExecutorService;
 +        // Paper start - Perf: use simpler thread pool that allows 1 thread and reduce worldgen thread worker count for low core count CPUs
-+        final ExecutorService executorService;
++        final ExecutorService directExecutorService;
          if (i <= 0) {
-             executorService = MoreExecutors.newDirectExecutorService();
+             directExecutorService = MoreExecutors.newDirectExecutorService();
          } else {
--            AtomicInteger atomicInteger = new AtomicInteger(1);
--            executorService = new ForkJoinPool(i, pool -> {
--                final String string2 = "Worker-" + name + "-" + atomicInteger.getAndIncrement();
-+            executorService = Executors.newFixedThreadPool(i, target -> new io.papermc.paper.util.ServerWorkerThread(target, name, priorityModifier));
-+        }
-+        /*      final String string2 = "Worker-" + name + "-" + atomicInteger.getAndIncrement();
-                 ForkJoinWorkerThread forkJoinWorkerThread = new ForkJoinWorkerThread(pool) {
-                     @Override
-                     protected void onStart() {
-@@ -177,13 +195,26 @@
-                 forkJoinWorkerThread.setName(string2);
+@@ -173,16 +_,30 @@
+                         super.onTermination(throwOnTermination);
+                     }
+                 };
++                forkJoinWorkerThread.setPriority(Thread.NORM_PRIORITY + priorityModifier); // Paper - Deprioritize over main
+                 forkJoinWorkerThread.setName(string);
                  return forkJoinWorkerThread;
-             }, Util::onThreadException, true);
--        }
-+        }*/
+-            }, Util::onThreadException, true);
++            }, Util::onThreadException, true, 0, Integer.MAX_VALUE, 1, null, 365, TimeUnit.DAYS); // Paper - do not expire threads
+         }
  
-         return new TracingExecutor(executorService);
+         return new TracingExecutor(directExecutorService);
      }
  
      public static int maxAllowedExecutorThreads() {
@@ -88,15 +91,10 @@
      }
  
      private static int getMaxThreads() {
-@@ -229,10 +260,25 @@
-             TracyClient.setThreadName(string2, namePrefix.hashCode());
-             thread.setName(string2);
-             thread.setDaemon(daemon);
-+            thread.setUncaughtExceptionHandler(Util::onThreadException);
-+            return thread;
-+        }));
-+    }
-+
+@@ -233,6 +_,21 @@
+         }));
+     }
+ 
 +    // Paper start - Separate dimension data IO pool
 +    private static TracingExecutor makeExtraIoExecutor(String namePrefix) {
 +        AtomicInteger atomicInteger = new AtomicInteger(1);
@@ -106,24 +104,16 @@
 +            TracyClient.setThreadName(string2, namePrefix.hashCode());
 +            thread.setName(string2);
 +            thread.setDaemon(false);
-             thread.setUncaughtExceptionHandler(Util::onThreadException);
-             return thread;
-         }));
-     }
++            thread.setUncaughtExceptionHandler(Util::onThreadException);
++            return thread;
++        }));
++    }
 +    // Paper end - Separate dimension data IO pool
- 
-     public static void throwAsRuntime(Throwable t) {
-         throw t instanceof RuntimeException ? (RuntimeException)t : new RuntimeException(t);
-@@ -537,7 +583,7 @@
-     public static <K extends Enum<K>, V> EnumMap<K, V> makeEnumMap(Class<K> enumClass, Function<K, V> mapper) {
-         EnumMap<K, V> enumMap = new EnumMap<>(enumClass);
- 
--        for (K enum_ : (Enum[])enumClass.getEnumConstants()) {
-+        for (K enum_ : enumClass.getEnumConstants()) { // Paper - decompile error
-             enumMap.put(enum_, mapper.apply(enum_));
-         }
- 
-@@ -1057,16 +1103,7 @@
++
+     public static void throwAsRuntime(Throwable throwable) {
+         throw throwable instanceof RuntimeException ? (RuntimeException)throwable : new RuntimeException(throwable);
+     }
+@@ -1075,16 +_,7 @@
          }
  
          public void openUri(URI uri) {
diff --git a/paper-server/patches/sources/net/minecraft/advancements/AdvancementHolder.java.patch b/paper-server/patches/sources/net/minecraft/advancements/AdvancementHolder.java.patch
new file mode 100644
index 0000000000..6450499bcb
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/advancements/AdvancementHolder.java.patch
@@ -0,0 +1,13 @@
+--- a/net/minecraft/advancements/AdvancementHolder.java
++++ b/net/minecraft/advancements/AdvancementHolder.java
+@@ -26,4 +_,10 @@
+     public String toString() {
+         return this.id.toString();
+     }
++
++    // CraftBukkit start
++    public final org.bukkit.advancement.Advancement toBukkit() {
++        return new org.bukkit.craftbukkit.advancement.CraftAdvancement(this);
++    }
++    // CraftBukkit end
+ }
diff --git a/paper-server/patches/sources/net/minecraft/advancements/AdvancementTree.java.patch b/paper-server/patches/sources/net/minecraft/advancements/AdvancementTree.java.patch
new file mode 100644
index 0000000000..aeb4408967
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/advancements/AdvancementTree.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/advancements/AdvancementTree.java
++++ b/net/minecraft/advancements/AdvancementTree.java
+@@ -26,7 +_,7 @@
+             this.remove(advancementNode);
+         }
+ 
+-        LOGGER.info("Forgot about advancement {}", node.holder());
++        LOGGER.debug("Forgot about advancement {}", node.holder()); // Paper - Improve logging and errors
+         this.nodes.remove(node.holder().id());
+         if (node.parent() == null) {
+             this.roots.remove(node);
+@@ -62,7 +_,7 @@
+             }
+         }
+ 
+-        LOGGER.info("Loaded {} advancements", this.nodes.size());
++        // LOGGER.info("Loaded {} advancements", this.nodes.size()); // CraftBukkit - moved to AdvancementDataWorld#reload // Paper - Improve logging and errors; you say it was moved... but it wasn't :) it should be moved however, since this is called when the API creates an advancement
+     }
+ 
+     private boolean tryInsert(AdvancementHolder advancement) {
diff --git a/paper-server/patches/unapplied/net/minecraft/advancements/DisplayInfo.java.patch b/paper-server/patches/sources/net/minecraft/advancements/DisplayInfo.java.patch
similarity index 95%
rename from paper-server/patches/unapplied/net/minecraft/advancements/DisplayInfo.java.patch
rename to paper-server/patches/sources/net/minecraft/advancements/DisplayInfo.java.patch
index a0fdd33f43..c7956c2de5 100644
--- a/paper-server/patches/unapplied/net/minecraft/advancements/DisplayInfo.java.patch
+++ b/paper-server/patches/sources/net/minecraft/advancements/DisplayInfo.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/advancements/DisplayInfo.java
 +++ b/net/minecraft/advancements/DisplayInfo.java
-@@ -37,6 +37,7 @@
+@@ -37,6 +_,7 @@
      private final boolean hidden;
      private float x;
      private float y;
diff --git a/paper-server/patches/unapplied/net/minecraft/advancements/critereon/LocationPredicate.java.patch b/paper-server/patches/sources/net/minecraft/advancements/critereon/LocationPredicate.java.patch
similarity index 72%
rename from paper-server/patches/unapplied/net/minecraft/advancements/critereon/LocationPredicate.java.patch
rename to paper-server/patches/sources/net/minecraft/advancements/critereon/LocationPredicate.java.patch
index 7f7873fc42..e43dabe617 100644
--- a/paper-server/patches/unapplied/net/minecraft/advancements/critereon/LocationPredicate.java.patch
+++ b/paper-server/patches/sources/net/minecraft/advancements/critereon/LocationPredicate.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/advancements/critereon/LocationPredicate.java
 +++ b/net/minecraft/advancements/critereon/LocationPredicate.java
-@@ -44,7 +44,7 @@
-     public boolean matches(ServerLevel world, double x, double y, double z) {
+@@ -44,7 +_,7 @@
+     public boolean matches(ServerLevel level, double x, double y, double z) {
          if (this.position.isPresent() && !this.position.get().matches(x, y, z)) {
              return false;
--        } else if (this.dimension.isPresent() && this.dimension.get() != world.dimension()) {
-+        } else if (this.dimension.isPresent() && this.dimension.get() != (io.papermc.paper.configuration.GlobalConfiguration.get().misc.strictAdvancementDimensionCheck ? world.dimension() : org.bukkit.craftbukkit.util.CraftDimensionUtil.getMainDimensionKey(world))) { // Paper - Add option for strict advancement dimension checks
+-        } else if (this.dimension.isPresent() && this.dimension.get() != level.dimension()) {
++        } else if (this.dimension.isPresent() && this.dimension.get() != (io.papermc.paper.configuration.GlobalConfiguration.get().misc.strictAdvancementDimensionCheck ? level.dimension() : org.bukkit.craftbukkit.util.CraftDimensionUtil.getMainDimensionKey(level))) { // Paper - Add option for strict advancement dimension checks
              return false;
          } else {
              BlockPos blockPos = BlockPos.containing(x, y, z);
diff --git a/paper-server/patches/unapplied/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java.patch b/paper-server/patches/sources/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java.patch
similarity index 59%
rename from paper-server/patches/unapplied/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java.patch
rename to paper-server/patches/sources/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java.patch
index d080616002..c4d7639889 100644
--- a/paper-server/patches/unapplied/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java.patch
+++ b/paper-server/patches/sources/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java.patch
@@ -1,41 +1,41 @@
 --- a/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java
 +++ b/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java
-@@ -15,41 +15,41 @@
+@@ -15,41 +_,41 @@
  import net.minecraft.world.level.storage.loot.LootContext;
  
  public abstract class SimpleCriterionTrigger<T extends SimpleCriterionTrigger.SimpleInstance> implements CriterionTrigger<T> {
 -    private final Map<PlayerAdvancements, Set<CriterionTrigger.Listener<T>>> players = Maps.newIdentityHashMap();
-+    // private final Map<PlayerAdvancements, Set<CriterionTrigger.Listener<T>>> players = Maps.newIdentityHashMap(); // Paper - fix AdvancementDataPlayer leak; moved into AdvancementDataPlayer to fix memory leak
++    // private final Map<PlayerAdvancements, Set<CriterionTrigger.Listener<T>>> players = Maps.newIdentityHashMap(); // Paper - fix PlayerAdvancements leak; moved into PlayerAdvancements to fix memory leak
  
      @Override
-     public final void addPlayerListener(PlayerAdvancements manager, CriterionTrigger.Listener<T> conditions) {
--        this.players.computeIfAbsent(manager, managerx -> Sets.newHashSet()).add(conditions);
-+        manager.criterionData.computeIfAbsent(this, managerx -> Sets.newHashSet()).add(conditions);  // Paper - fix AdvancementDataPlayer leak
+     public final void addPlayerListener(PlayerAdvancements playerAdvancements, CriterionTrigger.Listener<T> listener) {
+-        this.players.computeIfAbsent(playerAdvancements, advancements -> Sets.newHashSet()).add(listener);
++        playerAdvancements.criterionData.computeIfAbsent(this, managerx -> Sets.newHashSet()).add(listener); // Paper - fix PlayerAdvancements leak
      }
  
      @Override
-     public final void removePlayerListener(PlayerAdvancements manager, CriterionTrigger.Listener<T> conditions) {
--        Set<CriterionTrigger.Listener<T>> set = this.players.get(manager);
-+        Set<CriterionTrigger.Listener<T>> set = (Set) manager.criterionData.get(this); // Paper - fix AdvancementDataPlayer leak
+     public final void removePlayerListener(PlayerAdvancements playerAdvancements, CriterionTrigger.Listener<T> listener) {
+-        Set<CriterionTrigger.Listener<T>> set = this.players.get(playerAdvancements);
++        Set<CriterionTrigger.Listener<T>> set = (Set) playerAdvancements.criterionData.get(this); // Paper - fix PlayerAdvancements leak
          if (set != null) {
-             set.remove(conditions);
+             set.remove(listener);
              if (set.isEmpty()) {
--                this.players.remove(manager);
-+                manager.criterionData.remove(this); // Paper - fix AdvancementDataPlayer leak
+-                this.players.remove(playerAdvancements);
++                playerAdvancements.criterionData.remove(this); // Paper - fix PlayerAdvancements leak
              }
          }
      }
  
      @Override
-     public final void removePlayerListeners(PlayerAdvancements tracker) {
--        this.players.remove(tracker);
-+        tracker.criterionData.remove(this); // Paper - fix AdvancementDataPlayer leak
+     public final void removePlayerListeners(PlayerAdvancements playerAdvancements) {
+-        this.players.remove(playerAdvancements);
++        playerAdvancements.criterionData.remove(this); // Paper - fix PlayerAdvancements leak
      }
  
-     protected void trigger(ServerPlayer player, Predicate<T> predicate) {
-         PlayerAdvancements playerAdvancements = player.getAdvancements();
--        Set<CriterionTrigger.Listener<T>> set = this.players.get(playerAdvancements);
-+        Set<CriterionTrigger.Listener<T>> set = (Set) playerAdvancements.criterionData.get(this); // Paper - fix AdvancementDataPlayer leak
+     protected void trigger(ServerPlayer player, Predicate<T> testTrigger) {
+         PlayerAdvancements advancements = player.getAdvancements();
+-        Set<CriterionTrigger.Listener<T>> set = this.players.get(advancements);
++        Set<CriterionTrigger.Listener<T>> set = (Set) advancements.criterionData.get(this); // Paper - fix PlayerAdvancements leak
          if (set != null && !set.isEmpty()) {
 -            LootContext lootContext = EntityPredicate.createContext(player, player);
 +            LootContext lootContext = null; // EntityPredicate.createContext(player, player); // Paper - Perf: lazily create LootContext for criterions
@@ -43,7 +43,7 @@
  
              for (CriterionTrigger.Listener<T> listener : set) {
                  T simpleInstance = listener.trigger();
-                 if (predicate.test(simpleInstance)) {
+                 if (testTrigger.test(simpleInstance)) {
                      Optional<ContextAwarePredicate> optional = simpleInstance.player();
 -                    if (optional.isEmpty() || optional.get().matches(lootContext)) {
 +                    if (optional.isEmpty() || optional.get().matches(lootContext = (lootContext == null ? EntityPredicate.createContext(player, player) : lootContext))) { // Paper - Perf: lazily create LootContext for criterions
diff --git a/paper-server/patches/unapplied/net/minecraft/commands/CommandSource.java.patch b/paper-server/patches/sources/net/minecraft/commands/CommandSource.java.patch
similarity index 84%
rename from paper-server/patches/unapplied/net/minecraft/commands/CommandSource.java.patch
rename to paper-server/patches/sources/net/minecraft/commands/CommandSource.java.patch
index 4d74948c36..c541c1a047 100644
--- a/paper-server/patches/unapplied/net/minecraft/commands/CommandSource.java.patch
+++ b/paper-server/patches/sources/net/minecraft/commands/CommandSource.java.patch
@@ -1,20 +1,20 @@
 --- a/net/minecraft/commands/CommandSource.java
 +++ b/net/minecraft/commands/CommandSource.java
-@@ -22,6 +22,13 @@
+@@ -22,6 +_,13 @@
          public boolean shouldInformAdmins() {
              return false;
          }
 +
 +        // CraftBukkit start
 +        @Override
-+        public org.bukkit.command.CommandSender getBukkitSender(CommandSourceStack wrapper) {
++        public org.bukkit.command.CommandSender getBukkitSender(CommandSourceStack stack) {
 +            return io.papermc.paper.brigadier.NullCommandSender.INSTANCE; // Paper - expose a no-op CommandSender
 +        }
 +        // CraftBukkit end
      };
  
-     void sendSystemMessage(Component message);
-@@ -35,4 +42,6 @@
+     void sendSystemMessage(Component component);
+@@ -35,4 +_,6 @@
      default boolean alwaysAccepts() {
          return false;
      }
diff --git a/paper-server/patches/unapplied/net/minecraft/commands/CommandSourceStack.java.patch b/paper-server/patches/sources/net/minecraft/commands/CommandSourceStack.java.patch
similarity index 77%
rename from paper-server/patches/unapplied/net/minecraft/commands/CommandSourceStack.java.patch
rename to paper-server/patches/sources/net/minecraft/commands/CommandSourceStack.java.patch
index 94ee38720d..b8593758f7 100644
--- a/paper-server/patches/unapplied/net/minecraft/commands/CommandSourceStack.java.patch
+++ b/paper-server/patches/sources/net/minecraft/commands/CommandSourceStack.java.patch
@@ -1,33 +1,30 @@
 --- a/net/minecraft/commands/CommandSourceStack.java
 +++ b/net/minecraft/commands/CommandSourceStack.java
-@@ -45,9 +45,9 @@
- import net.minecraft.world.level.dimension.DimensionType;
+@@ -45,7 +_,7 @@
  import net.minecraft.world.phys.Vec2;
  import net.minecraft.world.phys.Vec3;
-+import com.mojang.brigadier.tree.CommandNode; // CraftBukkit
  
 -public class CommandSourceStack implements ExecutionCommandSource<CommandSourceStack>, SharedSuggestionProvider {
--
 +public class CommandSourceStack implements ExecutionCommandSource<CommandSourceStack>, SharedSuggestionProvider, io.papermc.paper.command.brigadier.PaperCommandSourceStack { // Paper - Brigadier API
      public static final SimpleCommandExceptionType ERROR_NOT_PLAYER = new SimpleCommandExceptionType(Component.translatable("permissions.requires.player"));
      public static final SimpleCommandExceptionType ERROR_NOT_ENTITY = new SimpleCommandExceptionType(Component.translatable("permissions.requires.entity"));
      public final CommandSource source;
-@@ -65,6 +65,8 @@
+@@ -63,6 +_,8 @@
      private final Vec2 rotation;
      private final CommandSigningContext signingContext;
      private final TaskChainer chatMessageChainer;
-+    public java.util.Map<Thread, CommandNode> currentCommand = new java.util.concurrent.ConcurrentHashMap<>(); // CraftBukkit // Paper - Thread Safe Vanilla Command permission checking
++    public java.util.Map<Thread, com.mojang.brigadier.tree.CommandNode> currentCommand = new java.util.concurrent.ConcurrentHashMap<>(); // CraftBukkit // Paper - Thread Safe Vanilla Command permission checking
 +    public boolean bypassSelectorPermissions = false; // Paper - add bypass for selector permissions
  
-     public CommandSourceStack(CommandSource output, Vec3 pos, Vec2 rot, ServerLevel world, int level, String name, Component displayName, MinecraftServer server, @Nullable Entity entity) {
-         this(output, pos, rot, world, level, name, displayName, server, entity, false, CommandResultCallback.EMPTY, EntityAnchorArgument.Anchor.FEET, CommandSigningContext.ANONYMOUS, TaskChainer.immediate(server));
-@@ -171,8 +173,43 @@
+     public CommandSourceStack(
+         CommandSource source,
+@@ -391,9 +_,44 @@
  
      @Override
      public boolean hasPermission(int level) {
 +        // CraftBukkit start
 +        // Paper start - Thread Safe Vanilla Command permission checking
-+        CommandNode currentCommand = this.currentCommand.get(Thread.currentThread());
++        com.mojang.brigadier.tree.CommandNode currentCommand = this.currentCommand.get(Thread.currentThread());
 +        if (currentCommand != null) {
 +            return this.hasPermission(level, org.bukkit.craftbukkit.command.VanillaCommandWrapper.getPermission(currentCommand));
 +            // Paper end - Thread Safe Vanilla Command permission checking
@@ -35,8 +32,8 @@
 +        // CraftBukkit end
 +
          return this.permissionLevel >= level;
-+    }
-+
+     }
+ 
 +    // Paper start - Fix permission levels for command blocks
 +    private boolean forceRespectPermissionLevel() {
 +        return this.source == CommandSource.NULL || (this.source instanceof final net.minecraft.world.level.BaseCommandBlock commandBlock && commandBlock.getLevel().paperConfig().commandBlocks.forceFollowPermLevel);
@@ -60,27 +57,27 @@
 +        }
 +        return hasBukkitPerm.getAsBoolean();
 +        // Paper end - Fix permission levels for command blocks
-     }
++    }
 +    // CraftBukkit end
- 
++
      public Vec3 getPosition() {
          return this.worldPosition;
-@@ -302,21 +339,26 @@
-             while (iterator.hasNext()) {
-                 ServerPlayer entityplayer = (ServerPlayer) iterator.next();
- 
--                if (entityplayer.commandSource() != this.source && this.server.getPlayerList().isOp(entityplayer.getGameProfile())) {
-+                if (entityplayer.commandSource() != this.source && entityplayer.getBukkitEntity().hasPermission("minecraft.admin.command_feedback")) { // CraftBukkit
-                     entityplayer.sendSystemMessage(ichatmutablecomponent);
+     }
+@@ -499,20 +_,25 @@
+         Component component = Component.translatable("chat.type.admin", this.getDisplayName(), message).withStyle(ChatFormatting.GRAY, ChatFormatting.ITALIC);
+         if (this.server.getGameRules().getBoolean(GameRules.RULE_SENDCOMMANDFEEDBACK)) {
+             for (ServerPlayer serverPlayer : this.server.getPlayerList().getPlayers()) {
+-                if (serverPlayer.commandSource() != this.source && this.server.getPlayerList().isOp(serverPlayer.getGameProfile())) {
++                if (serverPlayer.commandSource() != this.source && serverPlayer.getBukkitEntity().hasPermission("minecraft.admin.command_feedback")) { // CraftBukkit
+                     serverPlayer.sendSystemMessage(component);
                  }
              }
          }
  
 -        if (this.source != this.server && this.server.getGameRules().getBoolean(GameRules.RULE_LOGADMINCOMMANDS)) {
 +        if (this.source != this.server && this.server.getGameRules().getBoolean(GameRules.RULE_LOGADMINCOMMANDS) && !org.spigotmc.SpigotConfig.silentCommandBlocks) { // Spigot
-             this.server.sendSystemMessage(ichatmutablecomponent);
+             this.server.sendSystemMessage(component);
          }
- 
      }
  
      public void sendFailure(Component message) {
@@ -93,9 +90,9 @@
 -            this.source.sendSystemMessage(Component.empty().append(message).withStyle(ChatFormatting.RED));
 +            this.source.sendSystemMessage(withStyle ? Component.empty().append(message).withStyle(ChatFormatting.RED) : message); // Paper - Add UnknownCommandEvent
          }
- 
      }
-@@ -400,4 +442,16 @@
+ 
+@@ -598,4 +_,16 @@
      public boolean isSilent() {
          return this.silent;
      }
diff --git a/paper-server/patches/sources/net/minecraft/commands/Commands.java.patch b/paper-server/patches/sources/net/minecraft/commands/Commands.java.patch
new file mode 100644
index 0000000000..0c4203d037
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/commands/Commands.java.patch
@@ -0,0 +1,297 @@
+--- a/net/minecraft/commands/Commands.java
++++ b/net/minecraft/commands/Commands.java
+@@ -251,6 +_,30 @@
+             PublishCommand.register(this.dispatcher);
+         }
+ 
++        // Paper start - Vanilla command permission fixes
++        for (final CommandNode<CommandSourceStack> node : this.dispatcher.getRoot().getChildren()) {
++            if (node.getRequirement() == com.mojang.brigadier.builder.ArgumentBuilder.<CommandSourceStack>defaultRequirement()) {
++                node.requirement = stack -> stack.source == CommandSource.NULL || stack.getBukkitSender().hasPermission(org.bukkit.craftbukkit.command.VanillaCommandWrapper.getPermission(node));
++            }
++        }
++        // Paper end - Vanilla command permission fixes
++        // Paper start - Brigadier Command API
++        // Create legacy minecraft namespace commands
++        for (final CommandNode<CommandSourceStack> node : new java.util.ArrayList<>(this.dispatcher.getRoot().getChildren())) {
++            // The brigadier dispatcher is not able to resolve nested redirects.
++            // E.g. registering the alias minecraft:tp cannot redirect to tp, as tp itself redirects to teleport.
++            // Instead, target the first none redirecting node.
++            CommandNode<CommandSourceStack> flattenedAliasTarget = node;
++            while (flattenedAliasTarget.getRedirect() != null) flattenedAliasTarget = flattenedAliasTarget.getRedirect();
++
++            this.dispatcher.register(
++                com.mojang.brigadier.builder.LiteralArgumentBuilder.<CommandSourceStack>literal("minecraft:" + node.getName())
++                    .executes(flattenedAliasTarget.getCommand())
++                    .requires(flattenedAliasTarget.getRequirement())
++                    .redirect(flattenedAliasTarget)
++            );
++        }
++        // Paper end - Brigadier Command API
+         this.dispatcher.setConsumer(ExecutionCommandSource.resultConsumer());
+     }
+ 
+@@ -260,15 +_,58 @@
+         return new ParseResults<>(commandContextBuilder, parseResults.getReader(), parseResults.getExceptions());
+     }
+ 
++    // CraftBukkit start
++    public void dispatchServerCommand(CommandSourceStack sender, String command) {
++        com.google.common.base.Joiner joiner = com.google.common.base.Joiner.on(" ");
++        if (command.startsWith("/")) {
++            command = command.substring(1);
++        }
++
++        org.bukkit.event.server.ServerCommandEvent event = new org.bukkit.event.server.ServerCommandEvent(sender.getBukkitSender(), command);
++        org.bukkit.Bukkit.getPluginManager().callEvent(event);
++        if (event.isCancelled()) {
++            return;
++        }
++        command = event.getCommand();
++
++        String[] args = command.split(" ");
++        if (args.length == 0) return; // Paper - empty commands shall not be dispatched
++
++        // Paper - Fix permission levels for command blocks
++
++        // Handle vanilla commands; // Paper - handled in CommandNode/CommandDispatcher
++
++        String newCommand = joiner.join(args);
++        this.performPrefixedCommand(sender, newCommand, newCommand);
++    }
++    // CraftBukkit end
++
+     public void performPrefixedCommand(CommandSourceStack source, String command) {
++        // CraftBukkit start
++        this.performPrefixedCommand(source, command, command);
++    }
++
++    public void performPrefixedCommand(CommandSourceStack source, String command, String label) {
+         command = command.startsWith("/") ? command.substring(1) : command;
+-        this.performCommand(this.dispatcher.parse(command, source), command);
++        this.performCommand(this.dispatcher.parse(command, source), command, label);
++        // CraftBukkit end
+     }
+ 
+     public void performCommand(ParseResults<CommandSourceStack> parseResults, String command) {
++        // CraftBukkit start
++        this.performCommand(parseResults, command, command);
++    }
++
++    public void performCommand(ParseResults<CommandSourceStack> parseResults, String command, String label) {
++        // CraftBukkit end
++        // Paper start
++        this.performCommand(parseResults, command, label, false);
++    }
++    public void performCommand(ParseResults<CommandSourceStack> parseResults, String command, String label, boolean throwCommandError) {
++        // Paper end
+         CommandSourceStack commandSourceStack = parseResults.getContext().getSource();
+         Profiler.get().push(() -> "/" + command);
+-        ContextChain<CommandSourceStack> contextChain = finishParsing(parseResults, command, commandSourceStack);
++        ContextChain contextChain = this.finishParsing(parseResults, command, commandSourceStack, label); // CraftBukkit // Paper - Add UnknownCommandEvent
+ 
+         try {
+             if (contextChain != null) {
+@@ -280,9 +_,10 @@
+                 );
+             }
+         } catch (Exception var12) {
++            if (throwCommandError) throw var12; // Paper
+             MutableComponent mutableComponent = Component.literal(var12.getMessage() == null ? var12.getClass().getName() : var12.getMessage());
+-            if (LOGGER.isDebugEnabled()) {
+-                LOGGER.error("Command exception: /{}", command, var12);
++            LOGGER.error("Command exception: /{}", command, var12); // Paper - always show execution exception in console log
++            if (commandSourceStack.getServer().isDebugging() || LOGGER.isDebugEnabled()) { // Paper - Debugging
+                 StackTraceElement[] stackTrace = var12.getStackTrace();
+ 
+                 for (int i = 0; i < Math.min(stackTrace.length, 3); i++) {
+@@ -309,18 +_,22 @@
+     }
+ 
+     @Nullable
+-    private static ContextChain<CommandSourceStack> finishParsing(ParseResults<CommandSourceStack> parseResults, String command, CommandSourceStack source) {
++    private ContextChain<CommandSourceStack> finishParsing(ParseResults<CommandSourceStack> parseResults, String command, CommandSourceStack source, String label) { // CraftBukkit // Paper - Add UnknownCommandEvent
+         try {
+             validateParseResults(parseResults);
+             return ContextChain.tryFlatten(parseResults.getContext().build(command))
+                 .orElseThrow(() -> CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand().createWithContext(parseResults.getReader()));
+         } catch (CommandSyntaxException var7) {
+-            source.sendFailure(ComponentUtils.fromMessage(var7.getRawMessage()));
++            // Paper start - Add UnknownCommandEvent
++            final net.kyori.adventure.text.TextComponent.Builder builder = net.kyori.adventure.text.Component.text();
++            // source.sendFailure(ComponentUtils.fromMessage(var7.getRawMessage()));
++            builder.color(net.kyori.adventure.text.format.NamedTextColor.RED).append(io.papermc.paper.command.brigadier.MessageComponentSerializer.message().deserialize(var7.getRawMessage()));
++            // Paper end - Add UnknownCommandEvent
+             if (var7.getInput() != null && var7.getCursor() >= 0) {
+                 int min = Math.min(var7.getInput().length(), var7.getCursor());
+                 MutableComponent mutableComponent = Component.empty()
+                     .withStyle(ChatFormatting.GRAY)
+-                    .withStyle(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/" + command)));
++                    .withStyle(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/" + label))); // CraftBukkit // Paper
+                 if (min > 10) {
+                     mutableComponent.append(CommonComponents.ELLIPSIS);
+                 }
+@@ -332,7 +_,17 @@
+                 }
+ 
+                 mutableComponent.append(Component.translatable("command.context.here").withStyle(ChatFormatting.RED, ChatFormatting.ITALIC));
+-                source.sendFailure(mutableComponent);
++                // Paper start - Add UnknownCommandEvent
++                // source.sendFailure(mutableComponent);
++                builder
++                    .append(net.kyori.adventure.text.Component.newline())
++                    .append(io.papermc.paper.adventure.PaperAdventure.asAdventure(mutableComponent));
++            }
++            org.bukkit.event.command.UnknownCommandEvent event = new org.bukkit.event.command.UnknownCommandEvent(source.getBukkitSender(), command, org.spigotmc.SpigotConfig.unknownCommandMessage.isEmpty() ? null : builder.build());
++            org.bukkit.Bukkit.getServer().getPluginManager().callEvent(event);
++            if (event.message() != null) {
++                source.sendFailure(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.message()), false);
++                // Paper end - Add UnknownCommandEvent
+             }
+ 
+             return null;
+@@ -360,25 +_,130 @@
+     }
+ 
+     public void sendCommands(ServerPlayer player) {
+-        Map<CommandNode<CommandSourceStack>, CommandNode<SharedSuggestionProvider>> map = Maps.newHashMap();
++        // Paper start - Send empty commands if tab completion is disabled
++        if (org.spigotmc.SpigotConfig.tabComplete < 0) {
++            player.connection.send(new ClientboundCommandsPacket(new RootCommandNode<>()));
++            return;
++        }
++        // Paper end - Send empty commands if tab completion is disabled
++        // CraftBukkit start
++        // Register Vanilla commands into builtRoot as before
++        // Paper start - Perf: Async command map building
++        // Copy root children to avoid concurrent modification during building
++        final java.util.Collection<CommandNode<CommandSourceStack>> commandNodes = new java.util.ArrayList<>(this.dispatcher.getRoot().getChildren());
++        COMMAND_SENDING_POOL.execute(() -> this.sendAsync(player, commandNodes));
++    }
++
++    // Fixed pool, but with discard policy
++    public static final java.util.concurrent.ExecutorService COMMAND_SENDING_POOL = new java.util.concurrent.ThreadPoolExecutor(
++        2, 2, 0, java.util.concurrent.TimeUnit.MILLISECONDS,
++        new java.util.concurrent.LinkedBlockingQueue<>(),
++        new com.google.common.util.concurrent.ThreadFactoryBuilder()
++            .setNameFormat("Paper Async Command Builder Thread Pool - %1$d")
++            .setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(net.minecraft.server.MinecraftServer.LOGGER))
++            .build(),
++        new java.util.concurrent.ThreadPoolExecutor.DiscardPolicy()
++    );
++
++    private void sendAsync(ServerPlayer player, java.util.Collection<CommandNode<CommandSourceStack>> dispatcherRootChildren) {
++        // Paper end - Perf: Async command map building
++        Map<CommandNode<CommandSourceStack>, CommandNode<SharedSuggestionProvider>> map = Maps.newIdentityHashMap(); // Use identity to prevent aliasing issues
+         RootCommandNode<SharedSuggestionProvider> rootCommandNode = new RootCommandNode<>();
+         map.put(this.dispatcher.getRoot(), rootCommandNode);
+-        this.fillUsableCommands(this.dispatcher.getRoot(), rootCommandNode, player.createCommandSourceStack(), map);
++        this.fillUsableCommands(dispatcherRootChildren, rootCommandNode, player.createCommandSourceStack(), map); // Paper - Perf: Async command map building; pass copy of children
++
++        java.util.Collection<String> bukkit = new java.util.LinkedHashSet<>();
++        for (CommandNode node : rootCommandNode.getChildren()) {
++            bukkit.add(node.getName());
++        }
++        // Paper start - Perf: Async command map building
++        new com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent<CommandSourceStack>(player.getBukkitEntity(), (RootCommandNode) rootCommandNode, false).callEvent(); // Paper - Brigadier API
++        net.minecraft.server.MinecraftServer.getServer().execute(() -> {
++           runSync(player, bukkit, rootCommandNode);
++        });
++    }
++
++    private void runSync(ServerPlayer player, java.util.Collection<String> bukkit, RootCommandNode<SharedSuggestionProvider> rootCommandNode) {
++        // Paper end - Perf: Async command map building
++        new com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent<CommandSourceStack>(player.getBukkitEntity(), (RootCommandNode) rootCommandNode, true).callEvent(); // Paper - Brigadier API
++        org.bukkit.event.player.PlayerCommandSendEvent event = new org.bukkit.event.player.PlayerCommandSendEvent(player.getBukkitEntity(), new java.util.LinkedHashSet<>(bukkit));
++        event.getPlayer().getServer().getPluginManager().callEvent(event);
++
++        // Remove labels that were removed during the event
++        for (String orig : bukkit) {
++            if (!event.getCommands().contains(orig)) {
++                rootCommandNode.removeCommand(orig);
++            }
++        }
++        // CraftBukkit end
++
+         player.connection.send(new ClientboundCommandsPacket(rootCommandNode));
+     }
+ 
+     private void fillUsableCommands(
+-        CommandNode<CommandSourceStack> rootCommandSource,
++        java.util.Collection<CommandNode<CommandSourceStack>> children, // Paper - Perf: Async command map building; pass copy of children
+         CommandNode<SharedSuggestionProvider> rootSuggestion,
+         CommandSourceStack source,
+         Map<CommandNode<CommandSourceStack>, CommandNode<SharedSuggestionProvider>> commandNodeToSuggestionNode
+     ) {
+-        for (CommandNode<CommandSourceStack> commandNode : rootCommandSource.getChildren()) {
++        commandNodeToSuggestionNode.keySet().removeIf((node) -> !org.spigotmc.SpigotConfig.sendNamespaced && node.getName().contains(":")); // Paper - Remove namedspaced from result nodes to prevent redirect trimming ~ see comment below
++        for (CommandNode<CommandSourceStack> commandNode : children) { // Paper - Perf: Async command map building; pass copy of children
++            // Paper start - Brigadier API
++            if (commandNode.clientNode != null) {
++                commandNode = commandNode.clientNode;
++            }
++            // Paper end - Brigadier API
++            if (!org.spigotmc.SpigotConfig.sendNamespaced && commandNode.getName().contains(":")) continue; // Spigot
+             if (commandNode.canUse(source)) {
+                 ArgumentBuilder<SharedSuggestionProvider, ?> argumentBuilder = (ArgumentBuilder) commandNode.createBuilder();
++                // Paper start
++                /*
++                Because of how commands can be yeeted right left and center due to bad bukkit practices
++                we need to be able to ensure that ALL commands are registered (even redirects).
++
++                What this will do is IF the redirect seems to be "dead" it will create a builder and essentially populate (flatten)
++                all the children from the dead redirect to the node.
++
++                So, if minecraft:msg redirects to msg but the original msg node has been overriden minecraft:msg will now act as msg and will explicilty inherit its children.
++
++                The only way to fix this is to either:
++                - Send EVERYTHING flattened, don't use redirects
++                - Don't allow command nodes to be deleted
++                - Do this :)
++                 */
++
++                // Is there an invalid command redirect?
++                if (argumentBuilder.getRedirect() != null && commandNodeToSuggestionNode.get(argumentBuilder.getRedirect()) == null) {
++                    // Create the argument builder with the same values as the specified node, but with a different literal and populated children
++
++                    CommandNode<SharedSuggestionProvider> redirect = argumentBuilder.getRedirect();
++                    // Diff copied from LiteralCommand#createBuilder
++                    final com.mojang.brigadier.builder.LiteralArgumentBuilder<SharedSuggestionProvider> builder = com.mojang.brigadier.builder.LiteralArgumentBuilder.literal(commandNode.getName());
++                    builder.requires(redirect.getRequirement());
++                    // builder.forward(redirect.getRedirect(), redirect.getRedirectModifier(), redirect.isFork()); We don't want to migrate the forward, since it's invalid.
++                    if (redirect.getCommand() != null) {
++                        builder.executes(redirect.getCommand());
++                    }
++                    // Diff copied from LiteralCommand#createBuilder
++                    for (CommandNode<SharedSuggestionProvider> child : redirect.getChildren()) {
++                        builder.then(child);
++                    }
++
++                    argumentBuilder = builder;
++                }
++                // Paper end
+                 argumentBuilder.requires(suggestions -> true);
+                 if (argumentBuilder.getCommand() != null) {
+-                    argumentBuilder.executes(commandContext -> 0);
++                    // Paper start - fix suggestions due to falsely equal nodes
++                    // Always create a new instance
++                    //noinspection Convert2Lambda
++                    argumentBuilder.executes(new com.mojang.brigadier.Command<>() {
++                        @Override
++                        public int run(com.mojang.brigadier.context.CommandContext<SharedSuggestionProvider> commandContext) {
++                            return 0;
++                        }
++                    });
++                    // Paper end - fix suggestions due to falsely equal nodes
+                 }
+ 
+                 if (argumentBuilder instanceof RequiredArgumentBuilder) {
+@@ -396,7 +_,7 @@
+                 commandNodeToSuggestionNode.put(commandNode, commandNode1);
+                 rootSuggestion.addChild(commandNode1);
+                 if (!commandNode.getChildren().isEmpty()) {
+-                    this.fillUsableCommands(commandNode, commandNode1, source, commandNodeToSuggestionNode);
++                    this.fillUsableCommands(commandNode.getChildren(), commandNode1, source, commandNodeToSuggestionNode); // Paper - Perf: Async command map building; pass copy of children
+                 }
+             }
+         }
diff --git a/paper-server/patches/sources/net/minecraft/commands/arguments/EntityArgument.java.patch b/paper-server/patches/sources/net/minecraft/commands/arguments/EntityArgument.java.patch
new file mode 100644
index 0000000000..d39db32c3a
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/commands/arguments/EntityArgument.java.patch
@@ -0,0 +1,32 @@
+--- a/net/minecraft/commands/arguments/EntityArgument.java
++++ b/net/minecraft/commands/arguments/EntityArgument.java
+@@ -105,9 +_,14 @@
+     }
+ 
+     private EntitySelector parse(StringReader reader, boolean allowSelectors) throws CommandSyntaxException {
++        // CraftBukkit start
++        return this.parse(reader, allowSelectors, false);
++    }
++    public EntitySelector parse(StringReader reader, boolean allowSelectors, boolean overridePermissions) throws CommandSyntaxException {
++        // CraftBukkit end
+         int i = 0;
+         EntitySelectorParser entitySelectorParser = new EntitySelectorParser(reader, allowSelectors);
+-        EntitySelector entitySelector = entitySelectorParser.parse();
++        EntitySelector entitySelector = entitySelectorParser.parse(overridePermissions); // CraftBukkit
+         if (entitySelector.getMaxResults() > 1 && this.single) {
+             if (this.playersOnly) {
+                 reader.setCursor(0);
+@@ -129,7 +_,12 @@
+         if (context.getSource() instanceof SharedSuggestionProvider sharedSuggestionProvider) {
+             StringReader stringReader = new StringReader(builder.getInput());
+             stringReader.setCursor(builder.getStart());
+-            EntitySelectorParser entitySelectorParser = new EntitySelectorParser(stringReader, EntitySelectorParser.allowSelectors(sharedSuggestionProvider));
++            // Paper start - Fix EntityArgument permissions
++            final boolean permission = sharedSuggestionProvider instanceof CommandSourceStack stack
++                ? stack.bypassSelectorPermissions || stack.hasPermission(2, "minecraft.command.selector")
++                : sharedSuggestionProvider.hasPermission(2);
++            EntitySelectorParser entitySelectorParser = new EntitySelectorParser(stringReader, permission);
++            // Paper end - Fix EntityArgument permissions
+ 
+             try {
+                 entitySelectorParser.parse();
diff --git a/paper-server/patches/unapplied/net/minecraft/commands/arguments/MessageArgument.java.patch b/paper-server/patches/sources/net/minecraft/commands/arguments/MessageArgument.java.patch
similarity index 59%
rename from paper-server/patches/unapplied/net/minecraft/commands/arguments/MessageArgument.java.patch
rename to paper-server/patches/sources/net/minecraft/commands/arguments/MessageArgument.java.patch
index ba048d7f68..c2bfa96065 100644
--- a/paper-server/patches/unapplied/net/minecraft/commands/arguments/MessageArgument.java.patch
+++ b/paper-server/patches/sources/net/minecraft/commands/arguments/MessageArgument.java.patch
@@ -1,30 +1,30 @@
 --- a/net/minecraft/commands/arguments/MessageArgument.java
 +++ b/net/minecraft/commands/arguments/MessageArgument.java
-@@ -40,6 +40,11 @@
+@@ -40,6 +_,11 @@
  
-     public static void resolveChatMessage(CommandContext<CommandSourceStack> context, String name, Consumer<PlayerChatMessage> callback) throws CommandSyntaxException {
-         MessageArgument.Message message = context.getArgument(name, MessageArgument.Message.class);
-+    // Paper start
-+        resolveChatMessage(message, context, name, callback);
+     public static void resolveChatMessage(CommandContext<CommandSourceStack> context, String key, Consumer<PlayerChatMessage> callback) throws CommandSyntaxException {
+         MessageArgument.Message message = context.getArgument(key, MessageArgument.Message.class);
++        // Paper start - brig message argument support
++        resolveChatMessage(message, context, key, callback);
 +    }
-+    public static void resolveChatMessage(MessageArgument.Message message, CommandContext<CommandSourceStack> context, String name, Consumer<PlayerChatMessage> callback) throws CommandSyntaxException {
-+    // Paper end
++    public static void resolveChatMessage(MessageArgument.Message message, CommandContext<CommandSourceStack> context, String key, Consumer<PlayerChatMessage> callback) throws CommandSyntaxException {
++        // Paper end - brig message argument support
          CommandSourceStack commandSourceStack = context.getSource();
          Component component = message.resolveComponent(commandSourceStack);
-         CommandSigningContext commandSigningContext = commandSourceStack.getSigningContext();
-@@ -54,17 +59,21 @@
+         CommandSigningContext signingContext = commandSourceStack.getSigningContext();
+@@ -54,17 +_,21 @@
      private static void resolveSignedMessage(Consumer<PlayerChatMessage> callback, CommandSourceStack source, PlayerChatMessage message) {
-         MinecraftServer minecraftServer = source.getServer();
+         MinecraftServer server = source.getServer();
          CompletableFuture<FilteredText> completableFuture = filterPlainText(source, message);
--        Component component = minecraftServer.getChatDecorator().decorate(source.getPlayer(), message.decoratedContent());
--        source.getChatMessageChainer().append(completableFuture, filtered -> {
--            PlayerChatMessage playerChatMessage2 = message.withUnsignedContent(component).filter(filtered.mask());
+-        Component component = server.getChatDecorator().decorate(source.getPlayer(), message.decoratedContent());
+-        source.getChatMessageChainer().append(completableFuture, filteredText -> {
+-            PlayerChatMessage playerChatMessage = message.withUnsignedContent(component).filter(filteredText.mask());
 +        // Paper start - support asynchronous chat decoration
-+        CompletableFuture<Component> componentFuture = minecraftServer.getChatDecorator().decorate(source.getPlayer(), source, message.decoratedContent());
++        CompletableFuture<Component> componentFuture = server.getChatDecorator().decorate(source.getPlayer(), source, message.decoratedContent());
 +        source.getChatMessageChainer().append(CompletableFuture.allOf(completableFuture, componentFuture), filtered -> {
-+            PlayerChatMessage playerChatMessage2 = message.withUnsignedContent(componentFuture.join()).filter(completableFuture.join().mask());
++            PlayerChatMessage playerChatMessage = message.withUnsignedContent(componentFuture.join()).filter(completableFuture.join().mask());
 +            // Paper end - support asynchronous chat decoration
-             callback.accept(playerChatMessage2);
+             callback.accept(playerChatMessage);
          });
      }
  
diff --git a/paper-server/patches/sources/net/minecraft/commands/arguments/blocks/BlockStateParser.java.patch b/paper-server/patches/sources/net/minecraft/commands/arguments/blocks/BlockStateParser.java.patch
new file mode 100644
index 0000000000..5d7a0a0c8b
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/commands/arguments/blocks/BlockStateParser.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/commands/arguments/blocks/BlockStateParser.java
++++ b/net/minecraft/commands/arguments/blocks/BlockStateParser.java
+@@ -69,7 +_,7 @@
+     private final StringReader reader;
+     private final boolean forTesting;
+     private final boolean allowNbt;
+-    private final Map<Property<?>, Comparable<?>> properties = Maps.newHashMap();
++    private final Map<Property<?>, Comparable<?>> properties = Maps.newLinkedHashMap(); // CraftBukkit - stable
+     private final Map<String, String> vagueProperties = Maps.newHashMap();
+     private ResourceLocation id = ResourceLocation.withDefaultNamespace("");
+     @Nullable
diff --git a/paper-server/patches/unapplied/net/minecraft/commands/arguments/selector/EntitySelector.java.patch b/paper-server/patches/sources/net/minecraft/commands/arguments/selector/EntitySelector.java.patch
similarity index 96%
rename from paper-server/patches/unapplied/net/minecraft/commands/arguments/selector/EntitySelector.java.patch
rename to paper-server/patches/sources/net/minecraft/commands/arguments/selector/EntitySelector.java.patch
index cb23767a27..c89791f903 100644
--- a/paper-server/patches/unapplied/net/minecraft/commands/arguments/selector/EntitySelector.java.patch
+++ b/paper-server/patches/sources/net/minecraft/commands/arguments/selector/EntitySelector.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/commands/arguments/selector/EntitySelector.java
 +++ b/net/minecraft/commands/arguments/selector/EntitySelector.java
-@@ -93,7 +93,7 @@
+@@ -105,7 +_,7 @@
      }
  
      private void checkPermissions(CommandSourceStack source) throws CommandSyntaxException {
diff --git a/paper-server/patches/unapplied/net/minecraft/commands/arguments/selector/EntitySelectorParser.java.patch b/paper-server/patches/sources/net/minecraft/commands/arguments/selector/EntitySelectorParser.java.patch
similarity index 53%
rename from paper-server/patches/unapplied/net/minecraft/commands/arguments/selector/EntitySelectorParser.java.patch
rename to paper-server/patches/sources/net/minecraft/commands/arguments/selector/EntitySelectorParser.java.patch
index 088ffce992..7274630b68 100644
--- a/paper-server/patches/unapplied/net/minecraft/commands/arguments/selector/EntitySelectorParser.java.patch
+++ b/paper-server/patches/sources/net/minecraft/commands/arguments/selector/EntitySelectorParser.java.patch
@@ -1,24 +1,18 @@
 --- a/net/minecraft/commands/arguments/selector/EntitySelectorParser.java
 +++ b/net/minecraft/commands/arguments/selector/EntitySelectorParser.java
-@@ -133,7 +133,7 @@
-         boolean flag;
+@@ -122,6 +_,11 @@
+     }
  
-         if (source instanceof SharedSuggestionProvider icompletionprovider) {
--            if (icompletionprovider.hasPermission(2)) {
-+            if (source instanceof net.minecraft.commands.CommandSourceStack stack ? stack.bypassSelectorPermissions || stack.hasPermission(2, "minecraft.command.selector") : icompletionprovider.hasPermission(2)) { // Paper - Fix EntityArgument permissions
-                 flag = true;
-                 return flag;
-             }
-@@ -158,7 +158,7 @@
-             axisalignedbb = this.createAabb(this.deltaX == null ? 0.0D : this.deltaX, this.deltaY == null ? 0.0D : this.deltaY, this.deltaZ == null ? 0.0D : this.deltaZ);
-         }
+     public static <S> boolean allowSelectors(S suggestionProvider) {
++        // Paper start - Fix EntityArgument permissions
++        if (suggestionProvider instanceof net.minecraft.commands.CommandSourceStack stack) {
++            return stack.bypassSelectorPermissions || stack.hasPermission(2, "minecraft.command.selector");
++        }
++        // Paper end - Fix EntityArgument permissions
+         return suggestionProvider instanceof SharedSuggestionProvider sharedSuggestionProvider && sharedSuggestionProvider.hasPermission(2);
+     }
  
--        Function function;
-+        Function<Vec3, Vec3> function; // CraftBukkit - decompile error
- 
-         if (this.x == null && this.y == null && this.z == null) {
-             function = (vec3d) -> {
-@@ -215,8 +215,10 @@
+@@ -198,8 +_,10 @@
          };
      }
  
@@ -30,8 +24,8 @@
 +        // CraftBukkit end
          this.suggestions = this::suggestSelector;
          if (!this.reader.canRead()) {
-             throw EntitySelectorParser.ERROR_MISSING_SELECTOR_TYPE.createWithContext(this.reader);
-@@ -505,6 +507,12 @@
+             throw ERROR_MISSING_SELECTOR_TYPE.createWithContext(this.reader);
+@@ -467,6 +_,12 @@
      }
  
      public EntitySelector parse() throws CommandSyntaxException {
@@ -44,7 +38,7 @@
          this.startPosition = this.reader.getCursor();
          this.suggestions = this::suggestNameOrSelector;
          if (this.reader.canRead() && this.reader.peek() == '@') {
-@@ -513,7 +521,7 @@
+@@ -475,7 +_,7 @@
              }
  
              this.reader.skip();
diff --git a/paper-server/patches/unapplied/net/minecraft/commands/arguments/selector/SelectorPattern.java.patch b/paper-server/patches/sources/net/minecraft/commands/arguments/selector/SelectorPattern.java.patch
similarity index 66%
rename from paper-server/patches/unapplied/net/minecraft/commands/arguments/selector/SelectorPattern.java.patch
rename to paper-server/patches/sources/net/minecraft/commands/arguments/selector/SelectorPattern.java.patch
index ed6f4da14b..970df07ef3 100644
--- a/paper-server/patches/unapplied/net/minecraft/commands/arguments/selector/SelectorPattern.java.patch
+++ b/paper-server/patches/sources/net/minecraft/commands/arguments/selector/SelectorPattern.java.patch
@@ -1,10 +1,10 @@
 --- a/net/minecraft/commands/arguments/selector/SelectorPattern.java
 +++ b/net/minecraft/commands/arguments/selector/SelectorPattern.java
-@@ -13,7 +13,7 @@
-             EntitySelectorParser entitySelectorParser = new EntitySelectorParser(new StringReader(selector), true);
-             return DataResult.success(new SelectorPattern(selector, entitySelectorParser.parse()));
+@@ -13,7 +_,7 @@
+             EntitySelectorParser entitySelectorParser = new EntitySelectorParser(new StringReader(pattern), true);
+             return DataResult.success(new SelectorPattern(pattern, entitySelectorParser.parse()));
          } catch (CommandSyntaxException var2) {
--            return DataResult.error(() -> "Invalid selector component: " + selector + ": " + var2.getMessage());
+-            return DataResult.error(() -> "Invalid selector component: " + pattern + ": " + var2.getMessage());
 +            return DataResult.error(() -> "Invalid selector component"); // Paper - limit selector error message
          }
      }
diff --git a/paper-server/patches/unapplied/net/minecraft/core/BlockPos.java.patch b/paper-server/patches/sources/net/minecraft/core/BlockPos.java.patch
similarity index 57%
rename from paper-server/patches/unapplied/net/minecraft/core/BlockPos.java.patch
rename to paper-server/patches/sources/net/minecraft/core/BlockPos.java.patch
index 7858f6cef1..13a67b0e34 100644
--- a/paper-server/patches/unapplied/net/minecraft/core/BlockPos.java.patch
+++ b/paper-server/patches/sources/net/minecraft/core/BlockPos.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/core/BlockPos.java
 +++ b/net/minecraft/core/BlockPos.java
-@@ -158,67 +158,84 @@
+@@ -158,67 +_,84 @@
  
      @Override
      public BlockPos above() {
@@ -21,9 +21,9 @@
      }
  
      @Override
-     public BlockPos below(int i) {
--        return this.relative(Direction.DOWN, i);
-+        return i == 0 ? this.immutable() : new BlockPos(this.getX(), this.getY() - i, this.getZ()); // Paper - Perf: Optimize BlockPosition
+     public BlockPos below(int distance) {
+-        return this.relative(Direction.DOWN, distance);
++        return distance == 0 ? this.immutable() : new BlockPos(this.getX(), this.getY() - distance, this.getZ()); // Paper - Perf: Optimize BlockPosition
      }
  
      @Override
@@ -76,6 +76,7 @@
  
      @Override
      public BlockPos relative(Direction direction) {
+-        return new BlockPos(this.getX() + direction.getStepX(), this.getY() + direction.getStepY(), this.getZ() + direction.getStepZ());
 +        // Paper start - Perf: Optimize BlockPosition
 +        switch(direction) {
 +            case UP:
@@ -91,64 +92,13 @@
 +            case EAST:
 +                return new BlockPos(this.getX() + 1, this.getY(), this.getZ());
 +            default:
-         return new BlockPos(this.getX() + direction.getStepX(), this.getY() + direction.getStepY(), this.getZ() + direction.getStepZ());
++                return new BlockPos(this.getX() + direction.getStepX(), this.getY() + direction.getStepY(), this.getZ() + direction.getStepZ());
 +        }
 +        // Paper end - Perf: Optimize BlockPosition
      }
  
      @Override
-@@ -324,9 +341,11 @@
- 
-     public static Iterable<BlockPos> withinManhattan(BlockPos center, int rangeX, int rangeY, int rangeZ) {
-         int i = rangeX + rangeY + rangeZ;
--        int j = center.getX();
--        int k = center.getY();
--        int l = center.getZ();
-+        // Paper start - rename variables to fix conflict with anonymous class (remap fix)
-+        int centerX = center.getX();
-+        int centerY = center.getY();
-+        int centerZ = center.getZ();
-+        // Paper end
-         return () -> new AbstractIterator<BlockPos>() {
-                 private final BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos();
-                 private int currentDepth;
-@@ -340,7 +359,7 @@
-                 protected BlockPos computeNext() {
-                     if (this.zMirror) {
-                         this.zMirror = false;
--                        this.cursor.setZ(l - (this.cursor.getZ() - l));
-+                        this.cursor.setZ(centerZ - (this.cursor.getZ() - centerZ)); // Paper - remap fix
-                         return this.cursor;
-                     } else {
-                         BlockPos blockPos;
-@@ -366,7 +385,7 @@
-                             int k = this.currentDepth - Math.abs(i) - Math.abs(j);
-                             if (k <= rangeZ) {
-                                 this.zMirror = k != 0;
--                                blockPos = this.cursor.set(j + i, k + j, l + k);
-+                                blockPos = this.cursor.set(centerX + i, centerY + j, centerZ + k); // Paper - remap fix
-                             }
-                         }
- 
-@@ -444,12 +463,12 @@
-                     if (this.index == l) {
-                         return this.endOfData();
-                     } else {
--                        int i = this.index % i;
--                        int j = this.index / i;
--                        int k = j % j;
--                        int l = j / j;
-+                        int offsetX = this.index % i; // Paper - decomp fix
-+                        int u = this.index / i; // Paper - decomp fix
-+                        int offsetY = u % j; // Paper - decomp fix
-+                        int offsetZ = u / j; // Paper - decomp fix
-                         this.index++;
--                        return this.cursor.set(startX + i, startY + k, startZ + l);
-+                        return this.cursor.set(startX + offsetX, startY + offsetY, startZ + offsetZ); // Paper - decomp fix
-                     }
-                 }
-             };
-@@ -569,9 +588,9 @@
+@@ -573,9 +_,9 @@
          }
  
          public BlockPos.MutableBlockPos set(int x, int y, int z) {
@@ -161,26 +111,26 @@
              return this;
          }
  
-@@ -636,19 +655,19 @@
+@@ -638,19 +_,19 @@
  
          @Override
-         public BlockPos.MutableBlockPos setX(int i) {
--            super.setX(i);
-+            this.x = i; // Paper - Perf: Manually inline methods in BlockPosition
+         public BlockPos.MutableBlockPos setX(int x) {
+-            super.setX(x);
++            this.x = x; // Paper - Perf: Manually inline methods in BlockPosition
              return this;
          }
  
          @Override
-         public BlockPos.MutableBlockPos setY(int i) {
--            super.setY(i);
-+            this.y = i; // Paper - Perf: Manually inline methods in BlockPosition
+         public BlockPos.MutableBlockPos setY(int y) {
+-            super.setY(y);
++            this.y = y; // Paper - Perf: Manually inline methods in BlockPosition
              return this;
          }
  
          @Override
-         public BlockPos.MutableBlockPos setZ(int i) {
--            super.setZ(i);
-+            this.z = i; // Paper - Perf: Manually inline methods in BlockPosition
+         public BlockPos.MutableBlockPos setZ(int z) {
+-            super.setZ(z);
++            this.z = z; // Paper - Perf: Manually inline methods in BlockPosition
              return this;
          }
  
diff --git a/paper-server/patches/unapplied/net/minecraft/core/Direction.java.patch b/paper-server/patches/sources/net/minecraft/core/Direction.java.patch
similarity index 75%
rename from paper-server/patches/unapplied/net/minecraft/core/Direction.java.patch
rename to paper-server/patches/sources/net/minecraft/core/Direction.java.patch
index a0a77c5f10..1859b6bb9f 100644
--- a/paper-server/patches/unapplied/net/minecraft/core/Direction.java.patch
+++ b/paper-server/patches/sources/net/minecraft/core/Direction.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/core/Direction.java
 +++ b/net/minecraft/core/Direction.java
-@@ -57,6 +57,12 @@
+@@ -57,6 +_,12 @@
          .sorted(Comparator.comparingInt(direction -> direction.data2d))
          .toArray(Direction[]::new);
  
@@ -11,21 +11,21 @@
 +    // Paper end - Perf: Inline shift direction fields
 +
      private Direction(
-         final int id,
-         final int idOpposite,
-@@ -74,6 +80,11 @@
-         this.axisDirection = direction;
-         this.normal = vector;
-         this.normalVec3 = Vec3.atLowerCornerOf(vector);
+         final int data3d,
+         final int oppositeIndex,
+@@ -74,6 +_,11 @@
+         this.axisDirection = axisDirection;
+         this.normal = normal;
+         this.normalVec3 = Vec3.atLowerCornerOf(normal);
 +        // Paper start - Perf: Inline shift direction fields
-+        this.adjX = vector.getX();
-+        this.adjY = vector.getY();
-+        this.adjZ = vector.getZ();
++        this.adjX = normal.getX();
++        this.adjY = normal.getY();
++        this.adjZ = normal.getZ();
 +        // Paper end - Perf: Inline shift direction fields
      }
  
      public static Direction[] orderedByNearest(Entity entity) {
-@@ -247,15 +258,15 @@
+@@ -247,15 +_,15 @@
      }
  
      public int getStepX() {
diff --git a/paper-server/patches/sources/net/minecraft/core/Holder.java.patch b/paper-server/patches/sources/net/minecraft/core/Holder.java.patch
new file mode 100644
index 0000000000..c1d6d57ceb
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/Holder.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/core/Holder.java
++++ b/net/minecraft/core/Holder.java
+@@ -230,7 +_,7 @@
+         }
+ 
+         void bindTags(Collection<TagKey<T>> tags) {
+-            this.tags = Set.copyOf(tags);
++            this.tags = it.unimi.dsi.fastutil.objects.ReferenceSets.unmodifiable(new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(tags)); // Paper - use reference set because TagKey are interned
+         }
+ 
+         @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/core/MappedRegistry.java.patch b/paper-server/patches/sources/net/minecraft/core/MappedRegistry.java.patch
similarity index 94%
rename from paper-server/patches/unapplied/net/minecraft/core/MappedRegistry.java.patch
rename to paper-server/patches/sources/net/minecraft/core/MappedRegistry.java.patch
index 4d3c8b552e..400d714abe 100644
--- a/paper-server/patches/unapplied/net/minecraft/core/MappedRegistry.java.patch
+++ b/paper-server/patches/sources/net/minecraft/core/MappedRegistry.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/core/MappedRegistry.java
 +++ b/net/minecraft/core/MappedRegistry.java
-@@ -33,11 +33,11 @@
+@@ -33,11 +_,11 @@
  public class MappedRegistry<T> implements WritableRegistry<T> {
      private final ResourceKey<? extends Registry<T>> key;
      private final ObjectList<Holder.Reference<T>> byId = new ObjectArrayList<>(256);
@@ -17,17 +17,17 @@
      private Lifecycle registryLifecycle;
      private final Map<TagKey<T>, HolderSet.Named<T>> frozenTags = new IdentityHashMap<>();
      MappedRegistry.TagSet<T> allTags = MappedRegistry.TagSet.unbound();
-@@ -508,5 +508,13 @@
-         void forEach(BiConsumer<? super TagKey<T>, ? super HolderSet.Named<T>> consumer);
+@@ -509,4 +_,13 @@
  
          Stream<HolderSet.Named<T>> getTags();
-+    }
+     }
++
 +    // 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/paper-server/patches/unapplied/net/minecraft/core/Rotations.java.patch b/paper-server/patches/sources/net/minecraft/core/Rotations.java.patch
similarity index 84%
rename from paper-server/patches/unapplied/net/minecraft/core/Rotations.java.patch
rename to paper-server/patches/sources/net/minecraft/core/Rotations.java.patch
index 29c5bfc220..0b17d3ac2a 100644
--- a/paper-server/patches/unapplied/net/minecraft/core/Rotations.java.patch
+++ b/paper-server/patches/sources/net/minecraft/core/Rotations.java.patch
@@ -1,7 +1,7 @@
 --- a/net/minecraft/core/Rotations.java
 +++ b/net/minecraft/core/Rotations.java
-@@ -34,6 +34,18 @@
-         this(serialized.getFloat(0), serialized.getFloat(1), serialized.getFloat(2));
+@@ -34,6 +_,18 @@
+         this(tag.getFloat(0), tag.getFloat(1), tag.getFloat(2));
      }
  
 +    // Paper start - faster alternative constructor
diff --git a/paper-server/patches/unapplied/net/minecraft/core/Vec3i.java.patch b/paper-server/patches/sources/net/minecraft/core/Vec3i.java.patch
similarity index 51%
rename from paper-server/patches/unapplied/net/minecraft/core/Vec3i.java.patch
rename to paper-server/patches/sources/net/minecraft/core/Vec3i.java.patch
index 8c19b63ae6..09caa51586 100644
--- a/paper-server/patches/unapplied/net/minecraft/core/Vec3i.java.patch
+++ b/paper-server/patches/sources/net/minecraft/core/Vec3i.java.patch
@@ -1,7 +1,7 @@
 --- a/net/minecraft/core/Vec3i.java
 +++ b/net/minecraft/core/Vec3i.java
-@@ -16,9 +16,9 @@
-             vec -> IntStream.of(vec.getX(), vec.getY(), vec.getZ())
+@@ -16,9 +_,9 @@
+             vec3i -> IntStream.of(vec3i.getX(), vec3i.getY(), vec3i.getZ())
          );
      public static final Vec3i ZERO = new Vec3i(0, 0, 0);
 -    private int x;
@@ -11,15 +11,15 @@
 +    protected int y; // Paper - Perf: Manually inline methods in BlockPosition; protected
 +    protected int z; // Paper - Perf: Manually inline methods in BlockPosition; protected
  
-     public static Codec<Vec3i> offsetCodec(int maxAbsValue) {
+     public static Codec<Vec3i> offsetCodec(int maxOffset) {
          return CODEC.validate(
-@@ -35,12 +35,12 @@
+@@ -35,12 +_,12 @@
      }
  
      @Override
--    public boolean equals(Object object) {
-+    public final boolean equals(Object object) { // Paper - Perf: Final for inline
-         return this == object || object instanceof Vec3i vec3i && this.getX() == vec3i.getX() && this.getY() == vec3i.getY() && this.getZ() == vec3i.getZ();
+-    public boolean equals(Object other) {
++    public final boolean equals(Object other) { // Paper - Perf: Final for inline
+         return this == other || other instanceof Vec3i vec3i && this.getX() == vec3i.getX() && this.getY() == vec3i.getY() && this.getZ() == vec3i.getZ();
      }
  
      @Override
@@ -28,7 +28,7 @@
          return (this.getY() + this.getZ() * 31) * 31 + this.getX();
      }
  
-@@ -53,15 +53,15 @@
+@@ -53,15 +_,15 @@
          }
      }
  
@@ -47,3 +47,14 @@
          return this.z;
      }
  
+@@ -235,4 +_,10 @@
+     public String toShortString() {
+         return this.getX() + ", " + this.getY() + ", " + this.getZ();
+     }
++
++    // Paper start - Perf: Optimize isInWorldBounds
++    public final boolean isInsideBuildHeightAndWorldBoundsHorizontal(final net.minecraft.world.level.LevelHeightAccessor levelHeightAccessor) {
++        return this.getX() >= -30000000 && this.getZ() >= -30000000 && this.getX() < 30000000 && this.getZ() < 30000000 && !levelHeightAccessor.isOutsideBuildHeight(this.getY());
++    }
++    // Paper end - Perf: Optimize isInWorldBounds
+ }
diff --git a/paper-server/patches/sources/net/minecraft/core/cauldron/CauldronInteraction.java.patch b/paper-server/patches/sources/net/minecraft/core/cauldron/CauldronInteraction.java.patch
new file mode 100644
index 0000000000..d551548c54
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/cauldron/CauldronInteraction.java.patch
@@ -0,0 +1,305 @@
+--- a/net/minecraft/core/cauldron/CauldronInteraction.java
++++ b/net/minecraft/core/cauldron/CauldronInteraction.java
+@@ -42,26 +_,31 @@
+ 
+     static CauldronInteraction.InteractionMap newInteractionMap(String name) {
+         Object2ObjectOpenHashMap<Item, CauldronInteraction> map = new Object2ObjectOpenHashMap<>();
+-        map.defaultReturnValue((state, level, pos, player, hand, stack) -> InteractionResult.TRY_WITH_EMPTY_HAND);
++        map.defaultReturnValue((state, level, pos, player, hand, stack, hitDirection) -> InteractionResult.TRY_WITH_EMPTY_HAND); // Paper - add hitDirection
+         CauldronInteraction.InteractionMap interactionMap = new CauldronInteraction.InteractionMap(name, map);
+         INTERACTIONS.put(name, interactionMap);
+         return interactionMap;
+     }
+ 
+-    InteractionResult interact(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack stack);
++    InteractionResult interact(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, final net.minecraft.core.Direction hitDirection); // Paper - add hitDirection
+ 
+     static void bootStrap() {
+         Map<Item, CauldronInteraction> map = EMPTY.map();
+         addDefaultInteractions(map);
+-        map.put(Items.POTION, (state, level, pos, player, hand, stack) -> {
++        map.put(Items.POTION, (state, level, pos, player, hand, stack, hitDirection) -> { // Paper - add hitDirection
+             PotionContents potionContents = stack.get(DataComponents.POTION_CONTENTS);
+             if (potionContents != null && potionContents.is(Potions.WATER)) {
+                 if (!level.isClientSide) {
++                    // CraftBukkit start
++                    if (!LayeredCauldronBlock.changeLevel(level, pos, Blocks.WATER_CAULDRON.defaultBlockState(), player, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason.BOTTLE_EMPTY, false)) { // Paper - Call CauldronLevelChangeEvent
++                        return InteractionResult.SUCCESS;
++                    }
++                    // CraftBukkit end
+                     Item item = stack.getItem();
+                     player.setItemInHand(hand, ItemUtils.createFilledResult(stack, player, new ItemStack(Items.GLASS_BOTTLE)));
+                     player.awardStat(Stats.USE_CAULDRON);
+                     player.awardStat(Stats.ITEM_USED.get(item));
+-                    level.setBlockAndUpdate(pos, Blocks.WATER_CAULDRON.defaultBlockState());
++                    // level.setBlockAndUpdate(pos, Blocks.WATER_CAULDRON.defaultBlockState()); // CraftBukkit
+                     level.playSound(null, pos, SoundEvents.BOTTLE_EMPTY, SoundSource.BLOCKS, 1.0F, 1.0F);
+                     level.gameEvent(null, GameEvent.FLUID_PLACE, pos);
+                 }
+@@ -75,7 +_,7 @@
+         addDefaultInteractions(map1);
+         map1.put(
+             Items.BUCKET,
+-            (state, level, pos, player, hand, stack) -> fillBucket(
++            (state, level, pos, player, hand, stack, hitDirection) -> fillBucket( // Paper - add hitDirection
+                 state,
+                 level,
+                 pos,
+@@ -84,33 +_,43 @@
+                 stack,
+                 new ItemStack(Items.WATER_BUCKET),
+                 blockState -> blockState.getValue(LayeredCauldronBlock.LEVEL) == 3,
+-                SoundEvents.BUCKET_FILL
++                SoundEvents.BUCKET_FILL, hitDirection // Paper - add hitDirection
+             )
+         );
+-        map1.put(Items.GLASS_BOTTLE, (state, level, pos, player, hand, stack) -> {
++        map1.put(Items.GLASS_BOTTLE, (state, level, pos, player, hand, stack, hitDirection) -> { // Paper - add hitDirection
+             if (!level.isClientSide) {
++                // CraftBukkit start
++                if (!LayeredCauldronBlock.lowerFillLevel(state, level, pos, player, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason.BOTTLE_FILL)) {
++                    return InteractionResult.SUCCESS;
++                }
++                // CraftBukkit end
+                 Item item = stack.getItem();
+                 player.setItemInHand(hand, ItemUtils.createFilledResult(stack, player, PotionContents.createItemStack(Items.POTION, Potions.WATER)));
+                 player.awardStat(Stats.USE_CAULDRON);
+                 player.awardStat(Stats.ITEM_USED.get(item));
+-                LayeredCauldronBlock.lowerFillLevel(state, level, pos);
++                // LayeredCauldronBlock.lowerFillLevel(state, level, pos); // CraftBukkit
+                 level.playSound(null, pos, SoundEvents.BOTTLE_FILL, SoundSource.BLOCKS, 1.0F, 1.0F);
+                 level.gameEvent(null, GameEvent.FLUID_PICKUP, pos);
+             }
+ 
+             return InteractionResult.SUCCESS;
+         });
+-        map1.put(Items.POTION, (state, level, pos, player, hand, stack) -> {
++        map1.put(Items.POTION, (state, level, pos, player, hand, stack, hitDirection) -> { // Paper - add hitDirection
+             if (state.getValue(LayeredCauldronBlock.LEVEL) == 3) {
+                 return InteractionResult.TRY_WITH_EMPTY_HAND;
+             } else {
+                 PotionContents potionContents = stack.get(DataComponents.POTION_CONTENTS);
+                 if (potionContents != null && potionContents.is(Potions.WATER)) {
+                     if (!level.isClientSide) {
++                        // CraftBukkit start
++                        if (!LayeredCauldronBlock.changeLevel(level, pos, state.cycle(LayeredCauldronBlock.LEVEL), player, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason.BOTTLE_EMPTY, false)) { // Paper - Call CauldronLevelChangeEvent
++                            return InteractionResult.SUCCESS;
++                        }
++                        // CraftBukkit end
+                         player.setItemInHand(hand, ItemUtils.createFilledResult(stack, player, new ItemStack(Items.GLASS_BOTTLE)));
+                         player.awardStat(Stats.USE_CAULDRON);
+                         player.awardStat(Stats.ITEM_USED.get(stack.getItem()));
+-                        level.setBlockAndUpdate(pos, state.cycle(LayeredCauldronBlock.LEVEL));
++                        // level.setBlockAndUpdate(pos, state.cycle(LayeredCauldronBlock.LEVEL)); // CraftBukkit
+                         level.playSound(null, pos, SoundEvents.BOTTLE_EMPTY, SoundSource.BLOCKS, 1.0F, 1.0F);
+                         level.gameEvent(null, GameEvent.FLUID_PLACE, pos);
+                     }
+@@ -162,15 +_,15 @@
+         Map<Item, CauldronInteraction> map2 = LAVA.map();
+         map2.put(
+             Items.BUCKET,
+-            (state, level, pos, player, hand, stack) -> fillBucket(
+-                state, level, pos, player, hand, stack, new ItemStack(Items.LAVA_BUCKET), blockState -> true, SoundEvents.BUCKET_FILL_LAVA
++            (state, level, pos, player, hand, stack, hitDirection) -> fillBucket( // Paper - add hitDirection
++                state, level, pos, player, hand, stack, new ItemStack(Items.LAVA_BUCKET), blockState -> true, SoundEvents.BUCKET_FILL_LAVA, hitDirection // Paper - add hitDirection
+             )
+         );
+         addDefaultInteractions(map2);
+         Map<Item, CauldronInteraction> map3 = POWDER_SNOW.map();
+         map3.put(
+             Items.BUCKET,
+-            (state, level, pos, player, hand, stack) -> fillBucket(
++            (state, level, pos, player, hand, stack, hitDirection) -> fillBucket( // Paper - add hitDirection
+                 state,
+                 level,
+                 pos,
+@@ -179,7 +_,7 @@
+                 stack,
+                 new ItemStack(Items.POWDER_SNOW_BUCKET),
+                 blockState -> blockState.getValue(LayeredCauldronBlock.LEVEL) == 3,
+-                SoundEvents.BUCKET_FILL_POWDER_SNOW
++                SoundEvents.BUCKET_FILL_POWDER_SNOW, hitDirection // Paper - add hitDirection
+             )
+         );
+         addDefaultInteractions(map3);
+@@ -202,15 +_,34 @@
+         Predicate<BlockState> statePredicate,
+         SoundEvent fillSound
+     ) {
++        // Paper start - add hitDirection
++        return fillBucket(state, level, pos, player, hand, emptyStack, filledStack, statePredicate, fillSound, null); // Paper - add hitDirection
++    }
++    static InteractionResult fillBucket(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack emptyStack, ItemStack filledStack, Predicate<BlockState> statePredicate, SoundEvent fillSound, @javax.annotation.Nullable net.minecraft.core.Direction hitDirection) {
++        // Paper end - add hitDirection
+         if (!statePredicate.test(state)) {
+             return InteractionResult.TRY_WITH_EMPTY_HAND;
+         } else {
+             if (!level.isClientSide) {
++                // Paper start - fire PlayerBucketFillEvent
++                if (hitDirection != null) {
++                    org.bukkit.event.player.PlayerBucketEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerBucketFillEvent((net.minecraft.server.level.ServerLevel) level, player, pos, pos, hitDirection, emptyStack, filledStack.getItem(), hand);
++                    if (event.isCancelled()) {
++                        return InteractionResult.PASS;
++                    }
++                    filledStack = event.getItemStack() != null ? org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItemStack()) : ItemStack.EMPTY;
++                }
++                // Paper end - fire PlayerBucketFillEvent
++                // CraftBukkit start
++                if (!LayeredCauldronBlock.changeLevel(level, pos, Blocks.CAULDRON.defaultBlockState(), player, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason.BUCKET_FILL, false)) { // Paper - Call CauldronLevelChangeEvent
++                    return InteractionResult.SUCCESS;
++                }
++                // CraftBukkit end
+                 Item item = emptyStack.getItem();
+                 player.setItemInHand(hand, ItemUtils.createFilledResult(emptyStack, player, filledStack));
+                 player.awardStat(Stats.USE_CAULDRON);
+                 player.awardStat(Stats.ITEM_USED.get(item));
+-                level.setBlockAndUpdate(pos, Blocks.CAULDRON.defaultBlockState());
++                // level.setBlockAndUpdate(pos, Blocks.CAULDRON.defaultBlockState()); // CraftBukkit
+                 level.playSound(null, pos, fillSound, SoundSource.BLOCKS, 1.0F, 1.0F);
+                 level.gameEvent(null, GameEvent.FLUID_PICKUP, pos);
+             }
+@@ -222,12 +_,32 @@
+     static InteractionResult emptyBucket(
+         Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack filledStackl, BlockState state, SoundEvent emptySound
+     ) {
++        // Paper start - add hitDirection
++        return emptyBucket(level, pos, player, hand, filledStackl, state, emptySound, null);
++    }
++    static InteractionResult emptyBucket(Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack filledStackl, BlockState state, SoundEvent emptySound, @javax.annotation.Nullable net.minecraft.core.Direction hitDirection) {
++        // Paper end - add hitDirection
+         if (!level.isClientSide) {
++            // Paper start - fire PlayerBucketEmptyEvent
++            ItemStack output = new ItemStack(Items.BUCKET);
++            if (hitDirection != null) {
++                org.bukkit.event.player.PlayerBucketEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerBucketEmptyEvent((net.minecraft.server.level.ServerLevel) level, player, pos, pos, hitDirection, filledStackl, hand);
++                if (event.isCancelled()) {
++                    return InteractionResult.PASS;
++                }
++                output = event.getItemStack() != null ? org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItemStack()) : ItemStack.EMPTY;
++            }
++            // Paper end - fire PlayerBucketEmptyEvent
++            // CraftBukkit start
++            if (!LayeredCauldronBlock.changeLevel(level, pos, state, player, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason.BUCKET_EMPTY, false)) { // Paper - Call CauldronLevelChangeEvent
++                return InteractionResult.SUCCESS;
++            }
++            // CraftBukkit end
+             Item item = filledStackl.getItem();
+-            player.setItemInHand(hand, ItemUtils.createFilledResult(filledStackl, player, new ItemStack(Items.BUCKET)));
++            player.setItemInHand(hand, ItemUtils.createFilledResult(filledStackl, player, output)); // Paper
+             player.awardStat(Stats.FILL_CAULDRON);
+             player.awardStat(Stats.ITEM_USED.get(item));
+-            level.setBlockAndUpdate(pos, state);
++            // level.setBlockAndUpdate(pos, state); // CraftBukkit
+             level.playSound(null, pos, emptySound, SoundSource.BLOCKS, 1.0F, 1.0F);
+             level.gameEvent(null, GameEvent.FLUID_PLACE, pos);
+         }
+@@ -236,7 +_,7 @@
+     }
+ 
+     private static InteractionResult fillWaterInteraction(
+-        BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack filledStack
++        BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack filledStack, final net.minecraft.core.Direction hitDirection // Paper - add hitDirection
+     ) {
+         return emptyBucket(
+             level,
+@@ -245,20 +_,20 @@
+             hand,
+             filledStack,
+             Blocks.WATER_CAULDRON.defaultBlockState().setValue(LayeredCauldronBlock.LEVEL, Integer.valueOf(3)),
+-            SoundEvents.BUCKET_EMPTY
++            SoundEvents.BUCKET_EMPTY, hitDirection // Paper - add hitDirection
+         );
+     }
+ 
+     private static InteractionResult fillLavaInteraction(
+-        BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack filledStack
++        BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack filledStack, final net.minecraft.core.Direction hitDirection // Paper - add hitDirection
+     ) {
+         return (InteractionResult)(isUnderWater(level, pos)
+             ? InteractionResult.CONSUME
+-            : emptyBucket(level, pos, player, hand, filledStack, Blocks.LAVA_CAULDRON.defaultBlockState(), SoundEvents.BUCKET_EMPTY_LAVA));
++            : emptyBucket(level, pos, player, hand, filledStack, Blocks.LAVA_CAULDRON.defaultBlockState(), SoundEvents.BUCKET_EMPTY_LAVA, hitDirection)); // Paper - add hitDirection
+     }
+ 
+     private static InteractionResult fillPowderSnowInteraction(
+-        BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack filledStack
++        BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack filledStack, final net.minecraft.core.Direction hitDirection // Paper - add hitDirection
+     ) {
+         return (InteractionResult)(isUnderWater(level, pos)
+             ? InteractionResult.CONSUME
+@@ -269,53 +_,68 @@
+                 hand,
+                 filledStack,
+                 Blocks.POWDER_SNOW_CAULDRON.defaultBlockState().setValue(LayeredCauldronBlock.LEVEL, Integer.valueOf(3)),
+-                SoundEvents.BUCKET_EMPTY_POWDER_SNOW
++                SoundEvents.BUCKET_EMPTY_POWDER_SNOW, hitDirection // Paper - add hitDirection
+             ));
+     }
+ 
+-    private static InteractionResult shulkerBoxInteraction(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack stack) {
++    private static InteractionResult shulkerBoxInteraction(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, final net.minecraft.core.Direction hitDirection) { // Paper - add hitDirection
+         Block block = Block.byItem(stack.getItem());
+         if (!(block instanceof ShulkerBoxBlock)) {
+             return InteractionResult.TRY_WITH_EMPTY_HAND;
+         } else {
+             if (!level.isClientSide) {
++                // CraftBukkit start
++                if (!LayeredCauldronBlock.lowerFillLevel(state, level, pos, player, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason.SHULKER_WASH)) {
++                    return InteractionResult.SUCCESS;
++                }
++                // CraftBukkit end
+                 ItemStack itemStack = stack.transmuteCopy(Blocks.SHULKER_BOX, 1);
+                 player.setItemInHand(hand, ItemUtils.createFilledResult(stack, player, itemStack, false));
+                 player.awardStat(Stats.CLEAN_SHULKER_BOX);
+-                LayeredCauldronBlock.lowerFillLevel(state, level, pos);
++                // LayeredCauldronBlock.lowerFillLevel(state, level, pos); // CraftBukkit
+             }
+ 
+             return InteractionResult.SUCCESS;
+         }
+     }
+ 
+-    private static InteractionResult bannerInteraction(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack stack) {
++    private static InteractionResult bannerInteraction(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, final net.minecraft.core.Direction hitDirection) { // Paper - add hitDirection
+         BannerPatternLayers bannerPatternLayers = stack.getOrDefault(DataComponents.BANNER_PATTERNS, BannerPatternLayers.EMPTY);
+         if (bannerPatternLayers.layers().isEmpty()) {
+             return InteractionResult.TRY_WITH_EMPTY_HAND;
+         } else {
+             if (!level.isClientSide) {
++                // CraftBukkit start
++                if (!LayeredCauldronBlock.lowerFillLevel(state, level, pos, player, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason.BANNER_WASH)) {
++                    return InteractionResult.SUCCESS;
++                }
++                // CraftBukkit end
+                 ItemStack itemStack = stack.copyWithCount(1);
+                 itemStack.set(DataComponents.BANNER_PATTERNS, bannerPatternLayers.removeLast());
+                 player.setItemInHand(hand, ItemUtils.createFilledResult(stack, player, itemStack, false));
+                 player.awardStat(Stats.CLEAN_BANNER);
+-                LayeredCauldronBlock.lowerFillLevel(state, level, pos);
++                // LayeredCauldronBlock.lowerFillLevel(state, level, pos); // CraftBukkit
+             }
+ 
+             return InteractionResult.SUCCESS;
+         }
+     }
+ 
+-    private static InteractionResult dyedItemIteration(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack stack) {
++    private static InteractionResult dyedItemIteration(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, final net.minecraft.core.Direction hitDirection) { // Paper - add hitDirection
+         if (!stack.is(ItemTags.DYEABLE)) {
+             return InteractionResult.TRY_WITH_EMPTY_HAND;
+         } else if (!stack.has(DataComponents.DYED_COLOR)) {
+             return InteractionResult.TRY_WITH_EMPTY_HAND;
+         } else {
+             if (!level.isClientSide) {
++                // CraftBukkit start
++                if (!LayeredCauldronBlock.lowerFillLevel(state, level, pos, player, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason.ARMOR_WASH)) {
++                    return InteractionResult.SUCCESS;
++                }
++                // CraftBukkit end
+                 stack.remove(DataComponents.DYED_COLOR);
+                 player.awardStat(Stats.CLEAN_ARMOR);
+-                LayeredCauldronBlock.lowerFillLevel(state, level, pos);
++                // LayeredCauldronBlock.lowerFillLevel(state, level, pos); // CraftBukkit
+             }
+ 
+             return InteractionResult.SUCCESS;
diff --git a/paper-server/patches/unapplied/net/minecraft/core/component/DataComponentPatch.java.patch b/paper-server/patches/sources/net/minecraft/core/component/DataComponentPatch.java.patch
similarity index 62%
rename from paper-server/patches/unapplied/net/minecraft/core/component/DataComponentPatch.java.patch
rename to paper-server/patches/sources/net/minecraft/core/component/DataComponentPatch.java.patch
index 53846b237e..9a4fca81b5 100644
--- a/paper-server/patches/unapplied/net/minecraft/core/component/DataComponentPatch.java.patch
+++ b/paper-server/patches/sources/net/minecraft/core/component/DataComponentPatch.java.patch
@@ -1,33 +1,24 @@
 --- a/net/minecraft/core/component/DataComponentPatch.java
 +++ b/net/minecraft/core/component/DataComponentPatch.java
-@@ -61,7 +61,7 @@
-             }
+@@ -125,7 +_,13 @@
          }
  
--        return reference2objectmap;
-+        return (Reference2ObjectMap) reference2objectmap; // CraftBukkit - decompile error
-     });
-     public static final StreamCodec<RegistryFriendlyByteBuf, DataComponentPatch> STREAM_CODEC = new StreamCodec<RegistryFriendlyByteBuf, DataComponentPatch>() {
-         public DataComponentPatch decode(RegistryFriendlyByteBuf registryfriendlybytebuf) {
-@@ -144,7 +144,13 @@
-         }
- 
-         private static <T> void encodeComponent(RegistryFriendlyByteBuf buf, DataComponentType<T> type, Object value) {
--            type.streamCodec().encode(buf, value);
+         private static <T> void encodeComponent(RegistryFriendlyByteBuf buffer, DataComponentType<T> component, Object value) {
+-            component.streamCodec().encode(buffer, (T)value);
 +            // Paper start - codec errors of random anonymous classes are useless
 +            try {
-+            type.streamCodec().encode(buf, (T) value); // CraftBukkit - decompile error
++                component.streamCodec().encode(buffer, (T)value);
 +            } catch (final Exception e) {
-+                throw new RuntimeException("Error encoding component " + type, e);
++                throw new RuntimeException("Error encoding component " + component, e);
 +            }
 +            // Paper end - codec errors of random anonymous classes are useless
          }
      };
      private static final String REMOVED_PREFIX = "!";
-@@ -270,7 +276,43 @@
-         private final Reference2ObjectMap<DataComponentType<?>, Optional<?>> map = new Reference2ObjectArrayMap();
+@@ -230,6 +_,42 @@
  
-         Builder() {}
+         Builder() {
+         }
 +
 +        // CraftBukkit start
 +        public void copy(DataComponentPatch orig) {
@@ -45,7 +36,7 @@
 +        public boolean isEmpty() {
 +            return this.map.isEmpty();
 +        }
- 
++
 +        @Override
 +        public boolean equals(Object object) {
 +            if (this == object) {
@@ -64,7 +55,6 @@
 +            return this.map.hashCode();
 +        }
 +        // CraftBukkit end
-+
-         public <T> DataComponentPatch.Builder set(DataComponentType<T> type, T value) {
-             this.map.put(type, Optional.of(value));
-             return this;
+ 
+         public <T> DataComponentPatch.Builder set(DataComponentType<T> component, T value) {
+             this.map.put(component, Optional.of(value));
diff --git a/paper-server/patches/unapplied/net/minecraft/core/component/DataComponents.java.patch b/paper-server/patches/sources/net/minecraft/core/component/DataComponents.java.patch
similarity index 98%
rename from paper-server/patches/unapplied/net/minecraft/core/component/DataComponents.java.patch
rename to paper-server/patches/sources/net/minecraft/core/component/DataComponents.java.patch
index eb470da08e..c1f061a0af 100644
--- a/paper-server/patches/unapplied/net/minecraft/core/component/DataComponents.java.patch
+++ b/paper-server/patches/sources/net/minecraft/core/component/DataComponents.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/core/component/DataComponents.java
 +++ b/net/minecraft/core/component/DataComponents.java
-@@ -180,10 +180,10 @@
+@@ -180,10 +_,10 @@
          "map_post_processing", builder -> builder.networkSynchronized(MapPostProcessing.STREAM_CODEC)
      );
      public static final DataComponentType<ChargedProjectiles> CHARGED_PROJECTILES = register(
@@ -13,7 +13,7 @@
      );
      public static final DataComponentType<PotionContents> POTION_CONTENTS = register(
          "potion_contents", builder -> builder.persistent(PotionContents.CODEC).networkSynchronized(PotionContents.STREAM_CODEC).cacheEncoding()
-@@ -250,7 +250,7 @@
+@@ -250,7 +_,7 @@
          "pot_decorations", builder -> builder.persistent(PotDecorations.CODEC).networkSynchronized(PotDecorations.STREAM_CODEC).cacheEncoding()
      );
      public static final DataComponentType<ItemContainerContents> CONTAINER = register(
diff --git a/paper-server/patches/sources/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java.patch b/paper-server/patches/sources/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java.patch
new file mode 100644
index 0000000000..c40f77ee71
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java.patch
@@ -0,0 +1,45 @@
+--- a/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java
++++ b/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java
+@@ -40,13 +_,39 @@
+             d4 = 0.0;
+         }
+ 
++        // CraftBukkit start
++        ItemStack singleItemStack = item.copyWithCount(1); // Paper - shrink at end and single item in event
++        org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(serverLevel, blockSource.pos());
++        org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleItemStack);
++
++        org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(d1, d2 + d4, d3));
++        if (!DispenserBlock.eventFired) {
++            serverLevel.getCraftServer().getPluginManager().callEvent(event);
++        }
++
++        if (event.isCancelled()) {
++            // stack.grow(1); // Paper - shrink below
++            return item;
++        }
++
++        boolean shrink = true; // Paper
++        if (!event.getItem().equals(craftItem)) {
++            shrink = false; // Paper - shrink below
++            // Chain to handler for new item
++            ItemStack eventStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem());
++            DispenseItemBehavior dispenseBehavior = DispenserBlock.getDispenseBehavior(blockSource, eventStack); // Paper - Fix NPE with equippable and items without behavior
++            if (dispenseBehavior != DispenseItemBehavior.NOOP && dispenseBehavior != this) {
++                dispenseBehavior.dispense(blockSource, eventStack);
++                return item;
++            }
++        }
++        // CraftBukkit end
+         AbstractBoat abstractBoat = this.type.create(serverLevel, EntitySpawnReason.DISPENSER);
+         if (abstractBoat != null) {
+-            abstractBoat.setInitialPos(d1, d2 + d4, d3);
++            abstractBoat.setInitialPos(event.getVelocity().getX(), event.getVelocity().getY(), event.getVelocity().getZ()); // CraftBukkit
+             EntityType.<AbstractBoat>createDefaultStackConfig(serverLevel, item, null).accept(abstractBoat);
+             abstractBoat.setYRot(direction.toYRot());
+-            serverLevel.addFreshEntity(abstractBoat);
+-            item.shrink(1);
++            if (serverLevel.addFreshEntity(abstractBoat) && shrink) item.shrink(1); // Paper - if entity add was successful and supposed to shrink
+         }
+ 
+         return item;
diff --git a/paper-server/patches/sources/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java.patch b/paper-server/patches/sources/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java.patch
new file mode 100644
index 0000000000..3533ecd0e6
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java.patch
@@ -0,0 +1,98 @@
+--- a/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java
++++ b/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java
+@@ -10,23 +_,46 @@
+ public class DefaultDispenseItemBehavior implements DispenseItemBehavior {
+     private static final int DEFAULT_ACCURACY = 6;
+ 
++    // CraftBukkit start
++    private Direction direction; // Paper - cache facing direction
++    private boolean dropper;
++
++    public DefaultDispenseItemBehavior(boolean dropper) {
++        this.dropper = dropper;
++    }
++
++    public DefaultDispenseItemBehavior() {}
++    // CraftBukkit end
++
+     @Override
+     public final ItemStack dispense(BlockSource blockSource, ItemStack item) {
++        this.direction = blockSource.state().getValue(DispenserBlock.FACING); // Paper - cache facing direction
+         ItemStack itemStack = this.execute(blockSource, item);
+         this.playSound(blockSource);
+-        this.playAnimation(blockSource, blockSource.state().getValue(DispenserBlock.FACING));
++        this.playAnimation(blockSource, this.direction); // Paper - cache facing direction
+         return itemStack;
+     }
+ 
+     protected ItemStack execute(BlockSource blockSource, ItemStack item) {
+-        Direction direction = blockSource.state().getValue(DispenserBlock.FACING);
++        // Paper - cache facing direction
+         Position dispensePosition = DispenserBlock.getDispensePosition(blockSource);
+         ItemStack itemStack = item.split(1);
+-        spawnItem(blockSource.level(), itemStack, 6, direction, dispensePosition);
++        // CraftBukkit start
++        if (!DefaultDispenseItemBehavior.spawnItem(blockSource.level(), itemStack, 6, this.direction, dispensePosition, blockSource, this.dropper)) {
++            item.grow(1);
++        }
++        // CraftBukkit end
+         return item;
+     }
+ 
+     public static void spawnItem(Level level, ItemStack stack, int speed, Direction facing, Position position) {
++        // CraftBukkit start
++        ItemEntity itemEntity = prepareItem(level, stack, speed, facing, position);
++        level.addFreshEntity(itemEntity);
++    }
++
++    private static ItemEntity prepareItem(Level level, ItemStack stack, int speed, Direction facing, Position position) {
++        // CraftBukkit end
+         double d = position.x();
+         double d1 = position.y();
+         double d2 = position.z();
+@@ -43,7 +_,45 @@
+             level.random.triangle(0.2, 0.0172275 * speed),
+             level.random.triangle(facing.getStepZ() * d3, 0.0172275 * speed)
+         );
++        return itemEntity; // CraftBukkit
++    }
++
++    // CraftBukkit - void -> boolean return, IPosition -> ISourceBlock last argument, dropper
++    public static boolean spawnItem(Level level, ItemStack stack, int speed, Direction facing, Position dispensePosition, BlockSource blockSource, boolean dropper) {
++        if (stack.isEmpty()) return true;
++        ItemEntity itemEntity = DefaultDispenseItemBehavior.prepareItem(level, stack, speed, facing, dispensePosition);
++
++        org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(level, blockSource.pos());
++        org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(stack);
++
++        org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(itemEntity.getDeltaMovement()));
++        if (!DispenserBlock.eventFired) {
++            level.getCraftServer().getPluginManager().callEvent(event);
++        }
++
++        if (event.isCancelled()) {
++            return false;
++        }
++
++        itemEntity.setItem(org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem()));
++        itemEntity.setDeltaMovement(org.bukkit.craftbukkit.util.CraftVector.toNMS(event.getVelocity()));
++
++        if (!dropper && !event.getItem().getType().equals(craftItem.getType())) {
++            // Chain to handler for new item
++            ItemStack eventStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem());
++            DispenseItemBehavior dispenseBehavior = DispenserBlock.getDispenseBehavior(blockSource, eventStack); // Paper - Fix NPE with equippable and items without behavior
++            if (dispenseBehavior != DispenseItemBehavior.NOOP && dispenseBehavior.getClass() != DefaultDispenseItemBehavior.class) {
++                dispenseBehavior.dispense(blockSource, eventStack);
++            } else {
++                level.addFreshEntity(itemEntity);
++            }
++            return false;
++        }
++
+         level.addFreshEntity(itemEntity);
++
++        return true;
++        // CraftBukkit end
+     }
+ 
+     protected void playSound(BlockSource blockSource) {
diff --git a/paper-server/patches/sources/net/minecraft/core/dispenser/DispenseItemBehavior.java.patch b/paper-server/patches/sources/net/minecraft/core/dispenser/DispenseItemBehavior.java.patch
new file mode 100644
index 0000000000..c4e6ac4f58
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/dispenser/DispenseItemBehavior.java.patch
@@ -0,0 +1,596 @@
+--- a/net/minecraft/core/dispenser/DispenseItemBehavior.java
++++ b/net/minecraft/core/dispenser/DispenseItemBehavior.java
+@@ -82,16 +_,48 @@
+                 Direction direction = blockSource.state().getValue(DispenserBlock.FACING);
+                 EntityType<?> type = ((SpawnEggItem)item.getItem()).getType(blockSource.level().registryAccess(), item);
+ 
++                // CraftBukkit start
++                ServerLevel serverLevel = blockSource.level();
++                ItemStack singleItemStack = item.copyWithCount(1); // Paper - shrink below and single item in event
++                org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(serverLevel, blockSource.pos());
++                org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleItemStack);
++
++                org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0));
++                if (!DispenserBlock.eventFired) {
++                    serverLevel.getCraftServer().getPluginManager().callEvent(event);
++                }
++
++                if (event.isCancelled()) {
++                    // item.grow(1); // Paper - shrink below
++                    return item;
++                }
++
++                boolean shrink = true; // Paper
++                if (!event.getItem().equals(craftItem)) {
++                    shrink = false; // Paper - shrink below
++                    // Chain to handler for new item
++                    ItemStack eventStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem());
++                    DispenseItemBehavior dispenseBehavior = DispenserBlock.getDispenseBehavior(blockSource, eventStack); // Paper - Fix NPE with equippable and items without behavior
++                    if (dispenseBehavior != DispenseItemBehavior.NOOP && dispenseBehavior != this) {
++                        dispenseBehavior.dispense(blockSource, eventStack);
++                        return item;
++                    }
++                    // Paper start - track changed items in the dispense event
++                    singleItemStack = org.bukkit.craftbukkit.inventory.CraftItemStack.unwrap(event.getItem()); // unwrap is safe because the stack won't be modified
++                    type = ((SpawnEggItem) singleItemStack.getItem()).getType(serverLevel.registryAccess(), singleItemStack);
++                    // Paper end - track changed item from dispense event
++                }
+                 try {
+                     type.spawn(
+-                        blockSource.level(), item, null, blockSource.pos().relative(direction), EntitySpawnReason.DISPENSER, direction != Direction.UP, false
++                        blockSource.level(), singleItemStack, null, blockSource.pos().relative(direction), EntitySpawnReason.DISPENSER, direction != Direction.UP, false // Paper - track changed item in dispense event
+                     );
+                 } catch (Exception var6) {
+                     LOGGER.error("Error while dispensing spawn egg from dispenser at {}", blockSource.pos(), var6);
+                     return ItemStack.EMPTY;
+                 }
+ 
+-                item.shrink(1);
++                if (shrink) item.shrink(1); // Paper - actually handle here
++                // CraftBukkit end
+                 blockSource.level().gameEvent(null, GameEvent.ENTITY_PLACE, blockSource.pos());
+                 return item;
+             }
+@@ -109,12 +_,40 @@
+                     Direction direction = blockSource.state().getValue(DispenserBlock.FACING);
+                     BlockPos blockPos = blockSource.pos().relative(direction);
+                     ServerLevel serverLevel = blockSource.level();
++                    // CraftBukkit start
++                    ItemStack singleItemStack = item.copyWithCount(1); // Paper - shrink below and single item in event
++                    org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(serverLevel, blockSource.pos());
++                    org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleItemStack);
++
++                    org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0));
++                    if (!DispenserBlock.eventFired) {
++                        serverLevel.getCraftServer().getPluginManager().callEvent(event);
++                    }
++
++                    if (event.isCancelled()) {
++                        // item.grow(1); // Paper - shrink below
++                        return item;
++                    }
++
++                    boolean shrink = true; // Paper
++                    if (!event.getItem().equals(craftItem)) {
++                        shrink = false; // Paper - shrink below
++                        // Chain to handler for new item
++                        ItemStack eventStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem());
++                        DispenseItemBehavior dispenseBehavior = DispenserBlock.getDispenseBehavior(blockSource, eventStack); // Paper - Fix NPE with equippable and items without behavior
++                        if (dispenseBehavior != DispenseItemBehavior.NOOP && dispenseBehavior != this) {
++                            dispenseBehavior.dispense(blockSource, eventStack);
++                            return item;
++                        }
++                    }
++                    // CraftBukkit end
++                    final ItemStack newStack = org.bukkit.craftbukkit.inventory.CraftItemStack.unwrap(event.getItem()); // Paper - use event itemstack (unwrap is fine here because the stack won't be modified)
+                     Consumer<ArmorStand> consumer = EntityType.appendDefaultStackConfig(
+-                        armorStand1 -> armorStand1.setYRot(direction.toYRot()), serverLevel, item, null
++                        armorStand1 -> armorStand1.setYRot(direction.toYRot()), serverLevel, newStack, null // Paper - track changed items in the dispense event
+                     );
+                     ArmorStand armorStand = EntityType.ARMOR_STAND.spawn(serverLevel, consumer, blockPos, EntitySpawnReason.DISPENSER, false, false);
+                     if (armorStand != null) {
+-                        item.shrink(1);
++                        if (shrink) item.shrink(1); // Paper - actually handle here
+                     }
+ 
+                     return item;
+@@ -134,7 +_,36 @@
+                             livingEntity -> livingEntity instanceof Saddleable saddleable && !saddleable.isSaddled() && saddleable.isSaddleable()
+                         );
+                     if (!entitiesOfClass.isEmpty()) {
+-                        ((Saddleable)entitiesOfClass.get(0)).equipSaddle(item.split(1), SoundSource.BLOCKS);
++                        // CraftBukkit start
++                        ItemStack singleItemStack = item.copyWithCount(1); // Paper - shrink below and single item in event
++                        ServerLevel world = blockSource.level();
++                        org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(world, blockSource.pos());
++                        org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleItemStack);
++
++                        org.bukkit.event.block.BlockDispenseArmorEvent event = new org.bukkit.event.block.BlockDispenseArmorEvent(block, craftItem.clone(), entitiesOfClass.get(0).getBukkitLivingEntity());
++                        if (!DispenserBlock.eventFired) {
++                            world.getCraftServer().getPluginManager().callEvent(event);
++                        }
++
++                        if (event.isCancelled()) {
++                            // item.grow(1); // Paper - shrink below
++                            return item;
++                        }
++
++                        boolean shrink = true; // Paper
++                        if (!event.getItem().equals(craftItem)) {
++                            shrink = false; // Paper - shrink below
++                            // Chain to handler for new item
++                            ItemStack eventStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem());
++                            DispenseItemBehavior dispenseBehavior = DispenserBlock.getDispenseBehavior(blockSource, eventStack); // Paper - Fix NPE with equippable and items without behavior
++                            if (dispenseBehavior != DispenseItemBehavior.NOOP && dispenseBehavior != this) { // Paper - fix possible StackOverflowError
++                                dispenseBehavior.dispense(blockSource, eventStack);
++                                return item;
++                            }
++                        }
++                        ((Saddleable) entitiesOfClass.get(0)).equipSaddle(org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem()), SoundSource.BLOCKS); // Paper - track changed items in dispense event
++                        // CraftBukkit end
++                        if (shrink) item.shrink(1); // Paper - actually handle here
+                         this.setSuccess(true);
+                         return item;
+                     } else {
+@@ -156,8 +_,36 @@
+                             new AABB(blockPos),
+                             abstractChestedHorse1 -> abstractChestedHorse1.isAlive() && !abstractChestedHorse1.hasChest()
+                         )) {
+-                        if (abstractChestedHorse.isTamed() && abstractChestedHorse.getSlot(499).set(item)) {
+-                            item.shrink(1);
++                        if (abstractChestedHorse.isTamed()/* && abstractChestedHorse.getSlot(499).set(item)*/) {
++                            ItemStack singleCopy = item.copyWithCount(1); // Paper - shrink below
++                            ServerLevel world = blockSource.level();
++                            org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(world, blockSource.pos());
++                            org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleCopy);
++                            org.bukkit.event.block.BlockDispenseArmorEvent event = new org.bukkit.event.block.BlockDispenseArmorEvent(block, craftItem.clone(), abstractChestedHorse.getBukkitLivingEntity());
++                            if (!DispenserBlock.eventFired) {
++                                world.getCraftServer().getPluginManager().callEvent(event);
++                            }
++
++                            if (event.isCancelled()) {
++                                // stack.grow(1); // Paper - shrink below (this was actually missing and should be here, added it commented out to be consistent)
++                                return item;
++                            }
++
++                            boolean shrink = true; // Paper
++                            if (!event.getItem().equals(craftItem)) {
++                                shrink = false; // Paper - shrink below
++                                // Chain to handler for new item
++                                ItemStack eventStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem());
++                                DispenseItemBehavior dispenseBehavior = DispenserBlock.getDispenseBehavior(blockSource, eventStack); // Paper - Fix NPE with equippable and items without behavior
++                                if (dispenseBehavior != DispenseItemBehavior.NOOP && dispenseBehavior != this) { // Paper - fix possible StackOverflowError
++                                    dispenseBehavior.dispense(blockSource, eventStack);
++                                    return item;
++                                }
++                            }
++                            abstractChestedHorse.getSlot(499).set(org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem()));
++                            // CraftBukkit end
++
++                            if (shrink) item.shrink(1); // Paper - actually handle here
+                             this.setSuccess(true);
+                             return item;
+                         }
+@@ -195,8 +_,50 @@
+                 DispensibleContainerItem dispensibleContainerItem = (DispensibleContainerItem)item.getItem();
+                 BlockPos blockPos = blockSource.pos().relative(blockSource.state().getValue(DispenserBlock.FACING));
+                 Level level = blockSource.level();
++                // CraftBukkit start
++                int x = blockPos.getX();
++                int y = blockPos.getY();
++                int z = blockPos.getZ();
++                BlockState iblockdata = level.getBlockState(blockPos);
++                ItemStack dispensedItem = item; // Paper - track changed item from the dispense event
++                // Paper start - correctly check if the bucket place will succeed
++                /* Taken from SolidBucketItem#emptyContents */
++                boolean willEmptyContentsSolidBucketItem = dispensibleContainerItem instanceof net.minecraft.world.item.SolidBucketItem && level.isInWorldBounds(blockPos) && iblockdata.isAir();
++                /* Taken from BucketItem#emptyContents */
++                boolean willEmptyBucketItem = dispensibleContainerItem instanceof final net.minecraft.world.item.BucketItem bucketItem && bucketItem.content instanceof net.minecraft.world.level.material.FlowingFluid && (iblockdata.isAir() || iblockdata.canBeReplaced(bucketItem.content) || (iblockdata.getBlock() instanceof net.minecraft.world.level.block.LiquidBlockContainer liquidBlockContainer && liquidBlockContainer.canPlaceLiquid(null, level, blockPos, iblockdata, bucketItem.content)));
++                if (willEmptyContentsSolidBucketItem || willEmptyBucketItem) {
++                // Paper end - correctly check if the bucket place will succeed
++                    org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(level, blockSource.pos());
++                    org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event
++
++                    org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(x, y, z));
++                    if (!DispenserBlock.eventFired) {
++                        level.getCraftServer().getPluginManager().callEvent(event);
++                    }
++
++                    if (event.isCancelled()) {
++                        return item;
++                    }
++
++                    if (!event.getItem().equals(craftItem)) {
++                        // Chain to handler for new item
++                        ItemStack eventStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem());
++                        DispenseItemBehavior dispenseBehavior = DispenserBlock.getDispenseBehavior(blockSource, eventStack); // Paper - Fix NPE with equippable and items without behavior
++                        if (dispenseBehavior != DispenseItemBehavior.NOOP && dispenseBehavior != this) {
++                            dispenseBehavior.dispense(blockSource, eventStack);
++                            return item;
++                        }
++                    }
++
++                    // Paper start - track changed item from dispense event
++                    dispensedItem = org.bukkit.craftbukkit.inventory.CraftItemStack.unwrap(event.getItem()); // unwrap is safe here as the stack isn't mutated
++                    dispensibleContainerItem = (DispensibleContainerItem) dispensedItem.getItem();
++                    // Paper end - track changed item from dispense event
++                }
++                // CraftBukkit end
++
+                 if (dispensibleContainerItem.emptyContents(null, level, blockPos, null)) {
+-                    dispensibleContainerItem.checkExtraContent(null, level, item, blockPos);
++                    dispensibleContainerItem.checkExtraContent(null, level, dispensedItem, blockPos); // Paper - track changed item from dispense event
+                     return this.consumeWithRemainder(blockSource, item, new ItemStack(Items.BUCKET));
+                 } else {
+                     return this.defaultDispenseItemBehavior.dispense(blockSource, item);
+@@ -219,12 +_,37 @@
+                 BlockPos blockPos = blockSource.pos().relative(blockSource.state().getValue(DispenserBlock.FACING));
+                 BlockState blockState = levelAccessor.getBlockState(blockPos);
+                 if (blockState.getBlock() instanceof BucketPickup bucketPickup) {
+-                    ItemStack itemStack = bucketPickup.pickupBlock(null, levelAccessor, blockPos, blockState);
++                    ItemStack itemStack = bucketPickup.pickupBlock(null, org.bukkit.craftbukkit.util.DummyGeneratorAccess.INSTANCE, blockPos, blockState); // CraftBukkit
+                     if (itemStack.isEmpty()) {
+                         return super.execute(blockSource, item);
+                     } else {
+                         levelAccessor.gameEvent(null, GameEvent.FLUID_PICKUP, blockPos);
+                         Item item1 = itemStack.getItem();
++                        // CraftBukkit start
++                        org.bukkit.block.Block bukkitBlock = org.bukkit.craftbukkit.block.CraftBlock.at(levelAccessor, blockSource.pos());
++                        org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event
++
++                        org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(blockPos));
++                        if (!DispenserBlock.eventFired) {
++                            levelAccessor.getMinecraftWorld().getCraftServer().getPluginManager().callEvent(event);
++                        }
++
++                        if (event.isCancelled()) {
++                            return item;
++                        }
++
++                        if (!event.getItem().equals(craftItem)) {
++                            // Chain to handler for new item
++                            ItemStack eventStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem());
++                            DispenseItemBehavior dispenseBehavior = DispenserBlock.getDispenseBehavior(blockSource, eventStack); // Paper - Fix NPE with equippable and items without behavior
++                            if (dispenseBehavior != DispenseItemBehavior.NOOP && dispenseBehavior != this) {
++                                dispenseBehavior.dispense(blockSource, eventStack);
++                                return item;
++                            }
++                        }
++
++                        itemStack = bucketPickup.pickupBlock(null, levelAccessor, blockPos, blockState); // From above
++                        // CraftBukkit end
+                         return this.consumeWithRemainder(blockSource, item, new ItemStack(item1));
+                     }
+                 } else {
+@@ -236,17 +_,44 @@
+             @Override
+             protected ItemStack execute(BlockSource blockSource, ItemStack item) {
+                 ServerLevel serverLevel = blockSource.level();
++                // CraftBukkit start
++                org.bukkit.block.Block bukkitBlock = org.bukkit.craftbukkit.block.CraftBlock.at(serverLevel, blockSource.pos());
++                org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item); // Paper - ignore stack size on damageable items
++
++                org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0));
++                if (!DispenserBlock.eventFired) {
++                    serverLevel.getCraftServer().getPluginManager().callEvent(event);
++                }
++
++                if (event.isCancelled()) {
++                    return item;
++                }
++
++                if (!event.getItem().equals(craftItem)) {
++                    // Chain to handler for new item
++                    ItemStack eventStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem());
++                    DispenseItemBehavior dispenseBehavior = DispenserBlock.getDispenseBehavior(blockSource, eventStack); // Paper - Fix NPE with equippable and items without behavior
++                    if (dispenseBehavior != DispenseItemBehavior.NOOP && dispenseBehavior != this) {
++                        dispenseBehavior.dispense(blockSource, eventStack);
++                        return item;
++                    }
++                }
++                // CraftBukkit end
+                 this.setSuccess(true);
+                 Direction direction = blockSource.state().getValue(DispenserBlock.FACING);
+                 BlockPos blockPos = blockSource.pos().relative(direction);
+                 BlockState blockState = serverLevel.getBlockState(blockPos);
+                 if (BaseFireBlock.canBePlacedAt(serverLevel, blockPos, direction)) {
+-                    serverLevel.setBlockAndUpdate(blockPos, BaseFireBlock.getState(serverLevel, blockPos));
+-                    serverLevel.gameEvent(null, GameEvent.BLOCK_PLACE, blockPos);
++                    // CraftBukkit start - Ignition by dispensing flint and steel
++                    if (!org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(serverLevel, blockPos, blockSource.pos()).isCancelled()) {
++                        serverLevel.setBlockAndUpdate(blockPos, BaseFireBlock.getState(serverLevel, blockPos));
++                        serverLevel.gameEvent(null, GameEvent.BLOCK_PLACE, blockPos);
++                    }
++                    // CraftBukkit end
+                 } else if (CampfireBlock.canLight(blockState) || CandleBlock.canLight(blockState) || CandleCakeBlock.canLight(blockState)) {
+                     serverLevel.setBlockAndUpdate(blockPos, blockState.setValue(BlockStateProperties.LIT, Boolean.valueOf(true)));
+                     serverLevel.gameEvent(null, GameEvent.BLOCK_CHANGE, blockPos);
+-                } else if (blockState.getBlock() instanceof TntBlock) {
++                } else if (blockState.getBlock() instanceof TntBlock && org.bukkit.craftbukkit.event.CraftEventFactory.callTNTPrimeEvent(serverLevel, blockPos, org.bukkit.event.block.TNTPrimeEvent.PrimeCause.DISPENSER, null, blockSource.pos())) { // CraftBukkit - TNTPrimeEvent
+                     TntBlock.explode(serverLevel, blockPos);
+                     serverLevel.removeBlock(blockPos, false);
+                 } else {
+@@ -266,11 +_,62 @@
+                 this.setSuccess(true);
+                 Level level = blockSource.level();
+                 BlockPos blockPos = blockSource.pos().relative(blockSource.state().getValue(DispenserBlock.FACING));
++                // CraftBukkit start
++                org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(level, blockSource.pos());
++                org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event
++
++                org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0));
++                if (!DispenserBlock.eventFired) {
++                    level.getCraftServer().getPluginManager().callEvent(event);
++                }
++
++                if (event.isCancelled()) {
++                    return item;
++                }
++
++                if (!event.getItem().equals(craftItem)) {
++                    // Chain to handler for new item
++                    ItemStack eventStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem());
++                    DispenseItemBehavior dispenseBehavior = DispenserBlock.getDispenseBehavior(blockSource, eventStack); // Paper - Fix NPE with equippable and items without behavior
++                    if (dispenseBehavior != DispenseItemBehavior.NOOP && dispenseBehavior != this) {
++                        dispenseBehavior.dispense(blockSource, eventStack);
++                        return item;
++                    }
++                }
++
++                level.captureTreeGeneration = true;
++                // CraftBukkit end
+                 if (!BoneMealItem.growCrop(item, level, blockPos) && !BoneMealItem.growWaterPlant(item, level, blockPos, null)) {
+                     this.setSuccess(false);
+                 } else if (!level.isClientSide) {
+                     level.levelEvent(1505, blockPos, 15);
+                 }
++                // CraftBukkit start
++                level.captureTreeGeneration = false;
++                if (level.capturedBlockStates.size() > 0) {
++                    org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeType;
++                    net.minecraft.world.level.block.SaplingBlock.treeType = null;
++                    org.bukkit.Location location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(blockPos, level.getWorld());
++                    List<org.bukkit.block.BlockState> blocks = new java.util.ArrayList<>(level.capturedBlockStates.values());
++                    level.capturedBlockStates.clear();
++                    org.bukkit.event.world.StructureGrowEvent structureEvent = null;
++                    if (treeType != null) {
++                        structureEvent = new org.bukkit.event.world.StructureGrowEvent(location, treeType, false, null, blocks);
++                        org.bukkit.Bukkit.getPluginManager().callEvent(structureEvent);
++                    }
++
++                    org.bukkit.event.block.BlockFertilizeEvent fertilizeEvent = new org.bukkit.event.block.BlockFertilizeEvent(location.getBlock(), null, blocks);
++                    fertilizeEvent.setCancelled(structureEvent != null && structureEvent.isCancelled());
++                    org.bukkit.Bukkit.getPluginManager().callEvent(fertilizeEvent);
++
++                    if (!fertilizeEvent.isCancelled()) {
++                        for (org.bukkit.block.BlockState blockstate : blocks) {
++                            blockstate.update(true);
++                            blockSource.level().checkCapturedTreeStateForObserverNotify(blockPos, (org.bukkit.craftbukkit.block.CraftBlockState) blockstate); // Paper - notify observers even if grow failed
++                        }
++                    }
++                }
++                // CraftBukkit end
+ 
+                 return item;
+             }
+@@ -280,11 +_,39 @@
+             protected ItemStack execute(BlockSource blockSource, ItemStack item) {
+                 Level level = blockSource.level();
+                 BlockPos blockPos = blockSource.pos().relative(blockSource.state().getValue(DispenserBlock.FACING));
+-                PrimedTnt primedTnt = new PrimedTnt(level, blockPos.getX() + 0.5, blockPos.getY(), blockPos.getZ() + 0.5, null);
++                // CraftBukkit start
++                ItemStack singleItemStack = item.copyWithCount(1); // Paper - shrink at end and single item in event
++                org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(level, blockSource.pos());
++                org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleItemStack);
++
++                org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector((double) blockPos.getX() + 0.5D, (double) blockPos.getY(), (double) blockPos.getZ() + 0.5D));
++                if (!DispenserBlock.eventFired) {
++                    level.getCraftServer().getPluginManager().callEvent(event);
++                }
++
++                if (event.isCancelled()) {
++                    // item.grow(1); // Paper - shrink below
++                    return item;
++                }
++
++                boolean shrink = true; // Paper
++                if (!event.getItem().equals(craftItem)) {
++                    shrink = false; // Paper - shrink below
++                    // Chain to handler for new item
++                    ItemStack eventStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem());
++                    DispenseItemBehavior dispenseBehavior = DispenserBlock.getDispenseBehavior(blockSource, eventStack); // Paper - Fix NPE with equippable and items without behavior
++                    if (dispenseBehavior != DispenseItemBehavior.NOOP && dispenseBehavior != this) {
++                        dispenseBehavior.dispense(blockSource, eventStack);
++                        return item;
++                    }
++                }
++
++                PrimedTnt primedTnt = new PrimedTnt(level, event.getVelocity().getX(), event.getVelocity().getY(), event.getVelocity().getZ(), null);
++                // CraftBukkit end
+                 level.addFreshEntity(primedTnt);
+                 level.playSound(null, primedTnt.getX(), primedTnt.getY(), primedTnt.getZ(), SoundEvents.TNT_PRIMED, SoundSource.BLOCKS, 1.0F, 1.0F);
+                 level.gameEvent(null, GameEvent.ENTITY_PLACE, blockPos);
+-                item.shrink(1);
++                if (shrink) item.shrink(1); // Paper - actually handle here
+                 return item;
+             }
+         });
+@@ -296,6 +_,29 @@
+                     Level level = blockSource.level();
+                     Direction direction = blockSource.state().getValue(DispenserBlock.FACING);
+                     BlockPos blockPos = blockSource.pos().relative(direction);
++                    // CraftBukkit start
++                    org.bukkit.block.Block bukkitBlock = org.bukkit.craftbukkit.block.CraftBlock.at(level, blockSource.pos());
++                    org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event
++
++                    org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(blockPos));
++                    if (!DispenserBlock.eventFired) {
++                        level.getCraftServer().getPluginManager().callEvent(event);
++                    }
++
++                    if (event.isCancelled()) {
++                        return item;
++                    }
++
++                    if (!event.getItem().equals(craftItem)) {
++                        // Chain to handler for new item
++                        ItemStack eventStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem());
++                        DispenseItemBehavior dispenseBehavior = DispenserBlock.getDispenseBehavior(blockSource, eventStack); // Paper - Fix NPE with equippable and items without behavior
++                        if (dispenseBehavior != DispenseItemBehavior.NOOP && dispenseBehavior != this) {
++                            dispenseBehavior.dispense(blockSource, eventStack);
++                            return item;
++                        }
++                    }
++                    // CraftBukkit end
+                     if (level.isEmptyBlock(blockPos) && WitherSkullBlock.canSpawnMob(level, blockPos, item)) {
+                         level.setBlock(
+                             blockPos,
+@@ -313,7 +_,7 @@
+                         item.shrink(1);
+                         this.setSuccess(true);
+                     } else {
+-                        this.setSuccess(EquipmentDispenseItemBehavior.dispenseEquipment(blockSource, item));
++                        this.setSuccess(EquipmentDispenseItemBehavior.dispenseEquipment(blockSource, item, this)); // Paper - fix possible StackOverflowError
+                     }
+ 
+                     return item;
+@@ -326,6 +_,29 @@
+                 Level level = blockSource.level();
+                 BlockPos blockPos = blockSource.pos().relative(blockSource.state().getValue(DispenserBlock.FACING));
+                 CarvedPumpkinBlock carvedPumpkinBlock = (CarvedPumpkinBlock)Blocks.CARVED_PUMPKIN;
++                // CraftBukkit start
++                org.bukkit.block.Block bukkitBlock = org.bukkit.craftbukkit.block.CraftBlock.at(level, blockSource.pos());
++                org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event
++
++                org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(blockPos));
++                if (!DispenserBlock.eventFired) {
++                    level.getCraftServer().getPluginManager().callEvent(event);
++                }
++
++                if (event.isCancelled()) {
++                    return item;
++                }
++
++                if (!event.getItem().equals(craftItem)) {
++                    // Chain to handler for new item
++                    ItemStack eventStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem());
++                    DispenseItemBehavior dispenseBehavior = DispenserBlock.getDispenseBehavior(blockSource, eventStack); // Paper - Fix NPE with equippable and items without behavior
++                    if (dispenseBehavior != DispenseItemBehavior.NOOP && dispenseBehavior != this) {
++                        dispenseBehavior.dispense(blockSource, eventStack);
++                        return item;
++                    }
++                }
++                // CraftBukkit end
+                 if (level.isEmptyBlock(blockPos) && carvedPumpkinBlock.canSpawnGolem(level, blockPos)) {
+                     if (!level.isClientSide) {
+                         level.setBlock(blockPos, carvedPumpkinBlock.defaultBlockState(), 3);
+@@ -335,7 +_,7 @@
+                     item.shrink(1);
+                     this.setSuccess(true);
+                 } else {
+-                    this.setSuccess(EquipmentDispenseItemBehavior.dispenseEquipment(blockSource, item));
++                    this.setSuccess(EquipmentDispenseItemBehavior.dispenseEquipment(blockSource, item, this)); // Paper - fix possible StackOverflowError
+                 }
+ 
+                 return item;
+@@ -361,6 +_,29 @@
+                     ServerLevel serverLevel = blockSource.level();
+                     BlockPos blockPos = blockSource.pos().relative(blockSource.state().getValue(DispenserBlock.FACING));
+                     BlockState blockState = serverLevel.getBlockState(blockPos);
++                    // CraftBukkit start
++                    org.bukkit.block.Block bukkitBlock = org.bukkit.craftbukkit.block.CraftBlock.at(serverLevel, blockSource.pos());
++                    org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - only single item in event
++
++                    org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(blockPos));
++                    if (!DispenserBlock.eventFired) {
++                        serverLevel.getCraftServer().getPluginManager().callEvent(event);
++                    }
++
++                    if (event.isCancelled()) {
++                        return item;
++                    }
++
++                    if (!event.getItem().equals(craftItem)) {
++                        // Chain to handler for new item
++                        ItemStack eventStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem());
++                        DispenseItemBehavior dispenseBehavior = DispenserBlock.getDispenseBehavior(blockSource, eventStack); // Paper - Fix NPE with equippable and items without behavior
++                        if (dispenseBehavior != DispenseItemBehavior.NOOP && dispenseBehavior != this) {
++                            dispenseBehavior.dispense(blockSource, eventStack);
++                            return item;
++                        }
++                    }
++                    // CraftBukkit end
+                     if (blockState.is(
+                             BlockTags.BEEHIVES,
+                             blockStateBase -> blockStateBase.hasProperty(BeehiveBlock.HONEY_LEVEL) && blockStateBase.getBlock() instanceof BeehiveBlock
+@@ -389,6 +_,13 @@
+                 this.setSuccess(true);
+                 if (blockState.is(Blocks.RESPAWN_ANCHOR)) {
+                     if (blockState.getValue(RespawnAnchorBlock.CHARGE) != 4) {
++                        // Paper start - Call missing BlockDispenseEvent
++                        ItemStack result = org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockDispenseEvent(blockSource, blockPos, item, this);
++                        if (result != null) {
++                            this.setSuccess(false);
++                            return result;
++                        }
++                        // Paper end - Call missing BlockDispenseEvent
+                         RespawnAnchorBlock.charge(null, level, blockPos, blockState);
+                         item.shrink(1);
+                     } else {
+@@ -412,6 +_,29 @@
+                     this.setSuccess(false);
+                     return item;
+                 } else {
++                    // CraftBukkit start
++                    org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(serverLevel, blockSource.pos());
++                    org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item); // Paper - ignore stack size on damageable items
++
++                    org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseArmorEvent(block, craftItem.clone(), entitiesOfClass.get(0).getBukkitLivingEntity());
++                    if (!DispenserBlock.eventFired) {
++                        serverLevel.getCraftServer().getPluginManager().callEvent(event);
++                    }
++
++                    if (event.isCancelled()) {
++                        return item;
++                    }
++
++                    if (!event.getItem().equals(craftItem)) {
++                        // Chain to handler for new item
++                        ItemStack eventStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem());
++                        DispenseItemBehavior dispenseBehavior = DispenserBlock.getDispenseBehavior(blockSource, eventStack); // Paper - Fix NPE with equippable and items without behavior
++                        if (dispenseBehavior != DispenseItemBehavior.NOOP && dispenseBehavior != this) { // Paper - fix possible StackOverflowError
++                            dispenseBehavior.dispense(blockSource, eventStack);
++                            return item;
++                        }
++                    }
++                    // CraftBukkit end
+                     for (Armadillo armadillo : entitiesOfClass) {
+                         if (armadillo.brushOffScute()) {
+                             item.hurtAndBreak(16, serverLevel, null, item1 -> {});
+@@ -432,6 +_,13 @@
+                 BlockState blockState = level.getBlockState(blockPos);
+                 Optional<BlockState> waxed = HoneycombItem.getWaxed(blockState);
+                 if (waxed.isPresent()) {
++                    // Paper start - Call missing BlockDispenseEvent
++                    ItemStack result = org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockDispenseEvent(blockSource, blockPos, item, this);
++                    if (result != null) {
++                        this.setSuccess(false);
++                        return result;
++                    }
++                    // Paper end - Call missing BlockDispenseEvent
+                     level.setBlockAndUpdate(blockPos, waxed.get());
+                     level.levelEvent(3003, blockPos, 0);
+                     item.shrink(1);
+@@ -459,6 +_,12 @@
+                         if (!serverLevel.getBlockState(blockPos1).is(BlockTags.CONVERTABLE_TO_MUD)) {
+                             return this.defaultDispenseItemBehavior.dispense(blockSource, item);
+                         } else {
++                            // Paper start - Call missing BlockDispenseEvent
++                            ItemStack result = org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockDispenseEvent(blockSource, blockPos1, item, this);
++                            if (result != null) {
++                                return result;
++                            }
++                            // Paper end - Call missing BlockDispenseEvent
+                             if (!serverLevel.isClientSide) {
+                                 for (int i = 0; i < 5; i++) {
+                                     serverLevel.sendParticles(
diff --git a/paper-server/patches/sources/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java.patch b/paper-server/patches/sources/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java.patch
new file mode 100644
index 0000000000..08275c166f
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java.patch
@@ -0,0 +1,65 @@
+--- a/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java
++++ b/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java
+@@ -14,10 +_,16 @@
+ 
+     @Override
+     protected ItemStack execute(BlockSource blockSource, ItemStack item) {
+-        return dispenseEquipment(blockSource, item) ? item : super.execute(blockSource, item);
++        return dispenseEquipment(blockSource, item, this) ? item : super.execute(blockSource, item); // Paper - fix possible StackOverflowError
+     }
+ 
++    @Deprecated @io.papermc.paper.annotation.DoNotUse // Paper
+     public static boolean dispenseEquipment(BlockSource blockSource, ItemStack item) {
++        // Paper start
++        return dispenseEquipment(blockSource, item, null);
++    }
++    public static boolean dispenseEquipment(BlockSource blockSource, ItemStack item, @javax.annotation.Nullable DispenseItemBehavior currentBehavior) {
++        // Paper end
+         BlockPos blockPos = blockSource.pos().relative(blockSource.state().getValue(DispenserBlock.FACING));
+         List<LivingEntity> entitiesOfClass = blockSource.level()
+             .getEntitiesOfClass(LivingEntity.class, new AABB(blockPos), entity -> entity.canEquipWithDispenser(item));
+@@ -26,13 +_,42 @@
+         } else {
+             LivingEntity livingEntity = entitiesOfClass.getFirst();
+             EquipmentSlot equipmentSlotForItem = livingEntity.getEquipmentSlotForItem(item);
+-            ItemStack itemStack = item.split(1);
+-            livingEntity.setItemSlot(equipmentSlotForItem, itemStack);
++            ItemStack itemStack = item.copyWithCount(1); // Paper - shrink below and single item in event
++            // CraftBukkit start
++            net.minecraft.world.level.Level world = blockSource.level();
++            org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(world, blockSource.pos());
++            org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack);
++
++            org.bukkit.event.block.BlockDispenseArmorEvent event = new org.bukkit.event.block.BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) livingEntity.getBukkitEntity());
++            if (!DispenserBlock.eventFired) {
++                world.getCraftServer().getPluginManager().callEvent(event);
++            }
++
++            if (event.isCancelled()) {
++                // stack.grow(1); // Paper - shrink below
++                return false;
++            }
++
++            boolean shrink = true; // Paper
++            if (!event.getItem().equals(craftItem)) {
++                shrink = false; // Paper - shrink below
++                // Chain to handler for new item
++                ItemStack eventStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem());
++                DispenseItemBehavior dispenseItemBehavior = DispenserBlock.getDispenseBehavior(blockSource, eventStack); // Paper - Fix NPE with equippable and items without behavior
++                if (dispenseItemBehavior != DispenseItemBehavior.NOOP && (currentBehavior == null || dispenseItemBehavior != currentBehavior)) { // Paper - fix possible StackOverflowError
++                    dispenseItemBehavior.dispense(blockSource, eventStack);
++                    return true;
++                }
++            }
++
++            livingEntity.setItemSlot(equipmentSlotForItem, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem()));
++            // CraftBukkit end
+             if (livingEntity instanceof Mob mob) {
+                 mob.setDropChance(equipmentSlotForItem, 2.0F);
+                 mob.setPersistenceRequired();
+             }
+ 
++            if (shrink) item.shrink(1); // Paper - shrink here
+             return true;
+         }
+     }
diff --git a/paper-server/patches/sources/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java.patch b/paper-server/patches/sources/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java.patch
new file mode 100644
index 0000000000..1f891175a1
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java.patch
@@ -0,0 +1,46 @@
+--- a/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java
++++ b/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java
+@@ -57,12 +_,38 @@
+         }
+ 
+         Vec3 vec31 = new Vec3(d, d1 + d3, d2);
+-        AbstractMinecart abstractMinecart = AbstractMinecart.createMinecart(
+-            serverLevel, vec31.x, vec31.y, vec31.z, this.entityType, EntitySpawnReason.DISPENSER, item, null
+-        );
++        ItemStack itemstack1 = item.copyWithCount(1); // Paper - shrink below and single item in event
++        org.bukkit.block.Block block2 = org.bukkit.craftbukkit.block.CraftBlock.at(serverLevel, blockSource.pos());
++        org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack1);
++
++        org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block2, craftItem.clone(), new org.bukkit.util.Vector(vec31.x, vec31.y, vec31.z));
++        if (!DispenserBlock.eventFired) {
++            serverLevel.getCraftServer().getPluginManager().callEvent(event);
++        }
++
++        if (event.isCancelled()) {
++            // stack.grow(1); // Paper - shrink below
++            return item;
++        }
++
++        boolean shrink = true; // Paper
++        if (!event.getItem().equals(craftItem)) {
++            shrink = false; // Paper - shrink below
++            // Chain to handler for new item
++            ItemStack eventStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem());
++            DispenseItemBehavior dispenseItemBehavior = DispenserBlock.getDispenseBehavior(blockSource, eventStack); // Paper - Fix NPE with equippable and items without behavior
++            if (dispenseItemBehavior != DispenseItemBehavior.NOOP && dispenseItemBehavior != this) {
++                dispenseItemBehavior.dispense(blockSource, eventStack);
++                return item;
++            }
++        }
++
++        itemstack1 = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem());
++        AbstractMinecart abstractMinecart = AbstractMinecart.createMinecart(serverLevel, event.getVelocity().getX(), event.getVelocity().getY(), event.getVelocity().getZ(), this.entityType, EntitySpawnReason.DISPENSER, itemstack1, null);
++
+         if (abstractMinecart != null) {
+-            serverLevel.addFreshEntity(abstractMinecart);
+-            item.shrink(1);
++            if (serverLevel.addFreshEntity(abstractMinecart) && shrink) item.shrink(1); // Paper - if entity add was successful and supposed to shrink
++            // CraftBukkit end
+         }
+ 
+         return item;
diff --git a/paper-server/patches/sources/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java.patch b/paper-server/patches/sources/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java.patch
new file mode 100644
index 0000000000..73ef78c798
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java.patch
@@ -0,0 +1,53 @@
+--- a/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java
++++ b/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java
+@@ -27,17 +_,39 @@
+         ServerLevel serverLevel = blockSource.level();
+         Direction direction = blockSource.state().getValue(DispenserBlock.FACING);
+         Position dispensePosition = this.dispenseConfig.positionFunction().getDispensePosition(blockSource, direction);
+-        Projectile.spawnProjectileUsingShoot(
+-            this.projectileItem.asProjectile(serverLevel, dispensePosition, item, direction),
+-            serverLevel,
+-            item,
+-            direction.getStepX(),
+-            direction.getStepY(),
+-            direction.getStepZ(),
+-            this.dispenseConfig.power(),
+-            this.dispenseConfig.uncertainty()
+-        );
+-        item.shrink(1);
++        ItemStack itemstack1 = item.copyWithCount(1); // Paper - shrink below and single item in event
++        org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(serverLevel, blockSource.pos());
++        org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack1);
++
++        org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector((double) direction.getStepX(), (double) direction.getStepY(), (double) direction.getStepZ()));
++        if (!DispenserBlock.eventFired) {
++            serverLevel.getCraftServer().getPluginManager().callEvent(event);
++        }
++
++        if (event.isCancelled()) {
++            // item.grow(1); // Paper - shrink below
++            return item;
++        }
++
++        boolean shrink = true; // Paper
++        if (!event.getItem().equals(craftItem)) {
++            shrink = false; // Paper - shrink below
++            // Chain to handler for new item
++            ItemStack eventStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem());
++            DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(blockSource, eventStack); // Paper - Fix NPE with equippable and items without behavior
++            if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
++                idispensebehavior.dispense(blockSource, eventStack);
++                return item;
++            }
++        }
++
++        // SPIGOT-7923: Avoid create projectiles with empty item
++        if (!itemstack1.isEmpty()) {
++            Projectile iprojectile = Projectile.spawnProjectileUsingShoot(this.projectileItem.asProjectile(serverLevel, dispensePosition, org.bukkit.craftbukkit.inventory.CraftItemStack.unwrap(event.getItem()), direction), serverLevel, itemstack1, event.getVelocity().getX(), event.getVelocity().getY(), event.getVelocity().getZ(), this.dispenseConfig.power(), this.dispenseConfig.uncertainty()); // Paper - track changed items in the dispense event; unwrap is safe here because all uses of the stack make their own copies
++            iprojectile.projectileSource = new org.bukkit.craftbukkit.projectiles.CraftBlockProjectileSource(blockSource.blockEntity());
++        }
++        if (shrink) item.shrink(1); // Paper - actually handle here
++        // CraftBukkit end
+         return item;
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java.patch b/paper-server/patches/sources/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java.patch
new file mode 100644
index 0000000000..fb3b9ff276
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java.patch
@@ -0,0 +1,57 @@
+--- a/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java
++++ b/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java
+@@ -20,9 +_,32 @@
+     @Override
+     protected ItemStack execute(BlockSource blockSource, ItemStack item) {
+         ServerLevel serverLevel = blockSource.level();
++
++        // CraftBukkit start
++        org.bukkit.block.Block bukkitBlock = org.bukkit.craftbukkit.block.CraftBlock.at(serverLevel, blockSource.pos());
++        org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item); // Paper - ignore stack size on damageable items
++        org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0));
++        if (!DispenserBlock.eventFired) {
++            serverLevel.getCraftServer().getPluginManager().callEvent(event);
++        }
++
++        if (event.isCancelled()) {
++            return item;
++        }
++
++        if (!event.getItem().equals(craftItem)) {
++            // Chain to handler for new item
++            ItemStack eventStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem());
++            DispenseItemBehavior dispenseBehavior = DispenserBlock.getDispenseBehavior(blockSource, eventStack); // Paper - Fix NPE with equippable and items without behavior
++            if (dispenseBehavior != DispenseItemBehavior.NOOP && dispenseBehavior != this) {
++                dispenseBehavior.dispense(blockSource, eventStack);
++                return item;
++            }
++        }
++        // CraftBukkit end
+         if (!serverLevel.isClientSide()) {
+             BlockPos blockPos = blockSource.pos().relative(blockSource.state().getValue(DispenserBlock.FACING));
+-            this.setSuccess(tryShearBeehive(serverLevel, blockPos) || tryShearLivingEntity(serverLevel, blockPos, item));
++            this.setSuccess(tryShearBeehive(serverLevel, blockPos) || tryShearLivingEntity(serverLevel, blockPos, item, bukkitBlock, craftItem)); // CraftBukkit
+             if (this.isSuccess()) {
+                 item.hurtAndBreak(1, serverLevel, null, item1 -> {});
+             }
+@@ -50,10 +_,18 @@
+         return false;
+     }
+ 
+-    private static boolean tryShearLivingEntity(ServerLevel level, BlockPos pos, ItemStack stack) {
++    private static boolean tryShearLivingEntity(ServerLevel level, BlockPos pos, ItemStack stack, org.bukkit.block.Block bukkitBlock, org.bukkit.craftbukkit.inventory.CraftItemStack craftItem) { // CraftBukkit - add args
+         for (LivingEntity livingEntity : level.getEntitiesOfClass(LivingEntity.class, new AABB(pos), EntitySelector.NO_SPECTATORS)) {
+             if (livingEntity instanceof Shearable shearable && shearable.readyForShearing()) {
+-                shearable.shear(level, SoundSource.BLOCKS, stack);
++                // CraftBukkit start
++                // Paper start - Add drops to shear events
++                org.bukkit.event.block.BlockShearEntityEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callBlockShearEntityEvent(livingEntity, bukkitBlock, craftItem, shearable.generateDefaultDrops(level, stack));
++                if (event.isCancelled()) {
++                    // Paper end - Add drops to shear events
++                    continue;
++                }
++                // CraftBukkit end
++                shearable.shear(level, SoundSource.BLOCKS, stack, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getDrops())); // Paper - Add drops to shear events
+                 level.gameEvent(null, GameEvent.SHEAR, pos);
+                 return true;
+             }
diff --git a/paper-server/patches/sources/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java.patch b/paper-server/patches/sources/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java.patch
new file mode 100644
index 0000000000..56088e9418
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java.patch
@@ -0,0 +1,42 @@
+--- a/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java
++++ b/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java
+@@ -22,10 +_,38 @@
+             BlockPos blockPos = blockSource.pos().relative(direction);
+             Direction direction1 = blockSource.level().isEmptyBlock(blockPos.below()) ? direction : Direction.UP;
+ 
++            // CraftBukkit start
++            org.bukkit.block.Block bukkitBlock = org.bukkit.craftbukkit.block.CraftBlock.at(blockSource.level(), blockSource.pos());
++            org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event
++
++            org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockPos.getX(), blockPos.getY(), blockPos.getZ()));
++            if (!DispenserBlock.eventFired) {
++                blockSource.level().getCraftServer().getPluginManager().callEvent(event);
++            }
++
++            if (event.isCancelled()) {
++                return item;
++            }
++
++            if (!event.getItem().equals(craftItem)) {
++                // Chain to handler for new item
++                ItemStack eventStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem());
++                DispenseItemBehavior dispenseBehavior = DispenserBlock.getDispenseBehavior(blockSource, eventStack); // Paper - Fix NPE with equippable and items without behavior
++                if (dispenseBehavior != DispenseItemBehavior.NOOP && dispenseBehavior != this) {
++                    dispenseBehavior.dispense(blockSource, eventStack);
++                    return item;
++                }
++            }
++            // CraftBukkit end
+             try {
++                // Paper start - track changed items in the dispense event
+                 this.setSuccess(
+-                    ((BlockItem)item1).place(new DirectionalPlaceContext(blockSource.level(), blockPos, direction, item, direction1)).consumesAction()
++                    ((BlockItem) item1).place(new DirectionalPlaceContext(blockSource.level(), blockPos, direction, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem()), direction1)).consumesAction()
+                 );
++                if (this.isSuccess()) {
++                    item.shrink(1); // vanilla shrink is in the place function above, manually handle it here
++                }
++                // Paper end - track changed items in the dispense event
+             } catch (Exception var8) {
+                 LOGGER.error("Error trying to place shulker box at {}", blockPos, var8);
+             }
diff --git a/paper-server/patches/unapplied/net/minecraft/core/registries/BuiltInRegistries.java.patch b/paper-server/patches/sources/net/minecraft/core/registries/BuiltInRegistries.java.patch
similarity index 83%
rename from paper-server/patches/unapplied/net/minecraft/core/registries/BuiltInRegistries.java.patch
rename to paper-server/patches/sources/net/minecraft/core/registries/BuiltInRegistries.java.patch
index d363add98b..bc224c349b 100644
--- a/paper-server/patches/unapplied/net/minecraft/core/registries/BuiltInRegistries.java.patch
+++ b/paper-server/patches/sources/net/minecraft/core/registries/BuiltInRegistries.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/core/registries/BuiltInRegistries.java
 +++ b/net/minecraft/core/registries/BuiltInRegistries.java
-@@ -296,6 +296,17 @@
+@@ -296,6 +_,17 @@
      public static final Registry<SlotDisplay.Type<?>> SLOT_DISPLAY = registerSimple(Registries.SLOT_DISPLAY, SlotDisplays::bootstrap);
      public static final Registry<RecipeBookCategory> RECIPE_BOOK_CATEGORY = registerSimple(Registries.RECIPE_BOOK_CATEGORY, RecipeBookCategories::bootstrap);
      public static final Registry<? extends Registry<?>> REGISTRY = WRITABLE_REGISTRY;
@@ -16,18 +16,17 @@
 +    });
 +    // Paper end - add built-in registry conversions
  
-     private static <T> Registry<T> registerSimple(ResourceKey<? extends Registry<T>> key, BuiltInRegistries.RegistryBootstrap<T> initializer) {
-         return internalRegister(key, new MappedRegistry<>(key, Lifecycle.stable(), false), initializer);
-@@ -323,14 +334,22 @@
-         ResourceKey<? extends Registry<T>> key, R registry, BuiltInRegistries.RegistryBootstrap<T> initializer
+     private static <T> Registry<T> registerSimple(ResourceKey<? extends Registry<T>> key, BuiltInRegistries.RegistryBootstrap<T> bootstrap) {
+         return internalRegister(key, new MappedRegistry<>(key, Lifecycle.stable(), false), bootstrap);
+@@ -321,6 +_,7 @@
+         ResourceKey<? extends Registry<T>> key, R registry, BuiltInRegistries.RegistryBootstrap<T> bootstrap
      ) {
          Bootstrap.checkBootstrapCalled(() -> "registry " + key.location());
 +        io.papermc.paper.registry.PaperRegistryAccess.instance().registerRegistry(registry.key(), registry); // Paper - initialize API registry
          ResourceLocation resourceLocation = key.location();
-         LOADERS.put(resourceLocation, () -> initializer.run(registry));
--        WRITABLE_REGISTRY.register((ResourceKey<WritableRegistry<?>>)key, registry, RegistrationInfo.BUILT_IN);
-+        WRITABLE_REGISTRY.register((ResourceKey)key, registry, RegistrationInfo.BUILT_IN); // Paper - decompile fix
-         return registry;
+         LOADERS.put(resourceLocation, () -> bootstrap.run(registry));
+         WRITABLE_REGISTRY.register((ResourceKey)key, registry, RegistrationInfo.BUILT_IN);
+@@ -328,7 +_,14 @@
      }
  
      public static void bootStrap() {
@@ -42,7 +41,7 @@
          freeze();
          validate(REGISTRY);
      }
-@@ -348,6 +367,7 @@
+@@ -346,6 +_,7 @@
  
          for (Registry<?> registry : REGISTRY) {
              bindBootstrappedTagsToEmpty(registry);
diff --git a/paper-server/patches/unapplied/net/minecraft/data/loot/packs/VanillaChestLoot.java.patch b/paper-server/patches/sources/net/minecraft/data/loot/packs/VanillaChestLoot.java.patch
similarity index 97%
rename from paper-server/patches/unapplied/net/minecraft/data/loot/packs/VanillaChestLoot.java.patch
rename to paper-server/patches/sources/net/minecraft/data/loot/packs/VanillaChestLoot.java.patch
index 02e103aaf6..5d8aa86e40 100644
--- a/paper-server/patches/unapplied/net/minecraft/data/loot/packs/VanillaChestLoot.java.patch
+++ b/paper-server/patches/sources/net/minecraft/data/loot/packs/VanillaChestLoot.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/data/loot/packs/VanillaChestLoot.java
 +++ b/net/minecraft/data/loot/packs/VanillaChestLoot.java
-@@ -946,7 +946,6 @@
+@@ -946,7 +_,6 @@
                          .add(
                              LootItem.lootTableItem(Items.COMPASS)
                                  .apply(SetItemCountFunction.setCount(ConstantValue.exactly(1.0F)))
diff --git a/paper-server/patches/sources/net/minecraft/gametest/framework/GameTestInfo.java.patch b/paper-server/patches/sources/net/minecraft/gametest/framework/GameTestInfo.java.patch
new file mode 100644
index 0000000000..91fda07c6b
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/gametest/framework/GameTestInfo.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/gametest/framework/GameTestInfo.java
++++ b/net/minecraft/gametest/framework/GameTestInfo.java
+@@ -241,7 +_,7 @@
+             AABB structureBounds = this.getStructureBounds();
+             List<Entity> entitiesOfClass = this.getLevel()
+                 .getEntitiesOfClass(Entity.class, structureBounds.inflate(1.0), entity -> !(entity instanceof Player));
+-            entitiesOfClass.forEach(entity -> entity.remove(Entity.RemovalReason.DISCARDED));
++            entitiesOfClass.forEach(entity -> entity.remove(Entity.RemovalReason.DISCARDED, org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD)); // Paper
+         }
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/gametest/framework/GameTestServer.java.patch b/paper-server/patches/sources/net/minecraft/gametest/framework/GameTestServer.java.patch
new file mode 100644
index 0000000000..5bfe64d450
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/gametest/framework/GameTestServer.java.patch
@@ -0,0 +1,43 @@
+--- a/net/minecraft/gametest/framework/GameTestServer.java
++++ b/net/minecraft/gametest/framework/GameTestServer.java
+@@ -139,6 +_,8 @@
+         BlockPos spawnPos
+     ) {
+         super(
++            null, // Paper
++            null, // Paper
+             serverThread,
+             storageSource,
+             packRepository,
+@@ -154,8 +_,15 @@
+ 
+     @Override
+     public boolean initServer() {
+-        this.setPlayerList(new PlayerList(this, this.registries(), this.playerDataStorage, 1) {});
+-        this.loadLevel();
++        // Paper start
++        this.setPlayerList(new PlayerList(this, this.registries(), this.playerDataStorage, 1) {
++            @Override
++            public void loadAndSaveFiles() {
++                throw new UnsupportedOperationException("Should not be called in a GameTestServer");
++            }
++        });
++        this.loadLevel("blah");
++        // Paper end
+         ServerLevel serverLevel = this.overworld();
+         this.testBatches = Lists.newArrayList(GameTestBatchFactory.fromTestFunction(this.testFunctions, serverLevel));
+         serverLevel.setDefaultSpawnPos(this.spawnPos, 0.0F);
+@@ -303,6 +_,13 @@
+     public boolean shouldInformAdmins() {
+         return false;
+     }
++
++    // Paper start
++    @Override
++    public org.bukkit.command.CommandSender getBukkitSender(final net.minecraft.commands.CommandSourceStack wrapper) {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++    // Paper end
+ 
+     @Override
+     public boolean isSingleplayerOwner(GameProfile profile) {
diff --git a/paper-server/patches/sources/net/minecraft/gametest/framework/StructureUtils.java.patch b/paper-server/patches/sources/net/minecraft/gametest/framework/StructureUtils.java.patch
new file mode 100644
index 0000000000..23eeabf7b9
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/gametest/framework/StructureUtils.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/gametest/framework/StructureUtils.java
++++ b/net/minecraft/gametest/framework/StructureUtils.java
+@@ -187,7 +_,7 @@
+         level.clearBlockEvents(boundingBox1);
+         AABB aabb = AABB.of(boundingBox1);
+         List<Entity> entitiesOfClass = level.getEntitiesOfClass(Entity.class, aabb, entity -> !(entity instanceof Player));
+-        entitiesOfClass.forEach(Entity::discard);
++        entitiesOfClass.forEach(entity -> entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD)); // Paper
+     }
+ 
+     public static BlockPos getTransformedFarCorner(BlockPos pos, Vec3i offset, Rotation rotation) {
diff --git a/paper-server/patches/sources/net/minecraft/gametest/framework/TestCommand.java.patch b/paper-server/patches/sources/net/minecraft/gametest/framework/TestCommand.java.patch
new file mode 100644
index 0000000000..6ca1c0d342
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/gametest/framework/TestCommand.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/gametest/framework/TestCommand.java
++++ b/net/minecraft/gametest/framework/TestCommand.java
+@@ -278,7 +_,7 @@
+     }
+ 
+     private static int resetGameTestInfo(GameTestInfo gameTestInfo) {
+-        gameTestInfo.getLevel().getEntities(null, gameTestInfo.getStructureBounds()).stream().forEach(entity -> entity.remove(Entity.RemovalReason.DISCARDED));
++        gameTestInfo.getLevel().getEntities(null, gameTestInfo.getStructureBounds()).stream().forEach(entity -> entity.remove(Entity.RemovalReason.DISCARDED, org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD)); // Paper
+         gameTestInfo.getStructureBlockEntity().placeStructure(gameTestInfo.getLevel());
+         StructureUtils.removeBarriers(gameTestInfo.getStructureBounds(), gameTestInfo.getLevel());
+         say(gameTestInfo.getLevel(), "Reset succeded for: " + gameTestInfo.getTestName(), ChatFormatting.GREEN);
diff --git a/paper-server/patches/sources/net/minecraft/nbt/ByteArrayTag.java.patch b/paper-server/patches/sources/net/minecraft/nbt/ByteArrayTag.java.patch
new file mode 100644
index 0000000000..2d195ba0b8
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/nbt/ByteArrayTag.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/nbt/ByteArrayTag.java
++++ b/net/minecraft/nbt/ByteArrayTag.java
+@@ -23,6 +_,7 @@
+         private static byte[] readAccounted(DataInput input, NbtAccounter accounter) throws IOException {
+             accounter.accountBytes(24L);
+             int _int = input.readInt();
++            com.google.common.base.Preconditions.checkArgument(_int < 1 << 24); // Spigot
+             accounter.accountBytes(1L, _int);
+             byte[] bytes = new byte[_int];
+             input.readFully(bytes);
diff --git a/paper-server/patches/unapplied/net/minecraft/nbt/CompoundTag.java.patch b/paper-server/patches/sources/net/minecraft/nbt/CompoundTag.java.patch
similarity index 95%
rename from paper-server/patches/unapplied/net/minecraft/nbt/CompoundTag.java.patch
rename to paper-server/patches/sources/net/minecraft/nbt/CompoundTag.java.patch
index 1cb8cb2d7b..837aed26f3 100644
--- a/paper-server/patches/unapplied/net/minecraft/nbt/CompoundTag.java.patch
+++ b/paper-server/patches/sources/net/minecraft/nbt/CompoundTag.java.patch
@@ -1,15 +1,15 @@
 --- a/net/minecraft/nbt/CompoundTag.java
 +++ b/net/minecraft/nbt/CompoundTag.java
-@@ -49,7 +49,7 @@
+@@ -49,7 +_,7 @@
  
-         private static CompoundTag loadCompound(DataInput input, NbtAccounter tracker) throws IOException {
-             tracker.accountBytes(48L);
+         private static CompoundTag loadCompound(DataInput input, NbtAccounter nbtAccounter) throws IOException {
+             nbtAccounter.accountBytes(48L);
 -            Map<String, Tag> map = Maps.newHashMap();
 +            it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap<String, Tag> map = new it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap<>(8, 0.8f); // Paper - Reduce memory footprint of CompoundTag
  
              byte b;
              while ((b = input.readByte()) != 0) {
-@@ -166,7 +166,7 @@
+@@ -166,7 +_,7 @@
      }
  
      public CompoundTag() {
@@ -18,7 +18,7 @@
      }
  
      @Override
-@@ -232,14 +232,34 @@
+@@ -232,14 +_,34 @@
      }
  
      public void putUUID(String key, UUID value) {
@@ -53,7 +53,7 @@
          Tag tag = this.get(key);
          return tag != null && tag.getType() == IntArrayTag.TYPE && ((IntArrayTag)tag).getAsIntArray().length == 4;
      }
-@@ -477,8 +497,16 @@
+@@ -477,8 +_,16 @@
  
      @Override
      public CompoundTag copy() {
diff --git a/paper-server/patches/sources/net/minecraft/nbt/IntArrayTag.java.patch b/paper-server/patches/sources/net/minecraft/nbt/IntArrayTag.java.patch
new file mode 100644
index 0000000000..de1fb28f75
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/nbt/IntArrayTag.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/nbt/IntArrayTag.java
++++ b/net/minecraft/nbt/IntArrayTag.java
+@@ -23,6 +_,7 @@
+         private static int[] readAccounted(DataInput input, NbtAccounter accounter) throws IOException {
+             accounter.accountBytes(24L);
+             int _int = input.readInt();
++            com.google.common.base.Preconditions.checkArgument(_int < 1 << 24); // Spigot
+             accounter.accountBytes(4L, _int);
+             int[] ints = new int[_int];
+ 
diff --git a/paper-server/patches/sources/net/minecraft/nbt/NbtIo.java.patch b/paper-server/patches/sources/net/minecraft/nbt/NbtIo.java.patch
new file mode 100644
index 0000000000..5cd314e969
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/nbt/NbtIo.java.patch
@@ -0,0 +1,14 @@
+--- a/net/minecraft/nbt/NbtIo.java
++++ b/net/minecraft/nbt/NbtIo.java
+@@ -118,6 +_,11 @@
+     }
+ 
+     public static CompoundTag read(DataInput input, NbtAccounter accounter) throws IOException {
++        // Spigot start
++        if (input instanceof io.netty.buffer.ByteBufInputStream byteBufInputStream) {
++            input = new DataInputStream(new org.spigotmc.LimitStream(byteBufInputStream, accounter));
++        }
++        // Spigot end
+         Tag unnamedTag = readUnnamedTag(input, accounter);
+         if (unnamedTag instanceof CompoundTag) {
+             return (CompoundTag)unnamedTag;
diff --git a/paper-server/patches/unapplied/net/minecraft/nbt/NbtUtils.java.patch b/paper-server/patches/sources/net/minecraft/nbt/NbtUtils.java.patch
similarity index 65%
rename from paper-server/patches/unapplied/net/minecraft/nbt/NbtUtils.java.patch
rename to paper-server/patches/sources/net/minecraft/nbt/NbtUtils.java.patch
index a126b25999..00ff16e9e8 100644
--- a/paper-server/patches/unapplied/net/minecraft/nbt/NbtUtils.java.patch
+++ b/paper-server/patches/sources/net/minecraft/nbt/NbtUtils.java.patch
@@ -1,14 +1,14 @@
 --- a/net/minecraft/nbt/NbtUtils.java
 +++ b/net/minecraft/nbt/NbtUtils.java
-@@ -149,8 +149,10 @@
-         if (!nbt.contains("Name", 8)) {
+@@ -143,8 +_,10 @@
+         if (!tag.contains("Name", 8)) {
              return Blocks.AIR.defaultBlockState();
          } else {
--            ResourceLocation resourceLocation = ResourceLocation.parse(nbt.getString("Name"));
--            Optional<? extends Holder<Block>> optional = blockLookup.get(ResourceKey.create(Registries.BLOCK, resourceLocation));
+-            ResourceLocation resourceLocation = ResourceLocation.parse(tag.getString("Name"));
+-            Optional<? extends Holder<Block>> optional = blockGetter.get(ResourceKey.create(Registries.BLOCK, resourceLocation));
 +            // Paper start - Validate resource location
-+            ResourceLocation resourceLocation = ResourceLocation.tryParse(nbt.getString("Name"));
-+            Optional<? extends Holder<Block>> optional = resourceLocation != null ? blockLookup.get(ResourceKey.create(Registries.BLOCK, resourceLocation)) : Optional.empty();
++            ResourceLocation resourceLocation = ResourceLocation.tryParse(tag.getString("Name"));
++            Optional<? extends Holder<Block>> optional = resourceLocation != null ? blockGetter.get(ResourceKey.create(Registries.BLOCK, resourceLocation)) : Optional.empty();
 +            // Paper end - Validate resource location
              if (optional.isEmpty()) {
                  return Blocks.AIR.defaultBlockState();
diff --git a/paper-server/patches/unapplied/net/minecraft/nbt/TagParser.java.patch b/paper-server/patches/sources/net/minecraft/nbt/TagParser.java.patch
similarity index 57%
rename from paper-server/patches/unapplied/net/minecraft/nbt/TagParser.java.patch
rename to paper-server/patches/sources/net/minecraft/nbt/TagParser.java.patch
index dd6b67ed47..8d8938c690 100644
--- a/paper-server/patches/unapplied/net/minecraft/nbt/TagParser.java.patch
+++ b/paper-server/patches/sources/net/minecraft/nbt/TagParser.java.patch
@@ -1,14 +1,14 @@
 --- a/net/minecraft/nbt/TagParser.java
 +++ b/net/minecraft/nbt/TagParser.java
-@@ -49,6 +49,7 @@
+@@ -49,6 +_,7 @@
      }, CompoundTag::toString);
      public static final Codec<CompoundTag> LENIENT_CODEC = Codec.withAlternative(AS_CODEC, CompoundTag.CODEC);
      private final StringReader reader;
 +    private int depth; // Paper
  
-     public static CompoundTag parseTag(String string) throws CommandSyntaxException {
-         return new TagParser(new StringReader(string)).readSingleStruct();
-@@ -159,6 +160,7 @@
+     public static CompoundTag parseTag(String text) throws CommandSyntaxException {
+         return new TagParser(new StringReader(text)).readSingleStruct();
+@@ -159,6 +_,7 @@
  
      public CompoundTag readStruct() throws CommandSyntaxException {
          this.expect('{');
@@ -16,7 +16,7 @@
          CompoundTag compoundTag = new CompoundTag();
          this.reader.skipWhitespace();
  
-@@ -182,6 +184,7 @@
+@@ -182,6 +_,7 @@
          }
  
          this.expect('}');
@@ -24,7 +24,7 @@
          return compoundTag;
      }
  
-@@ -191,6 +194,7 @@
+@@ -191,6 +_,7 @@
          if (!this.reader.canRead()) {
              throw ERROR_EXPECTED_VALUE.createWithContext(this.reader);
          } else {
@@ -32,7 +32,7 @@
              ListTag listTag = new ListTag();
              TagType<?> tagType = null;
  
-@@ -216,6 +220,7 @@
+@@ -216,6 +_,7 @@
              }
  
              this.expect(']');
@@ -40,30 +40,15 @@
              return listTag;
          }
      }
-@@ -253,11 +258,11 @@
-             }
- 
-             if (typeReader == ByteTag.TYPE) {
--                list.add((T)((NumericTag)tag).getAsByte());
-+                list.add((T)(Byte)((NumericTag)tag).getAsByte()); // Paper - decompile fix
-             } else if (typeReader == LongTag.TYPE) {
--                list.add((T)((NumericTag)tag).getAsLong());
-+                list.add((T)(Long)((NumericTag)tag).getAsLong()); // Paper - decompile fix
-             } else {
--                list.add((T)((NumericTag)tag).getAsInt());
-+                list.add((T)(Integer)((NumericTag)tag).getAsInt()); // Paper - decompile fix
-             }
- 
-             if (!this.hasElementSeparator()) {
-@@ -288,4 +293,11 @@
+@@ -287,5 +_,11 @@
+     private void expect(char expected) throws CommandSyntaxException {
          this.reader.skipWhitespace();
-         this.reader.expect(c);
-     }
-+
+         this.reader.expect(expected);
++    }
 +    private void increaseDepth() throws CommandSyntaxException {
 +        this.depth++;
 +        if (this.depth > 512) {
 +            throw new io.papermc.paper.brigadier.TagParseCommandSyntaxException("NBT tag is too complex, depth > 512");
 +        }
-+    }
+     }
  }
diff --git a/paper-server/patches/unapplied/net/minecraft/network/Connection.java.patch b/paper-server/patches/sources/net/minecraft/network/Connection.java.patch
similarity index 69%
rename from paper-server/patches/unapplied/net/minecraft/network/Connection.java.patch
rename to paper-server/patches/sources/net/minecraft/network/Connection.java.patch
index f1e414f6b1..f34c32dc5a 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/Connection.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/Connection.java.patch
@@ -1,35 +1,35 @@
 --- a/net/minecraft/network/Connection.java
 +++ b/net/minecraft/network/Connection.java
-@@ -82,13 +82,13 @@
-         marker.add(Connection.PACKET_MARKER);
-     });
-     public static final Supplier<NioEventLoopGroup> NETWORK_WORKER_GROUP = Suppliers.memoize(() -> {
--        return new NioEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Client IO #%d").setDaemon(true).build());
-+        return new NioEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper
-     });
-     public static final Supplier<EpollEventLoopGroup> NETWORK_EPOLL_WORKER_GROUP = Suppliers.memoize(() -> {
--        return new EpollEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Epoll Client IO #%d").setDaemon(true).build());
-+        return new EpollEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Epoll Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper
-     });
-     public static final Supplier<DefaultEventLoopGroup> LOCAL_WORKER_GROUP = Suppliers.memoize(() -> {
--        return new DefaultEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Local Client IO #%d").setDaemon(true).build());
-+        return new DefaultEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Local Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper
-     });
+@@ -74,13 +_,13 @@
+     public static final Marker PACKET_RECEIVED_MARKER = Util.make(MarkerFactory.getMarker("PACKET_RECEIVED"), marker -> marker.add(PACKET_MARKER));
+     public static final Marker PACKET_SENT_MARKER = Util.make(MarkerFactory.getMarker("PACKET_SENT"), marker -> marker.add(PACKET_MARKER));
+     public static final Supplier<NioEventLoopGroup> NETWORK_WORKER_GROUP = Suppliers.memoize(
+-        () -> new NioEventLoopGroup(0, new ThreadFactoryBuilder().setNameFormat("Netty Client IO #%d").setDaemon(true).build())
++        () -> new NioEventLoopGroup(0, new ThreadFactoryBuilder().setNameFormat("Netty Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()) // Paper
+     );
+     public static final Supplier<EpollEventLoopGroup> NETWORK_EPOLL_WORKER_GROUP = Suppliers.memoize(
+-        () -> new EpollEventLoopGroup(0, new ThreadFactoryBuilder().setNameFormat("Netty Epoll Client IO #%d").setDaemon(true).build())
++        () -> new EpollEventLoopGroup(0, new ThreadFactoryBuilder().setNameFormat("Netty Epoll Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()) // Paper
+     );
+     public static final Supplier<DefaultEventLoopGroup> LOCAL_WORKER_GROUP = Suppliers.memoize(
+-        () -> new DefaultEventLoopGroup(0, new ThreadFactoryBuilder().setNameFormat("Netty Local Client IO #%d").setDaemon(true).build())
++        () -> new DefaultEventLoopGroup(0, new ThreadFactoryBuilder().setNameFormat("Netty Local Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()) // Paper
+     );
      private static final ProtocolInfo<ServerHandshakePacketListener> INITIAL_PROTOCOL = HandshakeProtocols.SERVERBOUND;
      private final PacketFlow receiving;
-@@ -96,6 +96,11 @@
+@@ -88,6 +_,11 @@
      private final Queue<Consumer<Connection>> pendingActions = Queues.newConcurrentLinkedQueue();
      public Channel channel;
      public SocketAddress address;
-+    // Spigot Start
++    // Spigot start
 +    public java.util.UUID spoofedUUID;
 +    public com.mojang.authlib.properties.Property[] spoofedProfile;
 +    public boolean preparing = true;
-+    // Spigot End
++    // Spigot end
      @Nullable
      private volatile PacketListener disconnectListener;
      @Nullable
-@@ -114,7 +119,42 @@
+@@ -106,6 +_,40 @@
      private volatile DisconnectionDetails delayedDisconnect;
      @Nullable
      BandwidthDebugMonitor bandwidthDebugMonitor;
@@ -39,7 +39,6 @@
 +    public java.net.InetSocketAddress virtualHost;
 +    private static boolean enableExplicitFlush = Boolean.getBoolean("paper.explicit-flush"); // Paper - Disable explicit network manager flushing
 +    // Paper end
- 
 +    // Paper start - add utility methods
 +    public final net.minecraft.server.level.ServerPlayer getPlayer() {
 +        if (this.packetListener instanceof net.minecraft.server.network.ServerGamePacketListenerImpl impl) {
@@ -68,76 +67,72 @@
 +    }
 +    // Paper end - packet limiter
 +    @Nullable public SocketAddress haProxyAddress; // Paper - Add API to get player's proxy address
-+
-     public Connection(PacketFlow side) {
-         this.receiving = side;
-     }
-@@ -123,6 +163,9 @@
-         super.channelActive(channelhandlercontext);
-         this.channel = channelhandlercontext.channel();
+ 
+     public Connection(PacketFlow receiving) {
+         this.receiving = receiving;
+@@ -116,6 +_,7 @@
+         super.channelActive(context);
+         this.channel = context.channel();
          this.address = this.channel.remoteAddress();
-+        // Spigot Start
-+        this.preparing = false;
-+        // Spigot End
++        this.preparing = false; // Spigot
          if (this.delayedDisconnect != null) {
              this.disconnect(this.delayedDisconnect);
          }
-@@ -134,6 +177,21 @@
-     }
+@@ -128,14 +_,31 @@
  
-     public void exceptionCaught(ChannelHandlerContext channelhandlercontext, Throwable throwable) {
+     @Override
+     public void exceptionCaught(ChannelHandlerContext context, Throwable exception) {
 +        // Paper start - Handle large packets disconnecting client
-+        if (throwable instanceof io.netty.handler.codec.EncoderException && throwable.getCause() instanceof PacketEncoder.PacketTooLargeException packetTooLargeException) {
++        if (exception instanceof io.netty.handler.codec.EncoderException && exception.getCause() instanceof PacketEncoder.PacketTooLargeException packetTooLargeException) {
 +            final Packet<?> packet = packetTooLargeException.getPacket();
 +            if (packet.packetTooLarge(this)) {
-+                ProtocolSwapHandler.handleOutboundTerminalPacket(channelhandlercontext, packet);
++                ProtocolSwapHandler.handleOutboundTerminalPacket(context, packet);
 +                return;
 +            } else if (packet.isSkippable()) {
-+                Connection.LOGGER.debug("Skipping packet due to errors", throwable.getCause());
-+                ProtocolSwapHandler.handleOutboundTerminalPacket(channelhandlercontext, packet);
++                Connection.LOGGER.debug("Skipping packet due to errors", exception.getCause());
++                ProtocolSwapHandler.handleOutboundTerminalPacket(context, packet);
 +                return;
 +            } else {
-+                throwable = throwable.getCause();
++                exception = exception.getCause();
 +            }
 +        }
 +        // Paper end - Handle large packets disconnecting client
-         if (throwable instanceof SkipPacketException) {
-             Connection.LOGGER.debug("Skipping packet due to errors", throwable.getCause());
+         if (exception instanceof SkipPacketException) {
+             LOGGER.debug("Skipping packet due to errors", exception.getCause());
          } else {
-@@ -141,8 +199,10 @@
- 
+             boolean flag = !this.handlingFault;
              this.handlingFault = true;
              if (this.channel.isOpen()) {
 +                net.minecraft.server.level.ServerPlayer player = this.getPlayer(); // Paper - Add API for quit reason
-                 if (throwable instanceof TimeoutException) {
-                     Connection.LOGGER.debug("Timeout", throwable);
+                 if (exception instanceof TimeoutException) {
+                     LOGGER.debug("Timeout", exception);
 +                    if (player != null) player.quitReason = org.bukkit.event.player.PlayerQuitEvent.QuitReason.TIMED_OUT; // Paper - Add API for quit reason
-                     this.disconnect((Component) Component.translatable("disconnect.timeout"));
+                     this.disconnect(Component.translatable("disconnect.timeout"));
                  } else {
-                     MutableComponent ichatmutablecomponent = Component.translatable("disconnect.genericReason", "Internal Exception: " + String.valueOf(throwable));
-@@ -155,9 +215,11 @@
-                         disconnectiondetails = new DisconnectionDetails(ichatmutablecomponent);
+                     Component component = Component.translatable("disconnect.genericReason", "Internal Exception: " + exception);
+@@ -147,9 +_,11 @@
+                         disconnectionDetails = new DisconnectionDetails(component);
                      }
  
 +                    if (player != null) player.quitReason = org.bukkit.event.player.PlayerQuitEvent.QuitReason.ERRONEOUS_STATE; // Paper - Add API for quit reason
                      if (flag) {
-                         Connection.LOGGER.debug("Failed to sent packet", throwable);
+                         LOGGER.debug("Failed to sent packet", exception);
 -                        if (this.getSending() == PacketFlow.CLIENTBOUND) {
 +                        boolean doesDisconnectExist = this.packetListener.protocol() != ConnectionProtocol.STATUS && this.packetListener.protocol() != ConnectionProtocol.HANDSHAKING; // Paper
 +                        if (this.getSending() == PacketFlow.CLIENTBOUND && doesDisconnectExist) { // Paper
-                             Packet<?> packet = this.sendLoginDisconnect ? new ClientboundLoginDisconnectPacket(ichatmutablecomponent) : new ClientboundDisconnectPacket(ichatmutablecomponent);
- 
-                             this.send((Packet) packet, PacketSendListener.thenRun(() -> {
-@@ -176,6 +238,7 @@
- 
+                             Packet<?> packet = (Packet<?>)(this.sendLoginDisconnect
+                                 ? new ClientboundLoginDisconnectPacket(component)
+                                 : new ClientboundDisconnectPacket(component));
+@@ -166,6 +_,7 @@
+                 }
              }
          }
-+        if (net.minecraft.server.MinecraftServer.getServer().isDebugging()) io.papermc.paper.util.TraceUtil.printStackTrace(throwable); // Spigot // Paper
++        if (net.minecraft.server.MinecraftServer.getServer().isDebugging()) io.papermc.paper.util.TraceUtil.printStackTrace(exception); // Spigot // Paper
      }
  
-     protected void channelRead0(ChannelHandlerContext channelhandlercontext, Packet<?> packet) {
-@@ -185,11 +248,61 @@
-             if (packetlistener == null) {
+     @Override
+@@ -175,10 +_,60 @@
+             if (packetListener == null) {
                  throw new IllegalStateException("Received a packet before the packet listener was initialized");
              } else {
 +                // Paper start - packet limiter
@@ -189,25 +184,15 @@
 +                    }
 +                }
 +                // Paper end - packet limiter
-                 if (packetlistener.shouldHandleMessage(packet)) {
+                 if (packetListener.shouldHandleMessage(packet)) {
                      try {
-                         Connection.genericsFtw(packet, packetlistener);
-                     } catch (RunningOnDifferentThreadException cancelledpackethandleexception) {
-                         ;
+                         genericsFtw(packet, packetListener);
+                     } catch (RunningOnDifferentThreadException var5) {
 +                    } catch (io.papermc.paper.util.ServerStopRejectedExecutionException ignored) { // Paper - do not prematurely disconnect players on stop
-                     } catch (RejectedExecutionException rejectedexecutionexception) {
-                         this.disconnect((Component) Component.translatable("multiplayer.disconnect.server_shutdown"));
-                     } catch (ClassCastException classcastexception) {
-@@ -205,7 +318,7 @@
-     }
- 
-     private static <T extends PacketListener> void genericsFtw(Packet<T> packet, PacketListener listener) {
--        packet.handle(listener);
-+        packet.handle((T) listener); // CraftBukkit - decompile error
-     }
- 
-     private void validateListener(ProtocolInfo<?> state, PacketListener listener) {
-@@ -418,12 +531,26 @@
+                     } catch (RejectedExecutionException var6) {
+                         this.disconnect(Component.translatable("multiplayer.disconnect.server_shutdown"));
+                     } catch (ClassCastException var7) {
+@@ -385,10 +_,30 @@
          }
      }
  
@@ -222,19 +207,23 @@
 +            Connection.joinAttemptsThisTick = 0;
 +        }
 +        // Paper end - Buffer joins to world
-         PacketListener packetlistener = this.packetListener;
- 
-         if (packetlistener instanceof TickablePacketListener tickablepacketlistener) {
+         if (this.packetListener instanceof TickablePacketListener tickablePacketListener) {
 +            // Paper start - Buffer joins to world
 +            if (!(this.packetListener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener)
 +                || loginPacketListener.state != net.minecraft.server.network.ServerLoginPacketListenerImpl.State.VERIFYING
 +                || Connection.joinAttemptsThisTick++ < MAX_PER_TICK) {
-             tickablepacketlistener.tick();
++            // Paper start - detailed watchdog information
++            net.minecraft.network.protocol.PacketUtils.packetProcessing.push(this.packetListener);
++            try {
+             tickablePacketListener.tick();
++            } finally {
++                net.minecraft.network.protocol.PacketUtils.packetProcessing.pop();
++            } // Paper end - detailed watchdog information
 +            } // Paper end - Buffer joins to world
          }
  
          if (!this.isConnected() && !this.disconnectionHandled) {
-@@ -431,7 +558,7 @@
+@@ -396,7 +_,7 @@
          }
  
          if (this.channel != null) {
@@ -243,34 +232,23 @@
          }
  
          if (this.tickCount++ % 20 == 0) {
-@@ -464,12 +591,15 @@
+@@ -432,12 +_,13 @@
      }
  
-     public void disconnect(DisconnectionDetails disconnectionInfo) {
-+        // Spigot Start
-+        this.preparing = false;
-+        // Spigot End
+     public void disconnect(DisconnectionDetails disconnectionDetails) {
++        this.preparing = false; // Spigot
          if (this.channel == null) {
-             this.delayedDisconnect = disconnectionInfo;
+             this.delayedDisconnect = disconnectionDetails;
          }
  
          if (this.isConnected()) {
 -            this.channel.close().awaitUninterruptibly();
 +            this.channel.close(); // We can't wait as this may be called from an event loop.
-             this.disconnectionDetails = disconnectionInfo;
+             this.disconnectionDetails = disconnectionDetails;
          }
- 
-@@ -537,7 +667,7 @@
      }
- 
-     public void configurePacketHandler(ChannelPipeline pipeline) {
--        pipeline.addLast("hackfix", new ChannelOutboundHandlerAdapter(this) {
-+        pipeline.addLast("hackfix", new ChannelOutboundHandlerAdapter() { // CraftBukkit - decompile error
-             public void write(ChannelHandlerContext channelhandlercontext, Object object, ChannelPromise channelpromise) throws Exception {
-                 super.write(channelhandlercontext, object, channelpromise);
-             }
-@@ -613,6 +743,14 @@
- 
+@@ -584,6 +_,13 @@
+         }
      }
  
 +    // Paper start - add proper async disconnect
@@ -280,33 +258,31 @@
 +        }
 +    }
 +    // Paper end - add proper async disconnect
-+
-     public void setupCompression(int compressionThreshold, boolean rejectsBadPackets) {
-         if (compressionThreshold >= 0) {
-             ChannelHandler channelhandler = this.channel.pipeline().get("decompress");
-@@ -633,6 +771,7 @@
+     public void setupCompression(int threshold, boolean validateDecompressed) {
+         if (threshold >= 0) {
+             if (this.channel.pipeline().get("decompress") instanceof CompressionDecoder compressionDecoder) {
+@@ -597,6 +_,7 @@
              } else {
-                 this.channel.pipeline().addAfter("prepender", "compress", new CompressionEncoder(compressionThreshold));
+                 this.channel.pipeline().addAfter("prepender", "compress", new CompressionEncoder(threshold));
              }
 +            this.channel.pipeline().fireUserEventTriggered(io.papermc.paper.network.ConnectionEvent.COMPRESSION_THRESHOLD_SET); // Paper - Add Channel initialization listeners
          } else {
              if (this.channel.pipeline().get("decompress") instanceof CompressionDecoder) {
                  this.channel.pipeline().remove("decompress");
-@@ -641,6 +780,7 @@
+@@ -605,6 +_,7 @@
              if (this.channel.pipeline().get("compress") instanceof CompressionEncoder) {
                  this.channel.pipeline().remove("compress");
              }
 +            this.channel.pipeline().fireUserEventTriggered(io.papermc.paper.network.ConnectionEvent.COMPRESSION_DISABLED); // Paper - Add Channel initialization listeners
          }
- 
      }
-@@ -661,6 +801,27 @@
  
-                     packetlistener1.onDisconnect(disconnectiondetails);
+@@ -622,6 +_,26 @@
+                     );
+                     packetListener1.onDisconnect(disconnectionDetails);
                  }
 +                this.pendingActions.clear(); // Free up packet queue.
 +                // Paper start - Add PlayerConnectionCloseEvent
-+                final PacketListener packetListener = this.getPacketListener();
 +                if (packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl commonPacketListener) {
 +                    /* Player was logged in, either game listener or configuration listener */
 +                    final com.mojang.authlib.GameProfile profile = commonPacketListener.getOwner();
@@ -325,6 +301,6 @@
 +                    }
 +                }
 +                // Paper end - Add PlayerConnectionCloseEvent
- 
              }
          }
+     }
diff --git a/paper-server/patches/sources/net/minecraft/network/FriendlyByteBuf.java.patch b/paper-server/patches/sources/net/minecraft/network/FriendlyByteBuf.java.patch
new file mode 100644
index 0000000000..f06b6bc4a6
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/network/FriendlyByteBuf.java.patch
@@ -0,0 +1,42 @@
+--- a/net/minecraft/network/FriendlyByteBuf.java
++++ b/net/minecraft/network/FriendlyByteBuf.java
+@@ -70,6 +_,7 @@
+ public class FriendlyByteBuf extends ByteBuf {
+     public static final int DEFAULT_NBT_QUOTA = 2097152;
+     private final ByteBuf source;
++    @Nullable public final java.util.Locale adventure$locale; // Paper - track player's locale for server-side translations
+     public static final short MAX_STRING_LENGTH = 32767;
+     public static final int MAX_COMPONENT_STRING_LENGTH = 262144;
+     private static final int PUBLIC_KEY_SIZE = 256;
+@@ -78,6 +_,7 @@
+     private static final Gson GSON = new Gson();
+ 
+     public FriendlyByteBuf(ByteBuf source) {
++        this.adventure$locale = PacketEncoder.ADVENTURE_LOCALE.get(); // Paper - track player's locale for server-side translations
+         this.source = source;
+     }
+ 
+@@ -106,8 +_,13 @@
+     }
+ 
+     public <T> void writeJsonWithCodec(Codec<T> codec, T value) {
++        // Paper start - Adventure; add max length parameter
++        this.writeJsonWithCodec(codec, value, MAX_STRING_LENGTH);
++    }
++    public <T> void writeJsonWithCodec(Codec<T> codec, T value, int maxLength) {
++        // Paper end - Adventure; add max length parameter
+         DataResult<JsonElement> dataResult = codec.encodeStart(JsonOps.INSTANCE, value);
+-        this.writeUtf(GSON.toJson(dataResult.getOrThrow(exception -> new EncoderException("Failed to encode: " + exception + " " + value))));
++        this.writeUtf(GSON.toJson(dataResult.getOrThrow(exception -> new EncoderException("Failed to encode: " + exception + " " + value))), maxLength); // Paper - Adventure; add max length parameter
+     }
+ 
+     public static <T> IntFunction<T> limitValue(IntFunction<T> function, int limit) {
+@@ -527,7 +_,7 @@
+ 
+         try {
+             NbtIo.writeAnyTag(nbt, new ByteBufOutputStream(buffer));
+-        } catch (IOException var3) {
++        } catch (Exception var3) { // CraftBukkit - IOException -> Exception
+             throw new EncoderException(var3);
+         }
+     }
diff --git a/paper-server/patches/unapplied/net/minecraft/network/PacketEncoder.java.patch b/paper-server/patches/sources/net/minecraft/network/PacketEncoder.java.patch
similarity index 79%
rename from paper-server/patches/unapplied/net/minecraft/network/PacketEncoder.java.patch
rename to paper-server/patches/sources/net/minecraft/network/PacketEncoder.java.patch
index ce1cafa04d..7318c99f0c 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/PacketEncoder.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/PacketEncoder.java.patch
@@ -1,10 +1,11 @@
 --- a/net/minecraft/network/PacketEncoder.java
 +++ b/net/minecraft/network/PacketEncoder.java
-@@ -17,10 +17,12 @@
-         this.protocolInfo = state;
+@@ -17,11 +_,13 @@
+         this.protocolInfo = protocolInfo;
      }
  
 +    static final ThreadLocal<java.util.Locale> ADVENTURE_LOCALE = ThreadLocal.withInitial(() -> null); // Paper - adventure; set player's locale
+     @Override
      protected void encode(ChannelHandlerContext channelHandlerContext, Packet<T> packet, ByteBuf byteBuf) throws Exception {
          PacketType<? extends Packet<? super T>> packetType = packet.type();
  
@@ -13,15 +14,7 @@
              this.protocolInfo.codec().encode(byteBuf, packet);
              int i = byteBuf.readableBytes();
              if (LOGGER.isDebugEnabled()) {
-@@ -31,14 +33,40 @@
- 
-             JvmProfiler.INSTANCE.onPacketSent(this.protocolInfo.id(), packetType, channelHandlerContext.channel().remoteAddress(), i);
-         } catch (Throwable var9) {
--            LOGGER.error("Error sending packet {}", packetType, var9);
-+            LOGGER.error("Error sending packet {} (skippable? {})", packetType, packet.isSkippable(), var9);
-             if (packet.isSkippable()) {
-                 throw new SkipPacketException(var9);
-             }
+@@ -39,7 +_,33 @@
  
              throw var9;
          } finally {
diff --git a/paper-server/patches/unapplied/net/minecraft/network/VarInt.java.patch b/paper-server/patches/sources/net/minecraft/network/VarInt.java.patch
similarity index 51%
rename from paper-server/patches/unapplied/net/minecraft/network/VarInt.java.patch
rename to paper-server/patches/sources/net/minecraft/network/VarInt.java.patch
index e2a07664c5..0a23124969 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/VarInt.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/VarInt.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/network/VarInt.java
 +++ b/net/minecraft/network/VarInt.java
-@@ -9,6 +9,18 @@
+@@ -9,6 +_,18 @@
      private static final int DATA_BITS_PER_BYTE = 7;
  
-     public static int getByteSize(int i) {
+     public static int getByteSize(int data) {
 +    // Paper start - Optimize VarInts
-+        return VARINT_EXACT_BYTE_LENGTHS[Integer.numberOfLeadingZeros(i)];
++        return VARINT_EXACT_BYTE_LENGTHS[Integer.numberOfLeadingZeros(data)];
 +    }
 +    private static final int[] VARINT_EXACT_BYTE_LENGTHS = new int[33];
 +    static {
@@ -14,30 +14,30 @@
 +        }
 +        VARINT_EXACT_BYTE_LENGTHS[32] = 1; // Special case for the number 0.
 +    }
-+    public static int getByteSizeOld(int i) {
++    public static int getByteSizeOld(int data) {
 +    // Paper end - Optimize VarInts
-         for (int j = 1; j < 5; j++) {
-             if ((i & -1 << j * 7) == 0) {
-                 return j;
-@@ -39,6 +51,21 @@
+         for (int i = 1; i < 5; i++) {
+             if ((data & -1 << i * 7) == 0) {
+                 return i;
+@@ -39,6 +_,21 @@
      }
  
-     public static ByteBuf write(ByteBuf buf, int i) {
+     public static ByteBuf write(ByteBuf buffer, int value) {
 +     // Paper start - Optimize VarInts
 +        // Peel the one and two byte count cases explicitly as they are the most common VarInt sizes
 +        // that the proxy will write, to improve inlining.
-+        if ((i & (0xFFFFFFFF << 7)) == 0) {
-+            buf.writeByte(i);
-+        } else if ((i & (0xFFFFFFFF << 14)) == 0) {
-+            int w = (i & 0x7F | 0x80) << 8 | (i >>> 7);
-+            buf.writeShort(w);
++        if ((value & (0xFFFFFFFF << 7)) == 0) {
++            buffer.writeByte(value);
++        } else if ((value & (0xFFFFFFFF << 14)) == 0) {
++            int w = (value & 0x7F | 0x80) << 8 | (value >>> 7);
++            buffer.writeShort(w);
 +        } else {
-+            writeOld(buf, i);
++            writeOld(buffer, value);
 +        }
-+        return buf;
++        return buffer;
 +    }
-+    public static ByteBuf writeOld(ByteBuf buf, int i) {
++    public static ByteBuf writeOld(ByteBuf buffer, int value) {
 +    // Paper end - Optimize VarInts
-         while ((i & -128) != 0) {
-             buf.writeByte(i & 127 | 128);
-             i >>>= 7;
+         while ((value & -128) != 0) {
+             buffer.writeByte(value & 127 | 128);
+             value >>>= 7;
diff --git a/paper-server/patches/unapplied/net/minecraft/network/Varint21FrameDecoder.java.patch b/paper-server/patches/sources/net/minecraft/network/Varint21FrameDecoder.java.patch
similarity index 50%
rename from paper-server/patches/unapplied/net/minecraft/network/Varint21FrameDecoder.java.patch
rename to paper-server/patches/sources/net/minecraft/network/Varint21FrameDecoder.java.patch
index e9aad632b1..5a1cea5c51 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/Varint21FrameDecoder.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/Varint21FrameDecoder.java.patch
@@ -1,15 +1,15 @@
 --- a/net/minecraft/network/Varint21FrameDecoder.java
 +++ b/net/minecraft/network/Varint21FrameDecoder.java
-@@ -39,6 +39,12 @@
-     }
+@@ -41,6 +_,12 @@
  
-     protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) {
+     @Override
+     protected void decode(ChannelHandlerContext context, ByteBuf in, List<Object> out) {
 +        // Paper start - Perf: Optimize exception handling; if channel is not active just discard the packet
-+        if (!channelHandlerContext.channel().isActive()) {
-+            byteBuf.skipBytes(byteBuf.readableBytes());
++        if (!context.channel().isActive()) {
++            in.skipBytes(in.readableBytes());
 +            return;
 +        }
 +        // Paper end - Perf: Optimize exception handling
-         byteBuf.markReaderIndex();
+         in.markReaderIndex();
          this.helperBuf.clear();
-         if (!copyVarint(byteBuf, this.helperBuf)) {
+         if (!copyVarint(in, this.helperBuf)) {
diff --git a/paper-server/patches/sources/net/minecraft/network/chat/ChatDecorator.java.patch b/paper-server/patches/sources/net/minecraft/network/chat/ChatDecorator.java.patch
new file mode 100644
index 0000000000..dbc8aeb60e
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/network/chat/ChatDecorator.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/network/chat/ChatDecorator.java
++++ b/net/minecraft/network/chat/ChatDecorator.java
+@@ -5,7 +_,14 @@
+ 
+ @FunctionalInterface
+ public interface ChatDecorator {
+-    ChatDecorator PLAIN = (player, message) -> message;
+-
+-    Component decorate(@Nullable ServerPlayer player, Component message);
++    ChatDecorator PLAIN = (player, message) -> java.util.concurrent.CompletableFuture.completedFuture(message); // Paper - adventure; support async chat decoration events
++
++    @io.papermc.paper.annotation.DoNotUse @Deprecated // Paper - adventure; support chat decoration events (callers should use the overload with CommandSourceStack)
++    java.util.concurrent.CompletableFuture<Component> decorate(@Nullable ServerPlayer player, Component message); // Paper - adventure; support async chat decoration events
++
++    // Paper start - adventure; support async chat decoration events
++    default java.util.concurrent.CompletableFuture<Component> decorate(@Nullable ServerPlayer sender, @Nullable net.minecraft.commands.CommandSourceStack commandSourceStack, Component message) {
++        throw new UnsupportedOperationException("Must override this implementation");
++    }
++    // Paper end - adventure; support async chat decoration events
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/network/chat/Component.java.patch b/paper-server/patches/sources/net/minecraft/network/chat/Component.java.patch
similarity index 58%
rename from paper-server/patches/unapplied/net/minecraft/network/chat/Component.java.patch
rename to paper-server/patches/sources/net/minecraft/network/chat/Component.java.patch
index 5db0b612a2..5df40c7c2d 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/chat/Component.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/chat/Component.java.patch
@@ -1,23 +1,19 @@
 --- a/net/minecraft/network/chat/Component.java
 +++ b/net/minecraft/network/chat/Component.java
-@@ -37,9 +37,23 @@
- import net.minecraft.resources.ResourceLocation;
+@@ -37,7 +_,19 @@
  import net.minecraft.util.FormattedCharSequence;
  import net.minecraft.world.level.ChunkPos;
-+// CraftBukkit start
-+import java.util.stream.Stream;
-+// CraftBukkit end
  
 -public interface Component extends Message, FormattedText {
 +public interface Component extends Message, FormattedText, Iterable<Component> { // CraftBukkit
 +
 +    // CraftBukkit start
-+    default Stream<Component> stream() {
-+        return com.google.common.collect.Streams.concat(new Stream[]{Stream.of(this), this.getSiblings().stream().flatMap(Component::stream)});
++    default java.util.stream.Stream<Component> stream() {
++        return com.google.common.collect.Streams.concat(java.util.stream.Stream.of(this), this.getSiblings().stream().flatMap(Component::stream));
 +    }
- 
++
 +    @Override
-+    default Iterator<Component> iterator() {
++    default java.util.Iterator<Component> iterator() {
 +        return this.stream().iterator();
 +    }
 +    // CraftBukkit end
diff --git a/paper-server/patches/unapplied/net/minecraft/network/chat/ComponentSerialization.java.patch b/paper-server/patches/sources/net/minecraft/network/chat/ComponentSerialization.java.patch
similarity index 89%
rename from paper-server/patches/unapplied/net/minecraft/network/chat/ComponentSerialization.java.patch
rename to paper-server/patches/sources/net/minecraft/network/chat/ComponentSerialization.java.patch
index 4136889ef4..daf7751fe7 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/chat/ComponentSerialization.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/chat/ComponentSerialization.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/network/chat/ComponentSerialization.java
 +++ b/net/minecraft/network/chat/ComponentSerialization.java
-@@ -37,9 +37,31 @@
+@@ -37,9 +_,31 @@
  
  public class ComponentSerialization {
      public static final Codec<Component> CODEC = Codec.recursive("Component", ComponentSerialization::createCodec);
@@ -34,8 +34,8 @@
      public static final StreamCodec<RegistryFriendlyByteBuf, Optional<Component>> TRUSTED_OPTIONAL_STREAM_CODEC = TRUSTED_STREAM_CODEC.apply(
          ByteBufCodecs::optional
      );
-@@ -100,7 +122,27 @@
-         return ExtraCodecs.orCompressed(mapCodec3, mapCodec2);
+@@ -102,7 +_,25 @@
+         return ExtraCodecs.orCompressed(mapCodec2, mapCodec1);
      }
  
 +    // Paper start - adventure; create separate codec for each locale
@@ -48,27 +48,25 @@
 +        return LOCALIZED_CODECS.computeIfAbsent(locale,
 +            loc -> Codec.recursive("Component", selfCodec -> createCodec(selfCodec, loc)));
 +    }
-+
-+
 +    // Paper end - adventure; create separate codec for each locale
 +
-     private static Codec<Component> createCodec(Codec<Component> selfCodec) {
+     private static Codec<Component> createCodec(Codec<Component> codec) {
 +        // Paper start - adventure; create separate codec for each locale
-+        return createCodec(selfCodec, null);
++        return createCodec(codec, null);
 +    }
 +
-+    private static Codec<Component> createCodec(Codec<Component> selfCodec, @javax.annotation.Nullable java.util.Locale locale) {
++    private static Codec<Component> createCodec(Codec<Component> codec, @javax.annotation.Nullable java.util.Locale locale) {
 +        // Paper end - adventure; create separate codec for each locale
          ComponentContents.Type<?>[] types = new ComponentContents.Type[]{
              PlainTextContents.TYPE, TranslatableContents.TYPE, KeybindContents.TYPE, ScoreContents.TYPE, SelectorContents.TYPE, NbtContents.TYPE
          };
-@@ -113,6 +155,34 @@
-                     )
-                     .apply(instance, MutableComponent::new)
+@@ -115,6 +_,34 @@
+                 )
+                 .apply(instance, MutableComponent::new)
          );
 +        // Paper start - adventure; create separate codec for each locale
-+        final Codec<Component> origCodec = codec;
-+        codec = new Codec<>() {
++        final Codec<Component> origCodec = codec1;
++        codec1 = new Codec<>() {
 +            @Override
 +            public <T> DataResult<com.mojang.datafixers.util.Pair<Component, T>> decode(final DynamicOps<T> ops, final T input) {
 +                return origCodec.decode(ops, input);
@@ -94,6 +92,6 @@
 +            }
 +        };
 +        // Paper end - adventure; create separate codec for each locale
-         return Codec.either(Codec.either(Codec.STRING, ExtraCodecs.nonEmptyList(selfCodec.listOf())), codec)
-             .xmap(either -> either.map(either2 -> either2.map(Component::literal, ComponentSerialization::createFromList), text -> (Component)text), text -> {
-                 String string = text.tryCollapseToString();
+         return Codec.either(Codec.either(Codec.STRING, ExtraCodecs.nonEmptyList(codec.listOf())), codec1)
+             .xmap(
+                 either -> either.map(either1 -> either1.map(Component::literal, ComponentSerialization::createFromList), component -> (Component)component),
diff --git a/paper-server/patches/unapplied/net/minecraft/network/chat/ComponentUtils.java.patch b/paper-server/patches/sources/net/minecraft/network/chat/ComponentUtils.java.patch
similarity index 57%
rename from paper-server/patches/unapplied/net/minecraft/network/chat/ComponentUtils.java.patch
rename to paper-server/patches/sources/net/minecraft/network/chat/ComponentUtils.java.patch
index c7b1b54e5a..e3ea7a636b 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/chat/ComponentUtils.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/chat/ComponentUtils.java.patch
@@ -1,12 +1,15 @@
 --- a/net/minecraft/network/chat/ComponentUtils.java
 +++ b/net/minecraft/network/chat/ComponentUtils.java
-@@ -33,14 +33,39 @@
+@@ -33,6 +_,7 @@
          }
      }
  
 +    @io.papermc.paper.annotation.DoNotUse // Paper - validate separators - right now this method is only used for separator evaluation. Error on build if this changes to re-evaluate.
-     public static Optional<MutableComponent> updateForEntity(@Nullable CommandSourceStack source, Optional<Component> text, @Nullable Entity sender, int depth) throws CommandSyntaxException {
-         return text.isPresent() ? Optional.of(updateForEntity(source, text.get(), sender, depth)) : Optional.empty();
+     public static Optional<MutableComponent> updateForEntity(
+         @Nullable CommandSourceStack commandSourceStack, Optional<Component> optionalComponent, @Nullable Entity entity, int recursionDepth
+     ) throws CommandSyntaxException {
+@@ -41,12 +_,40 @@
+             : Optional.empty();
      }
  
 +    // Paper start - validate separator
@@ -14,13 +17,16 @@
 +        if (text.isEmpty() || !isValidSelector(text.get())) return Optional.empty();
 +        return Optional.of(updateForEntity(source, text.get(), sender, depth));
 +    }
++
 +    public static boolean isValidSelector(final Component component) {
 +        final ComponentContents contents = component.getContents();
 +
-+        if (contents instanceof net.minecraft.network.chat.contents.NbtContents || contents instanceof net.minecraft.network.chat.contents.SelectorContents) return false;
++        if (contents instanceof net.minecraft.network.chat.contents.NbtContents || contents instanceof net.minecraft.network.chat.contents.SelectorContents)
++            return false;
 +        if (contents instanceof final net.minecraft.network.chat.contents.TranslatableContents translatableContents) {
 +            for (final Object arg : translatableContents.getArgs()) {
-+                if (arg instanceof final Component argumentAsComponent && !isValidSelector(argumentAsComponent)) return false;
++                if (arg instanceof final Component argumentAsComponent && !isValidSelector(argumentAsComponent))
++                    return false;
 +            }
 +        }
 +
@@ -28,15 +34,18 @@
 +    }
 +    // Paper end - validate separator
 +
-     public static MutableComponent updateForEntity(@Nullable CommandSourceStack source, Component text, @Nullable Entity sender, int depth) throws CommandSyntaxException {
-         if (depth > 100) {
-             return text.copy();
+     public static MutableComponent updateForEntity(
+         @Nullable CommandSourceStack commandSourceStack, Component component, @Nullable Entity entity, int recursionDepth
+     ) throws CommandSyntaxException {
+         if (recursionDepth > 100) {
+             return component.copy();
          } else {
 +            // Paper start - adventure; pass actual vanilla component
-+            if (text instanceof io.papermc.paper.adventure.AdventureComponent adventureComponent) {
-+                text = adventureComponent.deepConverted();
++            if (component instanceof io.papermc.paper.adventure.AdventureComponent adventureComponent) {
++                component = adventureComponent.deepConverted();
 +            }
 +            // Paper end - adventure; pass actual vanilla component
-             MutableComponent mutableComponent = text.getContents().resolve(source, sender, depth + 1);
++
+             MutableComponent mutableComponent = component.getContents().resolve(commandSourceStack, entity, recursionDepth + 1);
  
-             for (Component component : text.getSiblings()) {
+             for (Component component1 : component.getSiblings()) {
diff --git a/paper-server/patches/unapplied/net/minecraft/network/chat/MessageSignature.java.patch b/paper-server/patches/sources/net/minecraft/network/chat/MessageSignature.java.patch
similarity index 96%
rename from paper-server/patches/unapplied/net/minecraft/network/chat/MessageSignature.java.patch
rename to paper-server/patches/sources/net/minecraft/network/chat/MessageSignature.java.patch
index 2b6f690732..af25ad25c9 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/chat/MessageSignature.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/chat/MessageSignature.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/network/chat/MessageSignature.java
 +++ b/net/minecraft/network/chat/MessageSignature.java
-@@ -13,6 +13,7 @@
+@@ -13,6 +_,7 @@
  import net.minecraft.util.SignatureValidator;
  
  public record MessageSignature(byte[] bytes) {
diff --git a/paper-server/patches/unapplied/net/minecraft/network/chat/MutableComponent.java.patch b/paper-server/patches/sources/net/minecraft/network/chat/MutableComponent.java.patch
similarity index 50%
rename from paper-server/patches/unapplied/net/minecraft/network/chat/MutableComponent.java.patch
rename to paper-server/patches/sources/net/minecraft/network/chat/MutableComponent.java.patch
index c4694faca6..99e4fad8ac 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/chat/MutableComponent.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/chat/MutableComponent.java.patch
@@ -1,14 +1,14 @@
 --- a/net/minecraft/network/chat/MutableComponent.java
 +++ b/net/minecraft/network/chat/MutableComponent.java
-@@ -94,6 +94,11 @@
+@@ -94,6 +_,11 @@
  
      @Override
-     public boolean equals(Object object) {
+     public boolean equals(Object other) {
 +        // Paper start - make AdventureComponent equivalent
-+        if (object instanceof io.papermc.paper.adventure.AdventureComponent adventureComponent) {
-+            object = adventureComponent.deepConverted();
++        if (other instanceof io.papermc.paper.adventure.AdventureComponent adventureComponent) {
++            other = adventureComponent.deepConverted();
 +        }
 +        // Paper end - make AdventureComponent equivalent
-         return this == object
-             || object instanceof MutableComponent mutableComponent
+         return this == other
+             || other instanceof MutableComponent mutableComponent
                  && this.contents.equals(mutableComponent.contents)
diff --git a/paper-server/patches/sources/net/minecraft/network/chat/OutgoingChatMessage.java.patch b/paper-server/patches/sources/net/minecraft/network/chat/OutgoingChatMessage.java.patch
new file mode 100644
index 0000000000..4dd32b8b49
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/network/chat/OutgoingChatMessage.java.patch
@@ -0,0 +1,43 @@
+--- a/net/minecraft/network/chat/OutgoingChatMessage.java
++++ b/net/minecraft/network/chat/OutgoingChatMessage.java
+@@ -7,6 +_,12 @@
+ 
+     void sendToPlayer(ServerPlayer player, boolean filtered, ChatType.Bound boundType);
+ 
++    // Paper start
++    default void sendToPlayer(ServerPlayer sender, boolean filterMaskEnabled, ChatType.Bound params, @javax.annotation.Nullable Component unsigned) {
++        this.sendToPlayer(sender, filterMaskEnabled, params);
++    }
++    // Paper end
++
+     static OutgoingChatMessage create(PlayerChatMessage message) {
+         return (OutgoingChatMessage)(message.isSystem()
+             ? new OutgoingChatMessage.Disguised(message.decoratedContent())
+@@ -16,7 +_,12 @@
+     public record Disguised(@Override Component content) implements OutgoingChatMessage {
+         @Override
+         public void sendToPlayer(ServerPlayer player, boolean filtered, ChatType.Bound boundType) {
+-            player.connection.sendDisguisedChatMessage(this.content, boundType);
++            // Paper start
++            this.sendToPlayer(player, filtered, boundType, null);
++        }
++        public void sendToPlayer(ServerPlayer player, boolean filtered, ChatType.Bound boundType, @javax.annotation.Nullable Component unsigned) {
++            player.connection.sendDisguisedChatMessage(unsigned != null ? unsigned : this.content, boundType);
++            // Paper end
+         }
+     }
+ 
+@@ -28,7 +_,13 @@
+ 
+         @Override
+         public void sendToPlayer(ServerPlayer player, boolean filtered, ChatType.Bound boundType) {
++            // Paper start
++            this.sendToPlayer(player, filtered, boundType, null);
++        }
++        public void sendToPlayer(ServerPlayer player, boolean filtered, ChatType.Bound boundType, @javax.annotation.Nullable Component unsigned) {
++            // Paper end
+             PlayerChatMessage playerChatMessage = this.message.filter(filtered);
++            playerChatMessage = unsigned != null ? playerChatMessage.withUnsignedContent(unsigned) : playerChatMessage; // Paper
+             if (!playerChatMessage.isFullyFiltered()) {
+                 player.connection.sendPlayerChatMessage(playerChatMessage, boundType);
+             }
diff --git a/paper-server/patches/unapplied/net/minecraft/network/chat/PlayerChatMessage.java.patch b/paper-server/patches/sources/net/minecraft/network/chat/PlayerChatMessage.java.patch
similarity index 80%
rename from paper-server/patches/unapplied/net/minecraft/network/chat/PlayerChatMessage.java.patch
rename to paper-server/patches/sources/net/minecraft/network/chat/PlayerChatMessage.java.patch
index 8722bc8269..cd887f5354 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/chat/PlayerChatMessage.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/chat/PlayerChatMessage.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/network/chat/PlayerChatMessage.java
 +++ b/net/minecraft/network/chat/PlayerChatMessage.java
-@@ -17,6 +17,42 @@
+@@ -17,6 +_,43 @@
  public record PlayerChatMessage(
      SignedMessageLink link, @Nullable MessageSignature signature, SignedMessageBody signedBody, @Nullable Component unsignedContent, FilterMask filterMask
  ) {
@@ -40,20 +40,21 @@
 +        return new AdventureView();
 +    }
 +    // Paper end - adventure; support signed messages
++
      public static final MapCodec<PlayerChatMessage> MAP_CODEC = RecordCodecBuilder.mapCodec(
          instance -> instance.group(
-                     SignedMessageLink.CODEC.fieldOf("link").forGetter(PlayerChatMessage::link),
-@@ -47,7 +83,14 @@
+                 SignedMessageLink.CODEC.fieldOf("link").forGetter(PlayerChatMessage::link),
+@@ -47,7 +_,14 @@
      }
  
-     public PlayerChatMessage withUnsignedContent(Component unsignedContent) {
--        Component component = !unsignedContent.equals(Component.literal(this.signedContent())) ? unsignedContent : null;
+     public PlayerChatMessage withUnsignedContent(Component message) {
+-        Component component = !message.equals(Component.literal(this.signedContent())) ? message : null;
 +        // Paper start - adventure
 +        final Component component;
-+        if (unsignedContent instanceof io.papermc.paper.adventure.AdventureComponent advComponent) {
-+            component = this.signedContent().equals(io.papermc.paper.adventure.AdventureCodecs.tryCollapseToString(advComponent.adventure$component())) ? null : unsignedContent;
++        if (message instanceof io.papermc.paper.adventure.AdventureComponent advComponent) {
++            component = this.signedContent().equals(io.papermc.paper.adventure.AdventureCodecs.tryCollapseToString(advComponent.adventure$component())) ? null : message;
 +        } else {
-+            component = !unsignedContent.equals(Component.literal(this.signedContent())) ? unsignedContent : null;
++            component = !message.equals(Component.literal(this.signedContent())) ? message : null;
 +        }
 +        // Paper end - adventure
          return new PlayerChatMessage(this.link, this.signature, this.signedBody, component, this.filterMask);
diff --git a/paper-server/patches/unapplied/net/minecraft/network/chat/SignedMessageChain.java.patch b/paper-server/patches/sources/net/minecraft/network/chat/SignedMessageChain.java.patch
similarity index 81%
rename from paper-server/patches/unapplied/net/minecraft/network/chat/SignedMessageChain.java.patch
rename to paper-server/patches/sources/net/minecraft/network/chat/SignedMessageChain.java.patch
index 9404843b29..440e3748c5 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/chat/SignedMessageChain.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/chat/SignedMessageChain.java.patch
@@ -1,9 +1,9 @@
 --- a/net/minecraft/network/chat/SignedMessageChain.java
 +++ b/net/minecraft/network/chat/SignedMessageChain.java
-@@ -40,14 +40,14 @@
+@@ -40,14 +_,14 @@
                  if (signature == null) {
                      throw new SignedMessageChain.DecodeException(SignedMessageChain.DecodeException.MISSING_PROFILE_KEY);
-                 } else if (playerPublicKey.data().hasExpired()) {
+                 } else if (publicKey.data().hasExpired()) {
 -                    throw new SignedMessageChain.DecodeException(SignedMessageChain.DecodeException.EXPIRED_PROFILE_KEY);
 +                    throw new SignedMessageChain.DecodeException(SignedMessageChain.DecodeException.EXPIRED_PROFILE_KEY,  org.bukkit.event.player.PlayerKickEvent.Cause.EXPIRED_PROFILE_PUBLIC_KEY); // Paper - kick event causes
                  } else {
@@ -17,21 +17,20 @@
                      } else {
                          SignedMessageChain.this.lastTimeStamp = body.timeStamp();
                          PlayerChatMessage playerChatMessage = new PlayerChatMessage(signedMessageLink, signature, body, null, FilterMask.PASS_THROUGH);
-@@ -80,9 +80,16 @@
+@@ -80,8 +_,15 @@
          static final Component INVALID_SIGNATURE = Component.translatable("chat.disabled.invalid_signature");
          static final Component OUT_OF_ORDER_CHAT = Component.translatable("chat.disabled.out_of_order_chat");
  
--        public DecodeException(Component message) {
+-        public DecodeException(Component component) {
 +        // Paper start
 +        public final org.bukkit.event.player.PlayerKickEvent.Cause kickCause;
-+        public DecodeException(Component message, org.bukkit.event.player.PlayerKickEvent.Cause event) {
-             super(message);
++        public DecodeException(Component component, org.bukkit.event.player.PlayerKickEvent.Cause event) {
+             super(component);
 +            this.kickCause = event;
-         }
-+        // Paper end
-+        public DecodeException(Component message) {
-+            this(message, org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN); // Paper
 +        }
++        // Paper end
++        public DecodeException(Component component) {
++            this(component, org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN); // Paper
+         }
      }
  
-     @FunctionalInterface
diff --git a/paper-server/patches/sources/net/minecraft/network/chat/TextColor.java.patch b/paper-server/patches/sources/net/minecraft/network/chat/TextColor.java.patch
new file mode 100644
index 0000000000..b9e08a06cf
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/network/chat/TextColor.java.patch
@@ -0,0 +1,34 @@
+--- a/net/minecraft/network/chat/TextColor.java
++++ b/net/minecraft/network/chat/TextColor.java
+@@ -17,23 +_,29 @@
+     public static final Codec<TextColor> CODEC = Codec.STRING.comapFlatMap(TextColor::parseColor, TextColor::serialize);
+     private static final Map<ChatFormatting, TextColor> LEGACY_FORMAT_TO_COLOR = Stream.of(ChatFormatting.values())
+         .filter(ChatFormatting::isColor)
+-        .collect(ImmutableMap.toImmutableMap(Function.identity(), formatting -> new TextColor(formatting.getColor(), formatting.getName())));
++        .collect(ImmutableMap.toImmutableMap(Function.identity(), formatting -> new TextColor(formatting.getColor(), formatting.getName(), formatting))); // CraftBukkit
+     private static final Map<String, TextColor> NAMED_COLORS = LEGACY_FORMAT_TO_COLOR.values()
+         .stream()
+         .collect(ImmutableMap.toImmutableMap(textColor -> textColor.name, Function.identity()));
+     private final int value;
+     @Nullable
+     public final String name;
++    // CraftBukkit start
++    @Nullable
++    public final ChatFormatting format;
+ 
+-    private TextColor(int value, String name) {
++    private TextColor(int value, String name, ChatFormatting format) {
+         this.value = value & 16777215;
+         this.name = name;
++        this.format = format;
+     }
+ 
+     private TextColor(int value) {
+         this.value = value & 16777215;
+         this.name = null;
++        this.format = null;
+     }
++    // CraftBukkit end
+ 
+     public int getValue() {
+         return this.value;
diff --git a/paper-server/patches/sources/net/minecraft/network/chat/contents/NbtContents.java.patch b/paper-server/patches/sources/net/minecraft/network/chat/contents/NbtContents.java.patch
new file mode 100644
index 0000000000..ee118dfd02
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/network/chat/contents/NbtContents.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/network/chat/contents/NbtContents.java
++++ b/net/minecraft/network/chat/contents/NbtContents.java
+@@ -115,7 +_,7 @@
+             }).map(Tag::getAsString);
+             if (this.interpreting) {
+                 Component component = DataFixUtils.orElse(
+-                    ComponentUtils.updateForEntity(nbtPathPattern, this.separator, entity, recursionDepth), ComponentUtils.DEFAULT_NO_STYLE_SEPARATOR
++                    ComponentUtils.updateSeparatorForEntity(nbtPathPattern, this.separator, entity, recursionDepth), ComponentUtils.DEFAULT_NO_STYLE_SEPARATOR // Paper - validate separator
+                 );
+                 return stream.flatMap(text -> {
+                     try {
+@@ -127,7 +_,7 @@
+                     }
+                 }).reduce((mutableComponent, component1) -> mutableComponent.append(component).append(component1)).orElseGet(Component::empty);
+             } else {
+-                return ComponentUtils.updateForEntity(nbtPathPattern, this.separator, entity, recursionDepth)
++                return ComponentUtils.updateSeparatorForEntity(nbtPathPattern, this.separator, entity, recursionDepth) // Paper - validate separator
+                     .map(
+                         mutableComponent -> stream.map(Component::literal)
+                             .reduce((mutableComponent1, otherMutableComponent) -> mutableComponent1.append(mutableComponent).append(otherMutableComponent))
diff --git a/paper-server/patches/unapplied/net/minecraft/network/chat/contents/SelectorContents.java.patch b/paper-server/patches/sources/net/minecraft/network/chat/contents/SelectorContents.java.patch
similarity index 55%
rename from paper-server/patches/unapplied/net/minecraft/network/chat/contents/SelectorContents.java.patch
rename to paper-server/patches/sources/net/minecraft/network/chat/contents/SelectorContents.java.patch
index aff540bbdc..16c49dcbd5 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/chat/contents/SelectorContents.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/chat/contents/SelectorContents.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/network/chat/contents/SelectorContents.java
 +++ b/net/minecraft/network/chat/contents/SelectorContents.java
-@@ -36,7 +36,7 @@
-         if (source == null) {
+@@ -36,7 +_,7 @@
+         if (nbtPathPattern == null) {
              return Component.empty();
          } else {
--            Optional<? extends Component> optional = ComponentUtils.updateForEntity(source, this.separator, sender, depth);
-+            Optional<? extends Component> optional = ComponentUtils.updateSeparatorForEntity(source, this.separator, sender, depth); // Paper - validate separator
-             return ComponentUtils.formatList(this.selector.resolved().findEntities(source), optional, Entity::getDisplayName);
+-            Optional<? extends Component> optional = ComponentUtils.updateForEntity(nbtPathPattern, this.separator, entity, recursionDepth);
++            Optional<? extends Component> optional = ComponentUtils.updateSeparatorForEntity(nbtPathPattern, this.separator, entity, recursionDepth); // Paper - validate separator
+             return ComponentUtils.formatList(this.selector.resolved().findEntities(nbtPathPattern), optional, Entity::getDisplayName);
          }
      }
diff --git a/paper-server/patches/unapplied/net/minecraft/network/chat/contents/TranslatableContents.java.patch b/paper-server/patches/sources/net/minecraft/network/chat/contents/TranslatableContents.java.patch
similarity index 69%
rename from paper-server/patches/unapplied/net/minecraft/network/chat/contents/TranslatableContents.java.patch
rename to paper-server/patches/sources/net/minecraft/network/chat/contents/TranslatableContents.java.patch
index 88b91fae6d..6240ecc49b 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/chat/contents/TranslatableContents.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/chat/contents/TranslatableContents.java.patch
@@ -1,45 +1,47 @@
 --- a/net/minecraft/network/chat/contents/TranslatableContents.java
 +++ b/net/minecraft/network/chat/contents/TranslatableContents.java
-@@ -181,6 +181,15 @@
+@@ -181,6 +_,16 @@
  
      @Override
-     public <T> Optional<T> visit(FormattedText.ContentConsumer<T> visitor) {
+     public <T> Optional<T> visit(FormattedText.ContentConsumer<T> contentConsumer) {
 +        // Paper start - Count visited parts
 +        try {
-+            return this.visit(new TranslatableContentConsumer<>(visitor));
++            return this.visit(new TranslatableContentConsumer<>(contentConsumer));
 +        } catch (IllegalArgumentException ignored) {
-+            return visitor.accept("...");
++            return contentConsumer.accept("...");
 +        }
 +    }
-+    private <T> Optional<T> visit(TranslatableContentConsumer<T> visitor) {
++
++    private <T> Optional<T> visit(TranslatableContentConsumer<T> contentConsumer) {
 +        // Paper end - Count visited parts
          this.decompose();
  
          for (FormattedText formattedText : this.decomposedParts) {
-@@ -191,7 +200,26 @@
-         }
+@@ -192,6 +_,27 @@
  
          return Optional.empty();
-+    }
+     }
++
 +    // Paper start - Count visited parts
 +    private static final class TranslatableContentConsumer<T> implements FormattedText.ContentConsumer<T> {
-+        private static final IllegalArgumentException EX = new IllegalArgumentException("Too long");
++        private static final IllegalArgumentException NESTED_TOO_LONG = new IllegalArgumentException("Too long");
++
 +        private final FormattedText.ContentConsumer<T> visitor;
 +        private int visited;
 +
-+        private TranslatableContentConsumer(FormattedText.ContentConsumer<T> visitor) {
++        private TranslatableContentConsumer(final FormattedText.ContentConsumer<T> visitor) {
 +            this.visitor = visitor;
 +        }
 +
 +        @Override
 +        public Optional<T> accept(final String asString) {
-+            if (visited++ > 32) {
-+                throw EX;
++            if (this.visited++ > 32) {
++                throw NESTED_TOO_LONG;
 +            }
 +            return this.visitor.accept(asString);
 +        }
-     }
++    }
 +    // Paper end - Count visited parts
  
      @Override
-     public MutableComponent resolve(@Nullable CommandSourceStack source, @Nullable Entity sender, int depth) throws CommandSyntaxException {
+     public MutableComponent resolve(@Nullable CommandSourceStack nbtPathPattern, @Nullable Entity entity, int recursionDepth) throws CommandSyntaxException {
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/Packet.java.patch b/paper-server/patches/sources/net/minecraft/network/protocol/Packet.java.patch
similarity index 91%
rename from paper-server/patches/unapplied/net/minecraft/network/protocol/Packet.java.patch
rename to paper-server/patches/sources/net/minecraft/network/protocol/Packet.java.patch
index ba40a5940e..221a5587d3 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/protocol/Packet.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/protocol/Packet.java.patch
@@ -1,8 +1,8 @@
 --- a/net/minecraft/network/protocol/Packet.java
 +++ b/net/minecraft/network/protocol/Packet.java
-@@ -11,6 +11,19 @@
+@@ -11,6 +_,19 @@
  
-     void handle(T listener);
+     void handle(T handler);
  
 +    // Paper start
 +    default boolean hasLargePacketFallback() {
diff --git a/paper-server/patches/sources/net/minecraft/network/protocol/PacketUtils.java.patch b/paper-server/patches/sources/net/minecraft/network/protocol/PacketUtils.java.patch
new file mode 100644
index 0000000000..b2abd03b83
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/network/protocol/PacketUtils.java.patch
@@ -0,0 +1,48 @@
+--- a/net/minecraft/network/protocol/PacketUtils.java
++++ b/net/minecraft/network/protocol/PacketUtils.java
+@@ -21,6 +_,9 @@
+     public static <T extends PacketListener> void ensureRunningOnSameThread(Packet<T> packet, T processor, BlockableEventLoop<?> executor) throws RunningOnDifferentThreadException {
+         if (!executor.isSameThread()) {
+             executor.executeIfPossible(() -> {
++                packetProcessing.push(processor); // Paper - detailed watchdog information
++                try { // Paper - detailed watchdog information
++                if (processor instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl serverCommonPacketListener && serverCommonPacketListener.processedDisconnect) return; // Paper - Don't handle sync packets for kicked players
+                 if (processor.shouldHandleMessage(packet)) {
+                     try {
+                         packet.handle(processor);
+@@ -34,6 +_,12 @@
+                 } else {
+                     LOGGER.debug("Ignoring packet due to disconnection: {}", packet);
+                 }
++                // Paper start - detailed watchdog information
++                } finally {
++                    totalMainThreadPacketsProcessed.getAndIncrement();
++                    packetProcessing.pop();
++                }
++                // Paper end - detailed watchdog information
+             });
+             throw RunningOnDifferentThreadException.RUNNING_ON_DIFFERENT_THREAD;
+         }
+@@ -60,4 +_,22 @@
+ 
+         packetListener.fillCrashReport(crashReport);
+     }
++
++    // Paper start - detailed watchdog information
++    public static final java.util.concurrent.ConcurrentLinkedDeque<PacketListener> packetProcessing = new java.util.concurrent.ConcurrentLinkedDeque<>();
++    static final java.util.concurrent.atomic.AtomicLong totalMainThreadPacketsProcessed = new java.util.concurrent.atomic.AtomicLong();
++
++    public static long getTotalProcessedPackets() {
++        return totalMainThreadPacketsProcessed.get();
++    }
++
++    public static java.util.List<PacketListener> getCurrentPacketProcessors() {
++        java.util.List<PacketListener> listeners = new java.util.ArrayList<>(4);
++        for (PacketListener listener : packetProcessing) {
++            listeners.add(listener);
++        }
++
++        return listeners;
++    }
++    // Paper end - detailed watchdog information
+ }
diff --git a/paper-server/patches/sources/net/minecraft/network/protocol/common/ServerboundCustomPayloadPacket.java.patch b/paper-server/patches/sources/net/minecraft/network/protocol/common/ServerboundCustomPayloadPacket.java.patch
new file mode 100644
index 0000000000..10835a1af3
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/network/protocol/common/ServerboundCustomPayloadPacket.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/network/protocol/common/ServerboundCustomPayloadPacket.java
++++ b/net/minecraft/network/protocol/common/ServerboundCustomPayloadPacket.java
+@@ -14,7 +_,7 @@
+     private static final int MAX_PAYLOAD_SIZE = 32767;
+     public static final StreamCodec<FriendlyByteBuf, ServerboundCustomPayloadPacket> STREAM_CODEC = CustomPacketPayload.<FriendlyByteBuf>codec(
+             id -> DiscardedPayload.codec(id, 32767),
+-            Util.make(Lists.newArrayList(new CustomPacketPayload.TypeAndCodec<>(BrandPayload.TYPE, BrandPayload.STREAM_CODEC)), list -> {})
++            java.util.Collections.emptyList() // CraftBukkit - treat all packets the same
+         )
+         .map(ServerboundCustomPayloadPacket::new, ServerboundCustomPayloadPacket::payload);
+ 
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/common/custom/DiscardedPayload.java.patch b/paper-server/patches/sources/net/minecraft/network/protocol/common/custom/DiscardedPayload.java.patch
similarity index 50%
rename from paper-server/patches/unapplied/net/minecraft/network/protocol/common/custom/DiscardedPayload.java.patch
rename to paper-server/patches/sources/net/minecraft/network/protocol/common/custom/DiscardedPayload.java.patch
index 785d4efd47..38c086da6b 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/protocol/common/custom/DiscardedPayload.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/protocol/common/custom/DiscardedPayload.java.patch
@@ -1,24 +1,21 @@
 --- a/net/minecraft/network/protocol/common/custom/DiscardedPayload.java
 +++ b/net/minecraft/network/protocol/common/custom/DiscardedPayload.java
-@@ -4,16 +4,18 @@
+@@ -4,13 +_,14 @@
  import net.minecraft.network.codec.StreamCodec;
  import net.minecraft.resources.ResourceLocation;
  
 -public record DiscardedPayload(ResourceLocation id) implements CustomPacketPayload {
 +public record DiscardedPayload(ResourceLocation id, io.netty.buffer.ByteBuf data) implements CustomPacketPayload { // CraftBukkit - store data
- 
-     public static <T extends FriendlyByteBuf> StreamCodec<T, DiscardedPayload> codec(ResourceLocation id, int maxBytes) {
-         return CustomPacketPayload.codec((discardedpayload, packetdataserializer) -> {
-+            packetdataserializer.writeBytes(discardedpayload.data); // CraftBukkit - serialize
-         }, (packetdataserializer) -> {
-             int j = packetdataserializer.readableBytes();
- 
-             if (j >= 0 && j <= maxBytes) {
--                packetdataserializer.skipBytes(j);
+     public static <T extends FriendlyByteBuf> StreamCodec<T, DiscardedPayload> codec(ResourceLocation id, int maxSize) {
+-        return CustomPacketPayload.codec((value, output) -> {}, buffer -> {
++        return CustomPacketPayload.codec((value, output) -> {
++            output.writeBytes(value.data); // CraftBukkit - serialize
++        }, buffer -> {
+             int i = buffer.readableBytes();
+             if (i >= 0 && i <= maxSize) {
+-                buffer.skipBytes(i);
 -                return new DiscardedPayload(id);
-+                // CraftBukkit start
-+                return new DiscardedPayload(id, packetdataserializer.readBytes(j));
-+                // CraftBukkit end
++                return new DiscardedPayload(id, buffer.readBytes(i)); // CraftBukkit
              } else {
-                 throw new IllegalArgumentException("Payload may not be larger than " + maxBytes + " bytes");
+                 throw new IllegalArgumentException("Payload may not be larger than " + maxSize + " bytes");
              }
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundBlockEntityDataPacket.java.patch b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundBlockEntityDataPacket.java.patch
similarity index 77%
rename from paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundBlockEntityDataPacket.java.patch
rename to paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundBlockEntityDataPacket.java.patch
index 4967c17ea2..92492a6835 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundBlockEntityDataPacket.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundBlockEntityDataPacket.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/network/protocol/game/ClientboundBlockEntityDataPacket.java
 +++ b/net/minecraft/network/protocol/game/ClientboundBlockEntityDataPacket.java
-@@ -29,7 +29,7 @@
+@@ -29,7 +_,7 @@
  
-     public static ClientboundBlockEntityDataPacket create(BlockEntity blockEntity, BiFunction<BlockEntity, RegistryAccess, CompoundTag> nbtGetter) {
+     public static ClientboundBlockEntityDataPacket create(BlockEntity blockEntity, BiFunction<BlockEntity, RegistryAccess, CompoundTag> dataGetter) {
          RegistryAccess registryAccess = blockEntity.getLevel().registryAccess();
--        return new ClientboundBlockEntityDataPacket(blockEntity.getBlockPos(), blockEntity.getType(), nbtGetter.apply(blockEntity, registryAccess));
-+        return new ClientboundBlockEntityDataPacket(blockEntity.getBlockPos(), blockEntity.getType(), blockEntity.sanitizeSentNbt(nbtGetter.apply(blockEntity, registryAccess)));  // Paper - Sanitize sent data
+-        return new ClientboundBlockEntityDataPacket(blockEntity.getBlockPos(), blockEntity.getType(), dataGetter.apply(blockEntity, registryAccess));
++        return new ClientboundBlockEntityDataPacket(blockEntity.getBlockPos(), blockEntity.getType(), blockEntity.sanitizeSentNbt(dataGetter.apply(blockEntity, registryAccess))); // Paper - Sanitize sent data
      }
  
      public static ClientboundBlockEntityDataPacket create(BlockEntity blockEntity) {
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundContainerSetContentPacket.java.patch b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundContainerSetContentPacket.java.patch
similarity index 78%
rename from paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundContainerSetContentPacket.java.patch
rename to paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundContainerSetContentPacket.java.patch
index 1a14cb1295..5eb9a5a4eb 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundContainerSetContentPacket.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundContainerSetContentPacket.java.patch
@@ -1,9 +1,9 @@
 --- a/net/minecraft/network/protocol/game/ClientboundContainerSetContentPacket.java
 +++ b/net/minecraft/network/protocol/game/ClientboundContainerSetContentPacket.java
-@@ -36,6 +36,21 @@
-         this.carriedItem = ItemStack.OPTIONAL_STREAM_CODEC.decode(buf);
+@@ -35,6 +_,20 @@
+         this.items = ItemStack.OPTIONAL_LIST_STREAM_CODEC.decode(buffer);
+         this.carriedItem = ItemStack.OPTIONAL_STREAM_CODEC.decode(buffer);
      }
- 
 +    // Paper start - Handle large packets disconnecting client
 +    @Override
 +    public boolean hasLargePacketFallback() {
@@ -18,7 +18,6 @@
 +        return true;
 +    }
 +    // Paper end - Handle large packets disconnecting client
-+
-     private void write(RegistryFriendlyByteBuf buf) {
-         buf.writeContainerId(this.containerId);
-         buf.writeVarInt(this.stateId);
+ 
+     private void write(RegistryFriendlyByteBuf buffer) {
+         buffer.writeContainerId(this.containerId);
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundInitializeBorderPacket.java.patch b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundInitializeBorderPacket.java.patch
similarity index 97%
rename from paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundInitializeBorderPacket.java.patch
rename to paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundInitializeBorderPacket.java.patch
index a1740eccbe..9e7c36741a 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundInitializeBorderPacket.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundInitializeBorderPacket.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/network/protocol/game/ClientboundInitializeBorderPacket.java
 +++ b/net/minecraft/network/protocol/game/ClientboundInitializeBorderPacket.java
-@@ -30,8 +30,10 @@
+@@ -31,8 +_,10 @@
      }
  
      public ClientboundInitializeBorderPacket(WorldBorder worldBorder) {
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java.patch b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java.patch
similarity index 54%
rename from paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java.patch
rename to paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java.patch
index 26f8a0aa6a..0ffb3dd705 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java.patch
@@ -1,19 +1,19 @@
 --- a/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java
 +++ b/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java
-@@ -52,7 +52,7 @@
+@@ -52,7 +_,7 @@
              throw new RuntimeException("Can't read heightmap in packet for [" + x + ", " + z + "]");
          } else {
-             int i = buf.readVarInt();
--            if (i > 2097152) {
-+            if (i > 2097152) { // Paper - diff on change - if this changes, update PacketEncoder
+             int varInt = buffer.readVarInt();
+-            if (varInt > 2097152) {
++            if (varInt > 2097152) { // Paper - diff on change - if this changes, update PacketEncoder
                  throw new RuntimeException("Chunk Packet trying to allocate too much memory on read.");
              } else {
-                 this.buffer = new byte[i];
-@@ -154,6 +154,7 @@
-             CompoundTag compoundTag = blockEntity.getUpdateTag(blockEntity.getLevel().registryAccess());
+                 this.buffer = new byte[varInt];
+@@ -154,6 +_,7 @@
+             CompoundTag updateTag = blockEntity.getUpdateTag(blockEntity.getLevel().registryAccess());
              BlockPos blockPos = blockEntity.getBlockPos();
              int i = SectionPos.sectionRelative(blockPos.getX()) << 4 | SectionPos.sectionRelative(blockPos.getZ());
-+            blockEntity.sanitizeSentNbt(compoundTag); // Paper - Sanitize sent data
-             return new ClientboundLevelChunkPacketData.BlockEntityInfo(i, blockPos.getY(), blockEntity.getType(), compoundTag.isEmpty() ? null : compoundTag);
++            blockEntity.sanitizeSentNbt(updateTag); // Paper - Sanitize sent data
+             return new ClientboundLevelChunkPacketData.BlockEntityInfo(i, blockPos.getY(), blockEntity.getType(), updateTag.isEmpty() ? null : updateTag);
          }
      }
diff --git a/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java.patch b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java.patch
new file mode 100644
index 0000000000..edcd2a2d5d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java.patch
@@ -0,0 +1,14 @@
+--- a/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java
++++ b/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java
+@@ -18,6 +_,11 @@
+     private final int z;
+     private final ClientboundLevelChunkPacketData chunkData;
+     private final ClientboundLightUpdatePacketData lightData;
++    // Paper start - Anti-Xray
++    public void setReady(final boolean ready) {
++        // Empty hook, updated by feature patch
++    }
++    // Paper end - Anti-Xray
+ 
+     public ClientboundLevelChunkWithLightPacket(LevelChunk chunk, LevelLightEngine lightEngine, @Nullable BitSet skyLight, @Nullable BitSet blockLight) {
+         ChunkPos pos = chunk.getPos();
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundPlayerInfoUpdatePacket.java.patch b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundPlayerInfoUpdatePacket.java.patch
similarity index 83%
rename from paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundPlayerInfoUpdatePacket.java.patch
rename to paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundPlayerInfoUpdatePacket.java.patch
index edb5b971ee..de1815cf3d 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundPlayerInfoUpdatePacket.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundPlayerInfoUpdatePacket.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/network/protocol/game/ClientboundPlayerInfoUpdatePacket.java
 +++ b/net/minecraft/network/protocol/game/ClientboundPlayerInfoUpdatePacket.java
-@@ -38,7 +38,18 @@
+@@ -38,6 +_,17 @@
          this.actions = EnumSet.of(action);
          this.entries = List.of(new ClientboundPlayerInfoUpdatePacket.Entry(player));
      }
@@ -9,20 +9,19 @@
 +        this.actions = actions;
 +        this.entries = entries;
 +    }
- 
++
 +    public ClientboundPlayerInfoUpdatePacket(EnumSet<ClientboundPlayerInfoUpdatePacket.Action> actions, ClientboundPlayerInfoUpdatePacket.Entry entry) {
 +        this.actions = actions;
 +        this.entries = List.of(entry);
 +    }
 +    // Paper end - Add Listing API for Player
-+
-     public static ClientboundPlayerInfoUpdatePacket createPlayerInitializing(Collection<ServerPlayer> players) {
-         EnumSet<ClientboundPlayerInfoUpdatePacket.Action> enumSet = EnumSet.of(
-             ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER,
-@@ -53,6 +64,46 @@
-         return new ClientboundPlayerInfoUpdatePacket(enumSet, players);
-     }
  
+     public static ClientboundPlayerInfoUpdatePacket createPlayerInitializing(Collection<ServerPlayer> players) {
+         EnumSet<ClientboundPlayerInfoUpdatePacket.Action> set = EnumSet.of(
+@@ -52,6 +_,46 @@
+         );
+         return new ClientboundPlayerInfoUpdatePacket(set, players);
+     }
 +    // Paper start - Add Listing API for Player
 +    public static ClientboundPlayerInfoUpdatePacket createPlayerInitializing(Collection<ServerPlayer> players, ServerPlayer forPlayer) {
 +        final EnumSet<ClientboundPlayerInfoUpdatePacket.Action> enumSet = EnumSet.of(
@@ -63,27 +62,27 @@
 +        return new ClientboundPlayerInfoUpdatePacket(enumSet, new ClientboundPlayerInfoUpdatePacket.Entry(playerInfoId, listed));
 +    }
 +    // Paper end - Add Listing API for Player
-     private ClientboundPlayerInfoUpdatePacket(RegistryFriendlyByteBuf buf) {
-         this.actions = buf.readEnumSet(ClientboundPlayerInfoUpdatePacket.Action.class);
-         this.entries = buf.readList(buf2 -> {
-@@ -116,7 +167,15 @@
+ 
+     private ClientboundPlayerInfoUpdatePacket(RegistryFriendlyByteBuf buffer) {
+         this.actions = buffer.readEnumSet(ClientboundPlayerInfoUpdatePacket.Action.class);
+@@ -116,7 +_,15 @@
          }),
          INITIALIZE_CHAT(
-             (serialized, buf) -> serialized.chatSession = buf.readNullable(RemoteChatSession.Data::read),
--            (buf, entry) -> buf.writeNullable(entry.chatSession, RemoteChatSession.Data::write)
+             (entryBuilder, buffer) -> entryBuilder.chatSession = buffer.readNullable(RemoteChatSession.Data::read),
+-            (buffer, entry) -> buffer.writeNullable(entry.chatSession, RemoteChatSession.Data::write)
 +            // Paper start - Prevent causing expired keys from impacting new joins
-+            (buf, entry) -> {
++            (buffer, entry) -> {
 +                RemoteChatSession.Data chatSession = entry.chatSession;
 +                if (chatSession != null && chatSession.profilePublicKey().hasExpired()) {
 +                    chatSession = null;
 +                }
-+                buf.writeNullable(chatSession, RemoteChatSession.Data::write);
++                buffer.writeNullable(chatSession, RemoteChatSession.Data::write);
 +            }
 +            // Paper end - Prevent causing expired keys from impacting new joins
          ),
-         UPDATE_GAME_MODE((serialized, buf) -> serialized.gameMode = GameType.byId(buf.readVarInt()), (buf, entry) -> buf.writeVarInt(entry.gameMode().getId())),
-         UPDATE_LISTED((serialized, buf) -> serialized.listed = buf.readBoolean(), (buf, entry) -> buf.writeBoolean(entry.listed())),
-@@ -157,10 +216,15 @@
+         UPDATE_GAME_MODE(
+             (entryBuilder, buffer) -> entryBuilder.gameMode = GameType.byId(buffer.readVarInt()),
+@@ -160,10 +_,15 @@
          @Nullable RemoteChatSession.Data chatSession
      ) {
          Entry(ServerPlayer player) {
@@ -100,7 +99,7 @@
                  player.connection.latency(),
                  player.gameMode.getGameModeForPlayer(),
                  player.getTabListDisplayName(),
-@@ -169,6 +233,11 @@
+@@ -172,6 +_,11 @@
                  Optionull.map(player.getChatSession(), RemoteChatSession::asData)
              );
          }
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket.java.patch b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket.java.patch
similarity index 52%
rename from paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket.java.patch
rename to paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket.java.patch
index 372b4afd93..550a6f4c4b 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket.java.patch
@@ -1,15 +1,14 @@
 --- a/net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket.java
 +++ b/net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket.java
-@@ -33,11 +33,19 @@
-             short short0 = (Short) shortiterator.next();
+@@ -30,10 +_,25 @@
  
-             this.positions[j] = short0;
--            this.states[j] = section.getBlockState(SectionPos.sectionRelativeX(short0), SectionPos.sectionRelativeY(short0), SectionPos.sectionRelativeZ(short0));
-+            this.states[j] = (section != null) ? section.getBlockState(SectionPos.sectionRelativeX(short0), SectionPos.sectionRelativeY(short0), SectionPos.sectionRelativeZ(short0)) : net.minecraft.world.level.block.Blocks.AIR.defaultBlockState(); // CraftBukkit - SPIGOT-6076, Mojang bug when empty chunk section notified
+         for (short s : positions) {
+             this.positions[i] = s;
+-            this.states[i] = section.getBlockState(SectionPos.sectionRelativeX(s), SectionPos.sectionRelativeY(s), SectionPos.sectionRelativeZ(s));
++            this.states[i] = (section != null) ? section.getBlockState(SectionPos.sectionRelativeX(s), SectionPos.sectionRelativeY(s), SectionPos.sectionRelativeZ(s)) : net.minecraft.world.level.block.Blocks.AIR.defaultBlockState(); // CraftBukkit - SPIGOT-6076, Mojang bug when empty chunk section notified
+             i++;
          }
- 
      }
- 
 +    // CraftBukkit start - Add constructor
 +    public ClientboundSectionBlocksUpdatePacket(SectionPos sectionposition, ShortSet shortset, BlockState[] states) {
 +        this.sectionPos = sectionposition;
@@ -17,14 +16,6 @@
 +        this.states = states;
 +    }
 +    // CraftBukkit end
-+
-     private ClientboundSectionBlocksUpdatePacket(FriendlyByteBuf buf) {
-         this.sectionPos = SectionPos.of(buf.readLong());
-         int i = buf.readVarInt();
-@@ -54,6 +62,14 @@
- 
-     }
- 
 +    // Paper start - Multi Block Change API
 +    public ClientboundSectionBlocksUpdatePacket(SectionPos sectionPos, it.unimi.dsi.fastutil.shorts.Short2ObjectMap<BlockState> blockChanges) {
 +        this.sectionPos = sectionPos;
@@ -33,6 +24,6 @@
 +    }
 +    // Paper end - Multi Block Change API
 +
-     private void write(FriendlyByteBuf buf) {
-         buf.writeLong(this.sectionPos.asLong());
-         buf.writeVarInt(this.positions.length);
+ 
+     private ClientboundSectionBlocksUpdatePacket(FriendlyByteBuf buffer) {
+         this.sectionPos = SectionPos.of(buffer.readLong());
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSetBorderCenterPacket.java.patch b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundSetBorderCenterPacket.java.patch
similarity index 96%
rename from paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSetBorderCenterPacket.java.patch
rename to paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundSetBorderCenterPacket.java.patch
index 139eaa42d9..f228535ae7 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSetBorderCenterPacket.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundSetBorderCenterPacket.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/network/protocol/game/ClientboundSetBorderCenterPacket.java
 +++ b/net/minecraft/network/protocol/game/ClientboundSetBorderCenterPacket.java
-@@ -13,8 +13,10 @@
+@@ -14,8 +_,10 @@
      private final double newCenterZ;
  
      public ClientboundSetBorderCenterPacket(WorldBorder worldBorder) {
@@ -12,4 +12,4 @@
 +        // CraftBukkit end
      }
  
-     private ClientboundSetBorderCenterPacket(FriendlyByteBuf buf) {
+     private ClientboundSetBorderCenterPacket(FriendlyByteBuf buffer) {
diff --git a/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundSetEntityDataPacket.java.patch b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundSetEntityDataPacket.java.patch
new file mode 100644
index 0000000000..cb3ea7540d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundSetEntityDataPacket.java.patch
@@ -0,0 +1,14 @@
+--- a/net/minecraft/network/protocol/game/ClientboundSetEntityDataPacket.java
++++ b/net/minecraft/network/protocol/game/ClientboundSetEntityDataPacket.java
+@@ -19,9 +_,11 @@
+     }
+ 
+     private static void pack(List<SynchedEntityData.DataValue<?>> dataValues, RegistryFriendlyByteBuf buffer) {
++        try (io.papermc.paper.util.DataSanitizationUtil.DataSanitizer ignored = io.papermc.paper.util.DataSanitizationUtil.start(true)) { // Paper - data sanitization
+         for (SynchedEntityData.DataValue<?> dataValue : dataValues) {
+             dataValue.write(buffer);
+         }
++        } // Paper - data sanitization
+ 
+         buffer.writeByte(255);
+     }
diff --git a/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundSetEquipmentPacket.java.patch b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundSetEquipmentPacket.java.patch
new file mode 100644
index 0000000000..d5959b2866
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundSetEquipmentPacket.java.patch
@@ -0,0 +1,32 @@
+--- a/net/minecraft/network/protocol/game/ClientboundSetEquipmentPacket.java
++++ b/net/minecraft/network/protocol/game/ClientboundSetEquipmentPacket.java
+@@ -19,6 +_,13 @@
+     private final List<Pair<EquipmentSlot, ItemStack>> slots;
+ 
+     public ClientboundSetEquipmentPacket(int entity, List<Pair<EquipmentSlot, ItemStack>> slots) {
++    // Paper start - data sanitization
++        this(entity, slots, false);
++    }
++    private boolean sanitize;
++    public ClientboundSetEquipmentPacket(int entity, List<Pair<EquipmentSlot, ItemStack>> slots, boolean sanitize) {
++        this.sanitize = sanitize;
++    // Paper end - data sanitization
+         this.entity = entity;
+         this.slots = slots;
+     }
+@@ -40,6 +_,7 @@
+         buffer.writeVarInt(this.entity);
+         int size = this.slots.size();
+ 
++        try (io.papermc.paper.util.DataSanitizationUtil.DataSanitizer ignored = io.papermc.paper.util.DataSanitizationUtil.start(this.sanitize)) { // Paper - data sanitization
+         for (int i = 0; i < size; i++) {
+             Pair<EquipmentSlot, ItemStack> pair = this.slots.get(i);
+             EquipmentSlot equipmentSlot = pair.getFirst();
+@@ -48,6 +_,7 @@
+             buffer.writeByte(flag ? ordinal | -128 : ordinal);
+             ItemStack.OPTIONAL_STREAM_CODEC.encode(buffer, pair.getSecond());
+         }
++        } // Paper - data sanitization
+     }
+ 
+     @Override
diff --git a/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundSetPlayerTeamPacket.java.patch b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundSetPlayerTeamPacket.java.patch
new file mode 100644
index 0000000000..3f4c2e05a8
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundSetPlayerTeamPacket.java.patch
@@ -0,0 +1,23 @@
+--- a/net/minecraft/network/protocol/game/ClientboundSetPlayerTeamPacket.java
++++ b/net/minecraft/network/protocol/game/ClientboundSetPlayerTeamPacket.java
+@@ -30,6 +_,11 @@
+     private final Collection<String> players;
+     private final Optional<ClientboundSetPlayerTeamPacket.Parameters> parameters;
+ 
++    // Paper start - Multiple Entries with Scoreboards
++    public static ClientboundSetPlayerTeamPacket createMultiplePlayerPacket(PlayerTeam team, Collection<String> players, ClientboundSetPlayerTeamPacket.Action operation) {
++        return new ClientboundSetPlayerTeamPacket(team.getName(), operation == ClientboundSetPlayerTeamPacket.Action.ADD ? 3 : 4, Optional.empty(), players);
++    }
++    // Paper end - Multiple Entries with Scoreboards
+     private ClientboundSetPlayerTeamPacket(String name, int method, Optional<ClientboundSetPlayerTeamPacket.Parameters> parameters, Collection<String> players) {
+         this.name = name;
+         this.method = method;
+@@ -198,7 +_,7 @@
+             ComponentSerialization.TRUSTED_STREAM_CODEC.encode(buffer, this.displayName);
+             buffer.writeByte(this.options);
+             buffer.writeUtf(this.nametagVisibility);
+-            buffer.writeUtf(this.collisionRule);
++            buffer.writeUtf(!io.papermc.paper.configuration.GlobalConfiguration.get().collisions.enablePlayerCollisions ? PlayerTeam.CollisionRule.NEVER.name : this.collisionRule); // Paper - Configurable player collision
+             buffer.writeEnum(this.color);
+             ComponentSerialization.TRUSTED_STREAM_CODEC.encode(buffer, this.playerPrefix);
+             ComponentSerialization.TRUSTED_STREAM_CODEC.encode(buffer, this.playerSuffix);
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSystemChatPacket.java.patch b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundSystemChatPacket.java.patch
similarity index 59%
rename from paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSystemChatPacket.java.patch
rename to paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundSystemChatPacket.java.patch
index 64187dddbd..c741092d39 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSystemChatPacket.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundSystemChatPacket.java.patch
@@ -1,14 +1,9 @@
 --- a/net/minecraft/network/protocol/game/ClientboundSystemChatPacket.java
 +++ b/net/minecraft/network/protocol/game/ClientboundSystemChatPacket.java
-@@ -1,3 +1,4 @@
-+// mc-dev import
- package net.minecraft.network.protocol.game;
- 
- import net.minecraft.network.RegistryFriendlyByteBuf;
-@@ -12,6 +13,17 @@
- 
-     public static final StreamCodec<RegistryFriendlyByteBuf, ClientboundSystemChatPacket> STREAM_CODEC = StreamCodec.composite(ComponentSerialization.TRUSTED_STREAM_CODEC, ClientboundSystemChatPacket::content, ByteBufCodecs.BOOL, ClientboundSystemChatPacket::overlay, ClientboundSystemChatPacket::new);
- 
+@@ -16,6 +_,16 @@
+         ClientboundSystemChatPacket::overlay,
+         ClientboundSystemChatPacket::new
+     );
 +    // Spigot start
 +    public ClientboundSystemChatPacket(net.md_5.bungee.api.chat.BaseComponent[] content, boolean overlay) {
 +        this(org.bukkit.craftbukkit.util.CraftChatMessage.fromJSON(net.md_5.bungee.chat.ComponentSerializer.toString(content)), overlay);
@@ -19,7 +14,6 @@
 +        this(io.papermc.paper.adventure.PaperAdventure.asVanilla(content), overlay);
 +    }
 +    // Paper end
-+
+ 
      @Override
      public PacketType<ClientboundSystemChatPacket> type() {
-         return GamePacketTypes.CLIENTBOUND_SYSTEM_CHAT;
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java.patch b/paper-server/patches/sources/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java.patch
similarity index 52%
rename from paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java.patch
rename to paper-server/patches/sources/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java.patch
index 1771a837ae..3161af3164 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java
 +++ b/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java
-@@ -19,7 +19,7 @@
+@@ -19,7 +_,7 @@
  
-     private ServerboundCommandSuggestionPacket(FriendlyByteBuf buf) {
-         this.id = buf.readVarInt();
--        this.command = buf.readUtf(32500);
-+        this.command = buf.readUtf(2048); // Paper
+     private ServerboundCommandSuggestionPacket(FriendlyByteBuf buffer) {
+         this.id = buffer.readVarInt();
+-        this.command = buffer.readUtf(32500);
++        this.command = buffer.readUtf(2048); // Paper
      }
  
-     private void write(FriendlyByteBuf buf) {
+     private void write(FriendlyByteBuf buffer) {
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundInteractPacket.java.patch b/paper-server/patches/sources/net/minecraft/network/protocol/game/ServerboundInteractPacket.java.patch
similarity index 68%
rename from paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundInteractPacket.java.patch
rename to paper-server/patches/sources/net/minecraft/network/protocol/game/ServerboundInteractPacket.java.patch
index a4d170433a..d7bf58f0ce 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundInteractPacket.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/protocol/game/ServerboundInteractPacket.java.patch
@@ -1,10 +1,9 @@
 --- a/net/minecraft/network/protocol/game/ServerboundInteractPacket.java
 +++ b/net/minecraft/network/protocol/game/ServerboundInteractPacket.java
-@@ -176,4 +176,14 @@
-             buf.writeEnum(this.hand);
+@@ -145,6 +_,15 @@
+             buffer.writeEnum(this.hand);
          }
      }
-+
 +    // Paper start - PlayerUseUnknownEntityEvent
 +    public int getEntityId() {
 +        return this.entityId;
@@ -14,4 +13,6 @@
 +        return this.action.getType() == ActionType.ATTACK;
 +    }
 +    // Paper end - PlayerUseUnknownEntityEvent
- }
+ 
+     static class InteractionAtLocationAction implements ServerboundInteractPacket.Action {
+         private final InteractionHand hand;
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundUseItemOnPacket.java.patch b/paper-server/patches/sources/net/minecraft/network/protocol/game/ServerboundUseItemOnPacket.java.patch
similarity index 51%
rename from paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundUseItemOnPacket.java.patch
rename to paper-server/patches/sources/net/minecraft/network/protocol/game/ServerboundUseItemOnPacket.java.patch
index 1e2cc2dc52..73bc58e48b 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundUseItemOnPacket.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/protocol/game/ServerboundUseItemOnPacket.java.patch
@@ -1,23 +1,18 @@
 --- a/net/minecraft/network/protocol/game/ServerboundUseItemOnPacket.java
 +++ b/net/minecraft/network/protocol/game/ServerboundUseItemOnPacket.java
-@@ -1,3 +1,4 @@
-+// mc-dev import
- package net.minecraft.network.protocol.game;
- 
- import net.minecraft.network.FriendlyByteBuf;
-@@ -13,6 +14,7 @@
+@@ -14,6 +_,7 @@
      private final BlockHitResult blockHit;
      private final InteractionHand hand;
      private final int sequence;
 +    public long timestamp; // Spigot
  
-     public ServerboundUseItemOnPacket(InteractionHand hand, BlockHitResult blockHitResult, int sequence) {
+     public ServerboundUseItemOnPacket(InteractionHand hand, BlockHitResult blockHit, int sequence) {
          this.hand = hand;
-@@ -21,6 +23,7 @@
+@@ -22,6 +_,7 @@
      }
  
-     private ServerboundUseItemOnPacket(FriendlyByteBuf buf) {
+     private ServerboundUseItemOnPacket(FriendlyByteBuf buffer) {
 +        this.timestamp = System.currentTimeMillis(); // Spigot
-         this.hand = (InteractionHand) buf.readEnum(InteractionHand.class);
-         this.blockHit = buf.readBlockHitResult();
-         this.sequence = buf.readVarInt();
+         this.hand = buffer.readEnum(InteractionHand.class);
+         this.blockHit = buffer.readBlockHitResult();
+         this.sequence = buffer.readVarInt();
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundUseItemPacket.java.patch b/paper-server/patches/sources/net/minecraft/network/protocol/game/ServerboundUseItemPacket.java.patch
similarity index 51%
rename from paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundUseItemPacket.java.patch
rename to paper-server/patches/sources/net/minecraft/network/protocol/game/ServerboundUseItemPacket.java.patch
index 45ba3ec19d..b4e853fe82 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundUseItemPacket.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/protocol/game/ServerboundUseItemPacket.java.patch
@@ -1,23 +1,18 @@
 --- a/net/minecraft/network/protocol/game/ServerboundUseItemPacket.java
 +++ b/net/minecraft/network/protocol/game/ServerboundUseItemPacket.java
-@@ -1,3 +1,4 @@
-+// mc-dev import
- package net.minecraft.network.protocol.game;
- 
- import net.minecraft.network.FriendlyByteBuf;
-@@ -13,6 +14,7 @@
+@@ -14,6 +_,7 @@
      private final int sequence;
      private final float yRot;
      private final float xRot;
 +    public long timestamp; // Spigot
  
-     public ServerboundUseItemPacket(InteractionHand hand, int sequence, float yaw, float pitch) {
+     public ServerboundUseItemPacket(InteractionHand hand, int sequence, float yRot, float xRot) {
          this.hand = hand;
-@@ -22,6 +24,7 @@
+@@ -23,6 +_,7 @@
      }
  
-     private ServerboundUseItemPacket(FriendlyByteBuf buf) {
+     private ServerboundUseItemPacket(FriendlyByteBuf buffer) {
 +        this.timestamp = System.currentTimeMillis(); // Spigot
-         this.hand = (InteractionHand) buf.readEnum(InteractionHand.class);
-         this.sequence = buf.readVarInt();
-         this.yRot = buf.readFloat();
+         this.hand = buffer.readEnum(InteractionHand.class);
+         this.sequence = buffer.readVarInt();
+         this.yRot = buffer.readFloat();
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/VecDeltaCodec.java.patch b/paper-server/patches/sources/net/minecraft/network/protocol/game/VecDeltaCodec.java.patch
similarity index 92%
rename from paper-server/patches/unapplied/net/minecraft/network/protocol/game/VecDeltaCodec.java.patch
rename to paper-server/patches/sources/net/minecraft/network/protocol/game/VecDeltaCodec.java.patch
index ca1a2126c9..2f0544231c 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/VecDeltaCodec.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/protocol/game/VecDeltaCodec.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/network/protocol/game/VecDeltaCodec.java
 +++ b/net/minecraft/network/protocol/game/VecDeltaCodec.java
-@@ -5,16 +5,16 @@
+@@ -5,16 +_,16 @@
  
  public class VecDeltaCodec {
      private static final double TRUNCATION_STEPS = 4096.0;
@@ -15,7 +15,7 @@
  
      @VisibleForTesting
      static double decode(long value) {
--        return (double)value / 4096.0;
+-        return value / 4096.0;
 +        return value / 4096.0; // Paper - Fix MC-4; diff on change
      }
  
diff --git a/paper-server/patches/sources/net/minecraft/network/protocol/handshake/ClientIntentionPacket.java.patch b/paper-server/patches/sources/net/minecraft/network/protocol/handshake/ClientIntentionPacket.java.patch
new file mode 100644
index 0000000000..752d456ac2
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/network/protocol/handshake/ClientIntentionPacket.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/network/protocol/handshake/ClientIntentionPacket.java
++++ b/net/minecraft/network/protocol/handshake/ClientIntentionPacket.java
+@@ -20,7 +_,7 @@
+     }
+ 
+     private ClientIntentionPacket(FriendlyByteBuf buffer) {
+-        this(buffer.readVarInt(), buffer.readUtf(255), buffer.readUnsignedShort(), ClientIntent.byId(buffer.readVarInt()));
++        this(buffer.readVarInt(), buffer.readUtf(Short.MAX_VALUE), buffer.readUnsignedShort(), ClientIntent.byId(buffer.readVarInt())); // Spigot - increase max hostName length
+     }
+ 
+     private void write(FriendlyByteBuf buffer) {
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/login/ClientboundCustomQueryPacket.java.patch b/paper-server/patches/sources/net/minecraft/network/protocol/login/ClientboundCustomQueryPacket.java.patch
similarity index 81%
rename from paper-server/patches/unapplied/net/minecraft/network/protocol/login/ClientboundCustomQueryPacket.java.patch
rename to paper-server/patches/sources/net/minecraft/network/protocol/login/ClientboundCustomQueryPacket.java.patch
index 60ed80c610..e1eb0709fe 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/protocol/login/ClientboundCustomQueryPacket.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/protocol/login/ClientboundCustomQueryPacket.java.patch
@@ -1,13 +1,12 @@
 --- a/net/minecraft/network/protocol/login/ClientboundCustomQueryPacket.java
 +++ b/net/minecraft/network/protocol/login/ClientboundCustomQueryPacket.java
-@@ -47,4 +47,14 @@
-     public void handle(ClientLoginPacketListener listener) {
-         listener.handleCustomQuery(this);
+@@ -47,4 +_,13 @@
+     public void handle(ClientLoginPacketListener handler) {
+         handler.handleCustomQuery(this);
      }
 +
 +    // Paper start - MC Utils - default query payloads
 +    public static record PlayerInfoChannelPayload(ResourceLocation id, FriendlyByteBuf buffer) implements CustomQueryPayload {
-+
 +        @Override
 +        public void write(final FriendlyByteBuf buf) {
 +            buf.writeBytes(this.buffer.copy());
diff --git a/paper-server/patches/sources/net/minecraft/network/protocol/login/ClientboundLoginDisconnectPacket.java.patch b/paper-server/patches/sources/net/minecraft/network/protocol/login/ClientboundLoginDisconnectPacket.java.patch
new file mode 100644
index 0000000000..3bc9092eb9
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/network/protocol/login/ClientboundLoginDisconnectPacket.java.patch
@@ -0,0 +1,21 @@
+--- a/net/minecraft/network/protocol/login/ClientboundLoginDisconnectPacket.java
++++ b/net/minecraft/network/protocol/login/ClientboundLoginDisconnectPacket.java
+@@ -18,11 +_,16 @@
+     }
+ 
+     private ClientboundLoginDisconnectPacket(FriendlyByteBuf buffer) {
+-        this.reason = Component.Serializer.fromJsonLenient(buffer.readUtf(262144), RegistryAccess.EMPTY);
++        this.reason = Component.Serializer.fromJsonLenient(buffer.readUtf(FriendlyByteBuf.MAX_COMPONENT_STRING_LENGTH), RegistryAccess.EMPTY); // Paper - diff on change
+     }
+ 
+     private void write(FriendlyByteBuf buffer) {
+-        buffer.writeUtf(Component.Serializer.toJson(this.reason, RegistryAccess.EMPTY));
++        // Paper start - Adventure
++        // buffer.writeUtf(Component.Serializer.toJson(this.reason, RegistryAccess.EMPTY));
++        // In the login phase, buffer.adventure$locale field is most likely null, but plugins may use internals to set it via the channel attribute
++        java.util.Locale bufLocale = buffer.adventure$locale;
++        buffer.writeJsonWithCodec(net.minecraft.network.chat.ComponentSerialization.localizedCodec(bufLocale == null ? java.util.Locale.US : bufLocale), this.reason, FriendlyByteBuf.MAX_COMPONENT_STRING_LENGTH);
++        // Paper end - Adventure
+     }
+ 
+     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java.patch b/paper-server/patches/sources/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java.patch
similarity index 60%
rename from paper-server/patches/unapplied/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java.patch
rename to paper-server/patches/sources/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java.patch
index 663ca9ede2..40b8fdbc78 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java.patch
@@ -1,32 +1,31 @@
 --- a/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java
 +++ b/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java
-@@ -20,7 +20,17 @@
+@@ -20,7 +_,17 @@
      }
  
-     private static CustomQueryAnswerPayload readPayload(int queryId, FriendlyByteBuf buf) {
--        return readUnknownPayload(buf);
+     private static CustomQueryAnswerPayload readPayload(int transactionId, FriendlyByteBuf buffer) {
+-        return readUnknownPayload(buffer);
 +        // Paper start - MC Utils - default query payloads
-+        FriendlyByteBuf buffer = buf.readNullable((buf2) -> {
-+            int i = buf2.readableBytes();
-+            if (i >= 0 && i <= MAX_PAYLOAD_SIZE) {
-+                return new FriendlyByteBuf(buf2.readBytes(i));
++        FriendlyByteBuf buf = buffer.readNullable((buf2) -> {
++            int readableBytes = buf2.readableBytes();
++            if (readableBytes >= 0 && readableBytes <= MAX_PAYLOAD_SIZE) {
++                return new FriendlyByteBuf(buf2.readBytes(readableBytes));
 +            } else {
 +                throw new IllegalArgumentException("Payload may not be larger than " + MAX_PAYLOAD_SIZE + " bytes");
 +            }
 +        });
-+        return buffer == null ? null : new net.minecraft.network.protocol.login.ServerboundCustomQueryAnswerPacket.QueryAnswerPayload(buffer);
++        return buf == null ? null : new net.minecraft.network.protocol.login.ServerboundCustomQueryAnswerPacket.QueryAnswerPayload(buf);
 +        // Paper end - MC Utils - default query payloads
      }
  
-     private static CustomQueryAnswerPayload readUnknownPayload(FriendlyByteBuf buf) {
-@@ -47,4 +57,21 @@
-     public void handle(ServerLoginPacketListener listener) {
-         listener.handleCustomQueryPacket(this);
+     private static CustomQueryAnswerPayload readUnknownPayload(FriendlyByteBuf buffer) {
+@@ -47,4 +_,19 @@
+     public void handle(ServerLoginPacketListener handler) {
+         handler.handleCustomQueryPacket(this);
      }
 +
 +    // Paper start - MC Utils - default query payloads
 +    public static final class QueryAnswerPayload implements CustomQueryAnswerPayload {
-+
 +        public final FriendlyByteBuf buffer;
 +
 +        public QueryAnswerPayload(final net.minecraft.network.FriendlyByteBuf buffer) {
@@ -39,5 +38,4 @@
 +        }
 +    }
 +    // Paper end - MC Utils - default query payloads
-+
  }
diff --git a/paper-server/patches/unapplied/net/minecraft/network/syncher/SynchedEntityData.java.patch b/paper-server/patches/sources/net/minecraft/network/syncher/SynchedEntityData.java.patch
similarity index 56%
rename from paper-server/patches/unapplied/net/minecraft/network/syncher/SynchedEntityData.java.patch
rename to paper-server/patches/sources/net/minecraft/network/syncher/SynchedEntityData.java.patch
index d2519790c5..ac8c137be4 100644
--- a/paper-server/patches/unapplied/net/minecraft/network/syncher/SynchedEntityData.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/syncher/SynchedEntityData.java.patch
@@ -1,23 +1,21 @@
 --- a/net/minecraft/network/syncher/SynchedEntityData.java
 +++ b/net/minecraft/network/syncher/SynchedEntityData.java
-@@ -50,8 +50,8 @@
+@@ -45,7 +_,7 @@
          }
      }
  
 -    private <T> SynchedEntityData.DataItem<T> getItem(EntityDataAccessor<T> key) {
--        return this.itemsById[key.id()];
 +    public <T> SynchedEntityData.DataItem<T> getItem(EntityDataAccessor<T> key) { // Paper - public
-+        return (SynchedEntityData.DataItem<T>) this.itemsById[key.id()]; // CraftBukkit - decompile error
+         return (SynchedEntityData.DataItem<T>)this.itemsById[key.id()];
      }
  
-     public <T> T get(EntityDataAccessor<T> data) {
-@@ -74,6 +74,13 @@
- 
+@@ -67,6 +_,13 @@
+         }
      }
  
 +    // CraftBukkit start - add method from above
-+    public <T> void markDirty(EntityDataAccessor<T> datawatcherobject) {
-+        this.getItem(datawatcherobject).setDirty(true);
++    public <T> void markDirty(final EntityDataAccessor<T> entityDataAccessor) {
++        this.getItem(entityDataAccessor).setDirty(true);
 +        this.isDirty = true;
 +    }
 +    // CraftBukkit end
@@ -25,15 +23,11 @@
      public boolean isDirty() {
          return this.isDirty;
      }
-@@ -140,10 +147,24 @@
-         if (!Objects.equals(from.serializer(), to.accessor.serializer())) {
-             throw new IllegalStateException(String.format(Locale.ROOT, "Invalid entity data item type for field %d on entity %s: old=%s(%s), new=%s(%s)", to.accessor.id(), this.entity, to.value, to.value.getClass(), from.value, from.value.getClass()));
-         } else {
--            to.setValue(from.value);
-+            to.setValue((T) from.value); // CraftBukkit - decompile error
+@@ -169,6 +_,20 @@
+             return new SynchedEntityData(this.entity, this.itemsById);
          }
      }
- 
++
 +    // 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.
@@ -47,7 +41,6 @@
 +        return list;
 +    }
 +    // Paper end
-+
-     public static class DataItem<T> {
  
+     public static class DataItem<T> {
          final EntityDataAccessor<T> accessor;
diff --git a/paper-server/patches/sources/net/minecraft/resources/RegistryDataLoader.java.patch b/paper-server/patches/sources/net/minecraft/resources/RegistryDataLoader.java.patch
new file mode 100644
index 0000000000..c625d649d2
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/resources/RegistryDataLoader.java.patch
@@ -0,0 +1,71 @@
+--- a/net/minecraft/resources/RegistryDataLoader.java
++++ b/net/minecraft/resources/RegistryDataLoader.java
+@@ -247,13 +_,14 @@
+         RegistryOps<JsonElement> ops,
+         ResourceKey<E> resourceKey,
+         Resource resource,
+-        RegistrationInfo registrationInfo
++        RegistrationInfo registrationInfo,
++        io.papermc.paper.registry.data.util.Conversions conversions // Paper - pass conversions
+     ) throws IOException {
+         try (Reader reader = resource.openAsReader()) {
+             JsonElement jsonElement = JsonParser.parseReader(reader);
+             DataResult<E> dataResult = codec.parse(ops, jsonElement);
+             E orThrow = dataResult.getOrThrow();
+-            registry.register(resourceKey, orThrow, registrationInfo);
++            io.papermc.paper.registry.PaperRegistryListenerManager.INSTANCE.registerWithListeners(registry, resourceKey, orThrow, registrationInfo, conversions); // Paper - register with listeners
+         }
+     }
+ 
+@@ -267,6 +_,7 @@
+         FileToIdConverter fileToIdConverter = FileToIdConverter.registry(registry.key());
+         RegistryOps<JsonElement> registryOps = RegistryOps.create(JsonOps.INSTANCE, registryInfoLookup);
+ 
++        final io.papermc.paper.registry.data.util.Conversions conversions = new io.papermc.paper.registry.data.util.Conversions(registryInfoLookup); // Paper - create conversions
+         for (Entry<ResourceLocation, Resource> entry : fileToIdConverter.listMatchingResources(resourceManager).entrySet()) {
+             ResourceLocation resourceLocation = entry.getKey();
+             ResourceKey<E> resourceKey = ResourceKey.create(registry.key(), fileToIdConverter.fileToId(resourceLocation));
+@@ -274,7 +_,7 @@
+             RegistrationInfo registrationInfo = REGISTRATION_INFO_CACHE.apply(resource.knownPackInfo());
+ 
+             try {
+-                loadElementFromResource(registry, codec, registryOps, resourceKey, resource, registrationInfo);
++                loadElementFromResource(registry, codec, registryOps, resourceKey, resource, registrationInfo, conversions); // Paper - pass conversions
+             } catch (Exception var14) {
+                 loadingErrors.put(
+                     resourceKey,
+@@ -283,7 +_,8 @@
+             }
+         }
+ 
+-        TagLoader.loadTagsForRegistry(resourceManager, registry);
++        io.papermc.paper.registry.PaperRegistryListenerManager.INSTANCE.runFreezeListeners(registry.key(), conversions); // Paper - run pre-freeze listeners
++        TagLoader.loadTagsForRegistry(resourceManager, registry, io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent.Cause.INITIAL); // Paper - tag lifecycle - add cause
+     }
+ 
+     static <E> void loadContentsFromNetwork(
+@@ -300,6 +_,7 @@
+             RegistryOps<JsonElement> registryOps1 = RegistryOps.create(JsonOps.INSTANCE, registryInfoLookup);
+             FileToIdConverter fileToIdConverter = FileToIdConverter.registry(registry.key());
+ 
++            final io.papermc.paper.registry.data.util.Conversions conversions = new io.papermc.paper.registry.data.util.Conversions(registryInfoLookup); // Paper - create conversions
+             for (RegistrySynchronization.PackedRegistryEntry packedRegistryEntry : networkedRegistryData.elements) {
+                 ResourceKey<E> resourceKey = ResourceKey.create(registry.key(), packedRegistryEntry.id());
+                 Optional<Tag> optional = packedRegistryEntry.data();
+@@ -318,7 +_,7 @@
+ 
+                     try {
+                         Resource resourceOrThrow = resourceProvider.getResourceOrThrow(resourceLocation);
+-                        loadElementFromResource(registry, codec, registryOps1, resourceKey, resourceOrThrow, NETWORK_REGISTRATION_INFO);
++                        loadElementFromResource(registry, codec, registryOps1, resourceKey, resourceOrThrow, NETWORK_REGISTRATION_INFO, conversions); // Paper - pass conversions
+                     } catch (Exception var17) {
+                         loadingErrors.put(resourceKey, new IllegalStateException("Failed to parse local data", var17));
+                     }
+@@ -360,6 +_,7 @@
+ 
+         RegistryDataLoader.Loader<T> create(Lifecycle registryLifecycle, Map<ResourceKey<?>, Exception> loadingErrors) {
+             WritableRegistry<T> writableRegistry = new MappedRegistry<>(this.key, registryLifecycle);
++            io.papermc.paper.registry.PaperRegistryAccess.instance().registerRegistry(this.key, writableRegistry); // Paper - initialize API registry
+             return new RegistryDataLoader.Loader<>(this, writableRegistry, loadingErrors);
+         }
+ 
diff --git a/paper-server/patches/unapplied/net/minecraft/resources/ResourceLocation.java.patch b/paper-server/patches/sources/net/minecraft/resources/ResourceLocation.java.patch
similarity index 96%
rename from paper-server/patches/unapplied/net/minecraft/resources/ResourceLocation.java.patch
rename to paper-server/patches/sources/net/minecraft/resources/ResourceLocation.java.patch
index 4f2bd280fb..db3f21405a 100644
--- a/paper-server/patches/unapplied/net/minecraft/resources/ResourceLocation.java.patch
+++ b/paper-server/patches/sources/net/minecraft/resources/ResourceLocation.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/resources/ResourceLocation.java
 +++ b/net/minecraft/resources/ResourceLocation.java
-@@ -32,6 +32,7 @@
+@@ -32,6 +_,7 @@
      public static final char NAMESPACE_SEPARATOR = ':';
      public static final String DEFAULT_NAMESPACE = "minecraft";
      public static final String REALMS_NAMESPACE = "realms";
@@ -8,7 +8,7 @@
      private final String namespace;
      private final String path;
  
-@@ -40,6 +41,13 @@
+@@ -40,6 +_,13 @@
  
          assert isValidPath(path);
  
@@ -22,7 +22,7 @@
          this.namespace = namespace;
          this.path = path;
      }
-@@ -246,7 +254,7 @@
+@@ -252,7 +_,7 @@
  
      private static String assertValidNamespace(String namespace, String path) {
          if (!isValidNamespace(namespace)) {
@@ -31,7 +31,7 @@
          } else {
              return namespace;
          }
-@@ -267,7 +275,7 @@
+@@ -277,7 +_,7 @@
  
      private static String assertValidPath(String namespace, String path) {
          if (!isValidPath(path)) {
diff --git a/paper-server/patches/sources/net/minecraft/server/Bootstrap.java.patch b/paper-server/patches/sources/net/minecraft/server/Bootstrap.java.patch
new file mode 100644
index 0000000000..c2cbc6a238
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/Bootstrap.java.patch
@@ -0,0 +1,92 @@
+--- a/net/minecraft/server/Bootstrap.java
++++ b/net/minecraft/server/Bootstrap.java
+@@ -43,6 +_,7 @@
+         if (!isBootstrapped) {
+             isBootstrapped = true;
+             Instant instant = Instant.now();
++            io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler.enterBootstrappers(); // Paper - Entrypoint for bootstrapping
+             if (BuiltInRegistries.REGISTRY.keySet().isEmpty()) {
+                 throw new IllegalStateException("Unable to load registries");
+             } else {
+@@ -54,11 +_,80 @@
+                     EntitySelectorOptions.bootStrap();
+                     DispenseItemBehavior.bootStrap();
+                     CauldronInteraction.bootStrap();
+-                    BuiltInRegistries.bootStrap();
++                    // Paper start
++                    BuiltInRegistries.bootStrap(() -> {
++                        io.papermc.paper.world.worldgen.OptionallyFlatBedrockConditionSource.bootstrap(); // Paper - Flat bedrock generator settings
++                    });
++                    // Paper end
+                     CreativeModeTabs.validate();
+                     wrapStreams();
+                     bootstrapDuration.set(Duration.between(instant, Instant.now()).toMillis());
+                 }
++                // CraftBukkit start
++                // TODO Check what of this is needed, maybe report it to Mojira. if deemed relevant, move to the respective classes
++                //  Used in CraftLegacy
++                net.minecraft.util.datafix.fixes.BlockStateData.register(1008, "{Name:'minecraft:oak_sign',Properties:{rotation:'0'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'0'}}");
++                net.minecraft.util.datafix.fixes.BlockStateData.register(1009, "{Name:'minecraft:oak_sign',Properties:{rotation:'1'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'1'}}");
++                net.minecraft.util.datafix.fixes.BlockStateData.register(1010, "{Name:'minecraft:oak_sign',Properties:{rotation:'2'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'2'}}");
++                net.minecraft.util.datafix.fixes.BlockStateData.register(1011, "{Name:'minecraft:oak_sign',Properties:{rotation:'3'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'3'}}");
++                net.minecraft.util.datafix.fixes.BlockStateData.register(1012, "{Name:'minecraft:oak_sign',Properties:{rotation:'4'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'4'}}");
++                net.minecraft.util.datafix.fixes.BlockStateData.register(1013, "{Name:'minecraft:oak_sign',Properties:{rotation:'5'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'5'}}");
++                net.minecraft.util.datafix.fixes.BlockStateData.register(1014, "{Name:'minecraft:oak_sign',Properties:{rotation:'6'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'6'}}");
++                net.minecraft.util.datafix.fixes.BlockStateData.register(1015, "{Name:'minecraft:oak_sign',Properties:{rotation:'7'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'7'}}");
++                net.minecraft.util.datafix.fixes.BlockStateData.register(1016, "{Name:'minecraft:oak_sign',Properties:{rotation:'8'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'8'}}");
++                net.minecraft.util.datafix.fixes.BlockStateData.register(1017, "{Name:'minecraft:oak_sign',Properties:{rotation:'9'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'9'}}");
++                net.minecraft.util.datafix.fixes.BlockStateData.register(1018, "{Name:'minecraft:oak_sign',Properties:{rotation:'10'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'10'}}");
++                net.minecraft.util.datafix.fixes.BlockStateData.register(1019, "{Name:'minecraft:oak_sign',Properties:{rotation:'11'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'11'}}");
++                net.minecraft.util.datafix.fixes.BlockStateData.register(1020, "{Name:'minecraft:oak_sign',Properties:{rotation:'12'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'12'}}");
++                net.minecraft.util.datafix.fixes.BlockStateData.register(1021, "{Name:'minecraft:oak_sign',Properties:{rotation:'13'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'13'}}");
++                net.minecraft.util.datafix.fixes.BlockStateData.register(1022, "{Name:'minecraft:oak_sign',Properties:{rotation:'14'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'14'}}");
++                net.minecraft.util.datafix.fixes.BlockStateData.register(1023, "{Name:'minecraft:oak_sign',Properties:{rotation:'15'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'15'}}");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(323, "minecraft:oak_sign");
++
++                net.minecraft.util.datafix.fixes.BlockStateData.register(1440, "{Name:'minecraft:portal',Properties:{axis:'x'}}", "{Name:'minecraft:portal',Properties:{axis:'x'}}");
++
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(409, "minecraft:prismarine_shard");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(410, "minecraft:prismarine_crystals");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(411, "minecraft:rabbit");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(412, "minecraft:cooked_rabbit");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(413, "minecraft:rabbit_stew");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(414, "minecraft:rabbit_foot");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(415, "minecraft:rabbit_hide");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(416, "minecraft:armor_stand");
++
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(423, "minecraft:mutton");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(424, "minecraft:cooked_mutton");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(425, "minecraft:banner");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(426, "minecraft:end_crystal");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(427, "minecraft:spruce_door");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(428, "minecraft:birch_door");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(429, "minecraft:jungle_door");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(430, "minecraft:acacia_door");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(431, "minecraft:dark_oak_door");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(432, "minecraft:chorus_fruit");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(433, "minecraft:chorus_fruit_popped");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(434, "minecraft:beetroot");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(435, "minecraft:beetroot_seeds");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(436, "minecraft:beetroot_soup");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(437, "minecraft:dragon_breath");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(438, "minecraft:splash_potion");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(439, "minecraft:spectral_arrow");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(440, "minecraft:tipped_arrow");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(441, "minecraft:lingering_potion");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(442, "minecraft:shield");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(443, "minecraft:elytra");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(444, "minecraft:spruce_boat");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(445, "minecraft:birch_boat");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(446, "minecraft:jungle_boat");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(447, "minecraft:acacia_boat");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(448, "minecraft:dark_oak_boat");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(449, "minecraft:totem_of_undying");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(450, "minecraft:shulker_shell");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(452, "minecraft:iron_nugget");
++                net.minecraft.util.datafix.fixes.ItemIdFix.ITEM_NAMES.put(453, "minecraft:knowledge_book");
++
++                net.minecraft.util.datafix.fixes.ItemSpawnEggFix.ID_TO_ENTITY[23] = "Arrow";
++                // CraftBukkit end
+             }
+         }
+     }
diff --git a/paper-server/patches/sources/net/minecraft/server/Main.java.patch b/paper-server/patches/sources/net/minecraft/server/Main.java.patch
new file mode 100644
index 0000000000..b9d9570fdd
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/Main.java.patch
@@ -0,0 +1,252 @@
+--- a/net/minecraft/server/Main.java
++++ b/net/minecraft/server/Main.java
+@@ -67,8 +_,10 @@
+         reason = "System.out needed before bootstrap"
+     )
+     @DontObfuscate
+-    public static void main(String[] args) {
++    public static void main(final OptionSet optionSet) { // CraftBukkit - replaces main(String[] args)
++        io.papermc.paper.util.LogManagerShutdownThread.hook(); // Paper - Improved watchdog support
+         SharedConstants.tryDetectVersion();
++        /* CraftBukkit start - Replace everything
+         OptionParser optionParser = new OptionParser();
+         OptionSpec<Void> optionSpec = optionParser.accepts("nogui");
+         OptionSpec<Void> optionSpec1 = optionParser.accepts("initSettings", "Initializes 'server.properties' and 'eula.txt', then quits");
+@@ -93,41 +_,94 @@
+                 optionParser.printHelpOn(System.err);
+                 return;
+             }
++            */ // CraftBukkit end
++        try {
+ 
+-            Path path = optionSet.valueOf(optionSpec14);
++            Path path = (Path) optionSet.valueOf("pidFile"); // CraftBukkit
+             if (path != null) {
+                 writePidFile(path);
+             }
+ 
+             CrashReport.preload();
+-            if (optionSet.has(optionSpec13)) {
++            if (optionSet.has("jfrProfile")) { // CraftBukkit
+                 JvmProfiler.INSTANCE.start(Environment.SERVER);
+             }
+ 
++            io.papermc.paper.plugin.PluginInitializerManager.load(optionSet); // Paper
+             Bootstrap.bootStrap();
+             Bootstrap.validate();
+             Util.startTimerHackThread();
+             Path path1 = Paths.get("server.properties");
+-            DedicatedServerSettings dedicatedServerSettings = new DedicatedServerSettings(path1);
++            DedicatedServerSettings dedicatedServerSettings = new DedicatedServerSettings(optionSet); // CraftBukkit - CLI argument support
+             dedicatedServerSettings.forceSave();
+             RegionFileVersion.configure(dedicatedServerSettings.getProperties().regionFileComression);
+             Path path2 = Paths.get("eula.txt");
+             Eula eula = new Eula(path2);
+-            if (optionSet.has(optionSpec1)) {
++            // Paper start - load config files early for access below if needed
++            org.bukkit.configuration.file.YamlConfiguration bukkitConfiguration = io.papermc.paper.configuration.PaperConfigurations.loadLegacyConfigFile((File) optionSet.valueOf("bukkit-settings"));
++            org.bukkit.configuration.file.YamlConfiguration spigotConfiguration = io.papermc.paper.configuration.PaperConfigurations.loadLegacyConfigFile((File) optionSet.valueOf("spigot-settings"));
++            // Paper end - load config files early for access below if needed
++            if (optionSet.has("initSettings")) { // CraftBukkit
++                // CraftBukkit start - SPIGOT-5761: Create bukkit.yml and commands.yml if not present
++                File configFile = (File) optionSet.valueOf("bukkit-settings");
++                org.bukkit.configuration.file.YamlConfiguration configuration = org.bukkit.configuration.file.YamlConfiguration.loadConfiguration(configFile);
++                configuration.options().copyDefaults(true);
++                configuration.setDefaults(org.bukkit.configuration.file.YamlConfiguration.loadConfiguration(new java.io.InputStreamReader(Main.class.getClassLoader().getResourceAsStream("configurations/bukkit.yml"), com.google.common.base.Charsets.UTF_8)));
++                configuration.save(configFile);
++
++                File commandFile = (File) optionSet.valueOf("commands-settings");
++                org.bukkit.configuration.file.YamlConfiguration commandsConfiguration = org.bukkit.configuration.file.YamlConfiguration.loadConfiguration(commandFile);
++                commandsConfiguration.options().copyDefaults(true);
++                commandsConfiguration.setDefaults(org.bukkit.configuration.file.YamlConfiguration.loadConfiguration(new java.io.InputStreamReader(Main.class.getClassLoader().getResourceAsStream("configurations/commands.yml"), com.google.common.base.Charsets.UTF_8)));
++                commandsConfiguration.save(commandFile);
++                // CraftBukkit end
+                 LOGGER.info("Initialized '{}' and '{}'", path1.toAbsolutePath(), path2.toAbsolutePath());
+                 return;
+             }
+ 
+-            if (!eula.hasAgreedToEULA()) {
++            // Spigot start
++            boolean eulaAgreed = Boolean.getBoolean("com.mojang.eula.agree");
++            if (eulaAgreed) {
++                LOGGER.error("You have used the Spigot command line EULA agreement flag.");
++                LOGGER.error("By using this setting you are indicating your agreement to Mojang's EULA (https://aka.ms/MinecraftEULA).");
++                LOGGER.error("If you do not agree to the above EULA please stop your server and remove this flag immediately.");
++            }
++            if (!eula.hasAgreedToEULA() && !eulaAgreed) {
++                // Spigot end
+                 LOGGER.info("You need to agree to the EULA in order to run the server. Go to eula.txt for more info.");
+                 return;
+             }
+ 
+-            File file = new File(optionSet.valueOf(optionSpec9));
+-            Services services = Services.create(new YggdrasilAuthenticationService(Proxy.NO_PROXY), file);
+-            String string = Optional.ofNullable(optionSet.valueOf(optionSpec10)).orElse(dedicatedServerSettings.getProperties().levelName);
++            // Paper start - Detect headless JRE
++            String awtException = io.papermc.paper.util.ServerEnvironment.awtDependencyCheck();
++            if (awtException != null) {
++                LOGGER.error("You are using a headless JRE distribution.");
++                LOGGER.error("This distribution is missing certain graphic libraries that the Minecraft server needs to function.");
++                LOGGER.error("For instructions on how to install the non-headless JRE, see https://docs.papermc.io/misc/java-install");
++                LOGGER.error("");
++                LOGGER.error(awtException);
++                return;
++            }
++            // Paper end - Detect headless JRE
++
++            org.spigotmc.SpigotConfig.disabledAdvancements = spigotConfiguration.getStringList("advancements.disabled"); // Paper - fix SPIGOT-5885, must be set early in init
++
++            // Paper start - fix SPIGOT-5824
++            File file;
++            File userCacheFile = new File(Services.USERID_CACHE_FILE);
++            if (optionSet.has("universe")) {
++                file = (File) optionSet.valueOf("universe"); // CraftBukkit
++                userCacheFile = new File(file, Services.USERID_CACHE_FILE);
++            } else {
++                file = new File(bukkitConfiguration.getString("settings.world-container", "."));
++            }
++            // Paper end - fix SPIGOT-5824
++            Services services = Services.create(new com.destroystokyo.paper.profile.PaperAuthenticationService(Proxy.NO_PROXY), file, userCacheFile, optionSet); // Paper - pass OptionSet to load paper config files; override authentication service; fix world-container
++            // CraftBukkit start
++            String string = Optional.ofNullable((String) optionSet.valueOf("world")).orElse(dedicatedServerSettings.getProperties().levelName);
+             LevelStorageSource levelStorageSource = LevelStorageSource.createDefault(file.toPath());
+-            LevelStorageSource.LevelStorageAccess levelStorageAccess = levelStorageSource.validateAndCreateAccess(string);
++            LevelStorageSource.LevelStorageAccess levelStorageAccess = levelStorageSource.validateAndCreateAccess(string, LevelStem.OVERWORLD);
++            // CraftBukkit end
+             Dynamic<?> dataTag;
+             if (levelStorageAccess.hasWorldData()) {
+                 LevelSummary summary;
+@@ -169,12 +_,30 @@
+             }
+ 
+             Dynamic<?> dynamic = dataTag;
+-            boolean hasOptionSpec = optionSet.has(optionSpec7);
++            boolean hasOptionSpec = optionSet.has("safeMode"); // CraftBukkit
+             if (hasOptionSpec) {
+                 LOGGER.warn("Safe mode active, only vanilla datapack will be loaded");
+             }
+ 
+             PackRepository packRepository = ServerPacksSource.createPackRepository(levelStorageAccess);
++            // CraftBukkit start
++            File bukkitDataPackFolder = new File(levelStorageAccess.getLevelPath(net.minecraft.world.level.storage.LevelResource.DATAPACK_DIR).toFile(), "bukkit");
++            if (!bukkitDataPackFolder.exists()) {
++                bukkitDataPackFolder.mkdirs();
++            }
++            File mcMeta = new File(bukkitDataPackFolder, "pack.mcmeta");
++            try {
++                com.google.common.io.Files.write("{\n"
++                        + "    \"pack\": {\n"
++                        + "        \"description\": \"Data pack for resources provided by Bukkit plugins\",\n"
++                        + "        \"pack_format\": " + SharedConstants.getCurrentVersion().getPackVersion(net.minecraft.server.packs.PackType.SERVER_DATA) + "\n"
++                        + "    }\n"
++                        + "}\n", mcMeta, com.google.common.base.Charsets.UTF_8);
++            } catch (java.io.IOException ex) {
++                throw new RuntimeException("Could not initialize Bukkit datapack", ex);
++            }
++            java.util.concurrent.atomic.AtomicReference<WorldLoader.DataLoadContext> worldLoader = new java.util.concurrent.atomic.AtomicReference<>();
++            // CraftBukkit end
+ 
+             WorldStem worldStem;
+             try {
+@@ -183,6 +_,7 @@
+                         executor -> WorldLoader.load(
+                             initConfig,
+                             context -> {
++                                worldLoader.set(context); // CraftBukkit
+                                 Registry<LevelStem> registry = context.datapackDimensions().lookupOrThrow(Registries.LEVEL_STEM);
+                                 if (dynamic != null) {
+                                     LevelDataAndDimensions levelDataAndDimensions = LevelStorageSource.getLevelDataAndDimensions(
+@@ -196,7 +_,7 @@
+                                     LevelSettings levelSettings;
+                                     WorldOptions worldOptions;
+                                     WorldDimensions worldDimensions;
+-                                    if (optionSet.has(optionSpec2)) {
++                                    if (optionSet.has("demo")) { // CraftBukkit
+                                         levelSettings = MinecraftServer.DEMO_SETTINGS;
+                                         worldOptions = WorldOptions.DEMO_OPTIONS;
+                                         worldDimensions = WorldPresets.createNormalWorldDimensions(context.datapackWorldgen());
+@@ -211,7 +_,7 @@
+                                             new GameRules(context.dataConfiguration().enabledFeatures()),
+                                             context.dataConfiguration()
+                                         );
+-                                        worldOptions = optionSet.has(optionSpec3) ? properties.worldOptions.withBonusChest(true) : properties.worldOptions;
++                                        worldOptions = optionSet.has("bonusChest") ? properties.worldOptions.withBonusChest(true) : properties.worldOptions; // CraftBukkit
+                                         worldDimensions = properties.createDimensions(context.datapackWorldgen());
+                                     }
+ 
+@@ -237,6 +_,7 @@
+                 return;
+             }
+ 
++            /*
+             RegistryAccess.Frozen frozen = worldStem.registries().compositeAccess();
+             boolean hasOptionSpec1 = optionSet.has(optionSpec6);
+             if (optionSet.has(optionSpec4) || hasOptionSpec1) {
+@@ -245,9 +_,13 @@
+ 
+             WorldData worldData = worldStem.worldData();
+             levelStorageAccess.saveDataTag(frozen, worldData);
++            */
+             final DedicatedServer dedicatedServer = MinecraftServer.spin(
+                 thread1 -> {
+                     DedicatedServer dedicatedServer1 = new DedicatedServer(
++                        // CraftBukkit start
++                        optionSet,
++                        worldLoader.get(),
+                         thread1,
+                         levelStorageAccess,
+                         packRepository,
+@@ -257,17 +_,34 @@
+                         services,
+                         LoggerChunkProgressListener::createFromGameruleRadius
+                     );
++                    /*
+                     dedicatedServer1.setPort(optionSet.valueOf(optionSpec11));
+-                    dedicatedServer1.setDemo(optionSet.has(optionSpec2));
++                     */
++                    // Paper start
++                    if (optionSet.has("serverId")) {
++                        dedicatedServer1.setId((String) optionSet.valueOf("serverId"));
++                    }
++                    dedicatedServer1.setDemo(optionSet.has("demo"));
++                    // Paper end
++                    /*
+                     dedicatedServer1.setId(optionSet.valueOf(optionSpec12));
+-                    boolean flag = !optionSet.has(optionSpec) && !optionSet.valuesOf(optionSpec15).contains("nogui");
++                     */
++                    boolean flag = !optionSet.has("nogui") && !optionSet.nonOptionArguments().contains("nogui");
+                     if (flag && !GraphicsEnvironment.isHeadless()) {
+                         dedicatedServer1.showGui();
+                     }
+ 
++                    if (optionSet.has("port")) {
++                        int port = (Integer) optionSet.valueOf("port");
++                        if (port > 0) {
++                            dedicatedServer1.setPort(port);
++                        }
++                    }
++
+                     return dedicatedServer1;
+                 }
+             );
++            /* CraftBukkit start
+             Thread thread = new Thread("Server Shutdown Thread") {
+                 @Override
+                 public void run() {
+@@ -276,6 +_,7 @@
+             };
+             thread.setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandler(LOGGER));
+             Runtime.getRuntime().addShutdownHook(thread);
++            */ // CraftBukkit end
+         } catch (Exception var42) {
+             LOGGER.error(LogUtils.FATAL_MARKER, "Failed to start the minecraft server", (Throwable)var42);
+         }
+@@ -316,7 +_,7 @@
+         RegistryAccess registryAccess,
+         boolean recreateRegionFiles
+     ) {
+-        LOGGER.info("Forcing world upgrade!");
++        LOGGER.info("Forcing world upgrade! {}", levelStorage.getLevelId()); // CraftBukkit
+ 
+         try (WorldUpgrader worldUpgrader = new WorldUpgrader(levelStorage, dataFixer, registryAccess, eraseCache, recreateRegionFiles)) {
+             Component component = null;
diff --git a/paper-server/patches/sources/net/minecraft/server/MinecraftServer.java.patch b/paper-server/patches/sources/net/minecraft/server/MinecraftServer.java.patch
new file mode 100644
index 0000000000..d8518cce12
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/MinecraftServer.java.patch
@@ -0,0 +1,1352 @@
+--- a/net/minecraft/server/MinecraftServer.java
++++ b/net/minecraft/server/MinecraftServer.java
+@@ -174,11 +_,13 @@
+ import org.slf4j.Logger;
+ 
+ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTask> implements ServerInfo, ChunkIOErrorReporter, CommandSource {
++    private static MinecraftServer SERVER; // Paper
+     public static final Logger LOGGER = LogUtils.getLogger();
++    public static final net.kyori.adventure.text.logger.slf4j.ComponentLogger COMPONENT_LOGGER = net.kyori.adventure.text.logger.slf4j.ComponentLogger.logger(LOGGER.getName()); // Paper
+     public static final String VANILLA_BRAND = "vanilla";
+     private static final float AVERAGE_TICK_TIME_SMOOTHING = 0.8F;
+     private static final int TICK_STATS_SPAN = 100;
+-    private static final long OVERLOADED_THRESHOLD_NANOS = 20L * TimeUtil.NANOSECONDS_PER_SECOND / 20L;
++    private static final long OVERLOADED_THRESHOLD_NANOS = 30L * TimeUtil.NANOSECONDS_PER_SECOND / 20L; // CraftBukkit
+     private static final int OVERLOADED_TICKS_THRESHOLD = 20;
+     private static final long OVERLOADED_WARNING_INTERVAL_NANOS = 10L * TimeUtil.NANOSECONDS_PER_SECOND;
+     private static final int OVERLOADED_TICKS_WARNING_INTERVAL = 100;
+@@ -218,6 +_,7 @@
+     private Map<ResourceKey<Level>, ServerLevel> levels = Maps.newLinkedHashMap();
+     private PlayerList playerList;
+     private volatile boolean running = true;
++    private volatile boolean isRestarting = false; // Paper - flag to signify we're attempting to restart
+     private boolean stopped;
+     private int tickCount;
+     private int ticksUntilAutosave = 6000;
+@@ -226,11 +_,15 @@
+     private boolean preventProxyConnections;
+     private boolean pvp;
+     private boolean allowFlight;
+-    @Nullable
+-    private String motd;
++    private net.kyori.adventure.text.Component motd; // Paper - Adventure
+     private int playerIdleTimeout;
+     private final long[] tickTimesNanos = new long[100];
+     private long aggregatedTickTimesNanos = 0L;
++    // Paper start - Add tick times API and /mspt command
++    public final TickTimes tickTimes5s = new TickTimes(100);
++    public final TickTimes tickTimes10s = new TickTimes(200);
++    public final TickTimes tickTimes60s = new TickTimes(1200);
++    // Paper end - Add tick times API and /mspt command
+     @Nullable
+     private KeyPair keyPair;
+     @Nullable
+@@ -271,10 +_,37 @@
+     private final SuppressedExceptionCollector suppressedExceptions = new SuppressedExceptionCollector();
+     private final DiscontinuousFrame tickFrame;
+ 
++    // CraftBukkit start
++    public final WorldLoader.DataLoadContext worldLoader;
++    public org.bukkit.craftbukkit.CraftServer server;
++    public joptsimple.OptionSet options;
++    public org.bukkit.command.ConsoleCommandSender console;
++    public static int currentTick; // Paper - improve tick loop
++    public java.util.Queue<Runnable> processQueue = new java.util.concurrent.ConcurrentLinkedQueue<Runnable>();
++    public int autosavePeriod;
++    // Paper - don't store the vanilla dispatcher
++    public boolean forceTicks;
++    // CraftBukkit end
++    // Spigot start
++    public static final int TPS = 20;
++    public static final int TICK_TIME = 1000000000 / MinecraftServer.TPS;
++    private static final int SAMPLE_INTERVAL = 20; // Paper - improve server tick loop
++    @Deprecated(forRemoval = true) // Paper
++    public final double[] recentTps = new double[3];
++    // Spigot end
++    public volatile boolean hasFullyShutdown; // Paper - Improved watchdog support
++    public volatile boolean abnormalExit; // Paper - Improved watchdog support
++    public volatile Thread shutdownThread; // Paper - Improved watchdog support
++    public final io.papermc.paper.configuration.PaperConfigurations paperConfigurations; // Paper - add paper configuration files
++    public boolean isIteratingOverLevels = false; // Paper - Throw exception on world create while being ticked
++    private final Set<String> pluginsBlockingSleep = new java.util.HashSet<>(); // Paper - API to allow/disallow tick sleeping
++    public static final long SERVER_INIT = System.nanoTime(); // Paper - Lag compensation
++
+     public static <S extends MinecraftServer> S spin(Function<Thread, S> threadFunction) {
+         AtomicReference<S> atomicReference = new AtomicReference<>();
+-        Thread thread = new Thread(() -> atomicReference.get().runServer(), "Server thread");
++        Thread thread = new ca.spottedleaf.moonrise.common.util.TickThread(() -> atomicReference.get().runServer(), "Server thread");
+         thread.setUncaughtExceptionHandler((thread1, exception) -> LOGGER.error("Uncaught exception in server thread", exception));
++        thread.setPriority(Thread.NORM_PRIORITY+2); // Paper - Perf: Boost priority
+         if (Runtime.getRuntime().availableProcessors() > 4) {
+             thread.setPriority(8);
+         }
+@@ -286,6 +_,10 @@
+     }
+ 
+     public MinecraftServer(
++        // CraftBukkit start
++        joptsimple.OptionSet options,
++        WorldLoader.DataLoadContext worldLoader,
++        // CraftBukkit end
+         Thread serverThread,
+         LevelStorageSource.LevelStorageAccess storageSource,
+         PackRepository packRepository,
+@@ -296,9 +_,10 @@
+         ChunkProgressListenerFactory progressListenerFactory
+     ) {
+         super("Server");
++        SERVER = this; // Paper - better singleton
+         this.registries = worldStem.registries();
+         this.worldData = worldStem.worldData();
+-        if (!this.registries.compositeAccess().lookupOrThrow(Registries.LEVEL_STEM).containsKey(LevelStem.OVERWORLD)) {
++        if (false && !this.registries.compositeAccess().lookupOrThrow(Registries.LEVEL_STEM).containsKey(LevelStem.OVERWORLD)) { // CraftBukkit - initialised later
+             throw new IllegalStateException("Missing Overworld dimension data");
+         } else {
+             this.proxy = proxy;
+@@ -309,7 +_,7 @@
+                 services.profileCache().setExecutor(this);
+             }
+ 
+-            this.connection = new ServerConnectionListener(this);
++            // this.connection = new ServerConnectionListener(this); // Spigot
+             this.tickRateManager = new ServerTickRateManager(this);
+             this.progressListenerFactory = progressListenerFactory;
+             this.storageSource = storageSource;
+@@ -328,6 +_,38 @@
+             this.fuelValues = FuelValues.vanillaBurnTimes(this.registries.compositeAccess(), this.worldData.enabledFeatures());
+             this.tickFrame = TracyClient.createDiscontinuousFrame("Server Tick");
+         }
++        // CraftBukkit start
++        this.options = options;
++        this.worldLoader = worldLoader;
++        // Paper start - Handled by TerminalConsoleAppender
++        // Try to see if we're actually running in a terminal, disable jline if not
++        /*
++        if (System.console() == null && System.getProperty("jline.terminal") == null) {
++            System.setProperty("jline.terminal", "jline.UnsupportedTerminal");
++            Main.useJline = false;
++        }
++
++        try {
++            this.reader = new ConsoleReader(System.in, System.out);
++            this.reader.setExpandEvents(false); // Avoid parsing exceptions for uncommonly used event designators
++        } catch (Throwable e) {
++            try {
++                // Try again with jline disabled for Windows users without C++ 2008 Redistributable
++                System.setProperty("jline.terminal", "jline.UnsupportedTerminal");
++                System.setProperty("user.language", "en");
++                Main.useJline = false;
++                this.reader = new ConsoleReader(System.in, System.out);
++                this.reader.setExpandEvents(false);
++            } catch (IOException ex) {
++                MinecraftServer.LOGGER.warn((String) null, ex);
++            }
++        }
++        */
++        // Paper end
++        io.papermc.paper.util.LogManagerShutdownThread.unhook(); // Paper - Improved watchdog support
++        Runtime.getRuntime().addShutdownHook(new org.bukkit.craftbukkit.util.ServerShutdownThread(this));
++        // CraftBukkit end
++        this.paperConfigurations = services.paperConfigurations(); // Paper - add paper configuration files
+     }
+ 
+     private void readScoreboard(DimensionDataStorage dataStorage) {
+@@ -336,18 +_,13 @@
+ 
+     protected abstract boolean initServer() throws IOException;
+ 
+-    protected void loadLevel() {
++    protected void loadLevel(String levelId) { // CraftBukkit
+         if (!JvmProfiler.INSTANCE.isRunning()) {
+         }
+ 
+         boolean flag = false;
+         ProfiledDuration profiledDuration = JvmProfiler.INSTANCE.onWorldLoadedStarted();
+-        this.worldData.setModdedInfo(this.getServerModName(), this.getModdedStatus().shouldReportAsModified());
+-        ChunkProgressListener chunkProgressListener = this.progressListenerFactory
+-            .create(this.worldData.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS));
+-        this.createLevels(chunkProgressListener);
+-        this.forceDifficulty();
+-        this.prepareLevels(chunkProgressListener);
++        this.loadWorld0(levelId); // CraftBukkit
+         if (profiledDuration != null) {
+             profiledDuration.finish(true);
+         }
+@@ -364,25 +_,265 @@
+     protected void forceDifficulty() {
+     }
+ 
+-    protected void createLevels(ChunkProgressListener listener) {
+-        ServerLevelData serverLevelData = this.worldData.overworldData();
+-        boolean isDebugWorld = this.worldData.isDebugWorld();
+-        Registry<LevelStem> registry = this.registries.compositeAccess().lookupOrThrow(Registries.LEVEL_STEM);
+-        WorldOptions worldOptions = this.worldData.worldGenOptions();
+-        long seed = worldOptions.seed();
+-        long l = BiomeManager.obfuscateSeed(seed);
+-        List<CustomSpawner> list = ImmutableList.of(
+-            new PhantomSpawner(), new PatrolSpawner(), new CatSpawner(), new VillageSiege(), new WanderingTraderSpawner(serverLevelData)
+-        );
+-        LevelStem levelStem = registry.getValue(LevelStem.OVERWORLD);
+-        ServerLevel serverLevel = new ServerLevel(
+-            this, this.executor, this.storageSource, serverLevelData, Level.OVERWORLD, levelStem, listener, isDebugWorld, l, list, true, null
+-        );
+-        this.levels.put(Level.OVERWORLD, serverLevel);
+-        DimensionDataStorage dataStorage = serverLevel.getDataStorage();
+-        this.readScoreboard(dataStorage);
+-        this.commandStorage = new CommandStorage(dataStorage);
+-        WorldBorder worldBorder = serverLevel.getWorldBorder();
++    // CraftBukkit start
++    private void loadWorld0(String levelId) {
++        // Mostly modelled off of net.minecraft.server.Main
++        LevelStorageSource.LevelStorageAccess levelStorageAccess = this.storageSource;
++        RegistryAccess.Frozen registryAccess = this.registries.compositeAccess();
++        Registry<LevelStem> levelStemRegistry = registryAccess.lookupOrThrow(Registries.LEVEL_STEM);
++        for (LevelStem levelStem : levelStemRegistry) {
++            ResourceKey<LevelStem> levelStemKey = levelStemRegistry.getResourceKey(levelStem).get();
++            ServerLevel serverLevel;
++            int dimension = 0;
++
++            if (levelStemKey == LevelStem.NETHER) {
++                if (this.server.getAllowNether()) {
++                    dimension = -1;
++                } else {
++                    continue;
++                }
++            } else if (levelStemKey == LevelStem.END) {
++                if (this.server.getAllowEnd()) {
++                    dimension = 1;
++                } else {
++                    continue;
++                }
++            } else if (levelStemKey != LevelStem.OVERWORLD) {
++                dimension = -999;
++            }
++
++            // Migration of old CB world folders...
++            String worldType = (dimension == -999) ? levelStemKey.location().getNamespace() + "_" + levelStemKey.location().getPath() : org.bukkit.World.Environment.getEnvironment(dimension).toString().toLowerCase(Locale.ROOT);
++            String name = (levelStemKey == LevelStem.OVERWORLD) ? levelId : levelId + "_" + worldType;
++            if (dimension != 0) {
++                java.io.File newWorld = LevelStorageSource.getStorageFolder(new java.io.File(name).toPath(), levelStemKey).toFile();
++                java.io.File oldWorld = LevelStorageSource.getStorageFolder(new java.io.File(levelId).toPath(), levelStemKey).toFile();
++                java.io.File oldLevelDat = new java.io.File(new java.io.File(levelId), "level.dat"); // The data folders exist on first run as they are created in the PersistentCollection constructor above, but the level.dat won't
++
++                if (!newWorld.isDirectory() && oldWorld.isDirectory() && oldLevelDat.isFile()) {
++                    MinecraftServer.LOGGER.info("---- Migration of old " + worldType + " folder required ----");
++                    MinecraftServer.LOGGER.info("Unfortunately due to the way that Minecraft implemented multiworld support in 1.6, Bukkit requires that you move your " + worldType + " folder to a new location in order to operate correctly.");
++                    MinecraftServer.LOGGER.info("We will move this folder for you, but it will mean that you need to move it back should you wish to stop using Bukkit in the future.");
++                    MinecraftServer.LOGGER.info("Attempting to move " + oldWorld + " to " + newWorld + "...");
++
++                    if (newWorld.exists()) {
++                        MinecraftServer.LOGGER.warn("A file or folder already exists at " + newWorld + "!");
++                        MinecraftServer.LOGGER.info("---- Migration of old " + worldType + " folder failed ----");
++                    } else if (newWorld.getParentFile().mkdirs()) {
++                        if (oldWorld.renameTo(newWorld)) {
++                            MinecraftServer.LOGGER.info("Success! To restore " + worldType + " in the future, simply move " + newWorld + " to " + oldWorld);
++                            // Migrate world data too.
++                            try {
++                                com.google.common.io.Files.copy(oldLevelDat, new java.io.File(new java.io.File(name), "level.dat"));
++                                org.apache.commons.io.FileUtils.copyDirectory(new java.io.File(new java.io.File(levelId), "data"), new java.io.File(new java.io.File(name), "data"));
++                            } catch (IOException exception) {
++                                MinecraftServer.LOGGER.warn("Unable to migrate world data.");
++                            }
++                            MinecraftServer.LOGGER.info("---- Migration of old " + worldType + " folder complete ----");
++                        } else {
++                            MinecraftServer.LOGGER.warn("Could not move folder " + oldWorld + " to " + newWorld + "!");
++                            MinecraftServer.LOGGER.info("---- Migration of old " + worldType + " folder failed ----");
++                        }
++                    } else {
++                        MinecraftServer.LOGGER.warn("Could not create path for " + newWorld + "!");
++                        MinecraftServer.LOGGER.info("---- Migration of old " + worldType + " folder failed ----");
++                    }
++                }
++
++                try {
++                    levelStorageAccess = LevelStorageSource.createDefault(this.server.getWorldContainer().toPath()).validateAndCreateAccess(name, levelStemKey);
++                } catch (IOException | net.minecraft.world.level.validation.ContentValidationException ex) {
++                    throw new RuntimeException(ex);
++                }
++            }
++
++            com.mojang.serialization.Dynamic<?> dataTag;
++            if (levelStorageAccess.hasWorldData()) {
++                net.minecraft.world.level.storage.LevelSummary summary;
++                try {
++                    dataTag = levelStorageAccess.getDataTag();
++                    summary = levelStorageAccess.getSummary(dataTag);
++                } catch (net.minecraft.nbt.NbtException | net.minecraft.nbt.ReportedNbtException | IOException e) {
++                    LevelStorageSource.LevelDirectory levelDirectory = levelStorageAccess.getLevelDirectory();
++                    MinecraftServer.LOGGER.warn("Failed to load world data from {}", levelDirectory.dataFile(), e);
++                    MinecraftServer.LOGGER.info("Attempting to use fallback");
++
++                    try {
++                        dataTag = levelStorageAccess.getDataTagFallback();
++                        summary = levelStorageAccess.getSummary(dataTag);
++                    } catch (net.minecraft.nbt.NbtException | net.minecraft.nbt.ReportedNbtException | IOException e1) {
++                        MinecraftServer.LOGGER.error("Failed to load world data from {}", levelDirectory.oldDataFile(), e1);
++                        MinecraftServer.LOGGER.error(
++                            "Failed to load world data from {} and {}. World files may be corrupted. Shutting down.",
++                            levelDirectory.dataFile(),
++                            levelDirectory.oldDataFile()
++                        );
++                        return;
++                    }
++
++                    levelStorageAccess.restoreLevelDataFromOld();
++                }
++
++                if (summary.requiresManualConversion()) {
++                    MinecraftServer.LOGGER.info("This world must be opened in an older version (like 1.6.4) to be safely converted");
++                    return;
++                }
++
++                if (!summary.isCompatible()) {
++                    MinecraftServer.LOGGER.info("This world was created by an incompatible version.");
++                    return;
++                }
++            } else {
++                dataTag = null;
++            }
++
++            org.bukkit.generator.ChunkGenerator chunkGenerator = this.server.getGenerator(name);
++            org.bukkit.generator.BiomeProvider biomeProvider = this.server.getBiomeProvider(name);
++
++            net.minecraft.world.level.storage.PrimaryLevelData primaryLevelData;
++            WorldLoader.DataLoadContext context = this.worldLoader;
++            Registry<LevelStem> contextLevelStemRegistry = context.datapackDimensions().lookupOrThrow(Registries.LEVEL_STEM);
++            if (dataTag != null) {
++                net.minecraft.world.level.storage.LevelDataAndDimensions levelDataAndDimensions = LevelStorageSource.getLevelDataAndDimensions(
++                    dataTag, context.dataConfiguration(), contextLevelStemRegistry, context.datapackWorldgen()
++                );
++                primaryLevelData = (net.minecraft.world.level.storage.PrimaryLevelData) levelDataAndDimensions.worldData();
++            } else {
++                LevelSettings levelSettings;
++                WorldOptions worldOptions;
++                net.minecraft.world.level.levelgen.WorldDimensions worldDimensions;
++                if (this.isDemo()) {
++                    levelSettings = MinecraftServer.DEMO_SETTINGS;
++                    worldOptions = WorldOptions.DEMO_OPTIONS;
++                    worldDimensions = net.minecraft.world.level.levelgen.presets.WorldPresets.createNormalWorldDimensions(context.datapackWorldgen());
++                } else {
++                    net.minecraft.server.dedicated.DedicatedServerProperties properties = ((net.minecraft.server.dedicated.DedicatedServer) this).getProperties();
++                    levelSettings = new LevelSettings(
++                        properties.levelName,
++                        properties.gamemode,
++                        properties.hardcore,
++                        properties.difficulty,
++                        false,
++                        new GameRules(context.dataConfiguration().enabledFeatures()),
++                        context.dataConfiguration()
++                    );
++                    worldOptions = this.options.has("bonusChest") ? properties.worldOptions.withBonusChest(true) : properties.worldOptions; // CraftBukkit
++                    worldDimensions = properties.createDimensions(context.datapackWorldgen());
++                }
++
++                net.minecraft.world.level.levelgen.WorldDimensions.Complete complete = worldDimensions.bake(contextLevelStemRegistry);
++                com.mojang.serialization.Lifecycle lifecycle = complete.lifecycle().add(context.datapackWorldgen().allRegistriesLifecycle());
++
++                primaryLevelData = new net.minecraft.world.level.storage.PrimaryLevelData(levelSettings, worldOptions, complete.specialWorldProperty(), lifecycle);
++            }
++
++            primaryLevelData.checkName(name); // CraftBukkit - Migration did not rewrite the level.dat; This forces 1.8 to take the last loaded world as respawn (in this case the end)
++            if (this.options.has("forceUpgrade")) {
++                net.minecraft.server.Main.forceUpgrade(levelStorageAccess, net.minecraft.util.datafix.DataFixers.getDataFixer(), this.options.has("eraseCache"), () -> true, registryAccess, this.options.has("recreateRegionFiles"));
++            }
++
++            // Now modelled off the createLevels method
++            net.minecraft.world.level.storage.PrimaryLevelData serverLevelData = primaryLevelData;
++            boolean isDebugWorld = primaryLevelData.isDebugWorld();
++            WorldOptions worldOptions = primaryLevelData.worldGenOptions();
++            long seed = worldOptions.seed();
++            long l = BiomeManager.obfuscateSeed(seed);
++            List<CustomSpawner> list = ImmutableList.of(
++                new PhantomSpawner(), new PatrolSpawner(), new CatSpawner(), new VillageSiege(), new WanderingTraderSpawner(serverLevelData)
++            );
++            LevelStem customStem = levelStemRegistry.getValue(levelStemKey);
++
++            org.bukkit.generator.WorldInfo worldInfo = new org.bukkit.craftbukkit.generator.CraftWorldInfo(serverLevelData, levelStorageAccess, org.bukkit.World.Environment.getEnvironment(dimension), customStem.type().value(), customStem.generator(), this.registryAccess()); // Paper - Expose vanilla BiomeProvider from WorldInfo
++            if (biomeProvider == null && chunkGenerator != null) {
++                biomeProvider = chunkGenerator.getDefaultBiomeProvider(worldInfo);
++            }
++
++            ResourceKey<Level> dimensionKey = ResourceKey.create(Registries.DIMENSION, levelStemKey.location());
++
++            if (levelStemKey == LevelStem.OVERWORLD) {
++                this.worldData = primaryLevelData;
++                this.worldData.setGameType(((net.minecraft.server.dedicated.DedicatedServer) this).getProperties().gamemode); // From DedicatedServer.init
++
++                ChunkProgressListener listener = this.progressListenerFactory.create(this.worldData.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS));
++
++                serverLevel = new ServerLevel(
++                    this, this.executor, levelStorageAccess, serverLevelData, dimensionKey, customStem, listener, isDebugWorld, l, list, true, null,
++                    org.bukkit.World.Environment.getEnvironment(dimension), chunkGenerator, biomeProvider
++                );
++                DimensionDataStorage dataStorage = serverLevel.getDataStorage();
++                this.readScoreboard(dataStorage);
++                this.commandStorage = new CommandStorage(dataStorage);
++                this.server.scoreboardManager = new org.bukkit.craftbukkit.scoreboard.CraftScoreboardManager(this, serverLevel.getScoreboard());
++            } else {
++                ChunkProgressListener listener = this.progressListenerFactory.create(this.worldData.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS));
++                // Paper start - option to use the dimension_type to check if spawners should be added. I imagine mojang will add some datapack-y way of managing this in the future.
++                final List<CustomSpawner> spawners;
++                if (io.papermc.paper.configuration.GlobalConfiguration.get().misc.useDimensionTypeForCustomSpawners && this.registryAccess().lookupOrThrow(Registries.DIMENSION_TYPE).getResourceKey(customStem.type().value()).orElseThrow() == net.minecraft.world.level.dimension.BuiltinDimensionTypes.OVERWORLD) {
++                    spawners = list;
++                } else {
++                    spawners = Collections.emptyList();
++                }
++                serverLevel = new ServerLevel(
++                    this, this.executor, levelStorageAccess, serverLevelData, dimensionKey, customStem, listener, isDebugWorld, l, spawners, true, this.overworld().getRandomSequences(),
++                    org.bukkit.World.Environment.getEnvironment(dimension), chunkGenerator, biomeProvider
++                );
++                // Paper end - option to use the dimension_type to check if spawners should be added
++            }
++
++            // Back to the createLevels method without crazy modifications
++            primaryLevelData.setModdedInfo(this.getServerModName(), this.getModdedStatus().shouldReportAsModified());
++            this.addLevel(serverLevel); // Paper - Put world into worldlist before initing the world; move up
++            this.initWorld(serverLevel, primaryLevelData, this.worldData, worldOptions);
++
++            // Paper - Put world into worldlist before initing the world; move up
++            this.getPlayerList().addWorldborderListener(serverLevel);
++
++            if (primaryLevelData.getCustomBossEvents() != null) {
++                this.getCustomBossEvents().load(primaryLevelData.getCustomBossEvents(), this.registryAccess());
++            }
++        }
++        this.forceDifficulty();
++        for (ServerLevel serverLevel : this.getAllLevels()) {
++            this.prepareLevels(serverLevel.getChunkSource().chunkMap.progressListener, serverLevel);
++            serverLevel.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API
++            this.server.getPluginManager().callEvent(new org.bukkit.event.world.WorldLoadEvent(serverLevel.getWorld()));
++        }
++
++        // Paper start - Configurable player collision; Handle collideRule team for player collision toggle
++        final ServerScoreboard scoreboard = this.getScoreboard();
++        final java.util.Collection<String> toRemove = scoreboard.getPlayerTeams().stream().filter(team -> team.getName().startsWith("collideRule_")).map(net.minecraft.world.scores.PlayerTeam::getName).collect(java.util.stream.Collectors.toList());
++        for (String teamName : toRemove) {
++            scoreboard.removePlayerTeam(scoreboard.getPlayerTeam(teamName)); // Clean up after ourselves
++        }
++
++        if (!io.papermc.paper.configuration.GlobalConfiguration.get().collisions.enablePlayerCollisions) {
++            this.getPlayerList().collideRuleTeamName = org.apache.commons.lang3.StringUtils.left("collideRule_" + java.util.concurrent.ThreadLocalRandom.current().nextInt(), 16);
++            net.minecraft.world.scores.PlayerTeam collideTeam = scoreboard.addPlayerTeam(this.getPlayerList().collideRuleTeamName);
++            collideTeam.setSeeFriendlyInvisibles(false); // Because we want to mimic them not being on a team at all
++        }
++        // Paper end - Configurable player collision
++
++        this.server.enablePlugins(org.bukkit.plugin.PluginLoadOrder.POSTWORLD);
++        this.server.spark.registerCommandBeforePlugins(this.server); // Paper - spark
++        this.server.spark.enableAfterPlugins(this.server); // Paper - spark
++        if (io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper != null) io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper.pluginsEnabled(); // Paper - Remap plugins
++        io.papermc.paper.command.brigadier.PaperCommands.INSTANCE.setValid(); // Paper - reset invalid state for event fire below
++        io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner.INSTANCE.callReloadableRegistrarEvent(io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents.COMMANDS, io.papermc.paper.command.brigadier.PaperCommands.INSTANCE, org.bukkit.plugin.Plugin.class, io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent.Cause.INITIAL); // Paper - call commands event for regular plugins
++        ((org.bukkit.craftbukkit.help.SimpleHelpMap) this.server.getHelpMap()).initializeCommands();
++        this.server.getPluginManager().callEvent(new org.bukkit.event.server.ServerLoadEvent(org.bukkit.event.server.ServerLoadEvent.LoadType.STARTUP));
++        this.connection.acceptConnections();
++    }
++
++    public void initWorld(ServerLevel serverLevel, ServerLevelData serverLevelData, WorldData saveData, WorldOptions worldOptions) {
++        boolean isDebugWorld = saveData.isDebugWorld();
++        if (serverLevel.generator != null) {
++            serverLevel.getWorld().getPopulators().addAll(serverLevel.generator.getDefaultPopulators(serverLevel.getWorld()));
++        }
++        // CraftBukkit start
++        WorldBorder worldborder = serverLevel.getWorldBorder();
++        worldborder.applySettings(serverLevelData.getWorldBorder()); // CraftBukkit - move up so that WorldBorder is set during WorldInitEvent
++        this.server.getPluginManager().callEvent(new org.bukkit.event.world.WorldInitEvent(serverLevel.getWorld())); // CraftBukkit - SPIGOT-5569: Call WorldInitEvent before any chunks are generated
++
+         if (!serverLevelData.isInitialized()) {
+             try {
+                 setInitialSpawn(serverLevel, serverLevelData, worldOptions.generateBonusChest(), isDebugWorld);
+@@ -403,47 +_,30 @@
+ 
+             serverLevelData.setInitialized(true);
+         }
+-
+-        this.getPlayerList().addWorldborderListener(serverLevel);
+-        if (this.worldData.getCustomBossEvents() != null) {
+-            this.getCustomBossEvents().load(this.worldData.getCustomBossEvents(), this.registryAccess());
+-        }
+-
+-        RandomSequences randomSequences = serverLevel.getRandomSequences();
+-
+-        for (Entry<ResourceKey<LevelStem>, LevelStem> entry : registry.entrySet()) {
+-            ResourceKey<LevelStem> resourceKey = entry.getKey();
+-            if (resourceKey != LevelStem.OVERWORLD) {
+-                ResourceKey<Level> resourceKey1 = ResourceKey.create(Registries.DIMENSION, resourceKey.location());
+-                DerivedLevelData derivedLevelData = new DerivedLevelData(this.worldData, serverLevelData);
+-                ServerLevel serverLevel1 = new ServerLevel(
+-                    this,
+-                    this.executor,
+-                    this.storageSource,
+-                    derivedLevelData,
+-                    resourceKey1,
+-                    entry.getValue(),
+-                    listener,
+-                    isDebugWorld,
+-                    l,
+-                    ImmutableList.of(),
+-                    false,
+-                    randomSequences
+-                );
+-                worldBorder.addListener(new BorderChangeListener.DelegateBorderChangeListener(serverLevel1.getWorldBorder()));
+-                this.levels.put(resourceKey1, serverLevel1);
+-            }
+-        }
+-
+-        worldBorder.applySettings(serverLevelData.getWorldBorder());
+     }
++    // CraftBukkit end
+ 
+     private static void setInitialSpawn(ServerLevel level, ServerLevelData levelData, boolean generateBonusChest, boolean debug) {
+         if (debug) {
+             levelData.setSpawn(BlockPos.ZERO.above(80), 0.0F);
+         } else {
+             ServerChunkCache chunkSource = level.getChunkSource();
+-            ChunkPos chunkPos = new ChunkPos(chunkSource.randomState().sampler().findSpawnPosition());
++            // CraftBukkit start
++            if (level.generator != null) {
++                java.util.Random rand = new java.util.Random(level.getSeed());
++                org.bukkit.Location spawn = level.generator.getFixedSpawnLocation(level.getWorld(), rand);
++
++                if (spawn != null) {
++                    if (spawn.getWorld() != level.getWorld()) {
++                        throw new IllegalStateException("Cannot set spawn point for " + levelData.getLevelName() + " to be in another world (" + spawn.getWorld().getName() + ")");
++                    } else {
++                        levelData.setSpawn(new BlockPos(spawn.getBlockX(), spawn.getBlockY(), spawn.getBlockZ()), spawn.getYaw());
++                        return;
++                    }
++                }
++            }
++            // CraftBukkit end
++            ChunkPos chunkPos = new ChunkPos(chunkSource.randomState().sampler().findSpawnPosition()); // Paper - Only attempt to find spawn position if there isn't a fixed spawn position set
+             int spawnHeight = chunkSource.getGenerator().getSpawnHeight(level);
+             if (spawnHeight < level.getMinY()) {
+                 BlockPos worldPosition = chunkPos.getWorldPosition();
+@@ -495,26 +_,31 @@
+         serverLevelData.setGameType(GameType.SPECTATOR);
+     }
+ 
+-    public void prepareLevels(ChunkProgressListener listener) {
+-        ServerLevel serverLevel = this.overworld();
++    // CraftBukkit start
++    public void prepareLevels(ChunkProgressListener listener, ServerLevel serverLevel) {
++        this.forceTicks = true;
++        // CraftBukkit end
+         LOGGER.info("Preparing start region for dimension {}", serverLevel.dimension().location());
+         BlockPos sharedSpawnPos = serverLevel.getSharedSpawnPos();
+         listener.updateSpawnPos(new ChunkPos(sharedSpawnPos));
+         ServerChunkCache chunkSource = serverLevel.getChunkSource();
+         this.nextTickTimeNanos = Util.getNanos();
+         serverLevel.setDefaultSpawnPos(sharedSpawnPos, serverLevel.getSharedSpawnAngle());
+-        int _int = this.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS);
++        int _int = serverLevel.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS); // CraftBukkit - per-world
+         int i = _int > 0 ? Mth.square(ChunkProgressListener.calculateDiameter(_int)) : 0;
+ 
+         while (chunkSource.getTickingGenerated() < i) {
+-            this.nextTickTimeNanos = Util.getNanos() + PREPARE_LEVELS_DEFAULT_DELAY_NANOS;
+-            this.waitUntilNextTick();
++            // CraftBukkit start
++            // this.nextTickTimeNanos = Util.getNanos() + PREPARE_LEVELS_DEFAULT_DELAY_NANOS;
++            this.executeModerately();
+         }
+ 
+-        this.nextTickTimeNanos = Util.getNanos() + PREPARE_LEVELS_DEFAULT_DELAY_NANOS;
+-        this.waitUntilNextTick();
++        // this.nextTickTimeNanos = Util.getNanos() + PREPARE_LEVELS_DEFAULT_DELAY_NANOS;
++        this.executeModerately();
+ 
+-        for (ServerLevel serverLevel1 : this.levels.values()) {
++        if (true) {
++            ServerLevel serverLevel1 = serverLevel;
++            // CraftBukkit end
+             ForcedChunksSavedData forcedChunksSavedData = serverLevel1.getDataStorage().get(ForcedChunksSavedData.factory(), "chunks");
+             if (forcedChunksSavedData != null) {
+                 LongIterator longIterator = forcedChunksSavedData.getChunks().iterator();
+@@ -527,10 +_,17 @@
+             }
+         }
+ 
+-        this.nextTickTimeNanos = Util.getNanos() + PREPARE_LEVELS_DEFAULT_DELAY_NANOS;
+-        this.waitUntilNextTick();
++        // CraftBukkit start
++        // this.nextTickTimeNanos = SystemUtils.getNanos() + MinecraftServer.PREPARE_LEVELS_DEFAULT_DELAY_NANOS;
++        this.executeModerately();
++        // CraftBukkit end
+         listener.stop();
+-        this.updateMobSpawningFlags();
++        // CraftBukkit start
++        // this.updateMobSpawningFlags();
++        serverLevel.setSpawnSettings(serverLevel.serverLevelData.getDifficulty() != Difficulty.PEACEFUL && ((net.minecraft.server.dedicated.DedicatedServer) this).settings.getProperties().spawnMonsters); // Paper - per level difficulty (from setDifficulty(ServerLevel, Difficulty, boolean))
++
++        this.forceTicks = false;
++        // CraftBukkit end
+     }
+ 
+     public GameType getDefaultGameType() {
+@@ -559,11 +_,14 @@
+             flag = true;
+         }
+ 
++        /* // CraftBukkit start - moved to WorldServer.save
+         ServerLevel serverLevel1 = this.overworld();
+         ServerLevelData serverLevelData = this.worldData.overworldData();
+         serverLevelData.setWorldBorder(serverLevel1.getWorldBorder().createSettings());
+         this.worldData.setCustomBossEvents(this.getCustomBossEvents().save(this.registryAccess()));
+         this.storageSource.saveDataTag(this.registryAccess(), this.worldData, this.getPlayerList().getSingleplayerData());
++         */
++        // CraftBukkit end
+         if (flush) {
+             for (ServerLevel serverLevel2 : this.getAllLevels()) {
+                 LOGGER.info("ThreadedAnvilChunkStorage ({}): All chunks are saved", serverLevel2.getChunkSource().chunkMap.getStorageName());
+@@ -593,18 +_,48 @@
+         this.stopServer();
+     }
+ 
++    // CraftBukkit start
++    private boolean hasStopped = false;
++    private boolean hasLoggedStop = false; // Paper - Debugging
++    private final Object stopLock = new Object();
++    public final boolean hasStopped() {
++        synchronized (this.stopLock) {
++            return this.hasStopped;
++        }
++    }
++    // CraftBukkit end
++
+     public void stopServer() {
++        // CraftBukkit start - prevent double stopping on multiple threads
++        synchronized(this.stopLock) {
++            if (this.hasStopped) return;
++            this.hasStopped = true;
++        }
++        if (!hasLoggedStop && isDebugging()) io.papermc.paper.util.TraceUtil.dumpTraceForThread("Server stopped"); // Paper - Debugging
++        shutdownThread = Thread.currentThread(); // Paper - Improved watchdog support
++        org.spigotmc.WatchdogThread.doStop(); // Paper - Improved watchdog support
++        // CraftBukkit end
+         if (this.metricsRecorder.isRecording()) {
+             this.cancelRecordingMetrics();
+         }
+ 
+         LOGGER.info("Stopping server");
++        Commands.COMMAND_SENDING_POOL.shutdownNow(); // Paper - Perf: Async command map building; Shutdown and don't bother finishing
++        // CraftBukkit start
++        if (this.server != null) {
++            this.server.spark.disable(); // Paper - spark
++            this.server.disablePlugins();
++            this.server.waitForAsyncTasksShutdown(); // Paper - Wait for Async Tasks during shutdown
++        }
++        // CraftBukkit end
++        if (io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper != null) io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper.shutdown(); // Paper - Plugin remapping
+         this.getConnection().stop();
+         this.isSaving = true;
+         if (this.playerList != null) {
+             LOGGER.info("Saving players");
+             this.playerList.saveAll();
+-            this.playerList.removeAll();
++            this.playerList.removeAll(this.isRestarting); // Paper
++            try { Thread.sleep(100); } catch (InterruptedException ex) {} // CraftBukkit - SPIGOT-625 - give server at least a chance to send packets
+         }
+ 
+         LOGGER.info("Saving worlds");
+@@ -646,6 +_,25 @@
+         } catch (IOException var4) {
+             LOGGER.error("Failed to unlock level {}", this.storageSource.getLevelId(), var4);
+         }
++        // Spigot start
++        io.papermc.paper.util.MCUtil.ASYNC_EXECUTOR.shutdown(); // Paper
++        try {
++            io.papermc.paper.util.MCUtil.ASYNC_EXECUTOR.awaitTermination(30, java.util.concurrent.TimeUnit.SECONDS); // Paper
++        } catch (java.lang.InterruptedException ignored) {} // Paper
++        if (org.spigotmc.SpigotConfig.saveUserCacheOnStopOnly) {
++            LOGGER.info("Saving usercache.json");
++            this.getProfileCache().save(false); // Paper - Perf: Async GameProfileCache saving
++        }
++        // Spigot end
++        // Paper start - Improved watchdog support - move final shutdown items here
++        Util.shutdownExecutors();
++        try {
++            net.minecrell.terminalconsole.TerminalConsoleAppender.close(); // Paper - Use TerminalConsoleAppender
++        } catch (final Exception ignored) {
++        }
++        io.papermc.paper.log.CustomLogManager.forceReset(); // Paper - Reset loggers after shutdown
++        this.onServerExit();
++        // Paper end - Improved watchdog support - move final shutdown items here
+     }
+ 
+     public String getLocalIp() {
+@@ -661,6 +_,14 @@
+     }
+ 
+     public void halt(boolean waitForServer) {
++        // Paper start - allow passing of the intent to restart
++        this.safeShutdown(waitForServer, false);
++    }
++    public void safeShutdown(boolean waitForServer, boolean isRestarting) {
++        this.isRestarting = isRestarting;
++        this.hasLoggedStop = true; // Paper - Debugging
++        if (isDebugging()) io.papermc.paper.util.TraceUtil.dumpTraceForThread("Server stopped"); // Paper - Debugging
++        // Paper end
+         this.running = false;
+         if (waitForServer) {
+             try {
+@@ -671,6 +_,57 @@
+         }
+     }
+ 
++    // Paper start - Further improve server tick loop
++    private static final long SEC_IN_NANO = 1000000000;
++    private static final long MAX_CATCHUP_BUFFER = TICK_TIME * TPS * 60L;
++    private long lastTick = 0;
++    private long catchupTime = 0;
++    public final RollingAverage tps1 = new RollingAverage(60);
++    public final RollingAverage tps5 = new RollingAverage(60 * 5);
++    public final RollingAverage tps15 = new RollingAverage(60 * 15);
++
++    public static class RollingAverage {
++        private final int size;
++        private long time;
++        private java.math.BigDecimal total;
++        private int index = 0;
++        private final java.math.BigDecimal[] samples;
++        private final long[] times;
++
++        RollingAverage(int size) {
++            this.size = size;
++            this.time = size * SEC_IN_NANO;
++            this.total = dec(TPS).multiply(dec(SEC_IN_NANO)).multiply(dec(size));
++            this.samples = new java.math.BigDecimal[size];
++            this.times = new long[size];
++            for (int i = 0; i < size; i++) {
++                this.samples[i] = dec(TPS);
++                this.times[i] = SEC_IN_NANO;
++            }
++        }
++
++        private static java.math.BigDecimal dec(long t) {
++            return new java.math.BigDecimal(t);
++        }
++        public void add(java.math.BigDecimal x, long t) {
++            time -= times[index];
++            total = total.subtract(samples[index].multiply(dec(times[index])));
++            samples[index] = x;
++            times[index] = t;
++            time += t;
++            total = total.add(x.multiply(dec(t)));
++            if (++index == size) {
++                index = 0;
++            }
++        }
++
++        public double getAverage() {
++            return total.divide(dec(time), 30, java.math.RoundingMode.HALF_UP).doubleValue();
++        }
++    }
++    private static final java.math.BigDecimal TPS_BASE = new java.math.BigDecimal(1E9).multiply(new java.math.BigDecimal(SAMPLE_INTERVAL));
++    // Paper end
++
+     protected void runServer() {
+         try {
+             if (!this.initServer()) {
+@@ -681,6 +_,35 @@
+             this.statusIcon = this.loadStatusIcon().orElse(null);
+             this.status = this.buildServerStatus();
+ 
++            this.server.spark.enableBeforePlugins(); // Paper - spark
++            // Spigot start
++            // Paper start
++            LOGGER.info("Running delayed init tasks");
++            this.server.getScheduler().mainThreadHeartbeat(); // run all 1 tick delay tasks during init,
++            // this is going to be the first thing the tick process does anyways, so move done and run it after
++            // everything is init before watchdog tick.
++            // anything at 3+ won't be caught here but also will trip watchdog....
++            // tasks are default scheduled at -1 + delay, and first tick will tick at 1
++            final long actualDoneTimeMs = System.currentTimeMillis() - org.bukkit.craftbukkit.Main.BOOT_TIME.toEpochMilli(); // Paper - Improve startup message
++            LOGGER.info("Done ({})! For help, type \"help\"", String.format(java.util.Locale.ROOT, "%.3fs", actualDoneTimeMs / 1000.00D)); // Paper - Improve startup message
++            org.spigotmc.WatchdogThread.tick();
++            // Paper end
++            org.spigotmc.WatchdogThread.hasStarted = true; // Paper
++            Arrays.fill(this.recentTps, 20);
++            // Paper start - further improve server tick loop
++            long tickSection = Util.getNanos();
++            long currentTime;
++            // Paper end - further improve server tick loop
++            // Paper start - Add onboarding message for initial server start
++            if (io.papermc.paper.configuration.GlobalConfiguration.isFirstStart) {
++                LOGGER.info("*************************************************************************************");
++                LOGGER.info("This is the first time you're starting this server.");
++                LOGGER.info("It's recommended you read our 'Getting Started' documentation for guidance.");
++                LOGGER.info("View this and more helpful information here: https://docs.papermc.io/paper/next-steps");
++                LOGGER.info("*************************************************************************************");
++            }
++            // Paper end - Add onboarding message for initial server start
++
+             while (this.running) {
+                 long l;
+                 if (!this.isPaused() && this.tickRateManager.isSprinting() && this.tickRateManager.checkShouldSprintThisTick()) {
+@@ -693,11 +_,30 @@
+                     if (l1 > OVERLOADED_THRESHOLD_NANOS + 20L * l
+                         && this.nextTickTimeNanos - this.lastOverloadWarningNanos >= OVERLOADED_WARNING_INTERVAL_NANOS + 100L * l) {
+                         long l2 = l1 / l;
++                        if (this.server.getWarnOnOverload()) // CraftBukkit
+                         LOGGER.warn("Can't keep up! Is the server overloaded? Running {}ms or {} ticks behind", l1 / TimeUtil.NANOSECONDS_PER_MILLISECOND, l2);
+                         this.nextTickTimeNanos += l2 * l;
+                         this.lastOverloadWarningNanos = this.nextTickTimeNanos;
+                     }
+                 }
++                // Spigot start
++                // Paper start - further improve server tick loop
++                currentTime = Util.getNanos();
++                if (++MinecraftServer.currentTick % MinecraftServer.SAMPLE_INTERVAL == 0) {
++                    final long diff = currentTime - tickSection;
++                    final java.math.BigDecimal currentTps = TPS_BASE.divide(new java.math.BigDecimal(diff), 30, java.math.RoundingMode.HALF_UP);
++                    tps1.add(currentTps, diff);
++                    tps5.add(currentTps, diff);
++                    tps15.add(currentTps, diff);
++
++                    // Backwards compat with bad plugins
++                    this.recentTps[0] = tps1.getAverage();
++                    this.recentTps[1] = tps5.getAverage();
++                    this.recentTps[2] = tps15.getAverage();
++                    tickSection = currentTime;
++                }
++                // Paper end - further improve server tick loop
++                // Spigot end
+ 
+                 boolean flag = l == 0L;
+                 if (this.debugCommandProfilerDelayStart) {
+@@ -705,6 +_,8 @@
+                     this.debugCommandProfiler = new MinecraftServer.TimeProfiler(Util.getNanos(), this.tickCount);
+                 }
+ 
++                //MinecraftServer.currentTick = (int) (System.currentTimeMillis() / 50); // CraftBukkit // Paper - don't overwrite current tick time
++                lastTick = currentTime;
+                 this.nextTickTimeNanos += l;
+ 
+                 try (Profiler.Scope scope = Profiler.use(this.createProfiler())) {
+@@ -755,7 +_,7 @@
+                     this.services.profileCache().clearExecutor();
+                 }
+ 
+-                this.onServerExit();
++                //this.onServerExit(); // Paper - Improved watchdog support; moved into stop
+             }
+         }
+     }
+@@ -807,7 +_,14 @@
+     }
+ 
+     private boolean haveTime() {
+-        return this.runningTask() || Util.getNanos() < (this.mayHaveDelayedTasks ? this.delayedTasksMaxNextTickTimeNanos : this.nextTickTimeNanos);
++        // CraftBukkit start
++        return this.forceTicks || this.runningTask() || Util.getNanos() < (this.mayHaveDelayedTasks ? this.delayedTasksMaxNextTickTimeNanos : this.nextTickTimeNanos);
++    }
++
++    private void executeModerately() {
++        this.runAllTasks();
++        java.util.concurrent.locks.LockSupport.parkNanos("executing tasks", 1000L);
++        // CraftBukkit end
+     }
+ 
+     public static boolean throwIfFatalException() {
+@@ -852,6 +_,12 @@
+ 
+     @Override
+     public TickTask wrapRunnable(Runnable runnable) {
++        // Paper start - anything that does try to post to main during watchdog crash, run on watchdog
++        if (this.hasStopped && Thread.currentThread().equals(shutdownThread)) {
++            runnable.run();
++            runnable = () -> {};
++        }
++        // Paper end
+         return new TickTask(this.tickCount, runnable);
+     }
+ 
+@@ -871,15 +_,16 @@
+         if (super.pollTask()) {
+             return true;
+         } else {
++            boolean ret = false; // Paper - force execution of all worlds, do not just bias the first
+             if (this.tickRateManager.isSprinting() || this.haveTime()) {
+                 for (ServerLevel serverLevel : this.getAllLevels()) {
+                     if (serverLevel.getChunkSource().pollTask()) {
+-                        return true;
++                        ret = true; // Paper - force execution of all worlds, do not just bias the first
+                     }
+                 }
+             }
+ 
+-            return false;
++            return ret; // Paper - force execution of all worlds, do not just bias the first
+         }
+     }
+ 
+@@ -927,26 +_,44 @@
+     }
+ 
+     public void tickServer(BooleanSupplier hasTimeLeft) {
++        org.spigotmc.WatchdogThread.tick(); // Spigot
+         long nanos = Util.getNanos();
+         int i = this.pauseWhileEmptySeconds() * 20;
++        this.removeDisabledPluginsBlockingSleep(); // Paper - API to allow/disallow tick sleeping
+         if (i > 0) {
+-            if (this.playerList.getPlayerCount() == 0 && !this.tickRateManager.isSprinting()) {
++            if (this.playerList.getPlayerCount() == 0 && !this.tickRateManager.isSprinting() && this.pluginsBlockingSleep.isEmpty()) { // Paper - API to allow/disallow tick sleeping
+                 this.emptyTicks++;
+             } else {
+                 this.emptyTicks = 0;
+             }
+ 
+             if (this.emptyTicks >= i) {
++                this.server.spark.tickStart(); // Paper - spark
+                 if (this.emptyTicks == i) {
+                     LOGGER.info("Server empty for {} seconds, pausing", this.pauseWhileEmptySeconds());
+                     this.autoSave();
+                 }
+ 
++                this.server.getScheduler().mainThreadHeartbeat(); // CraftBukkit
++                // Paper start - avoid issues with certain tasks not processing during sleep
++                Runnable task;
++                while ((task = this.processQueue.poll()) != null) {
++                    task.run();
++                }
++                for (final ServerLevel level : this.levels.values()) {
++                    // process unloads
++                    level.getChunkSource().tick(() -> true, false);
++                }
++                // Paper end - avoid issues with certain tasks not processing during sleep
++                this.server.spark.executeMainThreadTasks(); // Paper - spark
+                 this.tickConnection();
++                this.server.spark.tickEnd(((double)(System.nanoTime() - lastTick) / 1000000D)); // Paper - spark
+                 return;
+             }
+         }
+ 
++        this.server.spark.tickStart(); // Paper - spark
++        new com.destroystokyo.paper.event.server.ServerTickStartEvent(this.tickCount+1).callEvent(); // Paper - Server Tick Events
+         this.tickCount++;
+         this.tickRateManager.tick();
+         this.tickChildren(hasTimeLeft);
+@@ -956,11 +_,19 @@
+         }
+ 
+         this.ticksUntilAutosave--;
+-        if (this.ticksUntilAutosave <= 0) {
++        if (this.autosavePeriod > 0 && this.ticksUntilAutosave <= 0) { // CraftBukkit
+             this.autoSave();
+         }
+ 
+         ProfilerFiller profilerFiller = Profiler.get();
++        this.runAllTasks(); // Paper - move runAllTasks() into full server tick (previously for timings)
++        this.server.spark.executeMainThreadTasks(); // Paper - spark
++        // Paper start - Server Tick Events
++        long endTime = System.nanoTime();
++        long remaining = (TICK_TIME - (endTime - lastTick)) - catchupTime;
++        new com.destroystokyo.paper.event.server.ServerTickEndEvent(this.tickCount, ((double)(endTime - lastTick) / 1000000D), remaining).callEvent();
++        // Paper end - Server Tick Events
++        this.server.spark.tickEnd(((double)(endTime - lastTick) / 1000000D)); // Paper - spark
+         profilerFiller.push("tallying");
+         long l = Util.getNanos() - nanos;
+         int i1 = this.tickCount % 100;
+@@ -968,12 +_,17 @@
+         this.aggregatedTickTimesNanos += l;
+         this.tickTimesNanos[i1] = l;
+         this.smoothedTickTimeMillis = this.smoothedTickTimeMillis * 0.8F + (float)l / (float)TimeUtil.NANOSECONDS_PER_MILLISECOND * 0.19999999F;
++        // Paper start - Add tick times API and /mspt command
++        this.tickTimes5s.add(this.tickCount, l);
++        this.tickTimes10s.add(this.tickCount, l);
++        this.tickTimes60s.add(this.tickCount, l);
++        // Paper end - Add tick times API and /mspt command
+         this.logTickMethodTime(nanos);
+         profilerFiller.pop();
+     }
+ 
+     private void autoSave() {
+-        this.ticksUntilAutosave = this.computeNextAutosaveInterval();
++        this.ticksUntilAutosave = this.autosavePeriod; // CraftBukkit
+         LOGGER.debug("Autosave started");
+         ProfilerFiller profilerFiller = Profiler.get();
+         profilerFiller.push("save");
+@@ -1015,7 +_,7 @@
+     private ServerStatus buildServerStatus() {
+         ServerStatus.Players players = this.buildPlayerStatus();
+         return new ServerStatus(
+-            Component.nullToEmpty(this.motd),
++            io.papermc.paper.adventure.PaperAdventure.asVanilla(this.motd), // Paper - Adventure
+             Optional.of(players),
+             Optional.of(ServerStatus.Version.current()),
+             Optional.ofNullable(this.statusIcon),
+@@ -1029,7 +_,7 @@
+         if (this.hidesOnlinePlayers()) {
+             return new ServerStatus.Players(maxPlayers, players.size(), List.of());
+         } else {
+-            int min = Math.min(players.size(), 12);
++            int min = Math.min(players.size(), org.spigotmc.SpigotConfig.playerSample); // Paper - PaperServerListPingEvent
+             ObjectArrayList<GameProfile> list = new ObjectArrayList<>(min);
+             int randomInt = Mth.nextInt(this.random, 0, players.size() - min);
+ 
+@@ -1046,17 +_,65 @@
+     protected void tickChildren(BooleanSupplier hasTimeLeft) {
+         ProfilerFiller profilerFiller = Profiler.get();
+         this.getPlayerList().getPlayers().forEach(serverPlayer1 -> serverPlayer1.connection.suspendFlushing());
++        this.server.getScheduler().mainThreadHeartbeat(); // CraftBukkit
++        // Paper start - Folia scheduler API
++        ((io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler) org.bukkit.Bukkit.getGlobalRegionScheduler()).tick();
++        getAllLevels().forEach(level -> {
++            for (final net.minecraft.world.entity.Entity entity : level.getEntities().getAll()) {
++                if (entity.isRemoved()) {
++                    continue;
++                }
++                final org.bukkit.craftbukkit.entity.CraftEntity bukkit = entity.getBukkitEntityRaw();
++                if (bukkit != null) {
++                    bukkit.taskScheduler.executeTick();
++                }
++            }
++        });
++        // Paper end - Folia scheduler API
++        io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue(this.tickCount); // Paper
+         profilerFiller.push("commandFunctions");
+         this.getFunctions().tick();
+         profilerFiller.popPush("levels");
+ 
++        // CraftBukkit start
++        // Run tasks that are waiting on processing
++        while (!this.processQueue.isEmpty()) {
++            this.processQueue.remove().run();
++        }
++
++        // Send time updates to everyone, it will get the right time from the world the player is in.
++        // Paper start - Perf: Optimize time updates
++        for (final ServerLevel level : this.getAllLevels()) {
++            final boolean doDaylight = level.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT);
++            final long dayTime = level.getDayTime();
++            long worldTime = level.getGameTime();
++            final ClientboundSetTimePacket worldPacket = new ClientboundSetTimePacket(worldTime, dayTime, doDaylight);
++            for (Player entityhuman : level.players()) {
++                if (!(entityhuman instanceof ServerPlayer) || (tickCount + entityhuman.getId()) % 20 != 0) {
++                    continue;
++                }
++                ServerPlayer entityplayer = (ServerPlayer) entityhuman;
++                long playerTime = entityplayer.getPlayerTime();
++                ClientboundSetTimePacket packet = (playerTime == dayTime) ? worldPacket :
++                    new ClientboundSetTimePacket(worldTime, playerTime, doDaylight);
++                entityplayer.connection.send(packet); // Add support for per player time
++                // Paper end - Perf: Optimize time updates
++            }
++        }
++
++        this.isIteratingOverLevels = true; // Paper - Throw exception on world create while being ticked
+         for (ServerLevel serverLevel : this.getAllLevels()) {
++            serverLevel.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - BlockPhysicsEvent
++            serverLevel.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent
++            serverLevel.updateLagCompensationTick(); // Paper - lag compensation
+             profilerFiller.push(() -> serverLevel + " " + serverLevel.dimension().location());
++            /* Drop global time updates
+             if (this.tickCount % 20 == 0) {
+                 profilerFiller.push("timeSync");
+                 this.synchronizeTime(serverLevel);
+                 profilerFiller.pop();
+             }
++            // CraftBukkit end */
+ 
+             profilerFiller.push("tick");
+ 
+@@ -1070,7 +_,9 @@
+ 
+             profilerFiller.pop();
+             profilerFiller.pop();
++            serverLevel.explosionDensityCache.clear(); // Paper - Optimize explosions
+         }
++        this.isIteratingOverLevels = false; // Paper - Throw exception on world create while being ticked
+ 
+         profilerFiller.popPush("connection");
+         this.tickConnection();
+@@ -1148,6 +_,22 @@
+         return this.levels.get(dimension);
+     }
+ 
++    // CraftBukkit start
++    public void addLevel(ServerLevel level) {
++        Map<ResourceKey<Level>, ServerLevel> oldLevels = this.levels;
++        Map<ResourceKey<Level>, ServerLevel> newLevels = Maps.newLinkedHashMap(oldLevels);
++        newLevels.put(level.dimension(), level);
++        this.levels = Collections.unmodifiableMap(newLevels);
++    }
++
++    public void removeLevel(ServerLevel level) {
++        Map<ResourceKey<Level>, ServerLevel> oldLevels = this.levels;
++        Map<ResourceKey<Level>, ServerLevel> newLevels = Maps.newLinkedHashMap(oldLevels);
++        newLevels.remove(level.dimension());
++        this.levels = Collections.unmodifiableMap(newLevels);
++    }
++    // CraftBukkit end
++
+     public Set<ResourceKey<Level>> levelKeys() {
+         return this.levels.keySet();
+     }
+@@ -1177,7 +_,7 @@
+ 
+     @DontObfuscate
+     public String getServerModName() {
+-        return "vanilla";
++        return io.papermc.paper.ServerBuildInfo.buildInfo().brandName(); // Paper
+     }
+ 
+     public SystemReport fillSystemReport(SystemReport systemReport) {
+@@ -1212,7 +_,7 @@
+ 
+     @Override
+     public void sendSystemMessage(Component component) {
+-        LOGGER.info(component.getString());
++        LOGGER.info(io.papermc.paper.adventure.PaperAdventure.ANSI_SERIALIZER.serialize(io.papermc.paper.adventure.PaperAdventure.asAdventure(component))); // Paper - Log message with colors
+     }
+ 
+     public KeyPair getKeyPair() {
+@@ -1250,11 +_,14 @@
+         }
+     }
+ 
+-    public void setDifficulty(Difficulty difficulty, boolean forced) {
+-        if (forced || !this.worldData.isDifficultyLocked()) {
+-            this.worldData.setDifficulty(this.worldData.isHardcore() ? Difficulty.HARD : difficulty);
+-            this.updateMobSpawningFlags();
+-            this.getPlayerList().getPlayers().forEach(this::sendDifficultyUpdate);
++    // Paper start - per level difficulty
++    public void setDifficulty(ServerLevel level, Difficulty difficulty, boolean forceUpdate) {
++        net.minecraft.world.level.storage.PrimaryLevelData worldData = (net.minecraft.world.level.storage.PrimaryLevelData) level.serverLevelData;
++        if (forceUpdate || !worldData.isDifficultyLocked()) {
++            worldData.setDifficulty(worldData.isHardcore() ? Difficulty.HARD : difficulty);
++            level.setSpawnSettings(worldData.getDifficulty() != Difficulty.PEACEFUL && ((net.minecraft.server.dedicated.DedicatedServer) this).settings.getProperties().spawnMonsters);
++            // this.getPlayerList().getPlayers().forEach(this::sendDifficultyUpdate);
++            // Paper end - per level difficulty
+         }
+     }
+ 
+@@ -1264,7 +_,7 @@
+ 
+     private void updateMobSpawningFlags() {
+         for (ServerLevel serverLevel : this.getAllLevels()) {
+-            serverLevel.setSpawnSettings(this.isSpawningMonsters());
++            serverLevel.setSpawnSettings(serverLevel.serverLevelData.getDifficulty() != Difficulty.PEACEFUL && ((net.minecraft.server.dedicated.DedicatedServer) this).settings.getProperties().spawnMonsters); // Paper - per level difficulty (from setDifficulty(ServerLevel, Difficulty, boolean))
+         }
+     }
+ 
+@@ -1340,10 +_,20 @@
+ 
+     @Override
+     public String getMotd() {
+-        return this.motd;
++        return net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(this.motd); // Paper - Adventure
+     }
+ 
+     public void setMotd(String motd) {
++        // Paper start - Adventure
++        this.motd = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserializeOr(motd, net.kyori.adventure.text.Component.empty());
++    }
++
++    public net.kyori.adventure.text.Component motd() {
++        return this.motd;
++    }
++
++    public void motd(net.kyori.adventure.text.Component motd) {
++        // Paper end - Adventure
+         this.motd = motd;
+     }
+ 
+@@ -1366,7 +_,7 @@
+     }
+ 
+     public ServerConnectionListener getConnection() {
+-        return this.connection;
++        return this.connection == null ? this.connection = new ServerConnectionListener(this) : this.connection; // Spigot
+     }
+ 
+     public boolean isReady() {
+@@ -1452,7 +_,7 @@
+     @Override
+     public void executeIfPossible(Runnable task) {
+         if (this.isStopped()) {
+-            throw new RejectedExecutionException("Server already shutting down");
++            throw new io.papermc.paper.util.ServerStopRejectedExecutionException("Server already shutting down"); // Paper - do not prematurely disconnect players on stop
+         } else {
+             super.executeIfPossible(task);
+         }
+@@ -1491,7 +_,13 @@
+         return this.functionManager;
+     }
+ 
++    // Paper start - Add ServerResourcesReloadedEvent
++    @Deprecated @io.papermc.paper.annotation.DoNotUse
+     public CompletableFuture<Void> reloadResources(Collection<String> selectedIds) {
++        return this.reloadResources(selectedIds, io.papermc.paper.event.server.ServerResourcesReloadedEvent.Cause.PLUGIN);
++    }
++    public CompletableFuture<Void> reloadResources(Collection<String> selectedIds, io.papermc.paper.event.server.ServerResourcesReloadedEvent.Cause cause) {
++        // Paper end - Add ServerResourcesReloadedEvent
+         CompletableFuture<Void> completableFuture = CompletableFuture.<ImmutableList>supplyAsync(
+                 () -> selectedIds.stream().map(this.packRepository::getPack).filter(Objects::nonNull).map(Pack::open).collect(ImmutableList.toImmutableList()),
+                 this
+@@ -1499,7 +_,7 @@
+             .thenCompose(
+                 list -> {
+                     CloseableResourceManager closeableResourceManager = new MultiPackResourceManager(PackType.SERVER_DATA, list);
+-                    List<Registry.PendingTags<?>> list1 = TagLoader.loadTagsForExistingRegistries(closeableResourceManager, this.registries.compositeAccess());
++                    List<Registry.PendingTags<?>> list1 = TagLoader.loadTagsForExistingRegistries(closeableResourceManager, this.registries.compositeAccess(), io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent.Cause.RELOAD); // Paper - tag lifecycle - add cause
+                     return ReloadableServerResources.loadResources(
+                             closeableResourceManager,
+                             this.registries,
+@@ -1520,6 +_,7 @@
+             )
+             .thenAcceptAsync(
+                 reloadableResources -> {
++                    io.papermc.paper.command.brigadier.PaperBrigadier.moveBukkitCommands(this.resources.managers().getCommands(), reloadableResources.managers().commands); // Paper
+                     this.resources.close();
+                     this.resources = reloadableResources;
+                     this.packRepository.setSelected(selectedIds);
+@@ -1529,11 +_,29 @@
+                     this.worldData.setDataConfiguration(worldDataConfiguration);
+                     this.resources.managers.updateStaticRegistryTags();
+                     this.resources.managers.getRecipeManager().finalizeRecipeLoading(this.worldData.enabledFeatures());
+-                    this.getPlayerList().saveAll();
++                    this.potionBrewing = this.potionBrewing.reload(this.worldData.enabledFeatures()); // Paper - Custom Potion Mixes
++                    if (Thread.currentThread() != this.serverThread) return; // Paper
++                    // Paper start - we don't need to save everything, just advancements
++                    // this.getPlayerList().saveAll();
++                    for (final ServerPlayer player : this.getPlayerList().getPlayers()) {
++                        player.getAdvancements().save();
++                    }
++                    // Paper end - we don't need to save everything, just advancements
+                     this.getPlayerList().reloadResources();
+                     this.functionManager.replaceLibrary(this.resources.managers.getFunctionLibrary());
+                     this.structureTemplateManager.onResourceManagerReload(this.resources.resourceManager);
+                     this.fuelValues = FuelValues.vanillaBurnTimes(this.registries.compositeAccess(), this.worldData.enabledFeatures());
++                    org.bukkit.craftbukkit.block.data.CraftBlockData.reloadCache(); // Paper - cache block data strings; they can be defined by datapacks so refresh it here
++                    // Paper start - brigadier command API
++                    io.papermc.paper.command.brigadier.PaperCommands.INSTANCE.setValid(); // reset invalid state for event fire below
++                    io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner.INSTANCE.callReloadableRegistrarEvent(io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents.COMMANDS, io.papermc.paper.command.brigadier.PaperCommands.INSTANCE, org.bukkit.plugin.Plugin.class, io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent.Cause.RELOAD); // call commands event for regular plugins
++                    final org.bukkit.craftbukkit.help.SimpleHelpMap helpMap = (org.bukkit.craftbukkit.help.SimpleHelpMap) this.server.getHelpMap();
++                    helpMap.clear();
++                    helpMap.initializeGeneralTopics();
++                    helpMap.initializeCommands();
++                    this.server.syncCommands(); // Refresh commands after event
++                    // Paper end
++                    new io.papermc.paper.event.server.ServerResourcesReloadedEvent(cause).callEvent(); // Paper - Add ServerResourcesReloadedEvent; fire after everything has been reloaded
+                 },
+                 this
+             );
+@@ -1652,10 +_,11 @@
+         if (this.isEnforceWhitelist()) {
+             PlayerList playerList = commandSource.getServer().getPlayerList();
+             UserWhiteList whiteList = playerList.getWhiteList();
++            if (!((net.minecraft.server.dedicated.DedicatedServer) this).getProperties().whiteList.get()) return; // Paper - whitelist not enabled
+ 
+             for (ServerPlayer serverPlayer : Lists.newArrayList(playerList.getPlayers())) {
+-                if (!whiteList.isWhiteListed(serverPlayer.getGameProfile())) {
+-                    serverPlayer.connection.disconnect(Component.translatable("multiplayer.disconnect.not_whitelisted"));
++                if (!whiteList.isWhiteListed(serverPlayer.getGameProfile()) && !this.getPlayerList().isOp(serverPlayer.getGameProfile())) { // Paper - Fix kicking ops when whitelist is reloaded (MC-171420)
++                    serverPlayer.connection.disconnect(net.kyori.adventure.text.Component.text(org.spigotmc.SpigotConfig.whitelistMessage), org.bukkit.event.player.PlayerKickEvent.Cause.WHITELIST); // Paper - use configurable message & kick event cause
+                 }
+             }
+         }
+@@ -1859,6 +_,22 @@
+         }
+     }
+ 
++
++    // CraftBukkit start
++    public boolean isDebugging() {
++        return false;
++    }
++
++    public static MinecraftServer getServer() {
++        return SERVER; // Paper
++    }
++
++    @Deprecated
++    public static RegistryAccess getDefaultRegistryAccess() {
++        return org.bukkit.craftbukkit.CraftRegistry.getMinecraftRegistry();
++    }
++    // CraftBukkit end
++
+     private ProfilerFiller createProfiler() {
+         if (this.willStartRecordingMetrics) {
+             this.metricsRecorder = ActiveMetricsRecorder.createStarted(
+@@ -1980,16 +_,22 @@
+     }
+ 
+     public void logChatMessage(Component content, ChatType.Bound boundChatType, @Nullable String header) {
+-        String string = boundChatType.decorate(content).getString();
++        // Paper start
++        net.kyori.adventure.text.Component string = io.papermc.paper.adventure.PaperAdventure.asAdventure(boundChatType.decorate(content));
+         if (header != null) {
+-            LOGGER.info("[{}] {}", header, string);
++            COMPONENT_LOGGER.info("[{}] {}", header, string);
+         } else {
+-            LOGGER.info("{}", string);
++            COMPONENT_LOGGER.info("{}", string);
++            // Paper end
+         }
+     }
++
++    public final java.util.concurrent.ExecutorService chatExecutor = java.util.concurrent.Executors.newCachedThreadPool(
++        new com.google.common.util.concurrent.ThreadFactoryBuilder().setDaemon(true).setNameFormat("Async Chat Thread - #%d").setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(net.minecraft.server.MinecraftServer.LOGGER)).build()); // Paper
++    public final ChatDecorator improvedChatDecorator = new io.papermc.paper.adventure.ImprovedChatDecorator(this); // Paper - adventure
+ 
+     public ChatDecorator getChatDecorator() {
+-        return ChatDecorator.PLAIN;
++        return this.improvedChatDecorator; // Paper - support async chat decoration events
+     }
+ 
+     public boolean logIPs() {
+@@ -2122,4 +_,53 @@
+             };
+         }
+     }
++
++    // Paper start - Add tick times API and /mspt command
++    public static class TickTimes {
++        private final long[] times;
++
++        public TickTimes(int length) {
++            times = new long[length];
++        }
++
++        void add(int index, long time) {
++            times[index % times.length] = time;
++        }
++
++        public long[] getTimes() {
++            return times.clone();
++        }
++
++        public double getAverage() {
++            long total = 0L;
++            for (long value : times) {
++                total += value;
++            }
++            return ((double) total / (double) times.length) * 1.0E-6D;
++        }
++    }
++    // Paper end - Add tick times API and /mspt command
++
++    // Paper start - API to check if the server is sleeping
++    public boolean isTickPaused() {
++        return this.emptyTicks > 0 && this.emptyTicks >= this.pauseWhileEmptySeconds() * 20;
++    }
++
++    public void addPluginAllowingSleep(final String pluginName, final boolean value) {
++        if (!value) {
++            this.pluginsBlockingSleep.add(pluginName);
++        } else {
++            this.pluginsBlockingSleep.remove(pluginName);
++        }
++    }
++
++    private void removeDisabledPluginsBlockingSleep() {
++        if (this.pluginsBlockingSleep.isEmpty()) {
++            return;
++        }
++        this.pluginsBlockingSleep.removeIf(plugin -> (
++            !io.papermc.paper.plugin.manager.PaperPluginManagerImpl.getInstance().isPluginEnabled(plugin)
++        ));
++    }
++    // Paper end - API to check if the server is sleeping
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/PlayerAdvancements.java.patch b/paper-server/patches/sources/net/minecraft/server/PlayerAdvancements.java.patch
similarity index 62%
rename from paper-server/patches/unapplied/net/minecraft/server/PlayerAdvancements.java.patch
rename to paper-server/patches/sources/net/minecraft/server/PlayerAdvancements.java.patch
index 93d25149bd..0c15dfbc32 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/PlayerAdvancements.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/PlayerAdvancements.java.patch
@@ -1,52 +1,52 @@
 --- a/net/minecraft/server/PlayerAdvancements.java
 +++ b/net/minecraft/server/PlayerAdvancements.java
-@@ -50,7 +50,7 @@
- public class PlayerAdvancements {
+@@ -47,7 +_,7 @@
  
+ public class PlayerAdvancements {
      private static final Logger LOGGER = LogUtils.getLogger();
--    private static final Gson GSON = (new GsonBuilder()).setPrettyPrinting().create();
-+    private static final Gson GSON = (new GsonBuilder()).create(); // Paper - Remove pretty printing from advancements
+-    private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
++    private static final Gson GSON = new GsonBuilder().create(); // Paper - Remove pretty printing from advancements
      private final PlayerList playerList;
      private final Path playerSavePath;
      private AdvancementTree tree;
-@@ -63,6 +63,7 @@
+@@ -60,6 +_,7 @@
      private AdvancementHolder lastSelectedTab;
      private boolean isFirstPacket = true;
      private final Codec<PlayerAdvancements.Data> codec;
 +    public final Map<net.minecraft.advancements.critereon.SimpleCriterionTrigger<?>, Set<CriterionTrigger.Listener<?>>> criterionData = new java.util.IdentityHashMap<>(); // Paper - fix advancement data player leakage
  
-     public PlayerAdvancements(DataFixer dataFixer, PlayerList playerManager, ServerAdvancementManager advancementLoader, Path filePath, ServerPlayer owner) {
-         this.playerList = playerManager;
-@@ -162,6 +163,7 @@
+     public PlayerAdvancements(DataFixer dataFixer, PlayerList playerList, ServerAdvancementManager manager, Path playerSavePath, ServerPlayer player) {
+         this.playerList = playerList;
+@@ -128,6 +_,7 @@
      }
  
      public void save() {
 +        if (org.spigotmc.SpigotConfig.disableAdvancementSaving) return; // Spigot
-         JsonElement jsonelement = (JsonElement) this.codec.encodeStart(JsonOps.INSTANCE, this.asData()).getOrThrow();
+         JsonElement jsonElement = this.codec.encodeStart(JsonOps.INSTANCE, this.asData()).getOrThrow();
  
          try {
-@@ -196,6 +198,7 @@
-             AdvancementHolder advancementholder = loader.get(minecraftkey);
- 
-             if (advancementholder == null) {
-+                if (!minecraftkey.getNamespace().equals("minecraft")) return; // CraftBukkit
-                 PlayerAdvancements.LOGGER.warn("Ignored advancement '{}' in progress file {} - it doesn't exist anymore?", minecraftkey, this.playerSavePath);
+@@ -145,6 +_,7 @@
+         data.forEach((path, progress) -> {
+             AdvancementHolder advancementHolder = advancementManager.get(path);
+             if (advancementHolder == null) {
++                if (!path.getNamespace().equals(ResourceLocation.DEFAULT_NAMESPACE)) return; // CraftBukkit
+                 LOGGER.warn("Ignored advancement '{}' in progress file {} - it doesn't exist anymore?", path, this.playerSavePath);
              } else {
-                 this.startProgress(advancementholder, advancementprogress);
-@@ -223,14 +226,31 @@
-         boolean flag1 = advancementprogress.isDone();
- 
-         if (advancementprogress.grantProgress(criterionName)) {
+                 this.startProgress(advancementHolder, progress);
+@@ -169,14 +_,31 @@
+         AdvancementProgress orStartProgress = this.getOrStartProgress(advancement);
+         boolean isDone = orStartProgress.isDone();
+         if (orStartProgress.grantProgress(criterionKey)) {
 +            // Paper start - Add PlayerAdvancementCriterionGrantEvent
-+            if (!new com.destroystokyo.paper.event.player.PlayerAdvancementCriterionGrantEvent(this.player.getBukkitEntity(), advancement.toBukkit(), criterionName).callEvent()) {
-+                advancementprogress.revokeProgress(criterionName);
++            if (!new com.destroystokyo.paper.event.player.PlayerAdvancementCriterionGrantEvent(this.player.getBukkitEntity(), advancement.toBukkit(), criterionKey).callEvent()) {
++                orStartProgress.revokeProgress(criterionKey);
 +                return false;
 +            }
 +            // Paper end - Add PlayerAdvancementCriterionGrantEvent
              this.unregisterListeners(advancement);
              this.progressChanged.add(advancement);
              flag = true;
-             if (!flag1 && advancementprogress.isDone()) {
+             if (!isDone && orStartProgress.isDone()) {
 +                // Paper start - Add Adventure message to PlayerAdvancementDoneEvent
 +                final net.kyori.adventure.text.Component message = advancement.value().display().flatMap(info -> {
 +                    return java.util.Optional.ofNullable(
@@ -57,13 +57,13 @@
 +                this.player.level().getCraftServer().getPluginManager().callEvent(event); // CraftBukkit
 +                // Paper end
                  advancement.value().rewards().grant(this.player);
-                 advancement.value().display().ifPresent((advancementdisplay) -> {
--                    if (advancementdisplay.shouldAnnounceChat() && this.player.serverLevel().getGameRules().getBoolean(GameRules.RULE_ANNOUNCE_ADVANCEMENTS)) {
--                        this.playerList.broadcastSystemMessage(advancementdisplay.getType().createAnnouncement(advancement, this.player), false);
+                 advancement.value().display().ifPresent(displayInfo -> {
+-                    if (displayInfo.shouldAnnounceChat() && this.player.serverLevel().getGameRules().getBoolean(GameRules.RULE_ANNOUNCE_ADVANCEMENTS)) {
+-                        this.playerList.broadcastSystemMessage(displayInfo.getType().createAnnouncement(advancement, this.player), false);
 +                    // Paper start - Add Adventure message to PlayerAdvancementDoneEvent
 +                    if (event.message() != null && this.player.serverLevel().getGameRules().getBoolean(GameRules.RULE_ANNOUNCE_ADVANCEMENTS)) {
 +                        this.playerList.broadcastSystemMessage(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.message()), false);
 +                        // Paper end
                      }
- 
                  });
+             }
diff --git a/paper-server/patches/sources/net/minecraft/server/ReloadableServerRegistries.java.patch b/paper-server/patches/sources/net/minecraft/server/ReloadableServerRegistries.java.patch
new file mode 100644
index 0000000000..4276054d2f
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/ReloadableServerRegistries.java.patch
@@ -0,0 +1,38 @@
+--- a/net/minecraft/server/ReloadableServerRegistries.java
++++ b/net/minecraft/server/ReloadableServerRegistries.java
+@@ -48,8 +_,9 @@
+         List<HolderLookup.RegistryLookup<?>> list = TagLoader.buildUpdatedLookups(registryAccess.getAccessForLoading(RegistryLayer.RELOADABLE), postponedTags);
+         HolderLookup.Provider provider = HolderLookup.Provider.create(list.stream());
+         RegistryOps<JsonElement> registryOps = provider.createSerializationContext(JsonOps.INSTANCE);
++        final io.papermc.paper.registry.data.util.Conversions conversions = new io.papermc.paper.registry.data.util.Conversions(registryOps.lookupProvider); // Paper
+         List<CompletableFuture<WritableRegistry<?>>> list1 = LootDataType.values()
+-            .map(lootDataType -> scheduleRegistryLoad((LootDataType<?>)lootDataType, registryOps, resourceManager, backgroundExecutor))
++            .map(lootDataType -> scheduleRegistryLoad((LootDataType<?>)lootDataType, registryOps, resourceManager, backgroundExecutor, conversions)) // Paper
+             .toList();
+         CompletableFuture<List<WritableRegistry<?>>> completableFuture = Util.sequence(list1);
+         return completableFuture.thenApplyAsync(
+@@ -58,19 +_,20 @@
+     }
+ 
+     private static <T> CompletableFuture<WritableRegistry<?>> scheduleRegistryLoad(
+-        LootDataType<T> lootDataType, RegistryOps<JsonElement> ops, ResourceManager resourceManager, Executor backgroundExecutor
++        LootDataType<T> lootDataType, RegistryOps<JsonElement> ops, ResourceManager resourceManager, Executor backgroundExecutor, io.papermc.paper.registry.data.util.Conversions conversions // Paper
+     ) {
+         return CompletableFuture.supplyAsync(
+             () -> {
+                 WritableRegistry<T> writableRegistry = new MappedRegistry<>(lootDataType.registryKey(), Lifecycle.experimental());
++                io.papermc.paper.registry.PaperRegistryAccess.instance().registerReloadableRegistry(lootDataType.registryKey(), writableRegistry); // Paper - register reloadable registry
+                 Map<ResourceLocation, T> map = new HashMap<>();
+                 SimpleJsonResourceReloadListener.scanDirectory(resourceManager, lootDataType.registryKey(), ops, lootDataType.codec(), map);
+                 map.forEach(
+-                    (resourceLocation, object) -> writableRegistry.register(
+-                        ResourceKey.create(lootDataType.registryKey(), resourceLocation), (T)object, DEFAULT_REGISTRATION_INFO
++                    (resourceLocation, object) -> io.papermc.paper.registry.PaperRegistryListenerManager.INSTANCE.registerWithListeners(writableRegistry, // Paper - register with listeners
++                        ResourceKey.create(lootDataType.registryKey(), resourceLocation), (T)object, DEFAULT_REGISTRATION_INFO, conversions // Paper - register with listeners
+                     )
+                 );
+-                TagLoader.loadTagsForRegistry(resourceManager, writableRegistry);
++                TagLoader.loadTagsForRegistry(resourceManager, writableRegistry, io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent.Cause.RELOAD); // Paper - tag life cycle - reload
+                 return writableRegistry;
+             },
+             backgroundExecutor
diff --git a/paper-server/patches/unapplied/net/minecraft/server/ReloadableServerResources.java.patch b/paper-server/patches/sources/net/minecraft/server/ReloadableServerResources.java.patch
similarity index 79%
rename from paper-server/patches/unapplied/net/minecraft/server/ReloadableServerResources.java.patch
rename to paper-server/patches/sources/net/minecraft/server/ReloadableServerResources.java.patch
index 59120184c0..dd754adca3 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/ReloadableServerResources.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/ReloadableServerResources.java.patch
@@ -1,16 +1,16 @@
 --- a/net/minecraft/server/ReloadableServerResources.java
 +++ b/net/minecraft/server/ReloadableServerResources.java
-@@ -39,6 +39,7 @@
-         this.postponedTags = pendingTagLoads;
+@@ -39,6 +_,7 @@
+         this.postponedTags = postponedTags;
          this.recipes = new RecipeManager(registries);
-         this.commands = new Commands(environment, CommandBuildContext.simple(registries, enabledFeatures));
+         this.commands = new Commands(commandSelection, CommandBuildContext.simple(registries, enabledFeatures));
 +        io.papermc.paper.command.brigadier.PaperCommands.INSTANCE.setDispatcher(this.commands, CommandBuildContext.simple(registries, enabledFeatures)); // Paper - Brigadier Command API
          this.advancements = new ServerAdvancementManager(registries);
-         this.functionLibrary = new ServerFunctionLibrary(functionPermissionLevel, this.commands.getDispatcher());
+         this.functionLibrary = new ServerFunctionLibrary(functionCompilationLevel, this.commands.getDispatcher());
      }
-@@ -83,6 +84,14 @@
+@@ -83,6 +_,14 @@
                      ReloadableServerResources reloadableServerResources = new ReloadableServerResources(
-                         reloadResult.layers(), reloadResult.lookupWithUpdatedTags(), enabledFeatures, environment, pendingTagLoads, functionPermissionLevel
+                         loadResult.layers(), loadResult.lookupWithUpdatedTags(), enabledFeatures, commandSelection, postponedTags, functionCompilationLevel
                      );
 +                    // Paper start - call commands event for bootstraps
 +                    //noinspection ConstantValue
diff --git a/paper-server/patches/unapplied/net/minecraft/server/ServerAdvancementManager.java.patch b/paper-server/patches/sources/net/minecraft/server/ServerAdvancementManager.java.patch
similarity index 50%
rename from paper-server/patches/unapplied/net/minecraft/server/ServerAdvancementManager.java.patch
rename to paper-server/patches/sources/net/minecraft/server/ServerAdvancementManager.java.patch
index 5a5a995288..3be388af55 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/ServerAdvancementManager.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/ServerAdvancementManager.java.patch
@@ -1,39 +1,31 @@
 --- a/net/minecraft/server/ServerAdvancementManager.java
 +++ b/net/minecraft/server/ServerAdvancementManager.java
-@@ -21,10 +21,14 @@
- import net.minecraft.util.profiling.ProfilerFiller;
- import org.slf4j.Logger;
+@@ -22,7 +_,7 @@
  
-+// CraftBukkit start
-+import java.util.HashMap;
-+// CraftBukkit end
-+
  public class ServerAdvancementManager extends SimpleJsonResourceReloadListener<Advancement> {
- 
      private static final Logger LOGGER = LogUtils.getLogger();
 -    public Map<ResourceLocation, AdvancementHolder> advancements = Map.of();
-+    public Map<ResourceLocation, AdvancementHolder> advancements = new HashMap<>(); // CraftBukkit - SPIGOT-7734: mutable
++    public Map<ResourceLocation, AdvancementHolder> advancements = new java.util.HashMap<>(); // CraftBukkit - SPIGOT-7734: mutable
      private AdvancementTree tree = new AdvancementTree();
      private final HolderLookup.Provider registries;
  
-@@ -37,13 +41,19 @@
+@@ -35,12 +_,18 @@
+     protected void apply(Map<ResourceLocation, Advancement> object, ResourceManager resourceManager, ProfilerFiller profiler) {
          Builder<ResourceLocation, AdvancementHolder> builder = ImmutableMap.builder();
- 
-         prepared.forEach((minecraftkey, advancement) -> {
+         object.forEach((resourceLocation, advancement) -> {
 +            // Spigot start
-+            if (org.spigotmc.SpigotConfig.disabledAdvancements != null && (org.spigotmc.SpigotConfig.disabledAdvancements.contains("*") || org.spigotmc.SpigotConfig.disabledAdvancements.contains(minecraftkey.toString()) || org.spigotmc.SpigotConfig.disabledAdvancements.contains(minecraftkey.getNamespace()))) {
++            if (org.spigotmc.SpigotConfig.disabledAdvancements != null && (org.spigotmc.SpigotConfig.disabledAdvancements.contains("*") || org.spigotmc.SpigotConfig.disabledAdvancements.contains(resourceLocation.toString()) || org.spigotmc.SpigotConfig.disabledAdvancements.contains(resourceLocation.getNamespace()))) {
 +                return;
 +            }
 +            // Spigot end
-             this.validate(minecraftkey, advancement);
-             builder.put(minecraftkey, new AdvancementHolder(minecraftkey, advancement));
+             this.validate(resourceLocation, advancement);
+             builder.put(resourceLocation, new AdvancementHolder(resourceLocation, advancement));
          });
 -        this.advancements = builder.buildOrThrow();
-+        this.advancements = new HashMap<>(builder.buildOrThrow()); // CraftBukkit - SPIGOT-7734: mutable
-         AdvancementTree advancementtree = new AdvancementTree();
++        this.advancements = new java.util.HashMap<>(builder.buildOrThrow()); // CraftBukkit - SPIGOT-7734: mutable
+         AdvancementTree advancementTree = new AdvancementTree();
+         advancementTree.addAll(this.advancements.values());
++        LOGGER.info("Loaded {} advancements", advancementTree.nodes().size()); // Paper - Improve logging and errors; moved from AdvancementTree#addAll
  
-         advancementtree.addAll(this.advancements.values());
-+        LOGGER.info("Loaded {} advancements", advancementtree.nodes().size()); // Paper - Improve logging and errors; moved from AdvancementTree#addAll
-         Iterator iterator = advancementtree.roots().iterator();
- 
-         while (iterator.hasNext()) {
+         for (AdvancementNode advancementNode : advancementTree.roots()) {
+             if (advancementNode.holder().value().display().isPresent()) {
diff --git a/paper-server/patches/sources/net/minecraft/server/ServerFunctionLibrary.java.patch b/paper-server/patches/sources/net/minecraft/server/ServerFunctionLibrary.java.patch
new file mode 100644
index 0000000000..55bb398552
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/ServerFunctionLibrary.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/server/ServerFunctionLibrary.java
++++ b/net/minecraft/server/ServerFunctionLibrary.java
+@@ -113,7 +_,7 @@
+                         return null;
+                     }).join());
+                     this.functions = builder.build();
+-                    this.tags = this.tagsLoader.build((Map<ResourceLocation, List<TagLoader.EntryWithSource>>)pair.getFirst());
++                    this.tags = this.tagsLoader.build((Map<ResourceLocation, List<TagLoader.EntryWithSource>>)pair.getFirst(), null); // Paper - command function tags are not implemented yet
+                 },
+                 gameExecutor
+             );
diff --git a/paper-server/patches/unapplied/net/minecraft/server/ServerScoreboard.java.patch b/paper-server/patches/sources/net/minecraft/server/ServerScoreboard.java.patch
similarity index 54%
rename from paper-server/patches/unapplied/net/minecraft/server/ServerScoreboard.java.patch
rename to paper-server/patches/sources/net/minecraft/server/ServerScoreboard.java.patch
index 712e8444b1..5b73435f3a 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/ServerScoreboard.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/ServerScoreboard.java.patch
@@ -1,24 +1,26 @@
 --- a/net/minecraft/server/ServerScoreboard.java
 +++ b/net/minecraft/server/ServerScoreboard.java
-@@ -42,7 +42,7 @@
+@@ -39,9 +_,7 @@
      protected void onScoreChanged(ScoreHolder scoreHolder, Objective objective, Score score) {
          super.onScoreChanged(scoreHolder, objective, score);
          if (this.trackedObjectives.contains(objective)) {
--            this.server.getPlayerList().broadcastAll(new ClientboundSetScorePacket(scoreHolder.getScoreboardName(), objective.getName(), score.value(), Optional.ofNullable(score.display()), Optional.ofNullable(score.numberFormat())));
-+            this.broadcastAll(new ClientboundSetScorePacket(scoreHolder.getScoreboardName(), objective.getName(), score.value(), Optional.ofNullable(score.display()), Optional.ofNullable(score.numberFormat()))); // CraftBukkit
-         }
- 
-         this.setDirty();
-@@ -57,7 +57,7 @@
+-            this.server
+-                .getPlayerList()
+-                .broadcastAll(
++            this.broadcastAll( // CraftBukkit
+                     new ClientboundSetScorePacket(
+                         scoreHolder.getScoreboardName(),
+                         objective.getName(),
+@@ -64,7 +_,7 @@
      @Override
      public void onPlayerRemoved(ScoreHolder scoreHolder) {
          super.onPlayerRemoved(scoreHolder);
--        this.server.getPlayerList().broadcastAll(new ClientboundResetScorePacket(scoreHolder.getScoreboardName(), (String) null));
-+        this.broadcastAll(new ClientboundResetScorePacket(scoreHolder.getScoreboardName(), (String) null)); // CraftBukkit
+-        this.server.getPlayerList().broadcastAll(new ClientboundResetScorePacket(scoreHolder.getScoreboardName(), null));
++        this.broadcastAll(new ClientboundResetScorePacket(scoreHolder.getScoreboardName(), null)); // CraftBukkit
          this.setDirty();
      }
  
-@@ -65,7 +65,7 @@
+@@ -72,7 +_,7 @@
      public void onPlayerScoreRemoved(ScoreHolder scoreHolder, Objective objective) {
          super.onPlayerScoreRemoved(scoreHolder, objective);
          if (this.trackedObjectives.contains(objective)) {
@@ -27,16 +29,16 @@
          }
  
          this.setDirty();
-@@ -78,7 +78,7 @@
+@@ -84,7 +_,7 @@
          super.setDisplayObjective(slot, objective);
-         if (scoreboardobjective1 != objective && scoreboardobjective1 != null) {
-             if (this.getObjectiveDisplaySlotCount(scoreboardobjective1) > 0) {
+         if (displayObjective != objective && displayObjective != null) {
+             if (this.getObjectiveDisplaySlotCount(displayObjective) > 0) {
 -                this.server.getPlayerList().broadcastAll(new ClientboundSetDisplayObjectivePacket(slot, objective));
 +                this.broadcastAll(new ClientboundSetDisplayObjectivePacket(slot, objective)); // CraftBukkit
              } else {
-                 this.stopTrackingObjective(scoreboardobjective1);
+                 this.stopTrackingObjective(displayObjective);
              }
-@@ -86,7 +86,7 @@
+@@ -92,7 +_,7 @@
  
          if (objective != null) {
              if (this.trackedObjectives.contains(objective)) {
@@ -45,19 +47,27 @@
              } else {
                  this.startTrackingObjective(objective);
              }
-@@ -98,7 +98,7 @@
+@@ -104,24 +_,50 @@
      @Override
-     public boolean addPlayerToTeam(String scoreHolderName, PlayerTeam team) {
-         if (super.addPlayerToTeam(scoreHolderName, team)) {
--            this.server.getPlayerList().broadcastAll(ClientboundSetPlayerTeamPacket.createPlayerPacket(team, scoreHolderName, ClientboundSetPlayerTeamPacket.Action.ADD));
-+            this.broadcastAll(ClientboundSetPlayerTeamPacket.createPlayerPacket(team, scoreHolderName, ClientboundSetPlayerTeamPacket.Action.ADD)); // CraftBukkit
-             this.setDirty();
-             return true;
-         } else {
-@@ -106,13 +106,43 @@
-         }
-     }
- 
+     public boolean addPlayerToTeam(String playerName, PlayerTeam team) {
+         if (super.addPlayerToTeam(playerName, team)) {
+-            this.server
+-                .getPlayerList()
+-                .broadcastAll(ClientboundSetPlayerTeamPacket.createPlayerPacket(team, playerName, ClientboundSetPlayerTeamPacket.Action.ADD));
+-            this.setDirty();
+-            return true;
+-        } else {
+-            return false;
+-        }
+-    }
++            this.broadcastAll(ClientboundSetPlayerTeamPacket.createPlayerPacket(team, playerName, ClientboundSetPlayerTeamPacket.Action.ADD)); // CraftBukkit
++            this.setDirty();
++            return true;
++        } else {
++            return false;
++        }
++    }
++
 +    // Paper start - Multiple Entries with Scoreboards
 +    public boolean addPlayersToTeam(java.util.Collection<String> players, PlayerTeam team) {
 +        boolean anyAdded = false;
@@ -76,15 +86,19 @@
 +        }
 +    }
 +    // Paper end - Multiple Entries with Scoreboards
-+
-     @Override
-     public void removePlayerFromTeam(String scoreHolderName, PlayerTeam team) {
-         super.removePlayerFromTeam(scoreHolderName, team);
--        this.server.getPlayerList().broadcastAll(ClientboundSetPlayerTeamPacket.createPlayerPacket(team, scoreHolderName, ClientboundSetPlayerTeamPacket.Action.REMOVE));
-+        this.broadcastAll(ClientboundSetPlayerTeamPacket.createPlayerPacket(team, scoreHolderName, ClientboundSetPlayerTeamPacket.Action.REMOVE)); // CraftBukkit
-         this.setDirty();
-     }
  
+     @Override
+     public void removePlayerFromTeam(String username, PlayerTeam playerTeam) {
+         super.removePlayerFromTeam(username, playerTeam);
+-        this.server
+-            .getPlayerList()
+-            .broadcastAll(ClientboundSetPlayerTeamPacket.createPlayerPacket(playerTeam, username, ClientboundSetPlayerTeamPacket.Action.REMOVE));
+-        this.setDirty();
+-    }
++        this.broadcastAll(ClientboundSetPlayerTeamPacket.createPlayerPacket(playerTeam, username, ClientboundSetPlayerTeamPacket.Action.REMOVE)); // CraftBukkit
++        this.setDirty();
++    }
++
 +    // Paper start - Multiple Entries with Scoreboards
 +    public void removePlayersFromTeam(java.util.Collection<String> players, PlayerTeam team) {
 +        for (String playerName : players) {
@@ -95,11 +109,10 @@
 +        this.setDirty();
 +    }
 +    // Paper end - Multiple Entries with Scoreboards
-+
+ 
      @Override
      public void onObjectiveAdded(Objective objective) {
-         super.onObjectiveAdded(objective);
-@@ -123,7 +153,7 @@
+@@ -133,7 +_,7 @@
      public void onObjectiveChanged(Objective objective) {
          super.onObjectiveChanged(objective);
          if (this.trackedObjectives.contains(objective)) {
@@ -108,61 +121,61 @@
          }
  
          this.setDirty();
-@@ -142,21 +172,21 @@
+@@ -152,21 +_,21 @@
      @Override
-     public void onTeamAdded(PlayerTeam team) {
-         super.onTeamAdded(team);
--        this.server.getPlayerList().broadcastAll(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(team, true));
-+        this.broadcastAll(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(team, true)); // CraftBukkit
+     public void onTeamAdded(PlayerTeam playerTeam) {
+         super.onTeamAdded(playerTeam);
+-        this.server.getPlayerList().broadcastAll(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(playerTeam, true));
++        this.broadcastAll(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(playerTeam, true)); // CraftBukkit
          this.setDirty();
      }
  
      @Override
-     public void onTeamChanged(PlayerTeam team) {
-         super.onTeamChanged(team);
--        this.server.getPlayerList().broadcastAll(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(team, false));
-+        this.broadcastAll(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(team, false)); // CraftBukkit
+     public void onTeamChanged(PlayerTeam playerTeam) {
+         super.onTeamChanged(playerTeam);
+-        this.server.getPlayerList().broadcastAll(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(playerTeam, false));
++        this.broadcastAll(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(playerTeam, false)); // CraftBukkit
          this.setDirty();
      }
  
      @Override
-     public void onTeamRemoved(PlayerTeam team) {
-         super.onTeamRemoved(team);
--        this.server.getPlayerList().broadcastAll(ClientboundSetPlayerTeamPacket.createRemovePacket(team));
-+        this.broadcastAll(ClientboundSetPlayerTeamPacket.createRemovePacket(team)); // CraftBukkit
+     public void onTeamRemoved(PlayerTeam playerTeam) {
+         super.onTeamRemoved(playerTeam);
+-        this.server.getPlayerList().broadcastAll(ClientboundSetPlayerTeamPacket.createRemovePacket(playerTeam));
++        this.broadcastAll(ClientboundSetPlayerTeamPacket.createRemovePacket(playerTeam)); // CraftBukkit
          this.setDirty();
      }
  
-@@ -207,6 +237,7 @@
+@@ -209,6 +_,7 @@
+         List<Packet<?>> startTrackingPackets = this.getStartTrackingPackets(objective);
  
-         while (iterator.hasNext()) {
-             ServerPlayer entityplayer = (ServerPlayer) iterator.next();
-+            if (entityplayer.getBukkitEntity().getScoreboard().getHandle() != this) continue; // CraftBukkit - Only players on this board
-             Iterator iterator1 = list.iterator();
+         for (ServerPlayer serverPlayer : this.server.getPlayerList().getPlayers()) {
++            if (serverPlayer.getBukkitEntity().getScoreboard().getHandle() != this) continue; // CraftBukkit - Only players on this board
+             for (Packet<?> packet : startTrackingPackets) {
+                 serverPlayer.connection.send(packet);
+             }
+@@ -234,6 +_,7 @@
+         List<Packet<?>> stopTrackingPackets = this.getStopTrackingPackets(objective);
  
-             while (iterator1.hasNext()) {
-@@ -243,6 +274,7 @@
- 
-         while (iterator.hasNext()) {
-             ServerPlayer entityplayer = (ServerPlayer) iterator.next();
-+            if (entityplayer.getBukkitEntity().getScoreboard().getHandle() != this) continue; // CraftBukkit - Only players on this board
-             Iterator iterator1 = list.iterator();
- 
-             while (iterator1.hasNext()) {
-@@ -287,6 +319,16 @@
-         return this.createData().load(nbt, registries);
+         for (ServerPlayer serverPlayer : this.server.getPlayerList().getPlayers()) {
++            if (serverPlayer.getBukkitEntity().getScoreboard().getHandle() != this) continue; // CraftBukkit - Only players on this board
+             for (Packet<?> packet : stopTrackingPackets) {
+                 serverPlayer.connection.send(packet);
+             }
+@@ -267,6 +_,16 @@
+     private ScoreboardSaveData createData(CompoundTag tag, HolderLookup.Provider registries) {
+         return this.createData().load(tag, registries);
      }
- 
++
 +    // CraftBukkit start - Send to players
-+    private void broadcastAll(Packet packet) {
-+        for (ServerPlayer entityplayer : (List<ServerPlayer>) this.server.getPlayerList().players) {
-+            if (entityplayer.getBukkitEntity().getScoreboard().getHandle() == this) {
-+                entityplayer.connection.send(packet);
++    private void broadcastAll(Packet<?> packet) {
++        for (ServerPlayer serverPlayer : this.server.getPlayerList().players) {
++            if (serverPlayer.getBukkitEntity().getScoreboard().getHandle() == this) {
++                serverPlayer.connection.send(packet);
 +            }
 +        }
 +    }
 +    // CraftBukkit end
-+
-     public static enum Method {
  
-         CHANGE, REMOVE;
+     public static enum Method {
+         CHANGE,
diff --git a/paper-server/patches/unapplied/net/minecraft/server/Services.java.patch b/paper-server/patches/sources/net/minecraft/server/Services.java.patch
similarity index 84%
rename from paper-server/patches/unapplied/net/minecraft/server/Services.java.patch
rename to paper-server/patches/sources/net/minecraft/server/Services.java.patch
index c556957aab..86364d06cd 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/Services.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/Services.java.patch
@@ -1,38 +1,38 @@
 --- a/net/minecraft/server/Services.java
 +++ b/net/minecraft/server/Services.java
-@@ -10,16 +10,32 @@
- import net.minecraft.server.players.GameProfileCache;
+@@ -11,15 +_,31 @@
  import net.minecraft.util.SignatureValidator;
  
-+
  public record Services(
 -    MinecraftSessionService sessionService, ServicesKeySet servicesKeySet, GameProfileRepository profileRepository, GameProfileCache profileCache
 +    MinecraftSessionService sessionService, ServicesKeySet servicesKeySet, GameProfileRepository profileRepository, GameProfileCache profileCache, @javax.annotation.Nullable io.papermc.paper.configuration.PaperConfigurations paperConfigurations // Paper - add paper configuration files
  ) {
 -    private static final String USERID_CACHE_FILE = "usercache.json";
+-
+-    public static Services create(YggdrasilAuthenticationService authenticationService, File profileRepository) {
++    public static final String USERID_CACHE_FILE = "usercache.json"; // Paper - private -> public
++
 +    // Paper start - add paper configuration files
 +    public Services(MinecraftSessionService sessionService, ServicesKeySet servicesKeySet, GameProfileRepository profileRepository, GameProfileCache profileCache) {
 +        this(sessionService, servicesKeySet, profileRepository, profileCache, null);
 +    }
- 
--    public static Services create(YggdrasilAuthenticationService authenticationService, File rootDirectory) {
++
 +    @Override
 +    public io.papermc.paper.configuration.PaperConfigurations paperConfigurations() {
 +        return java.util.Objects.requireNonNull(this.paperConfigurations);
 +    }
 +    // Paper end - add paper configuration files
-+    public static final String USERID_CACHE_FILE = "usercache.json"; // Paper - private -> public
 +
-+    public static Services create(YggdrasilAuthenticationService authenticationService, File rootDirectory, File userCacheFile, joptsimple.OptionSet optionSet) throws Exception { // Paper - add optionset to load paper config files; add userCacheFile parameter
++    public static Services create(YggdrasilAuthenticationService authenticationService, File profileRepository, File userCacheFile, joptsimple.OptionSet optionSet) throws Exception { // Paper - add optionset to load paper config files; add userCacheFile parameter
          MinecraftSessionService minecraftSessionService = authenticationService.createMinecraftSessionService();
          GameProfileRepository gameProfileRepository = authenticationService.createProfileRepository();
--        GameProfileCache gameProfileCache = new GameProfileCache(gameProfileRepository, new File(rootDirectory, "usercache.json"));
+-        GameProfileCache gameProfileCache = new GameProfileCache(gameProfileRepository, new File(profileRepository, "usercache.json"));
 -        return new Services(minecraftSessionService, authenticationService.getServicesKeySet(), gameProfileRepository, gameProfileCache);
 +        GameProfileCache gameProfileCache = new GameProfileCache(gameProfileRepository, userCacheFile); // Paper - use specified user cache file
 +        // Paper start - load paper config files from cli options
 +        final java.nio.file.Path legacyConfigPath = ((File) optionSet.valueOf("paper-settings")).toPath();
 +        final java.nio.file.Path configDirPath = ((File) optionSet.valueOf("paper-settings-directory")).toPath();
-+        io.papermc.paper.configuration.PaperConfigurations paperConfigurations = io.papermc.paper.configuration.PaperConfigurations.setup(legacyConfigPath, configDirPath, rootDirectory.toPath(), (File) optionSet.valueOf("spigot-settings"));
++        io.papermc.paper.configuration.PaperConfigurations paperConfigurations = io.papermc.paper.configuration.PaperConfigurations.setup(legacyConfigPath, configDirPath, profileRepository.toPath(), (File) optionSet.valueOf("spigot-settings"));
 +        return new Services(minecraftSessionService, authenticationService.getServicesKeySet(), gameProfileRepository, gameProfileCache, paperConfigurations);
 +        // Paper end - load paper config files from cli options
      }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/WorldLoader.java.patch b/paper-server/patches/sources/net/minecraft/server/WorldLoader.java.patch
similarity index 73%
rename from paper-server/patches/unapplied/net/minecraft/server/WorldLoader.java.patch
rename to paper-server/patches/sources/net/minecraft/server/WorldLoader.java.patch
index c95f9d743c..a2c0814152 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/WorldLoader.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/WorldLoader.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/server/WorldLoader.java
 +++ b/net/minecraft/server/WorldLoader.java
-@@ -37,7 +37,7 @@
+@@ -37,7 +_,7 @@
              CloseableResourceManager closeableResourceManager = pair.getSecond();
              LayeredRegistryAccess<RegistryLayer> layeredRegistryAccess = RegistryLayer.createRegistryAccess();
              List<Registry.PendingTags<?>> list = TagLoader.loadTagsForExistingRegistries(
 -                closeableResourceManager, layeredRegistryAccess.getLayer(RegistryLayer.STATIC)
 +                closeableResourceManager, layeredRegistryAccess.getLayer(RegistryLayer.STATIC), io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent.Cause.INITIAL // Paper - tag lifecycle - add cause
              );
-             RegistryAccess.Frozen frozen = layeredRegistryAccess.getAccessForLoading(RegistryLayer.WORLDGEN);
-             List<HolderLookup.RegistryLookup<?>> list2 = TagLoader.buildUpdatedLookups(frozen, list);
+             RegistryAccess.Frozen accessForLoading = layeredRegistryAccess.getAccessForLoading(RegistryLayer.WORLDGEN);
+             List<HolderLookup.RegistryLookup<?>> list1 = TagLoader.buildUpdatedLookups(accessForLoading, list);
diff --git a/paper-server/patches/sources/net/minecraft/server/bossevents/CustomBossEvent.java.patch b/paper-server/patches/sources/net/minecraft/server/bossevents/CustomBossEvent.java.patch
new file mode 100644
index 0000000000..9aa2e7d869
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/bossevents/CustomBossEvent.java.patch
@@ -0,0 +1,19 @@
+--- a/net/minecraft/server/bossevents/CustomBossEvent.java
++++ b/net/minecraft/server/bossevents/CustomBossEvent.java
+@@ -23,6 +_,16 @@
+     private final Set<UUID> players = Sets.newHashSet();
+     private int value;
+     private int max = 100;
++    // CraftBukkit start
++    private org.bukkit.boss.KeyedBossBar bossBar;
++
++    public org.bukkit.boss.KeyedBossBar getBukkitEntity() {
++        if (this.bossBar == null) {
++            this.bossBar = new org.bukkit.craftbukkit.boss.CraftKeyedBossbar(this);
++        }
++        return this.bossBar;
++    }
++    // CraftBukkit end
+ 
+     public CustomBossEvent(ResourceLocation id, Component name) {
+         super(name, BossEvent.BossBarColor.WHITE, BossEvent.BossBarOverlay.PROGRESS);
diff --git a/paper-server/patches/unapplied/net/minecraft/server/commands/BanIpCommands.java.patch b/paper-server/patches/sources/net/minecraft/server/commands/BanIpCommands.java.patch
similarity index 77%
rename from paper-server/patches/unapplied/net/minecraft/server/commands/BanIpCommands.java.patch
rename to paper-server/patches/sources/net/minecraft/server/commands/BanIpCommands.java.patch
index be620cd58a..3a29620e92 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/commands/BanIpCommands.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/commands/BanIpCommands.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/server/commands/BanIpCommands.java
 +++ b/net/minecraft/server/commands/BanIpCommands.java
-@@ -66,7 +66,7 @@
+@@ -68,7 +_,7 @@
              }
  
-             for (ServerPlayer serverPlayer : list) {
+             for (ServerPlayer serverPlayer : playersWithAddress) {
 -                serverPlayer.connection.disconnect(Component.translatable("multiplayer.disconnect.ip_banned"));
 +                serverPlayer.connection.disconnect(Component.translatable("multiplayer.disconnect.ip_banned"), org.bukkit.event.player.PlayerKickEvent.Cause.IP_BANNED); // Paper - kick event cause
              }
  
-             return list.size();
+             return playersWithAddress.size();
diff --git a/paper-server/patches/sources/net/minecraft/server/commands/BanPlayerCommands.java.patch b/paper-server/patches/sources/net/minecraft/server/commands/BanPlayerCommands.java.patch
new file mode 100644
index 0000000000..9c2282ee36
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/commands/BanPlayerCommands.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/server/commands/BanPlayerCommands.java
++++ b/net/minecraft/server/commands/BanPlayerCommands.java
+@@ -55,7 +_,7 @@
+                 );
+                 ServerPlayer player = source.getServer().getPlayerList().getPlayer(gameProfile.getId());
+                 if (player != null) {
+-                    player.connection.disconnect(Component.translatable("multiplayer.disconnect.banned"));
++                    player.connection.disconnect(Component.translatable("multiplayer.disconnect.banned"), org.bukkit.event.player.PlayerKickEvent.Cause.BANNED); // Paper - kick event cause
+                 }
+             }
+         }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/commands/DeOpCommands.java.patch b/paper-server/patches/sources/net/minecraft/server/commands/DeOpCommands.java.patch
similarity index 85%
rename from paper-server/patches/unapplied/net/minecraft/server/commands/DeOpCommands.java.patch
rename to paper-server/patches/sources/net/minecraft/server/commands/DeOpCommands.java.patch
index 5009b1bdcb..8f0c19b86d 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/commands/DeOpCommands.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/commands/DeOpCommands.java.patch
@@ -1,10 +1,10 @@
 --- a/net/minecraft/server/commands/DeOpCommands.java
 +++ b/net/minecraft/server/commands/DeOpCommands.java
-@@ -35,7 +35,7 @@
+@@ -35,7 +_,7 @@
              if (playerList.isOp(gameProfile)) {
                  playerList.deop(gameProfile);
                  i++;
--                source.sendSuccess(() -> Component.translatable("commands.deop.success", targets.iterator().next().getName()), true);
+-                source.sendSuccess(() -> Component.translatable("commands.deop.success", players.iterator().next().getName()), true);
 +                source.sendSuccess(() -> Component.translatable("commands.deop.success", gameProfile.getName()), true); // Paper - fixes MC-253721
              }
          }
diff --git a/paper-server/patches/sources/net/minecraft/server/commands/DebugCommand.java.patch b/paper-server/patches/sources/net/minecraft/server/commands/DebugCommand.java.patch
new file mode 100644
index 0000000000..9d1cc61c46
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/commands/DebugCommand.java.patch
@@ -0,0 +1,16 @@
+--- a/net/minecraft/server/commands/DebugCommand.java
++++ b/net/minecraft/server/commands/DebugCommand.java
+@@ -262,6 +_,13 @@
+             return true;
+         }
+ 
++        // Paper start
++        @Override
++        public org.bukkit.command.CommandSender getBukkitSender(final CommandSourceStack wrapper) {
++            throw new UnsupportedOperationException();
++        }
++        // Paper end
++
+         @Override
+         public void close() {
+             IOUtils.closeQuietly((Writer)this.output);
diff --git a/paper-server/patches/unapplied/net/minecraft/server/commands/DefaultGameModeCommands.java.patch b/paper-server/patches/sources/net/minecraft/server/commands/DefaultGameModeCommands.java.patch
similarity index 55%
rename from paper-server/patches/unapplied/net/minecraft/server/commands/DefaultGameModeCommands.java.patch
rename to paper-server/patches/sources/net/minecraft/server/commands/DefaultGameModeCommands.java.patch
index dfb815a54e..4f48d7dc70 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/commands/DefaultGameModeCommands.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/commands/DefaultGameModeCommands.java.patch
@@ -1,15 +1,15 @@
 --- a/net/minecraft/server/commands/DefaultGameModeCommands.java
 +++ b/net/minecraft/server/commands/DefaultGameModeCommands.java
-@@ -28,9 +28,13 @@
-         GameType gameType = minecraftServer.getForcedGameType();
-         if (gameType != null) {
-             for (ServerPlayer serverPlayer : minecraftServer.getPlayerList().getPlayers()) {
--                if (serverPlayer.setGameMode(gameType)) {
+@@ -28,9 +_,13 @@
+         GameType forcedGameType = server.getForcedGameType();
+         if (forcedGameType != null) {
+             for (ServerPlayer serverPlayer : server.getPlayerList().getPlayers()) {
+-                if (serverPlayer.setGameMode(forcedGameType)) {
 -                    i++;
 +                // Paper start - Expand PlayerGameModeChangeEvent
-+                org.bukkit.event.player.PlayerGameModeChangeEvent event = serverPlayer.setGameMode(gameType, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.DEFAULT_GAMEMODE, net.kyori.adventure.text.Component.empty());
++                org.bukkit.event.player.PlayerGameModeChangeEvent event = serverPlayer.setGameMode(gamemode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.DEFAULT_GAMEMODE, net.kyori.adventure.text.Component.empty());
 +                if (event != null && event.isCancelled()) {
-+                    source.sendSuccess(() -> io.papermc.paper.adventure.PaperAdventure.asVanilla(event.cancelMessage()), false);
++                    commandSource.sendSuccess(() -> io.papermc.paper.adventure.PaperAdventure.asVanilla(event.cancelMessage()), false);
                  }
 +                // Paper end - Expand PlayerGameModeChangeEvent
 +                    i++;
diff --git a/paper-server/patches/sources/net/minecraft/server/commands/DifficultyCommand.java.patch b/paper-server/patches/sources/net/minecraft/server/commands/DifficultyCommand.java.patch
new file mode 100644
index 0000000000..5206cb78a7
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/commands/DifficultyCommand.java.patch
@@ -0,0 +1,15 @@
+--- a/net/minecraft/server/commands/DifficultyCommand.java
++++ b/net/minecraft/server/commands/DifficultyCommand.java
+@@ -31,10 +_,10 @@
+ 
+     public static int setDifficulty(CommandSourceStack source, Difficulty difficulty) throws CommandSyntaxException {
+         MinecraftServer server = source.getServer();
+-        if (server.getWorldData().getDifficulty() == difficulty) {
++        if (source.getLevel().getDifficulty() == difficulty) { // CraftBukkit
+             throw ERROR_ALREADY_DIFFICULT.create(difficulty.getKey());
+         } else {
+-            server.setDifficulty(difficulty, true);
++            server.setDifficulty(source.getLevel(), difficulty, true); // Paper - per level difficulty; don't skip other difficulty-changing logic (fix upstream's fix)
+             source.sendSuccess(() -> Component.translatable("commands.difficulty.success", difficulty.getDisplayName()), true);
+             return 0;
+         }
diff --git a/paper-server/patches/sources/net/minecraft/server/commands/EffectCommands.java.patch b/paper-server/patches/sources/net/minecraft/server/commands/EffectCommands.java.patch
new file mode 100644
index 0000000000..b634208b16
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/commands/EffectCommands.java.patch
@@ -0,0 +1,29 @@
+--- a/net/minecraft/server/commands/EffectCommands.java
++++ b/net/minecraft/server/commands/EffectCommands.java
+@@ -180,7 +_,7 @@
+         for (Entity entity : targets) {
+             if (entity instanceof LivingEntity) {
+                 MobEffectInstance mobEffectInstance = new MobEffectInstance(effect, i1, amplifier, false, showParticles);
+-                if (((LivingEntity)entity).addEffect(mobEffectInstance, source.getEntity())) {
++                if (((LivingEntity)entity).addEffect(mobEffectInstance, source.getEntity(), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit
+                     i++;
+                 }
+             }
+@@ -210,7 +_,7 @@
+         int i = 0;
+ 
+         for (Entity entity : targets) {
+-            if (entity instanceof LivingEntity && ((LivingEntity)entity).removeAllEffects()) {
++            if (entity instanceof LivingEntity && ((LivingEntity)entity).removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit
+                 i++;
+             }
+         }
+@@ -235,7 +_,7 @@
+         int i = 0;
+ 
+         for (Entity entity : targets) {
+-            if (entity instanceof LivingEntity && ((LivingEntity)entity).removeEffect(effect)) {
++            if (entity instanceof LivingEntity && ((LivingEntity)entity).removeEffect(effect, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit
+                 i++;
+             }
+         }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/commands/GameModeCommand.java.patch b/paper-server/patches/sources/net/minecraft/server/commands/GameModeCommand.java.patch
similarity index 59%
rename from paper-server/patches/unapplied/net/minecraft/server/commands/GameModeCommand.java.patch
rename to paper-server/patches/sources/net/minecraft/server/commands/GameModeCommand.java.patch
index 5984de9d0f..51e5a4909d 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/commands/GameModeCommand.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/commands/GameModeCommand.java.patch
@@ -1,17 +1,17 @@
 --- a/net/minecraft/server/commands/GameModeCommand.java
 +++ b/net/minecraft/server/commands/GameModeCommand.java
-@@ -60,9 +60,14 @@
+@@ -54,9 +_,14 @@
          int i = 0;
  
-         for (ServerPlayer serverPlayer : targets) {
--            if (serverPlayer.setGameMode(gameMode)) {
+         for (ServerPlayer serverPlayer : players) {
+-            if (serverPlayer.setGameMode(gameType)) {
 +            // Paper start - Expand PlayerGameModeChangeEvent
-+            org.bukkit.event.player.PlayerGameModeChangeEvent event = serverPlayer.setGameMode(gameMode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.COMMAND, net.kyori.adventure.text.Component.empty());
++            org.bukkit.event.player.PlayerGameModeChangeEvent event = serverPlayer.setGameMode(gameType, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.COMMAND, net.kyori.adventure.text.Component.empty());
 +            if (event != null && !event.isCancelled()) {
-                 logGamemodeChange(context.getSource(), serverPlayer, gameMode);
+                 logGamemodeChange(source.getSource(), serverPlayer, gameType);
                  i++;
 +            } else if (event != null && event.cancelMessage() != null) {
-+                context.getSource().sendSuccess(() -> io.papermc.paper.adventure.PaperAdventure.asVanilla(event.cancelMessage()), true);
++                source.getSource().sendSuccess(() -> io.papermc.paper.adventure.PaperAdventure.asVanilla(event.cancelMessage()), true);
 +                // Paper end - Expand PlayerGameModeChangeEvent
              }
          }
diff --git a/paper-server/patches/sources/net/minecraft/server/commands/GameRuleCommand.java.patch b/paper-server/patches/sources/net/minecraft/server/commands/GameRuleCommand.java.patch
new file mode 100644
index 0000000000..1f8178fb56
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/commands/GameRuleCommand.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/server/commands/GameRuleCommand.java
++++ b/net/minecraft/server/commands/GameRuleCommand.java
+@@ -30,14 +_,14 @@
+ 
+     static <T extends GameRules.Value<T>> int setRule(CommandContext<CommandSourceStack> source, GameRules.Key<T> gameRule) {
+         CommandSourceStack commandSourceStack = source.getSource();
+-        T rule = commandSourceStack.getServer().getGameRules().getRule(gameRule);
+-        rule.setFromArgument(source, "value");
++        T rule = commandSourceStack.getLevel().getGameRules().getRule(gameRule); // CraftBukkit
++        rule.setFromArgument(source, "value", gameRule); // Paper - Add WorldGameRuleChangeEvent
+         commandSourceStack.sendSuccess(() -> Component.translatable("commands.gamerule.set", gameRule.getId(), rule.toString()), true);
+         return rule.getCommandResult();
+     }
+ 
+     static <T extends GameRules.Value<T>> int queryRule(CommandSourceStack source, GameRules.Key<T> gameRule) {
+-        T rule = source.getServer().getGameRules().getRule(gameRule);
++        T rule = source.getLevel().getGameRules().getRule(gameRule); // CraftBukkit
+         source.sendSuccess(() -> Component.translatable("commands.gamerule.query", gameRule.getId(), rule.toString()), false);
+         return rule.getCommandResult();
+     }
diff --git a/paper-server/patches/sources/net/minecraft/server/commands/GiveCommand.java.patch b/paper-server/patches/sources/net/minecraft/server/commands/GiveCommand.java.patch
new file mode 100644
index 0000000000..be76cc3572
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/commands/GiveCommand.java.patch
@@ -0,0 +1,33 @@
+--- a/net/minecraft/server/commands/GiveCommand.java
++++ b/net/minecraft/server/commands/GiveCommand.java
+@@ -51,6 +_,7 @@
+ 
+     private static int giveItem(CommandSourceStack source, ItemInput item, Collection<ServerPlayer> targets, int count) throws CommandSyntaxException {
+         ItemStack itemStack = item.createItemStack(1, false);
++        final Component displayName = itemStack.getDisplayName(); // Paper - get display name early
+         int maxStackSize = itemStack.getMaxStackSize();
+         int i = maxStackSize * 100;
+         if (count > i) {
+@@ -66,7 +_,7 @@
+                     ItemStack itemStack1 = item.createItemStack(min, false);
+                     boolean flag = serverPlayer.getInventory().add(itemStack1);
+                     if (flag && itemStack1.isEmpty()) {
+-                        ItemEntity itemEntity = serverPlayer.drop(itemStack, false);
++                        ItemEntity itemEntity = serverPlayer.drop(itemStack, false, false, false); // CraftBukkit - SPIGOT-2942: Add boolean to call event
+                         if (itemEntity != null) {
+                             itemEntity.makeFakeItem();
+                         }
+@@ -95,11 +_,11 @@
+ 
+             if (targets.size() == 1) {
+                 source.sendSuccess(
+-                    () -> Component.translatable("commands.give.success.single", count, itemStack.getDisplayName(), targets.iterator().next().getDisplayName()),
++                    () -> Component.translatable("commands.give.success.single", count, displayName, targets.iterator().next().getDisplayName()), // Paper - use cached display name
+                     true
+                 );
+             } else {
+-                source.sendSuccess(() -> Component.translatable("commands.give.success.single", count, itemStack.getDisplayName(), targets.size()), true);
++                source.sendSuccess(() -> Component.translatable("commands.give.success.single", count, displayName, targets.size()), true); // Paper - use cached display name
+             }
+ 
+             return targets.size();
diff --git a/paper-server/patches/unapplied/net/minecraft/server/commands/KickCommand.java.patch b/paper-server/patches/sources/net/minecraft/server/commands/KickCommand.java.patch
similarity index 89%
rename from paper-server/patches/unapplied/net/minecraft/server/commands/KickCommand.java.patch
rename to paper-server/patches/sources/net/minecraft/server/commands/KickCommand.java.patch
index 8138965b1c..f76fe82415 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/commands/KickCommand.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/commands/KickCommand.java.patch
@@ -1,8 +1,8 @@
 --- a/net/minecraft/server/commands/KickCommand.java
 +++ b/net/minecraft/server/commands/KickCommand.java
-@@ -48,7 +48,7 @@
+@@ -48,7 +_,7 @@
  
-             for (ServerPlayer serverPlayer : targets) {
+             for (ServerPlayer serverPlayer : players) {
                  if (!source.getServer().isSingleplayerOwner(serverPlayer.getGameProfile())) {
 -                    serverPlayer.connection.disconnect(reason);
 +                    serverPlayer.connection.disconnect(reason, org.bukkit.event.player.PlayerKickEvent.Cause.KICK_COMMAND); // Paper - kick event cause
diff --git a/paper-server/patches/sources/net/minecraft/server/commands/ListPlayersCommand.java.patch b/paper-server/patches/sources/net/minecraft/server/commands/ListPlayersCommand.java.patch
new file mode 100644
index 0000000000..feda570539
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/commands/ListPlayersCommand.java.patch
@@ -0,0 +1,18 @@
+--- a/net/minecraft/server/commands/ListPlayersCommand.java
++++ b/net/minecraft/server/commands/ListPlayersCommand.java
+@@ -32,7 +_,14 @@
+ 
+     private static int format(CommandSourceStack source, Function<ServerPlayer, Component> nameExtractor) {
+         PlayerList playerList = source.getServer().getPlayerList();
+-        List<ServerPlayer> players = playerList.getPlayers();
++        // CraftBukkit start
++        List<ServerPlayer> playersTemp = playerList.getPlayers();
++        if (source.getBukkitSender() instanceof org.bukkit.entity.Player) {
++            org.bukkit.entity.Player sender = (org.bukkit.entity.Player) source.getBukkitSender();
++            playersTemp = playersTemp.stream().filter((ep) -> sender.canSee(ep.getBukkitEntity())).collect(java.util.stream.Collectors.toList());
++        }
++        final List<ServerPlayer> players = playersTemp;
++        // CraftBukkit end
+         Component component = ComponentUtils.formatList(players, nameExtractor);
+         source.sendSuccess(() -> Component.translatable("commands.list.players", players.size(), playerList.getMaxPlayers(), component), false);
+         return players.size();
diff --git a/paper-server/patches/unapplied/net/minecraft/server/commands/OpCommand.java.patch b/paper-server/patches/sources/net/minecraft/server/commands/OpCommand.java.patch
similarity index 83%
rename from paper-server/patches/unapplied/net/minecraft/server/commands/OpCommand.java.patch
rename to paper-server/patches/sources/net/minecraft/server/commands/OpCommand.java.patch
index 3ca43d3777..bb5525dd82 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/commands/OpCommand.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/commands/OpCommand.java.patch
@@ -1,10 +1,10 @@
 --- a/net/minecraft/server/commands/OpCommand.java
 +++ b/net/minecraft/server/commands/OpCommand.java
-@@ -46,7 +46,7 @@
+@@ -46,7 +_,7 @@
              if (!playerList.isOp(gameProfile)) {
                  playerList.op(gameProfile);
                  i++;
--                source.sendSuccess(() -> Component.translatable("commands.op.success", targets.iterator().next().getName()), true);
+-                source.sendSuccess(() -> Component.translatable("commands.op.success", gameProfiles.iterator().next().getName()), true);
 +                source.sendSuccess(() -> Component.translatable("commands.op.success", gameProfile.getName()), true); // Paper - fixes MC-253721
              }
          }
diff --git a/paper-server/patches/sources/net/minecraft/server/commands/PlaceCommand.java.patch b/paper-server/patches/sources/net/minecraft/server/commands/PlaceCommand.java.patch
new file mode 100644
index 0000000000..ec190202c4
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/commands/PlaceCommand.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/server/commands/PlaceCommand.java
++++ b/net/minecraft/server/commands/PlaceCommand.java
+@@ -280,6 +_,7 @@
+         if (!structureStart.isValid()) {
+             throw ERROR_STRUCTURE_FAILED.create();
+         } else {
++            structureStart.generationEventCause = org.bukkit.event.world.AsyncStructureGenerateEvent.Cause.COMMAND; // CraftBukkit - set AsyncStructureGenerateEvent.Cause.COMMAND as generation cause
+             BoundingBox boundingBox = structureStart.getBoundingBox();
+             ChunkPos chunkPos = new ChunkPos(SectionPos.blockToSectionCoord(boundingBox.minX()), SectionPos.blockToSectionCoord(boundingBox.minZ()));
+             ChunkPos chunkPos1 = new ChunkPos(SectionPos.blockToSectionCoord(boundingBox.maxX()), SectionPos.blockToSectionCoord(boundingBox.maxZ()));
diff --git a/paper-server/patches/sources/net/minecraft/server/commands/ReloadCommand.java.patch b/paper-server/patches/sources/net/minecraft/server/commands/ReloadCommand.java.patch
new file mode 100644
index 0000000000..47fc313fa8
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/commands/ReloadCommand.java.patch
@@ -0,0 +1,28 @@
+--- a/net/minecraft/server/commands/ReloadCommand.java
++++ b/net/minecraft/server/commands/ReloadCommand.java
+@@ -16,7 +_,7 @@
+     private static final Logger LOGGER = LogUtils.getLogger();
+ 
+     public static void reloadPacks(Collection<String> selectedIds, CommandSourceStack source) {
+-        source.getServer().reloadResources(selectedIds).exceptionally(throwable -> {
++        source.getServer().reloadResources(selectedIds, io.papermc.paper.event.server.ServerResourcesReloadedEvent.Cause.COMMAND).exceptionally(throwable -> { // Paper - Add ServerResourcesReloadedEvent
+             LOGGER.warn("Failed to execute reload", throwable);
+             source.sendFailure(Component.translatable("commands.reload.failure"));
+             return null;
+@@ -36,6 +_,16 @@
+ 
+         return list;
+     }
++
++    // CraftBukkit start
++    public static void reload(MinecraftServer server) {
++        PackRepository packRepository = server.getPackRepository();
++        WorldData worldData = server.getWorldData();
++        Collection<String> selectedIds = packRepository.getSelectedIds();
++        Collection<String> collection = discoverNewPacks(packRepository, worldData, selectedIds);
++        server.reloadResources(collection, io.papermc.paper.event.server.ServerResourcesReloadedEvent.Cause.PLUGIN); // Paper - Add ServerResourcesReloadedEvent
++    }
++    // CraftBukkit end
+ 
+     public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
+         dispatcher.register(Commands.literal("reload").requires(source -> source.hasPermission(2)).executes(context -> {
diff --git a/paper-server/patches/sources/net/minecraft/server/commands/ScheduleCommand.java.patch b/paper-server/patches/sources/net/minecraft/server/commands/ScheduleCommand.java.patch
new file mode 100644
index 0000000000..bd614fe2f3
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/commands/ScheduleCommand.java.patch
@@ -0,0 +1,29 @@
+--- a/net/minecraft/server/commands/ScheduleCommand.java
++++ b/net/minecraft/server/commands/ScheduleCommand.java
+@@ -32,7 +_,7 @@
+     );
+     private static final SimpleCommandExceptionType ERROR_MACRO = new SimpleCommandExceptionType(Component.translatableEscape("commands.schedule.macro"));
+     private static final SuggestionProvider<CommandSourceStack> SUGGEST_SCHEDULE = (context, builder) -> SharedSuggestionProvider.suggest(
+-        context.getSource().getServer().getWorldData().overworldData().getScheduledEvents().getEventsIds(), builder
++        context.getSource().getLevel().serverLevelData.getScheduledEvents().getEventsIds(), builder // Paper - Make schedule command per-world
+     );
+ 
+     public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
+@@ -101,7 +_,7 @@
+         } else {
+             long l = source.getLevel().getGameTime() + time;
+             ResourceLocation resourceLocation = function.getFirst();
+-            TimerQueue<MinecraftServer> scheduledEvents = source.getServer().getWorldData().overworldData().getScheduledEvents();
++            TimerQueue<MinecraftServer> scheduledEvents = source.getLevel().serverLevelData.overworldData().getScheduledEvents(); // CraftBukkit - SPIGOT-6667: Use world specific function timer
+             Optional<CommandFunction<CommandSourceStack>> optional = function.getSecond().left();
+             if (optional.isPresent()) {
+                 if (optional.get() instanceof MacroFunction) {
+@@ -132,7 +_,7 @@
+     }
+ 
+     private static int remove(CommandSourceStack source, String function) throws CommandSyntaxException {
+-        int i = source.getServer().getWorldData().overworldData().getScheduledEvents().remove(function);
++        int i = source.getLevel().serverLevelData.overworldData().getScheduledEvents().remove(function); // Paper - Make schedule command per-world
+         if (i == 0) {
+             throw ERROR_CANT_REMOVE.create(function);
+         } else {
diff --git a/paper-server/patches/sources/net/minecraft/server/commands/SetSpawnCommand.java.patch b/paper-server/patches/sources/net/minecraft/server/commands/SetSpawnCommand.java.patch
new file mode 100644
index 0000000000..a2a722311b
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/commands/SetSpawnCommand.java.patch
@@ -0,0 +1,43 @@
+--- a/net/minecraft/server/commands/SetSpawnCommand.java
++++ b/net/minecraft/server/commands/SetSpawnCommand.java
+@@ -66,24 +_,34 @@
+     private static int setSpawn(CommandSourceStack source, Collection<ServerPlayer> targets, BlockPos pos, float angle) {
+         ResourceKey<Level> resourceKey = source.getLevel().dimension();
+ 
++        final Collection<ServerPlayer> actualTargets = new java.util.ArrayList<>(); // Paper - Add PlayerSetSpawnEvent
+         for (ServerPlayer serverPlayer : targets) {
+-            serverPlayer.setRespawnPosition(resourceKey, pos, angle, true, false);
+-        }
++            // Paper start - Add PlayerSetSpawnEvent
++            if (serverPlayer.setRespawnPosition(resourceKey, pos, angle, true, false, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.COMMAND)) {
++                actualTargets.add(serverPlayer);
++            }
++            // Paper end - Add PlayerSetSpawnEvent
++        }
++        // Paper start - Add PlayerSetSpawnEvent
++        if (actualTargets.isEmpty()) {
++            return 0;
++        }
++        // Paper end - Add PlayerSetSpawnEvent
+ 
+         String string = resourceKey.location().toString();
+-        if (targets.size() == 1) {
++        if (actualTargets.size() == 1) { // Paper - Add PlayerSetSpawnEvent
+             source.sendSuccess(
+                 () -> Component.translatable(
+-                    "commands.spawnpoint.success.single", pos.getX(), pos.getY(), pos.getZ(), angle, string, targets.iterator().next().getDisplayName()
++                    "commands.spawnpoint.success.single", pos.getX(), pos.getY(), pos.getZ(), angle, string, actualTargets.iterator().next().getDisplayName() // Paper - Add PlayerSetSpawnEvent
+                 ),
+                 true
+             );
+         } else {
+             source.sendSuccess(
+-                () -> Component.translatable("commands.spawnpoint.success.multiple", pos.getX(), pos.getY(), pos.getZ(), angle, string, targets.size()), true
++                () -> Component.translatable("commands.spawnpoint.success.multiple", pos.getX(), pos.getY(), pos.getZ(), angle, string, actualTargets.size()), true // Paper - Add PlayerSetSpawnEvent
+             );
+         }
+ 
+-        return targets.size();
++        return actualTargets.size(); // Paper - Add PlayerSetSpawnEvent
+     }
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/commands/SetWorldSpawnCommand.java.patch b/paper-server/patches/sources/net/minecraft/server/commands/SetWorldSpawnCommand.java.patch
similarity index 59%
rename from paper-server/patches/unapplied/net/minecraft/server/commands/SetWorldSpawnCommand.java.patch
rename to paper-server/patches/sources/net/minecraft/server/commands/SetWorldSpawnCommand.java.patch
index 5e3b2e9493..c947d07f33 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/commands/SetWorldSpawnCommand.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/commands/SetWorldSpawnCommand.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/server/commands/SetWorldSpawnCommand.java
 +++ b/net/minecraft/server/commands/SetWorldSpawnCommand.java
-@@ -30,7 +30,7 @@
-     private static int setSpawn(CommandSourceStack source, BlockPos pos, float angle) {
-         ServerLevel worldserver = source.getLevel();
+@@ -33,7 +_,7 @@
  
--        if (worldserver.dimension() != Level.OVERWORLD) {
-+        if (false && worldserver.dimension() != Level.OVERWORLD) { // CraftBukkit - SPIGOT-7649: allow in all worlds
+     private static int setSpawn(CommandSourceStack source, BlockPos pos, float angle) {
+         ServerLevel level = source.getLevel();
+-        if (level.dimension() != Level.OVERWORLD) {
++        if (false && level.dimension() != Level.OVERWORLD) { // CraftBukkit - SPIGOT-7649: allow in all worlds
              source.sendFailure(Component.translatable("commands.setworldspawn.failure.not_overworld"));
              return 0;
          } else {
diff --git a/paper-server/patches/sources/net/minecraft/server/commands/SpreadPlayersCommand.java.patch b/paper-server/patches/sources/net/minecraft/server/commands/SpreadPlayersCommand.java.patch
new file mode 100644
index 0000000000..a76793b039
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/commands/SpreadPlayersCommand.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/server/commands/SpreadPlayersCommand.java
++++ b/net/minecraft/server/commands/SpreadPlayersCommand.java
+@@ -255,6 +_,7 @@
+                 entity.getYRot(),
+                 entity.getXRot(),
+                 true
++                , org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.COMMAND // CraftBukkit - handle teleport reason
+             );
+             double d1 = Double.MAX_VALUE;
+ 
diff --git a/paper-server/patches/sources/net/minecraft/server/commands/SummonCommand.java.patch b/paper-server/patches/sources/net/minecraft/server/commands/SummonCommand.java.patch
new file mode 100644
index 0000000000..e35d0b0328
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/commands/SummonCommand.java.patch
@@ -0,0 +1,19 @@
+--- a/net/minecraft/server/commands/SummonCommand.java
++++ b/net/minecraft/server/commands/SummonCommand.java
+@@ -82,6 +_,7 @@
+             ServerLevel level = source.getLevel();
+             Entity entity = EntityType.loadEntityRecursive(compoundTag, level, EntitySpawnReason.COMMAND, entity1 -> {
+                 entity1.moveTo(pos.x, pos.y, pos.z, entity1.getYRot(), entity1.getXRot());
++                entity1.spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.COMMAND; // Paper - Entity#getEntitySpawnReason
+                 return entity1;
+             });
+             if (entity == null) {
+@@ -92,7 +_,7 @@
+                         .finalizeSpawn(source.getLevel(), source.getLevel().getCurrentDifficultyAt(entity.blockPosition()), EntitySpawnReason.COMMAND, null);
+                 }
+ 
+-                if (!level.tryAddFreshEntityWithPassengers(entity)) {
++                if (!level.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.COMMAND)) { // CraftBukkit - pass a spawn reason of "COMMAND"
+                     throw ERROR_DUPLICATE_UUID.create();
+                 } else {
+                     return entity;
diff --git a/paper-server/patches/sources/net/minecraft/server/commands/TeleportCommand.java.patch b/paper-server/patches/sources/net/minecraft/server/commands/TeleportCommand.java.patch
new file mode 100644
index 0000000000..52f25acba1
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/commands/TeleportCommand.java.patch
@@ -0,0 +1,35 @@
+--- a/net/minecraft/server/commands/TeleportCommand.java
++++ b/net/minecraft/server/commands/TeleportCommand.java
+@@ -290,7 +_,31 @@
+             float f1 = relatives.contains(Relative.X_ROT) ? xRot - target.getXRot() : xRot;
+             float f2 = Mth.wrapDegrees(f);
+             float f3 = Mth.wrapDegrees(f1);
+-            if (target.teleportTo(level, d, d1, d2, relatives, f2, f3, true)) {
++            // CraftBukkit start - Teleport event
++            boolean result;
++            if (target instanceof final net.minecraft.server.level.ServerPlayer player) {
++                result = player.teleportTo(level, d, d1, d2, relatives, f2, f3, true, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.COMMAND);
++            } else {
++                org.bukkit.Location to = new org.bukkit.Location(level.getWorld(), d, d1, d2, f2, f3);
++                org.bukkit.event.entity.EntityTeleportEvent event = new org.bukkit.event.entity.EntityTeleportEvent(target.getBukkitEntity(), target.getBukkitEntity().getLocation(), to);
++                level.getCraftServer().getPluginManager().callEvent(event);
++                if (event.isCancelled() || event.getTo() == null) { // Paper
++                    return;
++                }
++                to = event.getTo(); // Paper - actually track new location
++
++                d = to.getX();
++                d1 = to.getY();
++                d2 = to.getZ();
++                f2 = to.getYaw();
++                f3 = to.getPitch();
++                level = ((org.bukkit.craftbukkit.CraftWorld) to.getWorld()).getHandle();
++
++                result = target.teleportTo(level, d, d1, d2, relatives, f2, f3, true);
++            }
++
++            if (result) {
++                // CraftBukkit end
+                 if (lookAt != null) {
+                     lookAt.perform(source, target);
+                 }
diff --git a/paper-server/patches/sources/net/minecraft/server/commands/TimeCommand.java.patch b/paper-server/patches/sources/net/minecraft/server/commands/TimeCommand.java.patch
new file mode 100644
index 0000000000..efc275401c
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/commands/TimeCommand.java.patch
@@ -0,0 +1,37 @@
+--- a/net/minecraft/server/commands/TimeCommand.java
++++ b/net/minecraft/server/commands/TimeCommand.java
+@@ -56,8 +_,15 @@
+     }
+ 
+     public static int setTime(CommandSourceStack source, int time) {
+-        for (ServerLevel serverLevel : source.getServer().getAllLevels()) {
+-            serverLevel.setDayTime(time);
++        for (ServerLevel serverLevel : io.papermc.paper.configuration.GlobalConfiguration.get().commands.timeCommandAffectsAllWorlds ? source.getServer().getAllLevels() : java.util.List.of(source.getLevel())) { // CraftBukkit - SPIGOT-6496: Only set the time for the world the command originates in // Paper - add config option for spigot's change
++            // serverLevel.setDayTime(time);
++            // CraftBukkit start
++            org.bukkit.event.world.TimeSkipEvent event = new org.bukkit.event.world.TimeSkipEvent(serverLevel.getWorld(), org.bukkit.event.world.TimeSkipEvent.SkipReason.COMMAND, time - serverLevel.getDayTime());
++            org.bukkit.Bukkit.getPluginManager().callEvent(event);
++            if (!event.isCancelled()) {
++                serverLevel.setDayTime(serverLevel.getDayTime() + event.getSkipAmount());
++            }
++            // CraftBukkit end
+         }
+ 
+         source.getServer().forceTimeSynchronization();
+@@ -66,8 +_,14 @@
+     }
+ 
+     public static int addTime(CommandSourceStack source, int amount) {
+-        for (ServerLevel serverLevel : source.getServer().getAllLevels()) {
+-            serverLevel.setDayTime(serverLevel.getDayTime() + amount);
++        for (ServerLevel serverLevel : io.papermc.paper.configuration.GlobalConfiguration.get().commands.timeCommandAffectsAllWorlds ? source.getServer().getAllLevels() : java.util.List.of(source.getLevel())) { // CraftBukkit - SPIGOT-6496: Only set the time for the world the command originates in // Paper - add config option for spigot's change
++            // CraftBukkit start
++            org.bukkit.event.world.TimeSkipEvent event = new org.bukkit.event.world.TimeSkipEvent(serverLevel.getWorld(), org.bukkit.event.world.TimeSkipEvent.SkipReason.COMMAND, amount);
++            org.bukkit.Bukkit.getPluginManager().callEvent(event);
++            if (!event.isCancelled()) {
++                serverLevel.setDayTime(serverLevel.getDayTime() + event.getSkipAmount());
++            }
++            // CraftBukkit end
+         }
+ 
+         source.getServer().forceTimeSynchronization();
diff --git a/paper-server/patches/sources/net/minecraft/server/commands/WeatherCommand.java.patch b/paper-server/patches/sources/net/minecraft/server/commands/WeatherCommand.java.patch
new file mode 100644
index 0000000000..d5d445aa1e
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/commands/WeatherCommand.java.patch
@@ -0,0 +1,30 @@
+--- a/net/minecraft/server/commands/WeatherCommand.java
++++ b/net/minecraft/server/commands/WeatherCommand.java
+@@ -44,23 +_,23 @@
+     }
+ 
+     private static int getDuration(CommandSourceStack source, int time, IntProvider timeProvider) {
+-        return time == -1 ? timeProvider.sample(source.getServer().overworld().getRandom()) : time;
++        return time == -1 ? timeProvider.sample(source.getLevel().getRandom()) : time; // CraftBukkit - SPIGOT-7680: per-world
+     }
+ 
+     private static int setClear(CommandSourceStack source, int time) {
+-        source.getServer().overworld().setWeatherParameters(getDuration(source, time, ServerLevel.RAIN_DELAY), 0, false, false);
++        source.getLevel().setWeatherParameters(getDuration(source, time, ServerLevel.RAIN_DELAY), 0, false, false); // CraftBukkit - SPIGOT-7680: per-world
+         source.sendSuccess(() -> Component.translatable("commands.weather.set.clear"), true);
+         return time;
+     }
+ 
+     private static int setRain(CommandSourceStack source, int time) {
+-        source.getServer().overworld().setWeatherParameters(0, getDuration(source, time, ServerLevel.RAIN_DURATION), true, false);
++        source.getLevel().setWeatherParameters(0, getDuration(source, time, ServerLevel.RAIN_DURATION), true, false); // CraftBukkit - SPIGOT-7680: per-world
+         source.sendSuccess(() -> Component.translatable("commands.weather.set.rain"), true);
+         return time;
+     }
+ 
+     private static int setThunder(CommandSourceStack source, int time) {
+-        source.getServer().overworld().setWeatherParameters(0, getDuration(source, time, ServerLevel.THUNDER_DURATION), true, true);
++        source.getLevel().setWeatherParameters(0, getDuration(source, time, ServerLevel.THUNDER_DURATION), true, true); // CraftBukkit - SPIGOT-7680: per-world
+         source.sendSuccess(() -> Component.translatable("commands.weather.set.thunder"), true);
+         return time;
+     }
diff --git a/paper-server/patches/sources/net/minecraft/server/commands/WorldBorderCommand.java.patch b/paper-server/patches/sources/net/minecraft/server/commands/WorldBorderCommand.java.patch
new file mode 100644
index 0000000000..26d5ded309
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/commands/WorldBorderCommand.java.patch
@@ -0,0 +1,63 @@
+--- a/net/minecraft/server/commands/WorldBorderCommand.java
++++ b/net/minecraft/server/commands/WorldBorderCommand.java
+@@ -135,7 +_,7 @@
+     }
+ 
+     private static int setDamageBuffer(CommandSourceStack source, float distance) throws CommandSyntaxException {
+-        WorldBorder worldBorder = source.getServer().overworld().getWorldBorder();
++        WorldBorder worldBorder = source.getLevel().getWorldBorder(); // CraftBukkit
+         if (worldBorder.getDamageSafeZone() == distance) {
+             throw ERROR_SAME_DAMAGE_BUFFER.create();
+         } else {
+@@ -146,7 +_,7 @@
+     }
+ 
+     private static int setDamageAmount(CommandSourceStack source, float damagePerBlock) throws CommandSyntaxException {
+-        WorldBorder worldBorder = source.getServer().overworld().getWorldBorder();
++        WorldBorder worldBorder = source.getLevel().getWorldBorder(); // CraftBukkit
+         if (worldBorder.getDamagePerBlock() == damagePerBlock) {
+             throw ERROR_SAME_DAMAGE_AMOUNT.create();
+         } else {
+@@ -159,7 +_,7 @@
+     }
+ 
+     private static int setWarningTime(CommandSourceStack source, int time) throws CommandSyntaxException {
+-        WorldBorder worldBorder = source.getServer().overworld().getWorldBorder();
++        WorldBorder worldBorder = source.getLevel().getWorldBorder(); // CraftBukkit
+         if (worldBorder.getWarningTime() == time) {
+             throw ERROR_SAME_WARNING_TIME.create();
+         } else {
+@@ -170,7 +_,7 @@
+     }
+ 
+     private static int setWarningDistance(CommandSourceStack source, int distance) throws CommandSyntaxException {
+-        WorldBorder worldBorder = source.getServer().overworld().getWorldBorder();
++        WorldBorder worldBorder = source.getLevel().getWorldBorder(); // CraftBukkit
+         if (worldBorder.getWarningBlocks() == distance) {
+             throw ERROR_SAME_WARNING_DISTANCE.create();
+         } else {
+@@ -181,13 +_,13 @@
+     }
+ 
+     private static int getSize(CommandSourceStack source) {
+-        double size = source.getServer().overworld().getWorldBorder().getSize();
++        double size = source.getLevel().getWorldBorder().getSize(); // CraftBukkit
+         source.sendSuccess(() -> Component.translatable("commands.worldborder.get", String.format(Locale.ROOT, "%.0f", size)), false);
+         return Mth.floor(size + 0.5);
+     }
+ 
+     private static int setCenter(CommandSourceStack source, Vec2 pos) throws CommandSyntaxException {
+-        WorldBorder worldBorder = source.getServer().overworld().getWorldBorder();
++        WorldBorder worldBorder = source.getLevel().getWorldBorder(); // CraftBukkit
+         if (worldBorder.getCenterX() == pos.x && worldBorder.getCenterZ() == pos.y) {
+             throw ERROR_SAME_CENTER.create();
+         } else if (!(Math.abs(pos.x) > 2.9999984E7) && !(Math.abs(pos.y) > 2.9999984E7)) {
+@@ -205,7 +_,7 @@
+     }
+ 
+     private static int setSize(CommandSourceStack source, double newSize, long time) throws CommandSyntaxException {
+-        WorldBorder worldBorder = source.getServer().overworld().getWorldBorder();
++        WorldBorder worldBorder = source.getLevel().getWorldBorder(); // CraftBukkit
+         double size = worldBorder.getSize();
+         if (size == newSize) {
+             throw ERROR_SAME_SIZE.create();
diff --git a/paper-server/patches/unapplied/net/minecraft/server/dedicated/DedicatedPlayerList.java.patch b/paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedPlayerList.java.patch
similarity index 62%
rename from paper-server/patches/unapplied/net/minecraft/server/dedicated/DedicatedPlayerList.java.patch
rename to paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedPlayerList.java.patch
index 17d45a61ee..758bc8ca8d 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/dedicated/DedicatedPlayerList.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedPlayerList.java.patch
@@ -1,9 +1,9 @@
 --- a/net/minecraft/server/dedicated/DedicatedPlayerList.java
 +++ b/net/minecraft/server/dedicated/DedicatedPlayerList.java
-@@ -18,6 +18,11 @@
-         this.setViewDistance(dedicatedServerProperties.viewDistance);
-         this.setSimulationDistance(dedicatedServerProperties.simulationDistance);
-         super.setUsingWhiteList(dedicatedServerProperties.whiteList.get());
+@@ -18,6 +_,11 @@
+         this.setViewDistance(properties.viewDistance);
+         this.setSimulationDistance(properties.simulationDistance);
+         super.setUsingWhiteList(properties.whiteList.get());
 +        // Paper start - fix converting txt to json file; moved from constructor
 +    }
 +    @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/server/dedicated/DedicatedServer.java.patch b/paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedServer.java.patch
similarity index 55%
rename from paper-server/patches/unapplied/net/minecraft/server/dedicated/DedicatedServer.java.patch
rename to paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedServer.java.patch
index 0780484bbb..a71879d2eb 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/dedicated/DedicatedServer.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedServer.java.patch
@@ -1,28 +1,6 @@
 --- a/net/minecraft/server/dedicated/DedicatedServer.java
 +++ b/net/minecraft/server/dedicated/DedicatedServer.java
-@@ -54,20 +54,31 @@
- import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.level.GameRules;
- import net.minecraft.world.level.GameType;
--import net.minecraft.world.level.Level;
- import net.minecraft.world.level.block.entity.SkullBlockEntity;
- import net.minecraft.world.level.storage.LevelStorageSource;
- import org.slf4j.Logger;
- 
-+// CraftBukkit start
-+import net.minecraft.server.WorldLoader;
-+import org.apache.logging.log4j.Level;
-+import org.apache.logging.log4j.LogManager;
-+import org.apache.logging.log4j.io.IoBuilder;
-+import org.bukkit.command.CommandSender;
-+import org.bukkit.craftbukkit.util.TerminalCompletionHandler;
-+import org.bukkit.craftbukkit.util.TerminalConsoleWriterThread;
-+import org.bukkit.event.server.ServerCommandEvent;
-+import org.bukkit.event.server.RemoteServerCommandEvent;
-+// CraftBukkit end
-+
- public class DedicatedServer extends MinecraftServer implements ServerInterface {
- 
+@@ -63,10 +_,10 @@
      static final Logger LOGGER = LogUtils.getLogger();
      private static final int CONVERSION_RETRY_DELAY_MS = 5000;
      private static final int CONVERSION_RETRIES = 2;
@@ -31,44 +9,42 @@
      @Nullable
      private QueryThreadGs4 queryThreadGs4;
 -    private final RconConsoleSource rconConsoleSource;
-+    // private final RemoteControlCommandListener rconConsoleSource; // CraftBukkit - remove field
++    // private final RconConsoleSource rconConsoleSource; // CraftBukkit - remove field
      @Nullable
      private RconThread rconThread;
      public DedicatedServerSettings settings;
-@@ -81,41 +92,117 @@
-     private DebugSampleSubscriptionTracker debugSampleSubscriptionTracker;
+@@ -81,6 +_,7 @@
      public ServerLinks serverLinks;
  
--    public DedicatedServer(Thread serverThread, LevelStorageSource.LevelStorageAccess session, PackRepository dataPackManager, WorldStem saveLoader, DedicatedServerSettings propertiesLoader, DataFixer dataFixer, Services apiServices, ChunkProgressListenerFactory worldGenerationProgressListenerFactory) {
--        super(serverThread, session, dataPackManager, saveLoader, Proxy.NO_PROXY, dataFixer, apiServices, worldGenerationProgressListenerFactory);
--        this.settings = propertiesLoader;
+     public DedicatedServer(
++        joptsimple.OptionSet options, net.minecraft.server.WorldLoader.DataLoadContext worldLoader, // CraftBukkit - Signature changed
+         Thread serverThread,
+         LevelStorageSource.LevelStorageAccess storageSource,
+         PackRepository packRepository,
+@@ -90,9 +_,9 @@
+         Services services,
+         ChunkProgressListenerFactory progressListenerFactory
+     ) {
+-        super(serverThread, storageSource, packRepository, worldStem, Proxy.NO_PROXY, fixerUpper, services, progressListenerFactory);
++        super(options, worldLoader, serverThread, storageSource, packRepository, worldStem, Proxy.NO_PROXY, fixerUpper, services, progressListenerFactory); // CraftBukkit - Signature changed
+         this.settings = settings;
 -        this.rconConsoleSource = new RconConsoleSource(this);
--        this.serverTextFilter = ServerTextFilter.createFromConfig(propertiesLoader.getProperties());
--        this.serverLinks = DedicatedServer.createServerLinks(propertiesLoader);
-+    // CraftBukkit start - Signature changed
-+    public DedicatedServer(joptsimple.OptionSet options, WorldLoader.DataLoadContext worldLoader, Thread thread, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PackRepository resourcepackrepository, WorldStem worldstem, DedicatedServerSettings dedicatedserversettings, DataFixer datafixer, Services services, ChunkProgressListenerFactory worldloadlistenerfactory) {
-+        super(options, worldLoader, thread, convertable_conversionsession, resourcepackrepository, worldstem, Proxy.NO_PROXY, datafixer, services, worldloadlistenerfactory);
-+        // CraftBukkit end
-+        this.settings = dedicatedserversettings;
-+        // this.rconConsoleSource = new RemoteControlCommandListener(this); // CraftBukkit - remove field
-+        this.serverTextFilter = ServerTextFilter.createFromConfig(dedicatedserversettings.getProperties());
-+        this.serverLinks = DedicatedServer.createServerLinks(dedicatedserversettings);
++        //this.rconConsoleSource = new RconConsoleSource(this); // CraftBukkit - remove field
+         this.serverTextFilter = ServerTextFilter.createFromConfig(settings.getProperties());
+         this.serverLinks = createServerLinks(settings);
      }
- 
-     @Override
-     public boolean initServer() throws IOException {
+@@ -102,26 +_,95 @@
          Thread thread = new Thread("Server console handler") {
+             @Override
              public void run() {
--                BufferedReader bufferedreader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
+-                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
+-
 +                // CraftBukkit start
-+                if (!org.bukkit.craftbukkit.Main.useConsole) {
-+                    return;
-+                }
++                if (!org.bukkit.craftbukkit.Main.useConsole) return;
 +                // Paper start - Use TerminalConsoleAppender
 +                new com.destroystokyo.paper.console.PaperConsole(DedicatedServer.this).start();
 +                /*
 +                jline.console.ConsoleReader bufferedreader = DedicatedServer.this.reader;
- 
 +                // MC-33041, SPIGOT-5538: if System.in is not valid due to javaw, then return
 +                try {
 +                    System.in.available();
@@ -76,22 +52,20 @@
 +                    return;
 +                }
 +                // CraftBukkit end
-+
-                 String s;
- 
+                 String string1;
                  try {
--                    while (!DedicatedServer.this.isStopped() && DedicatedServer.this.isRunning() && (s = bufferedreader.readLine()) != null) {
--                        DedicatedServer.this.handleConsoleInput(s, DedicatedServer.this.createCommandSourceStack());
+-                    while (!DedicatedServer.this.isStopped() && DedicatedServer.this.isRunning() && (string1 = bufferedReader.readLine()) != null) {
+-                        DedicatedServer.this.handleConsoleInput(string1, DedicatedServer.this.createCommandSourceStack());
 +                    // CraftBukkit start - JLine disabling compatibility
 +                    while (!DedicatedServer.this.isStopped() && DedicatedServer.this.isRunning()) {
 +                        if (org.bukkit.craftbukkit.Main.useJline) {
-+                            s = bufferedreader.readLine(">", null);
++                            string1 = bufferedreader.readLine(">", null);
 +                        } else {
-+                            s = bufferedreader.readLine();
++                            string1 = bufferedreader.readLine();
 +                        }
 +
 +                        // SPIGOT-5220: Throttle if EOF (ctrl^d) or stdin is /dev/null
-+                        if (s == null) {
++                        if (string1 == null) {
 +                            try {
 +                                Thread.sleep(50L);
 +                            } catch (InterruptedException ex) {
@@ -99,20 +73,18 @@
 +                            }
 +                            continue;
 +                        }
-+                        if (s.trim().length() > 0) { // Trim to filter lines which are just spaces
++                        if (string1.trim().length() > 0) { // Trim to filter lines which are just spaces
 +                            DedicatedServer.this.issueCommand(s, DedicatedServer.this.getServerCommandListener());
 +                        }
 +                        // CraftBukkit end
                      }
-                 } catch (IOException ioexception) {
-                     DedicatedServer.LOGGER.error("Exception handling console input", ioexception);
+                 } catch (IOException var4) {
+                     DedicatedServer.LOGGER.error("Exception handling console input", (Throwable)var4);
                  }
- 
 +                */
 +                // Paper end
              }
          };
- 
 +        // CraftBukkit start - TODO: handle command-line logging arguments
 +        java.util.logging.Logger global = java.util.logging.Logger.getLogger("");
 +        global.setUseParentHandlers(false);
@@ -122,7 +94,7 @@
 +        global.addHandler(new org.bukkit.craftbukkit.util.ForwardLogHandler());
 +
 +        // Paper start - Not needed with TerminalConsoleAppender
-+        final org.apache.logging.log4j.Logger logger = LogManager.getRootLogger();
++        final org.apache.logging.log4j.Logger logger = org.apache.logging.log4j.LogManager.getRootLogger();
 +        /*
 +        final org.apache.logging.log4j.core.Logger logger = ((org.apache.logging.log4j.core.Logger) LogManager.getRootLogger());
 +        for (org.apache.logging.log4j.core.Appender appender : logger.getAppenders().values()) {
@@ -137,36 +109,35 @@
 +        */
 +        // Paper end - Not needed with TerminalConsoleAppender
 +
-+        System.setOut(IoBuilder.forLogger(logger).setLevel(Level.INFO).buildPrintStream());
-+        System.setErr(IoBuilder.forLogger(logger).setLevel(Level.WARN).buildPrintStream());
++        System.setOut(org.apache.logging.log4j.io.IoBuilder.forLogger(logger).setLevel(org.apache.logging.log4j.Level.INFO).buildPrintStream());
++        System.setErr(org.apache.logging.log4j.io.IoBuilder.forLogger(logger).setLevel(org.apache.logging.log4j.Level.WARN).buildPrintStream());
 +        // CraftBukkit end
-+
          thread.setDaemon(true);
-         thread.setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandler(DedicatedServer.LOGGER));
+         thread.setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandler(LOGGER));
 -        thread.start();
 +        // thread.start(); // Paper - Enhance console tab completions for brigadier commands; moved down
-         DedicatedServer.LOGGER.info("Starting minecraft server version {}", SharedConstants.getCurrentVersion().getName());
+         LOGGER.info("Starting minecraft server version {}", SharedConstants.getCurrentVersion().getName());
          if (Runtime.getRuntime().maxMemory() / 1024L / 1024L < 512L) {
-             DedicatedServer.LOGGER.warn("To start the server with more ram, launch it as \"java -Xmx1024M -Xms1024M -jar minecraft_server.jar\"");
+             LOGGER.warn("To start the server with more ram, launch it as \"java -Xmx1024M -Xms1024M -jar minecraft_server.jar\"");
          }
  
 +        // Paper start - detect running as root
 +        if (io.papermc.paper.util.ServerEnvironment.userIsRootOrAdmin()) {
-+            DedicatedServer.LOGGER.warn("****************************");
-+            DedicatedServer.LOGGER.warn("YOU ARE RUNNING THIS SERVER AS AN ADMINISTRATIVE OR ROOT USER. THIS IS NOT ADVISED.");
-+            DedicatedServer.LOGGER.warn("YOU ARE OPENING YOURSELF UP TO POTENTIAL RISKS WHEN DOING THIS.");
-+            DedicatedServer.LOGGER.warn("FOR MORE INFORMATION, SEE https://madelinemiller.dev/blog/root-minecraft-server/");
-+            DedicatedServer.LOGGER.warn("****************************");
++            LOGGER.warn("****************************");
++            LOGGER.warn("YOU ARE RUNNING THIS SERVER AS AN ADMINISTRATIVE OR ROOT USER. THIS IS NOT ADVISED.");
++            LOGGER.warn("YOU ARE OPENING YOURSELF UP TO POTENTIAL RISKS WHEN DOING THIS.");
++            LOGGER.warn("FOR MORE INFORMATION, SEE https://madelinemiller.dev/blog/root-minecraft-server/");
++            LOGGER.warn("****************************");
 +        }
 +        // Paper end - detect running as root
 +
-         DedicatedServer.LOGGER.info("Loading properties");
-         DedicatedServerProperties dedicatedserverproperties = this.settings.getProperties();
- 
-@@ -126,14 +213,51 @@
-             this.setPreventProxyConnections(dedicatedserverproperties.preventProxyConnections);
-             this.setLocalIp(dedicatedserverproperties.serverIp);
+         LOGGER.info("Loading properties");
+         DedicatedServerProperties properties = this.settings.getProperties();
+         if (this.isSingleplayer()) {
+@@ -132,13 +_,51 @@
+             this.setLocalIp(properties.serverIp);
          }
+ 
 +        // Spigot start
 +        this.setPlayerList(new DedicatedPlayerList(this, this.registries(), this.playerDataStorage));
 +        org.spigotmc.SpigotConfig.init((java.io.File) this.options.valueOf("spigot-settings"));
@@ -190,51 +161,51 @@
 +        this.server.spark.registerCommandBeforePlugins(this.server); // Paper - spark
 +        com.destroystokyo.paper.Metrics.PaperMetrics.startMetrics(); // Paper - start metrics
 +        com.destroystokyo.paper.VersionHistoryManager.INSTANCE.getClass(); // Paper - load version history now
- 
-         this.setPvpAllowed(dedicatedserverproperties.pvp);
-         this.setFlightAllowed(dedicatedserverproperties.allowFlight);
-         this.setMotd(dedicatedserverproperties.motd);
-         super.setPlayerIdleTimeout((Integer) dedicatedserverproperties.playerIdleTimeout.get());
-         this.setEnforceWhitelist(dedicatedserverproperties.enforceWhitelist);
--        this.worldData.setGameType(dedicatedserverproperties.gamemode);
-+        // this.worldData.setGameType(dedicatedserverproperties.gamemode); // CraftBukkit - moved to world loading
-         DedicatedServer.LOGGER.info("Default game type: {}", dedicatedserverproperties.gamemode);
++
+         this.setPvpAllowed(properties.pvp);
+         this.setFlightAllowed(properties.allowFlight);
+         this.setMotd(properties.motd);
+         super.setPlayerIdleTimeout(properties.playerIdleTimeout.get());
+         this.setEnforceWhitelist(properties.enforceWhitelist);
+-        this.worldData.setGameType(properties.gamemode);
++        // this.worldData.setGameType(properties.gamemode); // CraftBukkit - moved to world loading
+         LOGGER.info("Default game type: {}", properties.gamemode);
 +        // Paper start - Unix domain socket support
 +        java.net.SocketAddress bindAddress;
 +        if (this.getLocalIp().startsWith("unix:")) {
 +            if (!io.netty.channel.epoll.Epoll.isAvailable()) {
-+                DedicatedServer.LOGGER.error("**** INVALID CONFIGURATION!");
-+                DedicatedServer.LOGGER.error("You are trying to use a Unix domain socket but you're not on a supported OS.");
++                LOGGER.error("**** INVALID CONFIGURATION!");
++                LOGGER.error("You are trying to use a Unix domain socket but you're not on a supported OS.");
 +                return false;
 +            } else if (!io.papermc.paper.configuration.GlobalConfiguration.get().proxies.velocity.enabled && !org.spigotmc.SpigotConfig.bungee) {
-+                DedicatedServer.LOGGER.error("**** INVALID CONFIGURATION!");
-+                DedicatedServer.LOGGER.error("Unix domain sockets require IPs to be forwarded from a proxy.");
++                LOGGER.error("**** INVALID CONFIGURATION!");
++                LOGGER.error("Unix domain sockets require IPs to be forwarded from a proxy.");
 +                return false;
 +            }
 +            bindAddress = new io.netty.channel.unix.DomainSocketAddress(this.getLocalIp().substring("unix:".length()));
 +        } else {
-         InetAddress inetaddress = null;
- 
+         InetAddress inetAddress = null;
          if (!this.getLocalIp().isEmpty()) {
-@@ -143,34 +267,55 @@
+             inetAddress = InetAddress.getByName(this.getLocalIp());
+@@ -147,36 +_,62 @@
          if (this.getPort() < 0) {
-             this.setPort(dedicatedserverproperties.serverPort);
+             this.setPort(properties.serverPort);
          }
-+        bindAddress = new java.net.InetSocketAddress(inetaddress, this.getPort());
++        bindAddress = new java.net.InetSocketAddress(inetAddress, this.getPort());
 +        }
 +        // Paper end - Unix domain socket support
  
          this.initializeKeyPair();
-         DedicatedServer.LOGGER.info("Starting Minecraft server on {}:{}", this.getLocalIp().isEmpty() ? "*" : this.getLocalIp(), this.getPort());
+         LOGGER.info("Starting Minecraft server on {}:{}", this.getLocalIp().isEmpty() ? "*" : this.getLocalIp(), this.getPort());
  
          try {
--            this.getConnection().startTcpServerListener(inetaddress, this.getPort());
-+            this.getConnection().bind(bindAddress); // Paper - Unix domain socket support
-         } catch (IOException ioexception) {
-             DedicatedServer.LOGGER.warn("**** FAILED TO BIND TO PORT!");
-             DedicatedServer.LOGGER.warn("The exception was: {}", ioexception.toString());
-             DedicatedServer.LOGGER.warn("Perhaps a server is already running on that port?");
-+            if (true) throw new IllegalStateException("Failed to bind to port", ioexception); // Paper - Propagate failed to bind to port error
+-            this.getConnection().startTcpServerListener(inetAddress, this.getPort());
++            this.getConnection().startTcpServerListener(bindAddress); // Paper - Unix domain socket support
+         } catch (IOException var10) {
+             LOGGER.warn("**** FAILED TO BIND TO PORT!");
+             LOGGER.warn("The exception was: {}", var10.toString());
+             LOGGER.warn("Perhaps a server is already running on that port?");
++            if (true) throw new IllegalStateException("Failed to bind to port", var10); // Paper - Propagate failed to bind to port error
              return false;
          }
  
@@ -250,25 +221,31 @@
 +        String proxyLink = (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.velocity.enabled) ? "https://docs.papermc.io/velocity/security" : "http://www.spigotmc.org/wiki/firewall-guide/";
 +        // Paper end - Add Velocity IP Forwarding Support
          if (!this.usesAuthentication()) {
-             DedicatedServer.LOGGER.warn("**** SERVER IS RUNNING IN OFFLINE/INSECURE MODE!");
-             DedicatedServer.LOGGER.warn("The server will make no attempt to authenticate usernames. Beware.");
--            DedicatedServer.LOGGER.warn("While this makes the game possible to play without internet access, it also opens up the ability for hackers to connect with any username they choose.");
+             LOGGER.warn("**** SERVER IS RUNNING IN OFFLINE/INSECURE MODE!");
+             LOGGER.warn("The server will make no attempt to authenticate usernames. Beware.");
+-            LOGGER.warn(
+-                "While this makes the game possible to play without internet access, it also opens up the ability for hackers to connect with any username they choose."
+-            );
 +            // Spigot start
 +            // Paper start - Add Velocity IP Forwarding Support
 +            if (usingProxy) {
-+                DedicatedServer.LOGGER.warn("Whilst this makes it possible to use " + proxyFlavor + ", unless access to your server is properly restricted, it also opens up the ability for hackers to connect with any username they choose.");
-+                DedicatedServer.LOGGER.warn("Please see " + proxyLink + " for further information.");
-+            // Paper end - Add Velocity IP Forwarding Support
++                LOGGER.warn("Whilst this makes it possible to use {}, unless access to your server is properly restricted, it also opens up the ability for hackers to connect with any username they choose.", proxyFlavor);
++                LOGGER.warn("Please see {} for further information.", proxyLink);
++                // Paper end - Add Velocity IP Forwarding Support
 +            } else {
-+                DedicatedServer.LOGGER.warn("While this makes the game possible to play without internet access, it also opens up the ability for hackers to connect with any username they choose.");
++                LOGGER.warn("While this makes the game possible to play without internet access, it also opens up the ability for hackers to connect with any username they choose.");
 +            }
 +            // Spigot end
-             DedicatedServer.LOGGER.warn("To change this, set \"online-mode\" to \"true\" in the server.properties file.");
+             LOGGER.warn("To change this, set \"online-mode\" to \"true\" in the server.properties file.");
          }
  
--        if (this.convertOldUsers()) {
--            this.getProfileCache().save();
--        }
++        // CraftBukkit start
++        /*
+         if (this.convertOldUsers()) {
+             this.getProfileCache().save();
+         }
++        */
++        // CraftBukkit end
  
          if (!OldUsersConverter.serverReadyAfterUserconversion(this)) {
              return false;
@@ -276,34 +253,34 @@
 -            this.setPlayerList(new DedicatedPlayerList(this, this.registries(), this.playerDataStorage));
 +            // this.setPlayerList(new DedicatedPlayerList(this, this.registries(), this.playerDataStorage)); // CraftBukkit - moved up
              this.debugSampleSubscriptionTracker = new DebugSampleSubscriptionTracker(this.getPlayerList());
-             this.tickTimeLogger = new RemoteSampleLogger(TpsDebugDimensions.values().length, this.debugSampleSubscriptionTracker, RemoteDebugSampleType.TICK_TIME);
-             long i = Util.getNanos();
-@@ -178,13 +323,13 @@
+             this.tickTimeLogger = new RemoteSampleLogger(
+                 TpsDebugDimensions.values().length, this.debugSampleSubscriptionTracker, RemoteDebugSampleType.TICK_TIME
+@@ -185,12 +_,12 @@
              SkullBlockEntity.setup(this.services, this);
              GameProfileCache.setUsesAuthentication(this.usesAuthentication());
-             DedicatedServer.LOGGER.info("Preparing level \"{}\"", this.getLevelIdName());
+             LOGGER.info("Preparing level \"{}\"", this.getLevelIdName());
 -            this.loadLevel();
 +            this.loadLevel(this.storageSource.getLevelId()); // CraftBukkit
-             long j = Util.getNanos() - i;
-             String s = String.format(Locale.ROOT, "%.3fs", (double) j / 1.0E9D);
- 
-             DedicatedServer.LOGGER.info("Done ({})! For help, type \"help\"", s);
-             if (dedicatedserverproperties.announcePlayerAchievements != null) {
--                ((GameRules.BooleanValue) this.getGameRules().getRule(GameRules.RULE_ANNOUNCE_ADVANCEMENTS)).set(dedicatedserverproperties.announcePlayerAchievements, this);
-+                ((GameRules.BooleanValue) this.getGameRules().getRule(GameRules.RULE_ANNOUNCE_ADVANCEMENTS)).set(dedicatedserverproperties.announcePlayerAchievements, this.overworld()); // CraftBukkit - per-world
+             long l = Util.getNanos() - nanos;
+             String string = String.format(Locale.ROOT, "%.3fs", l / 1.0E9);
+-            LOGGER.info("Done ({})! For help, type \"help\"", string);
++            LOGGER.info("Done preparing level \"{}\" ({})", this.getLevelIdName(), string); // Paper - Improve startup message, add total time
+             if (properties.announcePlayerAchievements != null) {
+-                this.getGameRules().getRule(GameRules.RULE_ANNOUNCE_ADVANCEMENTS).set(properties.announcePlayerAchievements, this);
++                this.getGameRules().getRule(GameRules.RULE_ANNOUNCE_ADVANCEMENTS).set(properties.announcePlayerAchievements, this.overworld()); // CraftBukkit - per-world
              }
  
-             if (dedicatedserverproperties.enableQuery) {
-@@ -197,7 +342,7 @@
+             if (properties.enableQuery) {
+@@ -203,7 +_,7 @@
                  this.rconThread = RconThread.create(this);
              }
  
 -            if (this.getMaxTickLength() > 0L) {
-+            if (false && this.getMaxTickLength() > 0L) {  // Spigot - disable
++            if (false && this.getMaxTickLength() > 0L) { // Spigot - disable
                  Thread thread1 = new Thread(new ServerWatchdog(this));
- 
-                 thread1.setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandlerWithName(DedicatedServer.LOGGER));
-@@ -215,6 +360,12 @@
+                 thread1.setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandlerWithName(LOGGER));
+                 thread1.setName("Server Watchdog");
+@@ -220,6 +_,12 @@
          }
      }
  
@@ -316,7 +293,7 @@
      @Override
      public boolean isSpawningMonsters() {
          return this.settings.getProperties().spawnMonsters && super.isSpawningMonsters();
-@@ -227,7 +378,7 @@
+@@ -232,7 +_,7 @@
  
      @Override
      public void forceDifficulty() {
@@ -325,7 +302,7 @@
      }
  
      @Override
-@@ -286,13 +437,14 @@
+@@ -271,12 +_,15 @@
          }
  
          if (this.rconThread != null) {
@@ -337,37 +314,30 @@
 -            this.queryThreadGs4.stop();
 +            // this.remoteStatusListener.stop(); // Paper - don't wait for remote connections
          }
- 
-+        System.exit(0); // CraftBukkit
++
++        this.hasFullyShutdown = true; // Paper - Improved watchdog support
++        System.exit(this.abnormalExit ? 70 : 0); // CraftBukkit // Paper - Improved watchdog support
      }
  
      @Override
-@@ -302,19 +454,29 @@
+@@ -291,13 +_,23 @@
      }
  
-     @Override
--    public boolean isLevelEnabled(Level world) {
--        return world.dimension() == Level.NETHER ? this.getProperties().allowNether : true;
-+    public boolean isLevelEnabled(net.minecraft.world.level.Level world) {
-+        return world.dimension() == net.minecraft.world.level.Level.NETHER ? this.getProperties().allowNether : true;
-     }
- 
-     public void handleConsoleInput(String command, CommandSourceStack commandSource) {
--        this.consoleInput.add(new ConsoleInput(command, commandSource));
-+        this.serverCommandQueue.add(new ConsoleInput(command, commandSource)); // Paper - Perf: use proper queue
+     public void handleConsoleInput(String msg, CommandSourceStack source) {
+-        this.consoleInput.add(new ConsoleInput(msg, source));
++        this.serverCommandQueue.add(new ConsoleInput(msg, source)); // Paper - Perf: use proper queue
      }
  
      public void handleConsoleInputs() {
 -        while (!this.consoleInput.isEmpty()) {
--            ConsoleInput servercommand = (ConsoleInput) this.consoleInput.remove(0);
+-            ConsoleInput consoleInput = this.consoleInput.remove(0);
+-            this.getCommands().performPrefixedCommand(consoleInput.source, consoleInput.msg);
 +        // Paper start - Perf: use proper queue
 +        ConsoleInput servercommand;
 +        while ((servercommand = this.serverCommandQueue.poll()) != null) {
 +            // Paper end - Perf: use proper queue
- 
--            this.getCommands().performPrefixedCommand(servercommand.source, servercommand.msg);
 +            // CraftBukkit start - ServerCommand for preprocessing
-+            ServerCommandEvent event = new ServerCommandEvent(this.console, servercommand.msg);
++            org.bukkit.event.server.ServerCommandEvent event = new org.bukkit.event.server.ServerCommandEvent(this.console, servercommand.msg);
 +            this.server.getPluginManager().callEvent(event);
 +            if (event.isCancelled()) continue;
 +            servercommand = new ConsoleInput(event.getCommand(), servercommand.source);
@@ -376,31 +346,22 @@
 +            this.server.dispatchServerCommand(this.console, servercommand);
 +            // CraftBukkit end
          }
- 
      }
-@@ -383,7 +545,7 @@
  
+@@ -430,7 +_,11 @@
      @Override
-     public boolean isUnderSpawnProtection(ServerLevel world, BlockPos pos, Player player) {
--        if (world.dimension() != Level.OVERWORLD) {
-+        if (world.dimension() != net.minecraft.world.level.Level.OVERWORLD) {
-             return false;
-         } else if (this.getPlayerList().getOps().isEmpty()) {
-             return false;
-@@ -453,7 +615,11 @@
      public boolean enforceSecureProfile() {
-         DedicatedServerProperties dedicatedserverproperties = this.getProperties();
- 
--        return dedicatedserverproperties.enforceSecureProfile && dedicatedserverproperties.onlineMode && this.services.canValidateProfileKeys();
+         DedicatedServerProperties properties = this.getProperties();
+-        return properties.enforceSecureProfile && properties.onlineMode && this.services.canValidateProfileKeys();
 +        // Paper start - Add setting for proxy online mode status
-+        return dedicatedserverproperties.enforceSecureProfile
++        return properties.enforceSecureProfile
 +            && io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode()
 +            && this.services.canValidateProfileKeys();
 +        // Paper end - Add setting for proxy online mode status
      }
  
      @Override
-@@ -541,16 +707,52 @@
+@@ -515,14 +_,52 @@
  
      @Override
      public String getPluginNames() {
@@ -434,30 +395,39 @@
      @Override
      public String runCommand(String command) {
 -        this.rconConsoleSource.prepareForCommand();
+-        this.executeBlocking(() -> this.getCommands().performPrefixedCommand(this.rconConsoleSource.createCommandSourceStack(), command));
+-        return this.rconConsoleSource.getCommandResponse();
 +        // CraftBukkit start - fire RemoteServerCommandEvent
 +        throw new UnsupportedOperationException("Not supported - remote source required.");
 +    }
 +
 +    public String runCommand(RconConsoleSource rconConsoleSource, String s) {
 +        rconConsoleSource.prepareForCommand();
-         this.executeBlocking(() -> {
--            this.getCommands().performPrefixedCommand(this.rconConsoleSource.createCommandSourceStack(), command);
++        this.executeBlocking(() -> {
 +            CommandSourceStack wrapper = rconConsoleSource.createCommandSourceStack();
-+            RemoteServerCommandEvent event = new RemoteServerCommandEvent(rconConsoleSource.getBukkitSender(wrapper), s);
++            org.bukkit.event.server.RemoteServerCommandEvent event = new org.bukkit.event.server.RemoteServerCommandEvent(rconConsoleSource.getBukkitSender(wrapper), s);
 +            this.server.getPluginManager().callEvent(event);
 +            if (event.isCancelled()) {
 +                return;
 +            }
 +            ConsoleInput serverCommand = new ConsoleInput(event.getCommand(), wrapper);
 +            this.server.dispatchServerCommand(event.getSender(), serverCommand);
-         });
--        return this.rconConsoleSource.getCommandResponse();
++        });
 +        return rconConsoleSource.getCommandResponse();
 +        // CraftBukkit end
      }
  
-     public void storeUsingWhiteList(boolean useWhitelist) {
-@@ -660,4 +862,15 @@
+     public void storeUsingWhiteList(boolean isStoreUsingWhiteList) {
+@@ -532,7 +_,7 @@
+     @Override
+     public void stopServer() {
+         super.stopServer();
+-        Util.shutdownExecutors();
++        //Util.shutdownExecutors(); // Paper - Improved watchdog support; moved into super
+         SkullBlockEntity.clear();
+     }
+ 
+@@ -626,4 +_,15 @@
              }
          }
      }
@@ -468,7 +438,7 @@
 +    }
 +
 +    @Override
-+    public CommandSender getBukkitSender(CommandSourceStack wrapper) {
++    public org.bukkit.command.CommandSender getBukkitSender(CommandSourceStack wrapper) {
 +        return this.console;
 +    }
 +    // CraftBukkit end
diff --git a/paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedServerProperties.java.patch b/paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedServerProperties.java.patch
new file mode 100644
index 0000000000..9e44f8227b
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedServerProperties.java.patch
@@ -0,0 +1,65 @@
+--- a/net/minecraft/server/dedicated/DedicatedServerProperties.java
++++ b/net/minecraft/server/dedicated/DedicatedServerProperties.java
+@@ -45,6 +_,7 @@
+     static final Logger LOGGER = LogUtils.getLogger();
+     private static final Pattern SHA1 = Pattern.compile("^[a-fA-F0-9]{40}$");
+     private static final Splitter COMMA_SPLITTER = Splitter.on(',').trimResults();
++    public final boolean debug = this.get("debug", false); // CraftBukkit
+     public final boolean onlineMode = this.get("online-mode", true);
+     public final boolean preventProxyConnections = this.get("prevent-proxy-connections", false);
+     public final String serverIp = this.get("server-ip", "");
+@@ -85,7 +_,7 @@
+     public final boolean broadcastRconToOps = this.get("broadcast-rcon-to-ops", true);
+     public final boolean broadcastConsoleToOps = this.get("broadcast-console-to-ops", true);
+     public final int maxWorldSize = this.get("max-world-size", property -> Mth.clamp(property, 1, 29999984), 29999984);
+-    public final boolean syncChunkWrites = this.get("sync-chunk-writes", true);
++    public final boolean syncChunkWrites = this.get("sync-chunk-writes", true) && Boolean.getBoolean("Paper.enable-sync-chunk-writes"); // Paper - Hide sync chunk writes behind flag
+     public final String regionFileComression = this.get("region-file-compression", "deflate");
+     public final boolean enableJmxMonitoring = this.get("enable-jmx-monitoring", false);
+     public final boolean enableStatus = this.get("enable-status", true);
+@@ -99,13 +_,16 @@
+     public final Settings<DedicatedServerProperties>.MutableValue<Boolean> whiteList = this.getMutable("white-list", false);
+     public final boolean enforceSecureProfile = this.get("enforce-secure-profile", true);
+     public final boolean logIPs = this.get("log-ips", true);
+-    public int pauseWhenEmptySeconds = this.get("pause-when-empty-seconds", 60);
++    public int pauseWhenEmptySeconds = this.get("pause-when-empty-seconds", -1); // Paper - disable tick sleeping by default 
+     private final DedicatedServerProperties.WorldDimensionData worldDimensionData;
+     public final WorldOptions worldOptions;
+     public boolean acceptsTransfers = this.get("accepts-transfers", false);
++    public final String rconIp; // Paper - Configurable rcon ip
+ 
+-    public DedicatedServerProperties(Properties properties) {
+-        super(properties);
++    // CraftBukkit start
++    public DedicatedServerProperties(Properties properties, joptsimple.OptionSet optionset) {
++        super(properties, optionset);
++        // CraftBukkit end
+         String string = this.get("level-seed", "");
+         boolean flag = this.get("generate-structures", true);
+         long l = WorldOptions.parseSeed(string).orElse(WorldOptions.randomSeed());
+@@ -126,15 +_,21 @@
+             this.get("initial-enabled-packs", String.join(",", WorldDataConfiguration.DEFAULT.dataPacks().getEnabled())),
+             this.get("initial-disabled-packs", String.join(",", WorldDataConfiguration.DEFAULT.dataPacks().getDisabled()))
+         );
++        // Paper start - Configurable rcon ip
++        final String rconIp = this.getStringRaw("rcon.ip");
++        this.rconIp = rconIp == null ? this.serverIp : rconIp;
++        // Paper end - Configurable rcon ip
+     }
+ 
+-    public static DedicatedServerProperties fromFile(Path path) {
+-        return new DedicatedServerProperties(loadFromFile(path));
++    // CraftBukkit start
++    public static DedicatedServerProperties fromFile(Path path, joptsimple.OptionSet optionset) {
++        return new DedicatedServerProperties(loadFromFile(path), optionset);
+     }
+ 
+     @Override
+-    protected DedicatedServerProperties reload(RegistryAccess registryAccess, Properties properties) {
+-        return new DedicatedServerProperties(properties);
++    public DedicatedServerProperties reload(RegistryAccess registryAccess, Properties properties, joptsimple.OptionSet options) {
++        return new DedicatedServerProperties(properties, options);
++        // CraftBukkit end
+     }
+ 
+     @Nullable
diff --git a/paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedServerSettings.java.patch b/paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedServerSettings.java.patch
new file mode 100644
index 0000000000..acd4a0443e
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedServerSettings.java.patch
@@ -0,0 +1,17 @@
+--- a/net/minecraft/server/dedicated/DedicatedServerSettings.java
++++ b/net/minecraft/server/dedicated/DedicatedServerSettings.java
+@@ -7,9 +_,11 @@
+     private final Path source;
+     private DedicatedServerProperties properties;
+ 
+-    public DedicatedServerSettings(Path source) {
+-        this.source = source;
+-        this.properties = DedicatedServerProperties.fromFile(source);
++    // CraftBukkit start
++    public DedicatedServerSettings(joptsimple.OptionSet optionset) {
++        this.source = ((java.io.File) optionset.valueOf("config")).toPath();
++        this.properties = DedicatedServerProperties.fromFile(this.source, optionset);
++        // CraftBukkit end
+     }
+ 
+     public DedicatedServerProperties getProperties() {
diff --git a/paper-server/patches/sources/net/minecraft/server/dedicated/Settings.java.patch b/paper-server/patches/sources/net/minecraft/server/dedicated/Settings.java.patch
new file mode 100644
index 0000000000..b690197c4c
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/dedicated/Settings.java.patch
@@ -0,0 +1,128 @@
+--- a/net/minecraft/server/dedicated/Settings.java
++++ b/net/minecraft/server/dedicated/Settings.java
+@@ -26,12 +_,26 @@
+ public abstract class Settings<T extends Settings<T>> {
+     private static final Logger LOGGER = LogUtils.getLogger();
+     public final Properties properties;
++    private static final boolean skipComments = Boolean.getBoolean("Paper.skipServerPropertiesComments"); // Paper - allow skipping server.properties comments
++    // CraftBukkit start
++    private joptsimple.OptionSet options = null;
+ 
+-    public Settings(Properties properties) {
++    public Settings(Properties properties, final joptsimple.OptionSet options) {
+         this.properties = properties;
++        this.options = options;
++    }
++
++    private String getOverride(String name, String value) {
++        if ((this.options != null) && (this.options.has(name))) {
++            return String.valueOf(this.options.valueOf(name));
++        }
++
++        return value;
++        // CraftBukkit end
+     }
+ 
+     public static Properties loadFromFile(Path path) {
++        if (!Files.exists(path)) return new Properties(); // CraftBukkit - SPIGOT-7465, MC-264979: Don't load if file doesn't exist
+         try {
+             try {
+                 Properties var13;
+@@ -65,7 +_,53 @@
+     }
+ 
+     public void store(Path path) {
+-        try (Writer bufferedWriter = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
++        try /*(Writer bufferedWriter = Files.newBufferedWriter(path, StandardCharsets.UTF_8))*/ { // Paper
++            // CraftBukkit start - Don't attempt writing to file if it's read only
++            if (path.toFile().exists() && !path.toFile().canWrite()) {
++                Settings.LOGGER.warn("Can not write to file {}, skipping.", path); // Paper - log message file is read-only
++                return;
++            }
++            // CraftBukkit end
++            // Paper start - allow skipping server.properties comments
++            java.io.OutputStream outputstream = Files.newOutputStream(path);
++            java.io.BufferedOutputStream bufferedOutputStream = !skipComments ? new java.io.BufferedOutputStream(outputstream) : new java.io.BufferedOutputStream(outputstream) {
++                private boolean isRightAfterNewline = true; // If last written char was newline
++                private boolean isComment = false; // Are we writing comment currently?
++
++                @Override
++                public void write(@org.jetbrains.annotations.NotNull byte[] b) throws IOException {
++                    this.write(b, 0, b.length);
++                }
++
++                @Override
++                public void write(@org.jetbrains.annotations.NotNull byte[] bbuf, int off, int len) throws IOException {
++                    int latest_offset = off; // The latest offset, updated when comment ends
++                    for (int index = off; index < off + len; ++index ) {
++                        byte c = bbuf[index];
++                        boolean isNewline = (c == '\n' || c == '\r');
++                        if (isNewline && this.isComment) {
++                            // Comment has ended
++                            this.isComment = false;
++                            latest_offset = index+1;
++                        }
++                        if (c == '#' && this.isRightAfterNewline) {
++                            this.isComment = true;
++                            if (index != latest_offset) {
++                                // We got some non-comment data earlier
++                                super.write(bbuf, latest_offset, index-latest_offset);
++                            }
++                        }
++                        this.isRightAfterNewline = isNewline; // Store for next iteration
++
++                    }
++                    if (latest_offset < off+len && !this.isComment) {
++                        // We have some unwritten data, that isn't part of a comment
++                        super.write(bbuf, latest_offset, (off + len) - latest_offset);
++                    }
++                }
++            };
++            java.io.BufferedWriter bufferedWriter = new java.io.BufferedWriter(new java.io.OutputStreamWriter(bufferedOutputStream, java.nio.charset.StandardCharsets.UTF_8.newEncoder()));
++            // Paper end - allow skipping server.properties comments
+             this.properties.store(bufferedWriter, "Minecraft server properties");
+         } catch (IOException var7) {
+             LOGGER.error("Failed to store properties to file: {}", path);
+@@ -94,7 +_,7 @@
+ 
+     @Nullable
+     public String getStringRaw(String key) {
+-        return (String)this.properties.get(key);
++        return (String)this.getOverride(key, this.properties.getProperty(key)); // CraftBukkit
+     }
+ 
+     @Nullable
+@@ -109,6 +_,15 @@
+     }
+ 
+     protected <V> V get(String key, Function<String, V> serializer, Function<V, String> deserializer, V defaultValue) {
++        // CraftBukkit start
++        try {
++            return this.get0(key, serializer, deserializer, defaultValue);
++        } catch (Exception ex) {
++            throw new RuntimeException("Could not load invalidly configured property '" + key + "'", ex);
++        }
++    }
++    private <V> V get0(String key, Function<String, V> serializer, Function<V, String> deserializer, V defaultValue) {
++        // CraftBukkit end
+         String stringRaw = this.getStringRaw(key);
+         V object = MoreObjects.firstNonNull(stringRaw != null ? serializer.apply(stringRaw) : null, defaultValue);
+         this.properties.put(key, deserializer.apply(object));
+@@ -181,7 +_,7 @@
+         return map;
+     }
+ 
+-    protected abstract T reload(RegistryAccess registryAccess, Properties properties);
++    protected abstract T reload(RegistryAccess registryAccess, Properties properties, joptsimple.OptionSet optionset); // CraftBukkit
+ 
+     public class MutableValue<V> implements Supplier<V> {
+         private final String key;
+@@ -202,7 +_,7 @@
+         public T update(RegistryAccess registryAccess, V newValue) {
+             Properties map = Settings.this.cloneProperties();
+             map.put(this.key, this.serializer.apply(newValue));
+-            return Settings.this.reload(registryAccess, map);
++            return Settings.this.reload(registryAccess, properties, Settings.this.options); // CraftBukkit
+         }
+     }
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/gui/MinecraftServerGui.java.patch b/paper-server/patches/sources/net/minecraft/server/gui/MinecraftServerGui.java.patch
similarity index 56%
rename from paper-server/patches/unapplied/net/minecraft/server/gui/MinecraftServerGui.java.patch
rename to paper-server/patches/sources/net/minecraft/server/gui/MinecraftServerGui.java.patch
index 84332717d5..d78cde0d2c 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/gui/MinecraftServerGui.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/gui/MinecraftServerGui.java.patch
@@ -1,44 +1,58 @@
 --- a/net/minecraft/server/gui/MinecraftServerGui.java
 +++ b/net/minecraft/server/gui/MinecraftServerGui.java
-@@ -59,6 +59,15 @@
-         jframe.pack();
-         jframe.setLocationRelativeTo((Component) null);
-         jframe.setVisible(true);
-+        jframe.setName("Minecraft server"); // Paper - Improve ServerGUI
-+
+@@ -53,6 +_,13 @@
+         jFrame.pack();
+         jFrame.setLocationRelativeTo(null);
+         jFrame.setVisible(true);
 +        // Paper start - Improve ServerGUI
++        jFrame.setName("Minecraft server");
 +        try {
-+            jframe.setIconImage(javax.imageio.ImageIO.read(Objects.requireNonNull(MinecraftServerGui.class.getClassLoader().getResourceAsStream("logo.png"))));
++            jFrame.setIconImage(javax.imageio.ImageIO.read(java.util.Objects.requireNonNull(MinecraftServerGui.class.getClassLoader().getResourceAsStream("logo.png"))));
 +        } catch (java.io.IOException ignore) {
 +        }
 +        // Paper end - Improve ServerGUI
-+
-         jframe.addWindowListener(new WindowAdapter() {
-             public void windowClosing(WindowEvent windowevent) {
-                 if (!servergui.isClosing.getAndSet(true)) {
-@@ -81,6 +90,7 @@
+         jFrame.addWindowListener(new WindowAdapter() {
+             @Override
+             public void windowClosing(WindowEvent event) {
+@@ -74,6 +_,7 @@
          this.setLayout(new BorderLayout());
  
          try {
 +            this.add(this.buildOnboardingPanel(), "North"); // Paper - Add onboarding message for initial server start
              this.add(this.buildChatPanel(), "Center");
              this.add(this.buildInfoPanel(), "West");
-         } catch (Exception exception) {
-@@ -95,8 +105,8 @@
+         } catch (Exception var3) {
+@@ -87,7 +_,7 @@
  
      private JComponent buildInfoPanel() {
-         JPanel jpanel = new JPanel(new BorderLayout());
--        StatsComponent guistatscomponent = new StatsComponent(this.server);
--        Collection collection = this.finalizers;
-+        com.destroystokyo.paper.gui.GuiStatsComponent guistatscomponent = new com.destroystokyo.paper.gui.GuiStatsComponent(this.server); // Paper - Make GUI graph fancier
-+        Collection<Runnable> collection = this.finalizers; // CraftBukkit - decompile error
- 
-         Objects.requireNonNull(guistatscomponent);
-         collection.add(guistatscomponent::close);
-@@ -106,6 +116,39 @@
-         return jpanel;
+         JPanel jPanel = new JPanel(new BorderLayout());
+-        StatsComponent statsComponent = new StatsComponent(this.server);
++        com.destroystokyo.paper.gui.GuiStatsComponent statsComponent = new com.destroystokyo.paper.gui.GuiStatsComponent(this.server); // Paper - Make GUI graph fancier
+         this.finalizers.add(statsComponent::close);
+         jPanel.add(statsComponent, "North");
+         jPanel.add(this.buildPlayerPanel(), "Center");
+@@ -150,6 +_,7 @@
+         this.finalizers.forEach(Runnable::run);
      }
  
++    private static final java.util.regex.Pattern ANSI = java.util.regex.Pattern.compile("\\e\\[[\\d;]*[^\\d;]"); // CraftBukkit // Paper
+     public void print(JTextArea textArea, JScrollPane scrollPane, String line) {
+         if (!SwingUtilities.isEventDispatchThread()) {
+             SwingUtilities.invokeLater(() -> this.print(textArea, scrollPane, line));
+@@ -162,7 +_,7 @@
+             }
+ 
+             try {
+-                document.insertString(document.getLength(), line, null);
++                document.insertString(document.getLength(), MinecraftServerGui.ANSI.matcher(line).replaceAll(""), null); // CraftBukkit
+             } catch (BadLocationException var8) {
+             }
+ 
+@@ -171,4 +_,37 @@
+             }
+         }
+     }
++
 +    // Paper start - Add onboarding message for initial server start
 +    private JComponent buildOnboardingPanel() {
 +        String onboardingLink = "https://docs.papermc.io/paper/next-steps";
@@ -71,33 +85,4 @@
 +        return jPanel;
 +    }
 +    // Paper end - Add onboarding message for initial server start
-+
-     private JComponent buildPlayerPanel() {
-         JList<?> jlist = new PlayerListComponent(this.server);
-         JScrollPane jscrollpane = new JScrollPane(jlist, 22, 30);
-@@ -132,7 +175,7 @@
- 
-             jtextfield.setText("");
-         });
--        jtextarea.addFocusListener(new FocusAdapter(this) {
-+        jtextarea.addFocusListener(new FocusAdapter() { // CraftBukkit - decompile error
-             public void focusGained(FocusEvent focusevent) {}
-         });
-         jpanel.add(jscrollpane, "Center");
-@@ -166,6 +209,7 @@
-         this.finalizers.forEach(Runnable::run);
-     }
- 
-+    private static final java.util.regex.Pattern ANSI = java.util.regex.Pattern.compile("\\e\\[[\\d;]*[^\\d;]"); // CraftBukkit // Paper
-     public void print(JTextArea textArea, JScrollPane scrollPane, String message) {
-         if (!SwingUtilities.isEventDispatchThread()) {
-             SwingUtilities.invokeLater(() -> {
-@@ -181,7 +225,7 @@
-             }
- 
-             try {
--                document.insertString(document.getLength(), message, (AttributeSet) null);
-+                document.insertString(document.getLength(), MinecraftServerGui.ANSI.matcher(message).replaceAll(""), (AttributeSet) null); // CraftBukkit
-             } catch (BadLocationException badlocationexception) {
-                 ;
-             }
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/gui/StatsComponent.java.patch b/paper-server/patches/sources/net/minecraft/server/gui/StatsComponent.java.patch
similarity index 78%
rename from paper-server/patches/unapplied/net/minecraft/server/gui/StatsComponent.java.patch
rename to paper-server/patches/sources/net/minecraft/server/gui/StatsComponent.java.patch
index 3fa5e0162c..ab6d2d8cb5 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/gui/StatsComponent.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/gui/StatsComponent.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/server/gui/StatsComponent.java
 +++ b/net/minecraft/server/gui/StatsComponent.java
-@@ -34,10 +34,19 @@
+@@ -34,8 +_,17 @@
  
      private void tick() {
          long l = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
@@ -8,19 +8,17 @@
 +        double[] tps = org.bukkit.Bukkit.getTPS();
 +        String[] tpsAvg = new String[tps.length];
 +
-+        for ( int g = 0; g < tps.length; g++) {
-+            tpsAvg[g] = format( tps[g] );
++        for (int g = 0; g < tps.length; g++) {
++            tpsAvg[g] = format(tps[g]);
 +        }
          this.msgs[0] = "Memory use: " + l / 1024L / 1024L + " mb (" + Runtime.getRuntime().freeMemory() * 100L / Runtime.getRuntime().maxMemory() + "% free)";
-         this.msgs[1] = "Avg tick: "
-             + DECIMAL_FORMAT.format((double)this.server.getAverageTickTimeNanos() / (double)TimeUtil.NANOSECONDS_PER_MILLISECOND)
-             + " ms";
+         this.msgs[1] = "Avg tick: " + DECIMAL_FORMAT.format((double)this.server.getAverageTickTimeNanos() / TimeUtil.NANOSECONDS_PER_MILLISECOND) + " ms";
 +        this.msgs[2] = "TPS from last 1m, 5m, 15m: " + String.join(", ", tpsAvg);
 +        // Paper end - Improve ServerGUI
          this.values[this.vp++ & 0xFF] = (int)(l * 100L / Runtime.getRuntime().maxMemory());
          this.repaint();
      }
-@@ -66,4 +75,10 @@
+@@ -64,4 +_,10 @@
      public void close() {
          this.timer.stop();
      }
diff --git a/paper-server/patches/sources/net/minecraft/server/level/ChunkHolder.java.patch b/paper-server/patches/sources/net/minecraft/server/level/ChunkHolder.java.patch
new file mode 100644
index 0000000000..b3f729b8b5
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/level/ChunkHolder.java.patch
@@ -0,0 +1,199 @@
+--- a/net/minecraft/server/level/ChunkHolder.java
++++ b/net/minecraft/server/level/ChunkHolder.java
+@@ -33,9 +_,9 @@
+     public static final ChunkResult<LevelChunk> UNLOADED_LEVEL_CHUNK = ChunkResult.error("Unloaded level chunk");
+     private static final CompletableFuture<ChunkResult<LevelChunk>> UNLOADED_LEVEL_CHUNK_FUTURE = CompletableFuture.completedFuture(UNLOADED_LEVEL_CHUNK);
+     private final LevelHeightAccessor levelHeightAccessor;
+-    private volatile CompletableFuture<ChunkResult<LevelChunk>> fullChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE;
+-    private volatile CompletableFuture<ChunkResult<LevelChunk>> tickingChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE;
+-    private volatile CompletableFuture<ChunkResult<LevelChunk>> entityTickingChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE;
++    private volatile CompletableFuture<ChunkResult<LevelChunk>> fullChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage
++    private volatile CompletableFuture<ChunkResult<LevelChunk>> tickingChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage
++    private volatile CompletableFuture<ChunkResult<LevelChunk>> entityTickingChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage
+     public int oldTicketLevel;
+     private int ticketLevel;
+     private int queueLevel;
+@@ -71,6 +_,18 @@
+         this.changedBlocksPerSection = new ShortSet[levelHeightAccessor.getSectionsCount()];
+     }
+ 
++    // CraftBukkit start
++    public LevelChunk getFullChunkNow() {
++        // Note: We use the oldTicketLevel for isLoaded checks.
++        if (!ChunkLevel.fullStatus(this.oldTicketLevel).isOrAfter(FullChunkStatus.FULL)) return null;
++        return this.getFullChunkNowUnchecked();
++    }
++
++    public LevelChunk getFullChunkNowUnchecked() {
++        return (LevelChunk) this.getChunkIfPresentUnchecked(ChunkStatus.FULL);
++    }
++    // CraftBukkit end
++
+     public CompletableFuture<ChunkResult<LevelChunk>> getTickingChunkFuture() {
+         return this.tickingChunkFuture;
+     }
+@@ -84,7 +_,7 @@
+     }
+ 
+     @Nullable
+-    public LevelChunk getTickingChunk() {
++    public final LevelChunk getTickingChunk() { // Paper - final for inline
+         return this.getTickingChunkFuture().getNow(UNLOADED_LEVEL_CHUNK).orElse(null);
+     }
+ 
+@@ -129,6 +_,7 @@
+         } else {
+             boolean flag = this.hasChangedSections;
+             int sectionIndex = this.levelHeightAccessor.getSectionIndex(pos.getY());
++            if (sectionIndex < 0 || sectionIndex >= this.changedBlocksPerSection.length) return false; // CraftBukkit - SPIGOT-6086, SPIGOT-6296
+             if (this.changedBlocksPerSection[sectionIndex] == null) {
+                 this.hasChangedSections = true;
+                 this.changedBlocksPerSection[sectionIndex] = new ShortOpenHashSet();
+@@ -274,6 +_,38 @@
+         chunkMap.onFullChunkStatusChange(this.pos, fullChunkStatus);
+     }
+ 
++    // CraftBukkit start
++    // ChunkUnloadEvent: Called before the chunk is unloaded: isChunkLoaded is still true and chunk can still be modified by plugins.
++    // SPIGOT-7780: Moved out of updateFutures to call all chunk unload events before calling updateHighestAllowedStatus for all chunks
++    protected void callEventIfUnloading(ChunkMap chunkMap) {
++        FullChunkStatus oldFullChunkStatus = ChunkLevel.fullStatus(this.oldTicketLevel);
++        FullChunkStatus newFullChunkStatus = ChunkLevel.fullStatus(this.ticketLevel);
++        boolean oldIsFull = oldFullChunkStatus.isOrAfter(FullChunkStatus.FULL);
++        boolean newIsFull = newFullChunkStatus.isOrAfter(FullChunkStatus.FULL);
++        if (oldIsFull && !newIsFull) {
++            this.getFullChunkFuture().thenAccept((either) -> {
++                LevelChunk chunk = either.orElse(null);
++                if (chunk != null) {
++                    chunkMap.callbackExecutor.execute(() -> {
++                        // Minecraft will apply the chunks tick lists to the world once the chunk got loaded, and then store the tick
++                        // lists again inside the chunk once the chunk becomes inaccessible and set the chunk's needsSaving flag.
++                        // These actions may however happen deferred, so we manually set the needsSaving flag already here.
++                        chunk.markUnsaved();
++                        chunk.unloadCallback();
++                    });
++                }
++            }).exceptionally((throwable) -> {
++                // ensure exceptions are printed, by default this is not the case
++                net.minecraft.server.MinecraftServer.LOGGER.error("Failed to schedule unload callback for chunk " + ChunkHolder.this.pos, throwable);
++                return null;
++            });
++
++            // Run callback right away if the future was already done
++            chunkMap.callbackExecutor.run();
++        }
++    }
++    // CraftBukkit end
++
+     protected void updateFutures(ChunkMap chunkMap, Executor executor) {
+         FullChunkStatus fullChunkStatus = ChunkLevel.fullStatus(this.oldTicketLevel);
+         FullChunkStatus fullChunkStatus1 = ChunkLevel.fullStatus(this.ticketLevel);
+@@ -281,12 +_,28 @@
+         boolean isOrAfter1 = fullChunkStatus1.isOrAfter(FullChunkStatus.FULL);
+         this.wasAccessibleSinceLastSave |= isOrAfter1;
+         if (!isOrAfter && isOrAfter1) {
++            int expectCreateCount = ++this.fullChunkCreateCount; // Paper
+             this.fullChunkFuture = chunkMap.prepareAccessibleChunk(this);
+             this.scheduleFullChunkPromotion(chunkMap, this.fullChunkFuture, executor, FullChunkStatus.FULL);
++            // Paper start - cache ticking ready status
++            this.fullChunkFuture.thenAccept(chunkResult -> {
++                chunkResult.ifSuccess(chunk -> {
++                    if (ChunkHolder.this.fullChunkCreateCount == expectCreateCount) {
++                        ChunkHolder.this.isFullChunkReady = true;
++                        ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkBorder(chunk, this);
++                    }
++                });
++            });
++            // Paper end - cache ticking ready status
+             this.addSaveDependency(this.fullChunkFuture);
+         }
+ 
+         if (isOrAfter && !isOrAfter1) {
++            // Paper start
++            if (this.isFullChunkReady) {
++                ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkNotBorder(this.fullChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
++            }
++            // Paper end
+             this.fullChunkFuture.complete(UNLOADED_LEVEL_CHUNK);
+             this.fullChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE;
+         }
+@@ -296,11 +_,25 @@
+         if (!isOrAfter2 && isOrAfter3) {
+             this.tickingChunkFuture = chunkMap.prepareTickingChunk(this);
+             this.scheduleFullChunkPromotion(chunkMap, this.tickingChunkFuture, executor, FullChunkStatus.BLOCK_TICKING);
++            // Paper start - cache ticking ready status
++            this.tickingChunkFuture.thenAccept(chunkResult -> {
++                chunkResult.ifSuccess(chunk -> {
++                    // note: Here is a very good place to add callbacks to logic waiting on this.
++                    ChunkHolder.this.isTickingReady = true;
++                    ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkTicking(chunk, this);
++                });
++            });
++            // Paper end
+             this.addSaveDependency(this.tickingChunkFuture);
+         }
+ 
+         if (isOrAfter2 && !isOrAfter3) {
+-            this.tickingChunkFuture.complete(UNLOADED_LEVEL_CHUNK);
++            // Paper start
++            if (this.isTickingReady) {
++                ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkNotTicking(this.tickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
++            }
++            // Paper end
++            this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isTickingReady = false; // Paper - cache chunk ticking stage
+             this.tickingChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE;
+         }
+ 
+@@ -313,11 +_,24 @@
+ 
+             this.entityTickingChunkFuture = chunkMap.prepareEntityTickingChunk(this);
+             this.scheduleFullChunkPromotion(chunkMap, this.entityTickingChunkFuture, executor, FullChunkStatus.ENTITY_TICKING);
++            // Paper start - cache ticking ready status
++            this.entityTickingChunkFuture.thenAccept(chunkResult -> {
++                chunkResult.ifSuccess(chunk -> {
++                    ChunkHolder.this.isEntityTickingReady = true;
++                    ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkEntityTicking(chunk, this);
++                });
++            });
++            // Paper end
+             this.addSaveDependency(this.entityTickingChunkFuture);
+         }
+ 
+         if (isOrAfter4 && !isOrAfter5) {
+-            this.entityTickingChunkFuture.complete(UNLOADED_LEVEL_CHUNK);
++            // Paper start
++            if (this.isEntityTickingReady) {
++                ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkNotEntityTicking(this.entityTickingChunkFuture.join().orElseThrow(IllegalStateException::new), this);
++            }
++            // Paper end
++            this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isEntityTickingReady = false; // Paper - cache chunk ticking stage
+             this.entityTickingChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE;
+         }
+ 
+@@ -327,6 +_,26 @@
+ 
+         this.onLevelChange.onLevelChange(this.pos, this::getQueueLevel, this.ticketLevel, this::setQueueLevel);
+         this.oldTicketLevel = this.ticketLevel;
++        // CraftBukkit start
++        // ChunkLoadEvent: Called after the chunk is loaded: isChunkLoaded returns true and chunk is ready to be modified by plugins.
++        if (!fullChunkStatus.isOrAfter(FullChunkStatus.FULL) && fullChunkStatus1.isOrAfter(FullChunkStatus.FULL)) {
++            this.getFullChunkFuture().thenAccept((either) -> {
++                LevelChunk chunk = (LevelChunk) either.orElse(null);
++                if (chunk != null) {
++                    chunkMap.callbackExecutor.execute(() -> {
++                        chunk.loadCallback();
++                    });
++                }
++            }).exceptionally((throwable) -> {
++                // ensure exceptions are printed, by default this is not the case
++                net.minecraft.server.MinecraftServer.LOGGER.error("Failed to schedule load callback for chunk " + ChunkHolder.this.pos, throwable);
++                return null;
++            });
++
++            // Run callback right away if the future was already done
++            chunkMap.callbackExecutor.run();
++        }
++        // CraftBukkit end
+     }
+ 
+     public boolean wasAccessibleSinceLastSave() {
diff --git a/paper-server/patches/sources/net/minecraft/server/level/ChunkMap.java.patch b/paper-server/patches/sources/net/minecraft/server/level/ChunkMap.java.patch
new file mode 100644
index 0000000000..06c477be85
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/level/ChunkMap.java.patch
@@ -0,0 +1,366 @@
+--- a/net/minecraft/server/level/ChunkMap.java
++++ b/net/minecraft/server/level/ChunkMap.java
+@@ -145,6 +_,33 @@
+     public int serverViewDistance;
+     private final WorldGenContext worldGenContext;
+ 
++    // CraftBukkit start - recursion-safe executor for Chunk loadCallback() and unloadCallback()
++    public final CallbackExecutor callbackExecutor = new CallbackExecutor();
++    public static final class CallbackExecutor implements java.util.concurrent.Executor, Runnable {
++
++        private final java.util.Queue<Runnable> queue = new java.util.ArrayDeque<>();
++
++        @Override
++        public void execute(Runnable runnable) {
++            this.queue.add(runnable);
++        }
++
++        @Override
++        public void run() {
++            Runnable task;
++            while ((task = this.queue.poll()) != null) {
++                task.run();
++            }
++        }
++    };
++    // CraftBukkit end
++
++    // Paper start
++    public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) {
++        return this.pendingUnloads.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
++    }
++    // Paper end
++
+     public ChunkMap(
+         ServerLevel level,
+         LevelStorageSource.LevelStorageAccess levelStorageAccess,
+@@ -171,13 +_,19 @@
+         this.level = level;
+         RegistryAccess registryAccess = level.registryAccess();
+         long seed = level.getSeed();
+-        if (generator instanceof NoiseBasedChunkGenerator noiseBasedChunkGenerator) {
++        // CraftBukkit start - SPIGOT-7051: It's a rigged game! Use delegate for random state creation, otherwise it is not so random.
++        ChunkGenerator randomGenerator = generator;
++        if (randomGenerator instanceof org.bukkit.craftbukkit.generator.CustomChunkGenerator customChunkGenerator) {
++            randomGenerator = customChunkGenerator.getDelegate();
++        }
++        if (randomGenerator instanceof NoiseBasedChunkGenerator noiseBasedChunkGenerator) {
++        // CraftBukkit end
+             this.randomState = RandomState.create(noiseBasedChunkGenerator.generatorSettings().value(), registryAccess.lookupOrThrow(Registries.NOISE), seed);
+         } else {
+             this.randomState = RandomState.create(NoiseGeneratorSettings.dummy(), registryAccess.lookupOrThrow(Registries.NOISE), seed);
+         }
+ 
+-        this.chunkGeneratorState = generator.createState(registryAccess.lookupOrThrow(Registries.STRUCTURE_SET), this.randomState, seed);
++        this.chunkGeneratorState = generator.createState(registryAccess.lookupOrThrow(Registries.STRUCTURE_SET), this.randomState, seed, level.spigotConfig); // Spigot
+         this.mainThreadExecutor = mainThreadExecutor;
+         ConsecutiveExecutor consecutiveExecutor = new ConsecutiveExecutor(dispatcher, "worldgen");
+         this.progressListener = progressListener;
+@@ -207,6 +_,12 @@
+         this.chunksToEagerlySave.add(chunkPos.toLong());
+     }
+ 
++    // Paper start
++    public int getMobCountNear(final ServerPlayer player, final net.minecraft.world.entity.MobCategory mobCategory) {
++        return -1;
++    }
++    // Paper end
++
+     protected ChunkGenerator generator() {
+         return this.worldGenContext.generator();
+     }
+@@ -354,9 +_,9 @@
+                 }
+             );
+         stringBuilder.append("Updating:").append(System.lineSeparator());
+-        this.updatingChunkMap.values().forEach(consumer);
++        ca.spottedleaf.moonrise.common.PlatformHooks.get().getUpdatingChunkHolders(this.level).forEach(consumer); // Paper
+         stringBuilder.append("Visible:").append(System.lineSeparator());
+-        this.visibleChunkMap.values().forEach(consumer);
++        ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.level).forEach(consumer); // Paper
+         CrashReport crashReport = CrashReport.forThrowable(exception, "Chunk loading");
+         CrashReportCategory crashReportCategory = crashReport.addCategory("Chunk loading");
+         crashReportCategory.setDetail("Details", details);
+@@ -392,6 +_,9 @@
+                     holder.setTicketLevel(newLevel);
+                 } else {
+                     holder = new ChunkHolder(new ChunkPos(chunkPos), newLevel, this.level, this.lightEngine, this::onLevelChange, this);
++                    // Paper start
++                    ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkHolderCreate(this.level, holder);
++                    // Paper end
+                 }
+ 
+                 this.updatingChunkMap.put(chunkPos, holder);
+@@ -420,8 +_,8 @@
+ 
+     protected void saveAllChunks(boolean flush) {
+         if (flush) {
+-            List<ChunkHolder> list = this.visibleChunkMap
+-                .values()
++            List<ChunkHolder> list = ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.level) // Paper - moonrise
++                //.values() // Paper - moonrise
+                 .stream()
+                 .filter(ChunkHolder::wasAccessibleSinceLastSave)
+                 .peek(ChunkHolder::refreshAccessibility)
+@@ -447,7 +_,7 @@
+             this.nextChunkSaveTime.clear();
+             long millis = Util.getMillis();
+ 
+-            for (ChunkHolder chunkHolder : this.visibleChunkMap.values()) {
++            for (ChunkHolder chunkHolder : ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.level)) { // Paper
+                 this.saveChunkIfNeeded(chunkHolder, millis);
+             }
+         }
+@@ -468,6 +_,7 @@
+     public boolean hasWork() {
+         return this.lightEngine.hasLightWork()
+             || !this.pendingUnloads.isEmpty()
++            || ca.spottedleaf.moonrise.common.PlatformHooks.get().hasAnyChunkHolders(this.level) // Paper - moonrise
+             || !this.updatingChunkMap.isEmpty()
+             || this.poiManager.hasWork()
+             || !this.toDrop.isEmpty()
+@@ -526,7 +_,11 @@
+                 this.scheduleUnload(chunkPos, chunkHolder);
+             } else {
+                 ChunkAccess latestChunk = chunkHolder.getLatestChunk();
+-                if (this.pendingUnloads.remove(chunkPos, chunkHolder) && latestChunk != null) {
++                // Paper start
++                boolean removed;
++                if ((removed = this.pendingUnloads.remove(chunkPos, chunkHolder)) && latestChunk != null) {
++                    ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkHolderDelete(this.level, chunkHolder);
++                    // Paper end
+                     if (latestChunk instanceof LevelChunk levelChunk) {
+                         levelChunk.setLoaded(false);
+                     }
+@@ -540,7 +_,9 @@
+                     this.lightEngine.tryScheduleUpdate();
+                     this.progressListener.onStatusChange(latestChunk.getPos(), null);
+                     this.nextChunkSaveTime.remove(latestChunk.getPos().toLong());
+-                }
++                } else if (removed) { // Paper start
++                    ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkHolderDelete(this.level, chunkHolder);
++                } // Paper end
+             }
+         }, this.unloadQueue::add).whenComplete((_void, error) -> {
+             if (error != null) {
+@@ -856,7 +_,7 @@
+     }
+ 
+     public int size() {
+-        return this.visibleChunkMap.size();
++        return ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolderCount(this.level); // Paper
+     }
+ 
+     public net.minecraft.server.level.DistanceManager getDistanceManager() {
+@@ -864,7 +_,7 @@
+     }
+ 
+     protected Iterable<ChunkHolder> getChunks() {
+-        return Iterables.unmodifiableIterable(this.visibleChunkMap.values());
++        return Iterables.unmodifiableIterable(ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.level)); // Paper
+     }
+ 
+     void dumpChunks(Writer writer) throws IOException {
+@@ -888,10 +_,10 @@
+             .build(writer);
+         TickingTracker tickingTracker = this.distanceManager.tickingTracker();
+ 
+-        for (Entry<ChunkHolder> entry : this.visibleChunkMap.long2ObjectEntrySet()) {
+-            long longKey = entry.getLongKey();
++        for (ChunkHolder entry : ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.level)) { // Paper - Moonrise
++            long longKey = entry.pos.toLong(); // Paper - Moonrise
+             ChunkPos chunkPos = new ChunkPos(longKey);
+-            ChunkHolder chunkHolder = entry.getValue();
++            ChunkHolder chunkHolder = entry; // Paper - Moonrise
+             Optional<ChunkAccess> optional = Optional.ofNullable(chunkHolder.getLatestChunk());
+             Optional<LevelChunk> optional1 = optional.flatMap(chunk -> chunk instanceof LevelChunk ? Optional.of((LevelChunk)chunk) : Optional.empty());
+             csvOutput.writeRow(
+@@ -931,11 +_,13 @@
+     }
+ 
+     private CompletableFuture<Optional<CompoundTag>> readChunk(ChunkPos pos) {
+-        return this.read(pos).thenApplyAsync(optional -> optional.map(this::upgradeChunkTag), Util.backgroundExecutor().forName("upgradeChunk"));
++        return this.read(pos).thenApplyAsync(optional -> optional.map(tag -> upgradeChunkTag(tag, pos)), Util.backgroundExecutor().forName("upgradeChunk")); // CraftBukkit
+     }
+ 
+-    private CompoundTag upgradeChunkTag(CompoundTag tag) {
+-        return this.upgradeChunkTag(this.level.dimension(), this.overworldDataStorage, tag, this.generator().getTypeNameForDataFixer());
++    // CraftBukkit start
++    private CompoundTag upgradeChunkTag(CompoundTag tag, ChunkPos pos) {
++        return this.upgradeChunkTag(this.level.getTypeKey(), this.overworldDataStorage, tag, this.generator().getTypeNameForDataFixer(), pos, this.level);
++    // CraftBukkit end
+     }
+ 
+     void forEachSpawnCandidateChunk(Consumer<ChunkHolder> action) {
+@@ -951,12 +_,34 @@
+     }
+ 
+     public boolean anyPlayerCloseEnoughForSpawning(ChunkPos chunkPos) {
+-        return this.distanceManager.hasPlayersNearby(chunkPos.toLong()) && this.anyPlayerCloseEnoughForSpawningInternal(chunkPos);
++        // Spigot start
++        return this.anyPlayerCloseEnoughForSpawning(chunkPos, false);
++    }
++
++    boolean anyPlayerCloseEnoughForSpawning(ChunkPos chunkPos, boolean reducedRange) {
++        return this.distanceManager.hasPlayersNearby(chunkPos.toLong()) && this.anyPlayerCloseEnoughForSpawningInternal(chunkPos, reducedRange);
++        // Spigot end
+     }
+ 
+     private boolean anyPlayerCloseEnoughForSpawningInternal(ChunkPos chunkPos) {
++        // Spigot start
++        return this.anyPlayerCloseEnoughForSpawningInternal(chunkPos, false);
++    }
++
++    private boolean anyPlayerCloseEnoughForSpawningInternal(ChunkPos chunkPos, boolean reducedRange) {
++        double blockRange; // Paper - use from event
++        // Spigot end
+         for (ServerPlayer serverPlayer : this.playerMap.getAllPlayers()) {
+-            if (this.playerIsCloseEnoughForSpawning(serverPlayer, chunkPos)) {
++            // Paper start - PlayerNaturallySpawnCreaturesEvent
++            com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent event;
++            blockRange = 16384.0D;
++            if (reducedRange) {
++                event = serverPlayer.playerNaturallySpawnedEvent;
++                if (event == null || event.isCancelled()) continue;
++                blockRange = (double) ((event.getSpawnRadius() << 4) * (event.getSpawnRadius() << 4));
++            }
++            // Paper end - PlayerNaturallySpawnCreaturesEvent
++            if (this.playerIsCloseEnoughForSpawning(serverPlayer, chunkPos, blockRange)) {
+                 return true;
+             }
+         }
+@@ -972,7 +_,7 @@
+             Builder<ServerPlayer> builder = ImmutableList.builder();
+ 
+             for (ServerPlayer serverPlayer : this.playerMap.getAllPlayers()) {
+-                if (this.playerIsCloseEnoughForSpawning(serverPlayer, chunkPos)) {
++                if (this.playerIsCloseEnoughForSpawning(serverPlayer, chunkPos, 16384.0D)) { // Spigot
+                     builder.add(serverPlayer);
+                 }
+             }
+@@ -981,12 +_,12 @@
+         }
+     }
+ 
+-    private boolean playerIsCloseEnoughForSpawning(ServerPlayer player, ChunkPos chunkPos) {
++    private boolean playerIsCloseEnoughForSpawning(ServerPlayer player, ChunkPos chunkPos, double range) { // Spigot
+         if (player.isSpectator()) {
+             return false;
+         } else {
+             double d = euclideanDistanceSquared(chunkPos, player);
+-            return d < 16384.0;
++            return d < range; // Spigot
+         }
+     }
+ 
+@@ -1100,9 +_,19 @@
+     }
+ 
+     public void addEntity(Entity entity) {
++        org.spigotmc.AsyncCatcher.catchOp("entity track"); // Spigot
++        // Paper start - ignore and warn about illegal addEntity calls instead of crashing server
++        if (!entity.valid || entity.level() != this.level || this.entityMap.containsKey(entity.getId())) {
++            LOGGER.error("Illegal ChunkMap::addEntity for world " + this.level.getWorld().getName()
++                + ": " + entity  + (this.entityMap.containsKey(entity.getId()) ? " ALREADY CONTAINED (This would have crashed your server)" : ""), new Throwable());
++            return;
++        }
++        // Paper end - ignore and warn about illegal addEntity calls instead of crashing server
++        if (entity instanceof ServerPlayer && ((ServerPlayer) entity).supressTrackerForLogin) return; // Paper - Fire PlayerJoinEvent when Player is actually ready; Delay adding to tracker until after list packets
+         if (!(entity instanceof EnderDragonPart)) {
+             EntityType<?> type = entity.getType();
+             int i = type.clientTrackingRange() * 16;
++            i = org.spigotmc.TrackingRange.getEntityTrackingRange(entity, i); // Spigot
+             if (i != 0) {
+                 int updateInterval = type.updateInterval();
+                 if (this.entityMap.containsKey(entity.getId())) {
+@@ -1126,6 +_,7 @@
+     }
+ 
+     protected void removeEntity(Entity entity) {
++        org.spigotmc.AsyncCatcher.catchOp("entity untrack"); // Spigot
+         if (entity instanceof ServerPlayer serverPlayer) {
+             this.updatePlayerStatus(serverPlayer, false);
+ 
+@@ -1230,7 +_,7 @@
+         });
+     }
+ 
+-    class DistanceManager extends net.minecraft.server.level.DistanceManager {
++    public class DistanceManager extends net.minecraft.server.level.DistanceManager { // Paper - public
+         protected DistanceManager(final Executor dispatcher, final Executor mainThreadExecutor) {
+             super(dispatcher, mainThreadExecutor);
+         }
+@@ -1258,10 +_,10 @@
+         final Entity entity;
+         private final int range;
+         SectionPos lastSectionPos;
+-        public final Set<ServerPlayerConnection> seenBy = Sets.newIdentityHashSet();
++        public final Set<ServerPlayerConnection> seenBy = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); // Paper - Perf: optimise map impl
+ 
+         public TrackedEntity(final Entity entity, final int range, final int updateInterval, final boolean trackDelta) {
+-            this.serverEntity = new ServerEntity(ChunkMap.this.level, entity, updateInterval, trackDelta, this::broadcast);
++            this.serverEntity = new ServerEntity(ChunkMap.this.level, entity, updateInterval, trackDelta, this::broadcast, this.seenBy); // CraftBukkit
+             this.entity = entity;
+             this.range = range;
+             this.lastSectionPos = SectionPos.of(entity);
+@@ -1297,24 +_,47 @@
+         }
+ 
+         public void removePlayer(ServerPlayer player) {
++            org.spigotmc.AsyncCatcher.catchOp("player tracker clear"); // Spigot
+             if (this.seenBy.remove(player.connection)) {
+                 this.serverEntity.removePairing(player);
+             }
+         }
+ 
+         public void updatePlayer(ServerPlayer player) {
++            org.spigotmc.AsyncCatcher.catchOp("player tracker update"); // Spigot
+             if (player != this.entity) {
+-                Vec3 vec3 = player.position().subtract(this.entity.position());
++                // Paper start - remove allocation of Vec3D here
++                // Vec3 vec3 = player.position().subtract(this.entity.position());
++                double vec3_dx = player.getX() - this.entity.getX();
++                double vec3_dz = player.getZ() - this.entity.getZ();
++                // Paper end - remove allocation of Vec3D here
+                 int playerViewDistance = ChunkMap.this.getPlayerViewDistance(player);
+                 double d = Math.min(this.getEffectiveRange(), playerViewDistance * 16);
+-                double d1 = vec3.x * vec3.x + vec3.z * vec3.z;
++                double d1 = vec3_dx * vec3_dx + vec3_dz * vec3_dz; // Paper
+                 double d2 = d * d;
+-                boolean flag = d1 <= d2
+-                    && this.entity.broadcastToPlayer(player)
+-                    && ChunkMap.this.isChunkTracked(player, this.entity.chunkPosition().x, this.entity.chunkPosition().z);
++                // Paper start - Configurable entity tracking range by Y
++                boolean flag = d1 <= d2;
++                if (flag && level.paperConfig().entities.trackingRangeY.enabled) {
++                    double rangeY = level.paperConfig().entities.trackingRangeY.get(this.entity, -1);
++                    if (rangeY != -1) {
++                        double vec3_dy = player.getY() - this.entity.getY();
++                        flag = vec3_dy * vec3_dy <= rangeY * rangeY;
++                    }
++                }
++                flag = flag && this.entity.broadcastToPlayer(player) && ChunkMap.this.isChunkTracked(player, this.entity.chunkPosition().x, this.entity.chunkPosition().z);
++                // Paper end - Configurable entity tracking range by Y
++                // CraftBukkit start - respect vanish API
++                if (flag && !player.getBukkitEntity().canSee(this.entity.getBukkitEntity())) { // Paper - only consider hits
++                    flag = false;
++                }
++                // CraftBukkit end
+                 if (flag) {
+                     if (this.seenBy.add(player.connection)) {
++                        // Paper start - entity tracking events
++                        if (io.papermc.paper.event.player.PlayerTrackEntityEvent.getHandlerList().getRegisteredListeners().length == 0 || new io.papermc.paper.event.player.PlayerTrackEntityEvent(player.getBukkitEntity(), this.entity.getBukkitEntity()).callEvent()) {
+                         this.serverEntity.addPairing(player);
++                        }
++                        // Paper end - entity tracking events
+                     }
+                 } else if (this.seenBy.remove(player.connection)) {
+                     this.serverEntity.removePairing(player);
+@@ -1331,6 +_,7 @@
+ 
+             for (Entity entity : this.entity.getIndirectPassengers()) {
+                 int i1 = entity.getType().clientTrackingRange() * 16;
++                i1 = org.spigotmc.TrackingRange.getEntityTrackingRange(entity, i1); // Paper
+                 if (i1 > i) {
+                     i = i1;
+                 }
diff --git a/paper-server/patches/sources/net/minecraft/server/level/DistanceManager.java.patch b/paper-server/patches/sources/net/minecraft/server/level/DistanceManager.java.patch
new file mode 100644
index 0000000000..2ac4d5befd
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/level/DistanceManager.java.patch
@@ -0,0 +1,108 @@
+--- a/net/minecraft/server/level/DistanceManager.java
++++ b/net/minecraft/server/level/DistanceManager.java
+@@ -107,6 +_,12 @@
+         }
+ 
+         if (!this.chunksToUpdateFutures.isEmpty()) {
++            // CraftBukkit start - SPIGOT-7780: Call chunk unload events before updateHighestAllowedStatus
++            for (final ChunkHolder chunkHolder : this.chunksToUpdateFutures) {
++                chunkHolder.callEventIfUnloading(chunkMap);
++            }
++            // CraftBukkit end - SPIGOT-7780: Call chunk unload events before updateHighestAllowedStatus
++
+             for (ChunkHolder chunkHolder : this.chunksToUpdateFutures) {
+                 chunkHolder.updateHighestAllowedStatus(chunkMap);
+             }
+@@ -177,16 +_,42 @@
+     public <T> void addRegionTicket(TicketType<T> type, ChunkPos pos, int distance, T value) {
+         Ticket<T> ticket = new Ticket<>(type, ChunkLevel.byStatus(FullChunkStatus.FULL) - distance, value);
+         long packedChunkPos = pos.toLong();
+-        this.addTicket(packedChunkPos, ticket);
++        this.addTicket(packedChunkPos, ticket); // Paper - diff on change above
+         this.tickingTicketsTracker.addTicket(packedChunkPos, ticket);
+     }
+ 
+     public <T> void removeRegionTicket(TicketType<T> type, ChunkPos pos, int distance, T value) {
+         Ticket<T> ticket = new Ticket<>(type, ChunkLevel.byStatus(FullChunkStatus.FULL) - distance, value);
+         long packedChunkPos = pos.toLong();
++        this.removeTicket(packedChunkPos, ticket); // Paper - diff on change above
++        this.tickingTicketsTracker.removeTicket(packedChunkPos, ticket);
++    }
++
++    // Paper start
++    public boolean addPluginRegionTicket(final ChunkPos pos, final org.bukkit.plugin.Plugin value) {
++        Ticket<org.bukkit.plugin.Plugin> ticket = new Ticket<>(TicketType.PLUGIN_TICKET, ChunkLevel.byStatus(FullChunkStatus.FULL) - 2, value); // Copied from below and keep in-line with force loading, add at level 31
++        final long packedChunkPos = pos.toLong();
++        final Set<Ticket<?>> tickets = this.getTickets(packedChunkPos);
++        if (tickets.contains(ticket)) {
++            return false;
++        }
++        this.addTicket(packedChunkPos, ticket);
++        this.tickingTicketsTracker.addTicket(packedChunkPos, ticket);
++        return true;
++    }
++
++    public boolean removePluginRegionTicket(final ChunkPos pos, final org.bukkit.plugin.Plugin value) {
++        Ticket<org.bukkit.plugin.Plugin> ticket = new Ticket<>(TicketType.PLUGIN_TICKET, ChunkLevel.byStatus(FullChunkStatus.FULL) - 2, value); // Copied from below and keep in-line with force loading, add at level 31
++        final long packedChunkPos = pos.toLong();
++        final Set<Ticket<?>> tickets = this.tickets.get(packedChunkPos); // Don't use getTickets, we don't want to create a new set
++        if (tickets == null || !tickets.contains(ticket)) {
++            return false;
++        }
+         this.removeTicket(packedChunkPos, ticket);
+         this.tickingTicketsTracker.removeTicket(packedChunkPos, ticket);
++        return true;
+     }
++    // Paper end
+ 
+     private SortedArraySet<Ticket<?>> getTickets(long chunkPos) {
+         return this.tickets.computeIfAbsent(chunkPos, l -> SortedArraySet.create(4));
+@@ -217,8 +_,10 @@
+         ChunkPos chunkPos = sectionPos.chunk();
+         long packedChunkPos = chunkPos.toLong();
+         ObjectSet<ServerPlayer> set = this.playersPerChunk.get(packedChunkPos);
+-        set.remove(player);
+-        if (set.isEmpty()) {
++        // Paper start - some state corruption happens here, don't crash, clean up gracefully
++        if (set != null) set.remove(player);
++        if (set == null || set.isEmpty()) {
++        // Paper end - some state corruption happens here, don't crash, clean up gracefully
+             this.playersPerChunk.remove(packedChunkPos);
+             this.naturalSpawnChunkCounter.update(packedChunkPos, Integer.MAX_VALUE, false);
+             this.playerTicketManager.update(packedChunkPos, Integer.MAX_VALUE, false);
+@@ -299,7 +_,7 @@
+     }
+ 
+     public void removeTicketsOnClosing() {
+-        ImmutableSet<TicketType<?>> set = ImmutableSet.of(TicketType.UNKNOWN);
++        ImmutableSet<TicketType<?>> set = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.FUTURE_AWAIT); // Paper - add additional tickets to preserve
+         ObjectIterator<Entry<SortedArraySet<Ticket<?>>>> objectIterator = this.tickets.long2ObjectEntrySet().fastIterator();
+ 
+         while (objectIterator.hasNext()) {
+@@ -329,6 +_,26 @@
+     public boolean hasTickets() {
+         return !this.tickets.isEmpty();
+     }
++
++    // CraftBukkit start
++    public <T> void removeAllTicketsFor(TicketType<T> ticketType, int ticketLevel, T ticketIdentifier) {
++        Ticket<T> target = new Ticket<>(ticketType, ticketLevel, ticketIdentifier);
++
++        for (java.util.Iterator<Entry<SortedArraySet<Ticket<?>>>> iterator = this.tickets.long2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
++            Entry<SortedArraySet<Ticket<?>>> entry = iterator.next();
++            SortedArraySet<Ticket<?>> tickets = entry.getValue();
++            if (tickets.remove(target)) {
++                // copied from removeTicket
++                this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt(tickets), false);
++
++                // can't use entry after it's removed
++                if (tickets.isEmpty()) {
++                    iterator.remove();
++                }
++            }
++        }
++    }
++    // CraftBukkit end
+ 
+     class ChunkTicketTracker extends ChunkTracker {
+         private static final int MAX_LEVEL = ChunkLevel.MAX_LEVEL + 1;
diff --git a/paper-server/patches/unapplied/net/minecraft/server/level/ServerChunkCache.java.patch b/paper-server/patches/sources/net/minecraft/server/level/ServerChunkCache.java.patch
similarity index 54%
rename from paper-server/patches/unapplied/net/minecraft/server/level/ServerChunkCache.java.patch
rename to paper-server/patches/sources/net/minecraft/server/level/ServerChunkCache.java.patch
index f7949f17c6..ec01e0da0f 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/level/ServerChunkCache.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/level/ServerChunkCache.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/server/level/ServerChunkCache.java
 +++ b/net/minecraft/server/level/ServerChunkCache.java
-@@ -74,6 +74,13 @@
+@@ -73,6 +_,13 @@
      @Nullable
      @VisibleForDebug
      private NaturalSpawner.SpawnState lastSpawnState;
@@ -12,9 +12,9 @@
 +    long chunkFutureAwaitCounter;
 +    // Paper end
  
-     public ServerChunkCache(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, int simulationDistance, boolean dsync, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory) {
-         this.level = world;
-@@ -95,6 +102,64 @@
+     public ServerChunkCache(
+         ServerLevel level,
+@@ -121,6 +_,64 @@
          this.clearCache();
      }
  
@@ -79,53 +79,50 @@
      @Override
      public ThreadedLevelLightEngine getLightEngine() {
          return this.lightEngine;
-@@ -138,7 +203,7 @@
-                 if (k == this.lastChunkPos[l] && leastStatus == this.lastChunkStatus[l]) {
-                     ChunkAccess ichunkaccess = this.lastChunk[l];
- 
--                    if (ichunkaccess != null || !create) {
-+                    if (ichunkaccess != null) { // CraftBukkit - the chunk can become accessible in the meantime TODO for non-null chunks it might also make sense to check that the chunk's state hasn't changed in the meantime
-                         return ichunkaccess;
+@@ -160,7 +_,7 @@
+             for (int i = 0; i < 4; i++) {
+                 if (packedChunkPos == this.lastChunkPos[i] && chunkStatus == this.lastChunkStatus[i]) {
+                     ChunkAccess chunkAccess = this.lastChunk[i];
+-                    if (chunkAccess != null || !requireChunk) {
++                    if (chunkAccess != null) { // CraftBukkit - the chunk can become accessible in the meantime TODO for non-null chunks it might also make sense to check that the chunk's state hasn't changed in the meantime
+                         return chunkAccess;
                      }
                  }
-@@ -150,8 +215,9 @@
- 
-             Objects.requireNonNull(completablefuture);
-             chunkproviderserver_b.managedBlock(completablefuture::isDone);
+@@ -169,6 +_,7 @@
+             profilerFiller.incrementCounter("getChunkCacheMiss");
+             CompletableFuture<ChunkResult<ChunkAccess>> chunkFutureMainThread = this.getChunkFutureMainThread(x, z, chunkStatus, requireChunk);
+             this.mainThreadProcessor.managedBlock(chunkFutureMainThread::isDone);
 +            // com.destroystokyo.paper.io.SyncLoadFinder.logSyncLoad(this.level, x, z); // Paper - Add debug for sync chunk loads
-             ChunkResult<ChunkAccess> chunkresult = (ChunkResult) completablefuture.join();
--            ChunkAccess ichunkaccess1 = (ChunkAccess) chunkresult.orElse((Object) null);
-+            ChunkAccess ichunkaccess1 = (ChunkAccess) chunkresult.orElse(null); // CraftBukkit - decompile error
- 
-             if (ichunkaccess1 == null && create) {
-                 throw (IllegalStateException) Util.pauseInIde(new IllegalStateException("Chunk not there when requested: " + chunkresult.getError()));
-@@ -231,7 +297,15 @@
-         int l = ChunkLevel.byStatus(leastStatus);
-         ChunkHolder playerchunk = this.getVisibleChunkIfPresent(k);
- 
--        if (create) {
+             ChunkResult<ChunkAccess> chunkResult = chunkFutureMainThread.join();
+             ChunkAccess chunkAccess1 = chunkResult.orElse(null);
+             if (chunkAccess1 == null && requireChunk) {
+@@ -240,7 +_,15 @@
+         long packedChunkPos = chunkPos.toLong();
+         int i = ChunkLevel.byStatus(chunkStatus);
+         ChunkHolder visibleChunkIfPresent = this.getVisibleChunkIfPresent(packedChunkPos);
+-        if (requireChunk) {
 +        // CraftBukkit start - don't add new ticket for currently unloading chunk
 +        boolean currentlyUnloading = false;
-+        if (playerchunk != null) {
-+            FullChunkStatus oldChunkState = ChunkLevel.fullStatus(playerchunk.oldTicketLevel);
-+            FullChunkStatus currentChunkState = ChunkLevel.fullStatus(playerchunk.getTicketLevel());
++        if (visibleChunkIfPresent != null) {
++            FullChunkStatus oldChunkState = ChunkLevel.fullStatus(visibleChunkIfPresent.oldTicketLevel);
++            FullChunkStatus currentChunkState = ChunkLevel.fullStatus(visibleChunkIfPresent.getTicketLevel());
 +            currentlyUnloading = (oldChunkState.isOrAfter(FullChunkStatus.FULL) && !currentChunkState.isOrAfter(FullChunkStatus.FULL));
 +        }
-+        if (create && !currentlyUnloading) {
-+            // CraftBukkit end
-             this.distanceManager.addTicket(TicketType.UNKNOWN, chunkcoordintpair, l, chunkcoordintpair);
-             if (this.chunkAbsent(playerchunk, l)) {
-                 ProfilerFiller gameprofilerfiller = Profiler.get();
-@@ -250,7 +324,7 @@
++        if (requireChunk && !currentlyUnloading) {
++        // CraftBukkit end
+             this.distanceManager.addTicket(TicketType.UNKNOWN, chunkPos, i, chunkPos);
+             if (this.chunkAbsent(visibleChunkIfPresent, i)) {
+                 ProfilerFiller profilerFiller = Profiler.get();
+@@ -260,7 +_,7 @@
      }
  
-     private boolean chunkAbsent(@Nullable ChunkHolder holder, int maxLevel) {
--        return holder == null || holder.getTicketLevel() > maxLevel;
-+        return holder == null || holder.oldTicketLevel > maxLevel; // CraftBukkit using oldTicketLevel for isLoaded checks
+     private boolean chunkAbsent(@Nullable ChunkHolder chunkHolder, int status) {
+-        return chunkHolder == null || chunkHolder.getTicketLevel() > status;
++        return chunkHolder == null || chunkHolder.oldTicketLevel > status; // CraftBukkit using oldTicketLevel for isLoaded checks
      }
  
      @Override
-@@ -279,7 +353,7 @@
+@@ -287,7 +_,7 @@
          return this.mainThreadProcessor.pollTask();
      }
  
@@ -133,8 +130,8 @@
 +    public boolean runDistanceManagerUpdates() { // Paper - public
          boolean flag = this.distanceManager.runAllUpdates(this.chunkMap);
          boolean flag1 = this.chunkMap.promoteChunkMap();
- 
-@@ -309,18 +383,40 @@
+         this.chunkMap.runGenerationTasks();
+@@ -315,17 +_,39 @@
  
      @Override
      public void close() throws IOException {
@@ -168,26 +165,25 @@
 +    // CraftBukkit end
 +
      @Override
-     public void tick(BooleanSupplier shouldKeepTicking, boolean tickChunks) {
-         ProfilerFiller gameprofilerfiller = Profiler.get();
- 
-         gameprofilerfiller.push("purge");
+     public void tick(BooleanSupplier hasTimeLeft, boolean tickChunks) {
+         ProfilerFiller profilerFiller = Profiler.get();
+         profilerFiller.push("purge");
 -        if (this.level.tickRateManager().runsNormally() || !tickChunks) {
 +        if (this.level.tickRateManager().runsNormally() || !tickChunks || this.level.spigotConfig.unloadFrozenChunks) { // Spigot
              this.distanceManager.purgeStaleTickets();
          }
  
-@@ -401,14 +497,22 @@
- 
-         this.lastSpawnState = spawnercreature_d;
+@@ -400,12 +_,20 @@
+         );
+         this.lastSpawnState = spawnState;
          profiler.popPush("spawnAndTick");
--        boolean flag = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING);
-+        boolean flag = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.players().isEmpty(); // CraftBukkit
-         int k = this.level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING);
-         List list1;
- 
-         if (flag && (this.spawnEnemies || this.spawnFriendlies)) {
--            boolean flag1 = this.level.getLevelData().getGameTime() % 400L == 0L;
+-        boolean _boolean = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING);
++        boolean _boolean = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.players().isEmpty(); // CraftBukkit
+         int _int = this.level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING);
+         List<MobCategory> filteredSpawningCategories;
+         if (_boolean && (this.spawnEnemies || this.spawnFriendlies)) {
+-            boolean flag = this.level.getLevelData().getGameTime() % 400L == 0L;
+-            filteredSpawningCategories = NaturalSpawner.getFilteredSpawningCategories(spawnState, this.spawnFriendlies, this.spawnEnemies, flag);
 +            // Paper start - PlayerNaturallySpawnCreaturesEvent
 +            for (ServerPlayer entityPlayer : this.level.players()) {
 +                int chunkRange = Math.min(level.spigotConfig.mobSpawnRange, entityPlayer.getBukkitEntity().getViewDistance());
@@ -196,60 +192,51 @@
 +                entityPlayer.playerNaturallySpawnedEvent.callEvent();
 +            }
 +            // Paper end - PlayerNaturallySpawnCreaturesEvent
-+            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
- 
--            list1 = NaturalSpawner.getFilteredSpawningCategories(spawnercreature_d, this.spawnFriendlies, this.spawnEnemies, flag1);
-+            list1 = NaturalSpawner.getFilteredSpawningCategories(spawnercreature_d, this.spawnFriendlies, this.spawnEnemies, flag1, this.level); // CraftBukkit
++            boolean flag = 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
++            filteredSpawningCategories = NaturalSpawner.getFilteredSpawningCategories(spawnState, this.spawnFriendlies, this.spawnEnemies, flag, this.level); // CraftBukkit
          } else {
-             list1 = List.of();
+             filteredSpawningCategories = List.of();
          }
-@@ -420,7 +524,7 @@
-             ChunkPos chunkcoordintpair = chunk.getPos();
- 
-             chunk.incrementInhabitedTime(timeDelta);
--            if (!list1.isEmpty() && this.level.getWorldBorder().isWithinBounds(chunkcoordintpair)) {
-+            if (!list1.isEmpty() && this.level.getWorldBorder().isWithinBounds(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(chunkcoordintpair, true)) { // Spigot
-                 NaturalSpawner.spawnForChunk(this.level, chunk, spawnercreature_d, list1);
+@@ -413,7 +_,7 @@
+         for (LevelChunk levelChunk : chunks) {
+             ChunkPos pos = levelChunk.getPos();
+             levelChunk.incrementInhabitedTime(timeInhabited);
+-            if (!filteredSpawningCategories.isEmpty() && this.level.getWorldBorder().isWithinBounds(pos)) {
++            if (!filteredSpawningCategories.isEmpty() && this.level.getWorldBorder().isWithinBounds(pos) && this.chunkMap.anyPlayerCloseEnoughForSpawning(pos, true)) { // Spigot
+                 NaturalSpawner.spawnForChunk(this.level, levelChunk, spawnState, filteredSpawningCategories);
              }
  
-@@ -541,10 +645,16 @@
+@@ -526,8 +_,13 @@
  
      @Override
-     public void setSpawnSettings(boolean spawnMonsters) {
--        this.spawnEnemies = spawnMonsters;
--        this.spawnFriendlies = this.spawnFriendlies;
+     public void setSpawnSettings(boolean spawnSettings) {
 +        // CraftBukkit start
-+        this.setSpawnSettings(spawnMonsters, this.spawnFriendlies);
-     }
- 
-+    public void setSpawnSettings(boolean flag, boolean spawnFriendlies) {
-+        this.spawnEnemies = flag;
++        this.setSpawnSettings(spawnSettings, this.spawnFriendlies);
++    }
++    public void setSpawnSettings(boolean spawnSettings, boolean spawnFriendlies) {
+         this.spawnEnemies = spawnSettings;
+-        this.spawnFriendlies = this.spawnFriendlies;
 +        this.spawnFriendlies = spawnFriendlies;
 +        // CraftBukkit end
-+    }
-+
-     public String getChunkDebugData(ChunkPos pos) {
-         return this.chunkMap.getChunkDebugData(pos);
      }
-@@ -618,14 +728,20 @@
-         }
+ 
+     public String getChunkDebugData(ChunkPos chunkPos) {
+@@ -603,12 +_,18 @@
  
          @Override
--        protected boolean pollTask() {
-+        // CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task
-+        public boolean pollTask() {
-+        try {
+         public boolean pollTask() {
++            try { // CraftBukkit - process pending Chunk loadCallback() and unloadCallback() after each run task
              if (ServerChunkCache.this.runDistanceManagerUpdates()) {
                  return true;
              } else {
                  ServerChunkCache.this.lightEngine.tryScheduleUpdate();
                  return super.pollTask();
              }
-+        } finally {
-+            ServerChunkCache.this.chunkMap.callbackExecutor.run();
++            // CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task
++            } finally {
++                ServerChunkCache.this.chunkMap.callbackExecutor.run();
++            }
++            // CraftBukkit end - process pending Chunk loadCallback() and unloadCallback() after each run task
          }
-+        // CraftBukkit end
-+        }
      }
- 
-     private static record ChunkAndHolder(LevelChunk chunk, ChunkHolder holder) {
+ }
diff --git a/paper-server/patches/sources/net/minecraft/server/level/ServerEntity.java.patch b/paper-server/patches/sources/net/minecraft/server/level/ServerEntity.java.patch
new file mode 100644
index 0000000000..4413d91d0f
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/level/ServerEntity.java.patch
@@ -0,0 +1,134 @@
+--- a/net/minecraft/server/level/ServerEntity.java
++++ b/net/minecraft/server/level/ServerEntity.java
+@@ -65,13 +_,17 @@
+     private Vec3 lastSentMovement;
+     private int tickCount;
+     private int teleportDelay;
+-    private List<Entity> lastPassengers = Collections.emptyList();
++    private List<Entity> lastPassengers = com.google.common.collect.ImmutableList.of(); // Paper - optimize passenger checks
+     private boolean wasRiding;
+     private boolean wasOnGround;
+     @Nullable
+     private List<SynchedEntityData.DataValue<?>> trackedDataValues;
+ 
+-    public ServerEntity(ServerLevel level, Entity entity, int updateInterval, boolean trackDelta, Consumer<Packet<?>> broadcast) {
++    // CraftBukkit start
++    private final Set<net.minecraft.server.network.ServerPlayerConnection> trackedPlayers;
++    public ServerEntity(ServerLevel level, Entity entity, int updateInterval, boolean trackDelta, Consumer<Packet<?>> broadcast, final Set<net.minecraft.server.network.ServerPlayerConnection> trackedPlayers) {
++        this.trackedPlayers = trackedPlayers;
++    // CraftBukkit end
+         this.level = level;
+         this.broadcast = broadcast;
+         this.entity = entity;
+@@ -89,7 +_,7 @@
+     public void sendChanges() {
+         List<Entity> passengers = this.entity.getPassengers();
+         if (!passengers.equals(this.lastPassengers)) {
+-            this.broadcast.accept(new ClientboundSetPassengersPacket(this.entity));
++            this.broadcastAndSend(new ClientboundSetPassengersPacket(this.entity)); // CraftBukkit
+             removedPassengers(passengers, this.lastPassengers)
+                 .forEach(
+                     removedPassenger -> {
+@@ -102,10 +_,10 @@
+             this.lastPassengers = passengers;
+         }
+ 
+-        if (this.entity instanceof ItemFrame itemFrame && this.tickCount % 10 == 0) {
++        if (!this.trackedPlayers.isEmpty() && this.entity instanceof ItemFrame itemFrame /*&& this.tickCount % 10 == 0*/) { // CraftBukkit - moved tickCount below // Paper - Perf: Only tick item frames if players can see it
+             ItemStack item = itemFrame.getItem();
+-            if (item.getItem() instanceof MapItem) {
+-                MapId mapId = item.get(DataComponents.MAP_ID);
++            if (this.level.paperConfig().maps.itemFrameCursorUpdateInterval > 0 && this.tickCount % this.level.paperConfig().maps.itemFrameCursorUpdateInterval == 0 && item.getItem() instanceof MapItem) { // CraftBukkit - Moved this.tickCounter % 10 logic here so item frames do not enter the other blocks // Paper - Make item frame map cursor update interval configurable
++                MapId mapId = itemFrame.cachedMapId; // Paper - Perf: Cache map ids on item frames
+                 MapItemSavedData savedData = MapItem.getSavedData(mapId, this.level);
+                 if (savedData != null) {
+                     for (ServerPlayer serverPlayer : this.level.players()) {
+@@ -141,7 +_,13 @@
+             } else {
+                 this.teleportDelay++;
+                 Vec3 vec3 = this.entity.trackingPosition();
+-                boolean flag1 = this.positionCodec.delta(vec3).lengthSqr() >= 7.6293945E-6F;
++                // Paper start - reduce allocation of Vec3D here
++                Vec3 base = this.positionCodec.base;
++                double vec3_dx = vec3.x - base.x;
++                double vec3_dy = vec3.y - base.y;
++                double vec3_dz = vec3.z - base.z;
++                boolean flag1 = (vec3_dx * vec3_dx + vec3_dy * vec3_dy + vec3_dz * vec3_dz) >= 7.62939453125E-6D;
++                // Paper end - reduce allocation of Vec3D here
+                 Packet<?> packet = null;
+                 boolean flag2 = flag1 || this.tickCount % 60 == 0;
+                 boolean flag3 = false;
+@@ -219,6 +_,25 @@
+ 
+         this.tickCount++;
+         if (this.entity.hurtMarked) {
++            // CraftBukkit start - Create PlayerVelocity event
++            boolean cancelled = false;
++
++            if (this.entity instanceof ServerPlayer) {
++                org.bukkit.entity.Player player = (org.bukkit.entity.Player) this.entity.getBukkitEntity();
++                org.bukkit.util.Vector velocity = player.getVelocity();
++
++                org.bukkit.event.player.PlayerVelocityEvent event = new org.bukkit.event.player.PlayerVelocityEvent(player, velocity.clone());
++                if (!event.callEvent()) {
++                    cancelled = true;
++                } else if (!velocity.equals(event.getVelocity())) {
++                    player.setVelocity(event.getVelocity());
++                }
++            }
++
++            if (cancelled) {
++                return;
++            }
++            // CraftBukkit end
+             this.entity.hurtMarked = false;
+             this.broadcastAndSend(new ClientboundSetEntityMotionPacket(this.entity));
+         }
+@@ -273,7 +_,10 @@
+ 
+     public void sendPairingData(ServerPlayer player, Consumer<Packet<ClientGamePacketListener>> consumer) {
+         if (this.entity.isRemoved()) {
+-            LOGGER.warn("Fetching packet for removed entity {}", this.entity);
++            // CraftBukkit start - Remove useless error spam, just return
++            // LOGGER.warn("Fetching packet for removed entity {}", this.entity);
++            return;
++            // CraftBukkit end
+         }
+ 
+         Packet<ClientGamePacketListener> addEntityPacket = this.entity.getAddEntityPacket(this);
+@@ -285,6 +_,12 @@
+         boolean flag = this.trackDelta;
+         if (this.entity instanceof LivingEntity) {
+             Collection<AttributeInstance> syncableAttributes = ((LivingEntity)this.entity).getAttributes().getSyncableAttributes();
++
++            // CraftBukkit start - If sending own attributes send scaled health instead of current maximum health
++            if (this.entity.getId() == player.getId()) {
++                ((ServerPlayer) this.entity).getBukkitEntity().injectScaledMaxHealth(syncableAttributes, false);
++            }
++            // CraftBukkit end
+             if (!syncableAttributes.isEmpty()) {
+                 consumer.accept(new ClientboundUpdateAttributesPacket(this.entity.getId(), syncableAttributes));
+             }
+@@ -309,8 +_,9 @@
+             }
+ 
+             if (!list.isEmpty()) {
+-                consumer.accept(new ClientboundSetEquipmentPacket(this.entity.getId(), list));
++                consumer.accept(new ClientboundSetEquipmentPacket(this.entity.getId(), list, true)); // Paper - data sanitization
+             }
++            ((LivingEntity) this.entity).detectEquipmentUpdatesPublic(); // CraftBukkit - SPIGOT-3789: sync again immediately after sending
+         }
+ 
+         if (!this.entity.getPassengers().isEmpty()) {
+@@ -357,6 +_,11 @@
+         if (this.entity instanceof LivingEntity) {
+             Set<AttributeInstance> attributesToSync = ((LivingEntity)this.entity).getAttributes().getAttributesToSync();
+             if (!attributesToSync.isEmpty()) {
++                // CraftBukkit start - Send scaled max health
++                if (this.entity instanceof ServerPlayer serverPlayer) {
++                    serverPlayer.getBukkitEntity().injectScaledMaxHealth(attributesToSync, false);
++                }
++                // CraftBukkit end
+                 this.broadcastAndSend(new ClientboundUpdateAttributesPacket(this.entity.getId(), attributesToSync));
+             }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/server/level/ServerLevel.java.patch b/paper-server/patches/sources/net/minecraft/server/level/ServerLevel.java.patch
new file mode 100644
index 0000000000..acc43a7716
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/level/ServerLevel.java.patch
@@ -0,0 +1,1202 @@
+--- a/net/minecraft/server/level/ServerLevel.java
++++ b/net/minecraft/server/level/ServerLevel.java
+@@ -182,7 +_,7 @@
+     final List<ServerPlayer> players = Lists.newArrayList();
+     public final ServerChunkCache chunkSource;
+     private final MinecraftServer server;
+-    public final ServerLevelData serverLevelData;
++    public final net.minecraft.world.level.storage.PrimaryLevelData serverLevelData; // CraftBukkit - type
+     private int lastSpawnChunkRadius;
+     final EntityTickList entityTickList = new EntityTickList();
+     public final PersistentEntitySectionManager<Entity> entityManager;
+@@ -209,11 +_,132 @@
+     private final boolean tickTime;
+     private final RandomSequences randomSequences;
+ 
++    // CraftBukkit start
++    public final LevelStorageSource.LevelStorageAccess levelStorageAccess;
++    public final UUID uuid;
++    public boolean hasPhysicsEvent = true; // Paper - BlockPhysicsEvent
++    public boolean hasEntityMoveEvent; // Paper - Add EntityMoveEvent
++
++    public LevelChunk getChunkIfLoaded(int x, int z) {
++        return this.chunkSource.getChunkAtIfLoadedImmediately(x, z); // Paper - Use getChunkIfLoadedImmediately
++    }
++
++    @Override
++    public ResourceKey<LevelStem> getTypeKey() {
++        return this.levelStorageAccess.dimensionType;
++    }
++
++    // Paper start
++    public final boolean areChunksLoadedForMove(AABB axisalignedbb) {
++        // copied code from collision methods, so that we can guarantee that they wont load chunks (we don't override
++        // ICollisionAccess methods for VoxelShapes)
++        // be more strict too, add a block (dumb plugins in move events?)
++        int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3;
++        int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3;
++
++        int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3;
++        int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3;
++
++        int minChunkX = minBlockX >> 4;
++        int maxChunkX = maxBlockX >> 4;
++
++        int minChunkZ = minBlockZ >> 4;
++        int maxChunkZ = maxBlockZ >> 4;
++
++        ServerChunkCache chunkProvider = this.getChunkSource();
++
++        for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
++            for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
++                if (chunkProvider.getChunkAtIfLoadedImmediately(cx, cz) == null) {
++                    return false;
++                }
++            }
++        }
++
++        return true;
++    }
++
++    public final void loadChunksForMoveAsync(AABB axisalignedbb, ca.spottedleaf.concurrentutil.util.Priority priority,
++                                             java.util.function.Consumer<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
++        if (Thread.currentThread() != this.thread) {
++            this.getChunkSource().mainThreadProcessor.execute(() -> {
++                this.loadChunksForMoveAsync(axisalignedbb, priority, onLoad);
++            });
++            return;
++        }
++        int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3;
++        int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3;
++
++        int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3;
++        int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3;
++
++        int minChunkX = minBlockX >> 4;
++        int minChunkZ = minBlockZ >> 4;
++
++        int maxChunkX = maxBlockX >> 4;
++        int maxChunkZ = maxBlockZ >> 4;
++
++        this.loadChunks(minChunkX, minChunkZ, maxChunkX, maxChunkZ, priority, onLoad);
++    }
++
++    public final void loadChunks(int minChunkX, int minChunkZ, int maxChunkX, int maxChunkZ,
++                                 ca.spottedleaf.concurrentutil.util.Priority priority,
++                                 java.util.function.Consumer<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
++        List<net.minecraft.world.level.chunk.ChunkAccess> ret = new java.util.ArrayList<>();
++        it.unimi.dsi.fastutil.ints.IntArrayList ticketLevels = new it.unimi.dsi.fastutil.ints.IntArrayList();
++        ServerChunkCache chunkProvider = this.getChunkSource();
++
++        int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1);
++        int[] loadedChunks = new int[1];
++
++        Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter++);
++
++        java.util.function.Consumer<net.minecraft.world.level.chunk.ChunkAccess> consumer = (net.minecraft.world.level.chunk.ChunkAccess chunk) -> {
++            if (chunk != null) {
++                int ticketLevel = Math.max(33, chunkProvider.chunkMap.getUpdatingChunkIfPresent(chunk.getPos().toLong()).getTicketLevel());
++                ret.add(chunk);
++                ticketLevels.add(ticketLevel);
++                chunkProvider.addTicketAtLevel(TicketType.FUTURE_AWAIT, chunk.getPos(), ticketLevel, holderIdentifier);
++            }
++            if (++loadedChunks[0] == requiredChunks) {
++                try {
++                    onLoad.accept(java.util.Collections.unmodifiableList(ret));
++                } finally {
++                    for (int i = 0, len = ret.size(); i < len; ++i) {
++                        ChunkPos chunkPos = ret.get(i).getPos();
++                        int ticketLevel = ticketLevels.getInt(i);
++
++                        chunkProvider.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos);
++                        chunkProvider.removeTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, holderIdentifier);
++                    }
++                }
++            }
++        };
++
++        for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
++            for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
++                ca.spottedleaf.moonrise.common.PlatformHooks.get().scheduleChunkLoad(
++                    this, cx, cz, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true, priority, consumer
++                );
++            }
++        }
++    }
++    // Paper end
++
++    // Paper start - optimise getPlayerByUUID
++    @Nullable
++    @Override
++    public Player getPlayerByUUID(UUID uuid) {
++        final Player player = this.getServer().getPlayerList().getPlayer(uuid);
++        return player != null && player.level() == this ? player : null;
++    }
++    // Paper end - optimise getPlayerByUUID
++
+     public ServerLevel(
+         MinecraftServer server,
+         Executor dispatcher,
+         LevelStorageSource.LevelStorageAccess levelStorageAccess,
+-        ServerLevelData serverLevelData,
++        net.minecraft.world.level.storage.PrimaryLevelData serverLevelData, // CraftBukkit
+         ResourceKey<Level> dimension,
+         LevelStem levelStem,
+         ChunkProgressListener progressListener,
+@@ -221,14 +_,38 @@
+         long biomeZoomSeed,
+         List<CustomSpawner> customSpawners,
+         boolean tickTime,
+-        @Nullable RandomSequences randomSequences
++        @Nullable RandomSequences randomSequences,
++        org.bukkit.World.Environment env, // CraftBukkit
++        org.bukkit.generator.ChunkGenerator gen, // CraftBukkit
++        org.bukkit.generator.BiomeProvider biomeProvider // CraftBukkit
+     ) {
+-        super(serverLevelData, dimension, server.registryAccess(), levelStem.type(), false, isDebug, biomeZoomSeed, server.getMaxChainedNeighborUpdates());
++        // CraftBukkit start
++        super(serverLevelData, dimension, server.registryAccess(), levelStem.type(), false, isDebug, biomeZoomSeed, server.getMaxChainedNeighborUpdates(), gen, biomeProvider, env, spigotConfig -> server.paperConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(levelStorageAccess.levelDirectory.path(), serverLevelData.getLevelName(), dimension.location(), spigotConfig, server.registryAccess(), serverLevelData.getGameRules()))); // Paper - create paper world configs
++        this.pvpMode = server.isPvpAllowed();
++        this.levelStorageAccess = levelStorageAccess;
++        this.uuid = org.bukkit.craftbukkit.util.WorldUUID.getUUID(levelStorageAccess.levelDirectory.path().toFile());
++        // CraftBukkit end
+         this.tickTime = tickTime;
+         this.server = server;
+         this.customSpawners = customSpawners;
+         this.serverLevelData = serverLevelData;
+         ChunkGenerator chunkGenerator = levelStem.generator();
++        // CraftBukkit start
++        this.serverLevelData.setWorld(this);
++
++        if (biomeProvider != null) {
++            net.minecraft.world.level.biome.BiomeSource worldChunkManager = new org.bukkit.craftbukkit.generator.CustomWorldChunkManager(this.getWorld(), biomeProvider, this.server.registryAccess().lookupOrThrow(Registries.BIOME), chunkGenerator.getBiomeSource()); // Paper - add vanillaBiomeProvider
++            if (chunkGenerator instanceof net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator cga) {
++                chunkGenerator = new net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator(worldChunkManager, cga.settings);
++            } else if (chunkGenerator instanceof net.minecraft.world.level.levelgen.FlatLevelSource cpf) {
++                chunkGenerator = new net.minecraft.world.level.levelgen.FlatLevelSource(cpf.settings(), worldChunkManager);
++            }
++        }
++
++        if (gen != null) {
++            chunkGenerator = new org.bukkit.craftbukkit.generator.CustomChunkGenerator(this, chunkGenerator, gen);
++        }
++        // CraftBukkit end
+         boolean flag = server.forceSynchronousWrites();
+         DataFixer fixerUpper = server.getFixerUpper();
+         EntityPersistentStorage<Entity> entityPersistentStorage = new EntityStorage(
+@@ -250,8 +_,8 @@
+             server.getStructureManager(),
+             dispatcher,
+             chunkGenerator,
+-            server.getPlayerList().getViewDistance(),
+-            server.getPlayerList().getSimulationDistance(),
++            this.spigotConfig.viewDistance, // Spigot
++            this.spigotConfig.simulationDistance, // Spigot
+             flag,
+             progressListener,
+             this.entityManager::updateChunkStatus,
+@@ -272,7 +_,7 @@
+             this.chunkSource.chunkScanner(),
+             this.registryAccess(),
+             server.getStructureManager(),
+-            dimension,
++            getTypeKey(), // Paper - Fix missing CB diff
+             chunkGenerator,
+             this.chunkSource.randomState(),
+             this,
+@@ -281,7 +_,7 @@
+             fixerUpper
+         );
+         this.structureManager = new StructureManager(this, server.getWorldData().worldGenOptions(), this.structureCheck);
+-        if (this.dimension() == Level.END && this.dimensionTypeRegistration().is(BuiltinDimensionTypes.END)) {
++        if (this.dimension() == Level.END && this.dimensionTypeRegistration().is(BuiltinDimensionTypes.END) || env == org.bukkit.World.Environment.THE_END) { // CraftBukkit - Allow to create EnderDragonBattle in default and custom END
+             this.dragonFight = new EndDragonFight(this, seed, server.getWorldData().endDragonFightData());
+         } else {
+             this.dragonFight = null;
+@@ -292,7 +_,15 @@
+         this.randomSequences = Objects.requireNonNullElseGet(
+             randomSequences, () -> this.getDataStorage().computeIfAbsent(RandomSequences.factory(seed), "random_sequences")
+         );
+-    }
++        this.getCraftServer().addWorld(this.getWorld()); // CraftBukkit
++    }
++
++    // Paper start
++    @Override
++    public boolean hasChunk(int chunkX, int chunkZ) {
++        return this.getChunkSource().getChunkAtIfLoadedImmediately(chunkX, chunkZ) != null;
++    }
++    // Paper end
+ 
+     @Deprecated
+     @VisibleForTesting
+@@ -304,8 +_,8 @@
+         this.serverLevelData.setClearWeatherTime(clearTime);
+         this.serverLevelData.setRainTime(weatherTime);
+         this.serverLevelData.setThunderTime(weatherTime);
+-        this.serverLevelData.setRaining(isRaining);
+-        this.serverLevelData.setThundering(isThundering);
++        this.serverLevelData.setRaining(isRaining, org.bukkit.event.weather.WeatherChangeEvent.Cause.COMMAND); // Paper - Add cause to Weather/ThunderChangeEvents
++        this.serverLevelData.setThundering(isThundering, org.bukkit.event.weather.ThunderChangeEvent.Cause.COMMAND); // Paper - Add cause to Weather/ThunderChangeEvents
+     }
+ 
+     @Override
+@@ -332,12 +_,25 @@
+ 
+         int _int = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE);
+         if (this.sleepStatus.areEnoughSleeping(_int) && this.sleepStatus.areEnoughDeepSleeping(_int, this.players)) {
++            // Paper start - create time skip event - move up calculations
++            final long newDayTime = this.levelData.getDayTime() + 24000L;
++            org.bukkit.event.world.TimeSkipEvent event = new org.bukkit.event.world.TimeSkipEvent(
++                this.getWorld(),
++                org.bukkit.event.world.TimeSkipEvent.SkipReason.NIGHT_SKIP,
++                (newDayTime - newDayTime % 24000L) - this.getDayTime()
++            );
++            // Paper end - create time skip event - move up calculations
+             if (this.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) {
+-                long l = this.levelData.getDayTime() + 24000L;
+-                this.setDayTime(l - l % 24000L);
++                // Paper start - call time skip event if gamerule is enabled
++                // long l = this.levelData.getDayTime() + 24000L; // Paper - diff on change to above - newDayTime
++                // this.setDayTime(l - l % 24000L); // Paper - diff on change to above - event param
++                if (event.callEvent()) {
++                    this.setDayTime(this.getDayTime() + event.getSkipAmount());
++                }
++                // Paper end - call time skip event if gamerule is enabled
+             }
+ 
+-            this.wakeUpAllPlayers();
++            if (!event.isCancelled()) this.wakeUpAllPlayers(); // Paper - only wake up players if time skip event is not cancelled
+             if (this.getGameRules().getBoolean(GameRules.RULE_WEATHER_CYCLE) && this.isRaining()) {
+                 this.resetWeatherCycle();
+             }
+@@ -352,9 +_,9 @@
+         if (!this.isDebug() && runsNormally) {
+             long l = this.getGameTime();
+             profilerFiller.push("blockTicks");
+-            this.blockTicks.tick(l, 65536, this::tickBlock);
++            this.blockTicks.tick(l, paperConfig().environment.maxBlockTicks, this::tickBlock); // Paper - configurable max block ticks
+             profilerFiller.popPush("fluidTicks");
+-            this.fluidTicks.tick(l, 65536, this::tickFluid);
++            this.fluidTicks.tick(l, paperConfig().environment.maxFluidTicks, this::tickFluid); // Paper - configurable max fluid ticks
+             profilerFiller.pop();
+         }
+ 
+@@ -372,7 +_,7 @@
+ 
+         this.handlingTick = false;
+         profilerFiller.pop();
+-        boolean flag = !this.players.isEmpty() || !this.getForcedChunks().isEmpty();
++        boolean flag = !paperConfig().unsupportedSettings.disableWorldTickingWhenEmpty || !this.players.isEmpty() || !this.getForcedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players // Paper - restore this
+         if (flag) {
+             this.resetEmptyTime();
+         }
+@@ -461,12 +_,12 @@
+         int minBlockZ = pos.getMinBlockZ();
+         ProfilerFiller profilerFiller = Profiler.get();
+         profilerFiller.push("thunder");
+-        if (isRaining && this.isThundering() && this.random.nextInt(100000) == 0) {
++        if (!this.paperConfig().environment.disableThunder && isRaining && this.isThundering() && this.spigotConfig.thunderChance > 0 && this.random.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot // Paper - Option to disable thunder
+             BlockPos blockPos = this.findLightningTargetAround(this.getBlockRandomPos(minBlockX, 0, minBlockZ, 15));
+             if (this.isRainingAt(blockPos)) {
+                 DifficultyInstance currentDifficultyAt = this.getCurrentDifficultyAt(blockPos);
+                 boolean flag = this.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)
+-                    && this.random.nextDouble() < currentDifficultyAt.getEffectiveDifficulty() * 0.01
++                    && this.random.nextDouble() < currentDifficultyAt.getEffectiveDifficulty() * this.paperConfig().entities.spawning.skeletonHorseThunderSpawnChance.or(0.01) // Paper - Configurable spawn chances for skeleton horses
+                     && !this.getBlockState(blockPos.below()).is(Blocks.LIGHTNING_ROD);
+                 if (flag) {
+                     SkeletonHorse skeletonHorse = EntityType.SKELETON_HORSE.create(this, EntitySpawnReason.EVENT);
+@@ -474,7 +_,7 @@
+                         skeletonHorse.setTrap(true);
+                         skeletonHorse.setAge(0);
+                         skeletonHorse.setPos(blockPos.getX(), blockPos.getY(), blockPos.getZ());
+-                        this.addFreshEntity(skeletonHorse);
++                        this.addFreshEntity(skeletonHorse, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.LIGHTNING); // CraftBukkit
+                     }
+                 }
+ 
+@@ -482,18 +_,20 @@
+                 if (lightningBolt != null) {
+                     lightningBolt.moveTo(Vec3.atBottomCenterOf(blockPos));
+                     lightningBolt.setVisualOnly(flag);
+-                    this.addFreshEntity(lightningBolt);
++                    this.strikeLightning(lightningBolt, org.bukkit.event.weather.LightningStrikeEvent.Cause.WEATHER); // CraftBukkit
+                 }
+             }
+         }
+ 
+         profilerFiller.popPush("iceandsnow");
+ 
++        if (!this.paperConfig().environment.disableIceAndSnow) { // Paper - Option to disable ice and snow
+         for (int i = 0; i < randomTickSpeed; i++) {
+             if (this.random.nextInt(48) == 0) {
+                 this.tickPrecipitation(this.getBlockRandomPos(minBlockX, 0, minBlockZ, 15));
+             }
+         }
++        } // Paper - Option to disable ice and snow
+ 
+         profilerFiller.popPush("tickBlocks");
+         if (randomTickSpeed > 0) {
+@@ -535,7 +_,7 @@
+         BlockPos blockPos1 = heightmapPos.below();
+         Biome biome = this.getBiome(heightmapPos).value();
+         if (biome.shouldFreeze(this, blockPos1)) {
+-            this.setBlockAndUpdate(blockPos1, Blocks.ICE.defaultBlockState());
++            org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, blockPos1, Blocks.ICE.defaultBlockState(), null); // CraftBukkit
+         }
+ 
+         if (this.isRaining()) {
+@@ -547,10 +_,10 @@
+                     if (layersValue < Math.min(_int, 8)) {
+                         BlockState blockState1 = blockState.setValue(SnowLayerBlock.LAYERS, Integer.valueOf(layersValue + 1));
+                         Block.pushEntitiesUp(blockState, blockState1, this, heightmapPos);
+-                        this.setBlockAndUpdate(heightmapPos, blockState1);
++                        org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, heightmapPos, blockState1, null); // CraftBukkit
+                     }
+                 } else {
+-                    this.setBlockAndUpdate(heightmapPos, Blocks.SNOW.defaultBlockState());
++                    org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, heightmapPos, Blocks.SNOW.defaultBlockState(), null); // CraftBukkit
+                 }
+             }
+ 
+@@ -575,6 +_,11 @@
+     }
+ 
+     protected BlockPos findLightningTargetAround(BlockPos pos) {
++        // Paper start - Add methods to find targets for lightning strikes
++        return this.findLightningTargetAround(pos, false);
++    }
++    public BlockPos findLightningTargetAround(BlockPos pos, boolean returnNullWhenNoTarget) {
++        // Paper end - Add methods to find targets for lightning strikes
+         BlockPos heightmapPos = this.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, pos);
+         Optional<BlockPos> optional = this.findLightningRod(heightmapPos);
+         if (optional.isPresent()) {
+@@ -582,11 +_,12 @@
+         } else {
+             AABB aabb = AABB.encapsulatingFullBlocks(heightmapPos, heightmapPos.atY(this.getMaxY() + 1)).inflate(3.0);
+             List<LivingEntity> entitiesOfClass = this.getEntitiesOfClass(
+-                LivingEntity.class, aabb, entity -> entity != null && entity.isAlive() && this.canSeeSky(entity.blockPosition())
++                LivingEntity.class, aabb, entity -> entity != null && entity.isAlive() && this.canSeeSky(entity.blockPosition()) && !entity.isSpectator() // Paper - Fix lightning being able to hit spectators (MC-262422)
+             );
+             if (!entitiesOfClass.isEmpty()) {
+                 return entitiesOfClass.get(this.random.nextInt(entitiesOfClass.size())).blockPosition();
+             } else {
++                if (returnNullWhenNoTarget) return null; // Paper - Add methods to find targets for lightning strikes
+                 if (heightmapPos.getY() == this.getMinY() - 1) {
+                     heightmapPos = heightmapPos.above(2);
+                 }
+@@ -673,8 +_,8 @@
+                 this.serverLevelData.setThunderTime(thunderTime);
+                 this.serverLevelData.setRainTime(rainTime);
+                 this.serverLevelData.setClearWeatherTime(clearWeatherTime);
+-                this.serverLevelData.setThundering(isThundering);
+-                this.serverLevelData.setRaining(isRaining1);
++                this.serverLevelData.setThundering(isThundering, org.bukkit.event.weather.ThunderChangeEvent.Cause.NATURAL); // Paper - Add cause to Weather/ThunderChangeEvents
++                this.serverLevelData.setRaining(isRaining1, org.bukkit.event.weather.WeatherChangeEvent.Cause.NATURAL); // Paper - Add cause to Weather/ThunderChangeEvents
+             }
+ 
+             this.oThunderLevel = this.thunderLevel;
+@@ -695,6 +_,7 @@
+             this.rainLevel = Mth.clamp(this.rainLevel, 0.0F, 1.0F);
+         }
+ 
++        /* CraftBukkit start
+         if (this.oRainLevel != this.rainLevel) {
+             this.server
+                 .getPlayerList()
+@@ -717,14 +_,47 @@
+             this.server.getPlayerList().broadcastAll(new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, this.rainLevel));
+             this.server.getPlayerList().broadcastAll(new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, this.thunderLevel));
+         }
++        */
++        for (ServerPlayer player : this.players) {
++            if (player.level() == this) {
++                player.tickWeather();
++            }
++        }
++
++        if (isRaining != this.isRaining()) {
++            // Only send weather packets to those affected
++            for (ServerPlayer player : this.players) {
++                if (player.level() == this) {
++                    player.setPlayerWeather((!isRaining ? org.bukkit.WeatherType.DOWNFALL : org.bukkit.WeatherType.CLEAR), false);
++                }
++            }
++        }
++        for (ServerPlayer player : this.players) {
++            if (player.level() == this) {
++                player.updateWeather(this.oRainLevel, this.rainLevel, this.oThunderLevel, this.thunderLevel);
++            }
++        }
++        // CraftBukkit end
+     }
+ 
+     @VisibleForTesting
+     public void resetWeatherCycle() {
+-        this.serverLevelData.setRainTime(0);
+-        this.serverLevelData.setRaining(false);
+-        this.serverLevelData.setThunderTime(0);
+-        this.serverLevelData.setThundering(false);
++        // CraftBukkit start
++        this.serverLevelData.setRaining(false, org.bukkit.event.weather.WeatherChangeEvent.Cause.SLEEP); // Paper - Add cause to Weather/ThunderChangeEvents
++        // If we stop due to everyone sleeping we should reset the weather duration to some other random value.
++        // Not that everyone ever manages to get the whole server to sleep at the same time....
++        if (!this.serverLevelData.isRaining()) {
++            this.serverLevelData.setRainTime(0);
++        }
++        // CraftBukkit end
++        this.serverLevelData.setThundering(false, org.bukkit.event.weather.ThunderChangeEvent.Cause.SLEEP); // Paper - Add cause to Weather/ThunderChangeEvents
++        // CraftBukkit start
++        // If we stop due to everyone sleeping we should reset the weather duration to some other random value.
++        // Not that everyone ever manages to get the whole server to sleep at the same time....
++        if (!this.serverLevelData.isThundering()) {
++            this.serverLevelData.setThunderTime(0);
++        }
++        // CraftBukkit end
+     }
+ 
+     public void resetEmptyTime() {
+@@ -746,18 +_,45 @@
+         }
+     }
+ 
++    // Paper start - log detailed entity tick information
++    // TODO replace with varhandle
++    static final java.util.concurrent.atomic.AtomicReference<Entity> currentlyTickingEntity = new java.util.concurrent.atomic.AtomicReference<>();
++
++    public static List<Entity> getCurrentlyTickingEntities() {
++        Entity ticking = currentlyTickingEntity.get();
++        List<Entity> ret = java.util.Arrays.asList(ticking == null ? new Entity[0] : new Entity[] { ticking });
++
++        return ret;
++    }
++    // Paper end - log detailed entity tick information
++
+     public void tickNonPassenger(Entity entity) {
++        // Paper start - log detailed entity tick information
++        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread("Cannot tick an entity off-main");
++        try {
++            if (currentlyTickingEntity.get() == null) {
++                currentlyTickingEntity.lazySet(entity);
++            }
++            // Paper end - log detailed entity tick information
+         entity.setOldPosAndRot();
+         ProfilerFiller profilerFiller = Profiler.get();
+         entity.tickCount++;
+         profilerFiller.push(() -> BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString());
+         profilerFiller.incrementCounter("tickNonPassenger");
+         entity.tick();
++        entity.postTick(); // CraftBukkit
+         profilerFiller.pop();
+ 
+         for (Entity entity1 : entity.getPassengers()) {
+             this.tickPassenger(entity, entity1);
+         }
++        // Paper start - log detailed entity tick information
++        } finally {
++            if (currentlyTickingEntity.get() == entity) {
++                currentlyTickingEntity.lazySet(null);
++            }
++        }
++        // Paper end - log detailed entity tick information
+     }
+ 
+     private void tickPassenger(Entity ridingEntity, Entity passengerEntity) {
+@@ -770,6 +_,7 @@
+             profilerFiller.push(() -> BuiltInRegistries.ENTITY_TYPE.getKey(passengerEntity.getType()).toString());
+             profilerFiller.incrementCounter("tickPassenger");
+             passengerEntity.rideTick();
++            passengerEntity.postTick(); // CraftBukkit
+             profilerFiller.pop();
+ 
+             for (Entity entity : passengerEntity.getPassengers()) {
+@@ -786,6 +_,7 @@
+     public void save(@Nullable ProgressListener progress, boolean flush, boolean skipSave) {
+         ServerChunkCache chunkSource = this.getChunkSource();
+         if (!skipSave) {
++            org.bukkit.Bukkit.getPluginManager().callEvent(new org.bukkit.event.world.WorldSaveEvent(this.getWorld())); // CraftBukkit
+             if (progress != null) {
+                 progress.progressStartNoAbort(Component.translatable("menu.savingLevel"));
+             }
+@@ -802,11 +_,19 @@
+                 this.entityManager.autoSave();
+             }
+         }
++
++        // CraftBukkit start - moved from MinecraftServer.saveChunks
++        ServerLevel worldserver1 = this;
++
++        this.serverLevelData.setWorldBorder(worldserver1.getWorldBorder().createSettings());
++        this.serverLevelData.setCustomBossEvents(this.server.getCustomBossEvents().save(this.registryAccess()));
++        this.levelStorageAccess.saveDataTag(this.server.registryAccess(), this.serverLevelData, this.server.getPlayerList().getSingleplayerData());
++        // CraftBukkit end
+     }
+ 
+     private void saveLevelData(boolean join) {
+         if (this.dragonFight != null) {
+-            this.server.getWorldData().setEndDragonFightData(this.dragonFight.saveData());
++            this.serverLevelData.setEndDragonFightData(this.dragonFight.saveData()); // CraftBukkit
+         }
+ 
+         DimensionDataStorage dataStorage = this.getChunkSource().getDataStorage();
+@@ -871,18 +_,40 @@
+ 
+     @Override
+     public boolean addFreshEntity(Entity entity) {
+-        return this.addEntity(entity);
++        // CraftBukkit start
++        return this.addFreshEntity(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT);
++    }
++
++    @Override
++    public boolean addFreshEntity(Entity entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) {
++        return this.addEntity(entity, reason);
++        // CraftBukkit end
+     }
+ 
+     public boolean addWithUUID(Entity entity) {
+-        return this.addEntity(entity);
++        // CraftBukkit start
++        return this.addWithUUID(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT);
++    }
++
++    public boolean addWithUUID(Entity entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) {
++        return this.addEntity(entity, reason);
++        // CraftBukkit end
+     }
+ 
+     public void addDuringTeleport(Entity entity) {
++        // CraftBukkit start
++        // SPIGOT-6415: Don't call spawn event for entities which travel trough worlds,
++        // since it is only an implementation detail, that a new entity is created when
++        // they are traveling between worlds.
++        this.addDuringTeleport(entity, null);
++    }
++
++    public void addDuringTeleport(Entity entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) {
++        // CraftBukkit end
+         if (entity instanceof ServerPlayer serverPlayer) {
+             this.addPlayer(serverPlayer);
+         } else {
+-            this.addEntity(entity);
++            this.addEntity(entity, reason); // CraftBukkit
+         }
+     }
+ 
+@@ -905,40 +_,119 @@
+         this.entityManager.addNewEntity(player);
+     }
+ 
+-    private boolean addEntity(Entity entity) {
++    // CraftBukkit start
++    private boolean addEntity(Entity entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason spawnReason) {
++        org.spigotmc.AsyncCatcher.catchOp("entity add"); // Spigot
++        entity.generation = false; // Paper - Don't fire sync event during generation; Reset flag if it was added during a ServerLevel generation process
++        // Paper start - extra debug info
++        if (entity.valid) {
++            MinecraftServer.LOGGER.error("Attempted Double World add on {}", entity, new Throwable());
++            return true;
++        }
++        // Paper end - extra debug info
++        if (entity.spawnReason == null) entity.spawnReason = spawnReason; // Paper - Entity#getEntitySpawnReason
+         if (entity.isRemoved()) {
+-            LOGGER.warn("Tried to add entity {} but it was marked as removed already", EntityType.getKey(entity.getType()));
++            // LOGGER.warn("Tried to add entity {} but it was marked as removed already", EntityType.getKey(entity.getType())); // CraftBukkit - remove warning
+             return false;
+         } else {
++            if (entity instanceof net.minecraft.world.entity.item.ItemEntity itemEntity && itemEntity.getItem().isEmpty()) return false; // Paper - Prevent empty items from being added
++            // Paper start - capture all item additions to the world
++            if (captureDrops != null && entity instanceof net.minecraft.world.entity.item.ItemEntity) {
++                captureDrops.add((net.minecraft.world.entity.item.ItemEntity) entity);
++                return true;
++            }
++            // Paper end - capture all item additions to the world
++            // SPIGOT-6415: Don't call spawn event when reason is null. For example when an entity teleports to a new world.
++            if (spawnReason != null && !org.bukkit.craftbukkit.event.CraftEventFactory.doEntityAddEventCalling(this, entity, spawnReason)) {
++                return false;
++            }
++            // CraftBukkit end
++
+             return this.entityManager.addNewEntity(entity);
+         }
+     }
+ 
+     public boolean tryAddFreshEntityWithPassengers(Entity entity) {
++        // CraftBukkit start
++        return this.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT);
++    }
++
++    public boolean tryAddFreshEntityWithPassengers(Entity entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) {
++        // CraftBukkit end
+         if (entity.getSelfAndPassengers().map(Entity::getUUID).anyMatch(this.entityManager::isLoaded)) {
+             return false;
+         } else {
+-            this.addFreshEntityWithPassengers(entity);
++            this.addFreshEntityWithPassengers(entity, reason); // CraftBukkit
+             return true;
+         }
+     }
+ 
+     public void unload(LevelChunk chunk) {
++        // Spigot start
++        for (net.minecraft.world.level.block.entity.BlockEntity blockEntity : chunk.getBlockEntities().values()) {
++            if (blockEntity instanceof net.minecraft.world.Container) {
++                // Paper start - this area looks like it can load chunks, change the behavior
++                // chests for example can apply physics to the world
++                // so instead we just change the active container and call the event
++                for (org.bukkit.entity.HumanEntity human : Lists.newArrayList(((net.minecraft.world.Container) blockEntity).getViewers())) {
++                    ((org.bukkit.craftbukkit.entity.CraftHumanEntity) human).getHandle().closeUnloadedInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason.UNLOADED); // Paper - Inventory close reason
++                }
++                // Paper end - this area looks like it can load chunks, change the behavior
++            }
++        }
++        // Spigot end
+         chunk.clearAllBlockEntities();
+         chunk.unregisterTickContainerFromLevel(this);
+     }
+ 
+     public void removePlayerImmediately(ServerPlayer player, Entity.RemovalReason reason) {
+-        player.remove(reason);
+-    }
++        player.remove(reason, null); // CraftBukkit - add Bukkit remove cause
++    }
++
++    // CraftBukkit start
++    public boolean strikeLightning(Entity entitylightning) {
++        return this.strikeLightning(entitylightning, org.bukkit.event.weather.LightningStrikeEvent.Cause.UNKNOWN);
++    }
++
++    public boolean strikeLightning(Entity entitylightning, org.bukkit.event.weather.LightningStrikeEvent.Cause cause) {
++        org.bukkit.event.weather.LightningStrikeEvent lightning = org.bukkit.craftbukkit.event.CraftEventFactory.callLightningStrikeEvent((org.bukkit.entity.LightningStrike) entitylightning.getBukkitEntity(), cause);
++
++        if (lightning.isCancelled()) {
++            return false;
++        }
++
++        return this.addFreshEntity(entitylightning);
++    }
++    // CraftBukkit end
+ 
+     @Override
+     public void destroyBlockProgress(int breakerId, BlockPos pos, int progress) {
++        // CraftBukkit start
++        Player breakerPlayer = null;
++        Entity entity = this.getEntity(breakerId);
++        if (entity instanceof Player) breakerPlayer = (Player) entity;
++        // CraftBukkit end
++
++        // Paper start - Add BlockBreakProgressUpdateEvent
++        // If a plugin is using this method to send destroy packets for a client-side only entity id, no block progress occurred on the server.
++        // Hence, do not call the event.
++        if (entity != null) {
++            float progressFloat = Mth.clamp(progress, 0, 10) / 10.0f;
++            org.bukkit.craftbukkit.block.CraftBlock bukkitBlock = org.bukkit.craftbukkit.block.CraftBlock.at(this, pos);
++            new io.papermc.paper.event.block.BlockBreakProgressUpdateEvent(bukkitBlock, progressFloat, entity.getBukkitEntity())
++                .callEvent();
++        }
++        // Paper end - Add BlockBreakProgressUpdateEvent
+         for (ServerPlayer serverPlayer : this.server.getPlayerList().getPlayers()) {
+             if (serverPlayer != null && serverPlayer.level() == this && serverPlayer.getId() != breakerId) {
+                 double d = pos.getX() - serverPlayer.getX();
+                 double d1 = pos.getY() - serverPlayer.getY();
+                 double d2 = pos.getZ() - serverPlayer.getZ();
++                // CraftBukkit start
++                if (breakerPlayer != null && !serverPlayer.getBukkitEntity().canSee(breakerPlayer.getBukkitEntity())) {
++                    continue;
++                }
++                // CraftBukkit end
+                 if (d * d + d1 * d1 + d2 * d2 < 1024.0) {
+                     serverPlayer.connection.send(new ClientboundBlockDestructionPacket(breakerId, pos, progress));
+                 }
+@@ -1000,7 +_,7 @@
+     public void levelEvent(@Nullable Player player, int type, BlockPos pos, int data) {
+         this.server
+             .getPlayerList()
+-            .broadcast(player, pos.getX(), pos.getY(), pos.getZ(), 64.0, this.dimension(), new ClientboundLevelEventPacket(type, pos, data, false));
++            .broadcast(player, pos.getX(), pos.getY(), pos.getZ(), 64.0, this.dimension(), new ClientboundLevelEventPacket(type, pos, data, false)); // Paper - diff on change (the 64.0 distance is used as defaults for sound ranges in spigot config for ender dragon, end portal and wither)
+     }
+ 
+     public int getLogicalHeight() {
+@@ -1009,6 +_,11 @@
+ 
+     @Override
+     public void gameEvent(Holder<GameEvent> gameEvent, Vec3 pos, GameEvent.Context context) {
++        // Paper start - Prevent GameEvents being fired from unloaded chunks
++        if (this.getChunkIfLoadedImmediately((Mth.floor(pos.x) >> 4), (Mth.floor(pos.z) >> 4)) == null) {
++            return;
++        }
++        // Paper end - Prevent GameEvents being fired from unloaded chunks
+         this.gameEventDispatcher.post(gameEvent, pos, context);
+     }
+ 
+@@ -1021,17 +_,28 @@
+ 
+         this.getChunkSource().blockChanged(pos);
+         this.pathTypesByPosCache.invalidate(pos);
++        if (this.paperConfig().misc.updatePathfindingOnBlockUpdate) { // Paper - option to disable pathfinding updates
+         VoxelShape collisionShape = oldState.getCollisionShape(this, pos);
+         VoxelShape collisionShape1 = newState.getCollisionShape(this, pos);
+         if (Shapes.joinIsNotEmpty(collisionShape, collisionShape1, BooleanOp.NOT_SAME)) {
+             List<PathNavigation> list = new ObjectArrayList<>();
+ 
++            try { // Paper - catch CME see below why
+             for (Mob mob : this.navigatingMobs) {
+                 PathNavigation navigation = mob.getNavigation();
+                 if (navigation.shouldRecomputePath(pos)) {
+                     list.add(navigation);
+                 }
+             }
++            // Paper start - catch CME see below why
++            } catch (final java.util.ConcurrentModificationException concurrentModificationException) {
++                // This can happen because the pathfinder update below may trigger a chunk load, which in turn may cause more navigators to register
++                // In this case we just run the update again across all the iterators as the chunk will then be loaded
++                // As this is a relative edge case it is much faster than copying navigators (on either read or write)
++                this.sendBlockUpdated(pos, oldState, newState, flags);
++                return;
++            }
++            // Paper end - catch CME see below why
+ 
+             try {
+                 this.isUpdatingNavigations = true;
+@@ -1043,15 +_,18 @@
+                 this.isUpdatingNavigations = false;
+             }
+         }
++        } // Paper - option to disable pathfinding updates
+     }
+ 
+     @Override
+     public void updateNeighborsAt(BlockPos pos, Block block) {
++        if (captureBlockStates) { return; } // Paper - Cancel all physics during placement
+         this.updateNeighborsAt(pos, block, ExperimentalRedstoneUtils.initialOrientation(this, null, null));
+     }
+ 
+     @Override
+     public void updateNeighborsAt(BlockPos pos, Block block, @Nullable Orientation orientation) {
++        if (captureBlockStates) { return; } // Paper - Cancel all physics during placement
+         this.neighborUpdater.updateNeighborsAtExceptFromFacing(pos, block, null, orientation);
+     }
+ 
+@@ -1100,6 +_,42 @@
+         ParticleOptions largeExplosionParticles,
+         Holder<SoundEvent> explosionSound
+     ) {
++        // CraftBukkit start
++        this.explode0(source, damageSource, damageCalculator, x, y, z, radius, fire, explosionInteraction, smallExplosionParticles, largeExplosionParticles, explosionSound);
++    }
++
++    public ServerExplosion explode0(
++        @Nullable Entity source,
++        @Nullable DamageSource damageSource,
++        @Nullable ExplosionDamageCalculator damageCalculator,
++        double x,
++        double y,
++        double z,
++        float radius,
++        boolean fire,
++        Level.ExplosionInteraction explosionInteraction,
++        ParticleOptions smallExplosionParticles,
++        ParticleOptions largeExplosionParticles,
++        Holder<SoundEvent> explosionSound
++    ) {
++        return this.explode0(source, damageSource, damageCalculator, x, y, z, radius, fire, explosionInteraction, smallExplosionParticles, largeExplosionParticles, explosionSound, null);
++    }
++    public ServerExplosion explode0(
++        @Nullable Entity source,
++        @Nullable DamageSource damageSource,
++        @Nullable ExplosionDamageCalculator damageCalculator,
++        double x,
++        double y,
++        double z,
++        float radius,
++        boolean fire,
++        Level.ExplosionInteraction explosionInteraction,
++        ParticleOptions smallExplosionParticles,
++        ParticleOptions largeExplosionParticles,
++        Holder<SoundEvent> explosionSound,
++        @Nullable java.util.function.Consumer<ServerExplosion> configurator
++    ) {
++        // CraftBukkit end
+         Explosion.BlockInteraction blockInteraction = switch (explosionInteraction) {
+             case NONE -> Explosion.BlockInteraction.KEEP;
+             case BLOCK -> this.getDestroyType(GameRules.RULE_BLOCK_EXPLOSION_DROP_DECAY);
+@@ -1108,10 +_,17 @@
+                 : Explosion.BlockInteraction.KEEP;
+             case TNT -> this.getDestroyType(GameRules.RULE_TNT_EXPLOSION_DROP_DECAY);
+             case TRIGGER -> Explosion.BlockInteraction.TRIGGER_BLOCK;
++            case STANDARD -> Explosion.BlockInteraction.DESTROY; // CraftBukkit - handle custom explosion type
+         };
+         Vec3 vec3 = new Vec3(x, y, z);
+         ServerExplosion serverExplosion = new ServerExplosion(this, source, damageSource, damageCalculator, vec3, radius, fire, blockInteraction);
++        if (configurator != null) configurator.accept(serverExplosion);// Paper - Allow explosions to damage source
+         serverExplosion.explode();
++        // CraftBukkit start
++        if (serverExplosion.wasCanceled) {
++            return serverExplosion;
++        }
++        // CraftBukkit end
+         ParticleOptions particleOptions = serverExplosion.isSmall() ? smallExplosionParticles : largeExplosionParticles;
+ 
+         for (ServerPlayer serverPlayer : this.players) {
+@@ -1120,6 +_,8 @@
+                 serverPlayer.connection.send(new ClientboundExplodePacket(vec3, optional, particleOptions, explosionSound));
+             }
+         }
++
++        return serverExplosion; // CraftBukkit
+     }
+ 
+     private Explosion.BlockInteraction getDestroyType(GameRules.Key<GameRules.BooleanValue> decayGameRule) {
+@@ -1190,7 +_,7 @@
+     public <T extends ParticleOptions> int sendParticles(
+         T type, double posX, double posY, double posZ, int particleCount, double xOffset, double yOffset, double zOffset, double speed
+     ) {
+-        return this.sendParticles(type, false, false, posX, posY, posZ, particleCount, xOffset, yOffset, zOffset, speed);
++        return this.sendParticlesSource(null, type, false, false, posX, posY, posZ, particleCount, xOffset, yOffset, zOffset, speed); // CraftBukkit - visibility api support
+     }
+ 
+     public <T extends ParticleOptions> int sendParticles(
+@@ -1206,13 +_,49 @@
+         double zOffset,
+         double speed
+     ) {
++    // CraftBukkit start - visibility api support
++        return this.sendParticlesSource(null, type, overrideLimiter, alwaysShow, posX, posY, posZ, particleCount, xOffset, yOffset, zOffset, speed);
++    }
++    public <T extends ParticleOptions> int sendParticlesSource(
++        @javax.annotation.Nullable ServerPlayer sender,
++        T type,
++        boolean overrideLimiter,
++        boolean alwaysShow,
++        double posX,
++        double posY,
++        double posZ,
++        int particleCount,
++        double xOffset,
++        double yOffset,
++        double zOffset,
++        double speed
++    ) {
++        return sendParticlesSource(this.players, sender, type, overrideLimiter, alwaysShow, posX, posY, posZ, particleCount, xOffset, yOffset, zOffset, speed);
++    }
++    public <T extends ParticleOptions> int sendParticlesSource(
++        List<ServerPlayer> receivers,
++        @javax.annotation.Nullable ServerPlayer sender,
++        T type,
++        boolean overrideLimiter,
++        boolean alwaysShow,
++        double posX,
++        double posY,
++        double posZ,
++        int particleCount,
++        double xOffset,
++        double yOffset,
++        double zOffset,
++        double speed
++    ) {
++    // CraftBukkit end - visibility api support
+         ClientboundLevelParticlesPacket clientboundLevelParticlesPacket = new ClientboundLevelParticlesPacket(
+             type, overrideLimiter, alwaysShow, posX, posY, posZ, (float)xOffset, (float)yOffset, (float)zOffset, (float)speed, particleCount
+         );
+         int i = 0;
+ 
+-        for (int i1 = 0; i1 < this.players.size(); i1++) {
+-            ServerPlayer serverPlayer = this.players.get(i1);
++        for (int i1 = 0; i1 < receivers.size(); i1++) { // Paper - particle API
++            ServerPlayer serverPlayer = receivers.get(i1); // Paper - particle API
++            if (sender != null && !serverPlayer.getBukkitEntity().canSee(sender.getBukkitEntity())) continue; // CraftBukkit
+             if (this.sendParticles(serverPlayer, overrideLimiter, posX, posY, posZ, clientboundLevelParticlesPacket)) {
+                 i++;
+             }
+@@ -1280,7 +_,7 @@
+ 
+     @Nullable
+     public BlockPos findNearestMapStructure(TagKey<Structure> structureTag, BlockPos pos, int radius, boolean skipExistingChunks) {
+-        if (!this.server.getWorldData().worldGenOptions().generateStructures()) {
++        if (!this.serverLevelData.worldGenOptions().generateStructures()) { // CraftBukkit
+             return null;
+         } else {
+             Optional<HolderSet.Named<Structure>> optional = this.registryAccess().lookupOrThrow(Registries.STRUCTURE).get(structureTag);
+@@ -1327,11 +_,38 @@
+     @Nullable
+     @Override
+     public MapItemSavedData getMapData(MapId mapId) {
+-        return this.getServer().overworld().getDataStorage().get(MapItemSavedData.factory(), mapId.key());
++        // Paper start - Call missing map initialize event and set id
++        final DimensionDataStorage storage = this.getServer().overworld().getDataStorage();
++
++        final Optional<net.minecraft.world.level.saveddata.SavedData> cacheEntry = storage.cache.get(mapId.key());
++        if (cacheEntry == null) { // Cache did not contain, try to load and may init
++            final MapItemSavedData mapData = storage.get(MapItemSavedData.factory(), mapId.key()); // get populates the cache
++            if (mapData != null) { // map was read, init it and return
++                mapData.id = mapId;
++                new org.bukkit.event.server.MapInitializeEvent(mapData.mapView).callEvent();
++                return mapData;
++            }
++
++            return null; // Map does not exist, reading failed.
++        }
++
++        // Cache entry exists, update it with the id ref and return.
++        if (cacheEntry.orElse(null) instanceof final MapItemSavedData mapItemSavedData) {
++            mapItemSavedData.id = mapId;
++            return mapItemSavedData;
++        }
++
++        return null;
++        // Paper end - Call missing map initialize event and set id
+     }
+ 
+     @Override
+     public void setMapData(MapId mapId, MapItemSavedData mapData) {
++        // CraftBukkit start
++        mapData.id = mapId;
++        org.bukkit.event.server.MapInitializeEvent event = new org.bukkit.event.server.MapInitializeEvent(mapData.mapView);
++        event.callEvent();
++        // CraftBukkit end
+         this.getServer().overworld().getDataStorage().set(mapId.key(), mapData);
+     }
+ 
+@@ -1344,17 +_,27 @@
+         BlockPos spawnPos = this.levelData.getSpawnPos();
+         float spawnAngle = this.levelData.getSpawnAngle();
+         if (!spawnPos.equals(pos) || spawnAngle != angle) {
++            org.bukkit.Location prevSpawnLoc = this.getWorld().getSpawnLocation(); // Paper - Call SpawnChangeEvent
+             this.levelData.setSpawn(pos, angle);
++            new org.bukkit.event.world.SpawnChangeEvent(this.getWorld(), prevSpawnLoc).callEvent(); // Paper - Call SpawnChangeEvent
+             this.getServer().getPlayerList().broadcastAll(new ClientboundSetDefaultSpawnPositionPacket(pos, angle));
+         }
+ 
+         if (this.lastSpawnChunkRadius > 1) {
+-            this.getChunkSource().removeRegionTicket(TicketType.START, new ChunkPos(spawnPos), this.lastSpawnChunkRadius, Unit.INSTANCE);
++            // Paper start - allow disabling gamerule limits
++            for (ChunkPos chunkPos : io.papermc.paper.util.MCUtil.getSpiralOutChunks(spawnPos, this.lastSpawnChunkRadius - 2)) {
++                this.getChunkSource().removeTicketAtLevel(TicketType.START, chunkPos, net.minecraft.server.level.ChunkLevel.ENTITY_TICKING_LEVEL, Unit.INSTANCE);
++            }
++            // Paper end - allow disabling gamerule limits
+         }
+ 
+         int i = this.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS) + 1;
+         if (i > 1) {
+-            this.getChunkSource().addRegionTicket(TicketType.START, new ChunkPos(pos), i, Unit.INSTANCE);
++            // Paper start - allow disabling gamerule limits
++            for (ChunkPos chunkPos : io.papermc.paper.util.MCUtil.getSpiralOutChunks(pos, i - 2)) {
++                this.getChunkSource().addTicketAtLevel(TicketType.START, chunkPos, net.minecraft.server.level.ChunkLevel.ENTITY_TICKING_LEVEL, Unit.INSTANCE);
++            }
++            // Paper end - allow disabling gamerule limits
+         }
+ 
+         this.lastSpawnChunkRadius = i;
+@@ -1403,6 +_,11 @@
+                 DebugPackets.sendPoiRemovedPacket(this, blockPos);
+             }));
+             optional1.ifPresent(poiType -> this.getServer().execute(() -> {
++                // Paper start - Remove stale POIs
++                if (optional.isEmpty() && this.getPoiManager().exists(blockPos, ignored -> true)) {
++                    this.getPoiManager().remove(blockPos);
++                }
++                // Paper end - Remove stale POIs
+                 this.getPoiManager().add(blockPos, (Holder<PoiType>)poiType);
+                 DebugPackets.sendPoiAddedPacket(this, blockPos);
+             }));
+@@ -1543,6 +_,11 @@
+     @Override
+     public void blockUpdated(BlockPos pos, Block block) {
+         if (!this.isDebug()) {
++            // CraftBukkit start
++            if (this.populating) {
++                return;
++            }
++            // CraftBukkit end
+             this.updateNeighborsAt(pos, block);
+         }
+     }
+@@ -1562,12 +_,12 @@
+     }
+ 
+     public boolean isFlat() {
+-        return this.server.getWorldData().isFlatWorld();
++        return this.serverLevelData.isFlatWorld(); // CraftBukkit
+     }
+ 
+     @Override
+     public long getSeed() {
+-        return this.server.getWorldData().worldGenOptions().seed();
++        return this.serverLevelData.worldGenOptions().seed(); // CraftBukkit
+     }
+ 
+     @Nullable
+@@ -1618,6 +_,7 @@
+ 
+     @Override
+     public LevelEntityGetter<Entity> getEntities() {
++        org.spigotmc.AsyncCatcher.catchOp("Chunk getEntities call"); // Spigot
+         return this.entityManager.getEntityGetter();
+     }
+ 
+@@ -1699,6 +_,27 @@
+         return this.serverLevelData.getGameRules();
+     }
+ 
++    // Paper start - respect global sound events gamerule
++    public List<net.minecraft.server.level.ServerPlayer> getPlayersForGlobalSoundGamerule() {
++        return this.getGameRules().getBoolean(GameRules.RULE_GLOBAL_SOUND_EVENTS) ? ((ServerLevel) this).getServer().getPlayerList().players : ((ServerLevel) this).players();
++    }
++
++    public double getGlobalSoundRangeSquared(java.util.function.Function<org.spigotmc.SpigotWorldConfig, Integer> rangeFunction) {
++        final double range = rangeFunction.apply(this.spigotConfig);
++        return range <= 0 ? 64.0 * 64.0 : range * range; // 64 is taken from default in ServerLevel#levelEvent
++    }
++    // Paper end - respect global sound events gamerule
++    // Paper start - notify observers even if grow failed
++    public void checkCapturedTreeStateForObserverNotify(final BlockPos pos, final org.bukkit.craftbukkit.block.CraftBlockState craftBlockState) {
++        // notify observers if the block state is the same and the Y level equals the original y level (for mega trees)
++        // blocks at the same Y level with the same state can be assumed to be saplings which trigger observers regardless of if the
++        // tree grew or not
++        if (craftBlockState.getPosition().getY() == pos.getY() && this.getBlockState(craftBlockState.getPosition()) == craftBlockState.getHandle()) {
++            this.notifyAndUpdatePhysics(craftBlockState.getPosition(), null, craftBlockState.getHandle(), craftBlockState.getHandle(), craftBlockState.getHandle(), craftBlockState.getFlag(), 512);
++        }
++    }
++    // Paper end - notify observers even if grow failed
++
+     @Override
+     public CrashReportCategory fillReportDetails(CrashReport report) {
+         CrashReportCategory crashReportCategory = super.fillReportDetails(report);
+@@ -1723,24 +_,32 @@
+ 
+         @Override
+         public void onTickingStart(Entity entity) {
++            if (entity instanceof net.minecraft.world.entity.Marker && !paperConfig().entities.markers.tick) return; // Paper - Configurable marker ticking
+             ServerLevel.this.entityTickList.add(entity);
+         }
+ 
+         @Override
+         public void onTickingEnd(Entity entity) {
+             ServerLevel.this.entityTickList.remove(entity);
++            // Paper start - Reset pearls when they stop being ticked
++            if (ServerLevel.this.paperConfig().fixes.disableUnloadedChunkEnderpearlExploit && ServerLevel.this.paperConfig().misc.legacyEnderPearlBehavior && entity instanceof net.minecraft.world.entity.projectile.ThrownEnderpearl pearl) {
++                pearl.cachedOwner = null;
++                pearl.ownerUUID = null;
++            }
++            // Paper end - Reset pearls when they stop being ticked
+         }
+ 
+         @Override
+         public void onTrackingStart(Entity entity) {
+-            ServerLevel.this.getChunkSource().addEntity(entity);
++            org.spigotmc.AsyncCatcher.catchOp("entity register"); // Spigot
++            // ServerLevel.this.getChunkSource().addEntity(entity); // Paper - ignore and warn about illegal addEntity calls instead of crashing server; moved down below valid=true
+             if (entity instanceof ServerPlayer serverPlayer) {
+                 ServerLevel.this.players.add(serverPlayer);
+                 ServerLevel.this.updateSleepingPlayerList();
+             }
+ 
+             if (entity instanceof Mob mob) {
+-                if (ServerLevel.this.isUpdatingNavigations) {
++                if (false && ServerLevel.this.isUpdatingNavigations) { // Paper - Remove unnecessary onTrackingStart during navigation warning
+                     String string = "onTrackingStart called during navigation iteration";
+                     Util.logAndPauseIfInIde(
+                         "onTrackingStart called during navigation iteration", new IllegalStateException("onTrackingStart called during navigation iteration")
+@@ -1757,10 +_,52 @@
+             }
+ 
+             entity.updateDynamicGameEventListener(DynamicGameEventListener::add);
++            entity.inWorld = true; // CraftBukkit - Mark entity as in world
++            entity.valid = true; // CraftBukkit
++            ServerLevel.this.getChunkSource().addEntity(entity); // Paper - ignore and warn about illegal addEntity calls instead of crashing server
++            // Paper start - Entity origin API
++            if (entity.getOriginVector() == null) {
++                entity.setOrigin(entity.getBukkitEntity().getLocation());
++            }
++            // Default to current world if unknown, gross assumption but entities rarely change world
++            if (entity.getOriginWorld() == null) {
++                entity.setOrigin(entity.getOriginVector().toLocation(getWorld()));
++            }
++            // Paper end - Entity origin API
++            new com.destroystokyo.paper.event.entity.EntityAddToWorldEvent(entity.getBukkitEntity(), ServerLevel.this.getWorld()).callEvent(); // Paper - fire while valid
+         }
+ 
+         @Override
+         public void onTrackingEnd(Entity entity) {
++            org.spigotmc.AsyncCatcher.catchOp("entity unregister"); // Spigot
++            // Spigot start // TODO I don't think this is needed anymore
++            if (entity instanceof Player player) {
++                for (final ServerLevel level : ServerLevel.this.getServer().getAllLevels()) {
++                    for (final Optional<net.minecraft.world.level.saveddata.SavedData> savedData : level.getDataStorage().cache.values()) {
++                        if (savedData.isEmpty() || !(savedData.get() instanceof MapItemSavedData map)) {
++                            continue;
++                        }
++
++                        map.carriedByPlayers.remove(player);
++                        if (map.carriedBy.removeIf(holdingPlayer -> holdingPlayer.player == player)) {
++                            map.decorations.remove(player.getName().getString());
++                        }
++                    }
++                }
++            }
++            // Spigot end
++            // Spigot start
++            if (entity.getBukkitEntity() instanceof org.bukkit.inventory.InventoryHolder && (!(entity instanceof ServerPlayer) || entity.getRemovalReason() != Entity.RemovalReason.KILLED)) { // SPIGOT-6876: closeInventory clears death message
++                // Paper start - Fix merchant inventory not closing on entity removal
++                if (entity.getBukkitEntity() instanceof org.bukkit.inventory.Merchant merchant && merchant.getTrader() != null) {
++                    merchant.getTrader().closeInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason.UNLOADED);
++                }
++                // Paper end - Fix merchant inventory not closing on entity removal
++                for (org.bukkit.entity.HumanEntity h : Lists.newArrayList(((org.bukkit.inventory.InventoryHolder) entity.getBukkitEntity()).getInventory().getViewers())) {
++                    h.closeInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason.UNLOADED); // Paper - Inventory close reason
++                }
++            }
++            // Spigot end
+             ServerLevel.this.getChunkSource().removeEntity(entity);
+             if (entity instanceof ServerPlayer serverPlayer) {
+                 ServerLevel.this.players.remove(serverPlayer);
+@@ -1768,7 +_,7 @@
+             }
+ 
+             if (entity instanceof Mob mob) {
+-                if (ServerLevel.this.isUpdatingNavigations) {
++                if (false && ServerLevel.this.isUpdatingNavigations) { // Paper - Remove unnecessary onTrackingStart during navigation warning
+                     String string = "onTrackingStart called during navigation iteration";
+                     Util.logAndPauseIfInIde(
+                         "onTrackingStart called during navigation iteration", new IllegalStateException("onTrackingStart called during navigation iteration")
+@@ -1785,6 +_,15 @@
+             }
+ 
+             entity.updateDynamicGameEventListener(DynamicGameEventListener::remove);
++            // CraftBukkit start
++            entity.valid = false;
++            if (!(entity instanceof ServerPlayer)) {
++                for (ServerPlayer player : ServerLevel.this.server.getPlayerList().players) { // Paper - call onEntityRemove for all online players
++                    player.getBukkitEntity().onEntityRemove(entity);
++                }
++            }
++            // CraftBukkit end
++            new com.destroystokyo.paper.event.entity.EntityRemoveFromWorldEvent(entity.getBukkitEntity(), ServerLevel.this.getWorld()).callEvent(); // Paper - fire while valid
+         }
+ 
+         @Override
+@@ -1792,4 +_,24 @@
+             entity.updateDynamicGameEventListener(DynamicGameEventListener::move);
+         }
+     }
++
++    // Paper start - check global player list where appropriate
++    @Override
++    @Nullable
++    public Player getGlobalPlayerByUUID(UUID uuid) {
++        return this.server.getPlayerList().getPlayer(uuid);
++    }
++    // Paper end - check global player list where appropriate
++
++    // Paper start - lag compensation
++    private long lagCompensationTick = MinecraftServer.SERVER_INIT;
++
++    public long getLagCompensationTick() {
++        return this.lagCompensationTick;
++    }
++
++    public void updateLagCompensationTick() {
++        this.lagCompensationTick = (System.nanoTime() - MinecraftServer.SERVER_INIT) / (java.util.concurrent.TimeUnit.MILLISECONDS.toNanos(50L));
++    }
++    // Paper end - lag compensation
+ }
diff --git a/paper-server/patches/sources/net/minecraft/server/level/ServerPlayer.java.patch b/paper-server/patches/sources/net/minecraft/server/level/ServerPlayer.java.patch
new file mode 100644
index 0000000000..c32e052b23
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/level/ServerPlayer.java.patch
@@ -0,0 +1,1614 @@
+--- a/net/minecraft/server/level/ServerPlayer.java
++++ b/net/minecraft/server/level/ServerPlayer.java
+@@ -223,7 +_,8 @@
+     private int levitationStartTime;
+     private boolean disconnected;
+     private int requestedViewDistance = 2;
+-    public String language = "en_us";
++    public String language = null; // Paper - default to null
++    public java.util.Locale adventure$locale = java.util.Locale.US; // Paper
+     @Nullable
+     private Vec3 startingToFallPosition;
+     @Nullable
+@@ -258,6 +_,13 @@
+             }
+         }
+ 
++        // Paper start - Sync offhand slot in menus
++        @Override
++        public void sendOffHandSlotChange() {
++            ServerPlayer.this.connection.send(new ClientboundContainerSetSlotPacket(ServerPlayer.this.inventoryMenu.containerId, ServerPlayer.this.inventoryMenu.incrementStateId(), net.minecraft.world.inventory.InventoryMenu.SHIELD_SLOT, ServerPlayer.this.inventoryMenu.getSlot(net.minecraft.world.inventory.InventoryMenu.SHIELD_SLOT).getItem().copy()));
++        }
++        // Paper end - Sync offhand slot in menus
++
+         @Override
+         public void sendSlotChange(AbstractContainerMenu container, int slot, ItemStack itemStack) {
+             ServerPlayer.this.connection.send(new ClientboundContainerSetSlotPacket(container.containerId, container.incrementStateId(), slot, itemStack));
+@@ -288,6 +_,32 @@
+             }
+         }
+ 
++        // Paper start - Add PlayerInventorySlotChangeEvent
++        @Override
++        public void slotChanged(AbstractContainerMenu containerToSend, int dataSlotIndex, ItemStack oldStack, ItemStack stack) {
++            // See slotChanged above
++            Slot slot = containerToSend.getSlot(dataSlotIndex);
++            if (!(slot instanceof ResultSlot)) {
++                if (slot.container == ServerPlayer.this.getInventory()) {
++                    if (io.papermc.paper.event.player.PlayerInventorySlotChangeEvent.getHandlerList().getRegisteredListeners().length == 0) {
++                        CriteriaTriggers.INVENTORY_CHANGED.trigger(ServerPlayer.this, ServerPlayer.this.getInventory(), stack);
++                        return;
++                    }
++                    io.papermc.paper.event.player.PlayerInventorySlotChangeEvent event = new io.papermc.paper.event.player.PlayerInventorySlotChangeEvent(
++                        ServerPlayer.this.getBukkitEntity(),
++                        dataSlotIndex,
++                        org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitCopy(oldStack),
++                        org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitCopy(stack)
++                    );
++                    event.callEvent();
++                    if (event.shouldTriggerAdvancements()) {
++                        CriteriaTriggers.INVENTORY_CHANGED.trigger(ServerPlayer.this, ServerPlayer.this.getInventory(), stack);
++                    }
++                }
++            }
++        }
++        // Paper end - Add PlayerInventorySlotChangeEvent
++
+         @Override
+         public void dataChanged(AbstractContainerMenu containerMenu, int dataSlotIndex, int value) {
+         }
+@@ -316,9 +_,43 @@
+         public void sendSystemMessage(Component component) {
+             ServerPlayer.this.sendSystemMessage(component);
+         }
++
++        // CraftBukkit start
++        @Override
++        public org.bukkit.command.CommandSender getBukkitSender(CommandSourceStack wrapper) {
++            return ServerPlayer.this.getBukkitEntity();
++        }
++        // CraftBukkit end
+     };
+     private int containerCounter;
+     public boolean wonGame;
++    private int containerUpdateDelay; // Paper - Configurable container update tick rate
++    public long loginTime; // Paper - Replace OfflinePlayer#getLastPlayed
++    public int patrolSpawnDelay; // Paper - Pillager patrol spawn settings and per player options
++    // Paper start - cancellable death event
++    public boolean queueHealthUpdatePacket;
++    public net.minecraft.network.protocol.game.ClientboundSetHealthPacket queuedHealthUpdatePacket;
++    // Paper end - cancellable death event
++    // CraftBukkit start
++    public org.bukkit.craftbukkit.entity.CraftPlayer.TransferCookieConnection transferCookieConnection;
++    public String displayName;
++    public net.kyori.adventure.text.Component adventure$displayName; // Paper
++    public Component listName;
++    public int listOrder = 0;
++    public org.bukkit.Location compassTarget;
++    public int newExp = 0;
++    public int newLevel = 0;
++    public int newTotalExp = 0;
++    public boolean keepLevel = false;
++    public double maxHealthCache;
++    public boolean joining = true;
++    public boolean sentListPacket = false;
++    public boolean supressTrackerForLogin = false; // Paper - Fire PlayerJoinEvent when Player is actually ready
++    // CraftBukkit end
++    public boolean isRealPlayer; // Paper
++    public com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent playerNaturallySpawnedEvent; // Paper - PlayerNaturallySpawnCreaturesEvent
++    public @Nullable String clientBrandName = null; // Paper - Brand support
++    public org.bukkit.event.player.PlayerQuitEvent.QuitReason quitReason = null; // Paper - Add API for quit reason; there are a lot of changes to do if we change all methods leading to the event
+ 
+     public ServerPlayer(MinecraftServer server, ServerLevel level, GameProfile gameProfile, ClientInformation clientInformation) {
+         super(level, level.getSharedSpawnPos(), level.getSharedSpawnAngle(), gameProfile);
+@@ -328,16 +_,22 @@
+         this.server = server;
+         this.stats = server.getPlayerList().getPlayerStats(this);
+         this.advancements = server.getPlayerList().getPlayerAdvancements(this);
+-        this.moveTo(this.adjustSpawnLocation(level, level.getSharedSpawnPos()).getBottomCenter(), 0.0F, 0.0F);
+-        this.updateOptions(clientInformation);
++        // this.moveTo(this.adjustSpawnLocation(level, level.getSharedSpawnPos()).getBottomCenter(), 0.0F, 0.0F); // Paper - Don't move existing players to world spawn
++        this.updateOptionsNoEvents(clientInformation); // Paper - don't call options events on login
+         this.object = null;
++
++        // CraftBukkit start
++        this.displayName = this.getScoreboardName();
++        this.adventure$displayName = net.kyori.adventure.text.Component.text(this.getScoreboardName()); // Paper
++        this.bukkitPickUpLoot = true;
++        this.maxHealthCache = this.getMaxHealth();
+     }
+ 
+     @Override
+     public BlockPos adjustSpawnLocation(ServerLevel level, BlockPos pos) {
+         AABB aabb = this.getDimensions(Pose.STANDING).makeBoundingBox(Vec3.ZERO);
+         BlockPos blockPos = pos;
+-        if (level.dimensionType().hasSkyLight() && level.getServer().getWorldData().getGameType() != GameType.ADVENTURE) {
++        if (level.dimensionType().hasSkyLight() && level.serverLevelData.getGameType() != GameType.ADVENTURE) { // CraftBukkit
+             int max = Math.max(0, this.server.getSpawnRadius(level));
+             int floor = Mth.floor(level.getWorldBorder().getDistanceToBorder(pos.getX(), pos.getZ()));
+             if (floor < max) {
+@@ -420,11 +_,20 @@
+         if (compound.contains("recipeBook", 10)) {
+             this.recipeBook.fromNbt(compound.getCompound("recipeBook"), key -> this.server.getRecipeManager().byKey(key).isPresent());
+         }
++        this.getBukkitEntity().readExtraData(compound); // CraftBukkit
+ 
+         if (this.isSleeping()) {
+             this.stopSleeping();
+         }
+ 
++        // CraftBukkit start
++        String spawnWorld = compound.getString("SpawnWorld");
++        org.bukkit.craftbukkit.CraftWorld oldWorld = (org.bukkit.craftbukkit.CraftWorld) org.bukkit.Bukkit.getWorld(spawnWorld);
++        if (oldWorld != null) {
++            this.respawnDimension = oldWorld.getHandle().dimension();
++        }
++        // CraftBukkit end
++
+         if (compound.contains("SpawnX", 99) && compound.contains("SpawnY", 99) && compound.contains("SpawnZ", 99)) {
+             this.respawnPosition = new BlockPos(compound.getInt("SpawnX"), compound.getInt("SpawnY"), compound.getInt("SpawnZ"));
+             this.respawnForced = compound.getBoolean("SpawnForced");
+@@ -475,6 +_,7 @@
+                 .resultOrPartial(LOGGER::error)
+                 .ifPresent(spawnDimension -> compound.put("SpawnDimension", spawnDimension));
+         }
++        this.getBukkitEntity().setExtraData(compound); // CraftBukkit
+ 
+         compound.putBoolean("spawn_extra_particles_on_fall", this.spawnExtraParticlesOnFall);
+         if (this.raidOmenPosition != null) {
+@@ -490,7 +_,18 @@
+     private void saveParentVehicle(CompoundTag tag) {
+         Entity rootVehicle = this.getRootVehicle();
+         Entity vehicle = this.getVehicle();
+-        if (vehicle != null && rootVehicle != this && rootVehicle.hasExactlyOnePlayerPassenger()) {
++        // CraftBukkit start - handle non-persistent vehicles
++        boolean persistVehicle = true;
++        if (vehicle != null) {
++            for (Entity topVehicle = vehicle; topVehicle != null; topVehicle = topVehicle.getVehicle()) {
++                if (!topVehicle.persist) {
++                    persistVehicle = false;
++                    break;
++                }
++            }
++        }
++        if (persistVehicle && vehicle != null && rootVehicle != this && rootVehicle.hasExactlyOnePlayerPassenger() && !rootVehicle.isRemoved()) { // Paper - Ensure valid vehicle status
++            // CraftBukkit end
+             CompoundTag compoundTag = new CompoundTag();
+             CompoundTag compoundTag1 = new CompoundTag();
+             rootVehicle.save(compoundTag1);
+@@ -504,7 +_,7 @@
+         if (tag.isPresent() && tag.get().contains("RootVehicle", 10) && this.level() instanceof ServerLevel serverLevel) {
+             CompoundTag compound = tag.get().getCompound("RootVehicle");
+             Entity entity = EntityType.loadEntityRecursive(
+-                compound.getCompound("Entity"), serverLevel, EntitySpawnReason.LOAD, entity2 -> !serverLevel.addWithUUID(entity2) ? null : entity2
++                compound.getCompound("Entity"), serverLevel, EntitySpawnReason.LOAD, entity2 -> !serverLevel.addWithUUID(entity2, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.MOUNT) ? null : entity2 // Paper - Entity#getEntitySpawnReason
+             );
+             if (entity == null) {
+                 return;
+@@ -530,10 +_,10 @@
+ 
+             if (!this.isPassenger()) {
+                 LOGGER.warn("Couldn't reattach entity to player");
+-                entity.discard();
++                entity.discard(null); // CraftBukkit - add Bukkit remove cause
+ 
+                 for (Entity entity1x : entity.getIndirectPassengers()) {
+-                    entity1x.discard();
++                    entity1x.discard(null); // CraftBukkit - add Bukkit remove cause
+                 }
+             }
+         }
+@@ -544,6 +_,7 @@
+             ListTag listTag = new ListTag();
+ 
+             for (ThrownEnderpearl thrownEnderpearl : this.enderPearls) {
++                if (thrownEnderpearl.level().paperConfig().misc.legacyEnderPearlBehavior) continue; // Paper - Allow using old ender pearl behavior
+                 if (thrownEnderpearl.isRemoved()) {
+                     LOGGER.warn("Trying to save removed ender pearl, skipping");
+                 } else {
+@@ -593,6 +_,16 @@
+         }
+     }
+ 
++    // CraftBukkit start
++    public void spawnIn(final ServerLevel level) {
++        if (level == null) {
++            throw new IllegalArgumentException("level can't be null");
++        }
++        this.setLevel(level);
++        this.gameMode.setLevel(level);
++    }
++    // CraftBukkit end
++
+     public void setExperiencePoints(int experiencePoints) {
+         float f = this.getXpNeededForNextLevel();
+         float f1 = (f - 1.0F) / f;
+@@ -650,6 +_,11 @@
+ 
+     @Override
+     public void tick() {
++        // CraftBukkit start
++        if (this.joining) {
++            this.joining = false;
++        }
++        // CraftBukkit end
+         this.tickClientLoadTimeout();
+         this.gameMode.tick();
+         this.wardenSpawnTracker.tick();
+@@ -657,9 +_,14 @@
+             this.invulnerableTime--;
+         }
+ 
+-        this.containerMenu.broadcastChanges();
+-        if (!this.containerMenu.stillValid(this)) {
+-            this.closeContainer();
++        // Paper start - Configurable container update tick rate
++        if (--this.containerUpdateDelay <= 0) {
++            this.containerMenu.broadcastChanges();
++            this.containerUpdateDelay = this.level().paperConfig().tickRates.containerUpdate;
++        }
++        // Paper end - Configurable container update tick rate
++        if (this.containerMenu != this.inventoryMenu && (this.isImmobile() || !this.containerMenu.stillValid(this))) { // Paper - Prevent opening inventories when frozen
++            this.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.CANT_USE); // Paper - Inventory close reason
+             this.containerMenu = this.inventoryMenu;
+         }
+ 
+@@ -709,7 +_,7 @@
+ 
+     public void doTick() {
+         try {
+-            if (!this.isSpectator() || !this.touchingUnloadedChunk()) {
++            if (valid && !this.isSpectator() || !this.touchingUnloadedChunk()) { // Paper - don't tick dead players that are not in the world currently (pending respawn)
+                 super.tick();
+             }
+ 
+@@ -723,7 +_,7 @@
+             if (this.getHealth() != this.lastSentHealth
+                 || this.lastSentFood != this.foodData.getFoodLevel()
+                 || this.foodData.getSaturationLevel() == 0.0F != this.lastFoodSaturationZero) {
+-                this.connection.send(new ClientboundSetHealthPacket(this.getHealth(), this.foodData.getFoodLevel(), this.foodData.getSaturationLevel()));
++                this.connection.send(new ClientboundSetHealthPacket(this.getBukkitEntity().getScaledHealth(), this.foodData.getFoodLevel(), this.foodData.getSaturationLevel())); // CraftBukkit
+                 this.lastSentHealth = this.getHealth();
+                 this.lastSentFood = this.foodData.getFoodLevel();
+                 this.lastFoodSaturationZero = this.foodData.getSaturationLevel() == 0.0F;
+@@ -754,6 +_,12 @@
+                 this.updateScoreForCriteria(ObjectiveCriteria.EXPERIENCE, Mth.ceil((float)this.lastRecordedExperience));
+             }
+ 
++            // CraftBukkit start - Force max health updates
++            if (this.maxHealthCache != this.getMaxHealth()) {
++                this.getBukkitEntity().updateScaledHealth();
++            }
++            // CraftBukkit end
++
+             if (this.experienceLevel != this.lastRecordedLevel) {
+                 this.lastRecordedLevel = this.experienceLevel;
+                 this.updateScoreForCriteria(ObjectiveCriteria.LEVEL, Mth.ceil((float)this.lastRecordedLevel));
+@@ -767,6 +_,21 @@
+             if (this.tickCount % 20 == 0) {
+                 CriteriaTriggers.LOCATION.trigger(this);
+             }
++
++            // CraftBukkit start - initialize oldLevel, fire PlayerLevelChangeEvent, and tick client-sided world border
++            if (this.oldLevel == -1) {
++                this.oldLevel = this.experienceLevel;
++            }
++
++            if (this.oldLevel != this.experienceLevel) {
++                org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerLevelChangeEvent(this.getBukkitEntity(), this.oldLevel, this.experienceLevel);
++                this.oldLevel = this.experienceLevel;
++            }
++
++            if (this.getBukkitEntity().hasClientWorldBorder()) {
++                ((org.bukkit.craftbukkit.CraftWorldBorder) this.getBukkitEntity().getWorldBorder()).getHandle().tick();
++            }
++            // CraftBukkit end
+         } catch (Throwable var4) {
+             CrashReport crashReport = CrashReport.forThrowable(var4, "Ticking player");
+             CrashReportCategory crashReportCategory = crashReport.addCategory("Player being ticked");
+@@ -791,7 +_,7 @@
+         if (this.level().getDifficulty() == Difficulty.PEACEFUL && this.serverLevel().getGameRules().getBoolean(GameRules.RULE_NATURAL_REGENERATION)) {
+             if (this.tickCount % 20 == 0) {
+                 if (this.getHealth() < this.getMaxHealth()) {
+-                    this.heal(1.0F);
++                    this.heal(1.0F, org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.REGEN); // CraftBukkit - added regain reason of "REGEN" for filtering purposes.
+                 }
+ 
+                 float saturationLevel = this.foodData.getSaturationLevel();
+@@ -840,15 +_,100 @@
+     }
+ 
+     private void updateScoreForCriteria(ObjectiveCriteria criteria, int points) {
+-        this.getScoreboard().forAllObjectives(criteria, this, scoreAccess -> scoreAccess.set(points));
+-    }
++        this.level().getCraftServer().getScoreboardManager().forAllObjectives(criteria, this, scoreAccess -> scoreAccess.set(points)); // CraftBukkit - Use our scores instead
++    }
++
++    // Paper start - PlayerDeathEvent#getItemsToKeep
++    private static void processKeep(org.bukkit.event.entity.PlayerDeathEvent event, NonNullList<ItemStack> inv) {
++        List<org.bukkit.inventory.ItemStack> itemsToKeep = event.getItemsToKeep();
++        if (inv == null) {
++            // remainder of items left in toKeep - plugin added stuff on death that wasn't in the initial loot?
++            if (!itemsToKeep.isEmpty()) {
++                for (org.bukkit.inventory.ItemStack itemStack : itemsToKeep) {
++                    event.getEntity().getInventory().addItem(itemStack);
++                }
++            }
++
++            return;
++        }
++
++        for (int i = 0; i < inv.size(); ++i) {
++            ItemStack item = inv.get(i);
++            if (EnchantmentHelper.has(item, net.minecraft.world.item.enchantment.EnchantmentEffectComponents.PREVENT_EQUIPMENT_DROP) || itemsToKeep.isEmpty() || item.isEmpty()) {
++                inv.set(i, ItemStack.EMPTY);
++                continue;
++            }
++
++            final org.bukkit.inventory.ItemStack bukkitStack = item.getBukkitStack();
++            boolean keep = false;
++            final java.util.Iterator<org.bukkit.inventory.ItemStack> iterator = itemsToKeep.iterator();
++            while (iterator.hasNext()) {
++                final org.bukkit.inventory.ItemStack itemStack = iterator.next();
++                if (bukkitStack.equals(itemStack)) {
++                    iterator.remove();
++                    keep = true;
++                    break;
++                }
++            }
++
++            if (!keep) {
++                inv.set(i, ItemStack.EMPTY);
++            }
++        }
++    }
++    // Paper end - PlayerDeathEvent#getItemsToKeep
+ 
+     @Override
+     public void die(DamageSource cause) {
+-        this.gameEvent(GameEvent.ENTITY_DIE);
+-        boolean _boolean = this.serverLevel().getGameRules().getBoolean(GameRules.RULE_SHOWDEATHMESSAGES);
+-        if (_boolean) {
+-            Component deathMessage = this.getCombatTracker().getDeathMessage();
++        // this.gameEvent(GameEvent.ENTITY_DIE); // Paper - move below event cancellation check
++        boolean _boolean = this.serverLevel().getGameRules().getBoolean(GameRules.RULE_SHOWDEATHMESSAGES); final boolean showDeathMessage = _boolean; // Paper - OBFHELPER
++        // CraftBukkit start - fire PlayerDeathEvent
++        if (this.isRemoved()) {
++            return;
++        }
++        List<DefaultDrop> loot = new java.util.ArrayList<>(this.getInventory().getContainerSize()); // Paper - Restore vanilla drops behavior
++        boolean keepInventory = this.serverLevel().getGameRules().getBoolean(GameRules.RULE_KEEPINVENTORY) || this.isSpectator();
++        if (!keepInventory) {
++            for (ItemStack item : this.getInventory().getContents()) {
++                if (!item.isEmpty() && !EnchantmentHelper.has(item, net.minecraft.world.item.enchantment.EnchantmentEffectComponents.PREVENT_EQUIPMENT_DROP)) {
++                    loot.add(new DefaultDrop(item, stack -> this.drop(stack, true, false, false))); // Paper - Restore vanilla drops behavior; drop function taken from Inventory#dropAll (don't fire drop event)
++                }
++            }
++        }
++        if (this.shouldDropLoot() && this.serverLevel().getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) { // Paper - fix player loottables running when mob loot gamerule is false
++            // SPIGOT-5071: manually add player loot tables (SPIGOT-5195 - ignores keepInventory rule)
++            this.dropFromLootTable(this.serverLevel(), cause, this.lastHurtByPlayerTime > 0);
++            // Paper - Restore vanilla drops behaviour; custom death loot is a noop on server player, remove.
++            loot.addAll(this.drops);
++            this.drops.clear(); // SPIGOT-5188: make sure to clear
++        } // Paper - fix player loottables running when mob loot gamerule is false
++
++        Component defaultMessage = this.getCombatTracker().getDeathMessage();
++
++        String deathmessage = defaultMessage.getString();
++        this.keepLevel = keepInventory; // SPIGOT-2222: pre-set keepLevel
++        org.bukkit.event.entity.PlayerDeathEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerDeathEvent(this, cause, loot, io.papermc.paper.adventure.PaperAdventure.asAdventure(defaultMessage), keepInventory); // Paper - Adventure
++        // Paper start - cancellable death event
++        if (event.isCancelled()) {
++            // make compatible with plugins that might have already set the health in an event listener
++            if (this.getHealth() <= 0) {
++                this.setHealth((float) event.getReviveHealth());
++            }
++            return;
++        }
++        this.gameEvent(GameEvent.ENTITY_DIE); // moved from the top of this method
++        // Paper end
++
++        // SPIGOT-943 - only call if they have an inventory open
++        if (this.containerMenu != this.inventoryMenu) {
++            this.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.DEATH); // Paper - Inventory close reason
++        }
++
++        net.kyori.adventure.text.Component apiDeathMessage = event.deathMessage() != null ? event.deathMessage() : net.kyori.adventure.text.Component.empty(); // Paper - Adventure
++
++        if (apiDeathMessage != null && apiDeathMessage != net.kyori.adventure.text.Component.empty() && showDeathMessage) { // Paper - Adventure // TODO: allow plugins to override?
++            Component deathMessage = io.papermc.paper.adventure.PaperAdventure.asVanilla(apiDeathMessage); // Paper - Adventure
++
+             this.connection
+                 .send(
+                     new ClientboundPlayerCombatKillPacket(this.getId(), deathMessage),
+@@ -882,11 +_,22 @@
+             this.tellNeutralMobsThatIDied();
+         }
+ 
+-        if (!this.isSpectator()) {
+-            this.dropAllDeathLoot(this.serverLevel(), cause);
++        // SPIGOT-5478 must be called manually now
++        if (event.shouldDropExperience()) this.dropExperience(this.serverLevel(), cause.getEntity()); // Paper - tie to event
++        // we clean the player's inventory after the EntityDeathEvent is called so plugins can get the exact state of the inventory.
++        if (!event.getKeepInventory()) {
++            // Paper start - PlayerDeathEvent#getItemsToKeep
++            for (NonNullList<ItemStack> inv : this.getInventory().compartments) {
++                processKeep(event, inv);
++            }
++            processKeep(event, null);
++            // Paper end - PlayerDeathEvent#getItemsToKeep
+         }
+ 
+-        this.getScoreboard().forAllObjectives(ObjectiveCriteria.DEATH_COUNT, this, ScoreAccess::increment);
++        this.setCamera(this); // Remove spectated target
++        // CraftBukkit end
++
++        this.level().getCraftServer().getScoreboardManager().forAllObjectives(ObjectiveCriteria.DEATH_COUNT, this, ScoreAccess::increment); // CraftBukkit - Get our scores instead
+         LivingEntity killCredit = this.getKillCredit();
+         if (killCredit != null) {
+             this.awardStat(Stats.ENTITY_KILLED_BY.get(killCredit.getType()));
+@@ -919,10 +_,10 @@
+     public void awardKillScore(Entity entity, DamageSource damageSource) {
+         if (entity != this) {
+             super.awardKillScore(entity, damageSource);
+-            this.getScoreboard().forAllObjectives(ObjectiveCriteria.KILL_COUNT_ALL, this, ScoreAccess::increment);
++            this.level().getCraftServer().getScoreboardManager().forAllObjectives(ObjectiveCriteria.KILL_COUNT_ALL, this, ScoreAccess::increment); // CraftBukkit - Get our scores instead
+             if (entity instanceof Player) {
+                 this.awardStat(Stats.PLAYER_KILLS);
+-                this.getScoreboard().forAllObjectives(ObjectiveCriteria.KILL_COUNT_PLAYERS, this, ScoreAccess::increment);
++                this.level().getCraftServer().getScoreboardManager().forAllObjectives(ObjectiveCriteria.KILL_COUNT_PLAYERS, this, ScoreAccess::increment); // CraftBukkit - Get our scores instead
+             } else {
+                 this.awardStat(Stats.MOB_KILLS);
+             }
+@@ -938,7 +_,7 @@
+         if (playersTeam != null) {
+             int id = playersTeam.getColor().getId();
+             if (id >= 0 && id < crtieria.length) {
+-                this.getScoreboard().forAllObjectives(crtieria[id], scoreHolder, ScoreAccess::increment);
++                this.level().getCraftServer().getScoreboardManager().forAllObjectives(crtieria[id], scoreHolder, ScoreAccess::increment); // CraftBukkit - Get our scores instead
+             }
+         }
+     }
+@@ -949,9 +_,20 @@
+             return false;
+         } else {
+             Entity entity = damageSource.getEntity();
+-            return !(entity instanceof Player player && !this.canHarmPlayer(player))
++            if (!( // Paper - split the if statement. If below statement is false, hurtServer would not have been evaluated. Return false.
++             !(entity instanceof Player player && !this.canHarmPlayer(player))
+                 && !(entity instanceof AbstractArrow abstractArrow && abstractArrow.getOwner() instanceof Player player1 && !this.canHarmPlayer(player1))
+-                && super.hurtServer(level, damageSource, amount);
++            )) return false; // Paper - split the if statement. If below statement is false, hurtServer would not have been evaluated. Return false.
++            // Paper start - cancellable death events
++            this.queueHealthUpdatePacket = true;
++            boolean damaged = super.hurtServer(level, damageSource, amount);
++            this.queueHealthUpdatePacket = false;
++            if (this.queuedHealthUpdatePacket != null) {
++                this.connection.send(this.queuedHealthUpdatePacket);
++                this.queuedHealthUpdatePacket = null;
++            }
++            return damaged;
++            // Paper end - cancellable death events
+         }
+     }
+ 
+@@ -961,10 +_,15 @@
+     }
+ 
+     private boolean isPvpAllowed() {
+-        return this.server.isPvpAllowed();
++        return this.level().pvpMode; // CraftBukkit - this.server.isPvpAllowed() -> this.world.pvpMode
+     }
+ 
+-    public TeleportTransition findRespawnPositionAndUseSpawnBlock(boolean useCharge, TeleportTransition.PostTeleportTransition postTeleportTransition) {
++    // CraftBukkit start
++    public TeleportTransition findRespawnPositionAndUseSpawnBlock(boolean useCharge, TeleportTransition.PostTeleportTransition postTeleportTransition, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason respawnReason) {
++        TeleportTransition teleportTransition;
++        boolean isBedSpawn = false;
++        boolean isAnchorSpawn = false;
++        // CraftBukkit end
+         BlockPos respawnPosition = this.getRespawnPosition();
+         float respawnAngle = this.getRespawnAngle();
+         boolean isRespawnForced = this.isRespawnForced();
+@@ -973,13 +_,66 @@
+             Optional<ServerPlayer.RespawnPosAngle> optional = findRespawnAndUseSpawnBlock(level, respawnPosition, respawnAngle, isRespawnForced, useCharge);
+             if (optional.isPresent()) {
+                 ServerPlayer.RespawnPosAngle respawnPosAngle = optional.get();
+-                return new TeleportTransition(level, respawnPosAngle.position(), Vec3.ZERO, respawnPosAngle.yaw(), 0.0F, postTeleportTransition);
++                // CraftBukkit start
++                isBedSpawn = respawnPosAngle.isBedSpawn();
++                isAnchorSpawn = respawnPosAngle.isAnchorSpawn();
++                teleportTransition = new TeleportTransition(level, respawnPosAngle.position(), Vec3.ZERO, respawnPosAngle.yaw(), 0.0F, postTeleportTransition);
++                // CraftBukkit end
+             } else {
+-                return TeleportTransition.missingRespawnBlock(this.server.overworld(), this, postTeleportTransition);
++                teleportTransition = TeleportTransition.missingRespawnBlock(this.server.overworld(), this, postTeleportTransition); // CraftBukkit
+             }
+         } else {
+-            return new TeleportTransition(this.server.overworld(), this, postTeleportTransition);
+-        }
++            teleportTransition = new TeleportTransition(this.server.overworld(), this, postTeleportTransition); // CraftBukkit
++        }
++        // CraftBukkit start
++        if (respawnReason == null) {
++            return teleportTransition;
++        }
++
++        org.bukkit.entity.Player respawnPlayer = this.getBukkitEntity();
++        org.bukkit.Location location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(
++            teleportTransition.position(),
++            teleportTransition.newLevel().getWorld(),
++            teleportTransition.yRot(),
++            teleportTransition.xRot()
++        );
++
++        // Paper start - respawn flags
++        com.google.common.collect.ImmutableSet.Builder<org.bukkit.event.player.PlayerRespawnEvent.RespawnFlag> builder = com.google.common.collect.ImmutableSet.builder();
++        if (respawnReason == org.bukkit.event.player.PlayerRespawnEvent.RespawnReason.END_PORTAL) {
++            builder.add(org.bukkit.event.player.PlayerRespawnEvent.RespawnFlag.END_PORTAL);
++        }
++        org.bukkit.event.player.PlayerRespawnEvent respawnEvent = new org.bukkit.event.player.PlayerRespawnEvent(
++            respawnPlayer,
++            location,
++            isBedSpawn,
++            isAnchorSpawn,
++            respawnReason,
++            builder
++        );
++        // Paper end - respawn flags
++        this.level().getCraftServer().getPluginManager().callEvent(respawnEvent);
++        // Spigot start
++        if (this.connection.isDisconnected()) {
++            return null;
++        }
++        // Spigot end
++
++        location = respawnEvent.getRespawnLocation();
++
++        return new TeleportTransition(
++            ((org.bukkit.craftbukkit.CraftWorld) location.getWorld()).getHandle(),
++            org.bukkit.craftbukkit.util.CraftLocation.toVec3D(location),
++            teleportTransition.deltaMovement(),
++            location.getYaw(),
++            location.getPitch(),
++            teleportTransition.missingRespawnBlock(),
++            teleportTransition.asPassenger(),
++            teleportTransition.relatives(),
++            teleportTransition.postTeleportTransition(),
++            teleportTransition.cause()
++        );
++        // CraftBukkit end
+     }
+ 
+     public static Optional<ServerPlayer.RespawnPosAngle> findRespawnAndUseSpawnBlock(
+@@ -993,10 +_,10 @@
+                 level.setBlock(pos, blockState.setValue(RespawnAnchorBlock.CHARGE, Integer.valueOf(blockState.getValue(RespawnAnchorBlock.CHARGE) - 1)), 3);
+             }
+ 
+-            return optional.map(vec3 -> ServerPlayer.RespawnPosAngle.of(vec3, pos));
++            return optional.map(vec3 -> ServerPlayer.RespawnPosAngle.of(vec3, pos, false, true)); // CraftBukkit
+         } else if (block instanceof BedBlock && BedBlock.canSetSpawn(level)) {
+             return BedBlock.findStandUpPosition(EntityType.PLAYER, level, pos, blockState.getValue(BedBlock.FACING), angle)
+-                .map(vec3 -> ServerPlayer.RespawnPosAngle.of(vec3, pos));
++                .map(vec3 -> ServerPlayer.RespawnPosAngle.of(vec3, pos, true, false)); // CraftBukkit
+         } else if (!forced) {
+             return Optional.empty();
+         } else {
+@@ -1004,7 +_,7 @@
+             BlockState blockState1 = level.getBlockState(pos.above());
+             boolean isPossibleToRespawnInThis1 = blockState1.getBlock().isPossibleToRespawnInThis(blockState1);
+             return isPossibleToRespawnInThis && isPossibleToRespawnInThis1
+-                ? Optional.of(new ServerPlayer.RespawnPosAngle(new Vec3(pos.getX() + 0.5, pos.getY() + 0.1, pos.getZ() + 0.5), angle))
++                ? Optional.of(new ServerPlayer.RespawnPosAngle(new Vec3(pos.getX() + 0.5, pos.getY() + 0.1, pos.getZ() + 0.5), angle, false, false)) // CraftBukkit
+                 : Optional.empty();
+         }
+     }
+@@ -1022,6 +_,7 @@
+     @Nullable
+     @Override
+     public ServerPlayer teleport(TeleportTransition teleportTransition) {
++        if (this.isSleeping()) return null; // CraftBukkit - SPIGOT-3154
+         if (this.isRemoved()) {
+             return null;
+         } else {
+@@ -1031,17 +_,52 @@
+ 
+             ServerLevel level = teleportTransition.newLevel();
+             ServerLevel serverLevel = this.serverLevel();
+-            ResourceKey<Level> resourceKey = serverLevel.dimension();
++            // CraftBukkit start
++            ResourceKey<net.minecraft.world.level.dimension.LevelStem> resourceKey = serverLevel.getTypeKey();
++
++            org.bukkit.Location enter = this.getBukkitEntity().getLocation();
++            PositionMoveRotation absolutePosition = PositionMoveRotation.calculateAbsolute(PositionMoveRotation.of(this), PositionMoveRotation.of(teleportTransition), teleportTransition.relatives());
++            org.bukkit.Location exit = /* (worldserver == null) ? null : // Paper - always non-null */org.bukkit.craftbukkit.util.CraftLocation.toBukkit(absolutePosition.position(), level.getWorld(), absolutePosition.yRot(), absolutePosition.xRot());
++            org.bukkit.event.player.PlayerTeleportEvent tpEvent = new org.bukkit.event.player.PlayerTeleportEvent(this.getBukkitEntity(), enter, exit, teleportTransition.cause());
++            // Paper start - gateway-specific teleport event
++            if (this.portalProcess != null && this.portalProcess.isSamePortal(((net.minecraft.world.level.block.EndGatewayBlock) net.minecraft.world.level.block.Blocks.END_GATEWAY)) && this.serverLevel().getBlockEntity(this.portalProcess.getEntryPosition()) instanceof net.minecraft.world.level.block.entity.TheEndGatewayBlockEntity theEndGatewayBlockEntity) {
++                tpEvent = new com.destroystokyo.paper.event.player.PlayerTeleportEndGatewayEvent(this.getBukkitEntity(), enter, exit, new org.bukkit.craftbukkit.block.CraftEndGateway(this.serverLevel().getWorld(), theEndGatewayBlockEntity));
++            }
++            // Paper end - gateway-specific teleport event
++            org.bukkit.Bukkit.getServer().getPluginManager().callEvent(tpEvent);
++            org.bukkit.Location newExit = tpEvent.getTo();
++            if (tpEvent.isCancelled() || newExit == null) {
++                return null;
++            }
++            if (!newExit.equals(exit)) {
++                level = ((org.bukkit.craftbukkit.CraftWorld) newExit.getWorld()).getHandle();
++                teleportTransition = new TeleportTransition(
++                    level,
++                    org.bukkit.craftbukkit.util.CraftLocation.toVec3D(newExit),
++                    Vec3.ZERO,
++                    newExit.getYaw(),
++                    newExit.getPitch(),
++                    teleportTransition.missingRespawnBlock(),
++                    teleportTransition.asPassenger(),
++                    Set.of(),
++                    teleportTransition.postTeleportTransition(),
++                    teleportTransition.cause());
++            }
++            // CraftBukkit end
+             if (!teleportTransition.asPassenger()) {
+                 this.stopRiding();
+             }
+ 
+-            if (level.dimension() == resourceKey) {
+-                this.connection.teleport(PositionMoveRotation.of(teleportTransition), teleportTransition.relatives());
++            // CraftBukkit start
++            if (level != null && level.dimension() == serverLevel.dimension()) {
++                this.connection.internalTeleport(PositionMoveRotation.of(teleportTransition), teleportTransition.relatives());
++                // CraftBukkit end
+                 this.connection.resetPosition();
+                 teleportTransition.postTeleportTransition().onTransition(this);
+                 return this;
+             } else {
++                // CraftBukkit start
++                /*
+                 this.isChangingDimension = true;
+                 LevelData levelData = level.getLevelData();
+                 this.connection.send(new ClientboundRespawnPacket(this.createCommonSpawnInfo(level), (byte)3));
+@@ -1050,16 +_,30 @@
+                 playerList.sendPlayerPermissionLevel(this);
+                 serverLevel.removePlayerImmediately(this, Entity.RemovalReason.CHANGED_DIMENSION);
+                 this.unsetRemoved();
++                */
++                // CraftBukkit end
+                 ProfilerFiller profilerFiller = Profiler.get();
+                 profilerFiller.push("moving");
+-                if (resourceKey == Level.OVERWORLD && level.dimension() == Level.NETHER) {
++                if (level != null && resourceKey == net.minecraft.world.level.dimension.LevelStem.OVERWORLD && level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER) { // CraftBukkit - empty to fall through to null to event
+                     this.enteredNetherPosition = this.position();
+                 }
+ 
+                 profilerFiller.pop();
+                 profilerFiller.push("placing");
++                // CraftBukkit start
++                this.isChangingDimension = true; // CraftBukkit - Set teleport invulnerability only if player changing worlds
++                LevelData worlddata = level.getLevelData();
++
++                this.connection.send(new ClientboundRespawnPacket(this.createCommonSpawnInfo(level), (byte) 3));
++                this.connection.send(new ClientboundChangeDifficultyPacket(worlddata.getDifficulty(), worlddata.isDifficultyLocked()));
++                PlayerList playerList = this.server.getPlayerList();
++
++                playerList.sendPlayerPermissionLevel(this);
++                serverLevel.removePlayerImmediately(this, Entity.RemovalReason.CHANGED_DIMENSION);
++                this.unsetRemoved();
++                // CraftBukkit end
+                 this.setServerLevel(level);
+-                this.connection.teleport(PositionMoveRotation.of(teleportTransition), teleportTransition.relatives());
++                this.connection.internalTeleport(PositionMoveRotation.of(teleportTransition), teleportTransition.relatives()); // CraftBukkit - use internal teleport without event
+                 this.connection.resetPosition();
+                 level.addDuringTeleport(this);
+                 profilerFiller.pop();
+@@ -1073,10 +_,40 @@
+                 this.lastSentExp = -1;
+                 this.lastSentHealth = -1.0F;
+                 this.lastSentFood = -1;
++
++
++                // CraftBukkit start
++                org.bukkit.event.player.PlayerChangedWorldEvent changeEvent = new org.bukkit.event.player.PlayerChangedWorldEvent(this.getBukkitEntity(), serverLevel.getWorld());
++                this.level().getCraftServer().getPluginManager().callEvent(changeEvent);
++                // CraftBukkit end
++                // Paper start - Reset shield blocking on dimension change
++                if (this.isBlocking()) {
++                    this.stopUsingItem();
++                }
++                // Paper end - Reset shield blocking on dimension change
+                 return this;
+             }
+         }
+     }
++
++    // CraftBukkit start
++    @Override
++    public org.bukkit.craftbukkit.event.CraftPortalEvent callPortalEvent(
++        Entity entity,
++        org.bukkit.Location exit,
++        org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause,
++        int searchRadius,
++        int creationRadius
++    ) {
++        org.bukkit.Location enter = this.getBukkitEntity().getLocation();
++        org.bukkit.event.player.PlayerPortalEvent event = new org.bukkit.event.player.PlayerPortalEvent(this.getBukkitEntity(), enter, exit, cause, searchRadius, true, creationRadius);
++        org.bukkit.Bukkit.getServer().getPluginManager().callEvent(event);
++        if (event.isCancelled() || event.getTo() == null || event.getTo().getWorld() == null) {
++            return null;
++        }
++        return new org.bukkit.craftbukkit.event.CraftPortalEvent(event);
++    }
++    // CraftBukkit end
+ 
+     @Override
+     public void forceSetRotation(float yRot, float xRot) {
+@@ -1086,12 +_,26 @@
+     public void triggerDimensionChangeTriggers(ServerLevel level) {
+         ResourceKey<Level> resourceKey = level.dimension();
+         ResourceKey<Level> resourceKey1 = this.level().dimension();
+-        CriteriaTriggers.CHANGED_DIMENSION.trigger(this, resourceKey, resourceKey1);
+-        if (resourceKey == Level.NETHER && resourceKey1 == Level.OVERWORLD && this.enteredNetherPosition != null) {
++        // CraftBukkit start
++        ResourceKey<Level> maindimensionkey = org.bukkit.craftbukkit.util.CraftDimensionUtil.getMainDimensionKey(level);
++        ResourceKey<Level> maindimensionkey1 = org.bukkit.craftbukkit.util.CraftDimensionUtil.getMainDimensionKey(this.level());
++        // Paper start - Add option for strict advancement dimension checks
++        if (io.papermc.paper.configuration.GlobalConfiguration.get().misc.strictAdvancementDimensionCheck) {
++            maindimensionkey = resourceKey;
++            maindimensionkey1 = resourceKey1;
++        }
++        // Paper end - Add option for strict advancement dimension checks
++        CriteriaTriggers.CHANGED_DIMENSION.trigger(this, maindimensionkey, maindimensionkey1);
++        if (maindimensionkey != resourceKey || maindimensionkey1 != resourceKey1) {
++            CriteriaTriggers.CHANGED_DIMENSION.trigger(this, resourceKey, resourceKey1);
++        }
++
++        if (maindimensionkey == Level.NETHER && maindimensionkey1 == Level.OVERWORLD && this.enteredNetherPosition != null) {
++            // CraftBukkit end
+             CriteriaTriggers.NETHER_TRAVEL.trigger(this, this.enteredNetherPosition);
+         }
+ 
+-        if (resourceKey1 != Level.NETHER) {
++        if (maindimensionkey1 != Level.NETHER) { // CraftBukkit
+             this.enteredNetherPosition = null;
+         }
+     }
+@@ -1107,19 +_,18 @@
+         this.containerMenu.broadcastChanges();
+     }
+ 
+-    @Override
+-    public Either<Player.BedSleepingProblem, Unit> startSleepInBed(BlockPos at) {
+-        Direction direction = this.level().getBlockState(at).getValue(HorizontalDirectionalBlock.FACING);
++    // CraftBukkit start - moved bed result checks from below into separate method
++    private Either<Player.BedSleepingProblem, Unit> getBedResult(BlockPos at, Direction direction) {
+         if (this.isSleeping() || !this.isAlive()) {
+             return Either.left(Player.BedSleepingProblem.OTHER_PROBLEM);
+-        } else if (!this.level().dimensionType().natural()) {
++        } else if (!this.level().dimensionType().natural() && !this.level().dimensionType().bedWorks()) { // CraftBukkit - moved bed result checks from below into separate method
+             return Either.left(Player.BedSleepingProblem.NOT_POSSIBLE_HERE);
+         } else if (!this.bedInRange(at, direction)) {
+             return Either.left(Player.BedSleepingProblem.TOO_FAR_AWAY);
+         } else if (this.bedBlocked(at, direction)) {
+             return Either.left(Player.BedSleepingProblem.OBSTRUCTED);
+         } else {
+-            this.setRespawnPosition(this.level().dimension(), at, this.getYRot(), false, true);
++            this.setRespawnPosition(this.level().dimension(), at, this.getYRot(), false, true, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.BED); // Paper - Add PlayerSetSpawnEvent
+             if (this.level().isDay()) {
+                 return Either.left(Player.BedSleepingProblem.NOT_POSSIBLE_NOW);
+             } else {
+@@ -1138,7 +_,34 @@
+                     }
+                 }
+ 
+-                Either<Player.BedSleepingProblem, Unit> either = super.startSleepInBed(at).ifRight(unit -> {
++    // CraftBukkit start
++                return Either.right(Unit.INSTANCE);
++            }
++        }
++    }
++
++    @Override
++    public Either<net.minecraft.world.entity.player.Player.BedSleepingProblem, Unit> startSleepInBed(BlockPos at, boolean force) {
++        Direction enumdirection = (Direction) this.level().getBlockState(at).getValue(HorizontalDirectionalBlock.FACING);
++        Either<net.minecraft.world.entity.player.Player.BedSleepingProblem, Unit> bedResult = this.getBedResult(at, enumdirection);
++
++        if (bedResult.left().orElse(null) == net.minecraft.world.entity.player.Player.BedSleepingProblem.OTHER_PROBLEM) {
++            return bedResult; // return immediately if the result is not bypassable by plugins
++        }
++
++        if (force) {
++            bedResult = Either.right(Unit.INSTANCE);
++        }
++
++        bedResult = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerBedEnterEvent(this, at, bedResult);
++        if (bedResult.left().isPresent()) {
++            return bedResult;
++        }
++
++        {
++            {
++                Either<net.minecraft.world.entity.player.Player.BedSleepingProblem, Unit> either = super.startSleepInBed(at, force).ifRight(unit -> {
++                    // CraftBukkit end
+                     this.awardStat(Stats.SLEEP_IN_BED);
+                     CriteriaTriggers.SLEPT_IN_BED.trigger(this);
+                 });
+@@ -1174,13 +_,31 @@
+ 
+     @Override
+     public void stopSleepInBed(boolean wakeImmediately, boolean updateLevelForSleepingPlayers) {
++        if (!this.isSleeping()) return; // CraftBukkit - Can't leave bed if not in one!
++        // CraftBukkit start - fire PlayerBedLeaveEvent
++        org.bukkit.craftbukkit.entity.CraftPlayer player = this.getBukkitEntity();
++        BlockPos bedPosition = this.getSleepingPos().orElse(null);
++
++        org.bukkit.block.Block bed;
++        if (bedPosition != null) {
++            bed = this.level().getWorld().getBlockAt(bedPosition.getX(), bedPosition.getY(), bedPosition.getZ());
++        } else {
++            bed = this.level().getWorld().getBlockAt(player.getLocation());
++        }
++
++        org.bukkit.event.player.PlayerBedLeaveEvent event = new org.bukkit.event.player.PlayerBedLeaveEvent(player, bed, true);
++        this.level().getCraftServer().getPluginManager().callEvent(event);
++        if (event.isCancelled()) {
++            return;
++        }
++        // CraftBukkit end
+         if (this.isSleeping()) {
+             this.serverLevel().getChunkSource().broadcastAndSend(this, new ClientboundAnimatePacket(this, 2));
+         }
+ 
+         super.stopSleepInBed(wakeImmediately, updateLevelForSleepingPlayers);
+         if (this.connection != null) {
+-            this.connection.teleport(this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot());
++            this.connection.teleport(this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot(), org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.EXIT_BED); // CraftBukkit
+         }
+     }
+ 
+@@ -1192,9 +_,9 @@
+ 
+     @Override
+     public boolean isInvulnerableTo(ServerLevel level, DamageSource damageSource) {
+-        return super.isInvulnerableTo(level, damageSource)
++        return (super.isInvulnerableTo(level, damageSource) // Paper - disable player cramming;
+             || this.isChangingDimension() && !damageSource.is(DamageTypes.ENDER_PEARL)
+-            || !this.hasClientLoaded();
++            || !this.hasClientLoaded()) || (!this.level().paperConfig().collisions.allowPlayerCrammingDamage && damageSource.is(DamageTypes.CRAMMING)); // Paper - disable player cramming;
+     }
+ 
+     @Override
+@@ -1237,8 +_,9 @@
+         this.connection.send(new ClientboundOpenSignEditorPacket(signEntity.getBlockPos(), isFrontText));
+     }
+ 
+-    public void nextContainerCounter() {
++    public int nextContainerCounter() { // CraftBukkit - void -> int
+         this.containerCounter = this.containerCounter % 100 + 1;
++        return this.containerCounter; // CraftBukkit
+     }
+ 
+     @Override
+@@ -1246,12 +_,43 @@
+         if (menu == null) {
+             return OptionalInt.empty();
+         } else {
++            // CraftBukkit start - SPIGOT-6552: Handle inventory closing in CraftEventFactory#callInventoryOpenEvent(...)
++            /*
+             if (this.containerMenu != this.inventoryMenu) {
+                 this.closeContainer();
+             }
++            */
++            // CraftBukkit end
+ 
+             this.nextContainerCounter();
+             AbstractContainerMenu abstractContainerMenu = menu.createMenu(this.containerCounter, this.getInventory(), this);
++            Component title = null; // Paper - Add titleOverride to InventoryOpenEvent
++            // CraftBukkit start - Inventory open hook
++            if (abstractContainerMenu != null) {
++                abstractContainerMenu.setTitle(menu.getDisplayName());
++
++                boolean cancelled = false;
++                // Paper start - Add titleOverride to InventoryOpenEvent
++                final com.mojang.datafixers.util.Pair<net.kyori.adventure.text.Component, AbstractContainerMenu> result = org.bukkit.craftbukkit.event.CraftEventFactory.callInventoryOpenEventWithTitle(this, abstractContainerMenu, cancelled);
++                abstractContainerMenu = result.getSecond();
++                title = io.papermc.paper.adventure.PaperAdventure.asVanilla(result.getFirst());
++                // Paper end - Add titleOverride to InventoryOpenEvent
++                if (abstractContainerMenu == null && !cancelled) { // Let pre-cancelled events fall through
++                    // SPIGOT-5263 - close chest if cancelled
++                    if (menu instanceof Container) {
++                        ((Container) menu).stopOpen(this);
++                    } else if (menu instanceof net.minecraft.world.level.block.ChestBlock.DoubleInventory doubleInventory) {
++                        // SPIGOT-5355 - double chests too :(
++                        doubleInventory.inventorylargechest.stopOpen(this);
++                        // Paper start - Fix InventoryOpenEvent cancellation
++                    } else if (!this.enderChestInventory.isActiveChest(null)) {
++                        this.enderChestInventory.stopOpen(this);
++                        // Paper end - Fix InventoryOpenEvent cancellation
++                    }
++                    return OptionalInt.empty();
++                }
++            }
++            // CraftBukkit end
+             if (abstractContainerMenu == null) {
+                 if (this.isSpectator()) {
+                     this.displayClientMessage(Component.translatable("container.spectatorCantOpen").withStyle(ChatFormatting.RED), true);
+@@ -1259,10 +_,14 @@
+ 
+                 return OptionalInt.empty();
+             } else {
++                // CraftBukkit start
++                this.containerMenu = abstractContainerMenu; // Moved up
++                if (!this.isImmobile())
+                 this.connection
+-                    .send(new ClientboundOpenScreenPacket(abstractContainerMenu.containerId, abstractContainerMenu.getType(), menu.getDisplayName()));
++                    .send(new net.minecraft.network.protocol.game.ClientboundOpenScreenPacket(abstractContainerMenu.containerId, abstractContainerMenu.getType(), java.util.Objects.requireNonNullElseGet(title, abstractContainerMenu::getTitle))); // Paper - Add titleOverride to InventoryOpenEven
++                // CraftBukkit end
+                 this.initMenu(abstractContainerMenu);
+-                this.containerMenu = abstractContainerMenu;
++                // CraftBukkit - moved up
+                 return OptionalInt.of(this.containerCounter);
+             }
+         }
+@@ -1275,14 +_,25 @@
+ 
+     @Override
+     public void openHorseInventory(AbstractHorse horse, Container inventory) {
++        // CraftBukkit start - Inventory open hook
++        this.nextContainerCounter(); // Moved up from below
++        AbstractContainerMenu container = new HorseInventoryMenu(this.containerCounter, this.getInventory(), inventory, horse, horse.getInventoryColumns());
++        container.setTitle(horse.getDisplayName());
++        container = org.bukkit.craftbukkit.event.CraftEventFactory.callInventoryOpenEvent(this, container);
++
++        if (container == null) {
++            inventory.stopOpen(this);
++            return;
++        }
++        // CraftBukkit end
+         if (this.containerMenu != this.inventoryMenu) {
+-            this.closeContainer();
++            this.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.OPEN_NEW); // Paper - Inventory close reason
+         }
+ 
+-        this.nextContainerCounter();
++        // this.nextContainerCounter(); // CraftBukkit - moved up
+         int inventoryColumns = horse.getInventoryColumns();
+         this.connection.send(new ClientboundHorseScreenOpenPacket(this.containerCounter, inventoryColumns, horse.getId()));
+-        this.containerMenu = new HorseInventoryMenu(this.containerCounter, this.getInventory(), inventory, horse, inventoryColumns);
++        this.containerMenu = container; // CraftBukkit
+         this.initMenu(this.containerMenu);
+     }
+ 
+@@ -1304,9 +_,28 @@
+ 
+     @Override
+     public void closeContainer() {
++        // Paper start - Inventory close reason
++        this.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.UNKNOWN);
++    }
++    @Override
++    public void closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason reason) {
++        org.bukkit.craftbukkit.event.CraftEventFactory.handleInventoryCloseEvent(this, reason); // CraftBukkit
++        // Paper end - Inventory close reason
+         this.connection.send(new ClientboundContainerClosePacket(this.containerMenu.containerId));
+         this.doCloseContainer();
+     }
++    // Paper start - special close for unloaded inventory
++    @Override
++    public void closeUnloadedInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason reason) {
++        // copied from above
++        org.bukkit.craftbukkit.event.CraftEventFactory.handleInventoryCloseEvent(this, reason); // CraftBukkit
++        // Paper end
++        // copied from below
++        this.connection.send(new ClientboundContainerClosePacket(this.containerMenu.containerId));
++        this.containerMenu = this.inventoryMenu;
++        // do not run close logic
++    }
++    // Paper end - special close for unloaded inventory
+ 
+     @Override
+     public void doCloseContainer() {
+@@ -1330,19 +_,19 @@
+                 int rounded = Math.round((float)Math.sqrt(dx * dx + dy * dy + dz * dz) * 100.0F);
+                 if (rounded > 0) {
+                     this.awardStat(Stats.SWIM_ONE_CM, rounded);
+-                    this.causeFoodExhaustion(0.01F * rounded * 0.01F);
++                    this.causeFoodExhaustion(this.level().spigotConfig.swimMultiplier * (float) rounded * 0.01F, org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.SWIM); // CraftBukkit - EntityExhaustionEvent // Spigot
+                 }
+             } else if (this.isEyeInFluid(FluidTags.WATER)) {
+                 int rounded = Math.round((float)Math.sqrt(dx * dx + dy * dy + dz * dz) * 100.0F);
+                 if (rounded > 0) {
+                     this.awardStat(Stats.WALK_UNDER_WATER_ONE_CM, rounded);
+-                    this.causeFoodExhaustion(0.01F * rounded * 0.01F);
++                    this.causeFoodExhaustion(this.level().spigotConfig.swimMultiplier * (float) rounded * 0.01F, org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.WALK_UNDERWATER); // CraftBukkit - EntityExhaustionEvent // Spigot
+                 }
+             } else if (this.isInWater()) {
+                 int rounded = Math.round((float)Math.sqrt(dx * dx + dz * dz) * 100.0F);
+                 if (rounded > 0) {
+                     this.awardStat(Stats.WALK_ON_WATER_ONE_CM, rounded);
+-                    this.causeFoodExhaustion(0.01F * rounded * 0.01F);
++                    this.causeFoodExhaustion(this.level().spigotConfig.swimMultiplier * (float) rounded * 0.01F, org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.WALK_ON_WATER); // CraftBukkit - EntityExhaustionEvent // Spigot
+                 }
+             } else if (this.onClimbable()) {
+                 if (dy > 0.0) {
+@@ -1353,13 +_,13 @@
+                 if (rounded > 0) {
+                     if (this.isSprinting()) {
+                         this.awardStat(Stats.SPRINT_ONE_CM, rounded);
+-                        this.causeFoodExhaustion(0.1F * rounded * 0.01F);
++                        this.causeFoodExhaustion(this.level().spigotConfig.sprintMultiplier * (float) rounded * 0.01F, org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.SPRINT); // CraftBukkit - EntityExhaustionEvent // Spigot
+                     } else if (this.isCrouching()) {
+                         this.awardStat(Stats.CROUCH_ONE_CM, rounded);
+-                        this.causeFoodExhaustion(0.0F * rounded * 0.01F);
++                        this.causeFoodExhaustion(this.level().spigotConfig.otherMultiplier * (float) rounded * 0.01F, org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.CROUCH); // CraftBukkit - EntityExhaustionEvent // Spigot
+                     } else {
+                         this.awardStat(Stats.WALK_ONE_CM, rounded);
+-                        this.causeFoodExhaustion(0.0F * rounded * 0.01F);
++                        this.causeFoodExhaustion(this.level().spigotConfig.otherMultiplier * (float) rounded * 0.01F, org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.WALK); // CraftBukkit - EntityExhaustionEvent // Spigot
+                     }
+                 }
+             } else if (this.isFallFlying()) {
+@@ -1399,13 +_,13 @@
+     @Override
+     public void awardStat(Stat<?> stat, int amount) {
+         this.stats.increment(this, stat, amount);
+-        this.getScoreboard().forAllObjectives(stat, this, score -> score.add(amount));
++        this.level().getCraftServer().getScoreboardManager().forAllObjectives(stat, this, score -> score.add(amount)); // CraftBukkit - Get our scores instead
+     }
+ 
+     @Override
+     public void resetStat(Stat<?> stat) {
+         this.stats.setValue(this, stat, 0);
+-        this.getScoreboard().forAllObjectives(stat, this, ScoreAccess::reset);
++        this.level().getCraftServer().getScoreboardManager().forAllObjectives(stat, this, ScoreAccess::reset); // CraftBukkit - Get our scores instead
+     }
+ 
+     @Override
+@@ -1436,9 +_,9 @@
+         super.jumpFromGround();
+         this.awardStat(Stats.JUMP);
+         if (this.isSprinting()) {
+-            this.causeFoodExhaustion(0.2F);
++            this.causeFoodExhaustion(this.level().spigotConfig.jumpSprintExhaustion, org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.JUMP_SPRINT); // CraftBukkit - EntityExhaustionEvent // Spigot - Change to use configurable value
+         } else {
+-            this.causeFoodExhaustion(0.05F);
++            this.causeFoodExhaustion(this.level().spigotConfig.jumpWalkExhaustion, org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.JUMP); // CraftBukkit - EntityExhaustionEvent // Spigot - Change to use configurable value
+         }
+     }
+ 
+@@ -1451,6 +_,13 @@
+     public void disconnect() {
+         this.disconnected = true;
+         this.ejectPassengers();
++
++        // Paper start - Workaround vehicle not tracking the passenger disconnection dismount
++        if (this.isPassenger() && this.getVehicle() instanceof ServerPlayer) {
++            this.stopRiding();
++        }
++        // Paper end - Workaround vehicle not tracking the passenger disconnection dismount
++
+         if (this.isSleeping()) {
+             this.stopSleepInBed(true, false);
+         }
+@@ -1462,6 +_,7 @@
+ 
+     public void resetSentInfo() {
+         this.lastSentHealth = -1.0E8F;
++        this.lastSentExp = -1; // CraftBukkit - Added to reset
+     }
+ 
+     @Override
+@@ -1496,12 +_,12 @@
+         this.onUpdateAbilities();
+         if (keepEverything) {
+             this.getAttributes().assignBaseValues(that.getAttributes());
+-            this.getAttributes().assignPermanentModifiers(that.getAttributes());
++            // this.getAttributes().assignPermanentModifiers(that.getAttributes()); // CraftBukkit
+             this.setHealth(that.getHealth());
+             this.foodData = that.foodData;
+ 
+             for (MobEffectInstance mobEffectInstance : that.getActiveEffects()) {
+-                this.addEffect(new MobEffectInstance(mobEffectInstance));
++                // this.addEffect(new MobEffectInstance(mobEffectInstance)); // CraftBukkit
+             }
+ 
+             this.getInventory().replaceWith(that.getInventory());
+@@ -1512,7 +_,7 @@
+             this.portalProcess = that.portalProcess;
+         } else {
+             this.getAttributes().assignBaseValues(that.getAttributes());
+-            this.setHealth(this.getMaxHealth());
++            // this.setHealth(this.getMaxHealth()); // CraftBukkit
+             if (this.serverLevel().getGameRules().getBoolean(GameRules.RULE_KEEPINVENTORY) || that.isSpectator()) {
+                 this.getInventory().replaceWith(that.getInventory());
+                 this.experienceLevel = that.experienceLevel;
+@@ -1528,7 +_,7 @@
+         this.lastSentExp = -1;
+         this.lastSentHealth = -1.0F;
+         this.lastSentFood = -1;
+-        this.recipeBook.copyOverData(that.recipeBook);
++        // this.recipeBook.copyOverData(that.recipeBook); // CraftBukkit
+         this.seenCredits = that.seenCredits;
+         this.enteredNetherPosition = that.enteredNetherPosition;
+         this.chunkTrackingView = that.chunkTrackingView;
+@@ -1581,7 +_,7 @@
+     }
+ 
+     @Override
+-    public boolean teleportTo(ServerLevel level, double x, double y, double z, Set<Relative> relativeMovements, float yaw, float pitch, boolean setCamera) {
++    public boolean teleportTo(ServerLevel level, double x, double y, double z, Set<Relative> relativeMovements, float yaw, float pitch, boolean setCamera, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause) { // CraftBukkit
+         if (this.isSleeping()) {
+             this.stopSleepInBed(true, true);
+         }
+@@ -1590,7 +_,7 @@
+             this.setCamera(this);
+         }
+ 
+-        boolean flag = super.teleportTo(level, x, y, z, relativeMovements, yaw, pitch, setCamera);
++        boolean flag = super.teleportTo(level, x, y, z, relativeMovements, yaw, pitch, setCamera, cause); // CraftBukkit
+         if (flag) {
+             this.setYHeadRot(relativeMovements.contains(Relative.Y_ROT) ? this.getYHeadRot() + yaw : yaw);
+         }
+@@ -1627,9 +_,17 @@
+     }
+ 
+     public boolean setGameMode(GameType gameMode) {
++        // Paper start - Expand PlayerGameModeChangeEvent
++        org.bukkit.event.player.PlayerGameModeChangeEvent event = this.setGameMode(gameMode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.UNKNOWN, null);
++        return event == null ? false : event.isCancelled();
++    }
++    @Nullable
++    public org.bukkit.event.player.PlayerGameModeChangeEvent setGameMode(GameType gameMode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause cause, @Nullable net.kyori.adventure.text.Component message) {
+         boolean isSpectator = this.isSpectator();
+-        if (!this.gameMode.changeGameModeForPlayer(gameMode)) {
+-            return false;
++        org.bukkit.event.player.PlayerGameModeChangeEvent event = this.gameMode.changeGameModeForPlayer(gameMode, cause, message);
++        if (event == null || event.isCancelled()) {
++            return null;
++        // Paper end - Expand PlayerGameModeChangeEvent
+         } else {
+             this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.CHANGE_GAME_MODE, gameMode.getId()));
+             if (gameMode == GameType.SPECTATOR) {
+@@ -1645,7 +_,7 @@
+ 
+             this.onUpdateAbilities();
+             this.updateEffectVisibility();
+-            return true;
++            return event; // Paper - Expand PlayerGameModeChangeEvent
+         }
+     }
+ 
+@@ -1705,8 +_,13 @@
+     }
+ 
+     public void sendChatMessage(OutgoingChatMessage message, boolean filtered, ChatType.Bound boundType) {
++        // Paper start
++        this.sendChatMessage(message, filtered, boundType, null);
++    }
++    public void sendChatMessage(OutgoingChatMessage message, boolean filtered, ChatType.Bound boundType, @Nullable Component unsigned) {
++        // Paper end
+         if (this.acceptsChatMessages()) {
+-            message.sendToPlayer(this, filtered, boundType);
++            message.sendToPlayer(this, filtered, boundType, unsigned); // Paper
+         }
+     }
+ 
+@@ -1717,7 +_,42 @@
+     }
+ 
+     public void updateOptions(ClientInformation clientInformation) {
++        // Paper start - settings event
++        new com.destroystokyo.paper.event.player.PlayerClientOptionsChangeEvent(this.getBukkitEntity(), Util.make(new java.util.IdentityHashMap<>(), map -> {
++            map.put(com.destroystokyo.paper.ClientOption.LOCALE, clientInformation.language());
++            map.put(com.destroystokyo.paper.ClientOption.VIEW_DISTANCE, clientInformation.viewDistance());
++            map.put(com.destroystokyo.paper.ClientOption.CHAT_VISIBILITY, com.destroystokyo.paper.ClientOption.ChatVisibility.valueOf(clientInformation.chatVisibility().name()));
++            map.put(com.destroystokyo.paper.ClientOption.CHAT_COLORS_ENABLED, clientInformation.chatColors());
++            map.put(com.destroystokyo.paper.ClientOption.SKIN_PARTS, new com.destroystokyo.paper.PaperSkinParts(clientInformation.modelCustomisation()));
++            map.put(com.destroystokyo.paper.ClientOption.MAIN_HAND, clientInformation.mainHand() == HumanoidArm.LEFT ? org.bukkit.inventory.MainHand.LEFT : org.bukkit.inventory.MainHand.RIGHT);
++            map.put(com.destroystokyo.paper.ClientOption.TEXT_FILTERING_ENABLED, clientInformation.textFilteringEnabled());
++            map.put(com.destroystokyo.paper.ClientOption.ALLOW_SERVER_LISTINGS, clientInformation.allowsListing());
++            map.put(com.destroystokyo.paper.ClientOption.PARTICLE_VISIBILITY, com.destroystokyo.paper.ClientOption.ParticleVisibility.valueOf(clientInformation.particleStatus().name()));
++        })).callEvent();
++        // Paper end - settings event
++        // CraftBukkit start
++        if (this.getMainArm() != clientInformation.mainHand()) {
++            org.bukkit.event.player.PlayerChangedMainHandEvent event = new org.bukkit.event.player.PlayerChangedMainHandEvent(
++                this.getBukkitEntity(),
++                this.getMainArm() == HumanoidArm.LEFT ? org.bukkit.inventory.MainHand.LEFT : org.bukkit.inventory.MainHand.RIGHT
++            );
++            this.server.server.getPluginManager().callEvent(event);
++        }
++        if (this.language == null || !this.language.equals(clientInformation.language())) { // Paper
++            org.bukkit.event.player.PlayerLocaleChangeEvent event = new org.bukkit.event.player.PlayerLocaleChangeEvent(
++                this.getBukkitEntity(),
++                clientInformation.language()
++            );
++            this.server.server.getPluginManager().callEvent(event);
++        }
++        // CraftBukkit end
++        // Paper start - don't call options events on login
++        this.updateOptionsNoEvents(clientInformation);
++    }
++    public void updateOptionsNoEvents(ClientInformation clientInformation) {
++        // Paper end
+         this.language = clientInformation.language();
++        this.adventure$locale = java.util.Objects.requireNonNullElse(net.kyori.adventure.translation.Translator.parseLocale(this.language), java.util.Locale.US); // Paper
+         this.requestedViewDistance = clientInformation.viewDistance();
+         this.chatVisibility = clientInformation.chatVisibility();
+         this.canChatColor = clientInformation.chatColors();
+@@ -1803,8 +_,23 @@
+         Entity camera = this.getCamera();
+         this.camera = (Entity)(entityToSpectate == null ? this : entityToSpectate);
+         if (camera != this.camera) {
++            // Paper start - Add PlayerStartSpectatingEntityEvent and PlayerStopSpectatingEntity
++            if (this.camera == this) {
++                com.destroystokyo.paper.event.player.PlayerStopSpectatingEntityEvent playerStopSpectatingEntityEvent = new com.destroystokyo.paper.event.player.PlayerStopSpectatingEntityEvent(this.getBukkitEntity(), camera.getBukkitEntity());
++                if (!playerStopSpectatingEntityEvent.callEvent()) {
++                    this.camera = camera; // rollback camera entity again
++                    return;
++                }
++            } else {
++                com.destroystokyo.paper.event.player.PlayerStartSpectatingEntityEvent playerStartSpectatingEntityEvent = new com.destroystokyo.paper.event.player.PlayerStartSpectatingEntityEvent(this.getBukkitEntity(), camera.getBukkitEntity(), entityToSpectate.getBukkitEntity());
++                if (!playerStartSpectatingEntityEvent.callEvent()) {
++                    this.camera = camera; // rollback camera entity again
++                    return;
++                }
++            }
++            // Paper end - Add PlayerStartSpectatingEntityEvent and PlayerStopSpectatingEntity
+             if (this.camera.level() instanceof ServerLevel serverLevel) {
+-                this.teleportTo(serverLevel, this.camera.getX(), this.camera.getY(), this.camera.getZ(), Set.of(), this.getYRot(), this.getXRot(), false);
++                this.teleportTo(serverLevel, this.camera.getX(), this.camera.getY(), this.camera.getZ(), Set.of(), this.getYRot(), this.getXRot(), false, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.SPECTATE); // CraftBukkit
+             }
+ 
+             if (entityToSpectate != null) {
+@@ -1838,11 +_,11 @@
+ 
+     @Nullable
+     public Component getTabListDisplayName() {
+-        return null;
++        return this.listName; // CraftBukkit
+     }
+ 
+     public int getTabListOrder() {
+-        return 0;
++        return this.listOrder; // CraftBukkit
+     }
+ 
+     @Override
+@@ -1884,11 +_,56 @@
+         this.setRespawnPosition(player.getRespawnDimension(), player.getRespawnPosition(), player.getRespawnAngle(), player.isRespawnForced(), false);
+     }
+ 
++    @Deprecated // Paper - Add PlayerSetSpawnEvent
+     public void setRespawnPosition(ResourceKey<Level> dimension, @Nullable BlockPos position, float angle, boolean forced, boolean sendMessage) {
++        // Paper start - Add PlayerSetSpawnEvent
++        this.setRespawnPosition(dimension, position, angle, forced, sendMessage, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.UNKNOWN);
++    }
++    @Deprecated
++    public boolean setRespawnPosition(ResourceKey<Level> dimension, @Nullable BlockPos position, float angle, boolean forced, boolean sendMessage, org.bukkit.event.player.PlayerSpawnChangeEvent.Cause cause) {
++        return this.setRespawnPosition(dimension, position, angle, forced, sendMessage, cause == org.bukkit.event.player.PlayerSpawnChangeEvent.Cause.RESET ?
++            com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.PLAYER_RESPAWN : com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.valueOf(cause.name()));
++    }
++    public boolean setRespawnPosition(ResourceKey<Level> dimension, @Nullable BlockPos position, float angle, boolean forced, boolean sendMessage, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause cause) {
++        org.bukkit.Location spawnLoc = null;
++        boolean willNotify = false;
+         if (position != null) {
+             boolean flag = position.equals(this.respawnPosition) && dimension.equals(this.respawnDimension);
+-            if (sendMessage && !flag) {
+-                this.sendSystemMessage(Component.translatable("block.minecraft.set_spawn"));
++            spawnLoc = io.papermc.paper.util.MCUtil.toLocation(this.getServer().getLevel(dimension), position);
++            spawnLoc.setYaw(angle);
++            willNotify = sendMessage && !flag;
++        }
++        org.bukkit.event.player.PlayerSpawnChangeEvent dumbEvent = new org.bukkit.event.player.PlayerSpawnChangeEvent(
++            this.getBukkitEntity(),
++            spawnLoc,
++            forced,
++            cause == com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.PLAYER_RESPAWN
++                ? org.bukkit.event.player.PlayerSpawnChangeEvent.Cause.RESET
++                : org.bukkit.event.player.PlayerSpawnChangeEvent.Cause.valueOf(cause.name())
++        );
++        dumbEvent.callEvent();
++
++        com.destroystokyo.paper.event.player.PlayerSetSpawnEvent event = new com.destroystokyo.paper.event.player.PlayerSetSpawnEvent(
++            this.getBukkitEntity(),
++            cause,
++            dumbEvent.getNewSpawn(),
++            dumbEvent.isForced(),
++            willNotify,
++            willNotify ? net.kyori.adventure.text.Component.translatable("block.minecraft.set_spawn") : null
++        );
++        event.setCancelled(dumbEvent.isCancelled());
++        if (!event.callEvent()) {
++            return false;
++        }
++        if (event.getLocation() != null) {
++            dimension = event.getLocation().getWorld() != null ? ((org.bukkit.craftbukkit.CraftWorld) event.getLocation().getWorld()).getHandle().dimension() : dimension;
++            position = io.papermc.paper.util.MCUtil.toBlockPosition(event.getLocation());
++            angle = event.getLocation().getYaw();
++            forced = event.isForced();
++            // Paper end - Add PlayerSetSpawnEvent
++
++            if (event.willNotifyPlayer() && event.getNotification() != null) { // Paper - Add PlayerSetSpawnEvent
++                this.sendSystemMessage(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.getNotification())); // Paper - Add PlayerSetSpawnEvent
+             }
+ 
+             this.respawnPosition = position;
+@@ -1901,6 +_,8 @@
+             this.respawnAngle = 0.0F;
+             this.respawnForced = false;
+         }
++
++        return true; // Paper - Add PlayerSetSpawnEvent
+     }
+ 
+     public SectionPos getLastSectionPos() {
+@@ -1930,21 +_,54 @@
+     }
+ 
+     @Override
+-    public ItemEntity drop(ItemStack droppedItem, boolean dropAround, boolean traceItem) {
++    public ItemEntity drop(ItemStack droppedItem, boolean dropAround, boolean traceItem, boolean callEvent) { // CraftBukkit - SPIGOT-2942: Add boolean to call event
+         ItemEntity itemEntity = this.createItemStackToDrop(droppedItem, dropAround, traceItem);
+         if (itemEntity == null) {
+             return null;
+         } else {
++            // CraftBukkit start - fire PlayerDropItemEvent
++            if (callEvent) {
++                org.bukkit.entity.Player player = this.getBukkitEntity();
++                org.bukkit.entity.Item drop = (org.bukkit.entity.Item) itemEntity.getBukkitEntity();
++
++                org.bukkit.event.player.PlayerDropItemEvent event = new org.bukkit.event.player.PlayerDropItemEvent(player, drop);
++                this.level().getCraftServer().getPluginManager().callEvent(event);
++
++                if (event.isCancelled()) {
++                    org.bukkit.inventory.ItemStack cur = player.getInventory().getItemInHand();
++                    if (traceItem && (cur == null || cur.getAmount() == 0)) {
++                        // The complete stack was dropped
++                        player.getInventory().setItemInHand(drop.getItemStack());
++                    } else if (traceItem && cur.isSimilar(drop.getItemStack()) && cur.getAmount() < cur.getMaxStackSize() && drop.getItemStack().getAmount() == 1) {
++                        // Only one item is dropped
++                        cur.setAmount(cur.getAmount() + 1);
++                        player.getInventory().setItemInHand(cur);
++                    } else {
++                        // Fallback
++                        player.getInventory().addItem(drop.getItemStack());
++                    }
++                    return null;
++                }
++            }
++            // CraftBukkit end
+             this.level().addFreshEntity(itemEntity);
+             ItemStack item = itemEntity.getItem();
+             if (traceItem) {
+                 if (!item.isEmpty()) {
+-                    this.awardStat(Stats.ITEM_DROPPED.get(item.getItem()), droppedItem.getCount());
++                    this.awardStat(Stats.ITEM_DROPPED.get(item.getItem()), item.getCount()); // Paper - Fix PlayerDropItemEvent using wrong item
+                 }
+ 
+                 this.awardStat(Stats.DROP);
+             }
+ 
++            // Paper start - remove player from map on drop
++            if (item.getItem() == net.minecraft.world.item.Items.FILLED_MAP) {
++                final MapItemSavedData mapData = MapItem.getSavedData(item, this.level());
++                if (mapData != null) {
++                    mapData.tickCarriedBy(this, item);
++                }
++            }
++            // Paper end - remove player from map on drop
+             return itemEntity;
+         }
+     }
+@@ -1955,6 +_,11 @@
+             return null;
+         } else {
+             double d = this.getEyeY() - 0.3F;
++            // Paper start
++            ItemStack tmp = droppedItem.copy();
++            droppedItem.setCount(0);
++            droppedItem = tmp;
++            // Paper end
+             ItemEntity itemEntity = new ItemEntity(this.level(), this.getX(), d, this.getZ(), droppedItem);
+             itemEntity.setPickUpDelay(40);
+             if (includeThrowerName) {
+@@ -2008,6 +_,16 @@
+     }
+ 
+     public void loadGameTypes(@Nullable CompoundTag tag) {
++        // Paper start - Expand PlayerGameModeChangeEvent
++        if (this.server.getForcedGameType() != null && this.server.getForcedGameType() != ServerPlayer.readPlayerMode(tag, "playerGameType")) {
++            if (new org.bukkit.event.player.PlayerGameModeChangeEvent(this.getBukkitEntity(), org.bukkit.GameMode.getByValue(this.server.getDefaultGameType().getId()), org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.DEFAULT_GAMEMODE, null).callEvent()) {
++                this.gameMode.setGameModeForPlayer(this.server.getForcedGameType(), GameType.DEFAULT_MODE);
++            } else {
++                this.gameMode.setGameModeForPlayer(ServerPlayer.readPlayerMode(tag,"playerGameType"), ServerPlayer.readPlayerMode(tag, "previousPlayerGameType"));
++            }
++            return;
++        }
++        // Paper end - Expand PlayerGameModeChangeEvent
+         this.gameMode
+             .setGameModeForPlayer(this.calculateGameModeForNewPlayer(readPlayerMode(tag, "playerGameType")), readPlayerMode(tag, "previousPlayerGameType"));
+     }
+@@ -2108,8 +_,14 @@
+ 
+     @Override
+     public void stopRiding() {
++        // Paper start - Force entity dismount during teleportation
++        this.stopRiding(false);
++    }
++    @Override
++    public void stopRiding(boolean suppressCancellation) {
++        // Paper end - Force entity dismount during teleportation
+         Entity vehicle = this.getVehicle();
+-        super.stopRiding();
++        super.stopRiding(suppressCancellation); // Paper - Force entity dismount during teleportation
+         if (vehicle instanceof LivingEntity livingEntity) {
+             for (MobEffectInstance mobEffectInstance : livingEntity.getActiveEffects()) {
+                 this.connection.send(new ClientboundRemoveMobEffectPacket(vehicle.getId(), mobEffectInstance.getEffect()));
+@@ -2204,13 +_,15 @@
+     }
+ 
+     public static long placeEnderPearlTicket(ServerLevel level, ChunkPos pos) {
+-        level.getChunkSource().addRegionTicket(TicketType.ENDER_PEARL, pos, 2, pos);
++        if (!level.paperConfig().misc.legacyEnderPearlBehavior) level.getChunkSource().addRegionTicket(TicketType.ENDER_PEARL, pos, 2, pos); // Paper - Allow using old ender pearl behavior
+         return TicketType.ENDER_PEARL.timeout();
+     }
+ 
+-    public record RespawnPosAngle(Vec3 position, float yaw) {
+-        public static ServerPlayer.RespawnPosAngle of(Vec3 position, BlockPos towardsPos) {
+-            return new ServerPlayer.RespawnPosAngle(position, calculateLookAtYaw(position, towardsPos));
++    // CraftBukkit start
++    public record RespawnPosAngle(Vec3 position, float yaw, boolean isBedSpawn, boolean isAnchorSpawn) {
++        public static ServerPlayer.RespawnPosAngle of(Vec3 position, BlockPos towardsPos, boolean isBedSpawn, boolean isAnchorSpawn) {
++            return new ServerPlayer.RespawnPosAngle(position, calculateLookAtYaw(position, towardsPos), isBedSpawn, isAnchorSpawn);
++    // CraftBukkit end
+         }
+ 
+         private static float calculateLookAtYaw(Vec3 position, BlockPos towardsPos) {
+@@ -2218,4 +_,147 @@
+             return (float)Mth.wrapDegrees(Mth.atan2(vec3.z, vec3.x) * 180.0F / (float)Math.PI - 90.0);
+         }
+     }
++
++    // CraftBukkit start - Add per-player time and weather.
++    public long timeOffset = 0;
++    public boolean relativeTime = true;
++
++    public long getPlayerTime() {
++        if (this.relativeTime) {
++            // Adds timeOffset to the current server time.
++            return this.level().getDayTime() + this.timeOffset;
++        } else {
++            // Adds timeOffset to the beginning of this day.
++            return this.level().getDayTime() - (this.level().getDayTime() % 24000) + this.timeOffset;
++        }
++    }
++
++    public org.bukkit.WeatherType weather = null;
++
++    public org.bukkit.WeatherType getPlayerWeather() {
++        return this.weather;
++    }
++
++    public void setPlayerWeather(org.bukkit.WeatherType type, boolean plugin) {
++        if (!plugin && this.weather != null) {
++            return;
++        }
++
++        if (plugin) {
++            this.weather = type;
++        }
++
++        if (type == org.bukkit.WeatherType.DOWNFALL) {
++            this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.STOP_RAINING, 0));
++        } else {
++            this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0));
++        }
++    }
++
++    private float pluginRainPosition;
++    private float pluginRainPositionPrevious;
++
++    public void updateWeather(float oldRain, float newRain, float oldThunder, float newThunder) {
++        if (this.weather == null) {
++            // Vanilla
++            if (oldRain != newRain) {
++                this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, newRain));
++            }
++        } else {
++            // Plugin
++            if (this.pluginRainPositionPrevious != this.pluginRainPosition) {
++                this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, this.pluginRainPosition));
++            }
++        }
++
++        if (oldThunder != newThunder) {
++            if (this.weather == org.bukkit.WeatherType.DOWNFALL || this.weather == null) {
++                this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, newThunder));
++            } else {
++                this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, 0));
++            }
++        }
++    }
++
++    public void tickWeather() {
++        if (this.weather == null) return;
++
++        this.pluginRainPositionPrevious = this.pluginRainPosition;
++        if (this.weather == org.bukkit.WeatherType.DOWNFALL) {
++            this.pluginRainPosition += 0.01;
++        } else {
++            this.pluginRainPosition -= 0.01;
++        }
++
++        this.pluginRainPosition = Mth.clamp(this.pluginRainPosition, 0.0F, 1.0F);
++    }
++
++    public void resetPlayerWeather() {
++        this.weather = null;
++        this.setPlayerWeather(this.level().getLevelData().isRaining() ? org.bukkit.WeatherType.DOWNFALL : org.bukkit.WeatherType.CLEAR, false);
++    }
++
++    @Override
++    public String toString() {
++        return super.toString() + "(" + this.getScoreboardName() + " at " + this.getX() + "," + this.getY() + "," + this.getZ() + ")";
++    }
++
++    // SPIGOT-1903, MC-98153
++    public void forceSetPositionRotation(double x, double y, double z, float yaw, float pitch) {
++        this.moveTo(x, y, z, yaw, pitch);
++        this.connection.resetPosition();
++    }
++
++    @Override
++    public boolean isImmobile() {
++        return super.isImmobile() || (this.connection != null && this.connection.isDisconnected()); // Paper - Fix duplication bugs
++    }
++
++    @Override
++    public net.minecraft.world.scores.Scoreboard getScoreboard() {
++        return this.getBukkitEntity().getScoreboard().getHandle();
++    }
++
++    public void reset() {
++        float exp = 0;
++
++        if (this.keepLevel) { // CraftBukkit - SPIGOT-6687: Only use keepLevel (was pre-set with RULE_KEEPINVENTORY value in PlayerDeathEvent)
++            exp = this.experienceProgress;
++            this.newTotalExp = this.totalExperience;
++            this.newLevel = this.experienceLevel;
++        }
++
++        this.setHealth(this.getMaxHealth());
++        this.stopUsingItem(); // CraftBukkit - SPIGOT-6682: Clear active item on reset
++        this.setAirSupply(this.getMaxAirSupply()); // Paper - Reset players airTicks on respawn
++        this.setRemainingFireTicks(0);
++        this.fallDistance = 0;
++        this.foodData = new net.minecraft.world.food.FoodData();
++        this.experienceLevel = this.newLevel;
++        this.totalExperience = this.newTotalExp;
++        this.experienceProgress = 0;
++        this.deathTime = 0;
++        this.setArrowCount(0, true); // CraftBukkit - ArrowBodyCountChangeEvent
++        this.removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.DEATH);
++        this.effectsDirty = true;
++        this.containerMenu = this.inventoryMenu;
++        this.lastHurtByPlayer = null;
++        this.lastHurtByMob = null;
++        this.combatTracker = new net.minecraft.world.damagesource.CombatTracker(this);
++        this.lastSentExp = -1;
++        if (this.keepLevel) { // CraftBukkit - SPIGOT-6687: Only use keepLevel (was pre-set with RULE_KEEPINVENTORY value in PlayerDeathEvent)
++            this.experienceProgress = exp;
++        } else {
++            this.giveExperiencePoints(this.newExp);
++        }
++        this.keepLevel = false;
++        this.setDeltaMovement(0, 0, 0); // CraftBukkit - SPIGOT-6948: Reset velocity on death
++        this.skipDropExperience = false; // CraftBukkit - SPIGOT-7462: Reset experience drop skip, so that further deaths drop xp
++    }
++
++    @Override
++    public org.bukkit.craftbukkit.entity.CraftPlayer getBukkitEntity() {
++        return (org.bukkit.craftbukkit.entity.CraftPlayer) super.getBukkitEntity();
++    }
++    // CraftBukkit end
+ }
diff --git a/paper-server/patches/sources/net/minecraft/server/level/ServerPlayerGameMode.java.patch b/paper-server/patches/sources/net/minecraft/server/level/ServerPlayerGameMode.java.patch
new file mode 100644
index 0000000000..df18866114
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/level/ServerPlayerGameMode.java.patch
@@ -0,0 +1,383 @@
+--- a/net/minecraft/server/level/ServerPlayerGameMode.java
++++ b/net/minecraft/server/level/ServerPlayerGameMode.java
+@@ -41,28 +_,49 @@
+     private BlockPos delayedDestroyPos = BlockPos.ZERO;
+     private int delayedTickStart;
+     private int lastSentState = -1;
++    public boolean captureSentBlockEntities = false; // Paper - Send block entities after destroy prediction
++    public boolean capturedBlockEntity = false; // Paper - Send block entities after destroy prediction
+ 
+     public ServerPlayerGameMode(ServerPlayer player) {
+         this.player = player;
+         this.level = player.serverLevel();
+     }
+ 
++    @Deprecated @io.papermc.paper.annotation.DoNotUse // Paper
+     public boolean changeGameModeForPlayer(GameType gameModeForPlayer) {
++        // Paper start - Expand PlayerGameModeChangeEvent
++        org.bukkit.event.player.PlayerGameModeChangeEvent event = this.changeGameModeForPlayer(gameModeForPlayer, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.UNKNOWN, null);
++        return event != null && event.isCancelled();
++    }
++    @Nullable
++    public org.bukkit.event.player.PlayerGameModeChangeEvent changeGameModeForPlayer(GameType gameModeForPlayer, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause playerGameModeChangeCause, @Nullable net.kyori.adventure.text.Component cancelMessage) {
++        // Paper end - Expand PlayerGameModeChangeEvent
+         if (gameModeForPlayer == this.gameModeForPlayer) {
+-            return false;
++            return null; // Paper - Expand PlayerGameModeChangeEvent
+         } else {
+-            this.setGameModeForPlayer(gameModeForPlayer, this.previousGameModeForPlayer);
++            // CraftBukkit start
++            org.bukkit.event.player.PlayerGameModeChangeEvent event = new org.bukkit.event.player.PlayerGameModeChangeEvent(
++                this.player.getBukkitEntity(),
++                org.bukkit.GameMode.getByValue(gameModeForPlayer.getId()),
++                playerGameModeChangeCause, // Paper
++                cancelMessage
++            );
++            if (!event.callEvent()) {
++                return event; // Paper - Expand PlayerGameModeChangeEvent
++            }
++            // CraftBukkit end
++            this.setGameModeForPlayer(gameModeForPlayer, this.gameModeForPlayer); // Paper - Fix MC-259571
+             this.player.onUpdateAbilities();
+             this.player
+                 .server
+                 .getPlayerList()
+-                .broadcastAll(new ClientboundPlayerInfoUpdatePacket(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_GAME_MODE, this.player));
++                .broadcastAll(new ClientboundPlayerInfoUpdatePacket(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_GAME_MODE, this.player), this.player); // CraftBukkit
+             this.level.updateSleepingPlayerList();
+             if (gameModeForPlayer == GameType.CREATIVE) {
+                 this.player.resetCurrentImpulseContext();
+             }
+ 
+-            return true;
++            return event; // Paper - Expand PlayerGameModeChangeEvent
+         }
+     }
+ 
+@@ -90,10 +_,11 @@
+     }
+ 
+     public void tick() {
+-        this.gameTicks++;
++        // this.gameTicks = net.minecraft.server.MinecraftServer.currentTick; // CraftBukkit
++        this.gameTicks = (int) this.level.getLagCompensationTick(); // Paper - lag compensate eating
+         if (this.hasDelayedDestroy) {
+-            BlockState blockState = this.level.getBlockState(this.delayedDestroyPos);
+-            if (blockState.isAir()) {
++            BlockState blockState = this.level.getBlockStateIfLoaded(this.delayedDestroyPos); // Paper - Don't allow digging into unloaded chunks
++            if (blockState == null || blockState.isAir()) { // Paper - Don't allow digging into unloaded chunks
+                 this.hasDelayedDestroy = false;
+             } else {
+                 float f = this.incrementDestroyProgress(blockState, this.delayedDestroyPos, this.delayedTickStart);
+@@ -103,7 +_,13 @@
+                 }
+             }
+         } else if (this.isDestroyingBlock) {
+-            BlockState blockState = this.level.getBlockState(this.destroyPos);
++            // Paper start - Don't allow digging into unloaded chunks; don't want to do same logic as above, return instead
++            BlockState blockState = this.level.getBlockStateIfLoaded(this.destroyPos);
++            if (blockState == null) {
++                this.isDestroyingBlock = false;
++                return;
++            }
++            // Paper end - Don't allow digging into unloaded chunks
+             if (blockState.isAir()) {
+                 this.level.destroyBlockProgress(this.player.getId(), this.destroyPos, -1);
+                 this.lastSentState = -1;
+@@ -131,6 +_,7 @@
+ 
+     public void handleBlockBreakAction(BlockPos pos, ServerboundPlayerActionPacket.Action action, Direction face, int maxBuildHeight, int sequence) {
+         if (!this.player.canInteractWithBlock(pos, 1.0)) {
++            if (true) return; // Paper - Don't allow digging into unloaded chunks; Don't notify if unreasonably far away
+             this.debugLogging(pos, false, sequence, "too far");
+         } else if (pos.getY() > maxBuildHeight) {
+             this.player.connection.send(new ClientboundBlockUpdatePacket(pos, this.level.getBlockState(pos)));
+@@ -138,16 +_,39 @@
+         } else {
+             if (action == ServerboundPlayerActionPacket.Action.START_DESTROY_BLOCK) {
+                 if (!this.level.mayInteract(this.player, pos)) {
++                    // CraftBukkit start - fire PlayerInteractEvent
++                    org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerInteractEvent(this.player, org.bukkit.event.block.Action.LEFT_CLICK_BLOCK, pos, face, this.player.getInventory().getSelected(), InteractionHand.MAIN_HAND);
+                     this.player.connection.send(new ClientboundBlockUpdatePacket(pos, this.level.getBlockState(pos)));
+                     this.debugLogging(pos, false, sequence, "may not interact");
+-                    return;
+-                }
++                    // Update any tile entity data for this block
++                    this.capturedBlockEntity = true; // Paper - Send block entities after destroy prediction
++                    // CraftBukkit end
++                    return;
++                }
++
++                // CraftBukkit start
++                org.bukkit.event.player.PlayerInteractEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerInteractEvent(this.player, org.bukkit.event.block.Action.LEFT_CLICK_BLOCK, pos, face, this.player.getInventory().getSelected(), InteractionHand.MAIN_HAND);
++                if (event.isCancelled()) {
++                    // Let the client know the block still exists
++                    // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); // Paper - Don't resync blocks
++                    // Update any tile entity data for this block
++                    this.capturedBlockEntity = true; // Paper - Send block entities after destroy prediction
++                    return;
++                }
++                // CraftBukkit end
+ 
+                 if (this.isCreative()) {
+                     this.destroyAndAck(pos, sequence, "creative destroy");
+                     return;
+                 }
+ 
++                // Spigot start - handle debug stick left click for non-creative
++                if (this.player.getMainHandItem().is(net.minecraft.world.item.Items.DEBUG_STICK)
++                    && ((net.minecraft.world.item.DebugStickItem) net.minecraft.world.item.Items.DEBUG_STICK).handleInteraction(this.player, this.level.getBlockState(pos), this.level, pos, false, this.player.getMainHandItem())) {
++                    return;
++                }
++                // Spigot end
++
+                 if (this.player.blockActionRestricted(this.level, pos, this.gameModeForPlayer)) {
+                     this.player.connection.send(new ClientboundBlockUpdatePacket(pos, this.level.getBlockState(pos)));
+                     this.debugLogging(pos, false, sequence, "block action restricted");
+@@ -157,7 +_,7 @@
+                 this.destroyProgressStart = this.gameTicks;
+                 float f = 1.0F;
+                 BlockState blockState = this.level.getBlockState(pos);
+-                if (!blockState.isAir()) {
++                if (event.useInteractedBlock() != org.bukkit.event.Event.Result.DENY && !blockState.isAir()) { // Paper
+                     EnchantmentHelper.onHitBlock(
+                         this.level,
+                         this.player.getMainHandItem(),
+@@ -172,6 +_,23 @@
+                     f = blockState.getDestroyProgress(this.player, this.player.level(), pos);
+                 }
+ 
++                // CraftBukkit start
++                // Note that we don't need to resync blocks, block acks will handle it properly for everything but block entities already
++                if (event.useItemInHand() == org.bukkit.event.Event.Result.DENY) {
++                    return;
++                }
++
++                org.bukkit.event.block.BlockDamageEvent blockEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callBlockDamageEvent(this.player, pos, face, this.player.getInventory().getSelected(), f >= 1.0f); // Paper - Add BlockFace to BlockDamageEvent
++
++                if (blockEvent.isCancelled()) {
++                    return;
++                }
++
++                if (blockEvent.getInstaBreak()) {
++                    f = 2.0f;
++                }
++                // CraftBukkit end
++
+                 if (!blockState.isAir() && f >= 1.0F) {
+                     this.destroyAndAck(pos, sequence, "insta mine");
+                 } else {
+@@ -212,14 +_,22 @@
+                 this.debugLogging(pos, true, sequence, "stopped destroying");
+             } else if (action == ServerboundPlayerActionPacket.Action.ABORT_DESTROY_BLOCK) {
+                 this.isDestroyingBlock = false;
+-                if (!Objects.equals(this.destroyPos, pos)) {
+-                    LOGGER.warn("Mismatch in destroy block pos: {} {}", this.destroyPos, pos);
++                // Paper start - Don't allow digging into unloaded chunks
++                if (!Objects.equals(this.destroyPos, pos) && !BlockPos.ZERO.equals(this.destroyPos)) { // Paper
++                    ServerPlayerGameMode.LOGGER.debug("Mismatch in destroy block pos: {} {}", this.destroyPos, pos); // CraftBukkit - SPIGOT-5457 sent by client when interact event cancelled
++                    BlockState type = this.level.getBlockStateIfLoaded(this.destroyPos); // Don't load unloaded chunks for stale records here
++                    if (type != null) {
+                     this.level.destroyBlockProgress(this.player.getId(), this.destroyPos, -1);
+                     this.debugLogging(pos, true, sequence, "aborted mismatched destroying");
++                    }
++                    this.destroyPos = BlockPos.ZERO;
++                    // Paper end - Don't allow digging into unloaded chunks
+                 }
+ 
+                 this.level.destroyBlockProgress(this.player.getId(), pos, -1);
+                 this.debugLogging(pos, true, sequence, "aborted destroying");
++
++                org.bukkit.craftbukkit.event.CraftEventFactory.callBlockDamageAbortEvent(this.player, pos, this.player.getInventory().getSelected()); // CraftBukkit
+             }
+         }
+     }
+@@ -235,36 +_,108 @@
+ 
+     public boolean destroyBlock(BlockPos pos) {
+         BlockState blockState = this.level.getBlockState(pos);
+-        if (!this.player.getMainHandItem().getItem().canAttackBlock(blockState, this.level, pos, this.player)) {
++        // CraftBukkit start - fire BlockBreakEvent
++        org.bukkit.block.Block bblock = org.bukkit.craftbukkit.block.CraftBlock.at(this.level, pos);
++        org.bukkit.event.block.BlockBreakEvent event = null;
++        if (this.player instanceof ServerPlayer) {
++            // Sword + Creative mode pre-cancel
++            boolean canAttackBlock = !this.player.getMainHandItem().getItem().canAttackBlock(blockState, this.level, pos, this.player);
++            event = new org.bukkit.event.block.BlockBreakEvent(bblock, this.player.getBukkitEntity());
++
++            // Sword + Creative mode pre-cancel
++            event.setCancelled(canAttackBlock);
++
++            // Calculate default block experience
++            BlockState updatedBlockState = this.level.getBlockState(pos);
++            Block block = updatedBlockState.getBlock();
++
++            if (!event.isCancelled() && !this.isCreative() && this.player.hasCorrectToolForDrops(block.defaultBlockState())) {
++                ItemStack itemInHand = this.player.getItemBySlot(EquipmentSlot.MAINHAND);
++                event.setExpToDrop(block.getExpDrop(updatedBlockState, this.level, pos, itemInHand, true));
++            }
++
++            this.level.getCraftServer().getPluginManager().callEvent(event);
++
++            if (event.isCancelled()) {
++                if (canAttackBlock) {
++                    return false;
++                }
++
++                // Block entity data is not reset by the block acks, send after destroy prediction
++                if (!this.captureSentBlockEntities) {
++                    BlockEntity blockEntity = this.level.getBlockEntity(pos);
++                    if (blockEntity != null) {
++                        this.player.connection.send(blockEntity.getUpdatePacket());
++                    }
++                } else {
++                    this.capturedBlockEntity = true;
++                }
++                return false;
++            }
++        }
++        // CraftBukkit end
++
++        if (false && !this.player.getMainHandItem().getItem().canAttackBlock(blockState, this.level, pos, this.player)) { // CraftBukkit - false
+             return false;
+         } else {
++            blockState = this.level.getBlockState(pos); // CraftBukkit - update state from plugins
++            if (blockState.isAir()) return false; // CraftBukkit - A plugin set block to air without cancelling
+             BlockEntity blockEntity = this.level.getBlockEntity(pos);
+             Block block = blockState.getBlock();
+-            if (block instanceof GameMasterBlock && !this.player.canUseGameMasterBlocks()) {
++            if (block instanceof GameMasterBlock && !this.player.canUseGameMasterBlocks() && !(block instanceof net.minecraft.world.level.block.CommandBlock && (this.player.isCreative() && this.player.getBukkitEntity().hasPermission("minecraft.commandblock")))) { // Paper - command block permission
+                 this.level.sendBlockUpdated(pos, blockState, blockState, 3);
+                 return false;
+             } else if (this.player.blockActionRestricted(this.level, pos, this.gameModeForPlayer)) {
+                 return false;
+             } else {
++                // CraftBukkit start
++                org.bukkit.block.BlockState state = bblock.getState();
++                this.level.captureDrops = new java.util.ArrayList<>();
++                // CraftBukkit end
+                 BlockState blockState1 = block.playerWillDestroy(this.level, pos, blockState, this.player);
+                 boolean flag = this.level.removeBlock(pos, false);
+                 if (flag) {
+                     block.destroy(this.level, pos, blockState1);
+                 }
+ 
++                ItemStack mainHandStack = null; // Paper - Trigger bee_nest_destroyed trigger in the correct place
++                boolean isCorrectTool = false; // Paper - Trigger bee_nest_destroyed trigger in the correct place
+                 if (this.isCreative()) {
+-                    return true;
++                    // return true; // CraftBukkit
+                 } else {
+                     ItemStack mainHandItem = this.player.getMainHandItem();
+                     ItemStack itemStack = mainHandItem.copy();
+                     boolean hasCorrectToolForDrops = this.player.hasCorrectToolForDrops(blockState1);
++                    mainHandStack = itemStack; // Paper - Trigger bee_nest_destroyed trigger in the correct place
++                    isCorrectTool = hasCorrectToolForDrops; // Paper - Trigger bee_nest_destroyed trigger in the correct place
+                     mainHandItem.mineBlock(this.level, blockState1, pos, this.player);
+-                    if (flag && hasCorrectToolForDrops) {
+-                        block.playerDestroy(this.level, this.player, pos, blockState1, blockEntity, itemStack);
+-                    }
+-
+-                    return true;
+-                }
++                    if (flag && hasCorrectToolForDrops) { // CraftBukkit - Check if block should drop items // Paper - fix drops not preventing stats/food exhaustion
++                        block.playerDestroy(this.level, this.player, pos, blockState1, blockEntity, itemStack, event.isDropItems(), false); // Paper - fix drops not preventing stats/food exhaustion
++                    }
++
++                    // return true; // CraftBukkit
++                }
++                // CraftBukkit start
++                java.util.List<net.minecraft.world.entity.item.ItemEntity> itemsToDrop = this.level.captureDrops; // Paper - capture all item additions to the world
++                this.level.captureDrops = null; // Paper - capture all item additions to the world; Remove this earlier so that we can actually drop stuff
++                if (event.isDropItems()) {
++                    org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockDropItemEvent(bblock, state, this.player, itemsToDrop); // Paper - capture all item additions to the world
++                }
++
++                // Drop event experience
++                if (flag) {
++                    blockState.getBlock().popExperience(this.level, pos, event.getExpToDrop(), this.player); // Paper
++                }
++                // Paper start - Trigger bee_nest_destroyed trigger in the correct place (check impls of block#playerDestroy)
++                if (mainHandStack != null) {
++                    if (flag && isCorrectTool && event.isDropItems() && block instanceof net.minecraft.world.level.block.BeehiveBlock && blockEntity instanceof net.minecraft.world.level.block.entity.BeehiveBlockEntity beehiveBlockEntity) { // simulates the guard on block#playerDestroy above
++                        CriteriaTriggers.BEE_NEST_DESTROYED.trigger(player, blockState, mainHandStack, beehiveBlockEntity.getOccupantCount());
++                    }
++                }
++                // Paper end - Trigger bee_nest_destroyed trigger in the correct place
++
++                return true;
++                // CraftBukkit end
+             }
+         }
+     }
+@@ -307,15 +_,47 @@
+         }
+     }
+ 
++    // CraftBukkit start - whole method
++    public boolean interactResult = false;
++    public boolean firedInteract = false;
++    public BlockPos interactPosition;
++    public InteractionHand interactHand;
++    public ItemStack interactItemStack;
+     public InteractionResult useItemOn(ServerPlayer player, Level level, ItemStack stack, InteractionHand hand, BlockHitResult hitResult) {
+         BlockPos blockPos = hitResult.getBlockPos();
+         BlockState blockState = level.getBlockState(blockPos);
++        boolean cancelledBlock = false;
++        boolean cancelledItem = false; // Paper - correctly handle items on cooldown
+         if (!blockState.getBlock().isEnabled(level.enabledFeatures())) {
+             return InteractionResult.FAIL;
+         } else if (this.gameModeForPlayer == GameType.SPECTATOR) {
+             MenuProvider menuProvider = blockState.getMenuProvider(level, blockPos);
+-            if (menuProvider != null) {
+-                player.openMenu(menuProvider);
++            cancelledBlock = !(menuProvider instanceof MenuProvider);
++        }
++
++        if (player.getCooldowns().isOnCooldown(stack)) {
++            cancelledItem = true; // Paper - correctly handle items on cooldown
++        }
++        org.bukkit.event.player.PlayerInteractEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerInteractEvent(player, org.bukkit.event.block.Action.RIGHT_CLICK_BLOCK, blockPos, hitResult.getDirection(), stack, cancelledBlock, cancelledItem, hand, hitResult.getLocation()); // Paper - correctly handle items on cooldown
++        this.firedInteract = true;
++        this.interactResult = event.useItemInHand() == org.bukkit.event.Event.Result.DENY;
++        this.interactPosition = blockPos.immutable();
++        this.interactHand = hand;
++        this.interactItemStack = stack.copy();
++
++        if (event.useInteractedBlock() == org.bukkit.event.Event.Result.DENY) {
++            // Block acks will take care of most of it, just handle some special cases here
++            if (blockState.getBlock() instanceof net.minecraft.world.level.block.CakeBlock) {
++                player.getBukkitEntity().sendHealthUpdate(); // SPIGOT-1341 - reset health for cake
++            } else if (blockState.is(net.minecraft.world.level.block.Blocks.JIGSAW) || blockState.is(net.minecraft.world.level.block.Blocks.STRUCTURE_BLOCK) || blockState.getBlock() instanceof net.minecraft.world.level.block.CommandBlock) {
++                player.connection.send(new net.minecraft.network.protocol.game.ClientboundContainerClosePacket(this.player.containerMenu.containerId));
++            }
++            player.getBukkitEntity().updateInventory(); // SPIGOT-2867
++            this.player.resyncUsingItem(this.player); // Paper - Properly cancel usable items
++            return (event.useItemInHand() != org.bukkit.event.Event.Result.ALLOW) ? InteractionResult.SUCCESS : InteractionResult.PASS;
++        } else if (this.gameModeForPlayer == GameType.SPECTATOR) {
++            MenuProvider menuProvider = blockState.getMenuProvider(level, blockPos);
++            if (menuProvider != null && player.openMenu(menuProvider).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
+                 return InteractionResult.CONSUME;
+             } else {
+                 return InteractionResult.PASS;
+@@ -340,7 +_,7 @@
+                 }
+             }
+ 
+-            if (!stack.isEmpty() && !player.getCooldowns().isOnCooldown(stack)) {
++            if (!stack.isEmpty() && !this.interactResult) { // add !interactResult SPIGOT-764
+                 UseOnContext useOnContext = new UseOnContext(player, hand, hitResult);
+                 InteractionResult interactionResult1;
+                 if (this.isCreative()) {
+@@ -357,6 +_,11 @@
+ 
+                 return interactionResult1;
+             } 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/paper-server/patches/unapplied/net/minecraft/server/level/TicketType.java.patch b/paper-server/patches/sources/net/minecraft/server/level/TicketType.java.patch
similarity index 63%
rename from paper-server/patches/unapplied/net/minecraft/server/level/TicketType.java.patch
rename to paper-server/patches/sources/net/minecraft/server/level/TicketType.java.patch
index ca9484bbeb..12796a0ffe 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/level/TicketType.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/level/TicketType.java.patch
@@ -1,20 +1,20 @@
 --- a/net/minecraft/server/level/TicketType.java
 +++ b/net/minecraft/server/level/TicketType.java
-@@ -7,6 +7,7 @@
+@@ -7,6 +_,7 @@
  import net.minecraft.world.level.ChunkPos;
  
  public class TicketType<T> {
 +    public static final TicketType<Long> FUTURE_AWAIT = create("future_await", Long::compareTo); // Paper
- 
      private final String name;
      private final Comparator<T> comparator;
-@@ -22,6 +23,9 @@
-     public static final TicketType<BlockPos> PORTAL = TicketType.create("portal", Vec3i::compareTo, 300);
-     public static final TicketType<ChunkPos> ENDER_PEARL = TicketType.create("ender_pearl", Comparator.comparingLong(ChunkPos::toLong), 40);
-     public static final TicketType<ChunkPos> UNKNOWN = TicketType.create("unknown", Comparator.comparingLong(ChunkPos::toLong), 1);
+     public long timeout;
+@@ -17,6 +_,9 @@
+     public static final TicketType<BlockPos> PORTAL = create("portal", Vec3i::compareTo, 300);
+     public static final TicketType<ChunkPos> ENDER_PEARL = create("ender_pearl", Comparator.comparingLong(ChunkPos::toLong), 40);
+     public static final TicketType<ChunkPos> UNKNOWN = create("unknown", Comparator.comparingLong(ChunkPos::toLong), 1);
 +    public static final TicketType<Unit> PLUGIN = TicketType.create("plugin", (a, b) -> 0); // CraftBukkit
 +    public static final TicketType<org.bukkit.plugin.Plugin> PLUGIN_TICKET = TicketType.create("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // CraftBukkit
 +    public static final TicketType<Integer> POST_TELEPORT = TicketType.create("post_teleport", Integer::compare, 5); // Paper - post teleport ticket type
  
-     public static <T> TicketType<T> create(String name, Comparator<T> argumentComparator) {
-         return new TicketType<>(name, argumentComparator, 0L);
+     public static <T> TicketType<T> create(String name, Comparator<T> comparator) {
+         return new TicketType<>(name, comparator, 0L);
diff --git a/paper-server/patches/unapplied/net/minecraft/server/level/WorldGenRegion.java.patch b/paper-server/patches/sources/net/minecraft/server/level/WorldGenRegion.java.patch
similarity index 58%
rename from paper-server/patches/unapplied/net/minecraft/server/level/WorldGenRegion.java.patch
rename to paper-server/patches/sources/net/minecraft/server/level/WorldGenRegion.java.patch
index 415887c78f..6daf126e10 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/level/WorldGenRegion.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/level/WorldGenRegion.java.patch
@@ -1,11 +1,9 @@
 --- a/net/minecraft/server/level/WorldGenRegion.java
 +++ b/net/minecraft/server/level/WorldGenRegion.java
-@@ -167,7 +167,27 @@
-         int k = this.center.getPos().getChessboardDistance(chunkX, chunkZ);
+@@ -151,6 +_,26 @@
+         return chessboardDistance < this.generatingStep.directDependencies().size();
+     }
  
-         return k < this.generatingStep.directDependencies().size();
-+    }
-+
 +    // Paper start - if loaded util
 +    @Nullable
 +    @Override
@@ -23,36 +21,43 @@
 +    public final FluidState getFluidIfLoaded(BlockPos blockposition) {
 +        ChunkAccess chunk = this.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4);
 +        return chunk == null ? null : chunk.getFluidState(blockposition);
-     }
++    }
 +    // Paper end
- 
++
      @Override
      public BlockState getBlockState(BlockPos pos) {
-@@ -217,7 +237,8 @@
-         if (iblockdata.isAir()) {
+         return this.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ())).getBlockState(pos);
+@@ -198,7 +_,8 @@
+         if (blockState.isAir()) {
              return false;
          } else {
--            if (drop) {
-+            if (drop) LOGGER.warn("Potential async entity add during worldgen", new Throwable()); // Paper - Fix async entity add due to fungus trees; log when this happens
+-            if (dropBlock) {
++            if (dropBlock) LOGGER.warn("Potential async entity add during worldgen", new Throwable()); // Paper - Fix async entity add due to fungus trees; log when this happens
 +            if (false) { // CraftBukkit - SPIGOT-6833: Do not drop during world generation
-                 BlockEntity tileentity = iblockdata.hasBlockEntity() ? this.getBlockEntity(pos) : null;
- 
-                 Block.dropResources(iblockdata, this.level, pos, tileentity, breakingEntity, ItemStack.EMPTY);
-@@ -264,6 +285,7 @@
+                 BlockEntity blockEntity = blockState.hasBlockEntity() ? this.getBlockEntity(pos) : null;
+                 Block.dropResources(blockState, this.level, pos, blockEntity, entity, ItemStack.EMPTY);
+             }
+@@ -242,6 +_,7 @@
          }
      }
  
 +    private boolean hasSetFarWarned = false; // Paper - Buffer OOB setBlock calls
      @Override
      public boolean ensureCanWrite(BlockPos pos) {
-         int i = SectionPos.blockToSectionCoord(pos.getX());
-@@ -283,7 +305,15 @@
+         int sectionPosX = SectionPos.blockToSectionCoord(pos.getX());
+@@ -259,6 +_,8 @@
  
              return true;
          } else {
 +            // Paper start - Buffer OOB setBlock calls
 +            if (!hasSetFarWarned) {
-             Util.logAndPauseIfInIde("Detected setBlock in a far chunk [" + i + ", " + j + "], pos: " + String.valueOf(pos) + ", status: " + String.valueOf(this.generatingStep.targetStatus()) + (this.currentlyGenerating == null ? "" : ", currently generating: " + (String) this.currentlyGenerating.get()));
+             Util.logAndPauseIfInIde(
+                 "Detected setBlock in a far chunk ["
+                     + sectionPosX
+@@ -270,6 +_,12 @@
+                     + this.generatingStep.targetStatus()
+                     + (this.currentlyGenerating == null ? "" : ", currently generating: " + this.currentlyGenerating.get())
+             );
 +                hasSetFarWarned = true;
 +                if (this.getServer() != null && this.getServer().isDebugging()) {
 +                    io.papermc.paper.util.TraceUtil.dumpTraceForThread("far setBlock call");
@@ -62,17 +67,17 @@
              return false;
          }
      }
-@@ -294,7 +324,7 @@
+@@ -280,7 +_,7 @@
              return false;
          } else {
-             ChunkAccess ichunkaccess = this.getChunk(pos);
--            BlockState iblockdata1 = ichunkaccess.setBlockState(pos, state, false);
-+            BlockState iblockdata1 = ichunkaccess.setBlockState(pos, state, false); final BlockState previousBlockState = iblockdata1; // Paper - Clear block entity before setting up a DUMMY block entity - obfhelper
- 
-             if (iblockdata1 != null) {
-                 this.level.onBlockStateChange(pos, iblockdata1, state);
-@@ -310,6 +340,17 @@
-                         ichunkaccess.removeBlockEntity(pos);
+             ChunkAccess chunk = this.getChunk(pos);
+-            BlockState blockState = chunk.setBlockState(pos, state, false);
++            BlockState blockState = chunk.setBlockState(pos, state, false); final BlockState previousBlockState = blockState; // Paper - Clear block entity before setting up a DUMMY block entity - obfhelper
+             if (blockState != null) {
+                 this.level.onBlockStateChange(pos, blockState, state);
+             }
+@@ -294,6 +_,17 @@
+                         chunk.removeBlockEntity(pos);
                      }
                  } else {
 +                    // Paper start - Clear block entity before setting up a DUMMY block entity
@@ -83,13 +88,13 @@
 +                    // be waterlogged would remove its existing block entity (see PaperMC/Paper#10750)
 +                    // This logic is *also* found in LevelChunk#setBlockState.
 +                    if (previousBlockState != null && !java.util.Objects.equals(previousBlockState.getBlock(), state.getBlock())) {
-+                        ichunkaccess.removeBlockEntity(pos);
++                        chunk.removeBlockEntity(pos);
 +                    }
 +                    // Paper end - Clear block entity before setting up a DUMMY block entity
-                     CompoundTag nbttagcompound = new CompoundTag();
- 
-                     nbttagcompound.putInt("x", pos.getX());
-@@ -336,6 +377,13 @@
+                     CompoundTag compoundTag = new CompoundTag();
+                     compoundTag.putInt("x", pos.getX());
+                     compoundTag.putInt("y", pos.getY());
+@@ -319,6 +_,13 @@
  
      @Override
      public boolean addFreshEntity(Entity entity) {
@@ -100,6 +105,6 @@
 +    @Override
 +    public boolean addFreshEntity(Entity entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) {
 +        // CraftBukkit end
-         int i = SectionPos.blockToSectionCoord(entity.getBlockX());
-         int j = SectionPos.blockToSectionCoord(entity.getBlockZ());
- 
+         int sectionPosX = SectionPos.blockToSectionCoord(entity.getBlockX());
+         int sectionPosZ = SectionPos.blockToSectionCoord(entity.getBlockZ());
+         this.getChunk(sectionPosX, sectionPosZ).addEntity(entity);
diff --git a/paper-server/patches/sources/net/minecraft/server/network/LegacyQueryHandler.java.patch b/paper-server/patches/sources/net/minecraft/server/network/LegacyQueryHandler.java.patch
new file mode 100644
index 0000000000..9bda2a7540
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/network/LegacyQueryHandler.java.patch
@@ -0,0 +1,195 @@
+--- a/net/minecraft/server/network/LegacyQueryHandler.java
++++ b/net/minecraft/server/network/LegacyQueryHandler.java
+@@ -14,6 +_,7 @@
+ public class LegacyQueryHandler extends ChannelInboundHandlerAdapter {
+     private static final Logger LOGGER = LogUtils.getLogger();
+     private final ServerInfo server;
++    private ByteBuf buf; // Paper
+ 
+     public LegacyQueryHandler(ServerInfo server) {
+         this.server = server;
+@@ -22,6 +_,17 @@
+     @Override
+     public void channelRead(ChannelHandlerContext context, Object message) {
+         ByteBuf byteBuf = (ByteBuf)message;
++        // Paper start - Make legacy ping handler more reliable
++        if (this.buf != null) {
++            try {
++                readLegacy1_6(context, byteBuf);
++            } finally {
++                byteBuf.release();
++            }
++            return;
++        }
++        // Paper end - Make legacy ping handler more reliable
++
+         byteBuf.markReaderIndex();
+         boolean flag = true;
+ 
+@@ -33,9 +_,19 @@
+ 
+                 SocketAddress socketAddress = context.channel().remoteAddress();
+                 int i = byteBuf.readableBytes();
++                String string = null; // Paper
+                 if (i == 0) {
+-                    LOGGER.debug("Ping: (<1.3.x) from {}", socketAddress);
+-                    String string = createVersion0Response(this.server);
++                    LOGGER.debug("Ping: (<1.3.x) from {}", net.minecraft.server.MinecraftServer.getServer().logIPs() ? socketAddress : "<ip address withheld>"); // Paper - Respect logIPs option
++                    // Paper start - Call PaperServerListPingEvent and use results
++                    com.destroystokyo.paper.event.server.PaperServerListPingEvent event = com.destroystokyo.paper.network.PaperLegacyStatusClient.processRequest(net.minecraft.server.MinecraftServer.getServer(), (java.net.InetSocketAddress) socketAddress, 39, null);
++                    if (event == null) {
++                        context.close();
++                        byteBuf.release();
++                        flag = false;
++                        return;
++                    }
++                    string = String.format(Locale.ROOT, "%s§%d§%d", com.destroystokyo.paper.network.PaperLegacyStatusClient.getUnformattedMotd(event), event.getNumPlayers(), event.getMaxPlayers());
++                    // Paper end - Call PaperServerListPingEvent and use results
+                     sendFlushAndClose(context, createLegacyDisconnectPacket(context.alloc(), string));
+                 } else {
+                     if (byteBuf.readUnsignedByte() != 1) {
+@@ -43,16 +_,39 @@
+                     }
+ 
+                     if (byteBuf.isReadable()) {
+-                        if (!readCustomPayloadPacket(byteBuf)) {
++                        // Paper start - Replace below
++                        if (byteBuf.readUnsignedByte() != LegacyProtocolUtils.CUSTOM_PAYLOAD_PACKET_ID) {
++                            string = this.readLegacy1_6(context, byteBuf);
++                            if (string == null) {
++                                return;
++                            }
++                        }
++                        // Paper end - Replace below
++                    } else {
++                        LOGGER.debug("Ping: (1.4-1.5.x) from {}", net.minecraft.server.MinecraftServer.getServer().logIPs() ? socketAddress : "<ip address withheld>"); // Paper - Respect logIPs option
++                    }
++
++                    // Paper start - Call PaperServerListPingEvent and use results
++                    if (string == null) {
++                        com.destroystokyo.paper.event.server.PaperServerListPingEvent event = com.destroystokyo.paper.network.PaperLegacyStatusClient.processRequest(net.minecraft.server.MinecraftServer.getServer(), (java.net.InetSocketAddress) socketAddress, 127, null); // Paper
++                        if (event == null) {
++                            context.close();
++                            byteBuf.release();
++                            flag = false;
+                             return;
+                         }
+ 
+-                        LOGGER.debug("Ping: (1.6) from {}", socketAddress);
+-                    } else {
+-                        LOGGER.debug("Ping: (1.4-1.5.x) from {}", socketAddress);
++                        // See createVersion1Response
++                        string = String.format(
++                            Locale.ROOT,
++                            "§1\u0000%d\u0000%s\u0000%s\u0000%d\u0000%d",
++                            event.getProtocolVersion(), this.server.getServerVersion(),
++                            event.getMotd(),
++                            event.getNumPlayers(),
++                            event.getMaxPlayers()
++                        );
++                        // Paper end - Call PaperServerListPingEvent and use results
+                     }
+-
+-                    String string = createVersion1Response(this.server);
+                     sendFlushAndClose(context, createLegacyDisconnectPacket(context.alloc(), string));
+                 }
+ 
+@@ -110,6 +_,98 @@
+             server.getMaxPlayers()
+         );
+     }
++
++    // Paper start
++    private static String readLegacyString(ByteBuf buf) {
++        int size = buf.readShort() * Character.BYTES;
++        if (!buf.isReadable(size)) {
++            return null;
++        }
++
++        String result = buf.toString(buf.readerIndex(), size, java.nio.charset.StandardCharsets.UTF_16BE);
++        buf.skipBytes(size); // toString doesn't increase readerIndex automatically
++        return result;
++    }
++
++    private String readLegacy1_6(ChannelHandlerContext ctx, ByteBuf part) {
++        ByteBuf buf = this.buf;
++
++        if (buf == null) {
++            this.buf = buf = ctx.alloc().buffer();
++            buf.markReaderIndex();
++        } else {
++            buf.resetReaderIndex();
++        }
++
++        buf.writeBytes(part);
++
++        if (!buf.isReadable(Short.BYTES + Short.BYTES + Byte.BYTES + Short.BYTES + Integer.BYTES)) {
++            return null;
++        }
++
++        String string = readLegacyString(buf);
++        if (string == null) {
++            return null;
++        }
++
++        if (!string.equals(LegacyProtocolUtils.CUSTOM_PAYLOAD_PACKET_PING_CHANNEL)) {
++            removeHandler(ctx);
++            return null;
++        }
++
++        if (!buf.isReadable(Short.BYTES) || !buf.isReadable(buf.readShort())) {
++            return null;
++        }
++
++        int protocolVersion = buf.readByte();
++        String host = readLegacyString(buf);
++        if (host == null) {
++            removeHandler(ctx);
++            return null;
++        }
++
++        int port = buf.readInt();
++        if (buf.isReadable()) {
++            removeHandler(ctx);
++            return null;
++        }
++
++        buf.release();
++        this.buf = null;
++
++        LOGGER.debug("Ping: (1.6) from {}", net.minecraft.server.MinecraftServer.getServer().logIPs() ? ctx.channel().remoteAddress() : "<ip address withheld>"); // Paper - Respect logIPs option
++
++        net.minecraft.server.MinecraftServer server = net.minecraft.server.MinecraftServer.getServer();
++        java.net.InetSocketAddress virtualHost = com.destroystokyo.paper.network.PaperNetworkClient.prepareVirtualHost(host, port);
++        com.destroystokyo.paper.event.server.PaperServerListPingEvent event = com.destroystokyo.paper.network.PaperLegacyStatusClient.processRequest(
++            server, (java.net.InetSocketAddress) ctx.channel().remoteAddress(), protocolVersion, virtualHost);
++        if (event == null) {
++            ctx.close();
++            return null;
++        }
++
++        String response = String.format("\u00a71\u0000%d\u0000%s\u0000%s\u0000%d\u0000%d", event.getProtocolVersion(), event.getVersion(),
++            com.destroystokyo.paper.network.PaperLegacyStatusClient.getMotd(event), event.getNumPlayers(), event.getMaxPlayers());
++        return response;
++    }
++
++    private void removeHandler(ChannelHandlerContext ctx) {
++        ByteBuf buf = this.buf;
++        this.buf = null;
++
++        buf.resetReaderIndex();
++        ctx.pipeline().remove(this);
++        ctx.fireChannelRead(buf);
++    }
++
++    @Override
++    public void handlerRemoved(ChannelHandlerContext ctx) {
++        if (this.buf != null) {
++            this.buf.release();
++            this.buf = null;
++        }
++    }
++    // Paper end
+ 
+     private static void sendFlushAndClose(ChannelHandlerContext context, ByteBuf buffer) {
+         context.pipeline().firstContext().writeAndFlush(buffer).addListener(ChannelFutureListener.CLOSE);
diff --git a/paper-server/patches/unapplied/net/minecraft/server/network/PlayerChunkSender.java.patch b/paper-server/patches/sources/net/minecraft/server/network/PlayerChunkSender.java.patch
similarity index 55%
rename from paper-server/patches/unapplied/net/minecraft/server/network/PlayerChunkSender.java.patch
rename to paper-server/patches/sources/net/minecraft/server/network/PlayerChunkSender.java.patch
index e1ac133048..9c8b0b62af 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/network/PlayerChunkSender.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/network/PlayerChunkSender.java.patch
@@ -1,26 +1,26 @@
 --- a/net/minecraft/server/network/PlayerChunkSender.java
 +++ b/net/minecraft/server/network/PlayerChunkSender.java
-@@ -44,6 +44,11 @@
-     public void dropChunk(ServerPlayer player, ChunkPos pos) {
-         if (!this.pendingChunks.remove(pos.toLong()) && player.isAlive()) {
-             player.connection.send(new ClientboundForgetLevelChunkPacket(pos));
+@@ -44,6 +_,11 @@
+     public void dropChunk(ServerPlayer player, ChunkPos chunkPos) {
+         if (!this.pendingChunks.remove(chunkPos.toLong()) && player.isAlive()) {
+             player.connection.send(new ClientboundForgetLevelChunkPacket(chunkPos));
 +            // Paper start - PlayerChunkUnloadEvent
 +            if (io.papermc.paper.event.packet.PlayerChunkUnloadEvent.getHandlerList().getRegisteredListeners().length > 0) {
-+                new io.papermc.paper.event.packet.PlayerChunkUnloadEvent(player.getBukkitEntity().getWorld().getChunkAt(pos.longKey), player.getBukkitEntity()).callEvent();
++                new io.papermc.paper.event.packet.PlayerChunkUnloadEvent(player.getBukkitEntity().getWorld().getChunkAt(chunkPos.longKey), player.getBukkitEntity()).callEvent();
 +            }
 +            // Paper end - PlayerChunkUnloadEvent
          }
      }
  
-@@ -75,6 +80,11 @@
+@@ -75,6 +_,11 @@
  
-     private static void sendChunk(ServerGamePacketListenerImpl handler, ServerLevel world, LevelChunk chunk) {
-         handler.send(new ClientboundLevelChunkWithLightPacket(chunk, world.getLightEngine(), null, null));
+     private static void sendChunk(ServerGamePacketListenerImpl packetListener, ServerLevel level, LevelChunk chunk) {
+         packetListener.send(new ClientboundLevelChunkWithLightPacket(chunk, level.getLightEngine(), null, null));
 +        // Paper start - PlayerChunkLoadEvent
 +        if (io.papermc.paper.event.packet.PlayerChunkLoadEvent.getHandlerList().getRegisteredListeners().length > 0) {
-+            new io.papermc.paper.event.packet.PlayerChunkLoadEvent(new org.bukkit.craftbukkit.CraftChunk(chunk), handler.getPlayer().getBukkitEntity()).callEvent();
++            new io.papermc.paper.event.packet.PlayerChunkLoadEvent(new org.bukkit.craftbukkit.CraftChunk(chunk), packetListener.getPlayer().getBukkitEntity()).callEvent();
 +        }
 +        // Paper end - PlayerChunkLoadEvent
-         ChunkPos chunkPos = chunk.getPos();
-         DebugPackets.sendPoiPacketsForChunk(world, chunkPos);
+         ChunkPos pos = chunk.getPos();
+         DebugPackets.sendPoiPacketsForChunk(level, pos);
      }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/network/ServerCommonPacketListenerImpl.java.patch b/paper-server/patches/sources/net/minecraft/server/network/ServerCommonPacketListenerImpl.java.patch
similarity index 54%
rename from paper-server/patches/unapplied/net/minecraft/server/network/ServerCommonPacketListenerImpl.java.patch
rename to paper-server/patches/sources/net/minecraft/server/network/ServerCommonPacketListenerImpl.java.patch
index 4ba7794d53..e21dc4c54b 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/network/ServerCommonPacketListenerImpl.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/network/ServerCommonPacketListenerImpl.java.patch
@@ -1,67 +1,11 @@
 --- a/net/minecraft/server/network/ServerCommonPacketListenerImpl.java
 +++ b/net/minecraft/server/network/ServerCommonPacketListenerImpl.java
-@@ -4,11 +4,13 @@
- import com.mojang.logging.LogUtils;
- import java.util.Objects;
- import javax.annotation.Nullable;
-+import net.minecraft.ChatFormatting;
- import net.minecraft.CrashReport;
- import net.minecraft.CrashReportCategory;
- import net.minecraft.ReportedException;
- import net.minecraft.Util;
- import net.minecraft.network.Connection;
-+import net.minecraft.network.ConnectionProtocol;
- import net.minecraft.network.DisconnectionDetails;
- import net.minecraft.network.PacketSendListener;
- import net.minecraft.network.chat.Component;
-@@ -22,39 +24,88 @@
- import net.minecraft.network.protocol.common.ServerboundPongPacket;
- import net.minecraft.network.protocol.common.ServerboundResourcePackPacket;
- import net.minecraft.network.protocol.cookie.ServerboundCookieResponsePacket;
-+import net.minecraft.network.protocol.game.ClientboundSetDefaultSpawnPositionPacket;
-+import net.minecraft.resources.ResourceLocation;
- import net.minecraft.server.MinecraftServer;
- import net.minecraft.server.level.ClientInformation;
-+import net.minecraft.server.level.ServerPlayer;
- import net.minecraft.util.VisibleForDebug;
+@@ -27,30 +_,67 @@
  import net.minecraft.util.profiling.Profiler;
- import net.minecraft.util.thread.BlockableEventLoop;
  import org.slf4j.Logger;
  
 -public abstract class ServerCommonPacketListenerImpl implements ServerCommonPacketListener {
-+// CraftBukkit start
-+import io.netty.buffer.ByteBuf;
-+import java.util.concurrent.ExecutionException;
-+import net.minecraft.network.protocol.common.custom.DiscardedPayload;
-+import org.bukkit.craftbukkit.entity.CraftPlayer;
-+import org.bukkit.craftbukkit.util.CraftChatMessage;
-+import org.bukkit.craftbukkit.util.CraftLocation;
-+import org.bukkit.craftbukkit.util.Waitable;
-+import org.bukkit.event.player.PlayerKickEvent;
-+import org.bukkit.event.player.PlayerResourcePackStatusEvent;
- 
-+public abstract class ServerCommonPacketListenerImpl implements ServerCommonPacketListener, CraftPlayer.TransferCookieConnection {
-+
-+    @Override
-+    public boolean isTransferred() {
-+        return this.transferred;
-+    }
-+
-+    @Override
-+    public ConnectionProtocol getProtocol() {
-+        return this.protocol();
-+    }
-+
-+    @Override
-+    public void sendPacket(Packet<?> packet) {
-+        this.send(packet);
-+    }
-+
-+    @Override
-+    public void kickPlayer(Component reason, org.bukkit.event.player.PlayerKickEvent.Cause cause) { // Paper - kick event causes
-+        this.disconnect(reason, cause); // Paper - kick event causes
-+    }
-+    // CraftBukkit end
++public abstract class ServerCommonPacketListenerImpl implements ServerCommonPacketListener, org.bukkit.craftbukkit.entity.CraftPlayer.TransferCookieConnection { // CraftBukkit
      private static final Logger LOGGER = LogUtils.getLogger();
      public static final int LATENCY_CHECK_INTERVAL = 15000;
      private static final int CLOSED_LISTENER_TIMEOUT = 15000;
@@ -79,89 +23,101 @@
      private boolean closed = false;
      private int latency;
      private volatile boolean suspendFlushingOnServerThread = false;
++    // CraftBukkit start
++    protected final net.minecraft.server.level.ServerPlayer player;
++    protected final org.bukkit.craftbukkit.CraftServer cserver;
++    public boolean processedDisconnect;
++    // CraftBukkit end
 +    public final java.util.Map<java.util.UUID, net.kyori.adventure.resource.ResourcePackCallback> packCallbacks = new java.util.concurrent.ConcurrentHashMap<>(); // Paper - adventure resource pack callbacks
 +    private static final long KEEPALIVE_LIMIT = Long.getLong("paper.playerconnection.keepalive", 30) * 1000; // Paper - provide property to set keepalive limit
-+    protected static final ResourceLocation MINECRAFT_BRAND = ResourceLocation.withDefaultNamespace("brand"); // Paper - Brand support
++    protected static final net.minecraft.resources.ResourceLocation MINECRAFT_BRAND = net.minecraft.resources.ResourceLocation.withDefaultNamespace("brand"); // Paper - Brand support
  
--    public ServerCommonPacketListenerImpl(MinecraftServer server, Connection connection, CommonListenerCookie clientData) {
--        this.server = server;
--        this.connection = connection;
-+    public ServerCommonPacketListenerImpl(MinecraftServer minecraftserver, Connection networkmanager, CommonListenerCookie commonlistenercookie, ServerPlayer player) { // CraftBukkit
-+        this.server = minecraftserver;
-+        this.connection = networkmanager;
+-    public ServerCommonPacketListenerImpl(MinecraftServer server, Connection connection, CommonListenerCookie cookie) {
++    public ServerCommonPacketListenerImpl(MinecraftServer server, Connection connection, CommonListenerCookie cookie, net.minecraft.server.level.ServerPlayer player) { // CraftBukkit
+         this.server = server;
+         this.connection = connection;
          this.keepAliveTime = Util.getMillis();
--        this.latency = clientData.latency();
--        this.transferred = clientData.transferred();
-+        this.latency = commonlistenercookie.latency();
-+        this.transferred = commonlistenercookie.transferred();
+         this.latency = cookie.latency();
+         this.transferred = cookie.transferred();
+-    }
 +        // CraftBukkit start - add fields and methods
 +        this.player = player;
 +        this.player.transferCookieConnection = this;
-+        this.cserver = minecraftserver.server;
-     }
-+    protected final ServerPlayer player;
-+    protected final org.bukkit.craftbukkit.CraftServer cserver;
-+    public boolean processedDisconnect;
- 
-+    public CraftPlayer getCraftPlayer() {
-+        return (this.player == null) ? null : (CraftPlayer) this.player.getBukkitEntity();
-+        // CraftBukkit end
++        this.cserver = server.server;
 +    }
 +
++    public org.bukkit.craftbukkit.entity.CraftPlayer getCraftPlayer() {
++        return this.player == null ? null : this.player.getBukkitEntity();
++    }
++
++    @Override
++    public boolean isTransferred() {
++        return this.transferred;
++    }
++
++    @Override
++    public net.minecraft.network.ConnectionProtocol getProtocol() {
++        return this.protocol();
++    }
++
++    @Override
++    public void sendPacket(Packet<?> packet) {
++        this.send(packet);
++    }
++
++    @Override
++    public void kickPlayer(Component reason, org.bukkit.event.player.PlayerKickEvent.Cause cause) { // Paper - kick event causes
++        this.disconnect(reason, cause); // Paper - kick event causes
++    }
++    // CraftBukkit end
+ 
      private void close() {
          if (!this.closed) {
-             this.closedListenerTime = Util.getMillis();
-@@ -65,6 +116,11 @@
+@@ -61,6 +_,12 @@
  
      @Override
-     public void onDisconnect(DisconnectionDetails info) {
+     public void onDisconnect(DisconnectionDetails details) {
 +        // Paper start - Fix kick event leave message not being sent
-+        this.onDisconnect(info, null);
++        this.onDisconnect(details, null);
 +    }
++
 +    public void onDisconnect(DisconnectionDetails info, @Nullable net.kyori.adventure.text.Component quitMessage) {
 +        // Paper end - Fix kick event leave message not being sent
          if (this.isSingleplayerOwner()) {
-             ServerCommonPacketListenerImpl.LOGGER.info("Stopping singleplayer server as player logged out");
+             LOGGER.info("Stopping singleplayer server as player logged out");
              this.server.halt(false);
-@@ -80,13 +136,14 @@
- 
-     @Override
-     public void handleKeepAlive(ServerboundKeepAlivePacket packet) {
-+        //PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel()); // CraftBukkit // Paper - handle ServerboundKeepAlivePacket async
-         if (this.keepAlivePending && packet.getId() == this.keepAliveChallenge) {
-             int i = (int) (Util.getMillis() - this.keepAliveTime);
- 
+@@ -80,7 +_,7 @@
              this.latency = (this.latency * 3 + i) / 4;
              this.keepAlivePending = false;
          } else if (!this.isSingleplayerOwner()) {
--            this.disconnect(ServerCommonPacketListenerImpl.TIMEOUT_DISCONNECTION_MESSAGE);
-+            this.disconnectAsync(ServerCommonPacketListenerImpl.TIMEOUT_DISCONNECTION_MESSAGE, PlayerKickEvent.Cause.TIMEOUT); // Paper - add proper async disconnect
+-            this.disconnect(TIMEOUT_DISCONNECTION_MESSAGE);
++            this.disconnectAsync(TIMEOUT_DISCONNECTION_MESSAGE, org.bukkit.event.player.PlayerKickEvent.Cause.TIMEOUT); // Paper - add proper async disconnect
          }
- 
      }
-@@ -94,38 +151,127 @@
-     @Override
-     public void handlePong(ServerboundPongPacket packet) {}
  
-+    // CraftBukkit start
-+    private static final ResourceLocation CUSTOM_REGISTER = ResourceLocation.withDefaultNamespace("register");
-+    private static final ResourceLocation CUSTOM_UNREGISTER = ResourceLocation.withDefaultNamespace("unregister");
+@@ -88,30 +_,117 @@
+     public void handlePong(ServerboundPongPacket packet) {
+     }
+ 
++    private static final net.minecraft.resources.ResourceLocation CUSTOM_REGISTER = net.minecraft.resources.ResourceLocation.withDefaultNamespace("register"); // CraftBukkit
++    private static final net.minecraft.resources.ResourceLocation CUSTOM_UNREGISTER = net.minecraft.resources.ResourceLocation.withDefaultNamespace("unregister"); // CraftBukkit
 +
      @Override
--    public void handleCustomPayload(ServerboundCustomPayloadPacket packet) {}
-+    public void handleCustomPayload(ServerboundCustomPayloadPacket packet) {
+     public void handleCustomPayload(ServerboundCustomPayloadPacket packet) {
+-    }
++        // CraftBukkit start
 +        // Paper start - Brand support
-+        if (packet.payload() instanceof net.minecraft.network.protocol.common.custom.BrandPayload brandPayload) {
-+            this.player.clientBrandName = brandPayload.brand();
++        if (packet.payload() instanceof net.minecraft.network.protocol.common.custom.BrandPayload(String brand)) {
++            this.player.clientBrandName = brand;
 +        }
 +        // Paper end - Brand support
-+        if (!(packet.payload() instanceof DiscardedPayload)) {
++        if (!(packet.payload() instanceof final net.minecraft.network.protocol.common.custom.DiscardedPayload discardedPayload)) {
 +            return;
 +        }
 +        PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
-+        ResourceLocation identifier = packet.payload().type().id();
-+        ByteBuf payload = ((DiscardedPayload)packet.payload()).data();
- 
++        net.minecraft.resources.ResourceLocation identifier = packet.payload().type().id();
++        io.netty.buffer.ByteBuf payload = discardedPayload.data();
++
 +        if (identifier.equals(ServerCommonPacketListenerImpl.CUSTOM_REGISTER)) {
 +            try {
 +                String channels = payload.toString(com.google.common.base.Charsets.UTF_8);
@@ -169,8 +125,8 @@
 +                    this.getCraftPlayer().addChannel(channel);
 +                }
 +            } catch (Exception ex) {
-+                ServerGamePacketListenerImpl.LOGGER.error("Couldn\'t register custom payload", ex);
-+                this.disconnect(Component.literal("Invalid payload REGISTER!"), PlayerKickEvent.Cause.INVALID_PAYLOAD); // Paper - kick event cause
++                ServerGamePacketListenerImpl.LOGGER.error("Couldn't register custom payload", ex);
++                this.disconnect(Component.literal("Invalid payload REGISTER!"), org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PAYLOAD); // Paper - kick event cause
 +            }
 +        } else if (identifier.equals(ServerCommonPacketListenerImpl.CUSTOM_UNREGISTER)) {
 +            try {
@@ -179,8 +135,8 @@
 +                    this.getCraftPlayer().removeChannel(channel);
 +                }
 +            } catch (Exception ex) {
-+                ServerGamePacketListenerImpl.LOGGER.error("Couldn\'t unregister custom payload", ex);
-+                this.disconnect(Component.literal("Invalid payload UNREGISTER!"), PlayerKickEvent.Cause.INVALID_PAYLOAD); // Paper - kick event cause
++                ServerGamePacketListenerImpl.LOGGER.error("Couldn't unregister custom payload", ex);
++                this.disconnect(Component.literal("Invalid payload UNREGISTER!"), org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PAYLOAD); // Paper - kick event cause
 +            }
 +        } else {
 +            try {
@@ -197,26 +153,26 @@
 +                // Paper end - Brand support
 +                this.cserver.getMessenger().dispatchIncomingMessage(this.player.getBukkitEntity(), identifier.toString(), data);
 +            } catch (Exception ex) {
-+                ServerGamePacketListenerImpl.LOGGER.error("Couldn\'t dispatch custom payload", ex);
-+                this.disconnect(Component.literal("Invalid custom payload!"), PlayerKickEvent.Cause.INVALID_PAYLOAD); // Paper - kick event cause
++                ServerGamePacketListenerImpl.LOGGER.error("Couldn't dispatch custom payload", ex);
++                this.disconnect(Component.literal("Invalid custom payload!"), org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PAYLOAD); // Paper - kick event cause
 +            }
 +        }
-+
 +    }
 +
 +    public final boolean isDisconnected() {
 +        return (!this.player.joining && !this.connection.isConnected()) || this.processedDisconnect; // Paper - Fix duplication bugs
 +    }
 +    // CraftBukkit end
-+
+ 
      @Override
      public void handleResourcePackResponse(ServerboundResourcePackPacket packet) {
-         PacketUtils.ensureRunningOnSameThread(packet, this, (BlockableEventLoop) this.server);
+         PacketUtils.ensureRunningOnSameThread(packet, this, this.server);
          if (packet.action() == ServerboundResourcePackPacket.Action.DECLINED && this.server.isResourcePackRequired()) {
-             ServerCommonPacketListenerImpl.LOGGER.info("Disconnecting {} due to resource pack {} rejection", this.playerProfile().getName(), packet.id());
--            this.disconnect((Component) Component.translatable("multiplayer.requiredTexturePrompt.disconnect"));
-+            this.disconnect((Component) Component.translatable("multiplayer.requiredTexturePrompt.disconnect"), PlayerKickEvent.Cause.RESOURCE_PACK_REJECTION); // Paper - kick event cause
-         }
+             LOGGER.info("Disconnecting {} due to resource pack {} rejection", this.playerProfile().getName(), packet.id());
+-            this.disconnect(Component.translatable("multiplayer.requiredTexturePrompt.disconnect"));
+-        }
++            this.disconnect(Component.translatable("multiplayer.requiredTexturePrompt.disconnect"), org.bukkit.event.player.PlayerKickEvent.Cause.RESOURCE_PACK_REJECTION); // Paper - kick event cause
++        }
 +        // Paper start - adventure pack callbacks
 +        // call the callbacks before the previously-existing event so the event has final say
 +        final net.kyori.adventure.resource.ResourcePackCallback callback;
@@ -230,115 +186,102 @@
 +        }
 +        // Paper end
 +        // Paper start - store last pack status
-+        PlayerResourcePackStatusEvent.Status packStatus = PlayerResourcePackStatusEvent.Status.values()[packet.action().ordinal()];
-+        player.getBukkitEntity().resourcePackStatus = packStatus;
-+        this.cserver.getPluginManager().callEvent(new PlayerResourcePackStatusEvent(this.getCraftPlayer(), packet.id(), packStatus)); // CraftBukkit
++        org.bukkit.event.player.PlayerResourcePackStatusEvent.Status packStatus = org.bukkit.event.player.PlayerResourcePackStatusEvent.Status.values()[packet.action().ordinal()];
++        this.player.getBukkitEntity().resourcePackStatus = packStatus;
++        this.cserver.getPluginManager().callEvent(new org.bukkit.event.player.PlayerResourcePackStatusEvent(this.getCraftPlayer(), packet.id(), packStatus)); // CraftBukkit
 +        // Paper end - store last pack status
- 
      }
  
      @Override
      public void handleCookieResponse(ServerboundCookieResponsePacket packet) {
--        this.disconnect(ServerCommonPacketListenerImpl.DISCONNECT_UNEXPECTED_QUERY);
+-        this.disconnect(DISCONNECT_UNEXPECTED_QUERY);
 +        // CraftBukkit start
-+        PacketUtils.ensureRunningOnSameThread(packet, this, (BlockableEventLoop) this.server);
++        PacketUtils.ensureRunningOnSameThread(packet, this, this.server);
 +        if (this.player.getBukkitEntity().handleCookieResponse(packet)) {
 +            return;
 +        }
 +        // CraftBukkit end
-+        this.disconnect(ServerCommonPacketListenerImpl.DISCONNECT_UNEXPECTED_QUERY, PlayerKickEvent.Cause.INVALID_COOKIE); // Paper - kick event cause
++        this.disconnect(DISCONNECT_UNEXPECTED_QUERY, org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_COOKIE); // Paper - kick event cause
      }
  
      protected void keepConnectionAlive() {
          Profiler.get().push("keepAlive");
--        long i = Util.getMillis();
+         long millis = Util.getMillis();
+-        if (!this.isSingleplayerOwner() && millis - this.keepAliveTime >= 15000L) {
+-            if (this.keepAlivePending) {
+-                this.disconnect(TIMEOUT_DISCONNECTION_MESSAGE);
 +        // Paper start - give clients a longer time to respond to pings as per pre 1.12.2 timings
 +        // This should effectively place the keepalive handling back to "as it was" before 1.12.2
-+        long currentTime = Util.getMillis();
-+        long elapsedTime = currentTime - this.keepAliveTime;
- 
--        if (!this.isSingleplayerOwner() && i - this.keepAliveTime >= 15000L) {
--            if (this.keepAlivePending) {
--                this.disconnect(ServerCommonPacketListenerImpl.TIMEOUT_DISCONNECTION_MESSAGE);
--            } else if (this.checkIfClosed(i)) {
-+        if (!this.isSingleplayerOwner() && elapsedTime >= 15000L) { // Paper - use vanilla's 15000L between keep alive packets
-+            if (this.keepAlivePending && !this.processedDisconnect && elapsedTime >= KEEPALIVE_LIMIT) { // Paper - check keepalive limit, don't fire if already disconnected
-+                this.disconnect(ServerCommonPacketListenerImpl.TIMEOUT_DISCONNECTION_MESSAGE, PlayerKickEvent.Cause.TIMEOUT); // Paper - kick event cause
-+            } else if (this.checkIfClosed(currentTime)) { // Paper
++        final long elapsedTime = millis - this.keepAliveTime;
++        if (!this.isSingleplayerOwner() && elapsedTime >= 15000L) { // use vanilla's 15000L between keep alive packets
++            if (this.keepAlivePending && !this.processedDisconnect && elapsedTime >= KEEPALIVE_LIMIT) { // check keepalive limit, don't fire if already disconnected
++                // Paper end - give clients a longer time to respond to pings as per pre 1.12.2 timings
++                this.disconnect(TIMEOUT_DISCONNECTION_MESSAGE, org.bukkit.event.player.PlayerKickEvent.Cause.TIMEOUT); // Paper - kick event cause
+             } else if (this.checkIfClosed(millis)) {
                  this.keepAlivePending = true;
--                this.keepAliveTime = i;
--                this.keepAliveChallenge = i;
-+                this.keepAliveTime = currentTime;
-+                this.keepAliveChallenge = currentTime;
-                 this.send(new ClientboundKeepAlivePacket(this.keepAliveChallenge));
-             }
-         }
-+        // Paper end - give clients a longer time to respond to pings as per pre 1.12.2 timings
- 
-         Profiler.get().pop();
-     }
-@@ -133,7 +279,7 @@
+                 this.keepAliveTime = millis;
+@@ -126,7 +_,7 @@
      private boolean checkIfClosed(long time) {
          if (this.closed) {
              if (time - this.closedListenerTime >= 15000L) {
--                this.disconnect(ServerCommonPacketListenerImpl.TIMEOUT_DISCONNECTION_MESSAGE);
-+                this.disconnect(ServerCommonPacketListenerImpl.TIMEOUT_DISCONNECTION_MESSAGE, PlayerKickEvent.Cause.TIMEOUT); // Paper - kick event cause
+-                this.disconnect(TIMEOUT_DISCONNECTION_MESSAGE);
++                this.disconnect(TIMEOUT_DISCONNECTION_MESSAGE, org.bukkit.event.player.PlayerKickEvent.Cause.TIMEOUT); // Paper - kick event cause
              }
  
              return false;
-@@ -156,6 +302,14 @@
+@@ -149,6 +_,13 @@
      }
  
-     public void send(Packet<?> packet, @Nullable PacketSendListener callbacks) {
+     public void send(Packet<?> packet, @Nullable PacketSendListener listener) {
 +        // CraftBukkit start
 +        if (packet == null || this.processedDisconnect) { // Spigot
 +            return;
-+        } else if (packet instanceof ClientboundSetDefaultSpawnPositionPacket) {
-+            ClientboundSetDefaultSpawnPositionPacket packet6 = (ClientboundSetDefaultSpawnPositionPacket) packet;
-+            this.player.compassTarget = CraftLocation.toBukkit(packet6.pos, this.getCraftPlayer().getWorld());
++        } else if (packet instanceof net.minecraft.network.protocol.game.ClientboundSetDefaultSpawnPositionPacket defaultSpawnPositionPacket) {
++            this.player.compassTarget = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(defaultSpawnPositionPacket.getPos(), this.getCraftPlayer().getWorld());
 +        }
 +        // CraftBukkit end
          if (packet.isTerminal()) {
              this.close();
          }
-@@ -175,22 +329,109 @@
+@@ -165,19 +_,108 @@
          }
      }
  
 +    // Paper start - adventure
 +    public void disconnect(final net.kyori.adventure.text.Component reason) {
-+        this.disconnect(reason, PlayerKickEvent.Cause.UNKNOWN);
++        this.disconnect(reason, org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN);
 +    }
-+    public void disconnect(final net.kyori.adventure.text.Component reason, PlayerKickEvent.Cause cause) {
++
++    public void disconnect(final net.kyori.adventure.text.Component reason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {
 +        this.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(reason), cause);
-+        // Paper end - kick event causes
 +    }
 +    // Paper end - adventure
 +
 +    @Deprecated @io.papermc.paper.annotation.DoNotUse // Paper - kick event causes
      public void disconnect(Component reason) {
 -        this.disconnect(new DisconnectionDetails(reason));
+-    }
+-
+-    public void disconnect(DisconnectionDetails disconnectionDetails) {
 +        // Paper start - kick event causes
-+        this.disconnect(reason, PlayerKickEvent.Cause.UNKNOWN);
-     }
-+    public void disconnect(final Component reason, PlayerKickEvent.Cause cause) {
++        this.disconnect(reason, org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN);
++    }
++
++    public void disconnect(final Component reason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {
 +        this.disconnect(new DisconnectionDetails(reason), cause);
 +        // Paper end - kick event causes
 +    }
- 
--    public void disconnect(DisconnectionDetails disconnectionInfo) {
--        this.connection.send(new ClientboundDisconnectPacket(disconnectionInfo.reason()), PacketSendListener.thenRun(() -> {
--            this.connection.disconnect(disconnectionInfo);
-+    public void disconnect(DisconnectionDetails disconnectionInfo, PlayerKickEvent.Cause cause) { // Paper - kick event cause
++
++    public void disconnect(DisconnectionDetails disconnectionDetails, org.bukkit.event.player.PlayerKickEvent.Cause cause) { // Paper - kick event cause
 +        // CraftBukkit start - fire PlayerKickEvent
 +        if (this.processedDisconnect) {
 +            return;
 +        }
 +        if (!this.cserver.isPrimaryThread()) {
-+            Waitable waitable = new Waitable() {
++            org.bukkit.craftbukkit.util.Waitable waitable = new org.bukkit.craftbukkit.util.Waitable() {
 +                @Override
 +                protected Object evaluate() {
-+                    ServerCommonPacketListenerImpl.this.disconnect(disconnectionInfo, cause); // Paper - kick event causes
++                    ServerCommonPacketListenerImpl.this.disconnect(disconnectionDetails, cause); // Paper - kick event causes
 +                    return null;
 +                }
 +            };
@@ -349,7 +292,7 @@
 +                waitable.get();
 +            } catch (InterruptedException e) {
 +                Thread.currentThread().interrupt();
-+            } catch (ExecutionException e) {
++            } catch (java.util.concurrent.ExecutionException e) {
 +                throw new RuntimeException(e);
 +            }
 +            return;
@@ -357,7 +300,7 @@
 +
 +        net.kyori.adventure.text.Component leaveMessage = net.kyori.adventure.text.Component.translatable("multiplayer.player.left", net.kyori.adventure.text.format.NamedTextColor.YELLOW, io.papermc.paper.configuration.GlobalConfiguration.get().messages.useDisplayNameInQuitMessage ? this.player.getBukkitEntity().displayName() : net.kyori.adventure.text.Component.text(this.player.getScoreboardName())); // Paper - Adventure
 +
-+        PlayerKickEvent event = new PlayerKickEvent(this.player.getBukkitEntity(), io.papermc.paper.adventure.PaperAdventure.asAdventure(disconnectionInfo.reason()), leaveMessage, cause); // Paper - adventure & kick event causes
++        org.bukkit.event.player.PlayerKickEvent event = new org.bukkit.event.player.PlayerKickEvent(this.player.getBukkitEntity(), io.papermc.paper.adventure.PaperAdventure.asAdventure(disconnectionDetails.reason()), leaveMessage, cause); // Paper - adventure & kick event causes
 +
 +        if (this.cserver.getServer().isRunning()) {
 +            this.cserver.getPluginManager().callEvent(event);
@@ -368,40 +311,41 @@
 +            return;
 +        }
 +        // Send the possibly modified leave message
-+        this.disconnect0(new DisconnectionDetails(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.reason()), disconnectionInfo.report(), disconnectionInfo.bugReportLink()), event.leaveMessage()); // Paper - Adventure & use kick event leave message
++        this.disconnect0(new DisconnectionDetails(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.reason()), disconnectionDetails.report(), disconnectionDetails.bugReportLink()), event.leaveMessage()); // Paper - Adventure & use kick event leave message
 +    }
 +
-+    private void disconnect0(DisconnectionDetails disconnectiondetails, @Nullable net.kyori.adventure.text.Component leaveMessage) { // Paper - use kick event leave message
++    private void disconnect0(DisconnectionDetails disconnectionDetails, @Nullable net.kyori.adventure.text.Component leaveMessage) { // Paper - use kick event leave message
 +        // CraftBukkit end
 +        this.player.quitReason = org.bukkit.event.player.PlayerQuitEvent.QuitReason.KICKED; // Paper - Add API for quit reason
-+        this.connection.send(new ClientboundDisconnectPacket(disconnectiondetails.reason()), PacketSendListener.thenRun(() -> {
-+            this.connection.disconnect(disconnectiondetails);
-         }));
-+        this.onDisconnect(disconnectiondetails, leaveMessage); // CraftBukkit - fire quit instantly // Paper - use kick event leave message
-         this.connection.setReadOnly();
-         MinecraftServer minecraftserver = this.server;
-         Connection networkmanager = this.connection;
- 
-         Objects.requireNonNull(this.connection);
--        minecraftserver.executeBlocking(networkmanager::handleDisconnection);
+         this.connection
+             .send(
+                 new ClientboundDisconnectPacket(disconnectionDetails.reason()),
+                 PacketSendListener.thenRun(() -> this.connection.disconnect(disconnectionDetails))
+             );
+-        this.connection.setReadOnly();
+-        this.server.executeBlocking(this.connection::handleDisconnection);
+-    }
++        this.onDisconnect(disconnectionDetails, leaveMessage); // CraftBukkit - fire quit instantly // Paper - use kick event leave message
++        this.connection.setReadOnly();
 +        // CraftBukkit - Don't wait
-+        minecraftserver.scheduleOnMain(networkmanager::handleDisconnection); // Paper
-     }
- 
++        this.server.scheduleOnMain(this.connection::handleDisconnection); // Paper
++    }
++
 +    // Paper start - add proper async disconnect
-+    public void disconnectAsync(net.kyori.adventure.text.Component reason, PlayerKickEvent.Cause cause) {
++    public void disconnectAsync(net.kyori.adventure.text.Component reason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {
 +        this.disconnectAsync(io.papermc.paper.adventure.PaperAdventure.asVanilla(reason), cause);
 +    }
 +
-+    public void disconnectAsync(Component reason, PlayerKickEvent.Cause cause) {
++    public void disconnectAsync(Component reason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {
 +        this.disconnectAsync(new DisconnectionDetails(reason), cause);
 +    }
 +
-+    public void disconnectAsync(DisconnectionDetails disconnectionInfo, PlayerKickEvent.Cause cause) {
++    public void disconnectAsync(DisconnectionDetails disconnectionInfo, org.bukkit.event.player.PlayerKickEvent.Cause cause) {
 +        if (this.cserver.isPrimaryThread()) {
 +            this.disconnect(disconnectionInfo, cause);
 +            return;
 +        }
++
 +        this.connection.setReadOnly();
 +        this.server.scheduleOnMain(() -> {
 +            ServerCommonPacketListenerImpl.this.disconnect(disconnectionInfo, cause);
@@ -412,7 +356,6 @@
 +        });
 +    }
 +    // Paper end - add proper async disconnect
-+
+ 
      protected boolean isSingleplayerOwner() {
          return this.server.isSingleplayerOwner(this.playerProfile());
-     }
diff --git a/paper-server/patches/sources/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java.patch b/paper-server/patches/sources/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java.patch
new file mode 100644
index 0000000000..2ff7ea70fa
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java.patch
@@ -0,0 +1,72 @@
+--- a/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java
++++ b/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java
+@@ -48,8 +_,10 @@
+     @Nullable
+     private SynchronizeRegistriesTask synchronizeRegistriesTask;
+ 
+-    public ServerConfigurationPacketListenerImpl(MinecraftServer server, Connection connection, CommonListenerCookie cookie) {
+-        super(server, connection, cookie);
++    // CraftBukkit start
++    public ServerConfigurationPacketListenerImpl(MinecraftServer server, Connection connection, CommonListenerCookie cookie, ServerPlayer player) {
++        super(server, connection, cookie, player);
++        // CraftBukkit end
+         this.gameProfile = cookie.gameProfile();
+         this.clientInformation = cookie.clientInformation();
+     }
+@@ -61,6 +_,11 @@
+ 
+     @Override
+     public void onDisconnect(DisconnectionDetails details) {
++        // Paper start - Debugging
++        if (this.server.isDebugging()) {
++            ServerConfigurationPacketListenerImpl.LOGGER.info("{} lost connection: {}, while in configuration phase {}", this.gameProfile, details.reason().getString(), this.currentTask != null ? this.currentTask.type().id() : "null");
++        } else
++        // Paper end
+         LOGGER.info("{} lost connection: {}", this.gameProfile, details.reason().getString());
+         super.onDisconnect(details);
+     }
+@@ -73,6 +_,12 @@
+     public void startConfiguration() {
+         this.send(new ClientboundCustomPayloadPacket(new BrandPayload(this.server.getServerModName())));
+         ServerLinks serverLinks = this.server.serverLinks();
++        // CraftBukkit start
++        org.bukkit.craftbukkit.CraftServerLinks wrapper = new org.bukkit.craftbukkit.CraftServerLinks(serverLinks);
++        org.bukkit.event.player.PlayerLinksSendEvent event = new org.bukkit.event.player.PlayerLinksSendEvent(this.player.getBukkitEntity(), wrapper);
++        this.cserver.getPluginManager().callEvent(event);
++        serverLinks = wrapper.getServerLinks();
++        // CraftBukkit end
+         if (!serverLinks.isEmpty()) {
+             this.send(new ClientboundServerLinksPacket(serverLinks.untrust()));
+         }
+@@ -105,6 +_,7 @@
+     @Override
+     public void handleClientInformation(ServerboundClientInformationPacket packet) {
+         this.clientInformation = packet.information();
++        this.connection.channel.attr(io.papermc.paper.adventure.PaperAdventure.LOCALE_ATTRIBUTE).set(net.kyori.adventure.translation.Translator.parseLocale(packet.information().language())); // Paper
+     }
+ 
+     @Override
+@@ -139,16 +_,21 @@
+                 return;
+             }
+ 
+-            Component component = playerList.canPlayerLogin(this.connection.getRemoteAddress(), this.gameProfile);
++            Component component = null; // CraftBukkit - login checks already completed
+             if (component != null) {
+                 this.disconnect(component);
+                 return;
+             }
+ 
+-            ServerPlayer playerForLogin = playerList.getPlayerForLogin(this.gameProfile, this.clientInformation);
++            ServerPlayer playerForLogin = playerList.getPlayerForLogin(this.gameProfile, this.clientInformation, this.player); // CraftBukkit
+             playerList.placeNewPlayer(this.connection, playerForLogin, this.createCookie(this.clientInformation));
+         } catch (Exception var5) {
+             LOGGER.error("Couldn't place player in world", (Throwable)var5);
++            // Paper start - Debugging
++            if (this.server.isDebugging()) {
++                var5.printStackTrace();
++            }
++            // Paper end - Debugging
+             this.connection.send(new ClientboundDisconnectPacket(DISCONNECT_REASON_INVALID_DATA));
+             this.connection.disconnect(DISCONNECT_REASON_INVALID_DATA);
+         }
diff --git a/paper-server/patches/sources/net/minecraft/server/network/ServerConnectionListener.java.patch b/paper-server/patches/sources/net/minecraft/server/network/ServerConnectionListener.java.patch
new file mode 100644
index 0000000000..78b623e0aa
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/network/ServerConnectionListener.java.patch
@@ -0,0 +1,154 @@
+--- a/net/minecraft/server/network/ServerConnectionListener.java
++++ b/net/minecraft/server/network/ServerConnectionListener.java
+@@ -49,10 +_,10 @@
+ public class ServerConnectionListener {
+     private static final Logger LOGGER = LogUtils.getLogger();
+     public static final Supplier<NioEventLoopGroup> SERVER_EVENT_GROUP = Suppliers.memoize(
+-        () -> new NioEventLoopGroup(0, new ThreadFactoryBuilder().setNameFormat("Netty Server IO #%d").setDaemon(true).build())
++        () -> new NioEventLoopGroup(0, new ThreadFactoryBuilder().setNameFormat("Netty Server IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()) // Paper
+     );
+     public static final Supplier<EpollEventLoopGroup> SERVER_EPOLL_EVENT_GROUP = Suppliers.memoize(
+-        () -> new EpollEventLoopGroup(0, new ThreadFactoryBuilder().setNameFormat("Netty Epoll Server IO #%d").setDaemon(true).build())
++        () -> new EpollEventLoopGroup(0, new ThreadFactoryBuilder().setNameFormat("Netty Epoll Server IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()) // Paper
+     );
+     final MinecraftServer server;
+     public volatile boolean running;
+@@ -64,12 +_,35 @@
+         this.running = true;
+     }
+ 
++    // Paper start - prevent blocking on adding a new connection while the server is ticking
++    private final java.util.Queue<Connection> pending = new java.util.concurrent.ConcurrentLinkedQueue<>();
++
++    private final void addPending() {
++        Connection connection;
++        while ((connection = this.pending.poll()) != null) {
++            this.connections.add(connection);
++        }
++    }
++    // Paper end - prevent blocking on adding a new connection while the server is ticking
++
+     public void startTcpServerListener(@Nullable InetAddress address, int port) throws IOException {
++        // Paper start - Unix domain socket support
++        this.startTcpServerListener(new java.net.InetSocketAddress(address, port));
++    }
++
++    public void startTcpServerListener(SocketAddress address) throws IOException {
++        // Paper end - Unix domain socket support
+         synchronized (this.channels) {
+-            Class<? extends ServerSocketChannel> clazz;
++            Class<? extends io.netty.channel.ServerChannel> clazz; // Paper - Unix domain socket support
+             EventLoopGroup eventLoopGroup;
+             if (Epoll.isAvailable() && this.server.isEpollEnabled()) {
++                // Paper start - Unix domain socket support
++                if (address instanceof io.netty.channel.unix.DomainSocketAddress) {
++                    clazz = io.netty.channel.epoll.EpollServerDomainSocketChannel.class;
++                } else {
+                 clazz = EpollServerSocketChannel.class;
++                }
++                // Paper end - Unix domain socket support
+                 eventLoopGroup = SERVER_EPOLL_EVENT_GROUP.get();
+                 LOGGER.info("Using epoll channel type");
+             } else {
+@@ -78,6 +_,12 @@
+                 LOGGER.info("Using default channel type");
+             }
+ 
++            // Paper start - Warn people with console access that HAProxy is in use.
++            if (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.proxyProtocol) {
++                LOGGER.warn("Using HAProxy, please ensure the server port is adequately firewalled.");
++            }
++            // Paper end - Warn people with console access that HAProxy is in use.
++
+             this.channels
+                 .add(
+                     new ServerBootstrap()
+@@ -101,22 +_,64 @@
+                                     Connection connection = (Connection)(rateLimitPacketsPerSecond > 0
+                                         ? new RateKickingConnection(rateLimitPacketsPerSecond)
+                                         : new Connection(PacketFlow.SERVERBOUND));
+-                                    ServerConnectionListener.this.connections.add(connection);
++                                    // Paper start - Add support for Proxy Protocol
++                                    if (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.proxyProtocol) {
++                                        channel.pipeline().addAfter("timeout", "haproxy-decoder", new io.netty.handler.codec.haproxy.HAProxyMessageDecoder());
++                                        channel.pipeline().addAfter("haproxy-decoder", "haproxy-handler", new ChannelInboundHandlerAdapter() {
++                                            @Override
++                                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
++                                                if (msg instanceof io.netty.handler.codec.haproxy.HAProxyMessage message) {
++                                                    if (message.command() == io.netty.handler.codec.haproxy.HAProxyCommand.PROXY) {
++                                                        String realaddress = message.sourceAddress();
++                                                        int realport = message.sourcePort();
++
++                                                        SocketAddress socketaddr = new java.net.InetSocketAddress(realaddress, realport);
++
++                                                        Connection connection = (Connection) channel.pipeline().get("packet_handler");
++                                                        connection.address = socketaddr;
++                                                        // Paper start - Add API to get player's proxy address
++                                                        final String proxyAddress = message.destinationAddress();
++                                                        final int proxyPort = message.destinationPort();
++
++                                                        connection.haProxyAddress = new java.net.InetSocketAddress(proxyAddress, proxyPort);
++                                                        // Paper end - Add API to get player's proxy address
++                                                    }
++                                                } else {
++                                                    super.channelRead(ctx, msg);
++                                                }
++                                            }
++                                        });
++                                    }
++                                    // Paper end - Add support for proxy protocol
++                                    // ServerConnectionListener.this.connections.add(connection); // Paper - prevent blocking on adding a new connection while the server is ticking
++                                    ServerConnectionListener.this.pending.add(connection); // Paper - prevent blocking on adding a new connection while the server is ticking
+                                     connection.configurePacketHandler(channelPipeline);
+                                     connection.setListenerForServerboundHandshake(
+                                         new ServerHandshakePacketListenerImpl(ServerConnectionListener.this.server, connection)
+                                     );
++                                    io.papermc.paper.network.ChannelInitializeListenerHolder.callListeners(channel); // Paper - Add Channel initialization listeners
+                                 }
+                             }
+                         )
+                         .group(eventLoopGroup)
+-                        .localAddress(address, port)
++                        .localAddress(address) // Paper - Unix domain socket support
++                        .option(ChannelOption.AUTO_READ, false) // CraftBukkit
+                         .bind()
+                         .syncUninterruptibly()
+                 );
+         }
+     }
+ 
++    // CraftBukkit start
++    public void acceptConnections() {
++        synchronized (this.channels) {
++            for (ChannelFuture future : this.channels) {
++                future.channel().config().setAutoRead(true);
++            }
++        }
++    }
++    // CraftBukkit end
++
+     public SocketAddress startMemoryChannel() {
+         ChannelFuture channelFuture;
+         synchronized (this.channels) {
+@@ -161,6 +_,13 @@
+ 
+     public void tick() {
+         synchronized (this.connections) {
++            // Spigot start
++            this.addPending(); // Paper - prevent blocking on adding a new connection while the server is ticking
++            // This prevents players from 'gaming' the server, and strategically relogging to increase their position in the tick order
++            if (org.spigotmc.SpigotConfig.playerShuffle > 0 && MinecraftServer.currentTick % org.spigotmc.SpigotConfig.playerShuffle == 0) {
++                Collections.shuffle(this.connections);
++            }
++            // Spigot end
+             Iterator<Connection> iterator = this.connections.iterator();
+ 
+             while (iterator.hasNext()) {
+@@ -180,6 +_,7 @@
+                             connection.setReadOnly();
+                         }
+                     } else {
++                        if (connection.preparing) continue; // Spigot - Fix a race condition where a NetworkManager could be unregistered just before connection
+                         iterator.remove();
+                         connection.handleDisconnection();
+                     }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch b/paper-server/patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch
similarity index 60%
rename from paper-server/patches/unapplied/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch
rename to paper-server/patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch
index 6c1745ae43..37ab47c80b 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch
@@ -1,93 +1,34 @@
 --- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java
 +++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java
-@@ -45,6 +45,7 @@
- import net.minecraft.network.Connection;
- import net.minecraft.network.DisconnectionDetails;
- import net.minecraft.network.TickablePacketListener;
-+import net.minecraft.network.chat.ChatDecorator;
- import net.minecraft.network.chat.ChatType;
- import net.minecraft.network.chat.Component;
- import net.minecraft.network.chat.LastSeenMessages;
-@@ -65,12 +66,15 @@
- import net.minecraft.network.protocol.game.ClientboundBlockChangedAckPacket;
- import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket;
- import net.minecraft.network.protocol.game.ClientboundCommandSuggestionsPacket;
-+import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket;
- import net.minecraft.network.protocol.game.ClientboundDisguisedChatPacket;
- import net.minecraft.network.protocol.game.ClientboundMoveVehiclePacket;
- import net.minecraft.network.protocol.game.ClientboundPlaceGhostRecipePacket;
- import net.minecraft.network.protocol.game.ClientboundPlayerChatPacket;
- import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket;
- import net.minecraft.network.protocol.game.ClientboundPlayerPositionPacket;
-+import net.minecraft.network.protocol.game.ClientboundSetEntityLinkPacket;
-+import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket;
- import net.minecraft.network.protocol.game.ClientboundSetHeldSlotPacket;
- import net.minecraft.network.protocol.game.ClientboundStartConfigurationPacket;
- import net.minecraft.network.protocol.game.ClientboundSystemChatPacket;
-@@ -148,14 +152,13 @@
- import net.minecraft.world.entity.ExperienceOrb;
- import net.minecraft.world.entity.HasCustomInventoryScreen;
- import net.minecraft.world.entity.LivingEntity;
-+import net.minecraft.world.entity.Mob;
- import net.minecraft.world.entity.MoverType;
- import net.minecraft.world.entity.PlayerRideableJumping;
- import net.minecraft.world.entity.PositionMoveRotation;
- import net.minecraft.world.entity.Relative;
--import net.minecraft.world.entity.item.ItemEntity;
- import net.minecraft.world.entity.player.ChatVisiblity;
- import net.minecraft.world.entity.player.Inventory;
--import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.entity.player.PlayerModelPart;
- import net.minecraft.world.entity.player.ProfilePublicKey;
- import net.minecraft.world.entity.projectile.AbstractArrow;
-@@ -176,6 +179,7 @@
- import net.minecraft.world.item.crafting.RecipeHolder;
- import net.minecraft.world.item.crafting.RecipeManager;
- import net.minecraft.world.level.BaseCommandBlock;
-+import net.minecraft.world.level.ClipContext;
- import net.minecraft.world.level.GameRules;
- import net.minecraft.world.level.GameType;
- import net.minecraft.world.level.Level;
-@@ -192,11 +196,72 @@
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.phys.AABB;
- import net.minecraft.world.phys.BlockHitResult;
-+import net.minecraft.world.phys.HitResult;
- import net.minecraft.world.phys.Vec3;
- import net.minecraft.world.phys.shapes.BooleanOp;
- import net.minecraft.world.phys.shapes.Shapes;
+@@ -193,6 +_,59 @@
  import net.minecraft.world.phys.shapes.VoxelShape;
-+import org.bukkit.NamespacedKey;
  import org.slf4j.Logger;
-+
+ 
 +// CraftBukkit start
 +import io.papermc.paper.adventure.ChatProcessor; // Paper
 +import io.papermc.paper.adventure.PaperAdventure; // Paper
 +import com.mojang.datafixers.util.Pair;
 +import java.util.Arrays;
 +import java.util.concurrent.ExecutionException;
-+import java.util.concurrent.atomic.AtomicInteger;
 +import java.util.function.Function;
++import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket;
 +import net.minecraft.network.chat.OutgoingChatMessage;
 +import net.minecraft.world.entity.animal.Bucketable;
 +import net.minecraft.world.entity.animal.allay.Allay;
-+import net.minecraft.world.entity.item.ItemEntity;
++import net.minecraft.world.inventory.AbstractContainerMenu;
 +import net.minecraft.world.inventory.Slot;
-+import net.minecraft.world.item.crafting.RecipeHolder;
++import net.minecraft.world.level.ClipContext;
++import net.minecraft.world.phys.HitResult;
 +import org.bukkit.Location;
++import org.bukkit.NamespacedKey;
 +import org.bukkit.craftbukkit.CraftInput;
 +import org.bukkit.craftbukkit.entity.CraftEntity;
-+import org.bukkit.craftbukkit.entity.CraftPlayer;
 +import org.bukkit.craftbukkit.event.CraftEventFactory;
 +import org.bukkit.craftbukkit.inventory.CraftItemStack;
 +import org.bukkit.craftbukkit.inventory.CraftItemType;
-+import org.bukkit.craftbukkit.inventory.CraftRecipe;
-+import org.bukkit.craftbukkit.util.CraftChatMessage;
 +import org.bukkit.craftbukkit.util.CraftLocation;
-+import org.bukkit.craftbukkit.util.CraftNamespacedKey;
 +import org.bukkit.craftbukkit.util.LazyPlayerSet;
 +import org.bukkit.craftbukkit.util.Waitable;
-+import org.bukkit.entity.Player;
 +import org.bukkit.event.Event;
 +import org.bukkit.event.block.Action;
 +import org.bukkit.event.inventory.ClickType;
@@ -98,8 +39,6 @@
 +import org.bukkit.event.inventory.InventoryType.SlotType;
 +import org.bukkit.event.inventory.SmithItemEvent;
 +import org.bukkit.event.player.AsyncPlayerChatEvent;
-+import org.bukkit.event.player.PlayerAnimationEvent;
-+import org.bukkit.event.player.PlayerAnimationType;
 +import org.bukkit.event.player.PlayerChatEvent;
 +import org.bukkit.event.player.PlayerCommandPreprocessEvent;
 +import org.bukkit.event.player.PlayerInputEvent;
@@ -114,14 +53,14 @@
 +import org.bukkit.event.player.PlayerToggleSneakEvent;
 +import org.bukkit.event.player.PlayerToggleSprintEvent;
 +import org.bukkit.inventory.CraftingInventory;
-+import org.bukkit.inventory.EquipmentSlot;
 +import org.bukkit.inventory.InventoryView;
 +import org.bukkit.inventory.SmithingInventory;
 +// CraftBukkit end
- 
- public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl implements ServerGamePacketListener, ServerPlayerConnection, TickablePacketListener {
- 
-@@ -212,7 +277,9 @@
++
+ public class ServerGamePacketListenerImpl
+     extends ServerCommonPacketListenerImpl
+     implements ServerGamePacketListener,
+@@ -210,7 +_,9 @@
      private int tickCount;
      private int ackBlockChangesUpTo = -1;
      private final TickThrottler chatSpamThrottler = new TickThrottler(20, 200);
@@ -131,33 +70,11 @@
      private double firstGoodX;
      private double firstGoodY;
      private double firstGoodZ;
-@@ -240,14 +307,16 @@
+@@ -236,22 +_,39 @@
+     private int receivedMovePacketCount;
+     private int knownMovePacketCount;
      private boolean receivedMovementThisTick;
-     @Nullable
-     private RemoteChatSession chatSession;
-+    private boolean hasLoggedExpiry = false; // Paper - Prevent causing expired keys from impacting new joins
-     private SignedMessageChain.Decoder signedMessageDecoder;
-     private final LastSeenMessagesValidator lastSeenMessages = new LastSeenMessagesValidator(20);
-     private final MessageSignatureCache messageSignatureCache = MessageSignatureCache.createDefault();
-     private final FutureChain chatMessageChain;
-     private boolean waitingForSwitchToConfig;
-+    private static final int MAX_SIGN_LINE_LENGTH = Integer.getInteger("Paper.maxSignLength", 80); // Paper - Limit client sign length
- 
-     public ServerGamePacketListenerImpl(MinecraftServer server, Connection connection, ServerPlayer player, CommonListenerCookie clientData) {
--        super(server, connection, clientData);
-+        super(server, connection, clientData, player); // CraftBukkit
-         this.chunkSender = new PlayerChunkSender(connection.isMemoryConnection());
-         this.player = player;
-         player.connection = this;
-@@ -256,9 +325,25 @@
- 
-         Objects.requireNonNull(server);
-         this.signedMessageDecoder = SignedMessageChain.Decoder.unsigned(uuid, server::enforceSecureProfile);
--        this.chatMessageChain = new FutureChain(server);
-+        this.chatMessageChain = new FutureChain(server.chatExecutor); // CraftBukkit - async chat
-     }
- 
-+    // CraftBukkit start - add fields and methods
++    // CraftBukkit start - add fields
 +    private int lastTick = MinecraftServer.currentTick;
 +    private int allowedPlayerTicks = 1;
 +    private int lastDropTick = MinecraftServer.currentTick;
@@ -172,53 +89,72 @@
 +    private float lastYaw = Float.MAX_VALUE;
 +    private boolean justTeleported = false;
 +    // CraftBukkit end
-+
+     @Nullable
+     private RemoteChatSession chatSession;
++    private boolean hasLoggedExpiry = false; // Paper - Prevent causing expired keys from impacting new joins
+     private SignedMessageChain.Decoder signedMessageDecoder;
+     private final LastSeenMessagesValidator lastSeenMessages = new LastSeenMessagesValidator(20);
+     private final MessageSignatureCache messageSignatureCache = MessageSignatureCache.createDefault();
+     private final FutureChain chatMessageChain;
+     private boolean waitingForSwitchToConfig;
++    private static final int MAX_SIGN_LINE_LENGTH = Integer.getInteger("Paper.maxSignLength", 80); // Paper - Limit client sign length
+ 
+     public ServerGamePacketListenerImpl(MinecraftServer server, Connection connection, ServerPlayer player, CommonListenerCookie cookie) {
+-        super(server, connection, cookie);
++        super(server, connection, cookie, player); // CraftBukkit
+         this.chunkSender = new PlayerChunkSender(connection.isMemoryConnection());
+         this.player = player;
+         player.connection = this;
+         player.getTextFilter().join();
+         this.signedMessageDecoder = SignedMessageChain.Decoder.unsigned(player.getUUID(), server::enforceSecureProfile);
+-        this.chatMessageChain = new FutureChain(server);
++        this.chatMessageChain = new FutureChain(server.chatExecutor); // CraftBukkit - async chat
+     }
+ 
      @Override
-     public void tick() {
-         if (this.ackBlockChangesUpTo > -1) {
-@@ -277,7 +362,7 @@
+@@ -272,7 +_,7 @@
          if (this.clientIsFloating && !this.player.isSleeping() && !this.player.isPassenger() && !this.player.isDeadOrDying()) {
              if (++this.aboveGroundTickCount > this.getMaximumFlyingTicks(this.player)) {
-                 ServerGamePacketListenerImpl.LOGGER.warn("{} was kicked for floating too long!", this.player.getName().getString());
--                this.disconnect((Component) Component.translatable("multiplayer.disconnect.flying"));
+                 LOGGER.warn("{} was kicked for floating too long!", this.player.getName().getString());
+-                this.disconnect(Component.translatable("multiplayer.disconnect.flying"));
 +                this.disconnect(io.papermc.paper.configuration.GlobalConfiguration.get().messages.kick.flyingPlayer, org.bukkit.event.player.PlayerKickEvent.Cause.FLYING_PLAYER); // Paper - use configurable kick message & kick event cause
                  return;
              }
          } else {
-@@ -296,7 +381,7 @@
+@@ -291,7 +_,7 @@
              if (this.clientVehicleIsFloating && this.lastVehicle.getControllingPassenger() == this.player) {
                  if (++this.aboveGroundVehicleTickCount > this.getMaximumFlyingTicks(this.lastVehicle)) {
-                     ServerGamePacketListenerImpl.LOGGER.warn("{} was kicked for floating a vehicle too long!", this.player.getName().getString());
--                    this.disconnect((Component) Component.translatable("multiplayer.disconnect.flying"));
+                     LOGGER.warn("{} was kicked for floating a vehicle too long!", this.player.getName().getString());
+-                    this.disconnect(Component.translatable("multiplayer.disconnect.flying"));
 +                    this.disconnect(io.papermc.paper.configuration.GlobalConfiguration.get().messages.kick.flyingVehicle, org.bukkit.event.player.PlayerKickEvent.Cause.FLYING_VEHICLE); // Paper - use configurable kick message & kick event cause
                      return;
                  }
              } else {
-@@ -311,11 +396,21 @@
- 
+@@ -307,11 +_,20 @@
          this.keepConnectionAlive();
          this.chatSpamThrottler.tick();
+         this.dropSpamThrottler.tick();
 +        this.tabSpamThrottler.tick(); // Paper - configurable tab spam limits
 +        this.recipeSpamPackets.tick(); // Paper - auto recipe limit
-         this.dropSpamThrottler.tick();
--        if (this.player.getLastActionTime() > 0L && this.server.getPlayerIdleTimeout() > 0 && Util.getMillis() - this.player.getLastActionTime() > (long) this.server.getPlayerIdleTimeout() * 1000L * 60L) {
--            this.disconnect((Component) Component.translatable("multiplayer.disconnect.idling"));
-+        if (this.player.getLastActionTime() > 0L && this.server.getPlayerIdleTimeout() > 0 && Util.getMillis() - this.player.getLastActionTime() > (long) this.server.getPlayerIdleTimeout() * 1000L * 60L && !this.player.wonGame) { // Paper - Prevent AFK kick while watching end credits
+         if (this.player.getLastActionTime() > 0L
+             && this.server.getPlayerIdleTimeout() > 0
+-            && Util.getMillis() - this.player.getLastActionTime() > this.server.getPlayerIdleTimeout() * 1000L * 60L) {
+-            this.disconnect(Component.translatable("multiplayer.disconnect.idling"));
+-        }
++            && Util.getMillis() - this.player.getLastActionTime() > this.server.getPlayerIdleTimeout() * 1000L * 60L && !this.player.wonGame) { // Paper - Prevent AFK kick while watching end credits
 +            this.player.resetLastActionTime(); // CraftBukkit - SPIGOT-854
-+            this.disconnect((Component) Component.translatable("multiplayer.disconnect.idling"), org.bukkit.event.player.PlayerKickEvent.Cause.IDLING); // Paper - kick event cause
-         }
- 
++            this.disconnect(Component.translatable("multiplayer.disconnect.idling"), org.bukkit.event.player.PlayerKickEvent.Cause.IDLING); // Paper - kick event cause
++        }
 +        // Paper start - Prevent causing expired keys from impacting new joins
-+        if (!hasLoggedExpiry && this.chatSession != null && this.chatSession.profilePublicKey().data().hasExpired()) {
++        if (!this.hasLoggedExpiry && this.chatSession != null && this.chatSession.profilePublicKey().data().hasExpired()) {
 +            LOGGER.info("Player profile key for {} has expired!", this.player.getName().getString());
-+            hasLoggedExpiry = true;
++            this.hasLoggedExpiry = true;
 +        }
 +        // Paper end - Prevent causing expired keys from impacting new joins
-+
      }
  
-     private int getMaximumFlyingTicks(Entity vehicle) {
-@@ -376,6 +471,12 @@
+     private int getMaximumFlyingTicks(Entity entity) {
+@@ -371,6 +_,12 @@
      @Override
      public void handlePlayerInput(ServerboundPlayerInputPacket packet) {
          PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
@@ -231,28 +167,21 @@
          this.player.setLastClientInput(packet.input());
      }
  
-@@ -395,27 +496,84 @@
+@@ -390,17 +_,29 @@
      public void handleMoveVehicle(ServerboundMoveVehiclePacket packet) {
          PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
-         if (ServerGamePacketListenerImpl.containsInvalidValues(packet.position().x(), packet.position().y(), packet.position().z(), packet.yRot(), packet.xRot())) {
--            this.disconnect((Component) Component.translatable("multiplayer.disconnect.invalid_vehicle_movement"));
-+            this.disconnect((Component) Component.translatable("multiplayer.disconnect.invalid_vehicle_movement"), org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_VEHICLE_MOVEMENT); // Paper - kick event cause
+         if (containsInvalidValues(packet.position().x(), packet.position().y(), packet.position().z(), packet.yRot(), packet.xRot())) {
+-            this.disconnect(Component.translatable("multiplayer.disconnect.invalid_vehicle_movement"));
++            this.disconnect(Component.translatable("multiplayer.disconnect.invalid_vehicle_movement"), org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_VEHICLE_MOVEMENT); // Paper - kick event cause
          } else if (!this.updateAwaitingTeleport() && this.player.hasClientLoaded()) {
-             Entity entity = this.player.getRootVehicle();
+             Entity rootVehicle = this.player.getRootVehicle();
 +            // Paper start - Don't allow vehicle movement from players while teleporting
-+            if (this.awaitingPositionFromClient != null || this.player.isImmobile() || entity.isRemoved()) {
++            if (this.awaitingPositionFromClient != null || this.player.isImmobile() || rootVehicle.isRemoved()) {
 +                return;
 +            }
 +            // Paper end - Don't allow vehicle movement from players while teleporting
- 
-             if (entity != this.player && entity.getControllingPassenger() == this.player && entity == this.lastVehicle) {
-                 ServerLevel worldserver = this.player.serverLevel();
--                double d0 = entity.getX();
--                double d1 = entity.getY();
--                double d2 = entity.getZ();
--                double d3 = ServerGamePacketListenerImpl.clampHorizontal(packet.position().x());
--                double d4 = ServerGamePacketListenerImpl.clampVertical(packet.position().y());
--                double d5 = ServerGamePacketListenerImpl.clampHorizontal(packet.position().z());
+             if (rootVehicle != this.player && rootVehicle.getControllingPassenger() == this.player && rootVehicle == this.lastVehicle) {
+                 ServerLevel serverLevel = this.player.serverLevel();
 +                // CraftBukkit - store current player position
 +                double prevX = this.player.getX();
 +                double prevY = this.player.getY();
@@ -260,31 +189,34 @@
 +                float prevYaw = this.player.getYRot();
 +                float prevPitch = this.player.getXRot();
 +                // CraftBukkit end
-+                double d0 = entity.getX(); final double fromX = d0; // Paper - OBFHELPER
-+                double d1 = entity.getY(); final double fromY = d1; // Paper - OBFHELPER
-+                double d2 = entity.getZ(); final double fromZ = d2; // Paper - OBFHELPER
-+                double d3 = ServerGamePacketListenerImpl.clampHorizontal(packet.position().x()); final double toX = d3; // Paper - OBFHELPER
-+                double d4 = ServerGamePacketListenerImpl.clampVertical(packet.position().y()); final double toY = d4; // Paper - OBFHELPER
-+                double d5 = ServerGamePacketListenerImpl.clampHorizontal(packet.position().z()); final double toZ = d5; // Paper - OBFHELPER
+                 double x = rootVehicle.getX();
+                 double y = rootVehicle.getY();
+                 double z = rootVehicle.getZ();
+-                double d = clampHorizontal(packet.position().x());
+-                double d1 = clampVertical(packet.position().y());
+-                double d2 = clampHorizontal(packet.position().z());
++                double d = clampHorizontal(packet.position().x()); final double toX = d; // Paper - OBFHELPER
++                double d1 = clampVertical(packet.position().y()); final double toY = d1; // Paper - OBFHELPER
++                double d2 = clampHorizontal(packet.position().z()); final double toZ = d2; // Paper - OBFHELPER
                  float f = Mth.wrapDegrees(packet.yRot());
                  float f1 = Mth.wrapDegrees(packet.xRot());
-                 double d6 = d3 - this.vehicleFirstGoodX;
-                 double d7 = d4 - this.vehicleFirstGoodY;
-                 double d8 = d5 - this.vehicleFirstGoodZ;
-                 double d9 = entity.getDeltaMovement().lengthSqr();
--                double d10 = d6 * d6 + d7 * d7 + d8 * d8;
+                 double d3 = d - this.vehicleFirstGoodX;
+@@ -408,7 +_,53 @@
+                 double d5 = d2 - this.vehicleFirstGoodZ;
+                 double d6 = rootVehicle.getDeltaMovement().lengthSqr();
+                 double d7 = d3 * d3 + d4 * d4 + d5 * d5;
+-                if (d7 - d6 > 100.0 && !this.isSingleplayerOwner()) {
 +                // Paper start - fix large move vectors killing the server
-+                double currDeltaX = toX - fromX;
-+                double currDeltaY = toY - fromY;
-+                double currDeltaZ = toZ - fromZ;
-+                double d10 = Math.max(d6 * d6 + d7 * d7 + d8 * d8, (currDeltaX * currDeltaX + currDeltaY * currDeltaY + currDeltaZ * currDeltaZ) - 1);
-+                double otherFieldX = d3 - this.vehicleLastGoodX;
-+                double otherFieldY = d4 - this.vehicleLastGoodY;
-+                double otherFieldZ = d5 - this.vehicleLastGoodZ;
-+                d10 = Math.max(d10, (otherFieldX * otherFieldX + otherFieldY * otherFieldY + otherFieldZ * otherFieldZ) - 1);
++                double currDeltaX = toX - x;
++                double currDeltaY = toY - y;
++                double currDeltaZ = toZ - z;
++                d7 = Math.max(d7, (currDeltaX * currDeltaX + currDeltaY * currDeltaY + currDeltaZ * currDeltaZ) - 1);
++                double otherFieldX = toX - this.vehicleLastGoodX;
++                double otherFieldY = toY - this.vehicleLastGoodY;
++                double otherFieldZ = toZ - this.vehicleLastGoodZ;
++                d7 = Math.max(d7, (otherFieldX * otherFieldX + otherFieldY * otherFieldY + otherFieldZ * otherFieldZ) - 1);
 +                // Paper end - fix large move vectors killing the server
- 
--                if (d10 - d9 > 100.0D && !this.isSingleplayerOwner()) {
++
 +                // CraftBukkit start - handle custom speeds and skipped ticks
 +                this.allowedPlayerTicks += (System.currentTimeMillis() / 50) - this.lastTick;
 +                this.allowedPlayerTicks = Math.max(this.allowedPlayerTicks, 1);
@@ -297,7 +229,7 @@
 +                    i = 1;
 +                }
 +
-+                if (d10 > 0) {
++                if (d7 > 0) {
 +                    this.allowedPlayerTicks -= 1;
 +                } else {
 +                    this.allowedPlayerTicks = 20;
@@ -312,55 +244,60 @@
 +
 +                // Paper start - Prevent moving into unloaded chunks
 +                if (this.player.level().paperConfig().chunks.preventMovingIntoUnloadedChunks && (
-+                    !worldserver.areChunksLoadedForMove(this.player.getBoundingBox().expandTowards(new Vec3(toX, toY, toZ).subtract(this.player.position()))) ||
-+                        !worldserver.areChunksLoadedForMove(entity.getBoundingBox().expandTowards(new Vec3(toX, toY, toZ).subtract(entity.position())))
-+                    )) {
-+                    this.connection.send(ClientboundMoveVehiclePacket.fromEntity(entity));
++                    !serverLevel.areChunksLoadedForMove(this.player.getBoundingBox().expandTowards(new Vec3(toX, toY, toZ).subtract(this.player.position()))) ||
++                        !serverLevel.areChunksLoadedForMove(rootVehicle.getBoundingBox().expandTowards(new Vec3(toX, toY, toZ).subtract(rootVehicle.position())))
++                )) {
++                    this.connection.send(ClientboundMoveVehiclePacket.fromEntity(rootVehicle));
 +                    return;
 +                }
 +                // Paper end - Prevent moving into unloaded chunks
-+
-+                if (d10 - d9 > Math.max(100.0D, Math.pow((double) (org.spigotmc.SpigotConfig.movedTooQuicklyMultiplier * (float) i * speed), 2)) && !this.isSingleplayerOwner()) {
-+                // CraftBukkit end
-                     ServerGamePacketListenerImpl.LOGGER.warn("{} (vehicle of {}) moved too quickly! {},{},{}", new Object[]{entity.getName().getString(), this.player.getName().getString(), d6, d7, d8});
-                     this.send(ClientboundMoveVehiclePacket.fromEntity(entity));
-                     return;
-@@ -423,9 +581,9 @@
- 
-                 boolean flag = worldserver.noCollision(entity, entity.getBoundingBox().deflate(0.0625D));
- 
--                d6 = d3 - this.vehicleLastGoodX;
--                d7 = d4 - this.vehicleLastGoodY;
--                d8 = d5 - this.vehicleLastGoodZ;
-+                d6 = d3 - this.vehicleLastGoodX; // Paper - diff on change, used for checking large move vectors above
-+                d7 = d4 - this.vehicleLastGoodY; // Paper - diff on change, used for checking large move vectors above
-+                d8 = d5 - this.vehicleLastGoodZ; // Paper - diff on change, used for checking large move vectors above
-                 boolean flag1 = entity.verticalCollisionBelow;
- 
-                 if (entity instanceof LivingEntity) {
-@@ -449,19 +607,72 @@
-                 d10 = d6 * d6 + d7 * d7 + d8 * d8;
-                 boolean flag2 = false;
- 
--                if (d10 > 0.0625D) {
-+                if (d10 > org.spigotmc.SpigotConfig.movedWronglyThreshold) { // Spigot
-                     flag2 = true;
-                     ServerGamePacketListenerImpl.LOGGER.warn("{} (vehicle of {}) moved wrongly! {}", new Object[]{entity.getName().getString(), this.player.getName().getString(), Math.sqrt(d10)});
++                if (d7 - d6 > Math.max(100.0, Math.pow((double) (org.spigotmc.SpigotConfig.movedTooQuicklyMultiplier * (float) i * speed), 2)) && !this.isSingleplayerOwner()) {
++                    // CraftBukkit end
+                     LOGGER.warn(
+                         "{} (vehicle of {}) moved too quickly! {},{},{}", rootVehicle.getName().getString(), this.player.getName().getString(), d3, d4, d5
+                     );
+@@ -417,15 +_,16 @@
                  }
  
-                 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));
+                 boolean flag = serverLevel.noCollision(rootVehicle, rootVehicle.getBoundingBox().deflate(0.0625));
+-                d3 = d - this.vehicleLastGoodX;
+-                d4 = d1 - this.vehicleLastGoodY;
+-                d5 = d2 - this.vehicleLastGoodZ;
++                d3 = d - this.vehicleLastGoodX; // Paper - diff on change, used for checking large move vectors above
++                d4 = d1 - this.vehicleLastGoodY; // Paper - diff on change, used for checking large move vectors above
++                d5 = d2 - this.vehicleLastGoodZ; // Paper - diff on change, used for checking large move vectors above
+                 boolean flag1 = rootVehicle.verticalCollisionBelow;
+                 if (rootVehicle instanceof LivingEntity livingEntity && livingEntity.onClimbable()) {
+                     livingEntity.resetFallDistance();
+                 }
  
+                 rootVehicle.move(MoverType.PLAYER, new Vec3(d3, d4, d5));
++                double verticalDelta = d4; // Paper - Decompile fix, was named d11 previously, is now gone in the source
+                 d3 = d - rootVehicle.getX();
+                 d4 = d1 - rootVehicle.getY();
+                 if (d4 > -0.5 || d4 < 0.5) {
+@@ -435,18 +_,71 @@
+                 d5 = d2 - rootVehicle.getZ();
+                 d7 = d3 * d3 + d4 * d4 + d5 * d5;
+                 boolean flag2 = false;
+-                if (d7 > 0.0625) {
++                if (d7 > org.spigotmc.SpigotConfig.movedWronglyThreshold) { // Spigot
+                     flag2 = true;
+                     LOGGER.warn("{} (vehicle of {}) moved wrongly! {}", rootVehicle.getName().getString(), this.player.getName().getString(), Math.sqrt(d7));
+                 }
+ 
+                 rootVehicle.absMoveTo(d, d1, d2, f, f1);
++                this.player.absMoveTo(d, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit
+                 boolean flag3 = serverLevel.noCollision(rootVehicle, rootVehicle.getBoundingBox().deflate(0.0625));
                  if (flag && (flag2 || !flag3)) {
-                     entity.absMoveTo(d0, d1, d2, f, f1);
-+                    this.player.absMoveTo(d0, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit
-                     this.send(ClientboundMoveVehiclePacket.fromEntity(entity));
+                     rootVehicle.absMoveTo(x, y, z, f, f1);
++                    this.player.absMoveTo(x, y, z, this.player.getYRot(), this.player.getXRot()); // CraftBukkit
+                     this.send(ClientboundMoveVehiclePacket.fromEntity(rootVehicle));
                      return;
-+                }
+                 }
 +
 +                // CraftBukkit start - fire PlayerMoveEvent
-+                Player player = this.getCraftPlayer();
++                org.bukkit.entity.Player player = this.getCraftPlayer();
 +                if (!this.hasMoved) {
 +                    this.lastPosX = prevX;
 +                    this.lastPosY = prevY;
@@ -407,32 +344,44 @@
 +                        this.justTeleported = false;
 +                        return;
 +                    }
-                 }
++                }
 +                // CraftBukkit end
  
                  this.player.serverLevel().getChunkSource().move(this.player);
-                 entity.recordMovementThroughBlocks(new Vec3(d0, d1, d2), entity.position());
-@@ -489,16 +700,17 @@
+                 rootVehicle.recordMovementThroughBlocks(new Vec3(x, y, z), rootVehicle.position());
+@@ -455,7 +_,7 @@
+                 rootVehicle.setOnGroundWithMovement(packet.onGround(), vec3);
+                 rootVehicle.doCheckFallDamage(vec3.x, vec3.y, vec3.z, packet.onGround());
+                 this.player.checkMovementStatistics(vec3.x, vec3.y, vec3.z);
+-                this.clientVehicleIsFloating = d4 >= -0.03125
++                this.clientVehicleIsFloating = verticalDelta >= -0.03125 // Paper - Decompile fix
+                     && !flag1
+                     && !this.server.isFlightAllowed()
+                     && !rootVehicle.isNoGravity()
+@@ -478,12 +_,12 @@
          PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
          if (packet.getId() == this.awaitingTeleport) {
              if (this.awaitingPositionFromClient == null) {
--                this.disconnect((Component) Component.translatable("multiplayer.disconnect.invalid_player_movement"));
-+                this.disconnect((Component) Component.translatable("multiplayer.disconnect.invalid_player_movement"), org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PLAYER_MOVEMENT); // Paper - kick event cause
+-                this.disconnect(Component.translatable("multiplayer.disconnect.invalid_player_movement"));
++                this.disconnect(Component.translatable("multiplayer.disconnect.invalid_player_movement"), org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PLAYER_MOVEMENT); // Paper - kick event cause
                  return;
              }
  
--            this.player.absMoveTo(this.awaitingPositionFromClient.x, this.awaitingPositionFromClient.y, this.awaitingPositionFromClient.z, this.player.getYRot(), this.player.getXRot());
-+            this.player.moveTo(this.awaitingPositionFromClient.x, this.awaitingPositionFromClient.y, this.awaitingPositionFromClient.z, this.player.getYRot(), this.player.getXRot()); // Paper - Fix Entity Teleportation and cancel velocity if teleported
-             this.lastGoodX = this.awaitingPositionFromClient.x;
-             this.lastGoodY = this.awaitingPositionFromClient.y;
+             this.player
+-                .absMoveTo(
++                .moveTo( // Paper - Fix Entity Teleportation and cancel velocity if teleported
+                     this.awaitingPositionFromClient.x,
+                     this.awaitingPositionFromClient.y,
+                     this.awaitingPositionFromClient.z,
+@@ -495,6 +_,7 @@
              this.lastGoodZ = this.awaitingPositionFromClient.z;
              this.player.hasChangedDimension();
              this.awaitingPositionFromClient = null;
 +            this.player.serverLevel().getChunkSource().move(this.player); // CraftBukkit
          }
- 
      }
-@@ -528,6 +740,7 @@
+ 
+@@ -521,6 +_,7 @@
      @Override
      public void handleRecipeBookChangeSettingsPacket(ServerboundRecipeBookChangeSettingsPacket packet) {
          PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
@@ -440,14 +389,15 @@
          this.player.getRecipeBook().setBookSetting(packet.getBookType(), packet.isOpen(), packet.isFiltering());
      }
  
-@@ -545,21 +758,104 @@
- 
+@@ -536,25 +_,110 @@
+         }
      }
  
 +    // Paper start - AsyncTabCompleteEvent
 +    private static final java.util.concurrent.ExecutorService TAB_COMPLETE_EXECUTOR = java.util.concurrent.Executors.newFixedThreadPool(4,
-+        new com.google.common.util.concurrent.ThreadFactoryBuilder().setDaemon(true).setNameFormat("Async Tab Complete Thread - #%d").setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(net.minecraft.server.MinecraftServer.LOGGER)).build());
++        new com.google.common.util.concurrent.ThreadFactoryBuilder().setDaemon(true).setNameFormat("Async Tab Complete Thread - #%d").setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(MinecraftServer.LOGGER)).build());
 +    // Paper end - AsyncTabCompleteEvent
++
      @Override
      public void handleCustomCommandSuggestions(ServerboundCommandSuggestionPacket packet) {
 -        PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
@@ -475,12 +425,13 @@
 +    }
 +
 +    private void handleCustomCommandSuggestions0(final ServerboundCommandSuggestionPacket packet) {
-         StringReader stringreader = new StringReader(packet.getCommand());
- 
-         if (stringreader.canRead() && stringreader.peek() == '/') {
-             stringreader.skip();
++        // Paper end - AsyncTabCompleteEvent
+         StringReader stringReader = new StringReader(packet.getCommand());
+         if (stringReader.canRead() && stringReader.peek() == '/') {
+             stringReader.skip();
          }
  
++        // Paper start - AsyncTabCompleteEvent
 +        final com.destroystokyo.paper.event.server.AsyncTabCompleteEvent event = new com.destroystokyo.paper.event.server.AsyncTabCompleteEvent(this.getCraftPlayer(), packet.getCommand(), true, null);
 +        event.callEvent();
 +        final List<com.destroystokyo.paper.event.server.AsyncTabCompleteEvent.Completion> completions = event.isCancelled() ? com.google.common.collect.ImmutableList.of() : event.completions();
@@ -491,9 +442,9 @@
 +            }
 +
 +            // This needs to be on main
-+            this.server.scheduleOnMain(() -> this.sendServerSuggestions(packet, stringreader));
++            this.server.scheduleOnMain(() -> this.sendServerSuggestions(packet, stringReader));
 +        } else if (!completions.isEmpty()) {
-+            final com.mojang.brigadier.suggestion.SuggestionsBuilder builder0 = new com.mojang.brigadier.suggestion.SuggestionsBuilder(packet.getCommand(), stringreader.getTotalLength());
++            final com.mojang.brigadier.suggestion.SuggestionsBuilder builder0 = new com.mojang.brigadier.suggestion.SuggestionsBuilder(packet.getCommand(), stringReader.getTotalLength());
 +            final com.mojang.brigadier.suggestion.SuggestionsBuilder builder = builder0.createOffset(builder0.getInput().lastIndexOf(' ') + 1);
 +            for (final com.destroystokyo.paper.event.server.AsyncTabCompleteEvent.Completion completion : completions) {
 +                final Integer intSuggestion = com.google.common.primitives.Ints.tryParse(completion.suggestion());
@@ -519,37 +470,42 @@
 +    }
 +    // Paper end - brig API
 +
-+    private void sendServerSuggestions(final ServerboundCommandSuggestionPacket packet, final StringReader stringreader) {
++    private void sendServerSuggestions(final ServerboundCommandSuggestionPacket packet, final StringReader stringReader) {
 +        // Paper end - AsyncTabCompleteEvent
-         ParseResults<CommandSourceStack> parseresults = this.server.getCommands().getDispatcher().parse(stringreader, this.player.createCommandSourceStack());
+         ParseResults<CommandSourceStack> parseResults = this.server.getCommands().getDispatcher().parse(stringReader, this.player.createCommandSourceStack());
 +        // Paper start - Handle non-recoverable exceptions
-+        if (!parseresults.getExceptions().isEmpty()
-+            && parseresults.getExceptions().values().stream().anyMatch(e -> e instanceof io.papermc.paper.brigadier.TagParseCommandSyntaxException)) {
++        if (!parseResults.getExceptions().isEmpty()
++            && parseResults.getExceptions().values().stream().anyMatch(e -> e instanceof io.papermc.paper.brigadier.TagParseCommandSyntaxException)) {
 +            this.disconnect(Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM);
 +            return;
 +        }
 +        // Paper end - Handle non-recoverable exceptions
- 
-         this.server.getCommands().getDispatcher().getCompletionSuggestions(parseresults).thenAccept((suggestions) -> {
--            Suggestions suggestions1 = suggestions.getList().size() <= 1000 ? suggestions : new Suggestions(suggestions.getRange(), suggestions.getList().subList(0, 1000));
--
--            this.send(new ClientboundCommandSuggestionsPacket(packet.getId(), suggestions1));
-+            // Paper start - Don't tab-complete namespaced commands if send-namespaced is false
-+            if (!org.spigotmc.SpigotConfig.sendNamespaced && suggestions.getRange().getStart() <= 1) {
-+                suggestions.getList().removeIf(suggestion -> suggestion.getText().contains(":"));
-+            }
-+            // Paper end - Don't tab-complete namespaced commands if send-namespaced is false
-+            // Paper start - Brigadier API
-+            com.destroystokyo.paper.event.brigadier.AsyncPlayerSendSuggestionsEvent suggestEvent = new com.destroystokyo.paper.event.brigadier.AsyncPlayerSendSuggestionsEvent(this.getCraftPlayer(), suggestions, packet.getCommand());
-+            suggestEvent.setCancelled(suggestions.isEmpty());
-+            if (suggestEvent.callEvent()) {
-+                this.send(new ClientboundCommandSuggestionsPacket(packet.getId(), limitTo(suggestEvent.getSuggestions(), ServerGamePacketListenerImpl.MAX_COMMAND_SUGGESTIONS)));
-+            }
-+            // Paper end - Brigadier API
-         });
+         this.server
+             .getCommands()
+             .getDispatcher()
+             .getCompletionSuggestions(parseResults)
+             .thenAccept(
+                 suggestions -> {
+-                    Suggestions suggestions1 = suggestions.getList().size() <= 1000
+-                        ? suggestions
+-                        : new Suggestions(suggestions.getRange(), suggestions.getList().subList(0, 1000));
+-                    this.send(new ClientboundCommandSuggestionsPacket(packet.getId(), suggestions1));
++                    // Paper start - Don't tab-complete namespaced commands if send-namespaced is false
++                    if (!org.spigotmc.SpigotConfig.sendNamespaced && suggestions.getRange().getStart() <= 1) {
++                        suggestions.getList().removeIf(suggestion -> suggestion.getText().contains(":"));
++                    }
++                    // Paper end - Don't tab-complete namespaced commands if send-namespaced is false
++                    // Paper start - Brigadier API
++                    com.destroystokyo.paper.event.brigadier.AsyncPlayerSendSuggestionsEvent suggestEvent = new com.destroystokyo.paper.event.brigadier.AsyncPlayerSendSuggestionsEvent(this.getCraftPlayer(), suggestions, packet.getCommand());
++                    suggestEvent.setCancelled(suggestions.isEmpty());
++                    if (suggestEvent.callEvent()) {
++                        this.send(new ClientboundCommandSuggestionsPacket(packet.getId(), limitTo(suggestEvent.getSuggestions(), ServerGamePacketListenerImpl.MAX_COMMAND_SUGGESTIONS)));
++                    }
++                    // Paper end - Brigadier API
+                 }
+             );
      }
- 
-@@ -568,7 +864,7 @@
+@@ -564,7 +_,7 @@
          PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
          if (!this.server.isCommandBlockEnabled()) {
              this.player.sendSystemMessage(Component.translatable("advMode.notEnabled"));
@@ -557,8 +513,8 @@
 +        } else if (!this.player.canUseGameMasterBlocks() && (!this.player.isCreative() || !this.player.getBukkitEntity().hasPermission("minecraft.commandblock"))) { // Paper - command block permission
              this.player.sendSystemMessage(Component.translatable("advMode.notAllowed"));
          } else {
-             BaseCommandBlock commandblocklistenerabstract = null;
-@@ -635,7 +931,7 @@
+             BaseCommandBlock baseCommandBlock = null;
+@@ -620,7 +_,7 @@
          PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
          if (!this.server.isCommandBlockEnabled()) {
              this.player.sendSystemMessage(Component.translatable("advMode.notEnabled"));
@@ -566,61 +522,60 @@
 +        } else if (!this.player.canUseGameMasterBlocks() && (!this.player.isCreative() || !this.player.getBukkitEntity().hasPermission("minecraft.commandblock"))) { // Paper - command block permission
              this.player.sendSystemMessage(Component.translatable("advMode.notAllowed"));
          } else {
-             BaseCommandBlock commandblocklistenerabstract = packet.getCommandBlock(this.player.level());
-@@ -668,7 +964,7 @@
-                 ItemStack itemstack = iblockdata.getCloneItemStack(worldserver, blockposition, flag);
- 
-                 if (!itemstack.isEmpty()) {
+             BaseCommandBlock commandBlock = packet.getCommandBlock(this.player.level());
+@@ -648,7 +_,7 @@
+                 boolean flag = this.player.hasInfiniteMaterials() && packet.includeData();
+                 ItemStack cloneItemStack = blockState.getCloneItemStack(serverLevel, blockPos, flag);
+                 if (!cloneItemStack.isEmpty()) {
 -                    if (flag) {
 +                    if (flag && this.player.getBukkitEntity().hasPermission("minecraft.nbt.copy")) { // Spigot
-                         ServerGamePacketListenerImpl.addBlockDataToItem(iblockdata, worldserver, blockposition, itemstack);
+                         addBlockDataToItem(blockState, serverLevel, blockPos, cloneItemStack);
                      }
  
-@@ -712,15 +1008,25 @@
+@@ -685,14 +_,24 @@
          if (stack.isItemEnabled(this.player.level().enabledFeatures())) {
-             Inventory playerinventory = this.player.getInventory();
-             int i = playerinventory.findSlotMatchingItem(stack);
+             Inventory inventory = this.player.getInventory();
+             int i = inventory.findSlotMatchingItem(stack);
 +            // Paper start - Add PlayerPickItemEvent
 +            final int sourceSlot = i;
-+            final int targetSlot = Inventory.isHotbarSlot(sourceSlot) ? sourceSlot : playerinventory.getSuitableHotbarSlot();
-+            final Player bukkitPlayer = this.player.getBukkitEntity();
++            final int targetSlot = Inventory.isHotbarSlot(sourceSlot) ? sourceSlot : inventory.getSuitableHotbarSlot();
++            final org.bukkit.entity.Player bukkitPlayer = this.player.getBukkitEntity();
 +            final io.papermc.paper.event.player.PlayerPickItemEvent event = new io.papermc.paper.event.player.PlayerPickItemEvent(bukkitPlayer, targetSlot, sourceSlot);
 +            if (!event.callEvent()) {
 +                return;
 +            }
 +            i = event.getSourceSlot();
- 
++            // Paper end - Add PlayerPickItemEvent
              if (i != -1) {
 -                if (Inventory.isHotbarSlot(i)) {
--                    playerinventory.selected = i;
-+                if (Inventory.isHotbarSlot(i) && Inventory.isHotbarSlot(event.getTargetSlot())) {
-+                    playerinventory.selected = event.getTargetSlot();
+-                    inventory.selected = i;
++                if (Inventory.isHotbarSlot(i) && Inventory.isHotbarSlot(event.getTargetSlot())) { // Paper - Add PlayerPickItemEvent
++                    inventory.selected = event.getTargetSlot(); // Paper - Add PlayerPickItemEvent
                  } else {
--                    playerinventory.pickSlot(i);
-+                    playerinventory.pickSlot(i, event.getTargetSlot());
+-                    inventory.pickSlot(i);
++                    inventory.pickSlot(i, event.getTargetSlot()); // Paper - Add PlayerPickItemEvent
                  }
              } else if (this.player.hasInfiniteMaterials()) {
--                playerinventory.addAndPickItem(stack);
-+                playerinventory.addAndPickItem(stack, event.getTargetSlot());
-+                // Paper end - Add PlayerPickItemEvent
+-                inventory.addAndPickItem(stack);
++                inventory.addAndPickItem(stack, event.getTargetSlot()); // Paper - Add PlayerPickItemEvent
              }
  
-             this.player.connection.send(new ClientboundSetHeldSlotPacket(playerinventory.selected));
-@@ -866,6 +1172,13 @@
-         AbstractContainerMenu container = this.player.containerMenu;
- 
-         if (container instanceof MerchantMenu containermerchant) {
+             this.player.connection.send(new ClientboundSetHeldSlotPacket(inventory.selected));
+@@ -814,6 +_,13 @@
+         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
+         int item = packet.getItem();
+         if (this.player.containerMenu instanceof MerchantMenu merchantMenu) {
 +            // CraftBukkit start
-+            final org.bukkit.event.inventory.TradeSelectEvent tradeSelectEvent = CraftEventFactory.callTradeSelectEvent(this.player, i, containermerchant);
++            final org.bukkit.event.inventory.TradeSelectEvent tradeSelectEvent = CraftEventFactory.callTradeSelectEvent(this.player, item, merchantMenu);
 +            if (tradeSelectEvent.isCancelled()) {
-+                this.player.getBukkitEntity().updateInventory();
++                this.player.containerMenu.sendAllDataToRemote();
 +                return;
 +            }
 +            // CraftBukkit end
-             if (!containermerchant.stillValid(this.player)) {
-                 ServerGamePacketListenerImpl.LOGGER.debug("Player {} interacted with invalid menu {}", this.player, containermerchant);
+             if (!merchantMenu.stillValid(this.player)) {
+                 LOGGER.debug("Player {} interacted with invalid menu {}", this.player, merchantMenu);
                  return;
-@@ -879,6 +1192,51 @@
+@@ -826,6 +_,51 @@
  
      @Override
      public void handleEditBook(ServerboundEditBookPacket packet) {
@@ -669,52 +624,43 @@
 +        }
 +        this.lastBookTick = MinecraftServer.currentTick;
 +        // CraftBukkit end
-         int i = packet.slot();
- 
-         if (Inventory.isHotbarSlot(i) || i == 40) {
-@@ -899,12 +1257,16 @@
+         int slot = packet.slot();
+         if (Inventory.isHotbarSlot(slot) || slot == 40) {
+             List<String> list = Lists.newArrayList();
+@@ -840,10 +_,14 @@
      }
  
-     private void updateBookContents(List<FilteredText> pages, int slotId) {
--        ItemStack itemstack = this.player.getInventory().getItem(slotId);
+     private void updateBookContents(List<FilteredText> pages, int index) {
+-        ItemStack item = this.player.getInventory().getItem(index);
 +        // CraftBukkit start
-+        ItemStack handItem = this.player.getInventory().getItem(slotId);
-+        ItemStack itemstack = handItem.copy();
++        ItemStack handItem = this.player.getInventory().getItem(index);
++        ItemStack item = handItem.copy();
 +        // CraftBukkit end
- 
-         if (itemstack.has(DataComponents.WRITABLE_BOOK_CONTENT)) {
-             List<Filterable<String>> list1 = pages.stream().map(this::filterableFromOutgoing).toList();
- 
-             itemstack.set(DataComponents.WRITABLE_BOOK_CONTENT, new WritableBookContent(list1));
-+            this.player.getInventory().setItem(slotId, CraftEventFactory.handleEditBookEvent(this.player, slotId, handItem, itemstack)); // CraftBukkit // Paper - Don't ignore result (see other callsite for handleEditBookEvent)
+         if (item.has(DataComponents.WRITABLE_BOOK_CONTENT)) {
+             List<Filterable<String>> list = pages.stream().map(this::filterableFromOutgoing).toList();
+             item.set(DataComponents.WRITABLE_BOOK_CONTENT, new WritableBookContent(list));
++            this.player.getInventory().setItem(index, CraftEventFactory.handleEditBookEvent(this.player, index, handItem, item)); // CraftBukkit // Paper - Don't ignore result (see other callsite for handleEditBookEvent)
          }
      }
  
-@@ -915,12 +1277,13 @@
-             ItemStack itemstack1 = itemstack.transmuteCopy(Items.WRITTEN_BOOK);
- 
-             itemstack1.remove(DataComponents.WRITABLE_BOOK_CONTENT);
--            List<Filterable<Component>> list1 = pages.stream().map((filteredtext1) -> {
-+            List<Filterable<Component>> list1 = (List<Filterable<Component>>) (List) pages.stream().map((filteredtext1) -> { // CraftBukkit - decompile error
-                 return this.filterableFromOutgoing(filteredtext1).map(Component::literal);
-             }).toList();
- 
-             itemstack1.set(DataComponents.WRITTEN_BOOK_CONTENT, new WrittenBookContent(this.filterableFromOutgoing(title), this.player.getName().getString(), 0, list1, true));
--            this.player.getInventory().setItem(slotId, itemstack1);
-+            CraftEventFactory.handleEditBookEvent(this.player, slotId, itemstack, itemstack1); // CraftBukkit
-+            this.player.getInventory().setItem(slotId, itemstack); // CraftBukkit - event factory updates the hand book
+@@ -857,7 +_,8 @@
+                 DataComponents.WRITTEN_BOOK_CONTENT,
+                 new WrittenBookContent(this.filterableFromOutgoing(title), this.player.getName().getString(), 0, list, true)
+             );
+-            this.player.getInventory().setItem(index, itemStack);
++            CraftEventFactory.handleEditBookEvent(this.player, index, item, itemStack); // CraftBukkit
++            this.player.getInventory().setItem(index, item); // CraftBukkit - event factory updates the hand book
          }
      }
  
-@@ -978,26 +1341,34 @@
+@@ -901,24 +_,32 @@
      public void handleMovePlayer(ServerboundMovePlayerPacket packet) {
          PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
-         if (ServerGamePacketListenerImpl.containsInvalidValues(packet.getX(0.0D), packet.getY(0.0D), packet.getZ(0.0D), packet.getYRot(0.0F), packet.getXRot(0.0F))) {
--            this.disconnect((Component) Component.translatable("multiplayer.disconnect.invalid_player_movement"));
-+            this.disconnect((Component) Component.translatable("multiplayer.disconnect.invalid_player_movement"), org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PLAYER_MOVEMENT); // Paper - kick event cause
+         if (containsInvalidValues(packet.getX(0.0), packet.getY(0.0), packet.getZ(0.0), packet.getYRot(0.0F), packet.getXRot(0.0F))) {
+-            this.disconnect(Component.translatable("multiplayer.disconnect.invalid_player_movement"));
++            this.disconnect(Component.translatable("multiplayer.disconnect.invalid_player_movement"), org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PLAYER_MOVEMENT); // Paper - kick event cause
          } else {
-             ServerLevel worldserver = this.player.serverLevel();
- 
+             ServerLevel serverLevel = this.player.serverLevel();
 -            if (!this.player.wonGame) {
 +            if (!this.player.wonGame && !this.player.isImmobile()) { // CraftBukkit
                  if (this.tickCount == 0) {
@@ -722,17 +668,16 @@
                  }
  
                  if (!this.updateAwaitingTeleport() && this.player.hasClientLoaded()) {
--                    double d0 = ServerGamePacketListenerImpl.clampHorizontal(packet.getX(this.player.getX()));
--                    double d1 = ServerGamePacketListenerImpl.clampVertical(packet.getY(this.player.getY()));
--                    double d2 = ServerGamePacketListenerImpl.clampHorizontal(packet.getZ(this.player.getZ()));
+-                    double d = clampHorizontal(packet.getX(this.player.getX()));
+-                    double d1 = clampVertical(packet.getY(this.player.getY()));
+-                    double d2 = clampHorizontal(packet.getZ(this.player.getZ()));
 -                    float f = Mth.wrapDegrees(packet.getYRot(this.player.getYRot()));
 -                    float f1 = Mth.wrapDegrees(packet.getXRot(this.player.getXRot()));
-+                    double d0 = ServerGamePacketListenerImpl.clampHorizontal(packet.getX(this.player.getX())); final double toX = d0; // Paper - OBFHELPER
-+                    double d1 = ServerGamePacketListenerImpl.clampVertical(packet.getY(this.player.getY())); final double toY = d1; // Paper - OBFHELPER
-+                    double d2 = ServerGamePacketListenerImpl.clampHorizontal(packet.getZ(this.player.getZ())); final double toZ = d2; // Paper - OBFHELPER
++                    double d = clampHorizontal(packet.getX(this.player.getX())); final double toX = d; // Paper - OBFHELPER
++                    double d1 = clampVertical(packet.getY(this.player.getY())); final double toY = d1; // Paper - OBFHELPER
++                    double d2 = clampHorizontal(packet.getZ(this.player.getZ())); final double toZ = d2; // Paper - OBFHELPER
 +                    float f = Mth.wrapDegrees(packet.getYRot(this.player.getYRot())); final float toYaw = f; // Paper - OBFHELPER
 +                    float f1 = Mth.wrapDegrees(packet.getXRot(this.player.getXRot())); final float toPitch = f1; // Paper - OBFHELPER
- 
                      if (this.player.isPassenger()) {
                          this.player.absMoveTo(this.player.getX(), this.player.getY(), this.player.getZ(), f, f1);
                          this.player.serverLevel().getChunkSource().move(this.player);
@@ -745,43 +690,43 @@
 +                        float prevYaw = this.player.getYRot();
 +                        float prevPitch = this.player.getXRot();
 +                        // CraftBukkit end
-                         double d3 = this.player.getX();
-                         double d4 = this.player.getY();
-                         double d5 = this.player.getZ();
-@@ -1005,7 +1376,16 @@
-                         double d7 = d1 - this.firstGoodY;
-                         double d8 = d2 - this.firstGoodZ;
-                         double d9 = this.player.getDeltaMovement().lengthSqr();
--                        double d10 = d6 * d6 + d7 * d7 + d8 * d8;
+                         double x = this.player.getX();
+                         double y = this.player.getY();
+                         double z = this.player.getZ();
+@@ -927,6 +_,16 @@
+                         double d5 = d2 - this.firstGoodZ;
+                         double d6 = this.player.getDeltaMovement().lengthSqr();
+                         double d7 = d3 * d3 + d4 * d4 + d5 * d5;
 +                        // Paper start - fix large move vectors killing the server
 +                        double currDeltaX = toX - prevX;
 +                        double currDeltaY = toY - prevY;
 +                        double currDeltaZ = toZ - prevZ;
-+                        double d10 = Math.max(d6 * d6 + d7 * d7 + d8 * d8, (currDeltaX * currDeltaX + currDeltaY * currDeltaY + currDeltaZ * currDeltaZ) - 1);
-+                        double otherFieldX = d0 - this.lastGoodX;
++                        d7 = Math.max(d7, (currDeltaX * currDeltaX + currDeltaY * currDeltaY + currDeltaZ * currDeltaZ) - 1);
++                        double otherFieldX = d - this.lastGoodX;
 +                        double otherFieldY = d1 - this.lastGoodY;
 +                        double otherFieldZ = d2 - this.lastGoodZ;
-+                        d10 = Math.max(d10, (otherFieldX * otherFieldX + otherFieldY * otherFieldY + otherFieldZ * otherFieldZ) - 1);
++                        d7 = Math.max(d7, (otherFieldX * otherFieldX + otherFieldY * otherFieldY + otherFieldZ * otherFieldZ) - 1);
 +                        // Paper end - fix large move vectors killing the server
- 
                          if (this.player.isSleeping()) {
-                             if (d10 > 1.0D) {
-@@ -1019,36 +1399,106 @@
-                                 ++this.receivedMovePacketCount;
+                             if (d7 > 1.0) {
+                                 this.teleport(this.player.getX(), this.player.getY(), this.player.getZ(), f, f1);
+@@ -936,32 +_,105 @@
+                             if (serverLevel.tickRateManager().runsNormally()) {
+                                 this.receivedMovePacketCount++;
                                  int i = this.receivedMovePacketCount - this.knownMovePacketCount;
- 
 -                                if (i > 5) {
++
 +                                // CraftBukkit start - handle custom speeds and skipped ticks
 +                                this.allowedPlayerTicks += (System.currentTimeMillis() / 50) - this.lastTick;
 +                                this.allowedPlayerTicks = Math.max(this.allowedPlayerTicks, 1);
 +                                this.lastTick = (int) (System.currentTimeMillis() / 50);
 +
 +                                if (i > Math.max(this.allowedPlayerTicks, 5)) {
-                                     ServerGamePacketListenerImpl.LOGGER.debug("{} is sending move packets too frequently ({} packets since last tick)", this.player.getName().getString(), i);
+                                     LOGGER.debug("{} is sending move packets too frequently ({} packets since last tick)", this.player.getName().getString(), i);
                                      i = 1;
                                  }
  
-+                                if (packet.hasRot || d10 > 0) {
++                                if (packet.hasRot || d7 > 0) {
 +                                    this.allowedPlayerTicks -= 1;
 +                                } else {
 +                                    this.allowedPlayerTicks = 20;
@@ -793,7 +738,7 @@
 +                                    speed = this.player.getAbilities().walkingSpeed * 10f;
 +                                }
 +                                // Paper start - Prevent moving into unloaded chunks
-+                                if (this.player.level().paperConfig().chunks.preventMovingIntoUnloadedChunks && (this.player.getX() != toX || this.player.getZ() != toZ) && !worldserver.areChunksLoadedForMove(this.player.getBoundingBox().expandTowards(new Vec3(toX, toY, toZ).subtract(this.player.position())))) {
++                                if (this.player.level().paperConfig().chunks.preventMovingIntoUnloadedChunks && (this.player.getX() != toX || this.player.getZ() != toZ) && !serverLevel.areChunksLoadedForMove(this.player.getBoundingBox().expandTowards(new Vec3(toX, toY, toZ).subtract(this.player.position())))) {
 +                                    // Paper start - Add fail move event
 +                                    io.papermc.paper.event.player.PlayerFailMoveEvent event = fireFailMove(io.papermc.paper.event.player.PlayerFailMoveEvent.FailReason.MOVED_INTO_UNLOADED_CHUNK,
 +                                        toX, toY, toZ, toYaw, toPitch, false);
@@ -805,21 +750,21 @@
 +                                }
 +                                // Paper end - Prevent moving into unloaded chunks
 +
-                                 if (this.shouldCheckPlayerMovement(flag)) {
-                                     float f2 = flag ? 300.0F : 100.0F;
- 
--                                    if (d10 - d9 > (double) (f2 * (float) i)) {
--                                        ServerGamePacketListenerImpl.LOGGER.warn("{} moved too quickly! {},{},{}", new Object[]{this.player.getName().getString(), d6, d7, d8});
+                                 if (this.shouldCheckPlayerMovement(isFallFlying)) {
+                                     float f2 = isFallFlying ? 300.0F : 100.0F;
+-                                    if (d7 - d6 > f2 * i) {
+-                                        LOGGER.warn("{} moved too quickly! {},{},{}", this.player.getName().getString(), d3, d4, d5);
 -                                        this.teleport(this.player.getX(), this.player.getY(), this.player.getZ(), this.player.getYRot(), this.player.getXRot());
 -                                        return;
-+                                    if (d10 - d9 > Math.max(f2, Math.pow((double) (org.spigotmc.SpigotConfig.movedTooQuicklyMultiplier * (float) i * speed), 2))) {
-+                                    // CraftBukkit end
++                                    if (d7 - d6 > Math.max(f2, Mth.square(org.spigotmc.SpigotConfig.movedTooQuicklyMultiplier * (float) i * speed))) {
++                                        // CraftBukkit end
 +                                        // Paper start - Add fail move event
 +                                        io.papermc.paper.event.player.PlayerFailMoveEvent event = fireFailMove(io.papermc.paper.event.player.PlayerFailMoveEvent.FailReason.MOVED_TOO_QUICKLY,
 +                                            toX, toY, toZ, toYaw, toPitch, true);
 +                                        if (!event.isAllowed()) {
-+                                            if (event.getLogWarning())
-+                                                ServerGamePacketListenerImpl.LOGGER.warn("{} moved too quickly! {},{},{}", new Object[]{this.player.getName().getString(), d6, d7, d8});
++                                            if (event.getLogWarning()) {
++                                                LOGGER.warn("{} moved too quickly! {},{},{}", this.player.getName().getString(), d3, d4, d5);
++                                            }
 +                                            this.teleport(this.player.getX(), this.player.getY(), this.player.getZ(), this.player.getYRot(), this.player.getXRot());
 +                                            return;
 +                                        }
@@ -828,21 +773,19 @@
                                  }
                              }
  
-                             AABB axisalignedbb = this.player.getBoundingBox();
- 
--                            d6 = d0 - this.lastGoodX;
--                            d7 = d1 - this.lastGoodY;
--                            d8 = d2 - this.lastGoodZ;
-+                            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
-+                            d8 = d2 - this.lastGoodZ; // Paper - diff on change, used for checking large move vectors above
-                             boolean flag1 = d7 > 0.0D;
- 
-                             if (this.player.onGround() && !packet.isOnGround() && flag1) {
+                             AABB boundingBox = this.player.getBoundingBox();
+-                            d3 = d - this.lastGoodX;
+-                            d4 = d1 - this.lastGoodY;
+-                            d5 = d2 - this.lastGoodZ;
++                            d3 = d - this.lastGoodX; // Paper - diff on change, used for checking large move vectors above
++                            d4 = d1 - this.lastGoodY; // Paper - diff on change, used for checking large move vectors above
++                            d5 = d2 - this.lastGoodZ; // Paper - diff on change, used for checking large move vectors above
+                             boolean flag = d4 > 0.0;
+                             if (this.player.onGround() && !packet.isOnGround() && flag) {
 -                                this.player.jumpFromGround();
 +                                // Paper start - Add PlayerJumpEvent
-+                                Player player = this.getCraftPlayer();
-+                                Location from = new Location(player.getWorld(), lastPosX, lastPosY, lastPosZ, lastYaw, lastPitch); // Get the Players previous Event location.
++                                org.bukkit.entity.Player player = this.getCraftPlayer();
++                                Location from = new Location(player.getWorld(), this.lastPosX, this.lastPosY, this.lastPosZ, this.lastYaw, this.lastPitch); // Get the Players previous Event location.
 +                                Location to = player.getLocation().clone(); // Start off the To location as the Players current location.
 +
 +                                // If the packet contains movement information then we update the To location with the correct XYZ.
@@ -870,43 +813,50 @@
 +                                // Paper end - Add PlayerJumpEvent
                              }
  
-                             boolean flag2 = this.player.verticalCollisionBelow;
- 
-                             this.player.move(MoverType.PLAYER, new Vec3(d6, d7, d8));
+                             boolean flag1 = this.player.verticalCollisionBelow;
+                             this.player.move(MoverType.PLAYER, new Vec3(d3, d4, d5));
 +                            this.player.onGround = packet.isOnGround(); // CraftBukkit - SPIGOT-5810, SPIGOT-5835, SPIGOT-6828: reset by this.player.move
 +                            // Paper start - prevent position desync
 +                            if (this.awaitingPositionFromClient != null) {
 +                                return; // ... thanks Mojang for letting move calls teleport across dimensions.
 +                            }
 +                            // Paper end - prevent position desync
-                             double d11 = d7;
++                            double verticalDelta = d4; // Paper - Decompile fix, was named d11 previously, is now gone in the source
+                             d3 = d - this.player.getX();
+                             d4 = d1 - this.player.getY();
+                             if (d4 > -0.5 || d4 < 0.5) {
+@@ -970,23 +_,104 @@
  
-                             d6 = d0 - this.player.getX();
-@@ -1059,17 +1509,100 @@
- 
-                             d8 = d2 - this.player.getZ();
-                             d10 = d6 * d6 + d7 * d7 + d8 * d8;
--                            boolean flag3 = false;
+                             d5 = d2 - this.player.getZ();
+                             d7 = d3 * d3 + d4 * d4 + d5 * d5;
+-                            boolean flag2 = false;
 +                            boolean movedWrongly = false; // Paper - Add fail move event; rename
- 
--                            if (!this.player.isChangingDimension() && d10 > 0.0625D && !this.player.isSleeping() && !this.player.gameMode.isCreative() && this.player.gameMode.getGameModeForPlayer() != GameType.SPECTATOR) {
--                                flag3 = true;
-+                            if (!this.player.isChangingDimension() && d10 > org.spigotmc.SpigotConfig.movedWronglyThreshold && !this.player.isSleeping() && !this.player.gameMode.isCreative() && this.player.gameMode.getGameModeForPlayer() != GameType.SPECTATOR) { // Spigot
+                             if (!this.player.isChangingDimension()
+-                                && d7 > 0.0625
++                                && d7 > org.spigotmc.SpigotConfig.movedWronglyThreshold // Spigot
+                                 && !this.player.isSleeping()
+                                 && !this.player.gameMode.isCreative()
+                                 && this.player.gameMode.getGameModeForPlayer() != GameType.SPECTATOR) {
+-                                flag2 = true;
 +                                // Paper start - Add fail move event
 +                                io.papermc.paper.event.player.PlayerFailMoveEvent event = fireFailMove(io.papermc.paper.event.player.PlayerFailMoveEvent.FailReason.MOVED_WRONGLY,
 +                                    toX, toY, toZ, toYaw, toPitch, true);
 +                                if (!event.isAllowed()) {
 +                                    movedWrongly = true;
 +                                    if (event.getLogWarning())
-+                                // Paper end
-                                 ServerGamePacketListenerImpl.LOGGER.warn("{} moved wrongly!", this.player.getName().getString());
++                                    // Paper end
+                                 LOGGER.warn("{} moved wrongly!", this.player.getName().getString());
+-                            }
+-
+-                            if (this.player.noPhysics
+-                                || this.player.isSleeping()
+-                                || (!flag2 || !serverLevel.noCollision(this.player, boundingBox))
+-                                    && !this.isPlayerCollidingWithAnythingNew(serverLevel, boundingBox, d, d1, d2)) {
 +                                } // Paper
-                             }
- 
--                            if (!this.player.noPhysics && !this.player.isSleeping() && (flag3 && worldserver.noCollision(this.player, axisalignedbb) || this.isPlayerCollidingWithAnythingNew(worldserver, axisalignedbb, d0, d1, d2))) {
--                                this.teleport(d3, d4, d5, f, f1);
++                            }
++
 +                            // 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));
++                            boolean teleportBack = !this.player.noPhysics && !this.player.isSleeping() && (movedWrongly && serverLevel.noCollision(this.player, boundingBox) || this.isPlayerCollidingWithAnythingNew(serverLevel, boundingBox, d, d1, d2));
 +                            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);
@@ -914,16 +864,13 @@
 +                                    teleportBack = false;
 +                                }
 +                            }
-+                            if (teleportBack) {
-+                            // Paper end - Add fail move event
-+                                this.internalTeleport(d3, d4, d5, f, f1); // CraftBukkit - SPIGOT-1807: Don't call teleport event, when the client thinks the player is falling, because the chunks are not loaded on the client yet.
-                                 this.player.doCheckFallDamage(this.player.getX() - d3, this.player.getY() - d4, this.player.getZ() - d5, packet.isOnGround());
-                             } else {
++                            if (!teleportBack) {
++                                // Paper end - Add fail move event
 +                                // CraftBukkit start - fire PlayerMoveEvent
 +                                // Reset to old location first
 +                                this.player.absMoveTo(prevX, prevY, prevZ, prevYaw, prevPitch);
 +
-+                                Player player = this.getCraftPlayer();
++                                org.bukkit.entity.Player player = this.getCraftPlayer();
 +                                if (!this.hasMoved) {
 +                                    this.lastPosX = prevX;
 +                                    this.lastPosY = prevY;
@@ -932,6 +879,7 @@
 +                                    this.lastPitch = prevPitch;
 +                                    this.hasMoved = true;
 +                                }
++
 +                                Location from = new Location(player.getWorld(), this.lastPosX, this.lastPosY, this.lastPosZ, this.lastYaw, this.lastPitch); // Get the Players previous Event location.
 +                                Location to = player.getLocation().clone(); // Start off the To location as the Players current location.
 +
@@ -985,50 +933,63 @@
 +                                    }
 +                                }
 +                                // CraftBukkit end
-                                 this.player.absMoveTo(d0, d1, d2, f, f1);
-                                 boolean flag4 = this.player.isAutoSpinAttack();
- 
-@@ -1119,6 +1652,7 @@
-                 this.awaitingTeleportTime = this.tickCount;
-                 this.teleport(this.awaitingPositionFromClient.x, this.awaitingPositionFromClient.y, this.awaitingPositionFromClient.z, this.player.getYRot(), this.player.getXRot());
+                                 this.player.absMoveTo(d, d1, d2, f, f1);
+                                 boolean isAutoSpinAttack = this.player.isAutoSpinAttack();
+-                                this.clientIsFloating = d4 >= -0.03125
++                                this.clientIsFloating = verticalDelta >= -0.03125 // Paper - Decompile fix
+                                     && !flag1
+                                     && this.player.gameMode.getGameModeForPlayer() != GameType.SPECTATOR
+                                     && !this.server.isFlightAllowed()
+@@ -1019,7 +_,7 @@
+                                 this.lastGoodY = this.player.getY();
+                                 this.lastGoodZ = this.player.getZ();
+                             } else {
+-                                this.teleport(x, y, z, f, f1);
++                                this.internalTeleport(x, y, z, f, f1); // CraftBukkit - SPIGOT-1807: Don't call teleport event, when the client thinks the player is falling, because the chunks are not loaded on the client yet.
+                                 this.player.doCheckFallDamage(this.player.getX() - x, this.player.getY() - y, this.player.getZ() - z, packet.isOnGround());
+                             }
+                         }
+@@ -1053,6 +_,7 @@
+                     this.player.getXRot()
+                 );
              }
 +            this.allowedPlayerTicks = 20; // CraftBukkit
  
              return true;
          } else {
-@@ -1147,23 +1681,98 @@
+@@ -1076,10 +_,77 @@
      }
  
      public void teleport(double x, double y, double z, float yaw, float pitch) {
 -        this.teleport(new PositionMoveRotation(new Vec3(x, y, z), Vec3.ZERO, yaw, pitch), Collections.emptySet());
 +        // CraftBukkit start - Delegate to teleport(Location)
 +        this.teleport(x, y, z, yaw, pitch, PlayerTeleportEvent.TeleportCause.UNKNOWN);
-     }
- 
++    }
++
 +    public boolean teleport(double d0, double d1, double d2, float f, float f1, PlayerTeleportEvent.TeleportCause cause) {
 +        return this.teleport(new PositionMoveRotation(new Vec3(d0, d1, d2), Vec3.ZERO, f, f1), Collections.emptySet(), cause);
 +        // CraftBukkit end
-+    }
-+
-     public void teleport(PositionMoveRotation pos, Set<Relative> flags) {
+     }
+ 
+     public void teleport(PositionMoveRotation posMoveRotation, Set<Relative> relatives) {
 +        // CraftBukkit start
-+        this.teleport(pos, flags, PlayerTeleportEvent.TeleportCause.UNKNOWN);
++        this.teleport(posMoveRotation, relatives, PlayerTeleportEvent.TeleportCause.UNKNOWN);
 +    }
 +
-+    public boolean teleport(PositionMoveRotation positionmoverotation, Set<Relative> set, PlayerTeleportEvent.TeleportCause cause) { // CraftBukkit - Return event status
-+        Player player = this.getCraftPlayer();
++    public boolean teleport(PositionMoveRotation posMoveRotation, Set<Relative> relatives, PlayerTeleportEvent.TeleportCause cause) { // CraftBukkit - Return event status
++        org.bukkit.entity.Player player = this.getCraftPlayer();
 +        Location from = player.getLocation();
-+        PositionMoveRotation absolutePosition = PositionMoveRotation.calculateAbsolute(PositionMoveRotation.of(this.player), positionmoverotation, set);
++        PositionMoveRotation absolutePosition = PositionMoveRotation.calculateAbsolute(PositionMoveRotation.of(this.player), posMoveRotation, relatives);
 +        Location to = CraftLocation.toBukkit(absolutePosition.position(), this.getCraftPlayer().getWorld(), absolutePosition.yRot(), absolutePosition.xRot());
 +        // SPIGOT-5171: Triggered on join
 +        if (from.equals(to)) {
-+            this.internalTeleport(positionmoverotation, set);
++            this.internalTeleport(posMoveRotation, relatives);
 +            return true; // CraftBukkit - Return event status
 +        }
 +
 +        // Paper start - Teleport API
 +        final Set<io.papermc.paper.entity.TeleportFlag.Relative> relativeFlags = java.util.EnumSet.noneOf(io.papermc.paper.entity.TeleportFlag.Relative.class);
-+        for (final Relative relativeArgument : set) {
++        for (final Relative relativeArgument : relatives) {
 +            final io.papermc.paper.entity.TeleportFlag.Relative flag = org.bukkit.craftbukkit.entity.CraftPlayer.deltaRelativeToAPI(relativeArgument);
 +            if (flag != null) relativeFlags.add(flag);
 +        }
@@ -1039,10 +1000,10 @@
 +        if (event.isCancelled() || !to.equals(event.getTo())) {
 +            // set = Collections.emptySet(); // Can't relative teleport // Paper - Teleport API; Now you can!
 +            to = event.isCancelled() ? event.getFrom() : event.getTo();
-+            positionmoverotation = new PositionMoveRotation(CraftLocation.toVec3D(to), Vec3.ZERO, to.getYaw(), to.getPitch());
++            posMoveRotation = new PositionMoveRotation(CraftLocation.toVec3D(to), Vec3.ZERO, to.getYaw(), to.getPitch());
 +        }
 +
-+        this.internalTeleport(positionmoverotation, set);
++        this.internalTeleport(posMoveRotation, relatives);
 +        return !event.isCancelled(); // CraftBukkit - Return event status
 +    }
 +
@@ -1054,20 +1015,20 @@
 +        this.internalTeleport(new PositionMoveRotation(new Vec3(d0, d1, d2), Vec3.ZERO, f, f1), Collections.emptySet());
 +    }
 +
-+    public void internalTeleport(PositionMoveRotation positionmoverotation, Set<Relative> set) {
++    public void internalTeleport(PositionMoveRotation posMoveRotation, Set<Relative> relatives) {
 +        org.spigotmc.AsyncCatcher.catchOp("teleport"); // Paper
 +        // Paper start - Prevent teleporting dead entities
-+        if (player.isRemoved()) {
++        if (this.player.isRemoved()) {
 +            LOGGER.info("Attempt to teleport removed player {} restricted", player.getScoreboardName());
-+            if (server.isDebugging()) io.papermc.paper.util.TraceUtil.dumpTraceForThread("Attempt to teleport removed player");
++            if (this.server.isDebugging()) io.papermc.paper.util.TraceUtil.dumpTraceForThread("Attempt to teleport removed player");
 +            return;
 +        }
 +        // Paper end - Prevent teleporting dead entities
-+        if (Float.isNaN(positionmoverotation.yRot())) {
-+            positionmoverotation = new PositionMoveRotation(positionmoverotation.position(), positionmoverotation.deltaMovement(), 0, positionmoverotation.xRot());
++        if (Float.isNaN(posMoveRotation.yRot())) {
++            posMoveRotation = new PositionMoveRotation(posMoveRotation.position(), posMoveRotation.deltaMovement(), 0, posMoveRotation.xRot());
 +        }
-+        if (Float.isNaN(positionmoverotation.xRot())) {
-+            positionmoverotation = new PositionMoveRotation(positionmoverotation.position(), positionmoverotation.deltaMovement(), positionmoverotation.yRot(), 0);
++        if (Float.isNaN(posMoveRotation.xRot())) {
++            posMoveRotation = new PositionMoveRotation(posMoveRotation.position(), posMoveRotation.deltaMovement(), posMoveRotation.yRot(), 0);
 +        }
 +
 +        this.justTeleported = true;
@@ -1075,12 +1036,10 @@
          this.awaitingTeleportTime = this.tickCount;
          if (++this.awaitingTeleport == Integer.MAX_VALUE) {
              this.awaitingTeleport = 0;
-         }
+@@ -1087,12 +_,20 @@
  
--        this.player.teleportSetPosition(pos, flags);
-+        this.player.teleportSetPosition(positionmoverotation, set);
+         this.player.teleportSetPosition(posMoveRotation, relatives);
          this.awaitingPositionFromClient = this.player.position();
--        this.player.connection.send(ClientboundPlayerPositionPacket.of(this.awaitingTeleport, pos, flags));
 +        // CraftBukkit start - update last location
 +        this.lastPosX = this.awaitingPositionFromClient.x;
 +        this.lastPosY = this.awaitingPositionFromClient.y;
@@ -1088,7 +1047,7 @@
 +        this.lastYaw = this.player.getYRot();
 +        this.lastPitch = this.player.getXRot();
 +        // CraftBukkit end
-+        this.player.connection.send(ClientboundPlayerPositionPacket.of(this.awaitingTeleport, positionmoverotation, set));
+         this.player.connection.send(ClientboundPlayerPositionPacket.of(this.awaitingTeleport, posMoveRotation, relatives));
      }
  
      @Override
@@ -1096,16 +1055,16 @@
          PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
 +        if (this.player.isImmobile()) return; // CraftBukkit
          if (this.player.hasClientLoaded()) {
-             BlockPos blockposition = packet.getPos();
- 
-@@ -1175,14 +1784,46 @@
+             BlockPos pos = packet.getPos();
+             this.player.resetLastActionTime();
+@@ -1101,14 +_,46 @@
+                 case SWAP_ITEM_WITH_OFFHAND:
                      if (!this.player.isSpectator()) {
-                         ItemStack itemstack = this.player.getItemInHand(InteractionHand.OFF_HAND);
- 
+                         ItemStack itemInHand = this.player.getItemInHand(InteractionHand.OFF_HAND);
 -                        this.player.setItemInHand(InteractionHand.OFF_HAND, this.player.getItemInHand(InteractionHand.MAIN_HAND));
--                        this.player.setItemInHand(InteractionHand.MAIN_HAND, itemstack);
+-                        this.player.setItemInHand(InteractionHand.MAIN_HAND, itemInHand);
 +                        // CraftBukkit start - inspiration taken from DispenserRegistry (See SpigotCraft#394)
-+                        CraftItemStack mainHand = CraftItemStack.asCraftMirror(itemstack);
++                        CraftItemStack mainHand = CraftItemStack.asCraftMirror(itemInHand);
 +                        CraftItemStack offHand = CraftItemStack.asCraftMirror(this.player.getItemInHand(InteractionHand.MAIN_HAND));
 +                        PlayerSwapHandItemsEvent swapItemsEvent = new PlayerSwapHandItemsEvent(this.getCraftPlayer(), mainHand.clone(), offHand.clone());
 +                        this.cserver.getPluginManager().callEvent(swapItemsEvent);
@@ -1118,7 +1077,7 @@
 +                            this.player.setItemInHand(InteractionHand.OFF_HAND, CraftItemStack.asNMSCopy(swapItemsEvent.getOffHandItem()));
 +                        }
 +                        if (swapItemsEvent.getMainHandItem().equals(mainHand)) {
-+                            this.player.setItemInHand(InteractionHand.MAIN_HAND, itemstack);
++                            this.player.setItemInHand(InteractionHand.MAIN_HAND, itemInHand);
 +                        } else {
 +                            this.player.setItemInHand(InteractionHand.MAIN_HAND, CraftItemStack.asNMSCopy(swapItemsEvent.getMainHandItem()));
 +                        }
@@ -1147,48 +1106,45 @@
                          this.player.drop(false);
                      }
  
-@@ -1199,8 +1840,34 @@
+@@ -1125,8 +_,34 @@
                  case START_DESTROY_BLOCK:
                  case ABORT_DESTROY_BLOCK:
                  case STOP_DESTROY_BLOCK:
 +                    // Paper start - Don't allow digging into unloaded chunks
-+                    if (this.player.level().getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4) == null) {
++                    if (this.player.level().getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4) == null) {
 +                        this.player.connection.ackBlockChangesUpTo(packet.getSequence());
 +                        return;
 +                    }
 +                    // Paper end - Don't allow digging into unloaded chunks
-+                // Paper start - Send block entities after destroy prediction
-+                this.player.gameMode.capturedBlockEntity = false;
-+                this.player.gameMode.captureSentBlockEntities = true;
-+                // Paper end - Send block entities after destroy prediction
-                     this.player.gameMode.handleBlockBreakAction(blockposition, packetplayinblockdig_enumplayerdigtype, packet.getDirection(), this.player.level().getMaxY(), packet.getSequence());
-                     this.player.connection.ackBlockChangesUpTo(packet.getSequence());
-+                // Paper start - Send block entities after destroy prediction
-+                this.player.gameMode.captureSentBlockEntities = false;
-+                // If a block entity was modified speedup the block change ack to avoid the block entity
-+                // being overriden.
-+                if (this.player.gameMode.capturedBlockEntity) {
-+                    // manually tick
-+                    this.send(new ClientboundBlockChangedAckPacket(this.ackBlockChangesUpTo));
-+                    this.player.connection.ackBlockChangesUpTo = -1;
-+
++                    // Paper start - Send block entities after destroy prediction
 +                    this.player.gameMode.capturedBlockEntity = false;
-+                    BlockEntity tileentity = this.player.level().getBlockEntity(blockposition);
-+                    if (tileentity != null) {
-+                        this.player.connection.send(tileentity.getUpdatePacket());
++                    this.player.gameMode.captureSentBlockEntities = true;
++                    // Paper end - Send block entities after destroy prediction
+                     this.player.gameMode.handleBlockBreakAction(pos, action, packet.getDirection(), this.player.level().getMaxY(), packet.getSequence());
+                     this.player.connection.ackBlockChangesUpTo(packet.getSequence());
++                    // Paper start - Send block entities after destroy prediction
++                    this.player.gameMode.captureSentBlockEntities = false;
++                    // If a block entity was modified speedup the block change ack to avoid the block entity
++                    // being overridden.
++                    if (this.player.gameMode.capturedBlockEntity) {
++                        // manually tick
++                        this.send(new ClientboundBlockChangedAckPacket(this.ackBlockChangesUpTo));
++                        this.player.connection.ackBlockChangesUpTo = -1;
++
++                        this.player.gameMode.capturedBlockEntity = false;
++                        BlockEntity blockEntity = this.player.level().getBlockEntity(pos);
++                        if (blockEntity != null) {
++                            this.player.connection.send(blockEntity.getUpdatePacket());
++                        }
 +                    }
-+                }
-+                // Paper end - Send block entities after destroy prediction
++                    // Paper end - Send block entities after destroy prediction
                      return;
                  default:
                      throw new IllegalArgumentException("Invalid player action");
-@@ -1215,12 +1882,34 @@
-             Item item = stack.getItem();
+@@ -1143,9 +_,31 @@
+         }
+     }
  
-             return (item instanceof BlockItem || item instanceof BucketItem) && !player.getCooldowns().isOnCooldown(stack);
-+        }
-+    }
-+
 +    // Spigot start - limit place/interactions
 +    private int limitedPackets;
 +    private long lastLimitedPacket = -1;
@@ -1203,12 +1159,12 @@
 +            this.lastLimitedPacket = timestamp;
 +            this.limitedPackets = 0;
 +            return true;
-         }
++        }
 +
 +        return true;
-     }
-+    // Spigot end
- 
++    }
++    // Spigot end - limit place/interactions
++
      @Override
      public void handleUseItemOn(ServerboundUseItemOnPacket packet) {
          PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
@@ -1216,44 +1172,43 @@
 +        if (!this.checkLimit(packet.timestamp)) return; // Spigot - check limit
          if (this.player.hasClientLoaded()) {
              this.player.connection.ackBlockChangesUpTo(packet.getSequence());
-             ServerLevel worldserver = this.player.serverLevel();
-@@ -1230,6 +1919,11 @@
-             if (itemstack.isItemEnabled(worldserver.enabledFeatures())) {
-                 BlockHitResult movingobjectpositionblock = packet.getHitResult();
-                 Vec3 vec3d = movingobjectpositionblock.getLocation();
-+            // Paper start - improve distance check
-+            if (!Double.isFinite(vec3d.x) || !Double.isFinite(vec3d.y) || !Double.isFinite(vec3d.z)) {
-+                return;
-+            }
-+            // Paper end - improve distance check
-                 BlockPos blockposition = movingobjectpositionblock.getBlockPos();
- 
-                 if (this.player.canInteractWithBlock(blockposition, 1.0D)) {
-@@ -1243,7 +1937,8 @@
-                         int i = this.player.level().getMaxY();
- 
-                         if (blockposition.getY() <= i) {
--                            if (this.awaitingPositionFromClient == null && worldserver.mayInteract(this.player, blockposition)) {
-+                        if (this.awaitingPositionFromClient == null && (worldserver.mayInteract(this.player, blockposition) || (worldserver.paperConfig().spawn.allowUsingSignsInsideSpawnProtection && worldserver.getBlockState(blockposition).getBlock() instanceof net.minecraft.world.level.block.SignBlock))) { // Paper - Allow using signs inside spawn protection
+             ServerLevel serverLevel = this.player.serverLevel();
+@@ -1154,6 +_,11 @@
+             if (itemInHand.isItemEnabled(serverLevel.enabledFeatures())) {
+                 BlockHitResult hitResult = packet.getHitResult();
+                 Vec3 location = hitResult.getLocation();
++                // Paper start - improve distance check
++                if (!Double.isFinite(location.x()) || !Double.isFinite(location.y()) || !Double.isFinite(location.z())) {
++                    return;
++                }
++                // Paper end - improve distance check
+                 BlockPos blockPos = hitResult.getBlockPos();
+                 if (this.player.canInteractWithBlock(blockPos, 1.0)) {
+                     Vec3 vec3 = location.subtract(Vec3.atCenterOf(blockPos));
+@@ -1163,7 +_,8 @@
+                         this.player.resetLastActionTime();
+                         int maxY = this.player.level().getMaxY();
+                         if (blockPos.getY() <= maxY) {
+-                            if (this.awaitingPositionFromClient == null && serverLevel.mayInteract(this.player, blockPos)) {
++                            if (this.awaitingPositionFromClient == null && (serverLevel.mayInteract(this.player, blockPos) || (serverLevel.paperConfig().spawn.allowUsingSignsInsideSpawnProtection && serverLevel.getBlockState(blockPos).getBlock() instanceof net.minecraft.world.level.block.SignBlock))) { // Paper - Allow using signs inside spawn protection
 +                                this.player.stopUsingItem(); // CraftBukkit - SPIGOT-4706
-                                 InteractionResult enuminteractionresult = this.player.gameMode.useItemOn(this.player, worldserver, itemstack, enumhand, movingobjectpositionblock);
- 
-                                 if (enuminteractionresult.consumesAction()) {
-@@ -1257,11 +1952,11 @@
-                                 } else if (enuminteractionresult instanceof InteractionResult.Success) {
-                                     InteractionResult.Success enuminteractionresult_d = (InteractionResult.Success) enuminteractionresult;
- 
--                                    if (enuminteractionresult_d.swingSource() == InteractionResult.SwingSource.SERVER) {
-+                                    if (enuminteractionresult_d.swingSource() == InteractionResult.SwingSource.SERVER && !this.player.gameMode.interactResult) { // Paper - Call interact event
-                                         this.player.swing(enumhand, true);
-                                     }
+                                 InteractionResult interactionResult = this.player.gameMode.useItemOn(this.player, serverLevel, itemInHand, hand, hitResult);
+                                 if (interactionResult.consumesAction()) {
+                                     CriteriaTriggers.ANY_BLOCK_USE.trigger(this.player, hitResult.getBlockPos(), itemInHand.copy());
+@@ -1176,10 +_,10 @@
+                                     Component component = Component.translatable("build.tooHigh", maxY).withStyle(ChatFormatting.RED);
+                                     this.player.sendSystemMessage(component, true);
+                                 } else if (interactionResult instanceof InteractionResult.Success success
+-                                    && success.swingSource() == InteractionResult.SwingSource.SERVER) {
++                                    && success.swingSource() == InteractionResult.SwingSource.SERVER && !this.player.gameMode.interactResult) { // Paper - Call interact event
+                                     this.player.swing(hand, true);
                                  }
 -                            }
 +                            } else { this.player.containerMenu.sendAllDataToRemote(); } // Paper - Fix inventory desync; MC-99075
                          } else {
-                             MutableComponent ichatmutablecomponent1 = Component.translatable("build.tooHigh", i).withStyle(ChatFormatting.RED);
- 
-@@ -1281,6 +1976,8 @@
+                             Component component1 = Component.translatable("build.tooHigh", maxY).withStyle(ChatFormatting.RED);
+                             this.player.sendSystemMessage(component1, true);
+@@ -1203,6 +_,8 @@
      @Override
      public void handleUseItem(ServerboundUseItemPacket packet) {
          PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
@@ -1261,17 +1216,17 @@
 +        if (!this.checkLimit(packet.timestamp)) return; // Spigot - check limit
          if (this.player.hasClientLoaded()) {
              this.ackBlockChangesUpTo(packet.getSequence());
-             ServerLevel worldserver = this.player.serverLevel();
-@@ -1296,6 +1993,48 @@
+             ServerLevel serverLevel = this.player.serverLevel();
+@@ -1216,6 +_,48 @@
                      this.player.absRotateTo(f, f1);
                  }
  
 +                // CraftBukkit start
 +                // Raytrace to look for 'rogue armswings'
-+                double d0 = this.player.getX();
-+                double d1 = this.player.getY() + (double) this.player.getEyeHeight();
-+                double d2 = this.player.getZ();
-+                Vec3 vec3d = new Vec3(d0, d1, d2);
++                double x = this.player.getX();
++                double eyeY = this.player.getEyeY();
++                double z = this.player.getZ();
++                Vec3 from = new Vec3(x, eyeY, z);
 +
 +                float f3 = Mth.cos(-f * 0.017453292F - 3.1415927F);
 +                float f4 = Mth.sin(-f * 0.017453292F - 3.1415927F);
@@ -1280,55 +1235,56 @@
 +                float f7 = f4 * f5;
 +                float f8 = f3 * f5;
 +                double d3 = this.player.blockInteractionRange();
-+                Vec3 vec3d1 = vec3d.add((double) f7 * d3, (double) f6 * d3, (double) f8 * d3);
-+                HitResult movingobjectposition = this.player.level().clip(new ClipContext(vec3d, vec3d1, ClipContext.Block.OUTLINE, ClipContext.Fluid.NONE, this.player));
++                Vec3 to = from.add((double) f7 * d3, (double) f6 * d3, (double) f8 * d3);
++                BlockHitResult hitResult = this.player.level().clip(new ClipContext(from, to, ClipContext.Block.OUTLINE, ClipContext.Fluid.NONE, this.player));
 +
 +                boolean cancelled;
-+                if (movingobjectposition == null || movingobjectposition.getType() != HitResult.Type.BLOCK) {
-+                    org.bukkit.event.player.PlayerInteractEvent event = CraftEventFactory.callPlayerInteractEvent(this.player, Action.RIGHT_CLICK_AIR, itemstack, enumhand);
++                if (hitResult == null || hitResult.getType() != HitResult.Type.BLOCK) {
++                    org.bukkit.event.player.PlayerInteractEvent event = CraftEventFactory.callPlayerInteractEvent(this.player, Action.RIGHT_CLICK_AIR, itemInHand, hand);
 +                    cancelled = event.useItemInHand() == Event.Result.DENY;
 +                } else {
-+                    BlockHitResult movingobjectpositionblock = (BlockHitResult) movingobjectposition;
-+                    if (this.player.gameMode.firedInteract && this.player.gameMode.interactPosition.equals(movingobjectpositionblock.getBlockPos()) && this.player.gameMode.interactHand == enumhand && ItemStack.isSameItemSameComponents(this.player.gameMode.interactItemStack, itemstack)) {
++                    if (this.player.gameMode.firedInteract && this.player.gameMode.interactPosition.equals(hitResult.getBlockPos()) && this.player.gameMode.interactHand == hand && ItemStack.isSameItemSameComponents(this.player.gameMode.interactItemStack, itemInHand)) {
 +                        cancelled = this.player.gameMode.interactResult;
 +                    } else {
-+                        org.bukkit.event.player.PlayerInteractEvent event = CraftEventFactory.callPlayerInteractEvent(this.player, Action.RIGHT_CLICK_BLOCK, movingobjectpositionblock.getBlockPos(), movingobjectpositionblock.getDirection(), itemstack, true, enumhand, movingobjectpositionblock.getLocation());
++                        org.bukkit.event.player.PlayerInteractEvent event = CraftEventFactory.callPlayerInteractEvent(this.player, Action.RIGHT_CLICK_BLOCK, hitResult.getBlockPos(), hitResult.getDirection(), itemInHand, true, hand, hitResult.getLocation());
 +                        cancelled = event.useItemInHand() == Event.Result.DENY;
 +                    }
 +                    this.player.gameMode.firedInteract = false;
 +                }
 +
 +                if (cancelled) {
-+                this.player.resyncUsingItem(this.player); // Paper - Properly cancel usable items
-+                    this.player.getBukkitEntity().updateInventory(); // SPIGOT-2524
++                    this.player.resyncUsingItem(this.player); // Paper - Properly cancel usable items
++                    this.player.containerMenu.sendAllDataToRemote(); // SPIGOT-2524
 +                    return;
 +                }
-+                itemstack = this.player.getItemInHand(enumhand); // Update in case it was changed in the event
-+                if (itemstack.isEmpty()) {
++                itemInHand = this.player.getItemInHand(hand); // Update in case it was changed in the event
++                if (itemInHand.isEmpty()) {
 +                    return;
 +                }
 +                // CraftBukkit end
-                 InteractionResult enuminteractionresult = this.player.gameMode.useItem(this.player, worldserver, itemstack, enumhand);
- 
-                 if (enuminteractionresult instanceof InteractionResult.Success) {
-@@ -1321,7 +2060,7 @@
-                 Entity entity = packet.getEntity(worldserver);
- 
++
+                 if (this.player.gameMode.useItem(this.player, serverLevel, itemInHand, hand) instanceof InteractionResult.Success success
+                     && success.swingSource() == InteractionResult.SwingSource.SERVER) {
+                     this.player.swing(hand, true);
+@@ -1231,7 +_,7 @@
+             for (ServerLevel serverLevel : this.server.getAllLevels()) {
+                 Entity entity = packet.getEntity(serverLevel);
                  if (entity != null) {
--                    this.player.teleportTo(worldserver, entity.getX(), entity.getY(), entity.getZ(), Set.of(), entity.getYRot(), entity.getXRot(), true);
-+                    this.player.teleportTo(worldserver, entity.getX(), entity.getY(), entity.getZ(), Set.of(), entity.getYRot(), entity.getXRot(), true, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.SPECTATE); // CraftBukkit
+-                    this.player.teleportTo(serverLevel, entity.getX(), entity.getY(), entity.getZ(), Set.of(), entity.getYRot(), entity.getXRot(), true);
++                    this.player.teleportTo(serverLevel, entity.getX(), entity.getY(), entity.getZ(), Set.of(), entity.getYRot(), entity.getXRot(), true, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.SPECTATE); // CraftBukkit
                      return;
                  }
              }
-@@ -1342,22 +2081,52 @@
+@@ -1248,24 +_,54 @@
  
      @Override
-     public void onDisconnect(DisconnectionDetails info) {
+     public void onDisconnect(DisconnectionDetails details) {
 +        // Paper start - Fix kick event leave message not being sent
-+        this.onDisconnect(info, null);
++        this.onDisconnect(details, null);
 +    }
++
 +    @Override
-+    public void onDisconnect(DisconnectionDetails info, @Nullable net.kyori.adventure.text.Component quitMessage) {
++    public void onDisconnect(DisconnectionDetails details, @Nullable net.kyori.adventure.text.Component quitMessage) {
 +        // Paper end - Fix kick event leave message not being sent
 +        // CraftBukkit start - Rarely it would send a disconnect line twice
 +        if (this.processedDisconnect) {
@@ -1337,15 +1293,15 @@
 +            this.processedDisconnect = true;
 +        }
 +        // CraftBukkit end
-         ServerGamePacketListenerImpl.LOGGER.info("{} lost connection: {}", this.player.getName().getString(), info.reason().getString());
+         LOGGER.info("{} lost connection: {}", this.player.getName().getString(), details.reason().getString());
 -        this.removePlayerFromWorld();
--        super.onDisconnect(info);
+-        super.onDisconnect(details);
 +        this.removePlayerFromWorld(quitMessage); // Paper - Fix kick event leave message not being sent
-+        super.onDisconnect(info, quitMessage); // Paper - Fix kick event leave message not being sent
++        super.onDisconnect(details, quitMessage); // Paper - Fix kick event leave message not being sent
      }
  
-+    // Paper start - Fix kick event leave message not being sent
      private void removePlayerFromWorld() {
++        // Paper start - Fix kick event leave message not being sent
 +        this.removePlayerFromWorld(null);
 +    }
 +
@@ -1355,17 +1311,17 @@
 +        // CraftBukkit start - Replace vanilla quit message handling with our own.
 +        /*
          this.server.invalidateStatus();
--        this.server.getPlayerList().broadcastSystemMessage(Component.translatable("multiplayer.player.left", this.player.getDisplayName()).withStyle(ChatFormatting.YELLOW), false);
-+        this.server.getPlayerList().broadcastSystemMessage(IChatBaseComponent.translatable("multiplayer.player.left", this.player.getDisplayName()).withStyle(EnumChatFormat.YELLOW), false);
-+        */
-+
+         this.server
+             .getPlayerList()
+             .broadcastSystemMessage(Component.translatable("multiplayer.player.left", this.player.getDisplayName()).withStyle(ChatFormatting.YELLOW), false);
++         */
          this.player.disconnect();
 -        this.server.getPlayerList().remove(this.player);
 +        // Paper start - Adventure
 +        quitMessage = quitMessage == null ? this.server.getPlayerList().remove(this.player) : this.server.getPlayerList().remove(this.player, quitMessage); // Paper - pass in quitMessage to fix kick message not being used
 +        if ((quitMessage != null) && !quitMessage.equals(net.kyori.adventure.text.Component.empty())) {
 +            this.server.getPlayerList().broadcastSystemMessage(PaperAdventure.asVanilla(quitMessage), false);
-+            // Paper end
++            // Paper end - Adventure
 +        }
 +        // CraftBukkit end
          this.player.getTextFilter().leave();
@@ -1377,7 +1333,7 @@
              throw new IllegalArgumentException("Expected packet sequence nr >= 0");
          } else {
              this.ackBlockChangesUpTo = Math.max(sequence, this.ackBlockChangesUpTo);
-@@ -1367,7 +2136,17 @@
+@@ -1275,7 +_,17 @@
      @Override
      public void handleSetCarriedItem(ServerboundSetCarriedItemPacket packet) {
          PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
@@ -1395,10 +1351,10 @@
              if (this.player.getInventory().selected != packet.getSlot() && this.player.getUsedItemHand() == InteractionHand.MAIN_HAND) {
                  this.player.stopUsingItem();
              }
-@@ -1376,11 +2155,18 @@
+@@ -1284,11 +_,18 @@
              this.player.resetLastActionTime();
          } else {
-             ServerGamePacketListenerImpl.LOGGER.warn("{} tried to set an invalid carried item", this.player.getName().getString());
+             LOGGER.warn("{} tried to set an invalid carried item", this.player.getName().getString());
 +            this.disconnect(Component.literal("Invalid hotbar selection (Hacking?)"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // CraftBukkit // Paper - kick event cause
          }
      }
@@ -1412,23 +1368,22 @@
 +        }
 +        // CraftBukkit end
          Optional<LastSeenMessages> optional = this.unpackAndApplyLastSeen(packet.lastSeenMessages());
- 
          if (!optional.isEmpty()) {
-@@ -1394,27 +2180,46 @@
+             this.tryHandleChat(packet.message(), () -> {
+@@ -1300,25 +_,45 @@
                      return;
                  }
  
--                CompletableFuture<FilteredText> completablefuture = this.filterTextPacket(playerchatmessage.signedContent());
--                Component ichatbasecomponent = this.server.getChatDecorator().decorate(this.player, playerchatmessage.decoratedContent());
-+                CompletableFuture<FilteredText> completablefuture = this.filterTextPacket(playerchatmessage.signedContent()).thenApplyAsync(Function.identity(), this.server.chatExecutor); // CraftBukkit - async chat
-+                CompletableFuture<Component> componentFuture = this.server.getChatDecorator().decorate(this.player, null, playerchatmessage.decoratedContent()); // Paper - Adventure
- 
--                this.chatMessageChain.append(completablefuture, (filteredtext) -> {
--                    PlayerChatMessage playerchatmessage1 = playerchatmessage.withUnsignedContent(ichatbasecomponent).filter(filteredtext.mask());
-+                this.chatMessageChain.append(CompletableFuture.allOf(completablefuture, componentFuture), (filteredtext) -> { // Paper - Adventure
-+                    PlayerChatMessage playerchatmessage1 = playerchatmessage.withUnsignedContent(componentFuture.join()).filter(completablefuture.join().mask()); // Paper - Adventure
- 
-                     this.broadcastChatMessage(playerchatmessage1);
+-                CompletableFuture<FilteredText> completableFuture = this.filterTextPacket(signedMessage.signedContent());
+-                Component component = this.server.getChatDecorator().decorate(this.player, signedMessage.decoratedContent());
+-                this.chatMessageChain.append(completableFuture, filteredText -> {
+-                    PlayerChatMessage playerChatMessage = signedMessage.withUnsignedContent(component).filter(filteredText.mask());
++                CompletableFuture<FilteredText> completableFuture = this.filterTextPacket(signedMessage.signedContent()).thenApplyAsync(Function.identity(), this.server.chatExecutor); // CraftBukkit - async chat
++                CompletableFuture<Component> componentFuture = this.server.getChatDecorator().decorate(this.player, null, signedMessage.decoratedContent()); // Paper - Adventure
++
++                this.chatMessageChain.append(CompletableFuture.allOf(completableFuture, componentFuture), (filteredtext) -> { // Paper - Adventure
++                    PlayerChatMessage playerChatMessage = signedMessage.withUnsignedContent(componentFuture.join()).filter(completableFuture.join().mask()); // Paper - Adventure
+                     this.broadcastChatMessage(playerChatMessage);
                  });
 -            });
 +            }, false); // CraftBukkit - async chat
@@ -1452,12 +1407,12 @@
  
      private void performUnsignedChatCommand(String command) {
 +        // CraftBukkit start
-+        String command1 = "/" + command;
++        String prefixedCommand = "/" + command;
 +        if (org.spigotmc.SpigotConfig.logCommands) { // Paper - Add missing SpigotConfig logCommands check
-+        ServerGamePacketListenerImpl.LOGGER.info(this.player.getScoreboardName() + " issued server command: " + command1);
++            LOGGER.info("{} issued server command: {}", this.player.getScoreboardName(), prefixedCommand);
 +        }
 +
-+        PlayerCommandPreprocessEvent event = new PlayerCommandPreprocessEvent(this.getCraftPlayer(), command1, new LazyPlayerSet(this.server));
++        PlayerCommandPreprocessEvent event = new PlayerCommandPreprocessEvent(this.getCraftPlayer(), prefixedCommand, new LazyPlayerSet(this.server));
 +        this.cserver.getPluginManager().callEvent(event);
 +
 +        if (event.isCancelled()) {
@@ -1465,11 +1420,11 @@
 +        }
 +        command = event.getMessage().substring(1);
 +        // CraftBukkit end
-         ParseResults<CommandSourceStack> parseresults = this.parseCommand(command);
- 
-         if (this.server.enforceSecureProfile() && SignableCommand.hasSignableArguments(parseresults)) {
-@@ -1431,30 +2236,58 @@
- 
+         ParseResults<CommandSourceStack> parseResults = this.parseCommand(command);
+         if (this.server.enforceSecureProfile() && SignableCommand.hasSignableArguments(parseResults)) {
+             LOGGER.error(
+@@ -1335,26 +_,55 @@
+         Optional<LastSeenMessages> optional = this.unpackAndApplyLastSeen(packet.lastSeenMessages());
          if (!optional.isEmpty()) {
              this.tryHandleChat(packet.command(), () -> {
 +                // CraftBukkit start - SPIGOT-7346: Prevent disconnected players from executing commands
@@ -1477,7 +1432,7 @@
 +                    return;
 +                }
 +                // CraftBukkit end
-                 this.performSignedChatCommand(packet, (LastSeenMessages) optional.get());
+                 this.performSignedChatCommand(packet, optional.get());
 -                this.detectRateSpam();
 -            });
 +                this.detectRateSpam("/" + packet.command()); // Spigot
@@ -1486,30 +1441,28 @@
      }
  
      private void performSignedChatCommand(ServerboundChatCommandSignedPacket packet, LastSeenMessages lastSeenMessages) {
--        ParseResults<CommandSourceStack> parseresults = this.parseCommand(packet.command());
 +        // CraftBukkit start
 +        String command = "/" + packet.command();
 +        if (org.spigotmc.SpigotConfig.logCommands) { // Paper - Add missing SpigotConfig logCommands check
-+        ServerGamePacketListenerImpl.LOGGER.info(this.player.getScoreboardName() + " issued server command: " + command);
++            LOGGER.info("{} issued server command: {}", this.player.getScoreboardName(), command);
 +        } // Paper - Add missing SpigotConfig logCommands check
- 
--        Map map;
++
 +        PlayerCommandPreprocessEvent event = new PlayerCommandPreprocessEvent(this.getCraftPlayer(), command, new LazyPlayerSet(this.server));
 +        this.cserver.getPluginManager().callEvent(event);
 +        command = event.getMessage().substring(1);
- 
-+        // Paper start - Fix cancellation and message changing
-+        ParseResults<CommandSourceStack> parseresults = this.parseCommand(packet.command());
 +
-+        Map<String, PlayerChatMessage> map;
+         ParseResults<CommandSourceStack> parseResults = this.parseCommand(packet.command());
+ 
+         Map<String, PlayerChatMessage> map;
          try {
-+            // Always parse the original command to add to the chat chain
-             map = this.collectSignedArguments(packet, SignableCommand.of(parseresults), lastSeenMessages);
-         } catch (SignedMessageChain.DecodeException signedmessagechain_a) {
-             this.handleMessageDecodeFailure(signedmessagechain_a);
++            // Paper - Always parse the original command to add to the chat chain
+             map = this.collectSignedArguments(packet, SignableCommand.of(parseResults), lastSeenMessages);
+         } catch (SignedMessageChain.DecodeException var6) {
+             this.handleMessageDecodeFailure(var6);
              return;
          }
  
++        // Paper start - Fix cancellation and message changing
 +        if (event.isCancelled()) {
 +            // Only now are we actually good to return
 +            return;
@@ -1517,73 +1470,68 @@
 +
 +        // Remove signed parts if the command was changed
 +        if (!command.equals(packet.command())) {
-+            parseresults = this.parseCommand(command);
++            parseResults = this.parseCommand(command);
 +            map = Collections.emptyMap();
 +        }
 +        // Paper end - Fix cancellation and message changing
 +
-         CommandSigningContext.SignedArguments commandsigningcontext_a = new CommandSigningContext.SignedArguments(map);
- 
--        parseresults = Commands.mapSource(parseresults, (commandlistenerwrapper) -> {
-+        parseresults = Commands.<CommandSourceStack>mapSource(parseresults, (commandlistenerwrapper) -> { // CraftBukkit - decompile error
-             return commandlistenerwrapper.withSigningContext(commandsigningcontext_a, this.chatMessageChain);
-         });
--        this.server.getCommands().performCommand(parseresults, packet.command());
-+        this.server.getCommands().performCommand(parseresults, command); // CraftBukkit
+         CommandSigningContext commandSigningContext = new CommandSigningContext.SignedArguments(map);
+         parseResults = Commands.mapSource(parseResults, source -> source.withSigningContext(commandSigningContext, this.chatMessageChain));
+-        this.server.getCommands().performCommand(parseResults, packet.command());
++        this.server.getCommands().performCommand(parseResults, command); // CraftBukkit
      }
  
      private void handleMessageDecodeFailure(SignedMessageChain.DecodeException exception) {
-@@ -1530,14 +2363,20 @@
-         return com_mojang_brigadier_commanddispatcher.parse(command, this.player.createCommandSourceStack());
+@@ -1418,14 +_,20 @@
+         return dispatcher.parse(command, this.player.createCommandSourceStack());
      }
  
--    private void tryHandleChat(String message, Runnable callback) {
--        if (ServerGamePacketListenerImpl.isChatMessageIllegal(message)) {
--            this.disconnect((Component) Component.translatable("multiplayer.disconnect.illegal_characters"));
+-    private void tryHandleChat(String message, Runnable handler) {
++    private void tryHandleChat(String message, Runnable handler, boolean sync) { // CraftBukkit
+         if (isChatMessageIllegal(message)) {
+-            this.disconnect(Component.translatable("multiplayer.disconnect.illegal_characters"));
 -        } else if (this.player.getChatVisibility() == ChatVisiblity.HIDDEN) {
-+    private void tryHandleChat(String s, Runnable runnable, boolean sync) { // CraftBukkit
-+        if (ServerGamePacketListenerImpl.isChatMessageIllegal(s)) {
-+            this.disconnectAsync((Component) Component.translatable("multiplayer.disconnect.illegal_characters"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_CHARACTERS); // Paper // Paper - add proper async disconnect
++            this.disconnectAsync(Component.translatable("multiplayer.disconnect.illegal_characters"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_CHARACTERS); // Paper - add proper async disconnect
 +        } else if (this.player.isRemoved() || this.player.getChatVisibility() == ChatVisiblity.HIDDEN) { // CraftBukkit - dead men tell no tales
              this.send(new ClientboundSystemChatPacket(Component.translatable("chat.disabled.options").withStyle(ChatFormatting.RED), false));
          } else {
              this.player.resetLastActionTime();
--            this.server.execute(callback);
+-            this.server.execute(handler);
 +            // CraftBukkit start
 +            if (sync) {
-+                this.server.execute(runnable);
++                this.server.execute(handler);
 +            } else {
-+                runnable.run();
++                handler.run();
 +            }
 +            // CraftBukkit end
          }
      }
  
-@@ -1549,7 +2388,7 @@
- 
+@@ -1434,7 +_,7 @@
+             Optional<LastSeenMessages> optional = this.lastSeenMessages.applyUpdate(update);
              if (optional.isEmpty()) {
-                 ServerGamePacketListenerImpl.LOGGER.warn("Failed to validate message acknowledgements from {}", this.player.getName().getString());
--                this.disconnect(ServerGamePacketListenerImpl.CHAT_VALIDATION_FAILED);
-+                this.disconnectAsync(ServerGamePacketListenerImpl.CHAT_VALIDATION_FAILED, org.bukkit.event.player.PlayerKickEvent.Cause.CHAT_VALIDATION_FAILED); // Paper - kick event causes // Paper - add proper async disconnect
+                 LOGGER.warn("Failed to validate message acknowledgements from {}", this.player.getName().getString());
+-                this.disconnect(CHAT_VALIDATION_FAILED);
++                this.disconnectAsync(CHAT_VALIDATION_FAILED, org.bukkit.event.player.PlayerKickEvent.Cause.CHAT_VALIDATION_FAILED); // Paper - kick event causes & add proper async disconnect
              }
  
              return optional;
-@@ -1566,6 +2405,117 @@
+@@ -1451,22 +_,155 @@
          return false;
      }
  
 +    // CraftBukkit start - add method
-+    public void chat(String s, PlayerChatMessage original, boolean async) {
-+        if (s.isEmpty() || this.player.getChatVisibility() == ChatVisiblity.HIDDEN) {
++    public void chat(String msg, PlayerChatMessage original, boolean async) {
++        if (msg.isEmpty() || this.player.getChatVisibility() == ChatVisiblity.HIDDEN) {
 +            return;
 +        }
 +        OutgoingChatMessage outgoing = OutgoingChatMessage.create(original);
 +
-+        if (false && !async && s.startsWith("/")) { // Paper - Don't handle commands in chat logic
-+            this.handleCommand(s);
++        if (false && !async && msg.startsWith("/")) { // Paper - Don't handle commands in chat logic
++            this.handleCommand(msg);
 +        } else if (this.player.getChatVisibility() == ChatVisiblity.SYSTEM) {
 +            // Do nothing, this is coming from a plugin
-+        // Paper start
++            // Paper start
 +        } else if (true) {
 +            if (!async && !org.bukkit.Bukkit.isPrimaryThread()) {
 +                org.spigotmc.AsyncCatcher.catchOp("Asynchronous player chat is not allowed here");
@@ -1592,8 +1540,8 @@
 +            cp.process();
 +            // Paper end
 +        } else if (false) { // Paper
-+            Player player = this.getCraftPlayer();
-+            AsyncPlayerChatEvent event = new AsyncPlayerChatEvent(async, player, s, new LazyPlayerSet(this.server));
++            org.bukkit.entity.Player player = this.getCraftPlayer();
++            AsyncPlayerChatEvent event = new AsyncPlayerChatEvent(async, player, msg, new LazyPlayerSet(this.server));
 +            String originalFormat = event.getFormat(), originalMessage = event.getMessage();
 +            this.cserver.getPluginManager().callEvent(event);
 +
@@ -1601,7 +1549,7 @@
 +                // Evil plugins still listening to deprecated event
 +                final PlayerChatEvent queueEvent = new PlayerChatEvent(player, event.getMessage(), event.getFormat(), event.getRecipients());
 +                queueEvent.setCancelled(event.isCancelled());
-+                Waitable waitable = new Waitable() {
++                Waitable<Object> waitable = new Waitable<>() {
 +                    @Override
 +                    protected Object evaluate() {
 +                        org.bukkit.Bukkit.getPluginManager().callEvent(queueEvent);
@@ -1621,8 +1569,8 @@
 +                                recipient.getBukkitEntity().sendMessage(ServerGamePacketListenerImpl.this.player.getUUID(), message);
 +                            }
 +                        } else {
-+                            for (Player player : queueEvent.getRecipients()) {
-+                                player.sendMessage(ServerGamePacketListenerImpl.this.player.getUUID(), message);
++                            for (org.bukkit.entity.Player recipient : queueEvent.getRecipients()) {
++                                recipient.sendMessage(ServerGamePacketListenerImpl.this.player.getUUID(), message);
 +                            }
 +                        }
 +                        ServerGamePacketListenerImpl.this.server.console.sendMessage(message);
@@ -1646,22 +1594,22 @@
 +                    return;
 +                }
 +
-+                s = String.format(event.getFormat(), event.getPlayer().getDisplayName(), event.getMessage());
++                msg = String.format(event.getFormat(), event.getPlayer().getDisplayName(), event.getMessage());
 +                if (((LazyPlayerSet) event.getRecipients()).isLazy()) {
 +                    if (!org.spigotmc.SpigotConfig.bungee && originalFormat.equals(event.getFormat()) && originalMessage.equals(event.getMessage()) && event.getPlayer().getName().equalsIgnoreCase(event.getPlayer().getDisplayName())) { // Spigot
-+                        ServerGamePacketListenerImpl.this.server.getPlayerList().broadcastChatMessage(original, ServerGamePacketListenerImpl.this.player, ChatType.bind(ChatType.CHAT, (Entity) ServerGamePacketListenerImpl.this.player));
++                        ServerGamePacketListenerImpl.this.server.getPlayerList().broadcastChatMessage(original, ServerGamePacketListenerImpl.this.player, ChatType.bind(ChatType.CHAT, ServerGamePacketListenerImpl.this.player));
 +                        return;
 +                    }
 +
 +                    for (ServerPlayer recipient : this.server.getPlayerList().players) {
-+                        recipient.getBukkitEntity().sendMessage(ServerGamePacketListenerImpl.this.player.getUUID(), s);
++                        recipient.getBukkitEntity().sendMessage(ServerGamePacketListenerImpl.this.player.getUUID(), msg);
 +                    }
 +                } else {
-+                    for (Player recipient : event.getRecipients()) {
-+                        recipient.sendMessage(ServerGamePacketListenerImpl.this.player.getUUID(), s);
++                    for (org.bukkit.entity.Player recipient : event.getRecipients()) {
++                        recipient.sendMessage(ServerGamePacketListenerImpl.this.player.getUUID(), msg);
 +                    }
 +                }
-+                this.server.console.sendMessage(s);
++                this.server.console.sendMessage(msg);
 +            }
 +        }
 +    }
@@ -1684,69 +1632,62 @@
 +    // CraftBukkit end
 +
      private PlayerChatMessage getSignedMessage(ServerboundChatPacket packet, LastSeenMessages lastSeenMessages) throws SignedMessageChain.DecodeException {
-         SignedMessageBody signedmessagebody = new SignedMessageBody(packet.message(), packet.timeStamp(), packet.salt(), lastSeenMessages);
- 
-@@ -1573,15 +2523,44 @@
+         SignedMessageBody signedMessageBody = new SignedMessageBody(packet.message(), packet.timeStamp(), packet.salt(), lastSeenMessages);
+         return this.signedMessageDecoder.unpack(packet.signature(), signedMessageBody);
      }
  
      private void broadcastChatMessage(PlayerChatMessage message) {
--        this.server.getPlayerList().broadcastChatMessage(message, this.player, ChatType.bind(ChatType.CHAT, (Entity) this.player));
+-        this.server.getPlayerList().broadcastChatMessage(message, this.player, ChatType.bind(ChatType.CHAT, this.player));
 -        this.detectRateSpam();
 +        // CraftBukkit start
-+        String s = message.signedContent();
-+        if (s.isEmpty()) {
-+            ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send an empty message");
++        String rawMessage = message.signedContent();
++        if (rawMessage.isEmpty()) {
++            LOGGER.warn("{} tried to send an empty message", this.player.getScoreboardName());
 +        } else if (this.getCraftPlayer().isConversing()) {
-+            final String conversationInput = s;
-+            this.server.processQueue.add(new Runnable() {
-+                @Override
-+                public void run() {
-+                    ServerGamePacketListenerImpl.this.getCraftPlayer().acceptConversationInput(conversationInput);
-+                }
-+            });
++            final String conversationInput = rawMessage;
++            this.server.processQueue.add(() -> ServerGamePacketListenerImpl.this.getCraftPlayer().acceptConversationInput(conversationInput));
 +        } else if (this.player.getChatVisibility() == ChatVisiblity.SYSTEM) { // Re-add "Command Only" flag check
 +            this.send(new ClientboundSystemChatPacket(Component.translatable("chat.cannotSend").withStyle(ChatFormatting.RED), false));
 +        } else {
-+            this.chat(s, message, true);
++            this.chat(rawMessage, message, true);
 +        }
-+        // this.server.getPlayerList().broadcastChatMessage(playerchatmessage, this.player, ChatMessageType.bind(ChatMessageType.CHAT, (Entity) this.player));
++        // this.server.getPlayerList().broadcastChatMessage(message, this.player, ChatType.bind(ChatType.CHAT, this.player));
 +        // CraftBukkit end
-+        this.detectRateSpam(s); // Spigot
++        this.detectRateSpam(rawMessage); // Spigot
      }
  
 -    private void detectRateSpam() {
 -        this.chatSpamThrottler.increment();
--        if (!this.chatSpamThrottler.isUnderThreshold() && !this.server.getPlayerList().isOp(this.player.getGameProfile()) && !this.server.isSingleplayerOwner(this.player.getGameProfile())) {
--            this.disconnect((Component) Component.translatable("disconnect.spam"));
+-        if (!this.chatSpamThrottler.isUnderThreshold()
 +    // Spigot start - spam exclusions
-+    private void detectRateSpam(String s) {
++    private void detectRateSpam(String message) {
 +        // CraftBukkit start - replaced with thread safe throttle
-+        for ( String exclude : org.spigotmc.SpigotConfig.spamExclusions )
-+        {
-+            if ( exclude != null && s.startsWith( exclude ) )
-+            {
++        for (String exclude : org.spigotmc.SpigotConfig.spamExclusions) {
++            if (exclude != null && message.startsWith(exclude)) {
 +                return;
 +            }
-         }
++        }
 +        // Spigot end
 +        // this.chatSpamThrottler.increment();
-+        if (!this.chatSpamThrottler.isIncrementAndUnderThreshold() && !this.server.getPlayerList().isOp(this.player.getGameProfile()) && !this.server.isSingleplayerOwner(this.player.getGameProfile())) {
++        if (!this.chatSpamThrottler.isIncrementAndUnderThreshold()
 +            // CraftBukkit end
-+            this.disconnectAsync((Component) Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - kick event cause // Paper - add proper async disconnect
-+        }
- 
+             && !this.server.getPlayerList().isOp(this.player.getGameProfile())
+             && !this.server.isSingleplayerOwner(this.player.getGameProfile())) {
+-            this.disconnect(Component.translatable("disconnect.spam"));
++            this.disconnectAsync(Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - kick event cause & add proper async disconnect
+         }
      }
  
-@@ -1592,7 +2571,7 @@
+@@ -1475,7 +_,7 @@
          synchronized (this.lastSeenMessages) {
              if (!this.lastSeenMessages.applyOffset(packet.offset())) {
-                 ServerGamePacketListenerImpl.LOGGER.warn("Failed to validate message acknowledgements from {}", this.player.getName().getString());
--                this.disconnect(ServerGamePacketListenerImpl.CHAT_VALIDATION_FAILED);
-+                this.disconnectAsync(ServerGamePacketListenerImpl.CHAT_VALIDATION_FAILED, org.bukkit.event.player.PlayerKickEvent.Cause.CHAT_VALIDATION_FAILED); // Paper - kick event causes // Paper - add proper async disconnect
+                 LOGGER.warn("Failed to validate message acknowledgements from {}", this.player.getName().getString());
+-                this.disconnect(CHAT_VALIDATION_FAILED);
++                this.disconnectAsync(ServerGamePacketListenerImpl.CHAT_VALIDATION_FAILED, org.bukkit.event.player.PlayerKickEvent.Cause.CHAT_VALIDATION_FAILED); // Paper - kick event causes & add proper async disconnect
              }
- 
          }
-@@ -1601,7 +2580,40 @@
+     }
+@@ -1483,7 +_,40 @@
      @Override
      public void handleAnimate(ServerboundSwingPacket packet) {
          PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
@@ -1787,7 +1728,7 @@
          this.player.swing(packet.getHand());
      }
  
-@@ -1609,6 +2621,29 @@
+@@ -1491,10 +_,41 @@
      public void handlePlayerCommand(ServerboundPlayerCommandPacket packet) {
          PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
          if (this.player.hasClientLoaded()) {
@@ -1795,7 +1736,7 @@
 +            if (this.player.isRemoved()) return;
 +            switch (packet.getAction()) {
 +                case PRESS_SHIFT_KEY:
-+                case RELEASE_SHIFT_KEY:
++                case RELEASE_SHIFT_KEY: {
 +                    PlayerToggleSneakEvent event = new PlayerToggleSneakEvent(this.getCraftPlayer(), packet.getAction() == ServerboundPlayerCommandPacket.Action.PRESS_SHIFT_KEY);
 +                    this.cserver.getPluginManager().callEvent(event);
 +
@@ -1803,21 +1744,21 @@
 +                        return;
 +                    }
 +                    break;
++                }
 +                case START_SPRINTING:
-+                case STOP_SPRINTING:
-+                    PlayerToggleSprintEvent e2 = new PlayerToggleSprintEvent(this.getCraftPlayer(), packet.getAction() == ServerboundPlayerCommandPacket.Action.START_SPRINTING);
-+                    this.cserver.getPluginManager().callEvent(e2);
++                case STOP_SPRINTING: {
++                    PlayerToggleSprintEvent event = new PlayerToggleSprintEvent(this.getCraftPlayer(), packet.getAction() == ServerboundPlayerCommandPacket.Action.START_SPRINTING);
++                    this.cserver.getPluginManager().callEvent(event);
 +
-+                    if (e2.isCancelled()) {
++                    if (event.isCancelled()) {
 +                        return;
 +                    }
 +                    break;
++                }
 +            }
 +            // CraftBukkit end
++
              this.player.resetLastActionTime();
-             Entity entity;
-             PlayerRideableJumping ijumpable;
-@@ -1616,6 +2651,11 @@
              switch (packet.getAction()) {
                  case PRESS_SHIFT_KEY:
                      this.player.setShiftKeyDown(true);
@@ -1829,179 +1770,173 @@
                      break;
                  case RELEASE_SHIFT_KEY:
                      this.player.setShiftKeyDown(false);
-@@ -1684,15 +2724,25 @@
+@@ -1551,12 +_,20 @@
              }
  
              if (i > 4096) {
--                this.disconnect((Component) Component.translatable("multiplayer.disconnect.too_many_pending_chats"));
-+                this.disconnectAsync((Component) Component.translatable("multiplayer.disconnect.too_many_pending_chats"), org.bukkit.event.player.PlayerKickEvent.Cause.TOO_MANY_PENDING_CHATS); // Paper - kick event cause // Paper - add proper async disconnect
+-                this.disconnect(Component.translatable("multiplayer.disconnect.too_many_pending_chats"));
++                this.disconnectAsync(Component.translatable("multiplayer.disconnect.too_many_pending_chats"), org.bukkit.event.player.PlayerKickEvent.Cause.TOO_MANY_PENDING_CHATS); // Paper - kick event cause & add proper async disconnect
              }
- 
          }
      }
  
-     public void sendPlayerChatMessage(PlayerChatMessage message, ChatType.Bound params) {
+     public void sendPlayerChatMessage(PlayerChatMessage chatMessage, ChatType.Bound boundType) {
 +        // CraftBukkit start - SPIGOT-7262: if hidden we have to send as disguised message. Query whether we should send at all (but changing this may not be expected).
-+        if (!this.getCraftPlayer().canSeePlayer(message.link().sender())) {
-+            this.sendDisguisedChatMessage(message.decoratedContent(), params);
++        if (!this.getCraftPlayer().canSeePlayer(chatMessage.link().sender())) {
++            this.sendDisguisedChatMessage(chatMessage.decoratedContent(), boundType);
 +            return;
 +        }
 +        // CraftBukkit end
 +        // Paper start - Ensure that client receives chat packets in the same order that we add into the message signature cache
 +        synchronized (this.messageSignatureCache) {
-         this.send(new ClientboundPlayerChatPacket(message.link().sender(), message.link().index(), message.signature(), message.signedBody().pack(this.messageSignatureCache), message.unsignedContent(), message.filterMask(), params));
-         this.addPendingMessage(message);
+         this.send(
+             new ClientboundPlayerChatPacket(
+                 chatMessage.link().sender(),
+@@ -1569,6 +_,8 @@
+             )
+         );
+         this.addPendingMessage(chatMessage);
 +        }
 +        // Paper end - Ensure that client receives chat packets in the same order that we add into the message signature cache
      }
  
-     public void sendDisguisedChatMessage(Component message, ChatType.Bound params) {
-@@ -1703,6 +2753,18 @@
+     public void sendDisguisedChatMessage(Component message, ChatType.Bound boundType) {
+@@ -1579,6 +_,17 @@
          return this.connection.getRemoteAddress();
      }
  
-+    // Spigot Start
-+    public SocketAddress getRawAddress()
-+    {
++    // Spigot start
++    public SocketAddress getRawAddress() {
 +        // Paper start - Unix domain socket support; this can be nullable in the case of a Unix domain socket, so if it is, fake something
-+        if (connection.channel.remoteAddress() == null) {
++        if (this.connection.channel.remoteAddress() == null) {
 +            return new java.net.InetSocketAddress(java.net.InetAddress.getLoopbackAddress(), 0);
 +        }
 +        // Paper end - Unix domain socket support
 +        return this.connection.channel.remoteAddress();
 +    }
-+    // Spigot End
++    // Spigot end
 +
      public void switchToConfig() {
          this.waitingForSwitchToConfig = true;
          this.removePlayerFromWorld();
-@@ -1718,9 +2780,17 @@
+@@ -1594,9 +_,16 @@
      @Override
      public void handleInteract(ServerboundInteractPacket packet) {
          PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
 +        if (this.player.isImmobile()) return; // CraftBukkit
          if (this.player.hasClientLoaded()) {
-             final ServerLevel worldserver = this.player.serverLevel();
-             final Entity entity = packet.getTarget(worldserver);
-+            // Spigot Start
-+            if ( entity == this.player && !this.player.isSpectator() )
-+            {
-+                this.disconnect( Component.literal( "Cannot interact with self!" ), org.bukkit.event.player.PlayerKickEvent.Cause.SELF_INTERACTION ); // Paper - kick event cause
+             final ServerLevel serverLevel = this.player.serverLevel();
+             final Entity target = packet.getTarget(serverLevel);
++            // Spigot start
++            if (target == this.player && !this.player.isSpectator()) {
++                this.disconnect(Component.literal("Cannot interact with self!"), org.bukkit.event.player.PlayerKickEvent.Cause.SELF_INTERACTION); // Paper - kick event cause
 +                return;
 +            }
-+            // Spigot End
- 
++            // Spigot end
              this.player.resetLastActionTime();
              this.player.setShiftKeyDown(packet.isUsingSecondaryAction());
-@@ -1731,22 +2801,61 @@
+             if (target != null) {
+@@ -1605,16 +_,55 @@
+                 }
  
-                 AABB axisalignedbb = entity.getBoundingBox();
- 
--                if (this.player.canInteractWithEntity(axisalignedbb, 3.0D)) {
-+            if (this.player.canInteractWithEntity(axisalignedbb, io.papermc.paper.configuration.GlobalConfiguration.get().misc.clientInteractionLeniencyDistance.or(3.0D))) { // Paper - configurable lenience value for interact range
-                     packet.dispatch(new ServerboundInteractPacket.Handler() {
--                        private void performInteraction(InteractionHand hand, ServerGamePacketListenerImpl.EntityInteraction action) {
--                            ItemStack itemstack = ServerGamePacketListenerImpl.this.player.getItemInHand(hand);
-+                        private void performInteraction(InteractionHand enumhand, ServerGamePacketListenerImpl.EntityInteraction playerconnection_a, PlayerInteractEntityEvent event) { // CraftBukkit
-+                            ItemStack itemstack = ServerGamePacketListenerImpl.this.player.getItemInHand(enumhand);
- 
-                             if (itemstack.isItemEnabled(worldserver.enabledFeatures())) {
-                                 ItemStack itemstack1 = itemstack.copy();
--                                InteractionResult enuminteractionresult = action.run(ServerGamePacketListenerImpl.this.player, entity, hand);
-+                                // CraftBukkit start
-+                                ItemStack itemInHand = ServerGamePacketListenerImpl.this.player.getItemInHand(enumhand);
-+                                boolean triggerLeashUpdate = itemInHand != null && itemInHand.getItem() == Items.LEAD && entity instanceof Mob;
-+                                Item origItem = ServerGamePacketListenerImpl.this.player.getInventory().getSelected() == null ? null : ServerGamePacketListenerImpl.this.player.getInventory().getSelected().getItem();
- 
-+                                ServerGamePacketListenerImpl.this.cserver.getPluginManager().callEvent(event);
+                 AABB boundingBox = target.getBoundingBox();
+-                if (this.player.canInteractWithEntity(boundingBox, 3.0)) {
++                if (this.player.canInteractWithEntity(boundingBox, io.papermc.paper.configuration.GlobalConfiguration.get().misc.clientInteractionLeniencyDistance.or(3.0))) { // Paper - configurable lenience value for interact range
+                     packet.dispatch(
+                         new ServerboundInteractPacket.Handler() {
+-                            private void performInteraction(InteractionHand hand, ServerGamePacketListenerImpl.EntityInteraction entityInteraction) {
++                            private void performInteraction(InteractionHand hand, ServerGamePacketListenerImpl.EntityInteraction entityInteraction, PlayerInteractEntityEvent event) { // CraftBukkit
+                                 ItemStack itemInHand = ServerGamePacketListenerImpl.this.player.getItemInHand(hand);
+                                 if (itemInHand.isItemEnabled(serverLevel.enabledFeatures())) {
+                                     ItemStack itemStack = itemInHand.copy();
+-                                    if (entityInteraction.run(ServerGamePacketListenerImpl.this.player, target, hand) instanceof InteractionResult.Success success
+-                                        )
+-                                     {
++                                    // CraftBukkit start
++                                    boolean triggerLeashUpdate = itemInHand != null && itemInHand.getItem() == Items.LEAD && target instanceof net.minecraft.world.entity.Mob;
++                                    Item origItem = ServerGamePacketListenerImpl.this.player.getInventory().getSelected() == null ? null : ServerGamePacketListenerImpl.this.player.getInventory().getSelected().getItem();
 +
-+                                // 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.resendPossiblyDesyncedEntityData(ServerGamePacketListenerImpl.this.player); // Paper - The entire mob gets deleted, so resend it
-+                                    ServerGamePacketListenerImpl.this.player.containerMenu.sendAllDataToRemote();
-+                                }
++                                    ServerGamePacketListenerImpl.this.cserver.getPluginManager().callEvent(event);
 +
-+                                if (triggerLeashUpdate && (event.isCancelled() || ServerGamePacketListenerImpl.this.player.getInventory().getSelected() == null || ServerGamePacketListenerImpl.this.player.getInventory().getSelected().getItem() != origItem)) {
-+                                    // Refresh the current leash state
-+                                    ServerGamePacketListenerImpl.this.send(new ClientboundSetEntityLinkPacket(entity, ((Mob) entity).getLeashHolder()));
-+                                }
-+
-+                                if (event.isCancelled() || ServerGamePacketListenerImpl.this.player.getInventory().getSelected() == null || ServerGamePacketListenerImpl.this.player.getInventory().getSelected().getItem() != origItem) {
-+                                    // Refresh the current entity metadata
-+                                    entity.refreshEntityData(ServerGamePacketListenerImpl.this.player);
-+                                    // SPIGOT-7136 - Allays
-+                                if (entity instanceof Allay || entity instanceof net.minecraft.world.entity.animal.horse.AbstractHorse) { // Paper - Fix horse armor desync
-+                                    ServerGamePacketListenerImpl.this.send(new ClientboundSetEquipmentPacket(entity.getId(), Arrays.stream(net.minecraft.world.entity.EquipmentSlot.values()).map((slot) -> Pair.of(slot, ((LivingEntity) entity).getItemBySlot(slot).copy())).collect(Collectors.toList()), true)); // Paper - sanitize
++                                    // Entity in bucket - SPIGOT-4048 and SPIGOT-6859a
++                                    if ((target instanceof Bucketable && target instanceof LivingEntity && origItem != null && origItem.asItem() == Items.WATER_BUCKET) && (event.isCancelled() || ServerGamePacketListenerImpl.this.player.getInventory().getSelected() == null || ServerGamePacketListenerImpl.this.player.getInventory().getSelected().getItem() != origItem)) {
++                                        target.resendPossiblyDesyncedEntityData(ServerGamePacketListenerImpl.this.player); // Paper - The entire mob gets deleted, so resend it
++                                        ServerGamePacketListenerImpl.this.player.containerMenu.sendAllDataToRemote();
 +                                    }
 +
-+                                ServerGamePacketListenerImpl.this.player.containerMenu.sendAllDataToRemote(); // Paper - fix slot desync - always refresh player inventory
-+                                }
++                                    if (triggerLeashUpdate && (event.isCancelled() || ServerGamePacketListenerImpl.this.player.getInventory().getSelected() == null || ServerGamePacketListenerImpl.this.player.getInventory().getSelected().getItem() != origItem)) {
++                                        // Refresh the current leash state
++                                        ServerGamePacketListenerImpl.this.send(new net.minecraft.network.protocol.game.ClientboundSetEntityLinkPacket(target, ((net.minecraft.world.entity.Mob) target).getLeashHolder()));
++                                    }
 +
-+                                if (event.isCancelled()) {
-+                                    return;
-+                                }
-+                                // CraftBukkit end
-+                                InteractionResult enuminteractionresult = playerconnection_a.run(ServerGamePacketListenerImpl.this.player, entity, enumhand);
++                                    if (event.isCancelled() || ServerGamePacketListenerImpl.this.player.getInventory().getSelected() == null || ServerGamePacketListenerImpl.this.player.getInventory().getSelected().getItem() != origItem) {
++                                        // Refresh the current entity metadata
++                                        target.refreshEntityData(ServerGamePacketListenerImpl.this.player);
++                                        // SPIGOT-7136 - Allays
++                                        if (target instanceof Allay || target instanceof net.minecraft.world.entity.animal.horse.AbstractHorse) { // Paper - Fix horse armor desync
++                                            ServerGamePacketListenerImpl.this.send(new net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket(target.getId(), Arrays.stream(net.minecraft.world.entity.EquipmentSlot.values()).map((slot) -> Pair.of(slot, ((LivingEntity) target).getItemBySlot(slot).copy())).collect(Collectors.toList()), true)); // Paper - sanitize
++                                        }
 +
-+                                // CraftBukkit start
-+                                if (!itemInHand.isEmpty() && itemInHand.getCount() <= -1) {
-+                                    ServerGamePacketListenerImpl.this.player.containerMenu.sendAllDataToRemote();
-+                                }
-+                                // CraftBukkit end
++                                        ServerGamePacketListenerImpl.this.player.containerMenu.sendAllDataToRemote(); // Paper - fix slot desync - always refresh player inventory
++                                    }
++
++                                    if (event.isCancelled()) {
++                                        return;
++                                    }
++                                    // CraftBukkit end
++                                    InteractionResult result = entityInteraction.run(ServerGamePacketListenerImpl.this.player, target, hand);
 +
-                                 if (enuminteractionresult instanceof InteractionResult.Success) {
-                                     InteractionResult.Success enuminteractionresult_d = (InteractionResult.Success) enuminteractionresult;
-                                     ItemStack itemstack2 = enuminteractionresult_d.wasItemInteraction() ? itemstack1 : ItemStack.EMPTY;
- 
-                                     CriteriaTriggers.PLAYER_INTERACTED_WITH_ENTITY.trigger(ServerGamePacketListenerImpl.this.player, itemstack2, entity);
-                                     if (enuminteractionresult_d.swingSource() == InteractionResult.SwingSource.SERVER) {
--                                        ServerGamePacketListenerImpl.this.player.swing(hand, true);
-+                                        ServerGamePacketListenerImpl.this.player.swing(enumhand, true);
-                                     }
-                                 }
- 
-@@ -1755,19 +2864,20 @@
- 
-                         @Override
-                         public void onInteraction(InteractionHand hand) {
--                            this.performInteraction(hand, Player::interactOn);
-+                            this.performInteraction(hand, net.minecraft.world.entity.player.Player::interactOn, new PlayerInteractEntityEvent(ServerGamePacketListenerImpl.this.getCraftPlayer(), entity.getBukkitEntity(), (hand == InteractionHand.OFF_HAND) ? EquipmentSlot.OFF_HAND : EquipmentSlot.HAND)); // CraftBukkit
-                         }
- 
-                         @Override
-                         public void onInteraction(InteractionHand hand, Vec3 pos) {
-                             this.performInteraction(hand, (entityplayer, entity1, enumhand1) -> {
-                                 return entity1.interactAt(entityplayer, pos, enumhand1);
--                            });
-+                            }, new PlayerInteractAtEntityEvent(ServerGamePacketListenerImpl.this.getCraftPlayer(), entity.getBukkitEntity(), new org.bukkit.util.Vector(pos.x, pos.y, pos.z), (hand == InteractionHand.OFF_HAND) ? EquipmentSlot.OFF_HAND : EquipmentSlot.HAND)); // CraftBukkit
-                         }
- 
-                         @Override
-                         public void onAttack() {
--                            if (!(entity instanceof ItemEntity) && !(entity instanceof ExperienceOrb) && entity != ServerGamePacketListenerImpl.this.player) {
-+                            // CraftBukkit
-+                            if (!(entity instanceof ItemEntity) && !(entity instanceof ExperienceOrb) && (entity != ServerGamePacketListenerImpl.this.player || ServerGamePacketListenerImpl.this.player.isSpectator())) {
-                                 label23:
-                                 {
-                                     if (entity instanceof AbstractArrow) {
-@@ -1785,17 +2895,41 @@
-                                     }
- 
-                                     ServerGamePacketListenerImpl.this.player.attack(entity);
 +                                    // CraftBukkit start
-+                                    if (!itemstack.isEmpty() && itemstack.getCount() <= -1) {
++                                    if (!itemInHand.isEmpty() && itemInHand.getCount() <= -1) {
 +                                        ServerGamePacketListenerImpl.this.player.containerMenu.sendAllDataToRemote();
 +                                    }
 +                                    // CraftBukkit end
-                                     return;
-                                 }
++
++                                    if (result instanceof InteractionResult.Success success // CraftBukkit
++                                    ) {
+                                         ItemStack itemStack1 = success.wasItemInteraction() ? itemStack : ItemStack.EMPTY;
+                                         CriteriaTriggers.PLAYER_INTERACTED_WITH_ENTITY.trigger(ServerGamePacketListenerImpl.this.player, itemStack1, target);
+                                         if (success.swingSource() == InteractionResult.SwingSource.SERVER) {
+@@ -1626,13 +_,13 @@
+ 
+                             @Override
+                             public void onInteraction(InteractionHand hand) {
+-                                this.performInteraction(hand, Player::interactOn);
++                                this.performInteraction(hand, Player::interactOn, new PlayerInteractEntityEvent(ServerGamePacketListenerImpl.this.getCraftPlayer(), target.getBukkitEntity(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand))); // CraftBukkit
                              }
  
--                            ServerGamePacketListenerImpl.this.disconnect((Component) Component.translatable("multiplayer.disconnect.invalid_entity_attacked"));
-+                            ServerGamePacketListenerImpl.this.disconnect((Component) Component.translatable("multiplayer.disconnect.invalid_entity_attacked"), org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_ENTITY_ATTACKED); // Paper - add cause
-                             ServerGamePacketListenerImpl.LOGGER.warn("Player {} tried to attack an invalid entity", ServerGamePacketListenerImpl.this.player.getName().getString());
-                         }
-                     });
+                             @Override
+                             public void onInteraction(InteractionHand hand, Vec3 interactionLocation) {
+                                 this.performInteraction(
+-                                    hand, (player, entity, interactionHand) -> entity.interactAt(player, interactionLocation, interactionHand)
++                                    hand, (player, entity, interactionHand) -> entity.interactAt(player, interactionLocation, interactionHand), new PlayerInteractAtEntityEvent(ServerGamePacketListenerImpl.this.getCraftPlayer(), target.getBukkitEntity(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(interactionLocation), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand)) // CraftBukkit
+                                 );
+                             }
+ 
+@@ -1640,14 +_,19 @@
+                             public void onAttack() {
+                                 if (!(target instanceof ItemEntity)
+                                     && !(target instanceof ExperienceOrb)
+-                                    && target != ServerGamePacketListenerImpl.this.player
++                                    && (target != ServerGamePacketListenerImpl.this.player || ServerGamePacketListenerImpl.this.player.isSpectator()) // CraftBukkit
+                                     && !(target instanceof AbstractArrow abstractArrow && !abstractArrow.isAttackable())) {
+                                     ItemStack itemInHand = ServerGamePacketListenerImpl.this.player.getItemInHand(InteractionHand.MAIN_HAND);
+                                     if (itemInHand.isItemEnabled(serverLevel.enabledFeatures())) {
+                                         ServerGamePacketListenerImpl.this.player.attack(target);
++                                        // CraftBukkit start
++                                        if (!itemInHand.isEmpty() && itemInHand.getCount() <= -1) {
++                                            ServerGamePacketListenerImpl.this.player.containerMenu.sendAllDataToRemote();
++                                        }
++                                        // CraftBukkit end
+                                     }
+                                 } else {
+-                                    ServerGamePacketListenerImpl.this.disconnect(Component.translatable("multiplayer.disconnect.invalid_entity_attacked"));
++                                    ServerGamePacketListenerImpl.this.disconnect(Component.translatable("multiplayer.disconnect.invalid_entity_attacked"), org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_ENTITY_ATTACKED); // Paper - add cause
+                                     ServerGamePacketListenerImpl.LOGGER
+                                         .warn("Player {} tried to attack an invalid entity", ServerGamePacketListenerImpl.this.player.getName().getString());
+                                 }
+@@ -1656,6 +_,26 @@
+                     );
                  }
              }
 +            // Paper start - PlayerUseUnknownEntityEvent
@@ -2011,7 +1946,7 @@
 +                    public void onInteraction(net.minecraft.world.InteractionHand hand) {
 +                        CraftEventFactory.callPlayerUseUnknownEntityEvent(ServerGamePacketListenerImpl.this.player, packet, hand, null);
 +                    }
- 
++
 +                    @Override
 +                    public void onInteraction(net.minecraft.world.InteractionHand hand, net.minecraft.world.phys.Vec3 pos) {
 +                        CraftEventFactory.callPlayerUseUnknownEntityEvent(ServerGamePacketListenerImpl.this.player, packet, hand, pos);
@@ -2027,7 +1962,7 @@
          }
      }
  
-@@ -1809,7 +2943,7 @@
+@@ -1668,7 +_,7 @@
              case PERFORM_RESPAWN:
                  if (this.player.wonGame) {
                      this.player.wonGame = false;
@@ -2036,7 +1971,7 @@
                      this.resetPosition();
                      CriteriaTriggers.CHANGED_DIMENSION.trigger(this.player, Level.END, Level.OVERWORLD);
                  } else {
-@@ -1817,11 +2951,11 @@
+@@ -1676,11 +_,11 @@
                          return;
                      }
  
@@ -2045,19 +1980,20 @@
                      this.resetPosition();
                      if (this.server.isHardcore()) {
 -                        this.player.setGameMode(GameType.SPECTATOR);
--                        ((GameRules.BooleanValue) this.player.serverLevel().getGameRules().getRule(GameRules.RULE_SPECTATORSGENERATECHUNKS)).set(false, this.server);
+-                        this.player.serverLevel().getGameRules().getRule(GameRules.RULE_SPECTATORSGENERATECHUNKS).set(false, this.server);
 +                        this.player.setGameMode(GameType.SPECTATOR, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.HARDCORE_DEATH, null); // Paper - Expand PlayerGameModeChangeEvent
-+                        ((GameRules.BooleanValue) this.player.serverLevel().getGameRules().getRule(GameRules.RULE_SPECTATORSGENERATECHUNKS)).set(false, this.player.serverLevel()); // CraftBukkit - per-world
++                        this.player.serverLevel().getGameRules().getRule(GameRules.RULE_SPECTATORSGENERATECHUNKS).set(false, this.player.serverLevel()); // CraftBukkit - per-world
                      }
                  }
                  break;
-@@ -1833,16 +2967,27 @@
+@@ -1691,16 +_,28 @@
  
      @Override
      public void handleContainerClose(ServerboundContainerClosePacket packet) {
 +        // Paper start - Inventory close reason
 +        this.handleContainerClose(packet, org.bukkit.event.inventory.InventoryCloseEvent.Reason.PLAYER);
 +    }
++
 +    public void handleContainerClose(ServerboundContainerClosePacket packet, org.bukkit.event.inventory.InventoryCloseEvent.Reason reason) {
 +        // Paper end - Inventory close reason
          PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
@@ -2080,26 +2016,24 @@
 +            if (false/*this.player.isSpectator()*/) { // CraftBukkit
                  this.player.containerMenu.sendAllDataToRemote();
              } else if (!this.player.containerMenu.stillValid(this.player)) {
-                 ServerGamePacketListenerImpl.LOGGER.debug("Player {} interacted with invalid menu {}", this.player, this.player.containerMenu);
-@@ -1855,7 +3000,315 @@
+                 LOGGER.debug("Player {} interacted with invalid menu {}", this.player, this.player.containerMenu);
+@@ -1713,7 +_,313 @@
+                 } else {
                      boolean flag = packet.getStateId() != this.player.containerMenu.getStateId();
- 
                      this.player.containerMenu.suppressRemoteUpdates();
--                    this.player.containerMenu.clicked(i, packet.getButtonNum(), packet.getClickType(), this.player);
+-                    this.player.containerMenu.clicked(slotNum, packet.getButtonNum(), packet.getClickType(), this.player);
 +                    // CraftBukkit start - Call InventoryClickEvent
-+                    if (packet.getSlotNum() < -1 && packet.getSlotNum() != -999) {
++                    if (slotNum < -1 && slotNum != AbstractContainerMenu.SLOT_CLICKED_OUTSIDE) {
 +                        return;
 +                    }
 +
 +                    InventoryView inventory = this.player.containerMenu.getBukkitView();
-+                    SlotType type = inventory.getSlotType(packet.getSlotNum());
++                    SlotType type = inventory.getSlotType(slotNum);
 +
 +                    InventoryClickEvent event;
 +                    ClickType click = ClickType.UNKNOWN;
 +                    InventoryAction action = InventoryAction.UNKNOWN;
 +
-+                    ItemStack itemstack = ItemStack.EMPTY;
-+
 +                    switch (packet.getClickType()) {
 +                        case PICKUP:
 +                            if (packet.getButtonNum() == 0) {
@@ -2109,14 +2043,14 @@
 +                            }
 +                            if (packet.getButtonNum() == 0 || packet.getButtonNum() == 1) {
 +                                action = InventoryAction.NOTHING; // Don't want to repeat ourselves
-+                                if (packet.getSlotNum() == -999) {
++                                if (slotNum == AbstractContainerMenu.SLOT_CLICKED_OUTSIDE) {
 +                                    if (!this.player.containerMenu.getCarried().isEmpty()) {
 +                                        action = packet.getButtonNum() == 0 ? InventoryAction.DROP_ALL_CURSOR : InventoryAction.DROP_ONE_CURSOR;
 +                                    }
-+                                } else if (packet.getSlotNum() < 0)  {
++                                } else if (slotNum < 0)  {
 +                                    action = InventoryAction.NOTHING;
 +                                } else {
-+                                    Slot slot = this.player.containerMenu.getSlot(packet.getSlotNum());
++                                    Slot slot = this.player.containerMenu.getSlot(slotNum);
 +                                    if (slot != null) {
 +                                        ItemStack clickedItem = slot.getItem();
 +                                        ItemStack cursor = this.player.containerMenu.getCarried();
@@ -2165,10 +2099,10 @@
 +                                click = ClickType.SHIFT_RIGHT;
 +                            }
 +                            if (packet.getButtonNum() == 0 || packet.getButtonNum() == 1) {
-+                                if (packet.getSlotNum() < 0) {
++                                if (slotNum < 0) {
 +                                    action = InventoryAction.NOTHING;
 +                                } else {
-+                                    Slot slot = this.player.containerMenu.getSlot(packet.getSlotNum());
++                                    Slot slot = this.player.containerMenu.getSlot(slotNum);
 +                                    if (slot != null && slot.mayPickup(this.player) && slot.hasItem()) {
 +                                        action = InventoryAction.MOVE_TO_OTHER_INVENTORY;
 +                                    } else {
@@ -2177,150 +2111,150 @@
 +                                }
 +                            }
 +                            break;
-+                        case SWAP:
-+                            if ((packet.getButtonNum() >= 0 && packet.getButtonNum() < 9) || packet.getButtonNum() == 40) {
-+                                // Paper start - Add slot sanity checks to container clicks
-+                                if (packet.getSlotNum() < 0) {
-+                                    action = InventoryAction.NOTHING;
-+                                    break;
-+                                }
-+                                // Paper end - Add slot sanity checks to container clicks
-+                                click = (packet.getButtonNum() == 40) ? ClickType.SWAP_OFFHAND : ClickType.NUMBER_KEY;
-+                                Slot clickedSlot = this.player.containerMenu.getSlot(packet.getSlotNum());
-+                                if (clickedSlot.mayPickup(this.player)) {
-+                                    ItemStack hotbar = this.player.getInventory().getItem(packet.getButtonNum());
-+                                    if ((!hotbar.isEmpty() && clickedSlot.mayPlace(hotbar)) || (hotbar.isEmpty() && clickedSlot.hasItem())) { // Paper - modernify this logic (no such thing as a "hotbar move and readd"
-+                                        action = InventoryAction.HOTBAR_SWAP;
++                            case SWAP:
++                                if ((packet.getButtonNum() >= 0 && packet.getButtonNum() < 9) || packet.getButtonNum() == Inventory.SLOT_OFFHAND) {
++                                    // Paper start - Add slot sanity checks to container clicks
++                                    if (slotNum < 0) {
++                                        action = InventoryAction.NOTHING;
++                                        break;
++                                    }
++                                    // Paper end - Add slot sanity checks to container clicks
++                                    click = (packet.getButtonNum() == Inventory.SLOT_OFFHAND) ? ClickType.SWAP_OFFHAND : ClickType.NUMBER_KEY;
++                                    Slot clickedSlot = this.player.containerMenu.getSlot(slotNum);
++                                    if (clickedSlot.mayPickup(this.player)) {
++                                        ItemStack hotbar = this.player.getInventory().getItem(packet.getButtonNum());
++                                        if ((!hotbar.isEmpty() && clickedSlot.mayPlace(hotbar)) || (hotbar.isEmpty() && clickedSlot.hasItem())) { // Paper - modernify this logic (no such thing as a "hotbar move and readd"
++                                            action = InventoryAction.HOTBAR_SWAP;
++                                        } else {
++                                            action = InventoryAction.NOTHING;
++                                        }
 +                                    } else {
 +                                        action = InventoryAction.NOTHING;
 +                                    }
++                                }
++                                break;
++                            case CLONE:
++                                if (packet.getButtonNum() == 2) {
++                                    click = ClickType.MIDDLE;
++                                    if (slotNum < 0) {
++                                        action = InventoryAction.NOTHING;
++                                    } else {
++                                        Slot slot = this.player.containerMenu.getSlot(slotNum);
++                                        if (slot != null && slot.hasItem() && this.player.getAbilities().instabuild && this.player.containerMenu.getCarried().isEmpty()) {
++                                            action = InventoryAction.CLONE_STACK;
++                                        } else {
++                                            action = InventoryAction.NOTHING;
++                                        }
++                                    }
 +                                } else {
-+                                    action = InventoryAction.NOTHING;
++                                    click = ClickType.UNKNOWN;
++                                    action = InventoryAction.UNKNOWN;
 +                                }
-+                            }
-+                            break;
-+                        case CLONE:
-+                            if (packet.getButtonNum() == 2) {
-+                                click = ClickType.MIDDLE;
-+                                if (packet.getSlotNum() < 0) {
-+                                    action = InventoryAction.NOTHING;
++                                break;
++                            case THROW:
++                                if (slotNum >= 0) {
++                                    if (packet.getButtonNum() == 0) {
++                                        click = ClickType.DROP;
++                                        Slot slot = this.player.containerMenu.getSlot(slotNum);
++                                        if (slot != null && slot.hasItem() && slot.mayPickup(this.player) && !slot.getItem().isEmpty() && slot.getItem().getItem() != Items.AIR) {
++                                            action = InventoryAction.DROP_ONE_SLOT;
++                                        } else {
++                                            action = InventoryAction.NOTHING;
++                                        }
++                                    } else if (packet.getButtonNum() == 1) {
++                                        click = ClickType.CONTROL_DROP;
++                                        Slot slot = this.player.containerMenu.getSlot(slotNum);
++                                        if (slot != null && slot.hasItem() && slot.mayPickup(this.player) && !slot.getItem().isEmpty() && slot.getItem().getItem() != Items.AIR) {
++                                            action = InventoryAction.DROP_ALL_SLOT;
++                                        } else {
++                                            action = InventoryAction.NOTHING;
++                                        }
++                                    }
 +                                } else {
-+                                    Slot slot = this.player.containerMenu.getSlot(packet.getSlotNum());
-+                                    if (slot != null && slot.hasItem() && this.player.getAbilities().instabuild && this.player.containerMenu.getCarried().isEmpty()) {
-+                                        action = InventoryAction.CLONE_STACK;
-+                                    } else {
-+                                        action = InventoryAction.NOTHING;
++                                    // Sane default (because this happens when they are holding nothing. Don't ask why.)
++                                    click = ClickType.LEFT;
++                                    if (packet.getButtonNum() == 1) {
++                                        click = ClickType.RIGHT;
++                                    }
++                                    action = InventoryAction.NOTHING;
++                                }
++                                break;
++                            case QUICK_CRAFT:
++                                // Paper start - Fix CraftBukkit drag system
++                                AbstractContainerMenu containerMenu = this.player.containerMenu;
++                                int currentStatus = this.player.containerMenu.quickcraftStatus;
++                                int newStatus = AbstractContainerMenu.getQuickcraftHeader(packet.getButtonNum());
++                                if ((currentStatus != 1 || newStatus != 2 && currentStatus != newStatus)) {
++                                } else if (containerMenu.getCarried().isEmpty()) {
++                                } else if (newStatus == 0) {
++                                } else if (newStatus == 1) {
++                                } else if (newStatus == 2) {
++                                    if (!this.player.containerMenu.quickcraftSlots.isEmpty()) {
++                                        if (this.player.containerMenu.quickcraftSlots.size() == 1) {
++                                            int index = containerMenu.quickcraftSlots.iterator().next().index;
++                                            containerMenu.resetQuickCraft();
++                                            this.handleContainerClick(new ServerboundContainerClickPacket(packet.getContainerId(), packet.getStateId(), index, containerMenu.quickcraftType, net.minecraft.world.inventory.ClickType.PICKUP, packet.getCarriedItem(), packet.getChangedSlots()));
++                                            return;
++                                        }
 +                                    }
 +                                }
-+                            } else {
-+                                click = ClickType.UNKNOWN;
-+                                action = InventoryAction.UNKNOWN;
-+                            }
-+                            break;
-+                        case THROW:
-+                            if (packet.getSlotNum() >= 0) {
-+                                if (packet.getButtonNum() == 0) {
-+                                    click = ClickType.DROP;
-+                                    Slot slot = this.player.containerMenu.getSlot(packet.getSlotNum());
-+                                    if (slot != null && slot.hasItem() && slot.mayPickup(this.player) && !slot.getItem().isEmpty() && slot.getItem().getItem() != Item.byBlock(Blocks.AIR)) {
-+                                        action = InventoryAction.DROP_ONE_SLOT;
-+                                    } else {
-+                                        action = InventoryAction.NOTHING;
-+                                    }
-+                                } else if (packet.getButtonNum() == 1) {
-+                                    click = ClickType.CONTROL_DROP;
-+                                    Slot slot = this.player.containerMenu.getSlot(packet.getSlotNum());
-+                                    if (slot != null && slot.hasItem() && slot.mayPickup(this.player) && !slot.getItem().isEmpty() && slot.getItem().getItem() != Item.byBlock(Blocks.AIR)) {
-+                                        action = InventoryAction.DROP_ALL_SLOT;
-+                                    } else {
-+                                        action = InventoryAction.NOTHING;
-+                                    }
-+                                }
-+                            } else {
-+                                // Sane default (because this happens when they are holding nothing. Don't ask why.)
-+                                click = ClickType.LEFT;
-+                                if (packet.getButtonNum() == 1) {
-+                                    click = ClickType.RIGHT;
-+                                }
++                                // Paper end - Fix CraftBukkit drag system
++                                this.player.containerMenu.clicked(slotNum, packet.getButtonNum(), packet.getClickType(), this.player);
++                                break;
++                            case PICKUP_ALL:
++                                click = ClickType.DOUBLE_CLICK;
 +                                action = InventoryAction.NOTHING;
-+                            }
-+                            break;
-+                        case QUICK_CRAFT:
-+                            // Paper start - Fix CraftBukkit drag system
-+                            AbstractContainerMenu containerMenu = this.player.containerMenu;
-+                            int currentStatus = this.player.containerMenu.quickcraftStatus;
-+                            int newStatus = AbstractContainerMenu.getQuickcraftHeader(packet.getButtonNum());
-+                            if ((currentStatus != 1 || newStatus != 2 && currentStatus != newStatus)) {
-+                            } else if (containerMenu.getCarried().isEmpty()) {
-+                            } else if (newStatus == 0) {
-+                            } else if (newStatus == 1) {
-+                            } else if (newStatus == 2) {
-+                                if (!this.player.containerMenu.quickcraftSlots.isEmpty()) {
-+                                    if (this.player.containerMenu.quickcraftSlots.size() == 1) {
-+                                        int index = containerMenu.quickcraftSlots.iterator().next().index;
-+                                        containerMenu.resetQuickCraft();
-+                                        this.handleContainerClick(new ServerboundContainerClickPacket(packet.getContainerId(), packet.getStateId(), index, containerMenu.quickcraftType, net.minecraft.world.inventory.ClickType.PICKUP, packet.getCarriedItem(), packet.getChangedSlots()));
-+                                        return;
++                                if (slotNum >= 0 && !this.player.containerMenu.getCarried().isEmpty()) {
++                                    ItemStack cursor = this.player.containerMenu.getCarried();
++                                    action = InventoryAction.NOTHING;
++                                    // Quick check for if we have any of the item
++                                    if (inventory.getTopInventory().contains(CraftItemType.minecraftToBukkit(cursor.getItem())) || inventory.getBottomInventory().contains(CraftItemType.minecraftToBukkit(cursor.getItem()))) {
++                                        action = InventoryAction.COLLECT_TO_CURSOR;
 +                                    }
 +                                }
-+                            }
-+                            // Paper end - Fix CraftBukkit drag system
-+                            this.player.containerMenu.clicked(packet.getSlotNum(), packet.getButtonNum(), packet.getClickType(), this.player);
-+                            break;
-+                        case PICKUP_ALL:
-+                            click = ClickType.DOUBLE_CLICK;
-+                            action = InventoryAction.NOTHING;
-+                            if (packet.getSlotNum() >= 0 && !this.player.containerMenu.getCarried().isEmpty()) {
-+                                ItemStack cursor = this.player.containerMenu.getCarried();
-+                                action = InventoryAction.NOTHING;
-+                                // Quick check for if we have any of the item
-+                                if (inventory.getTopInventory().contains(CraftItemType.minecraftToBukkit(cursor.getItem())) || inventory.getBottomInventory().contains(CraftItemType.minecraftToBukkit(cursor.getItem()))) {
-+                                    action = InventoryAction.COLLECT_TO_CURSOR;
-+                                }
-+                            }
-+                            break;
-+                        default:
-+                            break;
++                                break;
++                            default:
++                                break;
 +                    }
 +
 +                    if (packet.getClickType() != net.minecraft.world.inventory.ClickType.QUICK_CRAFT) {
 +                        if (click == ClickType.NUMBER_KEY) {
-+                            event = new InventoryClickEvent(inventory, type, packet.getSlotNum(), click, action, packet.getButtonNum());
++                            event = new InventoryClickEvent(inventory, type, slotNum, click, action, packet.getButtonNum());
 +                        } else {
-+                            event = new InventoryClickEvent(inventory, type, packet.getSlotNum(), click, action);
++                            event = new InventoryClickEvent(inventory, type, slotNum, click, action);
 +                        }
 +
 +                        org.bukkit.inventory.Inventory top = inventory.getTopInventory();
-+                        if (packet.getSlotNum() == 0 && top instanceof CraftingInventory) {
++                        if (slotNum == 0 && top instanceof CraftingInventory) {
 +                            org.bukkit.inventory.Recipe recipe = ((CraftingInventory) top).getRecipe();
 +                            if (recipe != null) {
 +                                if (click == ClickType.NUMBER_KEY) {
-+                                    event = new CraftItemEvent(recipe, inventory, type, packet.getSlotNum(), click, action, packet.getButtonNum());
++                                    event = new CraftItemEvent(recipe, inventory, type, slotNum, click, action, packet.getButtonNum());
 +                                } else {
-+                                    event = new CraftItemEvent(recipe, inventory, type, packet.getSlotNum(), click, action);
++                                    event = new CraftItemEvent(recipe, inventory, type, slotNum, click, action);
 +                                }
 +                            }
 +                        }
 +
-+                        if (packet.getSlotNum() == 3 && top instanceof SmithingInventory) {
++                        if (slotNum == 3 && top instanceof SmithingInventory) {
 +                            org.bukkit.inventory.ItemStack result = ((SmithingInventory) top).getResult();
 +                            if (result != null) {
 +                                if (click == ClickType.NUMBER_KEY) {
-+                                    event = new SmithItemEvent(inventory, type, packet.getSlotNum(), click, action, packet.getButtonNum());
++                                    event = new SmithItemEvent(inventory, type, slotNum, click, action, packet.getButtonNum());
 +                                } else {
-+                                    event = new SmithItemEvent(inventory, type, packet.getSlotNum(), click, action);
++                                    event = new SmithItemEvent(inventory, type, slotNum, click, action);
 +                                }
 +                            }
 +                        }
 +
 +                        // Paper start - cartography item event
-+                        if (packet.getSlotNum() == net.minecraft.world.inventory.CartographyTableMenu.RESULT_SLOT && top instanceof org.bukkit.inventory.CartographyInventory cartographyInventory) {
++                        if (slotNum == net.minecraft.world.inventory.CartographyTableMenu.RESULT_SLOT && top instanceof org.bukkit.inventory.CartographyInventory cartographyInventory) {
 +                            org.bukkit.inventory.ItemStack result = cartographyInventory.getResult();
 +                            if (result != null && !result.isEmpty()) {
 +                                if (click == ClickType.NUMBER_KEY) {
-+                                    event = new io.papermc.paper.event.player.CartographyItemEvent(inventory, type, packet.getSlotNum(), click, action, packet.getButtonNum());
++                                    event = new io.papermc.paper.event.player.CartographyItemEvent(inventory, type, slotNum, click, action, packet.getButtonNum());
 +                                } else {
-+                                    event = new io.papermc.paper.event.player.CartographyItemEvent(inventory, type, packet.getSlotNum(), click, action);
++                                    event = new io.papermc.paper.event.player.CartographyItemEvent(inventory, type, slotNum, click, action);
 +                                }
 +                            }
 +                        }
@@ -2336,7 +2270,7 @@
 +                        switch (event.getResult()) {
 +                            case ALLOW:
 +                            case DEFAULT:
-+                                this.player.containerMenu.clicked(i, packet.getButtonNum(), packet.getClickType(), this.player);
++                                this.player.containerMenu.clicked(slotNum, packet.getButtonNum(), packet.getClickType(), this.player);
 +                                break;
 +                            case DENY:
 +                                /* Needs enum constructor in InventoryAction
@@ -2369,12 +2303,12 @@
 +                                    case PLACE_ONE:
 +                                    case SWAP_WITH_CURSOR:
 +                                        this.player.connection.send(new net.minecraft.network.protocol.game.ClientboundSetCursorItemPacket(this.player.containerMenu.getCarried().copy())); // Paper - correctly set cursor
-+                                        this.player.connection.send(new ClientboundContainerSetSlotPacket(this.player.containerMenu.containerId, this.player.inventoryMenu.incrementStateId(), packet.getSlotNum(), this.player.containerMenu.getSlot(packet.getSlotNum()).getItem()));
++                                        this.player.connection.send(new ClientboundContainerSetSlotPacket(this.player.containerMenu.containerId, this.player.inventoryMenu.incrementStateId(), slotNum, this.player.containerMenu.getSlot(slotNum).getItem()));
 +                                        break;
 +                                    // Modified clicked only
 +                                    case DROP_ALL_SLOT:
 +                                    case DROP_ONE_SLOT:
-+                                        this.player.connection.send(new ClientboundContainerSetSlotPacket(this.player.containerMenu.containerId, this.player.inventoryMenu.incrementStateId(), packet.getSlotNum(), this.player.containerMenu.getSlot(packet.getSlotNum()).getItem()));
++                                        this.player.connection.send(new ClientboundContainerSetSlotPacket(this.player.containerMenu.containerId, this.player.inventoryMenu.incrementStateId(), slotNum, this.player.containerMenu.getSlot(slotNum).getItem()));
 +                                        break;
 +                                    // Modified cursor only
 +                                    case DROP_ALL_CURSOR:
@@ -2394,11 +2328,11 @@
 +                            this.player.containerMenu.sendAllDataToRemote();
 +                        }
 +                    }
-+                    // CraftBukkit end
-                     ObjectIterator objectiterator = Int2ObjectMaps.fastIterable(packet.getChangedSlots()).iterator();
++                   // CraftBukkit end
  
-                     while (objectiterator.hasNext()) {
-@@ -1879,6 +3332,14 @@
+                     for (Entry<ItemStack> entry : Int2ObjectMaps.fastIterable(packet.getChangedSlots())) {
+                         this.player.containerMenu.setRemoteSlotNoCopy(entry.getIntKey(), entry.getValue());
+@@ -1733,6 +_,14 @@
  
      @Override
      public void handlePlaceRecipe(ServerboundPlaceRecipePacket packet) {
@@ -2413,12 +2347,12 @@
          PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
          this.player.resetLastActionTime();
          if (!this.player.isSpectator() && this.player.containerMenu.containerId == packet.containerId()) {
-@@ -1900,9 +3361,43 @@
-                                 ServerGamePacketListenerImpl.LOGGER.debug("Player {} tried to place impossible recipe {}", this.player, recipeholder.id().location());
+@@ -1749,9 +_,44 @@
                                  return;
                              }
+ 
 +                            // Paper start - Add PlayerRecipeBookClickEvent
-+                            NamespacedKey recipeName = org.bukkit.craftbukkit.util.CraftNamespacedKey.fromMinecraft(recipeholder.id().location());
++                            NamespacedKey recipeName = org.bukkit.craftbukkit.util.CraftNamespacedKey.fromMinecraft(recipeHolder.id().location());
 +                            boolean makeAll = packet.useMaxItems();
 +                            com.destroystokyo.paper.event.player.PlayerRecipeBookClickEvent paperEvent = new com.destroystokyo.paper.event.player.PlayerRecipeBookClickEvent(
 +                                this.player.getBukkitEntity(), recipeName, makeAll
@@ -2429,36 +2363,37 @@
 +                            recipeName = paperEvent.getRecipe();
 +                            makeAll = paperEvent.isMakeAll();
 +                            if (org.bukkit.event.player.PlayerRecipeBookClickEvent.getHandlerList().getRegisteredListeners().length > 0) {
-+                            // Paper end - Add PlayerRecipeBookClickEvent
- 
--                            RecipeBookMenu.PostPlaceAction containerrecipebook_a = containerrecipebook.handlePlacement(packet.useMaxItems(), this.player.isCreative(), recipeholder, this.player.serverLevel(), this.player.getInventory());
-+                            // CraftBukkit start - implement PlayerRecipeBookClickEvent
-+                            org.bukkit.inventory.Recipe recipe = this.cserver.getRecipe(recipeName); // Paper - Add PlayerRecipeBookClickEvent - forward to legacy event
-+                            if (recipe == null) {
-+                                return;
-+                            }
-+                            // Paper start - Add PlayerRecipeBookClickEvent - forward to legacy event
-+                            org.bukkit.event.player.PlayerRecipeBookClickEvent event = CraftEventFactory.callRecipeBookClickEvent(this.player, recipe, makeAll);
-+                            recipeName = ((org.bukkit.Keyed) event.getRecipe()).getKey();
-+                            makeAll = event.isShiftClick();
++                                // Paper end - Add PlayerRecipeBookClickEvent
++                                // CraftBukkit start - implement PlayerRecipeBookClickEvent
++                                org.bukkit.inventory.Recipe recipe = this.cserver.getRecipe(recipeName); // Paper - Add PlayerRecipeBookClickEvent - forward to legacy event
++                                if (recipe == null) {
++                                    return;
++                                }
++                                // Paper start - Add PlayerRecipeBookClickEvent - forward to legacy event
++                                org.bukkit.event.player.PlayerRecipeBookClickEvent event = CraftEventFactory.callRecipeBookClickEvent(this.player, recipe, makeAll);
++                                recipeName = ((org.bukkit.Keyed) event.getRecipe()).getKey();
++                                makeAll = event.isShiftClick();
 +                            }
 +                            if (!(this.player.containerMenu instanceof RecipeBookMenu)) {
 +                                return;
 +                            }
 +                            // Paper end - Add PlayerRecipeBookClickEvent - forward to legacy event
- 
++
 +                            // Cast to keyed should be safe as the recipe will never be a MerchantRecipe.
-+                            recipeholder = this.server.getRecipeManager().byKey(net.minecraft.resources.ResourceKey.create(net.minecraft.core.registries.Registries.RECIPE, org.bukkit.craftbukkit.util.CraftNamespacedKey.toMinecraft(recipeName))).orElse(null); // Paper - Add PlayerRecipeBookClickEvent - forward to legacy event
-+                            if (recipeholder == null) {
++                            recipeHolder = this.server.getRecipeManager().byKey(net.minecraft.resources.ResourceKey.create(net.minecraft.core.registries.Registries.RECIPE, org.bukkit.craftbukkit.util.CraftNamespacedKey.toMinecraft(recipeName))).orElse(null); // Paper - Add PlayerRecipeBookClickEvent - forward to legacy event
++                            if (recipeHolder == null) {
 +                                return;
 +                            }
-+                            RecipeBookMenu.PostPlaceAction containerrecipebook_a = containerrecipebook.handlePlacement(makeAll, this.player.isCreative(), recipeholder, this.player.serverLevel(), this.player.getInventory());
-+                            // CraftBukkit end
 +
-                             if (containerrecipebook_a == RecipeBookMenu.PostPlaceAction.PLACE_GHOST_RECIPE) {
-                                 this.player.connection.send(new ClientboundPlaceGhostRecipePacket(this.player.containerMenu.containerId, craftingmanager_d.display().display()));
-                             }
-@@ -1917,6 +3412,7 @@
+                             RecipeBookMenu.PostPlaceAction postPlaceAction = recipeBookMenu.handlePlacement(
+-                                packet.useMaxItems(), this.player.isCreative(), recipeHolder, this.player.serverLevel(), this.player.getInventory()
++                                makeAll, this.player.isCreative(), recipeHolder, this.player.serverLevel(), this.player.getInventory()
+                             );
++                            // CraftBukkit end
+                             if (postPlaceAction == RecipeBookMenu.PostPlaceAction.PLACE_GHOST_RECIPE) {
+                                 this.player
+                                     .connection
+@@ -1767,6 +_,7 @@
      @Override
      public void handleContainerButtonClick(ServerboundContainerButtonClickPacket packet) {
          PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
@@ -2466,15 +2401,15 @@
          this.player.resetLastActionTime();
          if (this.player.containerMenu.containerId == packet.containerId() && !this.player.isSpectator()) {
              if (!this.player.containerMenu.stillValid(this.player)) {
-@@ -1945,7 +3441,44 @@
+@@ -1792,6 +_,43 @@
  
              boolean flag1 = packet.slotNum() >= 1 && packet.slotNum() <= 45;
-             boolean flag2 = itemstack.isEmpty() || itemstack.getCount() <= itemstack.getMaxStackSize();
+             boolean flag2 = itemStack.isEmpty() || itemStack.getCount() <= itemStack.getMaxStackSize();
 +            if (flag || (flag1 && !ItemStack.matches(this.player.inventoryMenu.getSlot(packet.slotNum()).getItem(), packet.itemStack()))) { // Insist on valid slot
 +                // CraftBukkit start - Call click event
 +                InventoryView inventory = this.player.inventoryMenu.getBukkitView();
 +                org.bukkit.inventory.ItemStack item = CraftItemStack.asBukkitCopy(packet.itemStack());
- 
++
 +                SlotType type = SlotType.QUICKBAR;
 +                if (flag) {
 +                    type = SlotType.OUTSIDE;
@@ -2485,37 +2420,36 @@
 +                        type = SlotType.CONTAINER;
 +                    }
 +                }
-+                InventoryCreativeEvent event = new InventoryCreativeEvent(inventory, type, flag ? -999 : packet.slotNum(), item);
++                InventoryCreativeEvent event = new InventoryCreativeEvent(inventory, type, flag ? AbstractContainerMenu.SLOT_CLICKED_OUTSIDE : packet.slotNum(), item);
 +                this.cserver.getPluginManager().callEvent(event);
 +
-+                itemstack = CraftItemStack.asNMSCopy(event.getCursor());
++                itemStack = CraftItemStack.asNMSCopy(event.getCursor());
 +
 +                switch (event.getResult()) {
-+                case ALLOW:
-+                    // Plugin cleared the id / stacksize checks
-+                    flag2 = true;
-+                    break;
-+                case DEFAULT:
-+                    break;
-+                case DENY:
-+                    // Reset the slot
-+                    if (packet.slotNum() >= 0) {
-+                        this.player.connection.send(new ClientboundContainerSetSlotPacket(this.player.inventoryMenu.containerId, this.player.inventoryMenu.incrementStateId(), packet.slotNum(), this.player.inventoryMenu.getSlot(packet.slotNum()).getItem()));
-+                        this.player.connection.send(new net.minecraft.network.protocol.game.ClientboundSetCursorItemPacket(ItemStack.EMPTY.copy())); // Paper - correctly set cursor
-+                    }
-+                    return;
++                    case ALLOW:
++                        // Plugin cleared the id / stacksize checks
++                        flag2 = true;
++                        break;
++                    case DEFAULT:
++                        break;
++                    case DENY:
++                        // Reset the slot
++                        if (packet.slotNum() >= 0) {
++                            this.player.connection.send(new ClientboundContainerSetSlotPacket(this.player.inventoryMenu.containerId, this.player.inventoryMenu.incrementStateId(), packet.slotNum(), this.player.inventoryMenu.getSlot(packet.slotNum()).getItem()));
++                            this.player.connection.send(new net.minecraft.network.protocol.game.ClientboundSetCursorItemPacket(ItemStack.EMPTY.copy())); // Paper - correctly set cursor
++                        }
++                        return;
 +                }
 +            }
 +            // CraftBukkit end
-+
              if (flag1 && flag2) {
-                 this.player.inventoryMenu.getSlot(packet.slotNum()).setByPlayer(itemstack);
-                 this.player.inventoryMenu.setRemoteSlot(packet.slotNum(), itemstack);
-@@ -1964,7 +3497,19 @@
+                 this.player.inventoryMenu.getSlot(packet.slotNum()).setByPlayer(itemStack);
+                 this.player.inventoryMenu.setRemoteSlot(packet.slotNum(), itemStack);
+@@ -1809,11 +_,24 @@
  
      @Override
      public void handleSignUpdate(ServerboundSignUpdatePacket packet) {
--        List<String> list = (List) Stream.of(packet.getLines()).map(ChatFormatting::stripFormatting).collect(Collectors.toList());
+-        List<String> list = Stream.of(packet.getLines()).map(ChatFormatting::stripFormatting).collect(Collectors.toList());
 +        // Paper start - Limit client sign length
 +        String[] lines = packet.getLines();
 +        for (int i = 0; i < lines.length; ++i) {
@@ -2527,20 +2461,17 @@
 +                }
 +            }
 +        }
-+        List<String> list = (List) Stream.of(lines).map(ChatFormatting::stripFormatting).collect(Collectors.toList());
++        List<String> list = Stream.of(lines).map(ChatFormatting::stripFormatting).collect(Collectors.toList());
 +        // Paper end - Limit client sign length
- 
-         this.filterTextPacket(list).thenAcceptAsync((list1) -> {
-             this.updateSignText(packet, list1);
-@@ -1972,6 +3517,7 @@
+         this.filterTextPacket(list).thenAcceptAsync(list1 -> this.updateSignText(packet, (List<FilteredText>)list1), this.server);
      }
  
-     private void updateSignText(ServerboundSignUpdatePacket packet, List<FilteredText> signText) {
+     private void updateSignText(ServerboundSignUpdatePacket packet, List<FilteredText> filteredText) {
 +        if (this.player.isImmobile()) return; // CraftBukkit
          this.player.resetLastActionTime();
-         ServerLevel worldserver = this.player.serverLevel();
-         BlockPos blockposition = packet.getPos();
-@@ -1993,15 +3539,33 @@
+         ServerLevel serverLevel = this.player.serverLevel();
+         BlockPos pos = packet.getPos();
+@@ -1829,14 +_,32 @@
      @Override
      public void handlePlayerAbilities(ServerboundPlayerAbilitiesPacket packet) {
          PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
@@ -2563,19 +2494,18 @@
          PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
 +        // Paper start - do not accept invalid information
 +        if (packet.information().viewDistance() < 0) {
-+            LOGGER.warn("Disconnecting " + this.player.getScoreboardName() + " for invalid view distance: " + packet.information().viewDistance());
++            LOGGER.warn("Disconnecting {} for invalid view distance: {}", this.player.getScoreboardName(), packet.information().viewDistance());
 +            this.disconnect(Component.literal("Invalid client settings"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION);
 +            return;
 +        }
 +        // Paper end - do not accept invalid information
-         boolean flag = this.player.isModelPartShown(PlayerModelPart.HAT);
- 
+         boolean isModelPartShown = this.player.isModelPartShown(PlayerModelPart.HAT);
          this.player.updateOptions(packet.information());
 +        this.connection.channel.attr(io.papermc.paper.adventure.PaperAdventure.LOCALE_ATTRIBUTE).set(net.kyori.adventure.translation.Translator.parseLocale(packet.information().language())); // Paper
-         if (this.player.isModelPartShown(PlayerModelPart.HAT) != flag) {
+         if (this.player.isModelPartShown(PlayerModelPart.HAT) != isModelPartShown) {
              this.server.getPlayerList().broadcastAll(new ClientboundPlayerInfoUpdatePacket(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_HAT, this.player));
          }
-@@ -2012,7 +3576,7 @@
+@@ -1846,7 +_,7 @@
      public void handleChangeDifficulty(ServerboundChangeDifficultyPacket packet) {
          PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
          if (this.player.hasPermissions(2) || this.isSingleplayerOwner()) {
@@ -2584,66 +2514,74 @@
          }
      }
  
-@@ -2033,7 +3597,7 @@
- 
-         if (!Objects.equals(profilepublickey_a, profilepublickey_a1)) {
-             if (profilepublickey_a != null && profilepublickey_a1.expiresAt().isBefore(profilepublickey_a.expiresAt())) {
+@@ -1866,7 +_,7 @@
+         ProfilePublicKey.Data data2 = data.profilePublicKey();
+         if (!Objects.equals(data1, data2)) {
+             if (data1 != null && data2.expiresAt().isBefore(data1.expiresAt())) {
 -                this.disconnect(ProfilePublicKey.EXPIRED_PROFILE_PUBLIC_KEY);
 +                this.disconnect(ProfilePublicKey.EXPIRED_PROFILE_PUBLIC_KEY, org.bukkit.event.player.PlayerKickEvent.Cause.EXPIRED_PROFILE_PUBLIC_KEY); // Paper - kick event causes
              } else {
                  try {
-                     SignatureValidator signaturevalidator = this.server.getProfileKeySignatureValidator();
-@@ -2045,8 +3609,8 @@
+                     SignatureValidator profileKeySignatureValidator = this.server.getProfileKeySignatureValidator();
+@@ -1877,8 +_,8 @@
  
-                     this.resetPlayerChatState(remotechatsession_a.validate(this.player.getGameProfile(), signaturevalidator));
-                 } catch (ProfilePublicKey.ValidationException profilepublickey_b) {
--                    ServerGamePacketListenerImpl.LOGGER.error("Failed to validate profile key: {}", profilepublickey_b.getMessage());
--                    this.disconnect(profilepublickey_b.getComponent());
-+                    // ServerGamePacketListenerImpl.LOGGER.error("Failed to validate profile key: {}", profilepublickey_b.getMessage()); // Paper - Improve logging and errors
-+                    this.disconnect(profilepublickey_b.getComponent(), profilepublickey_b.kickCause); // Paper - kick event causes
+                     this.resetPlayerChatState(data.validate(this.player.getGameProfile(), profileKeySignatureValidator));
+                 } catch (ProfilePublicKey.ValidationException var6) {
+-                    LOGGER.error("Failed to validate profile key: {}", var6.getMessage());
+-                    this.disconnect(var6.getComponent());
++                    // LOGGER.error("Failed to validate profile key: {}", var6.getMessage()); // Paper - Improve logging and errors
++                    this.disconnect(var6.getComponent(), var6.kickCause); // Paper - kick event causes
                  }
- 
              }
-@@ -2058,7 +3622,7 @@
-         if (!this.waitingForSwitchToConfig) {
-             throw new IllegalStateException("Client acknowledged config, but none was requested");
-         } else {
--            this.connection.setupInboundProtocol(ConfigurationProtocols.SERVERBOUND, new ServerConfigurationPacketListenerImpl(this.server, this.connection, this.createCookie(this.player.clientInformation())));
-+            this.connection.setupInboundProtocol(ConfigurationProtocols.SERVERBOUND, new ServerConfigurationPacketListenerImpl(this.server, this.connection, this.createCookie(this.player.clientInformation()), this.player)); // CraftBukkit
+         }
+@@ -1892,7 +_,7 @@
+             this.connection
+                 .setupInboundProtocol(
+                     ConfigurationProtocols.SERVERBOUND,
+-                    new ServerConfigurationPacketListenerImpl(this.server, this.connection, this.createCookie(this.player.clientInformation()))
++                    new ServerConfigurationPacketListenerImpl(this.server, this.connection, this.createCookie(this.player.clientInformation()), this.player) // CraftBukkit
+                 );
          }
      }
+@@ -1911,6 +_,7 @@
  
-@@ -2076,15 +3640,18 @@
- 
-     private void resetPlayerChatState(RemoteChatSession session) {
-         this.chatSession = session;
+     private void resetPlayerChatState(RemoteChatSession chatSession) {
+         this.chatSession = chatSession;
 +        this.hasLoggedExpiry = false; // Paper - Prevent causing expired keys from impacting new joins
-         this.signedMessageDecoder = session.createMessageDecoder(this.player.getUUID());
-         this.chatMessageChain.append(() -> {
-             this.player.setChatSession(session);
--            this.server.getPlayerList().broadcastAll(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.INITIALIZE_CHAT), List.of(this.player)));
-+            this.server.getPlayerList().broadcastAll(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.INITIALIZE_CHAT), List.of(this.player)), this.player); // Paper - Use single player info update packet on join
-         });
+         this.signedMessageDecoder = chatSession.createMessageDecoder(this.player.getUUID());
+         this.chatMessageChain
+             .append(
+@@ -1919,15 +_,17 @@
+                     this.server
+                         .getPlayerList()
+                         .broadcastAll(
+-                            new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.INITIALIZE_CHAT), List.of(this.player))
++                            new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.INITIALIZE_CHAT), List.of(this.player)), this.player // Paper - Use single player info update packet on join
+                         );
+                 }
+             );
      }
  
 -    @Override
--    public void handleCustomPayload(ServerboundCustomPayloadPacket packet) {}
+-    public void handleCustomPayload(ServerboundCustomPayloadPacket packet) {
+-    }
 +    // CraftBukkit start - handled in super
 +    // @Override
-+    // public void handleCustomPayload(ServerboundCustomPayloadPacket serverboundcustompayloadpacket) {}
++    // public void handleCustomPayload(ServerboundCustomPayloadPacket packet) {
++    // }
 +    // CraftBukkit end
  
      @Override
      public void handleClientTickEnd(ServerboundClientTickEndPacket packet) {
-@@ -2115,4 +3682,17 @@
- 
+@@ -1957,4 +_,17 @@
+     interface EntityInteraction {
          InteractionResult run(ServerPlayer player, Entity entity, InteractionHand hand);
      }
 +
 +    // Paper start - Add fail move event
 +    private io.papermc.paper.event.player.PlayerFailMoveEvent fireFailMove(io.papermc.paper.event.player.PlayerFailMoveEvent.FailReason failReason,
 +                                                                           double toX, double toY, double toZ, float toYaw, float toPitch, boolean logWarning) {
-+        Player player = this.getCraftPlayer();
++        org.bukkit.entity.Player player = this.getCraftPlayer();
 +        Location from = new Location(player.getWorld(), this.lastPosX, this.lastPosY, this.lastPosZ, this.lastYaw, this.lastPitch);
 +        Location to = new Location(player.getWorld(), toX, toY, toZ, toYaw, toPitch);
 +        io.papermc.paper.event.player.PlayerFailMoveEvent event = new io.papermc.paper.event.player.PlayerFailMoveEvent(player, failReason,
diff --git a/paper-server/patches/unapplied/net/minecraft/server/network/ServerHandshakePacketListenerImpl.java.patch b/paper-server/patches/sources/net/minecraft/server/network/ServerHandshakePacketListenerImpl.java.patch
similarity index 59%
rename from paper-server/patches/unapplied/net/minecraft/server/network/ServerHandshakePacketListenerImpl.java.patch
rename to paper-server/patches/sources/net/minecraft/server/network/ServerHandshakePacketListenerImpl.java.patch
index bf3a6ca144..218905a0aa 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/network/ServerHandshakePacketListenerImpl.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/network/ServerHandshakePacketListenerImpl.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/server/network/ServerHandshakePacketListenerImpl.java
 +++ b/net/minecraft/server/network/ServerHandshakePacketListenerImpl.java
-@@ -13,11 +13,26 @@
+@@ -12,11 +_,27 @@
  import net.minecraft.network.protocol.status.StatusProtocols;
  import net.minecraft.server.MinecraftServer;
  
@@ -10,24 +10,25 @@
 +// CraftBukkit end
 +
  public class ServerHandshakePacketListenerImpl implements ServerHandshakePacketListener {
- 
+     private static final Component IGNORE_STATUS_REASON = Component.translatable("disconnect.ignoring_status_request");
 +    // Spigot start
 +    private static final com.google.gson.Gson gson = new com.google.gson.Gson();
 +    static final java.util.regex.Pattern HOST_PATTERN = java.util.regex.Pattern.compile("[0-9a-f\\.:]{0,45}");
 +    static final java.util.regex.Pattern PROP_PATTERN = java.util.regex.Pattern.compile("\\w{0,16}");
 +    // Spigot end
 +    // CraftBukkit start - add fields
-+    private static final HashMap<InetAddress, Long> throttleTracker = new HashMap<InetAddress, Long>();
++    private static final HashMap<InetAddress, Long> throttleTracker = new HashMap<>();
 +    private static int throttleCounter = 0;
 +    // CraftBukkit end
-     private static final Component IGNORE_STATUS_REASON = Component.translatable("disconnect.ignoring_status_request");
++    private static final boolean BYPASS_HOSTCHECK = Boolean.getBoolean("Paper.bypassHostCheck"); // Paper
      private final MinecraftServer server;
      private final Connection connection;
-+    private static final boolean BYPASS_HOSTCHECK = Boolean.getBoolean("Paper.bypassHostCheck"); // Paper
  
++
      public ServerHandshakePacketListenerImpl(MinecraftServer server, Connection connection) {
          this.server = server;
-@@ -26,6 +41,7 @@
+         this.connection = connection;
+@@ -24,6 +_,7 @@
  
      @Override
      public void handleIntention(ClientIntentionPacket packet) {
@@ -35,73 +36,66 @@
          switch (packet.intention()) {
              case LOGIN:
                  this.beginLogin(packet, false);
-@@ -55,23 +71,127 @@
-                 throw new UnsupportedOperationException("Invalid intention " + String.valueOf(packet.intention()));
+@@ -50,22 +_,117 @@
+             default:
+                 throw new UnsupportedOperationException("Invalid intention " + packet.intention());
          }
- 
++
 +        // Paper start - NetworkClient implementation
 +        this.connection.protocolVersion = packet.protocolVersion();
 +        this.connection.virtualHost = com.destroystokyo.paper.network.PaperNetworkClient.prepareVirtualHost(packet.hostName(), packet.port());
 +        // Paper end
      }
  
-     private void beginLogin(ClientIntentionPacket packet, boolean transfer) {
+     private void beginLogin(ClientIntentionPacket packet, boolean transferred) {
          this.connection.setupOutboundProtocol(LoginProtocols.CLIENTBOUND);
 +        // CraftBukkit start - Connection throttle
 +        try {
 +            if (!(this.connection.channel.localAddress() instanceof io.netty.channel.unix.DomainSocketAddress)) { // Paper - Unix domain socket support; the connection throttle is useless when you have a Unix domain socket
-+            long currentTime = System.currentTimeMillis();
-+            long connectionThrottle = this.server.server.getConnectionThrottle();
-+            InetAddress address = ((java.net.InetSocketAddress) this.connection.getRemoteAddress()).getAddress();
++                long currentTime = System.currentTimeMillis();
++                long connectionThrottle = this.server.server.getConnectionThrottle();
++                InetAddress address = ((java.net.InetSocketAddress) this.connection.getRemoteAddress()).getAddress();
++
++                synchronized (ServerHandshakePacketListenerImpl.throttleTracker) {
++                    if (ServerHandshakePacketListenerImpl.throttleTracker.containsKey(address) && !"127.0.0.1".equals(address.getHostAddress()) && currentTime - ServerHandshakePacketListenerImpl.throttleTracker.get(address) < connectionThrottle) {
++                        ServerHandshakePacketListenerImpl.throttleTracker.put(address, currentTime);
++                        Component chatmessage = io.papermc.paper.adventure.PaperAdventure.asVanilla(io.papermc.paper.configuration.GlobalConfiguration.get().messages.kick.connectionThrottle); // Paper - Configurable connection throttle kick message
++                        this.connection.send(new ClientboundLoginDisconnectPacket(chatmessage));
++                        this.connection.disconnect(chatmessage);
++                        return;
++                    }
 +
-+            synchronized (ServerHandshakePacketListenerImpl.throttleTracker) {
-+                if (ServerHandshakePacketListenerImpl.throttleTracker.containsKey(address) && !"127.0.0.1".equals(address.getHostAddress()) && currentTime - ServerHandshakePacketListenerImpl.throttleTracker.get(address) < connectionThrottle) {
 +                    ServerHandshakePacketListenerImpl.throttleTracker.put(address, currentTime);
-+                            Component chatmessage = io.papermc.paper.adventure.PaperAdventure.asVanilla(io.papermc.paper.configuration.GlobalConfiguration.get().messages.kick.connectionThrottle); // Paper - Configurable connection throttle kick message
-+                    this.connection.send(new ClientboundLoginDisconnectPacket(chatmessage));
-+                    this.connection.disconnect(chatmessage);
-+                    return;
-+                }
++                    ServerHandshakePacketListenerImpl.throttleCounter++;
++                    if (ServerHandshakePacketListenerImpl.throttleCounter > 200) {
++                        ServerHandshakePacketListenerImpl.throttleCounter = 0;
 +
-+                ServerHandshakePacketListenerImpl.throttleTracker.put(address, currentTime);
-+                ServerHandshakePacketListenerImpl.throttleCounter++;
-+                if (ServerHandshakePacketListenerImpl.throttleCounter > 200) {
-+                    ServerHandshakePacketListenerImpl.throttleCounter = 0;
-+
-+                    // Cleanup stale entries
-+                    java.util.Iterator iter = ServerHandshakePacketListenerImpl.throttleTracker.entrySet().iterator();
-+                    while (iter.hasNext()) {
-+                        java.util.Map.Entry<InetAddress, Long> entry = (java.util.Map.Entry) iter.next();
-+                        if (entry.getValue() > connectionThrottle) {
-+                            iter.remove();
-+                        }
++                        // Cleanup stale entries
++                        ServerHandshakePacketListenerImpl.throttleTracker.values().removeIf(time -> time > connectionThrottle);
 +                    }
 +                }
-+            }
 +            } // Paper - Unix domain socket support
 +        } catch (Throwable t) {
 +            org.apache.logging.log4j.LogManager.getLogger().debug("Failed to check connection throttle", t);
 +        }
 +        // CraftBukkit end
          if (packet.protocolVersion() != SharedConstants.getCurrentVersion().getProtocolVersion()) {
--            MutableComponent ichatmutablecomponent;
-+                    net.kyori.adventure.text.Component adventureComponent; // Paper - Fix hex colors not working in some kick messages
- 
+-            Component component;
 -            if (packet.protocolVersion() < 754) {
--                ichatmutablecomponent = Component.translatable("multiplayer.disconnect.outdated_client", SharedConstants.getCurrentVersion().getName());
+-                component = Component.translatable("multiplayer.disconnect.outdated_client", SharedConstants.getCurrentVersion().getName());
++            net.kyori.adventure.text.Component adventureComponent; // Paper - Fix hex colors not working in some kick messages
 +            if (packet.protocolVersion() < SharedConstants.getCurrentVersion().getProtocolVersion()) { // Spigot - SPIGOT-7546: Handle version check correctly for outdated client message
-+                        adventureComponent = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(java.text.MessageFormat.format(org.spigotmc.SpigotConfig.outdatedClientMessage.replaceAll("'", "''"), SharedConstants.getCurrentVersion().getName())); // Spigot // Paper - Fix hex colors not working in some kick messages
++                adventureComponent = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(java.text.MessageFormat.format(org.spigotmc.SpigotConfig.outdatedClientMessage.replaceAll("'", "''"), SharedConstants.getCurrentVersion().getName())); // Spigot // Paper - Fix hex colors not working in some kick messages
              } else {
--                ichatmutablecomponent = Component.translatable("multiplayer.disconnect.incompatible", SharedConstants.getCurrentVersion().getName());
-+                        adventureComponent = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(java.text.MessageFormat.format(org.spigotmc.SpigotConfig.outdatedServerMessage.replaceAll("'", "''"), SharedConstants.getCurrentVersion().getName())); // Spigot // Paper - Fix hex colors not working in some kick messages
+-                component = Component.translatable("multiplayer.disconnect.incompatible", SharedConstants.getCurrentVersion().getName());
++                adventureComponent = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(java.text.MessageFormat.format(org.spigotmc.SpigotConfig.outdatedServerMessage.replaceAll("'", "''"), SharedConstants.getCurrentVersion().getName())); // Spigot // Paper - Fix hex colors not working in some kick messages
              }
++            Component component = io.papermc.paper.adventure.PaperAdventure.asVanilla(adventureComponent); // Paper - Fix hex colors not working in some kick messages
  
-+                    Component ichatmutablecomponent = io.papermc.paper.adventure.PaperAdventure.asVanilla(adventureComponent); // Paper - Fix hex colors not working in some kick messages
-+
-             this.connection.send(new ClientboundLoginDisconnectPacket(ichatmutablecomponent));
-             this.connection.disconnect((Component) ichatmutablecomponent);
+             this.connection.send(new ClientboundLoginDisconnectPacket(component));
+             this.connection.disconnect(component);
          } else {
-             this.connection.setupInboundProtocol(LoginProtocols.SERVERBOUND, new ServerLoginPacketListenerImpl(this.server, this.connection, transfer));
+             this.connection.setupInboundProtocol(LoginProtocols.SERVERBOUND, new ServerLoginPacketListenerImpl(this.server, this.connection, transferred));
 +            // Paper start - PlayerHandshakeEvent
 +            boolean proxyLogicEnabled = org.spigotmc.SpigotConfig.bungee;
 +            boolean handledByEvent = false;
@@ -113,9 +107,9 @@
 +                if (event.callEvent()) {
 +                    // If we've failed somehow, let the client know so and go no further.
 +                    if (event.isFailed()) {
-+                        Component component = io.papermc.paper.adventure.PaperAdventure.asVanilla(event.failMessage());
-+                        this.connection.send(new ClientboundLoginDisconnectPacket(component));
-+                        this.connection.disconnect(component);
++                        Component message = io.papermc.paper.adventure.PaperAdventure.asVanilla(event.failMessage());
++                        this.connection.send(new ClientboundLoginDisconnectPacket(message));
++                        this.connection.disconnect(message);
 +                        return;
 +                    }
 +
@@ -135,35 +129,32 @@
 +                }
 +            }
 +            // Paper end
-+            // Spigot Start
++            // Spigot start
 +            String[] split = packet.hostName().split("\00");
 +            if (!handledByEvent && proxyLogicEnabled) { // Paper
 +                // if (org.spigotmc.SpigotConfig.bungee) { // Paper - comment out, we check above!
-+                if ( ( split.length == 3 || split.length == 4 ) && ( ServerHandshakePacketListenerImpl.BYPASS_HOSTCHECK || ServerHandshakePacketListenerImpl.HOST_PATTERN.matcher( split[1] ).matches() ) ) { // Paper - Add bypass host check
++                if ((split.length == 3 || split.length == 4) && (ServerHandshakePacketListenerImpl.BYPASS_HOSTCHECK || ServerHandshakePacketListenerImpl.HOST_PATTERN.matcher(split[1]).matches())) { // Paper - Add bypass host check
 +                    // Paper start - Unix domain socket support
 +                    java.net.SocketAddress socketAddress = this.connection.getRemoteAddress();
 +                    this.connection.hostname = split[0];
 +                    this.connection.address = new java.net.InetSocketAddress(split[1], socketAddress instanceof java.net.InetSocketAddress ? ((java.net.InetSocketAddress) socketAddress).getPort() : 0);
 +                    // Paper end - Unix domain socket support
 +                    this.connection.spoofedUUID = com.mojang.util.UndashedUuid.fromStringLenient( split[2] );
-+                } else
-+                {
-+                    Component chatmessage = Component.literal("If you wish to use IP forwarding, please enable it in your BungeeCord config as well!");
-+                    this.connection.send(new ClientboundLoginDisconnectPacket(chatmessage));
-+                    this.connection.disconnect(chatmessage);
++                } else {
++                    Component message = Component.literal("If you wish to use IP forwarding, please enable it in your BungeeCord config as well!");
++                    this.connection.send(new ClientboundLoginDisconnectPacket(message));
++                    this.connection.disconnect(message);
 +                    return;
 +                }
-+                if ( split.length == 4 )
-+                {
++                if (split.length == 4) {
 +                    this.connection.spoofedProfile = ServerHandshakePacketListenerImpl.gson.fromJson(split[3], com.mojang.authlib.properties.Property[].class);
 +                }
-+            } else if ( ( split.length == 3 || split.length == 4 ) && ( ServerHandshakePacketListenerImpl.HOST_PATTERN.matcher( split[1] ).matches() ) ) {
-+                Component chatmessage = Component.literal("Unknown data in login hostname, did you forget to enable BungeeCord in spigot.yml?");
-+                this.connection.send(new ClientboundLoginDisconnectPacket(chatmessage));
-+                this.connection.disconnect(chatmessage);
-+                return;
++            } else if ((split.length == 3 || split.length == 4) && (ServerHandshakePacketListenerImpl.HOST_PATTERN.matcher(split[1]).matches())) {
++                Component message = Component.literal("Unknown data in login hostname, did you forget to enable BungeeCord in spigot.yml?");
++                this.connection.send(new ClientboundLoginDisconnectPacket(message));
++                this.connection.disconnect(message);
 +            }
-+            // Spigot End
++            // Spigot end
          }
- 
      }
+ 
diff --git a/paper-server/patches/unapplied/net/minecraft/server/network/ServerLoginPacketListenerImpl.java.patch b/paper-server/patches/sources/net/minecraft/server/network/ServerLoginPacketListenerImpl.java.patch
similarity index 69%
rename from paper-server/patches/unapplied/net/minecraft/server/network/ServerLoginPacketListenerImpl.java.patch
rename to paper-server/patches/sources/net/minecraft/server/network/ServerLoginPacketListenerImpl.java.patch
index 2f73ee117e..4360d8653f 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/network/ServerLoginPacketListenerImpl.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/network/ServerLoginPacketListenerImpl.java.patch
@@ -1,25 +1,10 @@
 --- a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
 +++ b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
-@@ -20,6 +20,7 @@
- import net.minecraft.DefaultUncaughtExceptionHandler;
- import net.minecraft.core.UUIDUtil;
- import net.minecraft.network.Connection;
-+import net.minecraft.network.ConnectionProtocol;
- import net.minecraft.network.DisconnectionDetails;
- import net.minecraft.network.PacketSendListener;
- import net.minecraft.network.TickablePacketListener;
-@@ -36,6 +37,7 @@
- import net.minecraft.network.protocol.login.ServerboundKeyPacket;
- import net.minecraft.network.protocol.login.ServerboundLoginAcknowledgedPacket;
- import net.minecraft.server.MinecraftServer;
-+import net.minecraft.server.level.ServerPlayer;
- import net.minecraft.server.players.PlayerList;
- import net.minecraft.util.Crypt;
- import net.minecraft.util.CryptException;
-@@ -43,11 +45,38 @@
+@@ -43,10 +_,19 @@
  import net.minecraft.util.StringUtil;
  import org.apache.commons.lang3.Validate;
  import org.slf4j.Logger;
++// CraftBukkit start
 +import net.minecraft.network.protocol.Packet;
 +import net.minecraft.network.protocol.PacketUtils;
 +import org.bukkit.craftbukkit.entity.CraftPlayer;
@@ -29,15 +14,36 @@
  
 -public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, TickablePacketListener {
 +public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, TickablePacketListener, CraftPlayer.TransferCookieConnection {
++    // CraftBukkit end
+     private static final AtomicInteger UNIQUE_THREAD_ID = new AtomicInteger(0);
+     static final Logger LOGGER = LogUtils.getLogger();
++    private static final java.util.concurrent.ExecutorService authenticatorPool = java.util.concurrent.Executors.newCachedThreadPool(new com.google.common.util.concurrent.ThreadFactoryBuilder().setNameFormat("User Authenticator #%d").setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandler(LOGGER)).build()); // Paper - Cache authenticator threads
+     private static final int MAX_TICKS_BEFORE_LOGIN = 600;
+     private final byte[] challenge;
+     final MinecraftServer server;
+@@ -59,6 +_,9 @@
+     public GameProfile authenticatedProfile;
+     private final String serverId = "";
+     private final boolean transferred;
++    private net.minecraft.server.level.ServerPlayer player; // CraftBukkit
++    public boolean iKnowThisMayNotBeTheBestIdeaButPleaseDisableUsernameValidation = false; // Paper - username validation overriding
++    private int velocityLoginMessageId = -1; // Paper - Add Velocity IP Forwarding Support
  
+     public ServerLoginPacketListenerImpl(MinecraftServer server, Connection connection, boolean transferred) {
+         this.server = server;
+@@ -67,11 +_,48 @@
+         this.transferred = transferred;
+     }
+ 
++    // CraftBukkit start
 +    @Override
 +    public boolean isTransferred() {
 +        return this.transferred;
 +    }
 +
 +    @Override
-+    public ConnectionProtocol getProtocol() {
-+        return ConnectionProtocol.LOGIN;
++    public net.minecraft.network.ConnectionProtocol getProtocol() {
++        return net.minecraft.network.ConnectionProtocol.LOGIN;
 +    }
 +
 +    @Override
@@ -50,81 +56,61 @@
 +        this.disconnect(reason);
 +    }
 +    // CraftBukkit end
-     private static final AtomicInteger UNIQUE_THREAD_ID = new AtomicInteger(0);
-     static final Logger LOGGER = LogUtils.getLogger();
-+    private static final java.util.concurrent.ExecutorService authenticatorPool = java.util.concurrent.Executors.newCachedThreadPool(new com.google.common.util.concurrent.ThreadFactoryBuilder().setNameFormat("User Authenticator #%d").setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandler(LOGGER)).build()); // Paper - Cache authenticator threads
-     private static final int MAX_TICKS_BEFORE_LOGIN = 600;
-     private final byte[] challenge;
-     final MinecraftServer server;
-@@ -57,9 +86,12 @@
-     @Nullable
-     String requestedUsername;
-     @Nullable
--    private GameProfile authenticatedProfile;
-+    public GameProfile authenticatedProfile; // Paper - public
-     private final String serverId;
-     private final boolean transferred;
-+    private ServerPlayer player; // CraftBukkit
-+    public boolean iKnowThisMayNotBeTheBestIdeaButPleaseDisableUsernameValidation = false; // Paper - username validation overriding
-+    private int velocityLoginMessageId = -1; // Paper - Add Velocity IP Forwarding Support
- 
-     public ServerLoginPacketListenerImpl(MinecraftServer server, Connection connection, boolean transferred) {
-         this.state = ServerLoginPacketListenerImpl.State.HELLO;
-@@ -72,10 +104,24 @@
- 
++
      @Override
      public void tick() {
 +        // Paper start - Do not allow logins while the server is shutting down
-+        if (!MinecraftServer.getServer().isRunning()) {
++        if (!this.server.isRunning()) {
 +            this.disconnect(org.bukkit.craftbukkit.util.CraftChatMessage.fromString(org.spigotmc.SpigotConfig.restartMessage)[0]);
 +            return;
 +        }
 +        // Paper end - Do not allow logins while the server is shutting down
++
          if (this.state == ServerLoginPacketListenerImpl.State.VERIFYING) {
 +            if (this.connection.isConnected()) { // Paper - prevent logins to be processed even though disconnect was called
-             this.verifyLoginAndFinishConnectionSetup((GameProfile) Objects.requireNonNull(this.authenticatedProfile));
+             this.verifyLoginAndFinishConnectionSetup(Objects.requireNonNull(this.authenticatedProfile));
+-        }
 +            } // Paper - prevent logins to be processed even though disconnect was called
-         }
- 
++        }
++
 +        // CraftBukkit start
 +        if (this.state == ServerLoginPacketListenerImpl.State.WAITING_FOR_COOKIES && !this.player.getBukkitEntity().isAwaitingCookies()) {
 +            this.postCookies(this.authenticatedProfile);
 +        }
 +        // CraftBukkit end
-+
-         if (this.state == ServerLoginPacketListenerImpl.State.WAITING_FOR_DUPE_DISCONNECT && !this.isPlayerAlreadyInWorld((GameProfile) Objects.requireNonNull(this.authenticatedProfile))) {
-             this.finishLoginAndWaitForClient(this.authenticatedProfile);
-         }
-@@ -86,6 +132,13 @@
  
+         if (this.state == ServerLoginPacketListenerImpl.State.WAITING_FOR_DUPE_DISCONNECT
+             && !this.isPlayerAlreadyInWorld(Objects.requireNonNull(this.authenticatedProfile))) {
+@@ -83,6 +_,13 @@
+         }
      }
  
 +    // CraftBukkit start
 +    @Deprecated
-+    public void disconnect(String s) {
-+        this.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(s))); // Paper - Fix hex colors not working in some kick messages
++    public void disconnect(String reason) {
++        this.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(reason))); // Paper - Fix hex colors not working in some kick messages
 +    }
 +    // CraftBukkit end
 +
      @Override
      public boolean isAcceptingMessages() {
          return this.connection.isConnected();
-@@ -120,7 +173,13 @@
+@@ -115,7 +_,13 @@
      @Override
      public void handleHello(ServerboundHelloPacket packet) {
-         Validate.validState(this.state == ServerLoginPacketListenerImpl.State.HELLO, "Unexpected hello packet", new Object[0]);
--        Validate.validState(StringUtil.isValidPlayerName(packet.name()), "Invalid characters in username", new Object[0]);
+         Validate.validState(this.state == ServerLoginPacketListenerImpl.State.HELLO, "Unexpected hello packet");
+-        Validate.validState(StringUtil.isValidPlayerName(packet.name()), "Invalid characters in username");
 +        // Paper start - Validate usernames
 +        if (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode()
 +            && io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.performUsernameValidation
 +            && !this.iKnowThisMayNotBeTheBestIdeaButPleaseDisableUsernameValidation) {
-+            Validate.validState(StringUtil.isReasonablePlayerName(packet.name()), "Invalid characters in username", new Object[0]);
++            Validate.validState(StringUtil.isReasonablePlayerName(packet.name()), "Invalid characters in username");
 +        }
 +        // Paper end - Validate usernames
          this.requestedUsername = packet.name();
-         GameProfile gameprofile = this.server.getSingleplayerProfile();
- 
-@@ -131,7 +190,36 @@
+         GameProfile singleplayerProfile = this.server.getSingleplayerProfile();
+         if (singleplayerProfile != null && this.requestedUsername.equalsIgnoreCase(singleplayerProfile.getName())) {
+@@ -125,7 +_,32 @@
                  this.state = ServerLoginPacketListenerImpl.State.KEY;
                  this.connection.send(new ClientboundHelloPacket("", this.server.getKeyPair().getPublic().getEncoded(), this.challenge, true));
              } else {
@@ -141,37 +127,32 @@
 +                // Paper end - Add Velocity IP Forwarding Support
 +                // CraftBukkit start
 +                // Paper start - Cache authenticator threads
-+                authenticatorPool.execute(new Runnable() {
++                authenticatorPool.execute(() -> {
++                    try {
++                        GameProfile gameprofile = ServerLoginPacketListenerImpl.this.createOfflineProfile(ServerLoginPacketListenerImpl.this.requestedUsername); // Spigot
 +
-+                    @Override
-+                    public void run() {
-+                        try {
-+                            GameProfile gameprofile = ServerLoginPacketListenerImpl.this.createOfflineProfile(ServerLoginPacketListenerImpl.this.requestedUsername); // Spigot
-+
-+                            gameprofile = ServerLoginPacketListenerImpl.this.callPlayerPreLoginEvents(gameprofile); // Paper - Add more fields to AsyncPlayerPreLoginEvent
-+                            ServerLoginPacketListenerImpl.LOGGER.info("UUID of player {} is {}", gameprofile.getName(), gameprofile.getId());
-+                            ServerLoginPacketListenerImpl.this.startClientVerification(gameprofile);
-+                        } catch (Exception ex) {
-+                            ServerLoginPacketListenerImpl.this.disconnect("Failed to verify username!");
-+                            ServerLoginPacketListenerImpl.this.server.server.getLogger().log(java.util.logging.Level.WARNING, "Exception verifying " + ServerLoginPacketListenerImpl.this.requestedUsername, ex);
-+                        }
++                        gameprofile = ServerLoginPacketListenerImpl.this.callPlayerPreLoginEvents(gameprofile); // Paper - Add more fields to AsyncPlayerPreLoginEvent
++                        ServerLoginPacketListenerImpl.LOGGER.info("UUID of player {} is {}", gameprofile.getName(), gameprofile.getId());
++                        ServerLoginPacketListenerImpl.this.startClientVerification(gameprofile);
++                    } catch (Exception ex) {
++                        ServerLoginPacketListenerImpl.this.disconnect("Failed to verify username!");
++                        ServerLoginPacketListenerImpl.this.server.server.getLogger().log(java.util.logging.Level.WARNING, "Exception verifying " + ServerLoginPacketListenerImpl.this.requestedUsername, ex);
 +                    }
 +                });
 +                // Paper end - Cache authenticator threads
 +                // CraftBukkit end
              }
- 
          }
-@@ -144,10 +232,24 @@
+     }
+@@ -137,9 +_,23 @@
  
      private void verifyLoginAndFinishConnectionSetup(GameProfile profile) {
-         PlayerList playerlist = this.server.getPlayerList();
--        Component ichatbasecomponent = playerlist.canPlayerLogin(this.connection.getRemoteAddress(), profile);
+         PlayerList playerList = this.server.getPlayerList();
+-        Component component = playerList.canPlayerLogin(this.connection.getRemoteAddress(), profile);
+-        if (component != null) {
+-            this.disconnect(component);
 +        // CraftBukkit start - fire PlayerLoginEvent
-+        this.player = playerlist.canPlayerLogin(this, profile); // CraftBukkit
- 
--        if (ichatbasecomponent != null) {
--            this.disconnect(ichatbasecomponent);
++        this.player = playerList.canPlayerLogin(this, profile); // CraftBukkit
 +        if (this.player != null) {
 +            if (this.player.getBukkitEntity().isAwaitingCookies()) {
 +                this.state = ServerLoginPacketListenerImpl.State.WAITING_FOR_COOKIES;
@@ -181,94 +162,89 @@
 +        }
 +    }
 +
-+    private void postCookies(GameProfile gameprofile) {
-+        PlayerList playerlist = this.server.getPlayerList();
++    private void postCookies(GameProfile profile) {
++        PlayerList playerList = this.server.getPlayerList();
 +
 +        if (this.player == null) {
-+            // this.disconnect(ichatbasecomponent);
++            // this.disconnect(component);
 +            // CraftBukkit end
          } else {
              if (this.server.getCompressionThreshold() >= 0 && !this.connection.isMemoryConnection()) {
-                 this.connection.send(new ClientboundLoginCompressionPacket(this.server.getCompressionThreshold()), PacketSendListener.thenRun(() -> {
-@@ -155,12 +257,12 @@
-                 }));
+                 this.connection
+@@ -149,7 +_,7 @@
+                     );
              }
  
--            boolean flag = playerlist.disconnectAllPlayersWithProfile(profile);
-+            boolean flag = playerlist.disconnectAllPlayersWithProfile(gameprofile, this.player); // CraftBukkit - add player reference
- 
+-            boolean flag = playerList.disconnectAllPlayersWithProfile(profile);
++            boolean flag = playerList.disconnectAllPlayersWithProfile(profile, this.player); // CraftBukkit - add player reference
              if (flag) {
                  this.state = ServerLoginPacketListenerImpl.State.WAITING_FOR_DUPE_DISCONNECT;
              } else {
--                this.finishLoginAndWaitForClient(profile);
-+                this.finishLoginAndWaitForClient(gameprofile);
-             }
+@@ -184,7 +_,8 @@
+             throw new IllegalStateException("Protocol error", var7);
          }
  
-@@ -195,7 +297,8 @@
-             throw new IllegalStateException("Protocol error", cryptographyexception);
-         }
- 
--        Thread thread = new Thread("User Authenticator #" + ServerLoginPacketListenerImpl.UNIQUE_THREAD_ID.incrementAndGet()) {
+-        Thread thread = new Thread("User Authenticator #" + UNIQUE_THREAD_ID.incrementAndGet()) {
 +        // Paper start - Cache authenticator threads
 +        authenticatorPool.execute(new Runnable() {
+             @Override
              public void run() {
-                 String s1 = (String) Objects.requireNonNull(ServerLoginPacketListenerImpl.this.requestedUsername, "Player name not initialized");
- 
-@@ -205,11 +308,17 @@
-                     if (profileresult != null) {
-                         GameProfile gameprofile = profileresult.profile();
- 
+                 String string1 = Objects.requireNonNull(ServerLoginPacketListenerImpl.this.requestedUsername, "Player name not initialized");
+@@ -195,11 +_,17 @@
+                         .hasJoinedServer(string1, string, this.getAddress());
+                     if (profileResult != null) {
+                         GameProfile gameProfile = profileResult.profile();
 +                        // CraftBukkit start - fire PlayerPreLoginEvent
 +                        if (!ServerLoginPacketListenerImpl.this.connection.isConnected()) {
 +                            return;
 +                        }
-+                        gameprofile = ServerLoginPacketListenerImpl.this.callPlayerPreLoginEvents(gameprofile); // Paper - Add more fields to AsyncPlayerPreLoginEvent
++                        gameProfile = ServerLoginPacketListenerImpl.this.callPlayerPreLoginEvents(gameProfile); // Paper - Add more fields to AsyncPlayerPreLoginEvent
 +                        // CraftBukkit end
-                         ServerLoginPacketListenerImpl.LOGGER.info("UUID of player {} is {}", gameprofile.getName(), gameprofile.getId());
-                         ServerLoginPacketListenerImpl.this.startClientVerification(gameprofile);
+                         ServerLoginPacketListenerImpl.LOGGER.info("UUID of player {} is {}", gameProfile.getName(), gameProfile.getId());
+                         ServerLoginPacketListenerImpl.this.startClientVerification(gameProfile);
                      } else if (ServerLoginPacketListenerImpl.this.server.isSingleplayer()) {
                          ServerLoginPacketListenerImpl.LOGGER.warn("Failed to verify username but will let them in anyway!");
--                        ServerLoginPacketListenerImpl.this.startClientVerification(UUIDUtil.createOfflineProfile(s1));
-+                        ServerLoginPacketListenerImpl.this.startClientVerification(ServerLoginPacketListenerImpl.this.createOfflineProfile(s1)); // Spigot
+-                        ServerLoginPacketListenerImpl.this.startClientVerification(UUIDUtil.createOfflineProfile(string1));
++                        ServerLoginPacketListenerImpl.this.startClientVerification(ServerLoginPacketListenerImpl.this.createOfflineProfile(string1)); // Spigot
                      } else {
                          ServerLoginPacketListenerImpl.this.disconnect(Component.translatable("multiplayer.disconnect.unverified_username"));
-                         ServerLoginPacketListenerImpl.LOGGER.error("Username '{}' tried to join with an invalid session", s1);
-@@ -217,11 +326,16 @@
-                 } catch (AuthenticationUnavailableException authenticationunavailableexception) {
+                         ServerLoginPacketListenerImpl.LOGGER.error("Username '{}' tried to join with an invalid session", string1);
+@@ -207,11 +_,16 @@
+                 } catch (AuthenticationUnavailableException var4) {
                      if (ServerLoginPacketListenerImpl.this.server.isSingleplayer()) {
                          ServerLoginPacketListenerImpl.LOGGER.warn("Authentication servers are down but will let them in anyway!");
--                        ServerLoginPacketListenerImpl.this.startClientVerification(UUIDUtil.createOfflineProfile(s1));
-+                        ServerLoginPacketListenerImpl.this.startClientVerification(ServerLoginPacketListenerImpl.this.createOfflineProfile(s1)); // Spigot
+-                        ServerLoginPacketListenerImpl.this.startClientVerification(UUIDUtil.createOfflineProfile(string1));
++                        ServerLoginPacketListenerImpl.this.startClientVerification(ServerLoginPacketListenerImpl.this.createOfflineProfile(string1)); // Spigot
                      } else {
 -                        ServerLoginPacketListenerImpl.this.disconnect(Component.translatable("multiplayer.disconnect.authservers_down"));
 +                        ServerLoginPacketListenerImpl.this.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(io.papermc.paper.configuration.GlobalConfiguration.get().messages.kick.authenticationServersDown)); // Paper - Configurable kick message
                          ServerLoginPacketListenerImpl.LOGGER.error("Couldn't verify username because servers are unavailable");
                      }
 +                    // CraftBukkit start - catch all exceptions
-+                } catch (Exception exception) {
++                } catch (Exception ex) {
 +                    ServerLoginPacketListenerImpl.this.disconnect("Failed to verify username!");
-+                    ServerLoginPacketListenerImpl.this.server.server.getLogger().log(java.util.logging.Level.WARNING, "Exception verifying " + s1, exception);
++                    ServerLoginPacketListenerImpl.LOGGER.warn("Exception verifying {}", string1, ex);
 +                    // CraftBukkit end
                  }
- 
              }
-@@ -232,23 +346,118 @@
  
-                 return ServerLoginPacketListenerImpl.this.server.getPreventProxyConnections() && socketaddress instanceof InetSocketAddress ? ((InetSocketAddress) socketaddress).getAddress() : null;
+@@ -222,24 +_,120 @@
+                     ? ((InetSocketAddress)remoteAddress).getAddress()
+                     : null;
              }
 -        };
+-        thread.setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandler(LOGGER));
+-        thread.start();
+-    }
 +        });
 +        // Paper end - Cache authenticator threads
 +    }
- 
--        thread.setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandler(ServerLoginPacketListenerImpl.LOGGER));
--        thread.start();
++
 +    // CraftBukkit start
 +    private GameProfile callPlayerPreLoginEvents(GameProfile gameprofile) throws Exception { // Paper - Add more fields to AsyncPlayerPreLoginEvent
 +        // Paper start - Add Velocity IP Forwarding Support
 +        if (ServerLoginPacketListenerImpl.this.velocityLoginMessageId == -1 && io.papermc.paper.configuration.GlobalConfiguration.get().proxies.velocity.enabled) {
-+            disconnect("This server requires you to connect with Velocity.");
++            this.disconnect("This server requires you to connect with Velocity.");
 +            return gameprofile;
 +        }
 +        // Paper end - Add Velocity IP Forwarding Support
@@ -294,7 +270,7 @@
 +            if (asyncEvent.getResult() != PlayerPreLoginEvent.Result.ALLOWED) {
 +                event.disallow(asyncEvent.getResult(), asyncEvent.kickMessage()); // Paper - Adventure
 +            }
-+            Waitable<PlayerPreLoginEvent.Result> waitable = new Waitable<PlayerPreLoginEvent.Result>() {
++            Waitable<PlayerPreLoginEvent.Result> waitable = new Waitable<>() {
 +                @Override
 +                protected PlayerPreLoginEvent.Result evaluate() {
 +                    server.getPluginManager().callEvent(event);
@@ -312,7 +288,7 @@
 +            }
 +        }
 +        return gameprofile; // Paper - Add more fields to AsyncPlayerPreLoginEvent
-     }
++    }
 +    // CraftBukkit end
  
      @Override
@@ -326,7 +302,6 @@
 +            }
 +
 +            net.minecraft.network.FriendlyByteBuf buf = payload.buffer;
-+
 +            if (!com.destroystokyo.paper.proxy.VelocityProxy.checkIntegrity(buf)) {
 +                this.disconnect("Unable to verify player details");
 +                return;
@@ -368,15 +343,18 @@
      @Override
      public void handleLoginAcknowledgement(ServerboundLoginAcknowledgedPacket packet) {
 +        PacketUtils.ensureRunningOnSameThread(packet, this, this.server); // CraftBukkit
-         Validate.validState(this.state == ServerLoginPacketListenerImpl.State.PROTOCOL_SWITCHING, "Unexpected login acknowledgement packet", new Object[0]);
+         Validate.validState(this.state == ServerLoginPacketListenerImpl.State.PROTOCOL_SWITCHING, "Unexpected login acknowledgement packet");
          this.connection.setupOutboundProtocol(ConfigurationProtocols.CLIENTBOUND);
-         CommonListenerCookie commonlistenercookie = CommonListenerCookie.createInitial((GameProfile) Objects.requireNonNull(this.authenticatedProfile), this.transferred);
--        ServerConfigurationPacketListenerImpl serverconfigurationpacketlistenerimpl = new ServerConfigurationPacketListenerImpl(this.server, this.connection, commonlistenercookie);
-+        ServerConfigurationPacketListenerImpl serverconfigurationpacketlistenerimpl = new ServerConfigurationPacketListenerImpl(this.server, this.connection, commonlistenercookie, this.player); // CraftBukkit
- 
-         this.connection.setupInboundProtocol(ConfigurationProtocols.SERVERBOUND, serverconfigurationpacketlistenerimpl);
-         serverconfigurationpacketlistenerimpl.startConfiguration();
-@@ -264,12 +473,44 @@
+         CommonListenerCookie commonListenerCookie = CommonListenerCookie.createInitial(Objects.requireNonNull(this.authenticatedProfile), this.transferred);
+         ServerConfigurationPacketListenerImpl serverConfigurationPacketListenerImpl = new ServerConfigurationPacketListenerImpl(
+-            this.server, this.connection, commonListenerCookie
++            this.server, this.connection, commonListenerCookie, this.player // CraftBukkit
+         );
++
+         this.connection.setupInboundProtocol(ConfigurationProtocols.SERVERBOUND, serverConfigurationPacketListenerImpl);
+         serverConfigurationPacketListenerImpl.startConfiguration();
+         this.state = ServerLoginPacketListenerImpl.State.ACCEPTED;
+@@ -252,8 +_,36 @@
  
      @Override
      public void handleCookieResponse(ServerboundCookieResponsePacket packet) {
@@ -388,37 +366,36 @@
 +        // CraftBukkit end
          this.disconnect(ServerCommonPacketListenerImpl.DISCONNECT_UNEXPECTED_QUERY);
      }
- 
++
 +    // Spigot start
 +    protected GameProfile createOfflineProfile(String s) {
 +        java.util.UUID uuid;
-+        if ( this.connection.spoofedUUID != null )
-+        {
++        if (this.connection.spoofedUUID != null) {
 +            uuid = this.connection.spoofedUUID;
-+        } else
-+        {
-+            uuid = UUIDUtil.createOfflinePlayerUUID( s );
++        } else {
++            uuid = UUIDUtil.createOfflinePlayerUUID(s);
 +        }
 +
-+        GameProfile gameProfile = new GameProfile( uuid, s );
++        GameProfile gameProfile = new GameProfile(uuid, s);
 +
-+        if (this.connection.spoofedProfile != null)
-+        {
-+            for ( com.mojang.authlib.properties.Property property : this.connection.spoofedProfile )
-+            {
-+                if ( !ServerHandshakePacketListenerImpl.PROP_PATTERN.matcher( property.name()).matches() ) continue;
-+                gameProfile.getProperties().put( property.name(), property );
++        if (this.connection.spoofedProfile != null) {
++            for (com.mojang.authlib.properties.Property property : this.connection.spoofedProfile) {
++                if (!ServerHandshakePacketListenerImpl.PROP_PATTERN.matcher(property.name()).matches()) continue;
++                gameProfile.getProperties().put(property.name(), property);
 +            }
 +        }
 +
 +        return gameProfile;
 +    }
 +    // Spigot end
-+
+ 
      public static enum State {
- 
--        HELLO, KEY, AUTHENTICATING, NEGOTIATING, VERIFYING, WAITING_FOR_DUPE_DISCONNECT, PROTOCOL_SWITCHING, ACCEPTED;
-+        HELLO, KEY, AUTHENTICATING, NEGOTIATING, VERIFYING, WAITING_FOR_COOKIES, WAITING_FOR_DUPE_DISCONNECT, PROTOCOL_SWITCHING, ACCEPTED; // CraftBukkit
- 
-         private State() {}
-     }
+         HELLO,
+@@ -261,6 +_,7 @@
+         AUTHENTICATING,
+         NEGOTIATING,
+         VERIFYING,
++        WAITING_FOR_COOKIES, // CraftBukkit
+         WAITING_FOR_DUPE_DISCONNECT,
+         PROTOCOL_SWITCHING,
+         ACCEPTED;
diff --git a/paper-server/patches/sources/net/minecraft/server/network/ServerStatusPacketListenerImpl.java.patch b/paper-server/patches/sources/net/minecraft/server/network/ServerStatusPacketListenerImpl.java.patch
new file mode 100644
index 0000000000..b6419959d2
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/network/ServerStatusPacketListenerImpl.java.patch
@@ -0,0 +1,12 @@
+--- a/net/minecraft/server/network/ServerStatusPacketListenerImpl.java
++++ b/net/minecraft/server/network/ServerStatusPacketListenerImpl.java
+@@ -36,7 +_,8 @@
+             this.connection.disconnect(DISCONNECT_REASON);
+         } else {
+             this.hasRequestedStatus = true;
+-            this.connection.send(new ClientboundStatusResponsePacket(this.status));
++            // this.connection.send(new ClientboundStatusResponsePacket(this.status)); // Paper
++            com.destroystokyo.paper.network.StandardPaperServerListPingEventImpl.processRequest(net.minecraft.server.MinecraftServer.getServer(), this.connection); // Paper - handle status request
+         }
+     }
+ 
diff --git a/paper-server/patches/unapplied/net/minecraft/server/packs/PathPackResources.java.patch b/paper-server/patches/sources/net/minecraft/server/packs/PathPackResources.java.patch
similarity index 77%
rename from paper-server/patches/unapplied/net/minecraft/server/packs/PathPackResources.java.patch
rename to paper-server/patches/sources/net/minecraft/server/packs/PathPackResources.java.patch
index 4c21d9802f..7f7053b2b2 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/packs/PathPackResources.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/packs/PathPackResources.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/server/packs/PathPackResources.java
 +++ b/net/minecraft/server/packs/PathPackResources.java
-@@ -103,6 +103,12 @@
+@@ -103,6 +_,12 @@
          try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(path)) {
-             for (Path path2 : directoryStream) {
-                 String string = path2.getFileName().toString();
+             for (Path path1 : directoryStream) {
+                 String string = path1.getFileName().toString();
 +                // Paper start - Improve logging and errors
-+                if (!Files.isDirectory(path2)) {
++                if (!Files.isDirectory(path1)) {
 +                    LOGGER.error("Invalid directory entry: {} in {}.", string, this.root, new java.nio.file.NotDirectoryException(string));
 +                    continue;
 +                }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/packs/VanillaPackResourcesBuilder.java.patch b/paper-server/patches/sources/net/minecraft/server/packs/VanillaPackResourcesBuilder.java.patch
similarity index 97%
rename from paper-server/patches/unapplied/net/minecraft/server/packs/VanillaPackResourcesBuilder.java.patch
rename to paper-server/patches/sources/net/minecraft/server/packs/VanillaPackResourcesBuilder.java.patch
index 5f4418300b..acd2b45a58 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/packs/VanillaPackResourcesBuilder.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/packs/VanillaPackResourcesBuilder.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/server/packs/VanillaPackResourcesBuilder.java
 +++ b/net/minecraft/server/packs/VanillaPackResourcesBuilder.java
-@@ -138,6 +138,15 @@
+@@ -137,6 +_,15 @@
  
      public VanillaPackResourcesBuilder applyDevelopmentConfig() {
          developmentConfig.accept(this);
diff --git a/paper-server/patches/unapplied/net/minecraft/server/packs/repository/ServerPacksSource.java.patch b/paper-server/patches/sources/net/minecraft/server/packs/repository/ServerPacksSource.java.patch
similarity index 59%
rename from paper-server/patches/unapplied/net/minecraft/server/packs/repository/ServerPacksSource.java.patch
rename to paper-server/patches/sources/net/minecraft/server/packs/repository/ServerPacksSource.java.patch
index a5d66bbff8..dc3a6578a8 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/packs/repository/ServerPacksSource.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/packs/repository/ServerPacksSource.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/server/packs/repository/ServerPacksSource.java
 +++ b/net/minecraft/server/packs/repository/ServerPacksSource.java
-@@ -48,7 +48,7 @@
+@@ -48,7 +_,7 @@
      public static VanillaPackResources createVanillaPackSource() {
          return new VanillaPackResourcesBuilder()
              .setMetadata(BUILT_IN_METADATA)
@@ -9,23 +9,23 @@
              .applyDevelopmentConfig()
              .pushJarResources()
              .build(VANILLA_PACK_INFO);
-@@ -68,7 +68,18 @@
+@@ -68,7 +_,18 @@
      @Nullable
      @Override
-     protected Pack createBuiltinPack(String fileName, Pack.ResourcesSupplier packFactory, Component displayName) {
--        return Pack.readMetaAndCreate(createBuiltInPackLocation(fileName, displayName), packFactory, PackType.SERVER_DATA, FEATURE_SELECTION_CONFIG);
+     protected Pack createBuiltinPack(String id, Pack.ResourcesSupplier resources, Component title) {
+-        return Pack.readMetaAndCreate(createBuiltInPackLocation(id, title), resources, PackType.SERVER_DATA, FEATURE_SELECTION_CONFIG);
 +        // Paper start - custom built-in pack
 +        final PackLocationInfo info;
 +        final PackSelectionConfig packConfig;
-+        if ("paper".equals(fileName)) {
-+            info = new PackLocationInfo(fileName, displayName, PackSource.BUILT_IN, Optional.empty());
++        if ("paper".equals(id)) {
++            info = new PackLocationInfo(id, title, PackSource.BUILT_IN, Optional.empty());
 +            packConfig = new PackSelectionConfig(true, Pack.Position.TOP, true);
 +        } else {
-+            info = createBuiltInPackLocation(fileName, displayName);
++            info = createBuiltInPackLocation(id, title);
 +            packConfig = FEATURE_SELECTION_CONFIG;
 +        }
-+        return Pack.readMetaAndCreate(info, packFactory, PackType.SERVER_DATA, packConfig);
++        return Pack.readMetaAndCreate(info, resources, PackType.SERVER_DATA, packConfig);
 +        // Paper end - custom built-in pack
      }
  
-     public static PackRepository createPackRepository(Path dataPacksPath, DirectoryValidator symlinkFinder) {
+     public static PackRepository createPackRepository(Path folder, DirectoryValidator validator) {
diff --git a/paper-server/patches/unapplied/net/minecraft/server/players/BanListEntry.java.patch b/paper-server/patches/sources/net/minecraft/server/players/BanListEntry.java.patch
similarity index 65%
rename from paper-server/patches/unapplied/net/minecraft/server/players/BanListEntry.java.patch
rename to paper-server/patches/sources/net/minecraft/server/players/BanListEntry.java.patch
index 205eaad746..3e100a7758 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/players/BanListEntry.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/players/BanListEntry.java.patch
@@ -1,17 +1,17 @@
 --- a/net/minecraft/server/players/BanListEntry.java
 +++ b/net/minecraft/server/players/BanListEntry.java
-@@ -27,7 +27,7 @@
+@@ -26,7 +_,7 @@
      }
  
-     protected BanListEntry(@Nullable T key, JsonObject json) {
--        super(key);
-+        super(BanListEntry.checkExpiry(key, json)); // CraftBukkit
+     protected BanListEntry(@Nullable T user, JsonObject entryData) {
+-        super(user);
++        super(BanListEntry.checkExpiry(user, entryData)); // CraftBukkit
  
          Date date;
- 
-@@ -83,4 +83,22 @@
-         json.addProperty("expires", this.expires == null ? "forever" : BanListEntry.DATE_FORMAT.format(this.expires));
-         json.addProperty("reason", this.reason);
+         try {
+@@ -80,4 +_,22 @@
+         data.addProperty("expires", this.expires == null ? "forever" : DATE_FORMAT.format(this.expires));
+         data.addProperty("reason", this.reason);
      }
 +
 +    // CraftBukkit start
diff --git a/paper-server/patches/sources/net/minecraft/server/players/GameProfileCache.java.patch b/paper-server/patches/sources/net/minecraft/server/players/GameProfileCache.java.patch
new file mode 100644
index 0000000000..2336810f52
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/players/GameProfileCache.java.patch
@@ -0,0 +1,193 @@
+--- a/net/minecraft/server/players/GameProfileCache.java
++++ b/net/minecraft/server/players/GameProfileCache.java
+@@ -56,6 +_,10 @@
+     private final AtomicLong operationCount = new AtomicLong();
+     @Nullable
+     private Executor executor;
++    // Paper start - Fix GameProfileCache concurrency
++    protected final java.util.concurrent.locks.ReentrantLock stateLock = new java.util.concurrent.locks.ReentrantLock();
++    protected final java.util.concurrent.locks.ReentrantLock lookupLock = new java.util.concurrent.locks.ReentrantLock();
++    // Paper end - Fix GameProfileCache concurrency
+ 
+     public GameProfileCache(GameProfileRepository profileRepository, File file) {
+         this.profileRepository = profileRepository;
+@@ -64,10 +_,12 @@
+     }
+ 
+     private void safeAdd(GameProfileCache.GameProfileInfo profile) {
++        try { this.stateLock.lock(); // Paper - Fix GameProfileCache concurrency
+         GameProfile profile1 = profile.getProfile();
+         profile.setLastAccess(this.getNextOperation());
+         this.profilesByName.put(profile1.getName().toLowerCase(Locale.ROOT), profile);
+         this.profilesByUUID.put(profile1.getId(), profile);
++        } finally { this.stateLock.unlock(); } // Paper - Fix GameProfileCache concurrency
+     }
+ 
+     private static Optional<GameProfile> lookupGameProfile(GameProfileRepository profileRepo, String name) {
+@@ -86,6 +_,8 @@
+                     atomicReference.set(null);
+                 }
+             };
++            if (!org.apache.commons.lang3.StringUtils.isBlank(name) // Paper - Don't lookup a profile with a blank name
++                && io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode()) // Paper - Add setting for proxy online mode status
+             profileRepo.findProfilesByNames(new String[]{name}, profileLookupCallback);
+             GameProfile gameProfile = atomicReference.get();
+             return gameProfile != null ? Optional.of(gameProfile) : createUnknownProfile(name);
+@@ -101,7 +_,7 @@
+     }
+ 
+     private static boolean usesAuthentication() {
+-        return usesAuthentication;
++        return io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode(); // Paper - Add setting for proxy online mode status
+     }
+ 
+     public void add(GameProfile gameProfile) {
+@@ -111,15 +_,29 @@
+         Date time = instance.getTime();
+         GameProfileCache.GameProfileInfo gameProfileInfo = new GameProfileCache.GameProfileInfo(gameProfile, time);
+         this.safeAdd(gameProfileInfo);
+-        this.save();
++        if (!org.spigotmc.SpigotConfig.saveUserCacheOnStopOnly) this.save(true); // Spigot - skip saving if disabled // Paper - Perf: Async GameProfileCache saving
+     }
+ 
+     private long getNextOperation() {
+         return this.operationCount.incrementAndGet();
+     }
+ 
++    // Paper start
++    public @Nullable GameProfile getProfileIfCached(String name) {
++        try { this.stateLock.lock(); // Paper - Fix GameProfileCache concurrency
++        GameProfileCache.GameProfileInfo entry = this.profilesByName.get(name.toLowerCase(Locale.ROOT));
++        if (entry == null) {
++            return null;
++        }
++        entry.setLastAccess(this.getNextOperation());
++        return entry.getProfile();
++        } finally { this.stateLock.unlock(); } // Paper - Fix GameProfileCache concurrency
++    }
++    // Paper end
++
+     public Optional<GameProfile> get(String name) {
+         String string = name.toLowerCase(Locale.ROOT);
++        boolean stateLocked = true; try { this.stateLock.lock(); // Paper - Fix GameProfileCache concurrency
+         GameProfileCache.GameProfileInfo gameProfileInfo = this.profilesByName.get(string);
+         boolean flag = false;
+         if (gameProfileInfo != null && new Date().getTime() >= gameProfileInfo.expirationDate.getTime()) {
+@@ -133,19 +_,24 @@
+         if (gameProfileInfo != null) {
+             gameProfileInfo.setLastAccess(this.getNextOperation());
+             optional = Optional.of(gameProfileInfo.getProfile());
++            stateLocked = false; this.stateLock.unlock(); // Paper - Fix GameProfileCache concurrency
+         } else {
+-            optional = lookupGameProfile(this.profileRepository, string);
++            stateLocked = false; this.stateLock.unlock(); // Paper - Fix GameProfileCache concurrency
++            try { this.lookupLock.lock(); // Paper - Fix GameProfileCache concurrency
++            optional = lookupGameProfile(this.profileRepository, name); // CraftBukkit - use correct case for offline players
++            } finally { this.lookupLock.unlock(); } // Paper - Fix GameProfileCache concurrency
+             if (optional.isPresent()) {
+                 this.add(optional.get());
+                 flag = false;
+             }
+         }
+ 
+-        if (flag) {
+-            this.save();
++        if (flag && !org.spigotmc.SpigotConfig.saveUserCacheOnStopOnly) { // Spigot - skip saving if disabled
++            this.save(true); // Paper - Perf: Async GameProfileCache saving
+         }
+ 
+         return optional;
++        } finally { if (stateLocked) {  this.stateLock.unlock(); } } // Paper - Fix GameProfileCache concurrency
+     }
+ 
+     public CompletableFuture<Optional<GameProfile>> getAsync(String name) {
+@@ -157,7 +_,7 @@
+                 return completableFuture;
+             } else {
+                 CompletableFuture<Optional<GameProfile>> completableFuture1 = CompletableFuture.<Optional<GameProfile>>supplyAsync(
+-                        () -> this.get(name), Util.backgroundExecutor().forName("getProfile")
++                        () -> this.get(name), Util.PROFILE_EXECUTOR // Paper - don't submit BLOCKING PROFILE LOOKUPS to the world gen thread
+                     )
+                     .whenCompleteAsync((gameProfile, exception) -> this.requests.remove(name), this.executor);
+                 this.requests.put(name, completableFuture1);
+@@ -167,6 +_,7 @@
+     }
+ 
+     public Optional<GameProfile> get(UUID uuid) {
++        try { this.stateLock.lock(); // Paper - Fix GameProfileCache concurrency
+         GameProfileCache.GameProfileInfo gameProfileInfo = this.profilesByUUID.get(uuid);
+         if (gameProfileInfo == null) {
+             return Optional.empty();
+@@ -174,6 +_,7 @@
+             gameProfileInfo.setLastAccess(this.getNextOperation());
+             return Optional.of(gameProfileInfo.getProfile());
+         }
++        } finally { this.stateLock.unlock(); } // Paper - Fix GameProfileCache concurrency
+     }
+ 
+     public void setExecutor(Executor exectutor) {
+@@ -206,6 +_,11 @@
+ 
+             return (List<GameProfileCache.GameProfileInfo>)var9;
+         } catch (FileNotFoundException var7) {
++        // Spigot start
++        } catch (com.google.gson.JsonSyntaxException | NullPointerException ex) {
++            LOGGER.warn( "Usercache.json is corrupted or has bad formatting. Deleting it to prevent further issues." );
++            this.file.delete();
++        // Spigot end
+         } catch (JsonParseException | IOException var8) {
+             LOGGER.warn("Failed to load profile cache {}", this.file, var8);
+         }
+@@ -213,24 +_,45 @@
+         return list;
+     }
+ 
+-    public void save() {
++    public void save(boolean asyncSave) { // Paper - Perf: Async GameProfileCache saving
+         JsonArray jsonArray = new JsonArray();
+         DateFormat dateFormat = createDateFormat();
+-        this.getTopMRUProfiles(1000).forEach(info -> jsonArray.add(writeGameProfile(info, dateFormat)));
++        this.listTopMRUProfiles(org.spigotmc.SpigotConfig.userCacheCap).forEach((info) -> jsonArray.add(writeGameProfile(info, dateFormat))); // Spigot // Paper - Fix GameProfileCache concurrency
+         String string = this.gson.toJson((JsonElement)jsonArray);
++        Runnable save = () -> { // Paper - Perf: Async GameProfileCache saving
+ 
+         try (Writer writer = Files.newWriter(this.file, StandardCharsets.UTF_8)) {
+             writer.write(string);
+         } catch (IOException var9) {
+         }
++        // Paper start - Perf: Async GameProfileCache saving
++        };
++        if (asyncSave) {
++            io.papermc.paper.util.MCUtil.scheduleAsyncTask(save);
++        } else {
++            save.run();
++        }
++        // Paper end - Perf: Async GameProfileCache saving
+     }
+ 
+     private Stream<GameProfileCache.GameProfileInfo> getTopMRUProfiles(int limit) {
+-        return ImmutableList.copyOf(this.profilesByUUID.values())
+-            .stream()
+-            .sorted(Comparator.comparing(GameProfileCache.GameProfileInfo::getLastAccess).reversed())
+-            .limit(limit);
+-    }
++       // Paper start - Fix GameProfileCache concurrency
++       return this.listTopMRUProfiles(limit).stream();
++    }
++
++    private List<GameProfileCache.GameProfileInfo> listTopMRUProfiles(int limit) {
++        try {
++            this.stateLock.lock();
++            return this.profilesByUUID.values()
++                .stream()
++                .sorted(Comparator.comparing(GameProfileCache.GameProfileInfo::getLastAccess).reversed())
++                .limit(limit)
++                .toList();
++        } finally {
++            this.stateLock.unlock();
++        }
++    }
++    // Paper end - Fix GameProfileCache concurrency
+ 
+     private static JsonElement writeGameProfile(GameProfileCache.GameProfileInfo profileInfo, DateFormat dateFormat) {
+         JsonObject jsonObject = new JsonObject();
diff --git a/paper-server/patches/unapplied/net/minecraft/server/players/OldUsersConverter.java.patch b/paper-server/patches/sources/net/minecraft/server/players/OldUsersConverter.java.patch
similarity index 53%
rename from paper-server/patches/unapplied/net/minecraft/server/players/OldUsersConverter.java.patch
rename to paper-server/patches/sources/net/minecraft/server/players/OldUsersConverter.java.patch
index 7f3147ad37..cf078d7683 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/players/OldUsersConverter.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/players/OldUsersConverter.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/server/players/OldUsersConverter.java
 +++ b/net/minecraft/server/players/OldUsersConverter.java
-@@ -21,6 +21,9 @@
+@@ -20,6 +_,9 @@
  import java.util.UUID;
  import javax.annotation.Nullable;
  import net.minecraft.core.UUIDUtil;
@@ -10,62 +10,61 @@
  import net.minecraft.server.MinecraftServer;
  import net.minecraft.server.dedicated.DedicatedServer;
  import net.minecraft.util.StringUtil;
-@@ -62,7 +65,8 @@
-             return new String[i];
-         });
+@@ -49,7 +_,8 @@
  
+     private static void lookupPlayers(MinecraftServer server, Collection<String> names, ProfileLookupCallback callback) {
+         String[] strings = names.stream().filter(name -> !StringUtil.isNullOrEmpty(name)).toArray(String[]::new);
 -        if (server.usesAuthentication()) {
 +        if (server.usesAuthentication() ||
 +            (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode())) { // Spigot: bungee = online mode, for now.  // Paper - Add setting for proxy online mode status
-             server.getProfileRepository().findProfilesByNames(astring, callback);
+             server.getProfileRepository().findProfilesByNames(strings, callback);
          } else {
-             String[] astring1 = astring;
-@@ -85,7 +89,7 @@
+             for (String string : strings) {
+@@ -65,7 +_,7 @@
                  try {
-                     gameprofilebanlist.load();
-                 } catch (IOException ioexception) {
--                    OldUsersConverter.LOGGER.warn("Could not load existing file {}", gameprofilebanlist.getFile().getName(), ioexception);
-+                    OldUsersConverter.LOGGER.warn("Could not load existing file {}", gameprofilebanlist.getFile().getName()); // CraftBukkit - don't print stacktrace
+                     userBanList.load();
+                 } catch (IOException var6) {
+-                    LOGGER.warn("Could not load existing file {}", userBanList.getFile().getName(), var6);
++                    LOGGER.warn("Could not load existing file {}", userBanList.getFile().getName()); // CraftBukkit - don't print stacktrace
                  }
              }
  
-@@ -143,7 +147,7 @@
+@@ -120,7 +_,7 @@
                  try {
-                     ipbanlist.load();
-                 } catch (IOException ioexception) {
--                    OldUsersConverter.LOGGER.warn("Could not load existing file {}", ipbanlist.getFile().getName(), ioexception);
-+                    OldUsersConverter.LOGGER.warn("Could not load existing file {}", ipbanlist.getFile().getName()); // CraftBukkit - don't print stacktrace
+                     ipBanList.load();
+                 } catch (IOException var11) {
+-                    LOGGER.warn("Could not load existing file {}", ipBanList.getFile().getName(), var11);
++                    LOGGER.warn("Could not load existing file {}", ipBanList.getFile().getName()); // CraftBukkit - don't print stacktrace
                  }
              }
  
-@@ -184,7 +188,7 @@
+@@ -156,7 +_,7 @@
                  try {
-                     oplist.load();
-                 } catch (IOException ioexception) {
--                    OldUsersConverter.LOGGER.warn("Could not load existing file {}", oplist.getFile().getName(), ioexception);
-+                    OldUsersConverter.LOGGER.warn("Could not load existing file {}", oplist.getFile().getName()); // CraftBukkit - don't print stacktrace
+                     serverOpList.load();
+                 } catch (IOException var6) {
+-                    LOGGER.warn("Could not load existing file {}", serverOpList.getFile().getName(), var6);
++                    LOGGER.warn("Could not load existing file {}", serverOpList.getFile().getName()); // CraftBukkit - don't print stacktrace
                  }
              }
  
-@@ -228,7 +232,7 @@
+@@ -200,7 +_,7 @@
                  try {
-                     whitelist.load();
-                 } catch (IOException ioexception) {
--                    OldUsersConverter.LOGGER.warn("Could not load existing file {}", whitelist.getFile().getName(), ioexception);
-+                    OldUsersConverter.LOGGER.warn("Could not load existing file {}", whitelist.getFile().getName()); // CraftBukkit - don't print stacktrace
+                     userWhiteList.load();
+                 } catch (IOException var6) {
+-                    LOGGER.warn("Could not load existing file {}", userWhiteList.getFile().getName(), var6);
++                    LOGGER.warn("Could not load existing file {}", userWhiteList.getFile().getName()); // CraftBukkit - don't print stacktrace
                  }
              }
  
-@@ -346,7 +350,39 @@
-                     private void movePlayerFile(File playerDataFolder, String fileName, String uuid) {
-                         File file5 = new File(file, fileName + ".dat");
-                         File file6 = new File(playerDataFolder, uuid + ".dat");
-+
+@@ -313,6 +_,37 @@
+                     private void movePlayerFile(File file3, String oldFileName, String newFileName) {
+                         File file4 = new File(worldPlayersDirectory, oldFileName + ".dat");
+                         File file5 = new File(file3, newFileName + ".dat");
 +                        // CraftBukkit start - Use old file name to seed lastKnownName
 +                        CompoundTag root = null;
 +
 +                        try {
-+                            root = NbtIo.readCompressed(new java.io.FileInputStream(file5), NbtAccounter.unlimitedHeap());
++                            root = NbtIo.readCompressed(new java.io.FileInputStream(file4), NbtAccounter.unlimitedHeap());
 +                        } catch (Exception exception) {
 +                            // Paper start
 +                            io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateThrowable(exception);
@@ -73,16 +72,16 @@
 +                            com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(exception);
 +                            // Paper end
 +                        }
- 
++
 +                        if (root != null) {
 +                            if (!root.contains("bukkit")) {
 +                                root.put("bukkit", new CompoundTag());
 +                            }
 +                            CompoundTag data = root.getCompound("bukkit");
-+                            data.putString("lastKnownName", fileName);
++                            data.putString("lastKnownName", oldFileName);
 +
 +                            try {
-+                                NbtIo.writeCompressed(root, new java.io.FileOutputStream(file2));
++                                NbtIo.writeCompressed(root, new java.io.FileOutputStream(file1));
 +                            } catch (Exception exception) {
 +                                // Paper start
 +                                io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateThrowable(exception);
@@ -92,7 +91,6 @@
 +                            }
 +                       }
 +                        // CraftBukkit end
-+
-                         OldUsersConverter.ensureDirectoryExists(playerDataFolder);
-                         if (!file5.renameTo(file6)) {
-                             throw new OldUsersConverter.ConversionError("Could not convert file for " + fileName);
+                         OldUsersConverter.ensureDirectoryExists(file3);
+                         if (!file4.renameTo(file5)) {
+                             throw new OldUsersConverter.ConversionError("Could not convert file for " + oldFileName);
diff --git a/paper-server/patches/sources/net/minecraft/server/players/PlayerList.java.patch b/paper-server/patches/sources/net/minecraft/server/players/PlayerList.java.patch
new file mode 100644
index 0000000000..e9cb8b46e9
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/players/PlayerList.java.patch
@@ -0,0 +1,1133 @@
+--- a/net/minecraft/server/players/PlayerList.java
++++ b/net/minecraft/server/players/PlayerList.java
+@@ -112,14 +_,16 @@
+     private static final int SEND_PLAYER_INFO_INTERVAL = 600;
+     private static final SimpleDateFormat BAN_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z");
+     private final MinecraftServer server;
+-    public final List<ServerPlayer> players = Lists.newArrayList();
++    public final List<ServerPlayer> players = new java.util.concurrent.CopyOnWriteArrayList(); // CraftBukkit - ArrayList -> CopyOnWriteArrayList: Iterator safety
+     private final Map<UUID, ServerPlayer> playersByUUID = Maps.newHashMap();
+     private final UserBanList bans = new UserBanList(USERBANLIST_FILE);
+     private final IpBanList ipBans = new IpBanList(IPBANLIST_FILE);
+     private final ServerOpList ops = new ServerOpList(OPLIST_FILE);
+     private final UserWhiteList whitelist = new UserWhiteList(WHITELIST_FILE);
+-    private final Map<UUID, ServerStatsCounter> stats = Maps.newHashMap();
+-    private final Map<UUID, PlayerAdvancements> advancements = Maps.newHashMap();
++    // CraftBukkit start
++    // private final Map<UUID, ServerStatsCounter> stats = Maps.newHashMap();
++    // private final Map<UUID, PlayerAdvancements> advancements = Maps.newHashMap();
++    // CraftBukkit end
+     public final PlayerDataStorage playerIo;
+     private boolean doWhiteList;
+     private final LayeredRegistryAccess<RegistryLayer> registries;
+@@ -130,14 +_,26 @@
+     private static final boolean ALLOW_LOGOUTIVATOR = false;
+     private int sendAllPlayerInfoIn;
+ 
++    // CraftBukkit start
++    private org.bukkit.craftbukkit.CraftServer cserver;
++    private final Map<String,ServerPlayer> playersByName = new java.util.HashMap<>();
++    public @Nullable String collideRuleTeamName; // Paper - Configurable player collision
++
+     public PlayerList(MinecraftServer server, LayeredRegistryAccess<RegistryLayer> registries, PlayerDataStorage playerIo, int maxPlayers) {
++        this.cserver = server.server = new org.bukkit.craftbukkit.CraftServer((net.minecraft.server.dedicated.DedicatedServer) server, this);
++        server.console = new com.destroystokyo.paper.console.TerminalConsoleCommandSender(); // Paper
++        // CraftBukkit end
+         this.server = server;
+         this.registries = registries;
+         this.maxPlayers = maxPlayers;
+         this.playerIo = playerIo;
+     }
+ 
++    abstract public void loadAndSaveFiles(); // Paper - fix converting txt to json file; moved from DedicatedPlayerList constructor
++
+     public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie cookie) {
++        player.isRealPlayer = true; // Paper
++        player.loginTime = System.currentTimeMillis(); // Paper - Replace OfflinePlayer#getLastPlayed
+         GameProfile gameProfile = player.getGameProfile();
+         GameProfileCache profileCache = this.server.getProfileCache();
+         String string;
+@@ -150,30 +_,93 @@
+         }
+ 
+         Optional<CompoundTag> optional = this.load(player);
+-        ResourceKey<Level> resourceKey = optional.<ResourceKey<Level>>flatMap(
+-                compoundTag -> DimensionType.parseLegacy(new Dynamic<>(NbtOps.INSTANCE, compoundTag.get("Dimension"))).resultOrPartial(LOGGER::error)
++        // CraftBukkit start - Better rename detection
++        if (optional.isPresent()) {
++            CompoundTag nbttagcompound = optional.get();
++            if (nbttagcompound.contains("bukkit")) {
++                CompoundTag bukkit = nbttagcompound.getCompound("bukkit");
++                string = bukkit.contains("lastKnownName", 8) ? bukkit.getString("lastKnownName") : string;
++            }
++        }
++        // CraftBukkit end
++        // Paper start - move logic in Entity to here, to use bukkit supplied world UUID & reset to main world spawn if no valid world is found
++        ResourceKey<Level> resourceKey = null; // Paper
++        boolean[] invalidPlayerWorld = {false};
++        bukkitData: if (optional.isPresent()) {
++            // The main way for bukkit worlds to store the world is the world UUID despite mojang adding custom worlds
++            final org.bukkit.World bWorld;
++            if (optional.get().contains("WorldUUIDMost") && optional.get().contains("WorldUUIDLeast")) {
++                bWorld = org.bukkit.Bukkit.getServer().getWorld(new UUID(optional.get().getLong("WorldUUIDMost"), optional.get().getLong("WorldUUIDLeast")));
++            } else if (optional.get().contains("world", net.minecraft.nbt.Tag.TAG_STRING)) { // Paper - legacy bukkit world name
++                bWorld = org.bukkit.Bukkit.getServer().getWorld(optional.get().getString("world"));
++            } else {
++                break bukkitData; // if neither of the bukkit data points exist, proceed to the vanilla migration section
++            }
++            if (bWorld != null) {
++                resourceKey = ((org.bukkit.craftbukkit.CraftWorld) bWorld).getHandle().dimension();
++            } else {
++                resourceKey = Level.OVERWORLD;
++                invalidPlayerWorld[0] = true;
++            }
++        }
++        if (resourceKey == null) { // only run the vanilla logic if we haven't found a world from the bukkit data
++        // Below is the vanilla way of getting the dimension, this is for migration from vanilla servers
++        resourceKey = optional.<ResourceKey<Level>>flatMap(
++                compoundTag -> {
++                    com.mojang.serialization.DataResult<ResourceKey<Level>> dataResult = DimensionType.parseLegacy(new Dynamic<>(NbtOps.INSTANCE, compoundTag.get("Dimension")));
++                    final Optional<ResourceKey<Level>> result = dataResult.resultOrPartial(LOGGER::error);
++                    invalidPlayerWorld[0] = result.isEmpty(); // reset to main world spawn if no valid world is found
++                    return result;
++                }
+             )
+-            .orElse(Level.OVERWORLD);
++            .orElse(Level.OVERWORLD); // revert to vanilla default main world, this isn't an "invalid world" since no player data existed
++        }
++        // Paper end
+         ServerLevel level = this.server.getLevel(resourceKey);
+         ServerLevel serverLevel;
+         if (level == null) {
+             LOGGER.warn("Unknown respawn dimension {}, defaulting to overworld", resourceKey);
+             serverLevel = this.server.overworld();
++            invalidPlayerWorld[0] = true; // Paper - reset to main world if no world with parsed value is found
+         } else {
+             serverLevel = level;
+         }
+ 
++        // Paper start - Entity#getEntitySpawnReason
++        if (optional.isEmpty()) {
++            player.spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT; // set Player SpawnReason to DEFAULT on first login
++            // Paper start - reset to main world spawn if first spawn or invalid world
++        }
++        if (optional.isEmpty() || invalidPlayerWorld[0]) {
++            // Paper end - reset to main world spawn if first spawn or invalid world
++            player.moveTo(player.adjustSpawnLocation(serverLevel, serverLevel.getSharedSpawnPos()).getBottomCenter(), serverLevel.getSharedSpawnAngle(), 0.0F); // Paper - MC-200092 - fix first spawn pos yaw being ignored
++        }
++        // Paper end - Entity#getEntitySpawnReason
+         player.setServerLevel(serverLevel);
+         String loggableAddress = connection.getLoggableAddress(this.server.logIPs());
+-        LOGGER.info(
+-            "{}[{}] logged in with entity id {} at ({}, {}, {})",
+-            player.getName().getString(),
+-            loggableAddress,
+-            player.getId(),
+-            player.getX(),
+-            player.getY(),
+-            player.getZ()
+-        );
++        // Spigot start - spawn location event
++        org.bukkit.entity.Player spawnPlayer = player.getBukkitEntity();
++        org.spigotmc.event.player.PlayerSpawnLocationEvent ev = new org.spigotmc.event.player.PlayerSpawnLocationEvent(spawnPlayer, spawnPlayer.getLocation());
++        this.cserver.getPluginManager().callEvent(ev);
++
++        org.bukkit.Location loc = ev.getSpawnLocation();
++        serverLevel = ((org.bukkit.craftbukkit.CraftWorld) loc.getWorld()).getHandle();
++
++        player.spawnIn(serverLevel);
++        // Paper start - set raw so we aren't fully joined to the world (not added to chunk or world)
++        player.setPosRaw(loc.getX(), loc.getY(), loc.getZ());
++        player.setRot(loc.getYaw(), loc.getPitch());
++        // Paper end - set raw so we aren't fully joined to the world
++        // Spigot end
++        // LOGGER.info( // CraftBukkit - Moved message to after join
++        //     "{}[{}] logged in with entity id {} at ({}, {}, {})",
++        //     player.getName().getString(),
++        //     loggableAddress,
++        //     player.getId(),
++        //     player.getX(),
++        //     player.getY(),
++        //     player.getZ()
++        // );
+         LevelData levelData = serverLevel.getLevelData();
+         player.loadGameTypes(optional.orElse(null));
+         ServerGamePacketListenerImpl serverGamePacketListenerImpl = new ServerGamePacketListenerImpl(this.server, connection, player, cookie);
+@@ -190,8 +_,8 @@
+                 levelData.isHardcore(),
+                 this.server.levelKeys(),
+                 this.getMaxPlayers(),
+-                this.viewDistance,
+-                this.simulationDistance,
++                serverLevel.spigotConfig.viewDistance,// Spigot - view distance
++                serverLevel.spigotConfig.simulationDistance,
+                 _boolean1,
+                 !_boolean,
+                 _boolean2,
+@@ -199,6 +_,7 @@
+                 this.server.enforceSecureProfile()
+             )
+         );
++        player.getBukkitEntity().sendSupportedChannels(); // CraftBukkit
+         serverGamePacketListenerImpl.send(new ClientboundChangeDifficultyPacket(levelData.getDifficulty(), levelData.isDifficultyLocked()));
+         serverGamePacketListenerImpl.send(new ClientboundPlayerAbilitiesPacket(player.getAbilities()));
+         serverGamePacketListenerImpl.send(new ClientboundSetHeldSlotPacket(player.getInventory().selected));
+@@ -218,24 +_,117 @@
+             mutableComponent = Component.translatable("multiplayer.player.joined.renamed", player.getDisplayName(), string);
+         }
+ 
+-        this.broadcastSystemMessage(mutableComponent.withStyle(ChatFormatting.YELLOW), false);
++        // CraftBukkit start
++        mutableComponent.withStyle(ChatFormatting.YELLOW);
++        Component joinMessage = mutableComponent; // Paper - Adventure
+         serverGamePacketListenerImpl.teleport(player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot());
+         ServerStatus status = this.server.getStatus();
+         if (status != null && !cookie.transferred()) {
+             player.sendServerStatus(status);
+         }
+ 
+-        player.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(this.players));
++        // player.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(this.players)); // CraftBukkit - replaced with loop below
+         this.players.add(player);
++        this.playersByName.put(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT), player); // Spigot
+         this.playersByUUID.put(player.getUUID(), player);
+-        this.broadcastAll(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(player)));
+-        this.sendLevelInfo(player, serverLevel);
++        // this.broadcastAll(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(player))); // CraftBukkit - replaced with loop below
++        // Paper start - Fire PlayerJoinEvent when Player is actually ready; correctly register player BEFORE PlayerJoinEvent, so the entity is valid and doesn't require tick delay hacks
++        player.supressTrackerForLogin = true;
+         serverLevel.addNewPlayer(player);
+-        this.server.getCustomBossEvents().onPlayerConnect(player);
+-        this.sendActivePlayerEffects(player);
++        this.server.getCustomBossEvents().onPlayerConnect(player); // see commented out section below serverLevel.addPlayerJoin(player);
++        // Paper end - Fire PlayerJoinEvent when Player is actually ready
+         player.loadAndSpawnEnderpearls(optional);
+         player.loadAndSpawnParentVehicle(optional);
++        // CraftBukkit start
++        org.bukkit.craftbukkit.entity.CraftPlayer bukkitPlayer = player.getBukkitEntity();
++
++        // Ensure that player inventory is populated with its viewer
++        player.containerMenu.transferTo(player.containerMenu, bukkitPlayer);
++
++        org.bukkit.event.player.PlayerJoinEvent playerJoinEvent = new org.bukkit.event.player.PlayerJoinEvent(bukkitPlayer, io.papermc.paper.adventure.PaperAdventure.asAdventure(mutableComponent)); // Paper - Adventure
++        this.cserver.getPluginManager().callEvent(playerJoinEvent);
++
++        if (!player.connection.isAcceptingMessages()) {
++            return;
++        }
++
++        final net.kyori.adventure.text.Component jm = playerJoinEvent.joinMessage();
++
++        if (jm != null && !jm.equals(net.kyori.adventure.text.Component.empty())) { // Paper - Adventure
++            joinMessage = io.papermc.paper.adventure.PaperAdventure.asVanilla(jm); // Paper - Adventure
++            this.server.getPlayerList().broadcastSystemMessage(joinMessage, false); // Paper - Adventure
++        }
++        // CraftBukkit end
++
++        // CraftBukkit start - sendAll above replaced with this loop
++        ClientboundPlayerInfoUpdatePacket packet = ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(player)); // Paper - Add Listing API for Player
++
++        final List<ServerPlayer> onlinePlayers = Lists.newArrayListWithExpectedSize(this.players.size() - 1); // Paper - Use single player info update packet on join
++        for (int i = 0; i < this.players.size(); ++i) {
++            ServerPlayer entityplayer1 = (ServerPlayer) this.players.get(i);
++
++            if (entityplayer1.getBukkitEntity().canSee(bukkitPlayer)) {
++                // Paper start - Add Listing API for Player
++                if (entityplayer1.getBukkitEntity().isListed(bukkitPlayer)) {
++                // Paper end - Add Listing API for Player
++                entityplayer1.connection.send(packet);
++                // Paper start - Add Listing API for Player
++                } else {
++                    entityplayer1.connection.send(ClientboundPlayerInfoUpdatePacket.createSinglePlayerInitializing(player, false));
++                }
++                // Paper end - Add Listing API for Player
++            }
++
++            if (entityplayer1 == player || !bukkitPlayer.canSee(entityplayer1.getBukkitEntity())) { // Paper - Use single player info update packet on join; Don't include joining player
++                continue;
++            }
++
++            onlinePlayers.add(entityplayer1); // Paper - Use single player info update packet on join
++        }
++        // Paper start - Use single player info update packet on join
++        if (!onlinePlayers.isEmpty()) {
++            player.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(onlinePlayers, player)); // Paper - Add Listing API for Player
++        }
++        // Paper end - Use single player info update packet on join
++        player.sentListPacket = true;
++        player.supressTrackerForLogin = false; // Paper - Fire PlayerJoinEvent when Player is actually ready
++        ((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 // Paper - THIS IS NOT NEEDED ANYMORE
++
++        this.sendLevelInfo(player, serverLevel);
++
++        // CraftBukkit start - Only add if the player wasn't moved in the event
++        if (player.level() == serverLevel && !serverLevel.players().contains(player)) {
++            serverLevel.addNewPlayer(player);
++            this.server.getCustomBossEvents().onPlayerConnect(player);
++        }
++
++        serverLevel = player.serverLevel(); // CraftBukkit - Update in case join event changed it
++        // CraftBukkit end
++        this.sendActivePlayerEffects(player);
++        // Paper - move loading pearls / parent vehicle up
+         player.initInventoryMenu();
++        // CraftBukkit - Moved from above, added world
++        // Paper start - Configurable player collision; Add to collideRule team if needed
++        final net.minecraft.world.scores.Scoreboard scoreboard = this.getServer().getLevel(Level.OVERWORLD).getScoreboard();
++        final PlayerTeam collideRuleTeam = scoreboard.getPlayerTeam(this.collideRuleTeamName);
++        if (this.collideRuleTeamName != null && collideRuleTeam != null && player.getTeam() == null) {
++            scoreboard.addPlayerToTeam(player.getScoreboardName(), collideRuleTeam);
++        }
++        // Paper end - Configurable player collision
++        PlayerList.LOGGER.info("{}[{}] logged in with entity id {} at ([{}]{}, {}, {})", player.getName().getString(), loggableAddress, player.getId(), serverLevel.serverLevelData.getLevelName(), player.getX(), player.getY(), player.getZ());
++        // Paper start - Send empty chunk, so players aren't stuck in the world loading screen with our chunk system not sending chunks when dead
++        if (player.isDeadOrDying()) {
++            net.minecraft.core.Holder<net.minecraft.world.level.biome.Biome> plains = serverLevel.registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.BIOME)
++                    .getOrThrow(net.minecraft.world.level.biome.Biomes.PLAINS);
++            player.connection.send(new net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket(
++                    new net.minecraft.world.level.chunk.EmptyLevelChunk(serverLevel, player.chunkPosition(), plains),
++                    serverLevel.getLightEngine(), (java.util.BitSet)null, (java.util.BitSet) null)
++            );
++        }
++        // Paper end - Send empty chunk
+     }
+ 
+     public void updateEntireScoreboard(ServerScoreboard scoreboard, ServerPlayer player) {
+@@ -258,30 +_,31 @@
+     }
+ 
+     public void addWorldborderListener(ServerLevel level) {
++        if (this.playerIo != null) return; // CraftBukkit
+         level.getWorldBorder().addListener(new BorderChangeListener() {
+             @Override
+             public void onBorderSizeSet(WorldBorder border, double size) {
+-                PlayerList.this.broadcastAll(new ClientboundSetBorderSizePacket(border));
++                PlayerList.this.broadcastAll(new ClientboundSetBorderSizePacket(border), border.world); // CraftBukkit
+             }
+ 
+             @Override
+             public void onBorderSizeLerping(WorldBorder border, double oldSize, double newSize, long time) {
+-                PlayerList.this.broadcastAll(new ClientboundSetBorderLerpSizePacket(border));
++                PlayerList.this.broadcastAll(new ClientboundSetBorderLerpSizePacket(border), border.world); // CraftBukkit
+             }
+ 
+             @Override
+             public void onBorderCenterSet(WorldBorder border, double x, double z) {
+-                PlayerList.this.broadcastAll(new ClientboundSetBorderCenterPacket(border));
++                PlayerList.this.broadcastAll(new ClientboundSetBorderCenterPacket(border), border.world); // CraftBukkit
+             }
+ 
+             @Override
+             public void onBorderSetWarningTime(WorldBorder border, int warningTime) {
+-                PlayerList.this.broadcastAll(new ClientboundSetBorderWarningDelayPacket(border));
++                PlayerList.this.broadcastAll(new ClientboundSetBorderWarningDelayPacket(border), border.world); // CraftBukkit
+             }
+ 
+             @Override
+             public void onBorderSetWarningBlocks(WorldBorder border, int warningBlocks) {
+-                PlayerList.this.broadcastAll(new ClientboundSetBorderWarningDistancePacket(border));
++                PlayerList.this.broadcastAll(new ClientboundSetBorderWarningDistancePacket(border), border.world); // CraftBukkit
+             }
+ 
+             @Override
+@@ -309,56 +_,162 @@
+     }
+ 
+     protected void save(ServerPlayer player) {
++        if (!player.getBukkitEntity().isPersistent()) return; // CraftBukkit
+         this.playerIo.save(player);
+-        ServerStatsCounter serverStatsCounter = this.stats.get(player.getUUID());
++        ServerStatsCounter serverStatsCounter = player.getStats(); // CraftBukkit
+         if (serverStatsCounter != null) {
+             serverStatsCounter.save();
+         }
+ 
+-        PlayerAdvancements playerAdvancements = this.advancements.get(player.getUUID());
++        PlayerAdvancements playerAdvancements = player.getAdvancements(); // CraftBukkit
+         if (playerAdvancements != null) {
+             playerAdvancements.save();
+         }
+     }
+ 
+-    public void remove(ServerPlayer player) {
++    public net.kyori.adventure.text.Component remove(ServerPlayer player) { // CraftBukkit - return string // Paper - return Component
++        // Paper start - Fix kick event leave message not being sent
++        return this.remove(player, net.kyori.adventure.text.Component.translatable("multiplayer.player.left", net.kyori.adventure.text.format.NamedTextColor.YELLOW, io.papermc.paper.configuration.GlobalConfiguration.get().messages.useDisplayNameInQuitMessage ? player.getBukkitEntity().displayName() : io.papermc.paper.adventure.PaperAdventure.asAdventure(player.getDisplayName())));
++    }
++    public net.kyori.adventure.text.Component remove(ServerPlayer player, net.kyori.adventure.text.Component leaveMessage) {
++        // Paper end - Fix kick event leave message not being sent
+         ServerLevel serverLevel = player.serverLevel();
+         player.awardStat(Stats.LEAVE_GAME);
++        // CraftBukkit start - Quitting must be before we do final save of data, in case plugins need to modify it
++        // See SPIGOT-5799, SPIGOT-6145
++        if (player.containerMenu != player.inventoryMenu) {
++            player.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.DISCONNECT); // Paper - Inventory close reason
++        }
++
++        org.bukkit.event.player.PlayerQuitEvent playerQuitEvent = new org.bukkit.event.player.PlayerQuitEvent(player.getBukkitEntity(), leaveMessage, player.quitReason); // Paper - Adventure & Add API for quit reason
++        this.cserver.getPluginManager().callEvent(playerQuitEvent);
++        player.getBukkitEntity().disconnect(playerQuitEvent.getQuitMessage());
++
++        if (this.server.isSameThread()) player.doTick(); // SPIGOT-924 // Paper - Improved watchdog support; don't tick during emergency shutdowns
++        // CraftBukkit end
++
++        // Paper start - Configurable player collision; Remove from collideRule team if needed
++        if (this.collideRuleTeamName != null) {
++            final net.minecraft.world.scores.Scoreboard scoreBoard = this.server.getLevel(Level.OVERWORLD).getScoreboard();
++            final PlayerTeam team = scoreBoard.getPlayersTeam(this.collideRuleTeamName);
++            if (player.getTeam() == team && team != null) {
++                scoreBoard.removePlayerFromTeam(player.getScoreboardName(), team);
++            }
++        }
++        // Paper end - Configurable player collision
++
++        // Paper - Drop carried item when player has disconnected
++        if (!player.containerMenu.getCarried().isEmpty()) {
++            net.minecraft.world.item.ItemStack carried = player.containerMenu.getCarried();
++            player.containerMenu.setCarried(net.minecraft.world.item.ItemStack.EMPTY);
++            player.drop(carried, false);
++        }
++        // Paper end - Drop carried item when player has disconnected
+         this.save(player);
+         if (player.isPassenger()) {
+             Entity rootVehicle = player.getRootVehicle();
+             if (rootVehicle.hasExactlyOnePlayerPassenger()) {
+                 LOGGER.debug("Removing player mount");
+                 player.stopRiding();
+-                rootVehicle.getPassengersAndSelf().forEach(entity -> entity.setRemoved(Entity.RemovalReason.UNLOADED_WITH_PLAYER));
++                rootVehicle.getPassengersAndSelf().forEach(entity -> {
++                    // Paper start - Fix villager boat exploit
++                    if (entity instanceof net.minecraft.world.entity.npc.AbstractVillager villager) {
++                        final net.minecraft.world.entity.player.Player human = villager.getTradingPlayer();
++                        if (human != null) {
++                            villager.setTradingPlayer(null);
++                        }
++                    }
++                    // Paper end - Fix villager boat exploit
++                    entity.setRemoved(Entity.RemovalReason.UNLOADED_WITH_PLAYER, org.bukkit.event.entity.EntityRemoveEvent.Cause.PLAYER_QUIT); // CraftBukkit - add Bukkit remove cause
++                });
+             }
+         }
+ 
+         player.unRide();
+ 
+         for (ThrownEnderpearl thrownEnderpearl : player.getEnderPearls()) {
+-            thrownEnderpearl.setRemoved(Entity.RemovalReason.UNLOADED_WITH_PLAYER);
++            // Paper start - Allow using old ender pearl behavior
++            if (!thrownEnderpearl.level().paperConfig().misc.legacyEnderPearlBehavior) {
++                thrownEnderpearl.setRemoved(Entity.RemovalReason.UNLOADED_WITH_PLAYER, org.bukkit.event.entity.EntityRemoveEvent.Cause.PLAYER_QUIT); // CraftBukkit - add Bukkit remove cause
++            } else {
++                thrownEnderpearl.cachedOwner = null;
++            }
++            // Paper end - Allow using old ender pearl behavior
+         }
+ 
+         serverLevel.removePlayerImmediately(player, Entity.RemovalReason.UNLOADED_WITH_PLAYER);
++        player.retireScheduler(); // Paper - Folia schedulers
+         player.getAdvancements().stopListening();
+         this.players.remove(player);
++        this.playersByName.remove(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot
+         this.server.getCustomBossEvents().onPlayerDisconnect(player);
+         UUID uuid = player.getUUID();
+         ServerPlayer serverPlayer = this.playersByUUID.get(uuid);
+         if (serverPlayer == player) {
+             this.playersByUUID.remove(uuid);
+-            this.stats.remove(uuid);
+-            this.advancements.remove(uuid);
+-        }
+-
+-        this.broadcastAll(new ClientboundPlayerInfoRemovePacket(List.of(player.getUUID())));
++            // CraftBukkit start
++            // this.stats.remove(uuid);
++            // this.advancements.remove(uuid);
++            // CraftBukkit end
++        }
++
++        // CraftBukkit start
++        // this.broadcastAll(new ClientboundPlayerInfoRemovePacket(List.of(player.getUUID())));
++        ClientboundPlayerInfoRemovePacket packet = new ClientboundPlayerInfoRemovePacket(List.of(player.getUUID()));
++        for (int i = 0; i < this.players.size(); i++) {
++            ServerPlayer otherPlayer = (ServerPlayer) this.players.get(i);
++
++            if (otherPlayer.getBukkitEntity().canSee(player.getBukkitEntity())) {
++                otherPlayer.connection.send(packet);
++            } else {
++                otherPlayer.getBukkitEntity().onEntityRemove(player);
++            }
++        }
++        // This removes the scoreboard (and player reference) for the specific player in the manager
++        this.cserver.getScoreboardManager().removePlayer(player.getBukkitEntity());
++        // CraftBukkit end
++        return playerQuitEvent.quitMessage(); // Paper - Adventure
+     }
+ 
+-    @Nullable
+-    public Component canPlayerLogin(SocketAddress socketAddress, GameProfile gameProfile) {
+-        if (this.bans.isBanned(gameProfile)) {
+-            UserBanListEntry userBanListEntry = this.bans.get(gameProfile);
++    // CraftBukkit start - Whole method, SocketAddress to LoginListener, added hostname to signature, return EntityPlayer
++    public ServerPlayer canPlayerLogin(net.minecraft.server.network.ServerLoginPacketListenerImpl loginlistener, GameProfile gameProfile) {
++        // if (this.bans.isBanned(gameProfile)) {
++        //     UserBanListEntry userBanListEntry = this.bans.get(gameProfile);
++        // Moved from processLogin
++        UUID uuid = gameProfile.getId();
++        List<ServerPlayer> list = Lists.newArrayList();
++
++        ServerPlayer entityplayer;
++
++        for (int i = 0; i < this.players.size(); ++i) {
++            entityplayer = (ServerPlayer) this.players.get(i);
++            if (entityplayer.getUUID().equals(uuid) || (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode() && entityplayer.getGameProfile().getName().equalsIgnoreCase(gameProfile.getName()))) { // Paper - validate usernames
++                list.add(entityplayer);
++            }
++        }
++
++        java.util.Iterator iterator = list.iterator();
++
++        while (iterator.hasNext()) {
++            entityplayer = (ServerPlayer) iterator.next();
++            this.save(entityplayer); // CraftBukkit - Force the player's inventory to be saved
++            entityplayer.connection.disconnect(Component.translatable("multiplayer.disconnect.duplicate_login"), org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN); // Paper - kick event cause
++        }
++
++        // Instead of kicking then returning, we need to store the kick reason
++        // in the event, check with plugins to see if it's ok, and THEN kick
++        // depending on the outcome.
++        SocketAddress socketAddress = loginlistener.connection.getRemoteAddress();
++
++        ServerPlayer entity = new ServerPlayer(this.server, this.server.getLevel(Level.OVERWORLD), gameProfile, ClientInformation.createDefault());
++        entity.transferCookieConnection = loginlistener;
++        org.bukkit.entity.Player player = entity.getBukkitEntity();
++        org.bukkit.event.player.PlayerLoginEvent event = new org.bukkit.event.player.PlayerLoginEvent(player, loginlistener.connection.hostname, ((java.net.InetSocketAddress) socketAddress).getAddress(), ((java.net.InetSocketAddress) loginlistener.connection.channel.remoteAddress()).getAddress());
++
++        // Paper start - Fix MC-158900
++        UserBanListEntry userBanListEntry;
++        if (this.bans.isBanned(gameProfile) && (userBanListEntry = this.bans.get(gameProfile)) != null) {
++            // Paper end - Fix MC-158900
+             MutableComponent mutableComponent = Component.translatable("multiplayer.disconnect.banned.reason", userBanListEntry.getReason());
+             if (userBanListEntry.getExpires() != null) {
+                 mutableComponent.append(
+@@ -366,10 +_,12 @@
+                 );
+             }
+ 
+-            return mutableComponent;
+-        } else if (!this.isWhiteListed(gameProfile)) {
+-            return Component.translatable("multiplayer.disconnect.not_whitelisted");
+-        } else if (this.ipBans.isBanned(socketAddress)) {
++            // return mutableComponent;
++            event.disallow(org.bukkit.event.player.PlayerLoginEvent.Result.KICK_BANNED, io.papermc.paper.adventure.PaperAdventure.asAdventure(mutableComponent)); // Paper - Adventure
++        } else if (!this.isWhiteListed(gameProfile, event)) { // Paper - ProfileWhitelistVerifyEvent
++            // return Component.translatable("multiplayer.disconnect.not_whitelisted");
++            //event.disallow(PlayerLoginEvent.Result.KICK_WHITELIST, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.whitelistMessage)); // Spigot // Paper - Adventure - moved to isWhitelisted
++        } else if (this.ipBans.isBanned(socketAddress) && getIpBans().get(socketAddress) != null && !this.getIpBans().get(socketAddress).hasExpired()) { // Paper - fix NPE with temp ip bans
+             IpBanListEntry ipBanListEntry = this.ipBans.get(socketAddress);
+             MutableComponent mutableComponent = Component.translatable("multiplayer.disconnect.banned_ip.reason", ipBanListEntry.getReason());
+             if (ipBanListEntry.getExpires() != null) {
+@@ -378,69 +_,129 @@
+                 );
+             }
+ 
+-            return mutableComponent;
++            // return mutableComponent;
++            event.disallow(org.bukkit.event.player.PlayerLoginEvent.Result.KICK_BANNED, io.papermc.paper.adventure.PaperAdventure.asAdventure(mutableComponent)); // Paper - Adventure
+         } else {
+-            return this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameProfile)
+-                ? Component.translatable("multiplayer.disconnect.server_full")
+-                : null;
+-        }
+-    }
+-
+-    public ServerPlayer getPlayerForLogin(GameProfile gameProfile, ClientInformation clientInformation) {
+-        return new ServerPlayer(this.server, this.server.overworld(), gameProfile, clientInformation);
+-    }
+-
+-    public boolean disconnectAllPlayersWithProfile(GameProfile gameProfile) {
+-        UUID id = gameProfile.getId();
+-        Set<ServerPlayer> set = Sets.newIdentityHashSet();
+-
+-        for (ServerPlayer serverPlayer : this.players) {
+-            if (serverPlayer.getUUID().equals(id)) {
+-                set.add(serverPlayer);
++            // return this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameProfile)
++            //     ? Component.translatable("multiplayer.disconnect.server_full")
++            //     : null;
++            if (this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameProfile)) {
++                event.disallow(org.bukkit.event.player.PlayerLoginEvent.Result.KICK_FULL, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.serverFullMessage)); // Spigot // Paper - Adventure
+             }
+         }
+-
+-        ServerPlayer serverPlayer1 = this.playersByUUID.get(gameProfile.getId());
+-        if (serverPlayer1 != null) {
+-            set.add(serverPlayer1);
+-        }
+-
+-        for (ServerPlayer serverPlayer2 : set) {
+-            serverPlayer2.connection.disconnect(DUPLICATE_LOGIN_DISCONNECT_MESSAGE);
+-        }
+-
+-        return !set.isEmpty();
+-    }
+-
+-    public ServerPlayer respawn(ServerPlayer player, boolean keepInventory, Entity.RemovalReason reason) {
++        this.cserver.getPluginManager().callEvent(event);
++        if (event.getResult() != org.bukkit.event.player.PlayerLoginEvent.Result.ALLOWED) {
++            loginlistener.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.kickMessage())); // Paper - Adventure
++            return null;
++        }
++        return entity;
++    }
++
++    // CraftBukkit start - added EntityPlayer
++    public ServerPlayer getPlayerForLogin(GameProfile gameProfile, ClientInformation clientInformation, ServerPlayer player) {
++        player.updateOptions(clientInformation);
++        return player;
++        // CraftBukkit end
++    }
++
++    public boolean disconnectAllPlayersWithProfile(GameProfile gameProfile, ServerPlayer player) { // CraftBukkit - added ServerPlayer
++        // CraftBukkit start - Moved up
++        // UUID id = gameProfile.getId();
++        // Set<ServerPlayer> set = Sets.newIdentityHashSet();
++        //
++        // for (ServerPlayer serverPlayer : this.players) {
++        //     if (serverPlayer.getUUID().equals(id)) {
++        //         set.add(serverPlayer);
++        //     }
++        // }
++        //
++        // ServerPlayer serverPlayer1 = this.playersByUUID.get(gameProfile.getId());
++        // if (serverPlayer1 != null) {
++        //     set.add(serverPlayer1);
++        // }
++        //
++        // for (ServerPlayer serverPlayer2 : set) {
++        //     serverPlayer2.connection.disconnect(DUPLICATE_LOGIN_DISCONNECT_MESSAGE);
++        // }
++        //
++        // return !set.isEmpty();
++        return player == null;
++        // CraftBukkit end
++    }
++
++    // CraftBukkit start
++    public ServerPlayer respawn(ServerPlayer player, boolean keepInventory, Entity.RemovalReason reason, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason eventReason) {
++        return this.respawn(player, keepInventory, reason, eventReason, null);
++    }
++    public ServerPlayer respawn(ServerPlayer player, boolean keepInventory, Entity.RemovalReason reason, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason eventReason, org.bukkit.Location location) {
++        player.stopRiding(); // CraftBukkit
+         this.players.remove(player);
++        this.playersByName.remove(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot
+         player.serverLevel().removePlayerImmediately(player, reason);
+-        TeleportTransition teleportTransition = player.findRespawnPositionAndUseSpawnBlock(!keepInventory, TeleportTransition.DO_NOTHING);
+-        ServerLevel level = teleportTransition.newLevel();
+-        ServerPlayer serverPlayer = new ServerPlayer(this.server, level, player.getGameProfile(), player.clientInformation());
++        // TeleportTransition teleportTransition = player.findRespawnPositionAndUseSpawnBlock(!keepInventory, TeleportTransition.DO_NOTHING);
++        // ServerLevel level = teleportTransition.newLevel();
++        // ServerPlayer serverPlayer = new ServerPlayer(this.server, level, player.getGameProfile(), player.clientInformation());
++        ServerPlayer serverPlayer = player;
++        Level fromWorld = player.level();
++        player.wonGame = false;
++        // CraftBukkit end
+         serverPlayer.connection = player.connection;
+         serverPlayer.restoreFrom(player, keepInventory);
+         serverPlayer.setId(player.getId());
+         serverPlayer.setMainArm(player.getMainArm());
+-        if (!teleportTransition.missingRespawnBlock()) {
+-            serverPlayer.copyRespawnPosition(player);
+-        }
++        // CraftBukkit - not required, just copies old location into reused entity
++        // if (!teleportTransition.missingRespawnBlock()) {
++        //     serverPlayer.copyRespawnPosition(player);
++        // }
+ 
+         for (String string : player.getTags()) {
+             serverPlayer.addTag(string);
+         }
+-
++        // Paper start - Add PlayerPostRespawnEvent
++        boolean isBedSpawn = false;
++        boolean isRespawn = false;
++        // Paper end - Add PlayerPostRespawnEvent
++
++        // CraftBukkit start - fire PlayerRespawnEvent
++        TeleportTransition teleportTransition;
++        if (location == null) {
++            teleportTransition = player.findRespawnPositionAndUseSpawnBlock(!keepInventory, TeleportTransition.DO_NOTHING, eventReason);
++
++            if (!keepInventory) player.reset(); // SPIGOT-4785
++           // Paper start - Add PlayerPostRespawnEvent
++           if (teleportTransition == null) return player; // Early exit, mirrors belows early return for disconnected players in respawn event
++           isRespawn = true;
++           location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(teleportTransition.position(), teleportTransition.newLevel().getWorld(), teleportTransition.yRot(), teleportTransition.xRot());
++           // Paper end - Add PlayerPostRespawnEvent
++        } else {
++            teleportTransition = new TeleportTransition(((org.bukkit.craftbukkit.CraftWorld) location.getWorld()).getHandle(), org.bukkit.craftbukkit.util.CraftLocation.toVec3D(location), Vec3.ZERO, location.getYaw(), location.getPitch(), TeleportTransition.DO_NOTHING);
++        }
++        // Spigot start
++        if (teleportTransition == null) { // Paper - Add PlayerPostRespawnEvent - diff on change - spigot early returns if respawn pos is null, that is how they handle disconnected player in respawn event
++            return player;
++        }
++        // Spigot end
++        ServerLevel level = teleportTransition.newLevel();
++        serverPlayer.spawnIn(level);
++        serverPlayer.unsetRemoved();
++        serverPlayer.setShiftKeyDown(false);
+         Vec3 vec3 = teleportTransition.position();
+-        serverPlayer.moveTo(vec3.x, vec3.y, vec3.z, teleportTransition.yRot(), teleportTransition.xRot());
++        serverPlayer.forceSetPositionRotation(vec3.x, vec3.y, vec3.z, teleportTransition.yRot(), teleportTransition.xRot());
++        level.getChunkSource().addRegionTicket(net.minecraft.server.level.TicketType.POST_TELEPORT, new net.minecraft.world.level.ChunkPos(net.minecraft.util.Mth.floor(vec3.x()) >> 4, net.minecraft.util.Mth.floor(vec3.z()) >> 4), 1, player.getId()); // Paper - post teleport ticket type
++        // CraftBukkit end
+         if (teleportTransition.missingRespawnBlock()) {
+             serverPlayer.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.NO_RESPAWN_BLOCK_AVAILABLE, 0.0F));
++            serverPlayer.setRespawnPosition(null, null, 0f, false, false, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.PLAYER_RESPAWN); // CraftBukkit - SPIGOT-5988: Clear respawn location when obstructed // Paper - PlayerSetSpawnEvent
+         }
+ 
+         byte b = (byte)(keepInventory ? 1 : 0);
+         ServerLevel serverLevel = serverPlayer.serverLevel();
+         LevelData levelData = serverLevel.getLevelData();
+         serverPlayer.connection.send(new ClientboundRespawnPacket(serverPlayer.createCommonSpawnInfo(serverLevel), b));
+-        serverPlayer.connection.teleport(serverPlayer.getX(), serverPlayer.getY(), serverPlayer.getZ(), serverPlayer.getYRot(), serverPlayer.getXRot());
++        // serverPlayer.connection.teleport(serverPlayer.getX(), serverPlayer.getY(), serverPlayer.getZ(), serverPlayer.getYRot(), serverPlayer.getXRot());
++        serverPlayer.connection.send(new ClientboundSetChunkCacheRadiusPacket(serverLevel.spigotConfig.viewDistance)); // Spigot
++        serverPlayer.connection.send(new ClientboundSetSimulationDistancePacket(serverLevel.spigotConfig.simulationDistance)); // Spigot
++        serverPlayer.connection.teleport(org.bukkit.craftbukkit.util.CraftLocation.toBukkit(serverPlayer.position(), serverLevel.getWorld(), serverPlayer.getYRot(), serverPlayer.getXRot())); // CraftBukkit
+         serverPlayer.connection.send(new ClientboundSetDefaultSpawnPositionPacket(level.getSharedSpawnPos(), level.getSharedSpawnAngle()));
+         serverPlayer.connection.send(new ClientboundChangeDifficultyPacket(levelData.getDifficulty(), levelData.isDifficultyLocked()));
+         serverPlayer.connection
+@@ -448,10 +_,13 @@
+         this.sendActivePlayerEffects(serverPlayer);
+         this.sendLevelInfo(serverPlayer, level);
+         this.sendPlayerPermissionLevel(serverPlayer);
+-        level.addRespawnedPlayer(serverPlayer);
+-        this.players.add(serverPlayer);
+-        this.playersByUUID.put(serverPlayer.getUUID(), serverPlayer);
+-        serverPlayer.initInventoryMenu();
++        if (!serverPlayer.connection.isDisconnected()) {
++            level.addRespawnedPlayer(serverPlayer);
++            this.players.add(serverPlayer);
++            this.playersByName.put(serverPlayer.getScoreboardName().toLowerCase(java.util.Locale.ROOT), serverPlayer); // Spigot
++            this.playersByUUID.put(serverPlayer.getUUID(), serverPlayer);
++        }
++        // serverPlayer.initInventoryMenu();
+         serverPlayer.setHealth(serverPlayer.getHealth());
+         BlockPos respawnPosition = serverPlayer.getRespawnPosition();
+         ServerLevel level1 = this.server.getLevel(serverPlayer.getRespawnDimension());
+@@ -472,7 +_,40 @@
+                         )
+                     );
+             }
+-        }
++            // Paper start - Add PlayerPostRespawnEvent
++            if (blockState.is(net.minecraft.tags.BlockTags.BEDS) && !teleportTransition.missingRespawnBlock()) {
++                isBedSpawn = true;
++            }
++            // Paper end - Add PlayerPostRespawnEvent
++        }
++        // Added from changeDimension
++        this.sendAllPlayerInfo(player); // Update health, etc...
++        player.onUpdateAbilities();
++        for (MobEffectInstance mobEffect : player.getActiveEffects()) {
++            player.connection.send(new ClientboundUpdateMobEffectPacket(player.getId(), mobEffect, false)); // blend = false
++        }
++
++        // Fire advancement trigger
++        player.triggerDimensionChangeTriggers(level);
++
++        // Don't fire on respawn
++        if (fromWorld != level) {
++            org.bukkit.event.player.PlayerChangedWorldEvent event = new org.bukkit.event.player.PlayerChangedWorldEvent(player.getBukkitEntity(), fromWorld.getWorld());
++            this.server.server.getPluginManager().callEvent(event);
++        }
++
++        // Save player file again if they were disconnected
++        if (player.connection.isDisconnected()) {
++            this.save(player);
++        }
++
++        // Paper start - Add PlayerPostRespawnEvent
++        if (isRespawn) {
++            cserver.getPluginManager().callEvent(new com.destroystokyo.paper.event.player.PlayerPostRespawnEvent(player.getBukkitEntity(), location, isBedSpawn));
++        }
++        // Paper end - Add PlayerPostRespawnEvent
++
++        // CraftBukkit end
+ 
+         return serverPlayer;
+     }
+@@ -482,24 +_,60 @@
+     }
+ 
+     public void sendActiveEffects(LivingEntity entity, ServerGamePacketListenerImpl connection) {
++        // Paper start - collect packets
++        this.sendActiveEffects(entity, connection::send);
++    }
++    public void sendActiveEffects(LivingEntity entity, java.util.function.Consumer<Packet<? super net.minecraft.network.protocol.game.ClientGamePacketListener>> packetConsumer) {
++        // Paper end - collect packets
+         for (MobEffectInstance mobEffectInstance : entity.getActiveEffects()) {
+-            connection.send(new ClientboundUpdateMobEffectPacket(entity.getId(), mobEffectInstance, false));
++            packetConsumer.accept(new ClientboundUpdateMobEffectPacket(entity.getId(), mobEffectInstance, false)); // Paper - collect packets
+         }
+     }
+ 
+     public void sendPlayerPermissionLevel(ServerPlayer player) {
++    // Paper start - avoid recalculating permissions if possible
++        this.sendPlayerPermissionLevel(player, true);
++    }
++
++    public void sendPlayerPermissionLevel(ServerPlayer player, boolean recalculatePermissions) {
++    // Paper end - avoid recalculating permissions if possible
+         GameProfile gameProfile = player.getGameProfile();
+         int profilePermissions = this.server.getProfilePermissions(gameProfile);
+-        this.sendPlayerPermissionLevel(player, profilePermissions);
++        this.sendPlayerPermissionLevel(player, profilePermissions, recalculatePermissions); // Paper - avoid recalculating permissions if possible
+     }
+ 
+     public void tick() {
+         if (++this.sendAllPlayerInfoIn > 600) {
+-            this.broadcastAll(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY), this.players));
++            // CraftBukkit start
++            for (int i = 0; i < this.players.size(); ++i) {
++                final ServerPlayer target = this.players.get(i);
++
++                target.connection.send(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY), com.google.common.collect.Collections2.filter(this.players, t -> target.getBukkitEntity().canSee(t.getBukkitEntity()))));
++            }
++            // CraftBukkit end
+             this.sendAllPlayerInfoIn = 0;
+         }
+     }
+ 
++    // CraftBukkit start - add a world/entity limited version
++    public void broadcastAll(Packet packet, net.minecraft.world.entity.player.Player entityhuman) {
++        for (int i = 0; i < this.players.size(); ++i) {
++            ServerPlayer entityplayer =  this.players.get(i);
++            if (entityhuman != null && !entityplayer.getBukkitEntity().canSee(entityhuman.getBukkitEntity())) {
++                continue;
++            }
++            ((ServerPlayer) this.players.get(i)).connection.send(packet);
++        }
++    }
++
++    public void broadcastAll(Packet packet, Level world) {
++        for (int i = 0; i < world.players().size(); ++i) {
++            ((ServerPlayer) world.players().get(i)).connection.send(packet);
++        }
++
++    }
++    // CraftBukkit end
++
+     public void broadcastAll(Packet<?> packet) {
+         for (ServerPlayer serverPlayer : this.players) {
+             serverPlayer.connection.send(packet);
+@@ -575,6 +_,11 @@
+     }
+ 
+     private void sendPlayerPermissionLevel(ServerPlayer player, int permLevel) {
++        // Paper start - Add sendOpLevel API
++        this.sendPlayerPermissionLevel(player, permLevel, true);
++    }
++    public void sendPlayerPermissionLevel(ServerPlayer player, int permLevel, boolean recalculatePermissions) {
++        // Paper end - Add sendOpLevel API
+         if (player.connection != null) {
+             byte b;
+             if (permLevel <= 0) {
+@@ -588,11 +_,32 @@
+             player.connection.send(new ClientboundEntityEventPacket(player, b));
+         }
+ 
++        if (recalculatePermissions) { // Paper - Add sendOpLevel API
++        player.getBukkitEntity().recalculatePermissions(); // CraftBukkit
+         this.server.getCommands().sendCommands(player);
++        } // Paper - Add sendOpLevel API
+     }
+ 
+     public boolean isWhiteListed(GameProfile profile) {
+-        return !this.doWhiteList || this.ops.contains(profile) || this.whitelist.contains(profile);
++        // Paper start - ProfileWhitelistVerifyEvent
++        return this.isWhiteListed(profile, null);
++    }
++    public boolean isWhiteListed(GameProfile gameprofile, @Nullable org.bukkit.event.player.PlayerLoginEvent loginEvent) {
++        boolean isOp = this.ops.contains(gameprofile);
++        boolean isWhitelisted = !this.doWhiteList || isOp || this.whitelist.contains(gameprofile);
++        final com.destroystokyo.paper.event.profile.ProfileWhitelistVerifyEvent event;
++
++        final net.kyori.adventure.text.Component configuredMessage = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.whitelistMessage);
++        event = new com.destroystokyo.paper.event.profile.ProfileWhitelistVerifyEvent(com.destroystokyo.paper.profile.CraftPlayerProfile.asBukkitMirror(gameprofile), this.doWhiteList, isWhitelisted, isOp, configuredMessage);
++        event.callEvent();
++        if (!event.isWhitelisted()) {
++            if (loginEvent != null) {
++                loginEvent.disallow(org.bukkit.event.player.PlayerLoginEvent.Result.KICK_WHITELIST, event.kickMessage() == null ? configuredMessage : event.kickMessage());
++            }
++            return false;
++        }
++        return true;
++        // Paper end - ProfileWhitelistVerifyEvent
+     }
+ 
+     public boolean isOp(GameProfile profile) {
+@@ -603,21 +_,17 @@
+ 
+     @Nullable
+     public ServerPlayer getPlayerByName(String username) {
+-        int size = this.players.size();
+-
+-        for (int i = 0; i < size; i++) {
+-            ServerPlayer serverPlayer = this.players.get(i);
+-            if (serverPlayer.getGameProfile().getName().equalsIgnoreCase(username)) {
+-                return serverPlayer;
+-            }
+-        }
+-
+-        return null;
++        return this.playersByName.get(username.toLowerCase(java.util.Locale.ROOT)); // Spigot
+     }
+ 
+     public void broadcast(@Nullable Player except, double x, double y, double z, double radius, ResourceKey<Level> dimension, Packet<?> packet) {
+         for (int i = 0; i < this.players.size(); i++) {
+             ServerPlayer serverPlayer = this.players.get(i);
++            // CraftBukkit start - Test if player receiving packet can see the source of the packet
++            if (except != null && !serverPlayer.getBukkitEntity().canSee(except.getBukkitEntity())) {
++               continue;
++            }
++            // CraftBukkit end
+             if (serverPlayer != except && serverPlayer.level().dimension() == dimension) {
+                 double d = x - serverPlayer.getX();
+                 double d1 = y - serverPlayer.getY();
+@@ -630,9 +_,11 @@
+     }
+ 
+     public void saveAll() {
++        io.papermc.paper.util.MCUtil.ensureMain("Save Players" , () -> { // Paper - Ensure main
+         for (int i = 0; i < this.players.size(); i++) {
+             this.save(this.players.get(i));
+         }
++        return null; }); // Paper - ensure main
+     }
+ 
+     public UserWhiteList getWhiteList() {
+@@ -655,14 +_,18 @@
+     }
+ 
+     public void sendLevelInfo(ServerPlayer player, ServerLevel level) {
+-        WorldBorder worldBorder = this.server.overworld().getWorldBorder();
++        WorldBorder worldBorder = player.level().getWorldBorder(); // CraftBukkit
+         player.connection.send(new ClientboundInitializeBorderPacket(worldBorder));
+         player.connection.send(new ClientboundSetTimePacket(level.getGameTime(), level.getDayTime(), level.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)));
+         player.connection.send(new ClientboundSetDefaultSpawnPositionPacket(level.getSharedSpawnPos(), level.getSharedSpawnAngle()));
+         if (level.isRaining()) {
+-            player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0.0F));
+-            player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, level.getRainLevel(1.0F)));
+-            player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, level.getThunderLevel(1.0F)));
++            // CraftBukkit start - handle player weather
++            // player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0.0F));
++            // player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, level.getRainLevel(1.0F)));
++            // player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, level.getThunderLevel(1.0F)));
++            player.setPlayerWeather(org.bukkit.WeatherType.DOWNFALL, false);
++            player.updateWeather(-level.rainLevel, level.rainLevel, -level.thunderLevel, level.thunderLevel);
++            // CraftBukkit end
+         }
+ 
+         player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.LEVEL_CHUNKS_LOAD_START, 0.0F));
+@@ -671,8 +_,16 @@
+ 
+     public void sendAllPlayerInfo(ServerPlayer player) {
+         player.inventoryMenu.sendAllDataToRemote();
+-        player.resetSentInfo();
++        // entityplayer.resetSentInfo();
++        player.getBukkitEntity().updateScaledHealth(); // CraftBukkit - Update scaled health on respawn and worldchange
++        player.refreshEntityData(player); // CraftBukkkit - SPIGOT-7218: sync metadata
+         player.connection.send(new ClientboundSetHeldSlotPacket(player.getInventory().selected));
++        // CraftBukkit start - from GameRules
++        int i = player.serverLevel().getGameRules().getBoolean(GameRules.RULE_REDUCEDDEBUGINFO) ? 22 : 23;
++        player.connection.send(new ClientboundEntityEventPacket(player, (byte) i));
++        float immediateRespawn = player.serverLevel().getGameRules().getBoolean(GameRules.RULE_DO_IMMEDIATE_RESPAWN) ? 1.0F: 0.0F;
++        player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.IMMEDIATE_RESPAWN, immediateRespawn));
++        // CraftBukkit end
+     }
+ 
+     public int getPlayerCount() {
+@@ -688,6 +_,7 @@
+     }
+ 
+     public void setUsingWhiteList(boolean whitelistEnabled) {
++        new com.destroystokyo.paper.event.server.WhitelistToggleEvent(whitelistEnabled).callEvent(); // Paper - WhitelistToggleEvent
+         this.doWhiteList = whitelistEnabled;
+     }
+ 
+@@ -725,10 +_,35 @@
+     }
+ 
+     public void removeAll() {
+-        for (int i = 0; i < this.players.size(); i++) {
+-            this.players.get(i).connection.disconnect(Component.translatable("multiplayer.disconnect.server_shutdown"));
+-        }
+-    }
++        // Paper start - Extract method to allow for restarting flag
++        this.removeAll(false);
++    }
++
++    public void removeAll(boolean isRestarting) {
++        // Paper end
++        // CraftBukkit start - disconnect safely
++        for (ServerPlayer player : this.players) {
++            if (isRestarting) player.connection.disconnect(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.restartMessage), org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN); else // Paper - kick event cause (cause is never used here)
++            player.connection.disconnect(java.util.Objects.requireNonNullElseGet(this.server.server.shutdownMessage(), net.kyori.adventure.text.Component::empty)); // CraftBukkit - add custom shutdown message // Paper - Adventure
++        }
++        // CraftBukkit end
++
++        // Paper start - Configurable player collision; Remove collideRule team if it exists
++        if (this.collideRuleTeamName != null) {
++            final net.minecraft.world.scores.Scoreboard scoreboard = this.getServer().getLevel(Level.OVERWORLD).getScoreboard();
++            final PlayerTeam team = scoreboard.getPlayersTeam(this.collideRuleTeamName);
++            if (team != null) scoreboard.removePlayerTeam(team);
++        }
++        // Paper end - Configurable player collision
++    }
++
++    // CraftBukkit start
++    public void broadcastMessage(Component[] iChatBaseComponents) {
++        for (Component component : iChatBaseComponents) {
++            this.broadcastSystemMessage(component, false);
++        }
++    }
++    // CraftBukkit end
+ 
+     public void broadcastSystemMessage(Component message, boolean bypassHiddenChat) {
+         this.broadcastSystemMessage(message, serverPlayer -> message, bypassHiddenChat);
+@@ -750,20 +_,39 @@
+     }
+ 
+     public void broadcastChatMessage(PlayerChatMessage message, ServerPlayer sender, ChatType.Bound boundChatType) {
+-        this.broadcastChatMessage(message, sender::shouldFilterMessageTo, sender, boundChatType);
++        // Paper start
++        this.broadcastChatMessage(message, sender, boundChatType, null);
++    }
++    public void broadcastChatMessage(PlayerChatMessage message, ServerPlayer sender, ChatType.Bound boundChatType, @Nullable Function<net.kyori.adventure.audience.Audience, Component> unsignedFunction) {
++        // Paper end
++        this.broadcastChatMessage(message, sender::shouldFilterMessageTo, sender, boundChatType, unsignedFunction); // Paper
+     }
+ 
+     private void broadcastChatMessage(
+         PlayerChatMessage message, Predicate<ServerPlayer> shouldFilterMessageTo, @Nullable ServerPlayer sender, ChatType.Bound boundChatType
+     ) {
++        // Paper start
++        this.broadcastChatMessage(message, shouldFilterMessageTo, sender, boundChatType, null);
++    }
++    public void broadcastChatMessage(PlayerChatMessage message, Predicate<ServerPlayer> shouldFilterMessageTo, @Nullable ServerPlayer sender, ChatType.Bound boundChatType, @Nullable Function<net.kyori.adventure.audience.Audience, Component> unsignedFunction) {
++        // Paper end
+         boolean flag = this.verifyChatTrusted(message);
+-        this.server.logChatMessage(message.decoratedContent(), boundChatType, flag ? null : "Not Secure");
++        this.server.logChatMessage((unsignedFunction == null ? message.decoratedContent() : unsignedFunction.apply(this.server.console)), boundChatType, flag ? null : "Not Secure"); // Paper
+         OutgoingChatMessage outgoingChatMessage = OutgoingChatMessage.create(message);
+         boolean flag1 = false;
+ 
++        Packet<?> disguised = sender != null && unsignedFunction == null ? new net.minecraft.network.protocol.game.ClientboundDisguisedChatPacket(outgoingChatMessage.content(), boundChatType) : null; // Paper - don't send player chat packets from vanished players
+         for (ServerPlayer serverPlayer : this.players) {
+             boolean flag2 = shouldFilterMessageTo.test(serverPlayer);
+-            serverPlayer.sendChatMessage(outgoingChatMessage, flag2, boundChatType);
++            // Paper start - don't send player chat packets from vanished players
++            if (sender != null && !serverPlayer.getBukkitEntity().canSee(sender.getBukkitEntity())) {
++                serverPlayer.connection.send(unsignedFunction != null
++                    ? new net.minecraft.network.protocol.game.ClientboundDisguisedChatPacket(unsignedFunction.apply(serverPlayer.getBukkitEntity()), boundChatType)
++                    : disguised);
++                continue;
++            }
++            serverPlayer.sendChatMessage(outgoingChatMessage, flag2, boundChatType, unsignedFunction == null ? null : unsignedFunction.apply(serverPlayer.getBukkitEntity()));
++            // Paper end
+             flag1 |= flag2 && message.isFullyFiltered();
+         }
+ 
+@@ -772,18 +_,25 @@
+         }
+     }
+ 
+-    private boolean verifyChatTrusted(PlayerChatMessage message) {
++    public boolean verifyChatTrusted(PlayerChatMessage message) { // Paper - private -> public
+         return message.hasSignature() && !message.hasExpiredServer(Instant.now());
+     }
+ 
+-    public ServerStatsCounter getPlayerStats(Player player) {
+-        UUID uuid = player.getUUID();
+-        ServerStatsCounter serverStatsCounter = this.stats.get(uuid);
++    // CraftBukkit start
++    public ServerStatsCounter getPlayerStats(ServerPlayer player) {
++        ServerStatsCounter serverstatisticmanager = player.getStats();
++        return serverstatisticmanager == null ? this.getPlayerStats(player.getUUID(), player.getGameProfile().getName()) : serverstatisticmanager; // Paper - use username and not display name
++    }
++
++    public ServerStatsCounter getPlayerStats(UUID uuid, String displayName) {
++        ServerPlayer player = this.getPlayer(uuid);
++        ServerStatsCounter serverStatsCounter = player == null ? null : player.getStats();
++        // CraftBukkit end
+         if (serverStatsCounter == null) {
+             File file = this.server.getWorldPath(LevelResource.PLAYER_STATS_DIR).toFile();
+             File file1 = new File(file, uuid + ".json");
+             if (!file1.exists()) {
+-                File file2 = new File(file, player.getName().getString() + ".json");
++                File file2 = new File(file, displayName + ".json"); // CraftBukkit
+                 Path path = file2.toPath();
+                 if (FileUtil.isPathNormalized(path) && FileUtil.isPathPortable(path) && path.startsWith(file.getPath()) && file2.isFile()) {
+                     file2.renameTo(file1);
+@@ -791,7 +_,7 @@
+             }
+ 
+             serverStatsCounter = new ServerStatsCounter(this.server, file1);
+-            this.stats.put(uuid, serverStatsCounter);
++            // this.stats.put(uuid, serverStatsCounter); // CraftBukkit
+         }
+ 
+         return serverStatsCounter;
+@@ -799,11 +_,11 @@
+ 
+     public PlayerAdvancements getPlayerAdvancements(ServerPlayer player) {
+         UUID uuid = player.getUUID();
+-        PlayerAdvancements playerAdvancements = this.advancements.get(uuid);
++        PlayerAdvancements playerAdvancements = player.getAdvancements(); // CraftBukkit
+         if (playerAdvancements == null) {
+             Path path = this.server.getWorldPath(LevelResource.PLAYER_ADVANCEMENTS_DIR).resolve(uuid + ".json");
+             playerAdvancements = new PlayerAdvancements(this.server.getFixerUpper(), this, this.server.getAdvancements(), path, player);
+-            this.advancements.put(uuid, playerAdvancements);
++            // this.advancements.put(uuid, playerAdvancements); // CraftBukkit
+         }
+ 
+         playerAdvancements.setPlayer(player);
+@@ -846,11 +_,34 @@
+     }
+ 
+     public void reloadResources() {
+-        for (PlayerAdvancements playerAdvancements : this.advancements.values()) {
+-            playerAdvancements.reload(this.server.getAdvancements());
++        // Paper start - API for updating recipes on clients
++        this.reloadAdvancementData();
++        this.reloadTagData();
++        this.reloadRecipes();
++    }
++    public void reloadAdvancementData() {
++        // Paper end - API for updating recipes on clients
++        // CraftBukkit start
++        // for (PlayerAdvancements playerAdvancements : this.advancements.values()) {
++        //     playerAdvancements.reload(this.server.getAdvancements());
++        // }
++        for (ServerPlayer player : this.players) {
++            player.getAdvancements().reload(this.server.getAdvancements());
++            player.getAdvancements().flushDirty(player); // CraftBukkit - trigger immediate flush of advancements
+         }
++        // CraftBukkit end
+ 
++        // Paper start - API for updating recipes on clients
++    }
++    public void reloadTagData() {
+         this.broadcastAll(new ClientboundUpdateTagsPacket(TagNetworkSerialization.serializeTagsToNetwork(this.registries)));
++        // CraftBukkit start
++        // this.reloadRecipes(); // Paper - do not reload recipes just because tag data was reloaded
++        // Paper end - API for updating recipes on clients
++    }
++
++    public void reloadRecipes() {
++        // CraftBukkit end
+         RecipeManager recipeManager = this.server.getRecipeManager();
+         ClientboundUpdateRecipesPacket clientboundUpdateRecipesPacket = new ClientboundUpdateRecipesPacket(
+             recipeManager.getSynchronizedItemProperties(), recipeManager.getSynchronizedStonecutterRecipes()
diff --git a/paper-server/patches/sources/net/minecraft/server/players/SleepStatus.java.patch b/paper-server/patches/sources/net/minecraft/server/players/SleepStatus.java.patch
new file mode 100644
index 0000000000..feab2a6264
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/players/SleepStatus.java.patch
@@ -0,0 +1,41 @@
+--- a/net/minecraft/server/players/SleepStatus.java
++++ b/net/minecraft/server/players/SleepStatus.java
+@@ -14,8 +_,11 @@
+     }
+ 
+     public boolean areEnoughDeepSleeping(int requiredSleepPercentage, List<ServerPlayer> sleepingPlayers) {
+-        int i = (int)sleepingPlayers.stream().filter(Player::isSleepingLongEnough).count();
+-        return i >= this.sleepersNeeded(requiredSleepPercentage);
++        // CraftBukkit start
++        int i = (int) sleepingPlayers.stream().filter(player -> player.isSleepingLongEnough() || player.fauxSleeping).count();
++        boolean anyDeepSleep = sleepingPlayers.stream().anyMatch(Player::isSleepingLongEnough);
++        return anyDeepSleep && i >= this.sleepersNeeded(requiredSleepPercentage);
++        // CraftBukkit end
+     }
+ 
+     public int sleepersNeeded(int requiredSleepPercentage) {
+@@ -35,16 +_,22 @@
+         int i1 = this.sleepingPlayers;
+         this.activePlayers = 0;
+         this.sleepingPlayers = 0;
++        boolean anySleep = false; // CraftBukkit
+ 
+         for (ServerPlayer serverPlayer : players) {
+             if (!serverPlayer.isSpectator()) {
+                 this.activePlayers++;
+-                if (serverPlayer.isSleeping()) {
++                if (serverPlayer.isSleeping() || serverPlayer.fauxSleeping) { // CraftBukkit
+                     this.sleepingPlayers++;
+                 }
++                // CraftBukkit start
++                if (serverPlayer.isSleeping()) {
++                    anySleep = true;
++                }
++                // CraftBukkit end
+             }
+         }
+ 
+-        return (i1 > 0 || this.sleepingPlayers > 0) && (i != this.activePlayers || i1 != this.sleepingPlayers);
++        return anySleep && (i1 > 0 || this.sleepingPlayers > 0) && (i != this.activePlayers || i1 != this.sleepingPlayers); // CraftBukkit
+     }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/server/players/StoredUserList.java.patch b/paper-server/patches/sources/net/minecraft/server/players/StoredUserList.java.patch
new file mode 100644
index 0000000000..cfe6571868
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/players/StoredUserList.java.patch
@@ -0,0 +1,81 @@
+--- a/net/minecraft/server/players/StoredUserList.java
++++ b/net/minecraft/server/players/StoredUserList.java
+@@ -26,7 +_,7 @@
+     private static final Logger LOGGER = LogUtils.getLogger();
+     private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
+     private final File file;
+-    private final Map<String, V> map = Maps.newHashMap();
++    private final Map<String, V> map = Maps.newConcurrentMap(); // Paper - Use ConcurrentHashMap in JsonList
+ 
+     public StoredUserList(File file) {
+         this.file = file;
+@@ -48,8 +_,11 @@
+ 
+     @Nullable
+     public V get(K obj) {
+-        this.removeExpired();
+-        return this.map.get(this.getKeyForUser(obj));
++        // Paper start - Use ConcurrentHashMap in JsonList
++        return this.map.computeIfPresent(this.getKeyForUser(obj), (k, v) -> {
++            return v.hasExpired() ? null : v;
++        });
++        // Paper end - Use ConcurrentHashMap in JsonList
+     }
+ 
+     public void remove(K user) {
+@@ -71,7 +_,7 @@
+     }
+ 
+     public boolean isEmpty() {
+-        return this.map.size() < 1;
++        return this.map.isEmpty(); // Paper - Use ConcurrentHashMap in JsonList
+     }
+ 
+     protected String getKeyForUser(K obj) {
+@@ -79,21 +_,12 @@
+     }
+ 
+     protected boolean contains(K entry) {
++        this.removeExpired(); // CraftBukkit - SPIGOT-7589: Consistently remove expired entries to mirror .get(...)
+         return this.map.containsKey(this.getKeyForUser(entry));
+     }
+ 
+     private void removeExpired() {
+-        List<K> list = Lists.newArrayList();
+-
+-        for (V storedUserEntry : this.map.values()) {
+-            if (storedUserEntry.hasExpired()) {
+-                list.add(storedUserEntry.getUser());
+-            }
+-        }
+-
+-        for (K object : list) {
+-            this.map.remove(this.getKeyForUser(object));
+-        }
++        this.map.values().removeIf(StoredUserEntry::hasExpired); // Paper - Use ConcurrentHashMap in JsonList
+     }
+ 
+     protected abstract StoredUserEntry<K> createEntry(JsonObject entryData);
+@@ -103,6 +_,7 @@
+     }
+ 
+     public void save() throws IOException {
++        this.removeExpired(); // Paper - remove expired values before saving
+         JsonArray jsonArray = new JsonArray();
+         this.map.values().stream().map(storedEntry -> Util.make(new JsonObject(), storedEntry::serialize)).forEach(jsonArray::add);
+ 
+@@ -127,7 +_,14 @@
+                         this.map.put(this.getKeyForUser(storedUserEntry.getUser()), (V)storedUserEntry);
+                     }
+                 }
++            // Spigot start
++            } catch (com.google.gson.JsonParseException | NullPointerException ex) {
++                File backup = new File(this.file + ".backup");
++                LOGGER.warn("Unable to read file {}, backing it up to {} and creating new copy.", this.file, backup, ex);
++                this.file.renameTo(backup);
++                this.file.delete();
+             }
++            // Spigot end
+         }
+     }
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/players/UserBanListEntry.java.patch b/paper-server/patches/sources/net/minecraft/server/players/UserBanListEntry.java.patch
similarity index 64%
rename from paper-server/patches/unapplied/net/minecraft/server/players/UserBanListEntry.java.patch
rename to paper-server/patches/sources/net/minecraft/server/players/UserBanListEntry.java.patch
index df038411ce..7d07f44004 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/players/UserBanListEntry.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/players/UserBanListEntry.java.patch
@@ -1,11 +1,6 @@
 --- a/net/minecraft/server/players/UserBanListEntry.java
 +++ b/net/minecraft/server/players/UserBanListEntry.java
-@@ -1,3 +1,4 @@
-+// mc-dev import
- package net.minecraft.server.players;
- 
- import com.google.gson.JsonObject;
-@@ -39,20 +40,29 @@
+@@ -37,19 +_,27 @@
  
      @Nullable
      private static GameProfile createGameProfile(JsonObject json) {
@@ -15,28 +10,25 @@
 +        UUID uuid = null;
 +        String name = null;
 +        if (json.has("uuid")) {
-             String s = json.get("uuid").getAsString();
+             String asString = json.get("uuid").getAsString();
  
 -            UUID uuid;
--
              try {
-                 uuid = UUID.fromString(s);
-             } catch (Throwable throwable) {
+                 uuid = UUID.fromString(asString);
+             } catch (Throwable var4) {
 -                return null;
              }
  
 -            return new GameProfile(uuid, json.get("name").getAsString());
 +        }
-+        if ( json.has("name"))
-+        {
++        if (json.has("name")) {
 +            name = json.get("name").getAsString();
 +        }
-+        if ( uuid != null || name != null )
-+        {
-+            return new GameProfile( uuid, name );
++        if (uuid != null || name != null) {
++            return new GameProfile(uuid, name);
          } else {
              return null;
          }
-+        // Spigot End
++        // Spigot end
      }
  }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/players/UserWhiteList.java.patch b/paper-server/patches/sources/net/minecraft/server/players/UserWhiteList.java.patch
similarity index 88%
rename from paper-server/patches/unapplied/net/minecraft/server/players/UserWhiteList.java.patch
rename to paper-server/patches/sources/net/minecraft/server/players/UserWhiteList.java.patch
index 4cef2c2bc4..9f81bd284e 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/players/UserWhiteList.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/players/UserWhiteList.java.patch
@@ -1,8 +1,8 @@
 --- a/net/minecraft/server/players/UserWhiteList.java
 +++ b/net/minecraft/server/players/UserWhiteList.java
-@@ -28,4 +28,23 @@
-     protected String getKeyForUser(GameProfile gameProfile) {
-         return gameProfile.getId().toString();
+@@ -28,4 +_,23 @@
+     protected String getKeyForUser(GameProfile obj) {
+         return obj.getId().toString();
      }
 +    // Paper start - Add whitelist events
 +    @Override
diff --git a/paper-server/patches/sources/net/minecraft/server/rcon/RconConsoleSource.java.patch b/paper-server/patches/sources/net/minecraft/server/rcon/RconConsoleSource.java.patch
new file mode 100644
index 0000000000..7ad11d0409
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/rcon/RconConsoleSource.java.patch
@@ -0,0 +1,35 @@
+--- a/net/minecraft/server/rcon/RconConsoleSource.java
++++ b/net/minecraft/server/rcon/RconConsoleSource.java
+@@ -13,8 +_,13 @@
+     private static final Component RCON_COMPONENT = Component.literal("Rcon");
+     private final StringBuffer buffer = new StringBuffer();
+     private final MinecraftServer server;
++    // CraftBukkit start
++    public final java.net.SocketAddress socketAddress;
++    private final org.bukkit.craftbukkit.command.CraftRemoteConsoleCommandSender remoteConsole = new org.bukkit.craftbukkit.command.CraftRemoteConsoleCommandSender(this);
+ 
+-    public RconConsoleSource(MinecraftServer server) {
++    public RconConsoleSource(MinecraftServer server, java.net.SocketAddress socketAddress) {
++        this.socketAddress = socketAddress;
++        // CraftBukkit end
+         this.server = server;
+     }
+ 
+@@ -32,6 +_,17 @@
+             this, Vec3.atLowerCornerOf(serverLevel.getSharedSpawnPos()), Vec2.ZERO, serverLevel, 4, "Rcon", RCON_COMPONENT, this.server, null
+         );
+     }
++
++    // CraftBukkit start - Send a String
++    public void sendMessage(String message) {
++        this.buffer.append(message);
++    }
++
++    @Override
++    public org.bukkit.command.CommandSender getBukkitSender(CommandSourceStack wrapper) {
++        return this.remoteConsole;
++    }
++    // CraftBukkit end
+ 
+     @Override
+     public void sendSystemMessage(Component component) {
diff --git a/paper-server/patches/unapplied/net/minecraft/server/rcon/thread/QueryThreadGs4.java.patch b/paper-server/patches/sources/net/minecraft/server/rcon/thread/QueryThreadGs4.java.patch
similarity index 94%
rename from paper-server/patches/unapplied/net/minecraft/server/rcon/thread/QueryThreadGs4.java.patch
rename to paper-server/patches/sources/net/minecraft/server/rcon/thread/QueryThreadGs4.java.patch
index 6a8d04320c..d1a66ee01a 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/rcon/thread/QueryThreadGs4.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/rcon/thread/QueryThreadGs4.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/server/rcon/thread/QueryThreadGs4.java
 +++ b/net/minecraft/server/rcon/thread/QueryThreadGs4.java
-@@ -106,13 +106,32 @@
+@@ -106,13 +_,32 @@
                          NetworkDataOutputStream networkDataOutputStream = new NetworkDataOutputStream(1460);
                          networkDataOutputStream.write(0);
-                         networkDataOutputStream.writeBytes(this.getIdentBytes(packet.getSocketAddress()));
+                         networkDataOutputStream.writeBytes(this.getIdentBytes(requestPacket.getSocketAddress()));
 -                        networkDataOutputStream.writeString(this.serverName);
-+
++                        // Paper start
 +                        com.destroystokyo.paper.event.server.GS4QueryEvent.QueryType queryType =
 +                            com.destroystokyo.paper.event.server.GS4QueryEvent.QueryType.BASIC;
 +                        com.destroystokyo.paper.event.server.GS4QueryEvent.QueryResponse queryResponse = com.destroystokyo.paper.event.server.GS4QueryEvent.QueryResponse.builder()
@@ -19,7 +19,7 @@
 +                            .serverVersion(org.bukkit.Bukkit.getServer().getName() + " on " + org.bukkit.Bukkit.getServer().getBukkitVersion())
 +                            .build();
 +                        com.destroystokyo.paper.event.server.GS4QueryEvent queryEvent =
-+                            new com.destroystokyo.paper.event.server.GS4QueryEvent(queryType, packet.getAddress(), queryResponse);
++                            new com.destroystokyo.paper.event.server.GS4QueryEvent(queryType, requestPacket.getAddress(), queryResponse);
 +                        queryEvent.callEvent();
 +                        queryResponse = queryEvent.getResponse();
 +
@@ -36,10 +36,10 @@
 +                        networkDataOutputStream.writeShort((short) queryResponse.getPort());
 +                        networkDataOutputStream.writeString(queryResponse.getHostname());
 +                        // Paper end
-                         this.sendTo(networkDataOutputStream.toByteArray(), packet);
+                         this.sendTo(networkDataOutputStream.toByteArray(), requestPacket);
                          LOGGER.debug("Status [{}]", socketAddress);
                      }
-@@ -147,31 +166,75 @@
+@@ -147,31 +_,75 @@
              this.rulesResponse.writeString("splitnum");
              this.rulesResponse.write(128);
              this.rulesResponse.write(0);
@@ -68,7 +68,7 @@
 +            com.destroystokyo.paper.event.server.GS4QueryEvent.QueryType queryType =
 +                com.destroystokyo.paper.event.server.GS4QueryEvent.QueryType.FULL;
 +            com.destroystokyo.paper.event.server.GS4QueryEvent queryEvent =
-+                new com.destroystokyo.paper.event.server.GS4QueryEvent(queryType, packet.getAddress(), queryResponse);
++                new com.destroystokyo.paper.event.server.GS4QueryEvent(queryType, requestPacket.getAddress(), queryResponse);
 +            queryEvent.callEvent();
 +            queryResponse = queryEvent.getResponse();
              this.rulesResponse.writeString("hostname");
@@ -119,8 +119,8 @@
              this.rulesResponse.write(1);
              this.rulesResponse.writeString("player_");
              this.rulesResponse.write(0);
--            String[] strings = this.serverInterface.getPlayerNames();
-+            String[] strings = queryResponse.getPlayers().toArray(String[]::new);
+-            String[] playerNames = this.serverInterface.getPlayerNames();
++            String[] playerNames = queryResponse.getPlayers().toArray(String[]::new);
  
-             for (String string : strings) {
+             for (String string : playerNames) {
                  this.rulesResponse.writeString(string);
diff --git a/paper-server/patches/sources/net/minecraft/server/rcon/thread/RconClient.java.patch b/paper-server/patches/sources/net/minecraft/server/rcon/thread/RconClient.java.patch
new file mode 100644
index 0000000000..e84fb83f9d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/rcon/thread/RconClient.java.patch
@@ -0,0 +1,36 @@
+--- a/net/minecraft/server/rcon/thread/RconClient.java
++++ b/net/minecraft/server/rcon/thread/RconClient.java
+@@ -23,11 +_,14 @@
+     private final Socket client;
+     private final byte[] buf = new byte[1460];
+     private final String rconPassword;
+-    private final ServerInterface serverInterface;
++    // CraftBukkit start
++    private final net.minecraft.server.dedicated.DedicatedServer serverInterface;
++    private final net.minecraft.server.rcon.RconConsoleSource rconConsoleSource;
++    // CraftBukkit end
+ 
+     RconClient(ServerInterface serverInterface, String rconPassword, Socket client) {
+         super("RCON Client " + client.getInetAddress());
+-        this.serverInterface = serverInterface;
++        this.serverInterface = (net.minecraft.server.dedicated.DedicatedServer) serverInterface; // CraftBukkit
+         this.client = client;
+ 
+         try {
+@@ -37,6 +_,7 @@
+         }
+ 
+         this.rconPassword = rconPassword;
++        this.rconConsoleSource = new net.minecraft.server.rcon.RconConsoleSource(this.serverInterface, client.getRemoteSocketAddress()); // CraftBukkit
+     }
+ 
+     @Override
+@@ -67,7 +_,7 @@
+                                 String string1 = PktUtils.stringFromByteArray(this.buf, i1, i);
+ 
+                                 try {
+-                                    this.sendCmdResponse(i3, this.serverInterface.runCommand(string1));
++                                    this.sendCmdResponse(i3, this.serverInterface.runCommand(this.rconConsoleSource, string1)); // CraftBukkit
+                                 } catch (Exception var15) {
+                                     this.sendCmdResponse(i3, "Error executing: " + string1 + " (" + var15.getMessage() + ")");
+                                 }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/rcon/thread/RconThread.java.patch b/paper-server/patches/sources/net/minecraft/server/rcon/thread/RconThread.java.patch
similarity index 58%
rename from paper-server/patches/unapplied/net/minecraft/server/rcon/thread/RconThread.java.patch
rename to paper-server/patches/sources/net/minecraft/server/rcon/thread/RconThread.java.patch
index 5bc2ff1f90..62a462ca73 100644
--- a/paper-server/patches/unapplied/net/minecraft/server/rcon/thread/RconThread.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/rcon/thread/RconThread.java.patch
@@ -1,15 +1,15 @@
 --- a/net/minecraft/server/rcon/thread/RconThread.java
 +++ b/net/minecraft/server/rcon/thread/RconThread.java
-@@ -57,7 +57,7 @@
+@@ -57,7 +_,7 @@
      @Nullable
-     public static RconThread create(ServerInterface server) {
-         DedicatedServerProperties dedicatedServerProperties = server.getProperties();
--        String string = server.getServerIp();
-+        String string = dedicatedServerProperties.rconIp; // Paper - Configurable rcon ip
-         if (string.isEmpty()) {
-             string = "0.0.0.0";
+     public static RconThread create(ServerInterface serverInterface) {
+         DedicatedServerProperties properties = serverInterface.getProperties();
+-        String serverIp = serverInterface.getServerIp();
++        String serverIp = properties.rconIp; // Paper - Configurable rcon ip
+         if (serverIp.isEmpty()) {
+             serverIp = "0.0.0.0";
          }
-@@ -104,6 +104,14 @@
+@@ -104,6 +_,14 @@
  
          this.clients.clear();
      }
diff --git a/paper-server/patches/sources/net/minecraft/stats/ServerRecipeBook.java.patch b/paper-server/patches/sources/net/minecraft/stats/ServerRecipeBook.java.patch
new file mode 100644
index 0000000000..d289aee489
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/stats/ServerRecipeBook.java.patch
@@ -0,0 +1,29 @@
+--- a/net/minecraft/stats/ServerRecipeBook.java
++++ b/net/minecraft/stats/ServerRecipeBook.java
+@@ -67,7 +_,7 @@
+ 
+         for (RecipeHolder<?> recipeHolder : recipes) {
+             ResourceKey<Recipe<?>> resourceKey = recipeHolder.id();
+-            if (!this.known.contains(resourceKey) && !recipeHolder.value().isSpecial()) {
++            if (!this.known.contains(resourceKey) && !recipeHolder.value().isSpecial() && org.bukkit.craftbukkit.event.CraftEventFactory.handlePlayerRecipeListUpdateEvent(player, resourceKey.location())) { // CraftBukkit
+                 this.add(resourceKey);
+                 this.addHighlight(resourceKey);
+                 this.displayResolver
+@@ -78,7 +_,7 @@
+             }
+         }
+ 
+-        if (!list.isEmpty()) {
++        if (!list.isEmpty() && player.connection != null) { // SPIGOT-4478 during PlayerLoginEvent
+             player.connection.send(new ClientboundRecipeBookAddPacket(list, false));
+         }
+ 
+@@ -96,7 +_,7 @@
+             }
+         }
+ 
+-        if (!list.isEmpty()) {
++        if (!list.isEmpty() && player.connection != null) { // SPIGOT-4478 during PlayerLoginEvent
+             player.connection.send(new ClientboundRecipeBookRemovePacket(list));
+         }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/stats/ServerStatsCounter.java.patch b/paper-server/patches/sources/net/minecraft/stats/ServerStatsCounter.java.patch
new file mode 100644
index 0000000000..4d4c9288fb
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/stats/ServerStatsCounter.java.patch
@@ -0,0 +1,33 @@
+--- a/net/minecraft/stats/ServerStatsCounter.java
++++ b/net/minecraft/stats/ServerStatsCounter.java
+@@ -51,9 +_,21 @@
+                 LOGGER.error("Couldn't parse statistics file {}", file, var5);
+             }
+         }
++        // Paper start - Moved after stat fetching for player state file
++        // Moves the loading after vanilla loading, so it overrides the values.
++        // Disables saving any forced stats, so it stays at the same value (without enabling disableStatSaving)
++        // Fixes stat initialization to not cause a NullPointerException
++        // Spigot start
++        for (Map.Entry<ResourceLocation, Integer> entry : org.spigotmc.SpigotConfig.forcedStats.entrySet()) {
++            Stat<ResourceLocation> wrapper = Stats.CUSTOM.get(java.util.Objects.requireNonNull(BuiltInRegistries.CUSTOM_STAT.getValue(entry.getKey()))); // Paper - ensured by SpigotConfig#stats
++            this.stats.put(wrapper, entry.getValue().intValue());
++        }
++        // Spigot end
++        // Paper end - Moved after stat fetching for player state file
+     }
+ 
+     public void save() {
++        if (org.spigotmc.SpigotConfig.disableStatSaving) return; // Spigot
+         try {
+             FileUtils.writeStringToFile(this.file, this.toJson());
+         } catch (IOException var2) {
+@@ -63,6 +_,8 @@
+ 
+     @Override
+     public void setValue(Player player, Stat<?> stat, int i) {
++        if (org.spigotmc.SpigotConfig.disableStatSaving) return; // Spigot
++        if (stat.getType() == Stats.CUSTOM && stat.getValue() instanceof final ResourceLocation resourceLocation && org.spigotmc.SpigotConfig.forcedStats.get(resourceLocation) != null) return; // Paper - disable saving forced stats
+         super.setValue(player, stat, i);
+         this.dirty.add(stat);
+     }
diff --git a/paper-server/patches/unapplied/net/minecraft/stats/StatsCounter.java.patch b/paper-server/patches/sources/net/minecraft/stats/StatsCounter.java.patch
similarity index 61%
rename from paper-server/patches/unapplied/net/minecraft/stats/StatsCounter.java.patch
rename to paper-server/patches/sources/net/minecraft/stats/StatsCounter.java.patch
index 51c6680579..a1a5baa229 100644
--- a/paper-server/patches/unapplied/net/minecraft/stats/StatsCounter.java.patch
+++ b/paper-server/patches/sources/net/minecraft/stats/StatsCounter.java.patch
@@ -1,15 +1,15 @@
 --- a/net/minecraft/stats/StatsCounter.java
 +++ b/net/minecraft/stats/StatsCounter.java
-@@ -16,6 +16,12 @@
-     public void increment(Player player, Stat<?> stat, int value) {
-         int j = (int) Math.min((long) this.getValue(stat) + (long) value, 2147483647L);
+@@ -14,6 +_,12 @@
  
+     public void increment(Player player, Stat<?> stat, int amount) {
+         int i = (int)Math.min((long)this.getValue(stat) + amount, 2147483647L);
 +        // CraftBukkit start - fire Statistic events
-+        org.bukkit.event.Cancellable cancellable = org.bukkit.craftbukkit.event.CraftEventFactory.handleStatisticsIncrease(player, stat, this.getValue(stat), j);
++        org.bukkit.event.Cancellable cancellable = org.bukkit.craftbukkit.event.CraftEventFactory.handleStatisticsIncrease(player, stat, this.getValue(stat), i);
 +        if (cancellable != null && cancellable.isCancelled()) {
 +            return;
 +        }
 +        // CraftBukkit end
-         this.setValue(player, stat, j);
+         this.setValue(player, stat, i);
      }
  
diff --git a/paper-server/patches/unapplied/net/minecraft/tags/TagLoader.java.patch b/paper-server/patches/sources/net/minecraft/tags/TagLoader.java.patch
similarity index 63%
rename from paper-server/patches/unapplied/net/minecraft/tags/TagLoader.java.patch
rename to paper-server/patches/sources/net/minecraft/tags/TagLoader.java.patch
index 10e1ff5cb3..fc75f3c8b7 100644
--- a/paper-server/patches/unapplied/net/minecraft/tags/TagLoader.java.patch
+++ b/paper-server/patches/sources/net/minecraft/tags/TagLoader.java.patch
@@ -1,56 +1,58 @@
 --- a/net/minecraft/tags/TagLoader.java
 +++ b/net/minecraft/tags/TagLoader.java
-@@ -86,7 +86,10 @@
-         return list.isEmpty() ? Either.right(List.copyOf(sequencedSet)) : Either.left(list);
+@@ -86,7 +_,10 @@
+         return list.isEmpty() ? Either.right(List.copyOf(set)) : Either.left(list);
      }
  
--    public Map<ResourceLocation, List<T>> build(Map<ResourceLocation, List<TagLoader.EntryWithSource>> tags) {
+-    public Map<ResourceLocation, List<T>> build(Map<ResourceLocation, List<TagLoader.EntryWithSource>> builders) {
 +    // Paper start - fire tag registrar events
-+    public Map<ResourceLocation, List<T>> build(Map<ResourceLocation, List<TagLoader.EntryWithSource>> tags, @Nullable io.papermc.paper.tag.TagEventConfig<T, ?> eventConfig) {
-+        tags = io.papermc.paper.tag.PaperTagListenerManager.INSTANCE.firePreFlattenEvent(tags, eventConfig);
-+    // Paper end - fire tag registrar event
++    public Map<ResourceLocation, List<T>> build(Map<ResourceLocation, List<TagLoader.EntryWithSource>> builders, @Nullable io.papermc.paper.tag.TagEventConfig<T, ?> eventConfig) {
++        builders = io.papermc.paper.tag.PaperTagListenerManager.INSTANCE.firePreFlattenEvent(builders, eventConfig);
++        // Paper end
          final Map<ResourceLocation, List<T>> map = new HashMap<>();
          TagEntry.Lookup<T> lookup = new TagEntry.Lookup<T>() {
              @Nullable
-@@ -114,7 +117,7 @@
-                     )
-                     .ifRight(values -> map.put(id, (List<T>)values))
+@@ -114,7 +_,7 @@
+                 )
+                 .ifRight(list -> map.put(path, (List<T>)list))
          );
 -        return map;
 +        return io.papermc.paper.tag.PaperTagListenerManager.INSTANCE.firePostFlattenEvent(map, eventConfig); // Paper - fire tag registrar events
      }
  
-     public static <T> void loadTagsFromNetwork(TagNetworkSerialization.NetworkPayload tags, WritableRegistry<T> registry) {
-@@ -122,28 +125,38 @@
+     public static <T> void loadTagsFromNetwork(TagNetworkSerialization.NetworkPayload payload, WritableRegistry<T> registry) {
+@@ -122,16 +_,27 @@
      }
  
-     public static List<Registry.PendingTags<?>> loadTagsForExistingRegistries(ResourceManager resourceManager, RegistryAccess registryManager) {
-+    // Paper start - tag lifecycle - add cause
-+        return loadTagsForExistingRegistries(resourceManager, registryManager, io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent.Cause.INITIAL);
+     public static List<Registry.PendingTags<?>> loadTagsForExistingRegistries(ResourceManager resourceManager, RegistryAccess registryAccess) {
++        // Paper start - tag lifecycle - add cause
++        return loadTagsForExistingRegistries(resourceManager, registryAccess, io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent.Cause.INITIAL);
 +    }
-+    public static List<Registry.PendingTags<?>> loadTagsForExistingRegistries(ResourceManager resourceManager, RegistryAccess registryManager, io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent.Cause cause) {
-+    // Paper end - tag lifecycle - add cause
-         return registryManager.registries()
--            .map(registry -> loadPendingTags(resourceManager, registry.value()))
-+            .map(registry -> loadPendingTags(resourceManager, registry.value(), cause)) // Paper - tag lifecycle - add cause
++
++    public static List<Registry.PendingTags<?>> loadTagsForExistingRegistries(ResourceManager resourceManager, RegistryAccess registryAccess, io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent.Cause cause) {
++        // Paper end - tag lifecycle - add cause
+         return registryAccess.registries()
+-            .map(registryEntry -> loadPendingTags(resourceManager, registryEntry.value()))
++            .map(registryEntry -> loadPendingTags(resourceManager, registryEntry.value(), cause)) // Paper - tag lifecycle - add cause
              .flatMap(Optional::stream)
              .collect(Collectors.toUnmodifiableList());
      }
  
      public static <T> void loadTagsForRegistry(ResourceManager resourceManager, WritableRegistry<T> registry) {
-+    // Paper start - tag lifecycle - add registrar event cause
++        // Paper start - tag lifecycle - add registrar event cause
 +        loadTagsForRegistry(resourceManager, registry, io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent.Cause.INITIAL);
 +    }
 +    public static <T> void loadTagsForRegistry(ResourceManager resourceManager, WritableRegistry<T> registry, io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent.Cause cause) {
-+    // Paper end - tag lifecycle - add registrar event cause
++        // Paper end - tag lifecycle - add registrar event cause
          ResourceKey<? extends Registry<T>> resourceKey = registry.key();
          TagLoader<Holder<T>> tagLoader = new TagLoader<>(TagLoader.ElementLookup.fromWritableRegistry(registry), Registries.tagsDirPath(resourceKey));
--        tagLoader.build(tagLoader.load(resourceManager)).forEach((id, entries) -> registry.bindTag(TagKey.create(resourceKey, id), (List<Holder<T>>)entries));
-+        tagLoader.build(tagLoader.load(resourceManager), io.papermc.paper.tag.PaperTagListenerManager.INSTANCE.createEventConfig(registry, cause)).forEach((id, entries) -> registry.bindTag(TagKey.create(resourceKey, id), (List<Holder<T>>)entries)); // Paper - tag lifecycle - add registrar event cause
+-        tagLoader.build(tagLoader.load(resourceManager))
++        tagLoader.build(tagLoader.load(resourceManager), io.papermc.paper.tag.PaperTagListenerManager.INSTANCE.createEventConfig(registry, cause)) // Paper - tag lifecycle - add registrar event cause
+             .forEach((resourceLocation, list) -> registry.bindTag(TagKey.create(resourceKey, resourceLocation), (List<Holder<T>>)list));
      }
  
-     private static <T> Map<TagKey<T>, List<Holder<T>>> wrapTags(ResourceKey<? extends Registry<T>> registryRef, Map<ResourceLocation, List<Holder<T>>> tags) {
-         return tags.entrySet().stream().collect(Collectors.toUnmodifiableMap(entry -> TagKey.create(registryRef, entry.getKey()), Entry::getValue));
+@@ -139,12 +_,12 @@
+         return tags.entrySet().stream().collect(Collectors.toUnmodifiableMap(entry -> TagKey.create(registryKey, entry.getKey()), Entry::getValue));
      }
  
 -    private static <T> Optional<Registry.PendingTags<T>> loadPendingTags(ResourceManager resourceManager, Registry<T> registry) {
diff --git a/paper-server/patches/unapplied/net/minecraft/util/SimpleBitStorage.java.patch b/paper-server/patches/sources/net/minecraft/util/SimpleBitStorage.java.patch
similarity index 58%
rename from paper-server/patches/unapplied/net/minecraft/util/SimpleBitStorage.java.patch
rename to paper-server/patches/sources/net/minecraft/util/SimpleBitStorage.java.patch
index d3a669c38d..29822af20e 100644
--- a/paper-server/patches/unapplied/net/minecraft/util/SimpleBitStorage.java.patch
+++ b/paper-server/patches/sources/net/minecraft/util/SimpleBitStorage.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/util/SimpleBitStorage.java
 +++ b/net/minecraft/util/SimpleBitStorage.java
-@@ -204,8 +204,8 @@
+@@ -204,8 +_,8 @@
      private final long mask;
      private final int size;
      private final int valuesPerLong;
@@ -10,28 +10,26 @@
 +    private final int divideAdd; private final long divideAddUnsigned; // Paper - Perf: Optimize SimpleBitStorage
      private final int divideShift;
  
-     public SimpleBitStorage(int elementBits, int size, int[] data) {
-@@ -248,8 +248,8 @@
-         this.mask = (1L << elementBits) - 1L;
-         this.valuesPerLong = (char)(64 / elementBits);
+     public SimpleBitStorage(int bits, int size, int[] data) {
+@@ -248,8 +_,8 @@
+         this.mask = (1L << bits) - 1L;
+         this.valuesPerLong = (char)(64 / bits);
          int i = 3 * (this.valuesPerLong - 1);
 -        this.divideMul = MAGIC[i + 0];
 -        this.divideAdd = MAGIC[i + 1];
 +        this.divideMul = MAGIC[i + 0]; this.divideMulUnsigned = Integer.toUnsignedLong(this.divideMul); // Paper - Perf: Optimize SimpleBitStorage
 +        this.divideAdd = MAGIC[i + 1]; this.divideAddUnsigned = Integer.toUnsignedLong(this.divideAdd); // Paper - Perf: Optimize SimpleBitStorage
          this.divideShift = MAGIC[i + 2];
-         int j = (size + this.valuesPerLong - 1) / this.valuesPerLong;
+         int i1 = (size + this.valuesPerLong - 1) / this.valuesPerLong;
          if (data != null) {
-@@ -264,15 +264,15 @@
+@@ -264,15 +_,11 @@
      }
  
      private int cellIndex(int index) {
 -        long l = Integer.toUnsignedLong(this.divideMul);
--        long m = Integer.toUnsignedLong(this.divideAdd);
--        return (int)((long)index * l + m >> 32 >> this.divideShift);
-+        //long l = Integer.toUnsignedLong(this.divideMul); // Paper - Perf: Optimize SimpleBitStorage
-+        //long m = Integer.toUnsignedLong(this.divideAdd); // Paper - Perf: Optimize SimpleBitStorage
-+        return (int) (index * this.divideMulUnsigned + this.divideAddUnsigned >> 32 >> this.divideShift); // Paper - Perf: Optimize SimpleBitStorage
+-        long l1 = Integer.toUnsignedLong(this.divideAdd);
+-        return (int)(index * l + l1 >> 32 >> this.divideShift);
++        return (int)(index * this.divideMulUnsigned + this.divideAddUnsigned >> 32 >> this.divideShift); // Paper - Perf: Optimize SimpleBitStorage
      }
  
      @Override
@@ -39,12 +37,10 @@
 -        Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index);
 -        Validate.inclusiveBetween(0L, this.mask, (long)value);
 +    public final int getAndSet(int index, int value) { // Paper - Perf: Optimize SimpleBitStorage
-+        //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage
-+        //Validate.inclusiveBetween(0L, this.mask, (long)value); // Paper - Perf: Optimize SimpleBitStorage
          int i = this.cellIndex(index);
          long l = this.data[i];
-         int j = (index - i * this.valuesPerLong) * this.bits;
-@@ -282,9 +282,9 @@
+         int i1 = (index - i * this.valuesPerLong) * this.bits;
+@@ -282,9 +_,7 @@
      }
  
      @Override
@@ -52,19 +48,16 @@
 -        Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index);
 -        Validate.inclusiveBetween(0L, this.mask, (long)value);
 +    public final void set(int index, int value) { // Paper - Perf: Optimize SimpleBitStorage
-+        //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage
-+        //Validate.inclusiveBetween(0L, this.mask, (long)value); // Paper - Perf: Optimize SimpleBitStorage
          int i = this.cellIndex(index);
          long l = this.data[i];
-         int j = (index - i * this.valuesPerLong) * this.bits;
-@@ -292,8 +292,8 @@
+         int i1 = (index - i * this.valuesPerLong) * this.bits;
+@@ -292,8 +_,7 @@
      }
  
      @Override
 -    public int get(int index) {
 -        Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index);
 +    public final int get(int index) { // Paper - Perf: Optimize SimpleBitStorage
-+        //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage
          int i = this.cellIndex(index);
          long l = this.data[i];
-         int j = (index - i * this.valuesPerLong) * this.bits;
+         int i1 = (index - i * this.valuesPerLong) * this.bits;
diff --git a/paper-server/patches/sources/net/minecraft/util/SpawnUtil.java.patch b/paper-server/patches/sources/net/minecraft/util/SpawnUtil.java.patch
new file mode 100644
index 0000000000..89419f5c21
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/util/SpawnUtil.java.patch
@@ -0,0 +1,70 @@
+--- a/net/minecraft/util/SpawnUtil.java
++++ b/net/minecraft/util/SpawnUtil.java
+@@ -16,6 +_,7 @@
+ import net.minecraft.world.level.block.state.BlockState;
+ 
+ public class SpawnUtil {
++
+     public static <T extends Mob> Optional<T> trySpawnMob(
+         EntityType<T> entityType,
+         EntitySpawnReason spawnReason,
+@@ -27,6 +_,24 @@
+         SpawnUtil.Strategy strategy,
+         boolean checkCollision
+     ) {
++        // CraftBukkit start
++        return trySpawnMob(entityType, spawnReason, level, pos, attempts, range, yOffset, strategy, checkCollision, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT, null); // Paper - pre creature spawn event
++    }
++
++    public static <T extends Mob> Optional<T> trySpawnMob(
++        EntityType<T> entityType,
++        EntitySpawnReason spawnReason,
++        ServerLevel level,
++        BlockPos pos,
++        int attempts,
++        int range,
++        int yOffset,
++        SpawnUtil.Strategy strategy,
++        boolean checkCollision,
++        org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason,
++        @javax.annotation.Nullable Runnable onAbort // Paper - pre creature spawn event
++    ) {
++        // CraftBukkit end
+         BlockPos.MutableBlockPos mutableBlockPos = pos.mutable();
+ 
+         for (int i = 0; i < attempts; i++) {
+@@ -39,15 +_,32 @@
+                     !checkCollision
+                         || level.noCollision(entityType.getSpawnAABB(mutableBlockPos.getX() + 0.5, mutableBlockPos.getY(), mutableBlockPos.getZ() + 0.5))
+                 )) {
++                // Paper start - PreCreatureSpawnEvent
++                final com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent event = new com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent(
++                    io.papermc.paper.util.MCUtil.toLocation(level, pos),
++                    org.bukkit.craftbukkit.entity.CraftEntityType.minecraftToBukkit(entityType),
++                    reason
++                );
++                if (!event.callEvent()) {
++                    if (event.shouldAbortSpawn()) {
++                        if (onAbort != null) {
++                            onAbort.run();
++                        }
++                        return Optional.empty();
++                    }
++                    break;
++                }
++                // Paper end - PreCreatureSpawnEvent
+                 T mob = (T)entityType.create(level, null, mutableBlockPos, spawnReason, false, false);
+                 if (mob != null) {
+                     if (mob.checkSpawnRules(level, spawnReason) && mob.checkSpawnObstruction(level)) {
+-                        level.addFreshEntityWithPassengers(mob);
++                        level.addFreshEntityWithPassengers(mob, reason); // CraftBukkit
++                        if (mob.isRemoved()) return Optional.empty(); // CraftBukkit
+                         mob.playAmbientSound();
+                         return Optional.of(mob);
+                     }
+ 
+-                    mob.discard();
++                    mob.discard(null); // CraftBukkit - add Bukkit remove cause
+                 }
+             }
+         }
diff --git a/paper-server/patches/unapplied/net/minecraft/util/StringUtil.java.patch b/paper-server/patches/sources/net/minecraft/util/StringUtil.java.patch
similarity index 75%
rename from paper-server/patches/unapplied/net/minecraft/util/StringUtil.java.patch
rename to paper-server/patches/sources/net/minecraft/util/StringUtil.java.patch
index d0f9b93e86..99b6032596 100644
--- a/paper-server/patches/unapplied/net/minecraft/util/StringUtil.java.patch
+++ b/paper-server/patches/sources/net/minecraft/util/StringUtil.java.patch
@@ -1,7 +1,7 @@
 --- a/net/minecraft/util/StringUtil.java
 +++ b/net/minecraft/util/StringUtil.java
-@@ -67,6 +67,25 @@
-         return name.length() <= 16 && name.chars().filter(c -> c <= 32 || c >= 127).findAny().isEmpty();
+@@ -85,6 +_,25 @@
+         return stringBuilder.toString();
      }
  
 +    // Paper start - Username validation
@@ -23,6 +23,6 @@
 +    }
 +    // Paper end - Username validation
 +
-     public static String filterText(String string) {
-         return filterText(string, false);
+     public static boolean isWhitespace(int character) {
+         return Character.isWhitespace(character) || Character.isSpaceChar(character);
      }
diff --git a/paper-server/patches/unapplied/net/minecraft/util/TickThrottler.java.patch b/paper-server/patches/sources/net/minecraft/util/TickThrottler.java.patch
similarity index 73%
rename from paper-server/patches/unapplied/net/minecraft/util/TickThrottler.java.patch
rename to paper-server/patches/sources/net/minecraft/util/TickThrottler.java.patch
index b829a1224e..b50d74a2be 100644
--- a/paper-server/patches/unapplied/net/minecraft/util/TickThrottler.java.patch
+++ b/paper-server/patches/sources/net/minecraft/util/TickThrottler.java.patch
@@ -1,26 +1,19 @@
 --- a/net/minecraft/util/TickThrottler.java
 +++ b/net/minecraft/util/TickThrottler.java
-@@ -1,10 +1,14 @@
- package net.minecraft.util;
- 
-+// CraftBukkit start
-+import java.util.concurrent.atomic.AtomicInteger;
-+// CraftBukkit end
-+
+@@ -3,7 +_,7 @@
  public class TickThrottler {
- 
      private final int incrementStep;
      private final int threshold;
 -    private int count;
-+    private final AtomicInteger count = new AtomicInteger(); // CraftBukkit - multithreaded field
++    private final java.util.concurrent.atomic.AtomicInteger count = new java.util.concurrent.atomic.AtomicInteger(); // CraftBukkit - multithreaded field
  
-     public TickThrottler(int increment, int threshold) {
-         this.incrementStep = increment;
-@@ -12,17 +16,32 @@
+     public TickThrottler(int incrementStep, int threshold) {
+         this.incrementStep = incrementStep;
+@@ -11,16 +_,31 @@
      }
  
      public void increment() {
--        this.count += this.incrementStep;
+-        this.count = this.count + this.incrementStep;
 +        this.count.addAndGet(this.incrementStep); // CraftBukkit - use thread-safe field access instead
      }
  
@@ -29,18 +22,17 @@
 +        for (int val; (val = this.count.get()) > 0 && !this.count.compareAndSet(val, val - 1); ) ;
 +        /* Use thread-safe field access instead
          if (this.count > 0) {
-             --this.count;
+             this.count--;
          }
 +        */
 +        // CraftBukkit end
- 
      }
  
      public boolean isUnderThreshold() {
 -        return this.count < this.threshold;
 +        // CraftBukkit start - use thread-safe field access instead
 +        return this.count.get() < this.threshold;
-     }
++    }
 +
 +    public boolean isIncrementAndUnderThreshold() {
 +        return this.isIncrementAndUnderThreshold(this.incrementStep, this.threshold);
@@ -49,5 +41,5 @@
 +    public boolean isIncrementAndUnderThreshold(int incrementStep, int threshold) {
 +        return this.count.addAndGet(incrementStep) < threshold;
 +        // CraftBukkit end
-+    }
+     }
  }
diff --git a/paper-server/patches/unapplied/net/minecraft/util/ZeroBitStorage.java.patch b/paper-server/patches/sources/net/minecraft/util/ZeroBitStorage.java.patch
similarity index 98%
rename from paper-server/patches/unapplied/net/minecraft/util/ZeroBitStorage.java.patch
rename to paper-server/patches/sources/net/minecraft/util/ZeroBitStorage.java.patch
index a9e7cdc67b..93069aec37 100644
--- a/paper-server/patches/unapplied/net/minecraft/util/ZeroBitStorage.java.patch
+++ b/paper-server/patches/sources/net/minecraft/util/ZeroBitStorage.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/util/ZeroBitStorage.java
 +++ b/net/minecraft/util/ZeroBitStorage.java
-@@ -13,21 +13,21 @@
+@@ -13,21 +_,21 @@
      }
  
      @Override
diff --git a/paper-server/patches/sources/net/minecraft/util/datafix/DataFixers.java.patch b/paper-server/patches/sources/net/minecraft/util/datafix/DataFixers.java.patch
new file mode 100644
index 0000000000..5c13ab7817
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/util/datafix/DataFixers.java.patch
@@ -0,0 +1,21 @@
+--- a/net/minecraft/util/datafix/DataFixers.java
++++ b/net/minecraft/util/datafix/DataFixers.java
+@@ -505,6 +_,18 @@
+         Schema schema44 = builder.addSchema(1456, SAME_NAMESPACED);
+         builder.addFixer(new EntityItemFrameDirectionFix(schema44, false));
+         Schema schema45 = builder.addSchema(1458, SAME_NAMESPACED);
++        // CraftBukkit start
++        builder.addFixer(new com.mojang.datafixers.DataFix(schema45, false) {
++            @Override
++            protected com.mojang.datafixers.TypeRewriteRule makeRule() {
++                return this.fixTypeEverywhereTyped("Player CustomName", this.getInputSchema().getType(References.PLAYER), (typed) -> {
++                    return typed.update(DSL.remainderFinder(), (dynamic) -> {
++                        return EntityCustomNameToComponentFix.fixTagCustomName(dynamic);
++                    });
++                });
++            }
++        });
++        // CraftBukkit end
+         builder.addFixer(new EntityCustomNameToComponentFix(schema45, false));
+         builder.addFixer(new ItemCustomNameToComponentFix(schema45, false));
+         builder.addFixer(new BlockEntityCustomNameToComponentFix(schema45, false));
diff --git a/paper-server/patches/sources/net/minecraft/util/datafix/fixes/ItemStackMapIdFix.java.patch b/paper-server/patches/sources/net/minecraft/util/datafix/fixes/ItemStackMapIdFix.java.patch
new file mode 100644
index 0000000000..c40c8e02d2
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/util/datafix/fixes/ItemStackMapIdFix.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/util/datafix/fixes/ItemStackMapIdFix.java
++++ b/net/minecraft/util/datafix/fixes/ItemStackMapIdFix.java
+@@ -29,7 +_,7 @@
+                 Dynamic<?> dynamic = typed.get(DSL.remainderFinder());
+                 Typed<?> typed1 = typed.getOrCreateTyped(opticFinder1);
+                 Dynamic<?> dynamic1 = typed1.get(DSL.remainderFinder());
+-                dynamic1 = dynamic1.set("map", dynamic1.createInt(dynamic.get("Damage").asInt(0)));
++                if (dynamic1.getElement("map").result().isEmpty()) dynamic1 = dynamic1.set("map", dynamic1.createInt(dynamic.get("Damage").asInt(0))); // CraftBukkit
+                 return typed.set(opticFinder1, typed1.set(DSL.remainderFinder(), dynamic1));
+             } else {
+                 return typed;
diff --git a/paper-server/patches/sources/net/minecraft/util/datafix/fixes/ItemStackTheFlatteningFix.java.patch b/paper-server/patches/sources/net/minecraft/util/datafix/fixes/ItemStackTheFlatteningFix.java.patch
new file mode 100644
index 0000000000..c20365405c
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/util/datafix/fixes/ItemStackTheFlatteningFix.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/util/datafix/fixes/ItemStackTheFlatteningFix.java
++++ b/net/minecraft/util/datafix/fixes/ItemStackTheFlatteningFix.java
+@@ -423,7 +_,7 @@
+                 if (DAMAGE_IDS.contains(optional.get().getSecond())) {
+                     Typed<?> typed2 = typed.getOrCreateTyped(opticFinder1);
+                     Dynamic<?> dynamic1 = typed2.get(DSL.remainderFinder());
+-                    dynamic1 = dynamic1.set("Damage", dynamic1.createInt(_int));
++                    if (_int != 0) dynamic1 = dynamic1.set("Damage", dynamic1.createInt(_int)); // CraftBukkit
+                     typed1 = typed1.set(opticFinder1, typed2.set(DSL.remainderFinder(), dynamic1));
+                 }
+ 
diff --git a/paper-server/patches/unapplied/net/minecraft/util/thread/BlockableEventLoop.java.patch b/paper-server/patches/sources/net/minecraft/util/thread/BlockableEventLoop.java.patch
similarity index 82%
rename from paper-server/patches/unapplied/net/minecraft/util/thread/BlockableEventLoop.java.patch
rename to paper-server/patches/sources/net/minecraft/util/thread/BlockableEventLoop.java.patch
index 9692da77b5..186d682197 100644
--- a/paper-server/patches/unapplied/net/minecraft/util/thread/BlockableEventLoop.java.patch
+++ b/paper-server/patches/sources/net/minecraft/util/thread/BlockableEventLoop.java.patch
@@ -1,9 +1,9 @@
 --- a/net/minecraft/util/thread/BlockableEventLoop.java
 +++ b/net/minecraft/util/thread/BlockableEventLoop.java
-@@ -82,6 +82,13 @@
-             runnable.run();
+@@ -83,6 +_,14 @@
          }
      }
+ 
 +    // Paper start
 +    public void scheduleOnMain(Runnable runnable) {
 +        // postToMainThread does not work the same as older versions of mc
@@ -11,6 +11,7 @@
 +        this.schedule(this.wrapRunnable(runnable));
 +    }
 +    // Paper end
- 
++
      @Override
-     public void schedule(R runnable) {
+     public void schedule(R task) {
+         this.pendingRunnables.add(task);
diff --git a/paper-server/patches/sources/net/minecraft/util/worldupdate/WorldUpgrader.java.patch b/paper-server/patches/sources/net/minecraft/util/worldupdate/WorldUpgrader.java.patch
new file mode 100644
index 0000000000..ac9d8ccaca
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/util/worldupdate/WorldUpgrader.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/util/worldupdate/WorldUpgrader.java
++++ b/net/minecraft/util/worldupdate/WorldUpgrader.java
+@@ -79,7 +_,7 @@
+         LevelStorageSource.LevelStorageAccess levelStorage, DataFixer dataFixer, RegistryAccess registryAccess, boolean eraseCache, boolean recreateRegionFiles
+     ) {
+         this.dimensions = registryAccess.lookupOrThrow(Registries.LEVEL_STEM);
+-        this.levels = this.dimensions.registryKeySet().stream().map(Registries::levelStemToLevel).collect(Collectors.toUnmodifiableSet());
++        this.levels = java.util.stream.Stream.of(levelStorage.dimensionType).map(Registries::levelStemToLevel).collect(Collectors.toUnmodifiableSet()); // CraftBukkit
+         this.eraseCache = eraseCache;
+         this.dataFixer = dataFixer;
+         this.levelStorage = levelStorage;
+@@ -358,7 +_,7 @@
+                 int version = ChunkStorage.getVersion(compoundTag);
+                 ChunkGenerator chunkGenerator = WorldUpgrader.this.dimensions.getValueOrThrow(Registries.levelToLevelStem(dimension)).generator();
+                 CompoundTag compoundTag1 = chunkStorage.upgradeChunkTag(
+-                    dimension, () -> WorldUpgrader.this.overworldDataStorage, compoundTag, chunkGenerator.getTypeNameForDataFixer()
++                    Registries.levelToLevelStem(dimension), () -> WorldUpgrader.this.overworldDataStorage, compoundTag, chunkGenerator.getTypeNameForDataFixer(), chunkPos, null // CraftBukkit
+                 );
+                 ChunkPos chunkPos1 = new ChunkPos(compoundTag1.getInt("xPos"), compoundTag1.getInt("zPos"));
+                 if (!chunkPos1.equals(chunkPos)) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/BossEvent.java.patch b/paper-server/patches/sources/net/minecraft/world/BossEvent.java.patch
similarity index 77%
rename from paper-server/patches/unapplied/net/minecraft/world/BossEvent.java.patch
rename to paper-server/patches/sources/net/minecraft/world/BossEvent.java.patch
index 3210ddc320..ae696def49 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/BossEvent.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/BossEvent.java.patch
@@ -1,14 +1,14 @@
 --- a/net/minecraft/world/BossEvent.java
 +++ b/net/minecraft/world/BossEvent.java
-@@ -13,6 +13,7 @@
+@@ -13,6 +_,7 @@
      protected boolean darkenScreen;
      protected boolean playBossMusic;
      protected boolean createWorldFog;
 +    public net.kyori.adventure.bossbar.BossBar adventure; // Paper
  
-     public BossEvent(UUID uuid, Component name, BossEvent.BossBarColor color, BossEvent.BossBarOverlay style) {
-         this.id = uuid;
-@@ -27,61 +28,75 @@
+     public BossEvent(UUID id, Component name, BossEvent.BossBarColor color, BossEvent.BossBarOverlay overlay) {
+         this.id = id;
+@@ -27,61 +_,75 @@
      }
  
      public Component getName() {
@@ -26,9 +26,9 @@
          return this.progress;
      }
  
-     public void setProgress(float percent) {
-+        if (this.adventure != null) this.adventure.progress(percent); // Paper
-         this.progress = percent;
+     public void setProgress(float progress) {
++        if (this.adventure != null) this.adventure.progress(progress); // Paper
+         this.progress = progress;
      }
  
      public BossEvent.BossBarColor getColor() {
@@ -46,9 +46,9 @@
          return this.overlay;
      }
  
-     public void setOverlay(BossEvent.BossBarOverlay style) {
-+        if (this.adventure != null) this.adventure.overlay(io.papermc.paper.adventure.PaperAdventure.asAdventure(style)); // Paper
-         this.overlay = style;
+     public void setOverlay(BossEvent.BossBarOverlay overlay) {
++        if (this.adventure != null) this.adventure.overlay(io.papermc.paper.adventure.PaperAdventure.asAdventure(overlay)); // Paper
+         this.overlay = overlay;
      }
  
      public boolean shouldDarkenScreen() {
@@ -67,15 +67,15 @@
          return this.playBossMusic;
      }
  
-     public BossEvent setPlayBossMusic(boolean dragonMusic) {
-+        if (this.adventure != null) io.papermc.paper.adventure.PaperAdventure.setFlag(this.adventure, net.kyori.adventure.bossbar.BossBar.Flag.PLAY_BOSS_MUSIC, dragonMusic); // Paper
-         this.playBossMusic = dragonMusic;
+     public BossEvent setPlayBossMusic(boolean playEndBossMusic) {
++        if (this.adventure != null) io.papermc.paper.adventure.PaperAdventure.setFlag(this.adventure, net.kyori.adventure.bossbar.BossBar.Flag.PLAY_BOSS_MUSIC, playEndBossMusic); // Paper
+         this.playBossMusic = playEndBossMusic;
          return this;
      }
  
-     public BossEvent setCreateWorldFog(boolean thickenFog) {
-+        if (this.adventure != null) io.papermc.paper.adventure.PaperAdventure.setFlag(this.adventure, net.kyori.adventure.bossbar.BossBar.Flag.CREATE_WORLD_FOG, thickenFog); // Paper
-         this.createWorldFog = thickenFog;
+     public BossEvent setCreateWorldFog(boolean createFog) {
++        if (this.adventure != null) io.papermc.paper.adventure.PaperAdventure.setFlag(this.adventure, net.kyori.adventure.bossbar.BossBar.Flag.CREATE_WORLD_FOG, createFog); // Paper
+         this.createWorldFog = createFog;
          return this;
      }
  
diff --git a/paper-server/patches/sources/net/minecraft/world/CompoundContainer.java.patch b/paper-server/patches/sources/net/minecraft/world/CompoundContainer.java.patch
new file mode 100644
index 0000000000..cf4dc60873
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/CompoundContainer.java.patch
@@ -0,0 +1,60 @@
+--- a/net/minecraft/world/CompoundContainer.java
++++ b/net/minecraft/world/CompoundContainer.java
+@@ -7,6 +_,48 @@
+     public final Container container1;
+     public final Container container2;
+ 
++    // CraftBukkit start - add fields and methods
++    public java.util.List<org.bukkit.entity.HumanEntity> transaction = new java.util.ArrayList<>();
++
++    public java.util.List<ItemStack> getContents() {
++        java.util.List<ItemStack> result = new java.util.ArrayList<>(this.getContainerSize());
++        for (int i = 0; i < this.getContainerSize(); i++) {
++            result.add(this.getItem(i));
++        }
++        return result;
++    }
++
++    public void onOpen(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.container1.onOpen(player);
++        this.container2.onOpen(player);
++        this.transaction.add(player);
++    }
++
++    public void onClose(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.container1.onClose(player);
++        this.container2.onClose(player);
++        this.transaction.remove(player);
++    }
++
++    public java.util.List<org.bukkit.entity.HumanEntity> getViewers() {
++        return this.transaction;
++    }
++
++    public org.bukkit.inventory.InventoryHolder getOwner() {
++        return null; // This method won't be called since CraftInventoryDoubleChest doesn't defer to here
++    }
++
++    public void setMaxStackSize(int size) {
++        this.container1.setMaxStackSize(size);
++        this.container2.setMaxStackSize(size);
++    }
++
++    @Override
++    public org.bukkit.Location getLocation() {
++        return this.container1.getLocation(); // TODO: right?
++    }
++    // CraftBukkit end
++
+     public CompoundContainer(Container container1, Container container2) {
+         this.container1 = container1;
+         this.container2 = container2;
+@@ -58,7 +_,7 @@
+ 
+     @Override
+     public int getMaxStackSize() {
+-        return this.container1.getMaxStackSize();
++        return Math.min(this.container1.getMaxStackSize(), this.container2.getMaxStackSize()); // CraftBukkit - check both sides
+     }
+ 
+     @Override
diff --git a/paper-server/patches/sources/net/minecraft/world/Container.java.patch b/paper-server/patches/sources/net/minecraft/world/Container.java.patch
new file mode 100644
index 0000000000..e86d0c9b24
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/Container.java.patch
@@ -0,0 +1,36 @@
+--- a/net/minecraft/world/Container.java
++++ b/net/minecraft/world/Container.java
+@@ -24,9 +_,7 @@
+ 
+     void setItem(int slot, ItemStack stack);
+ 
+-    default int getMaxStackSize() {
+-        return 99;
+-    }
++    int getMaxStackSize(); // CraftBukkit
+ 
+     default int getMaxStackSize(ItemStack stack) {
+         return Math.min(this.getMaxStackSize(), stack.getMaxStackSize());
+@@ -87,4 +_,22 @@
+         BlockPos blockPos = blockEntity.getBlockPos();
+         return level != null && level.getBlockEntity(blockPos) == blockEntity && player.canInteractWithBlock(blockPos, distance);
+     }
++
++    // CraftBukkit start
++    java.util.List<ItemStack> getContents();
++
++    void onOpen(org.bukkit.craftbukkit.entity.CraftHumanEntity player);
++
++    void onClose(org.bukkit.craftbukkit.entity.CraftHumanEntity player);
++
++    java.util.List<org.bukkit.entity.HumanEntity> getViewers();
++
++    org.bukkit.inventory.@org.jetbrains.annotations.Nullable InventoryHolder getOwner();
++
++    void setMaxStackSize(int size);
++
++    org.bukkit.Location getLocation();
++
++    int MAX_STACK = Item.ABSOLUTE_MAX_STACK_SIZE;
++    // CraftBukkit end
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/RandomizableContainer.java.patch b/paper-server/patches/sources/net/minecraft/world/RandomizableContainer.java.patch
similarity index 65%
rename from paper-server/patches/unapplied/net/minecraft/world/RandomizableContainer.java.patch
rename to paper-server/patches/sources/net/minecraft/world/RandomizableContainer.java.patch
index a79a33da44..8467c86645 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/RandomizableContainer.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/RandomizableContainer.java.patch
@@ -1,23 +1,23 @@
 --- a/net/minecraft/world/RandomizableContainer.java
 +++ b/net/minecraft/world/RandomizableContainer.java
-@@ -28,7 +28,7 @@
+@@ -28,7 +_,7 @@
  
      void setLootTable(@Nullable ResourceKey<LootTable> lootTable);
  
--    default void setLootTable(ResourceKey<LootTable> lootTableId, long lootTableSeed) {
-+    default void setLootTable(@Nullable ResourceKey<LootTable> lootTableId, long lootTableSeed) { // Paper - add nullable
-         this.setLootTable(lootTableId);
-         this.setLootTableSeed(lootTableSeed);
+-    default void setLootTable(ResourceKey<LootTable> lootTable, long seed) {
++    default void setLootTable(@Nullable ResourceKey<LootTable> lootTable, long seed) { // Paper - add nullable
+         this.setLootTable(lootTable);
+         this.setLootTableSeed(seed);
      }
-@@ -50,14 +50,15 @@
+@@ -50,14 +_,15 @@
  
-     default boolean tryLoadLootTable(CompoundTag nbt) {
-         if (nbt.contains("LootTable", 8)) {
--            this.setLootTable(ResourceKey.create(Registries.LOOT_TABLE, ResourceLocation.parse(nbt.getString("LootTable"))));
-+            this.setLootTable(net.minecraft.Optionull.map(ResourceLocation.tryParse(nbt.getString("LootTable")), rl -> ResourceKey.create(Registries.LOOT_TABLE, rl))); // Paper - Validate ResourceLocation
-+            if (this.lootableData() != null && this.getLootTable() != null) this.lootableData().loadNbt(nbt); // Paper - LootTable API
-             if (nbt.contains("LootTableSeed", 4)) {
-                 this.setLootTableSeed(nbt.getLong("LootTableSeed"));
+     default boolean tryLoadLootTable(CompoundTag tag) {
+         if (tag.contains("LootTable", 8)) {
+-            this.setLootTable(ResourceKey.create(Registries.LOOT_TABLE, ResourceLocation.parse(tag.getString("LootTable"))));
++            this.setLootTable(net.minecraft.Optionull.map(ResourceLocation.tryParse(tag.getString("LootTable")), rl -> ResourceKey.create(Registries.LOOT_TABLE, rl))); // Paper - Validate ResourceLocation
++            if (this.lootableData() != null && this.getLootTable() != null) this.lootableData().loadNbt(tag); // Paper - LootTable API
+             if (tag.contains("LootTableSeed", 4)) {
+                 this.setLootTableSeed(tag.getLong("LootTableSeed"));
              } else {
                  this.setLootTableSeed(0L);
              }
@@ -27,14 +27,14 @@
          } else {
              return false;
          }
-@@ -69,26 +70,44 @@
+@@ -69,26 +_,42 @@
              return false;
          } else {
-             nbt.putString("LootTable", resourceKey.location().toString());
-+            if (this.lootableData() != null) this.lootableData().saveNbt(nbt); // Paper - LootTable API
-             long l = this.getLootTableSeed();
-             if (l != 0L) {
-                 nbt.putLong("LootTableSeed", l);
+             tag.putString("LootTable", lootTable.location().toString());
++            if (this.lootableData() != null) this.lootableData().saveNbt(tag); // Paper - LootTable API
+             long lootTableSeed = this.getLootTableSeed();
+             if (lootTableSeed != 0L) {
+                 tag.putLong("LootTableSeed", lootTableSeed);
              }
  
 -            return true;
@@ -50,10 +50,10 @@
 +        // Paper end - LootTable API
          Level level = this.getLevel();
          BlockPos blockPos = this.getBlockPos();
-         ResourceKey<LootTable> resourceKey = this.getLootTable();
--        if (resourceKey != null && level != null && level.getServer() != null) {
+         ResourceKey<LootTable> lootTable = this.getLootTable();
+-        if (lootTable != null && level != null && level.getServer() != null) {
 +        // Paper start - LootTable API
-+        lootReplenish: if (resourceKey != null && level != null && level.getServer() != null) {
++        lootReplenish: if (lootTable != null && level != null && level.getServer() != null) {
 +            if (this.lootableData() != null && !this.lootableData().shouldReplenish(this, com.destroystokyo.paper.loottable.PaperLootableInventoryData.CONTAINER, player)) {
 +                if (forceClearLootTable) {
 +                    this.setLootTable(null);
@@ -61,27 +61,25 @@
 +                break lootReplenish;
 +            }
 +            // Paper end - LootTable API
-             LootTable lootTable = level.getServer().reloadableRegistries().getLootTable(resourceKey);
+             LootTable lootTable1 = level.getServer().reloadableRegistries().getLootTable(lootTable);
              if (player instanceof ServerPlayer) {
-                 CriteriaTriggers.GENERATE_LOOT.trigger((ServerPlayer)player, resourceKey);
+                 CriteriaTriggers.GENERATE_LOOT.trigger((ServerPlayer)player, lootTable);
              }
  
--            this.setLootTable(null);
-+            // Paper start - LootTable API
-+            if (forceClearLootTable || this.lootableData() == null || this.lootableData().shouldClearLootTable(this, com.destroystokyo.paper.loottable.PaperLootableInventoryData.CONTAINER, player)) {
-+                this.setLootTable(null);
-+            }
-+            // Paper end - LootTable API
++            if (forceClearLootTable || this.lootableData() == null || this.lootableData().shouldClearLootTable(this, com.destroystokyo.paper.loottable.PaperLootableInventoryData.CONTAINER, player)) { // Paper - LootTable API
+             this.setLootTable(null);
++            } // Paper - LootTable API
              LootParams.Builder builder = new LootParams.Builder((ServerLevel)level).withParameter(LootContextParams.ORIGIN, Vec3.atCenterOf(blockPos));
              if (player != null) {
                  builder.withLuck(player.getLuck()).withParameter(LootContextParams.THIS_ENTITY, player);
-@@ -97,4 +116,16 @@
-             lootTable.fill(this, builder.create(LootContextParamSets.CHEST), this.getLootTableSeed());
+@@ -97,4 +_,17 @@
+             lootTable1.fill(this, builder.create(LootContextParamSets.CHEST), this.getLootTableSeed());
          }
      }
 +
 +    // Paper start - LootTable API
-+    @Nullable @org.jetbrains.annotations.Contract(pure = true)
++    @Nullable
++    @org.jetbrains.annotations.Contract(pure = true)
 +    default com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData() {
 +        return null; // some containers don't really have a "replenish" ability like decorated pots
 +    }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/SimpleContainer.java.patch b/paper-server/patches/sources/net/minecraft/world/SimpleContainer.java.patch
similarity index 66%
rename from paper-server/patches/unapplied/net/minecraft/world/SimpleContainer.java.patch
rename to paper-server/patches/sources/net/minecraft/world/SimpleContainer.java.patch
index ff3587753c..7fe6062581 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/SimpleContainer.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/SimpleContainer.java.patch
@@ -1,24 +1,11 @@
 --- a/net/minecraft/world/SimpleContainer.java
 +++ b/net/minecraft/world/SimpleContainer.java
-@@ -14,18 +14,98 @@
- import net.minecraft.world.item.Item;
- import net.minecraft.world.item.ItemStack;
- 
-+// CraftBukkit start
-+import org.bukkit.Location;
-+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
-+import org.bukkit.entity.HumanEntity;
-+// CraftBukkit end
-+
- public class SimpleContainer implements Container, StackedContentsCompatible {
- 
-     private final int size;
-     public final NonNullList<ItemStack> items;
+@@ -19,7 +_,84 @@
      @Nullable
      private List<ContainerListener> listeners;
-+
-+    // CraftBukkit start - add fields and methods
-+    public List<HumanEntity> transaction = new java.util.ArrayList<HumanEntity>();
+ 
++    // Paper start - add fields and methods
++    public List<org.bukkit.entity.HumanEntity> transaction = new java.util.ArrayList<>();
 +    private int maxStack = MAX_STACK;
 +    protected @Nullable org.bukkit.inventory.InventoryHolder bukkitOwner; // Paper - annotation
 +
@@ -26,15 +13,15 @@
 +        return this.items;
 +    }
 +
-+    public void onOpen(CraftHumanEntity who) {
-+        this.transaction.add(who);
++    public void onOpen(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.add(player);
 +    }
 +
-+    public void onClose(CraftHumanEntity who) {
-+        this.transaction.remove(who);
++    public void onClose(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.remove(player);
 +    }
 +
-+    public List<HumanEntity> getViewers() {
++    public List<org.bukkit.entity.HumanEntity> getViewers() {
 +        return this.transaction;
 +    }
 +
@@ -55,9 +42,9 @@
 +        // Paper end - Add missing InventoryHolders
 +        return this.bukkitOwner;
 +    }
- 
++
 +    @Override
-+    public Location getLocation() {
++    public org.bukkit.Location getLocation() {
 +        // Paper start - Fix inventories returning null Locations
 +        // When the block inventory does not have a tile state that implements getLocation, e. g. composters
 +        if (this.bukkitOwner instanceof org.bukkit.inventory.BlockInventoryHolder blockInventoryHolder) {
@@ -77,27 +64,24 @@
 +            this.items.set(slot, original.items.get(slot).copy());
 +        }
 +    }
++    // Paper end
 +
      public SimpleContainer(int size) {
--        this.size = size;
--        this.items = NonNullList.withSize(size, ItemStack.EMPTY);
 +        this(size, null);
 +    }
++
 +    // Paper start - Add missing InventoryHolders
 +    private @Nullable java.util.function.Supplier<? extends org.bukkit.inventory.InventoryHolder> bukkitOwnerCreator;
++
 +    public SimpleContainer(java.util.function.Supplier<? extends org.bukkit.inventory.InventoryHolder> bukkitOwnerCreator, int size) {
 +        this(size);
 +        this.bukkitOwnerCreator = bukkitOwnerCreator;
-     }
-+    // Paper end - Add missing InventoryHolders
- 
-+    public SimpleContainer(int i, org.bukkit.inventory.InventoryHolder owner) {
-+        this.bukkitOwner = owner;
-+        // CraftBukkit end
-+        this.size = i;
-+        this.items = NonNullList.withSize(i, ItemStack.EMPTY);
 +    }
++    // Paper end - Add missing InventoryHolders
 +
-     public SimpleContainer(ItemStack... items) {
-         this.size = items.length;
-         this.items = NonNullList.of(ItemStack.EMPTY, items);
++    public SimpleContainer(int size, org.bukkit.inventory.InventoryHolder owner) {
++        this.bukkitOwner = owner;
++        // Paper end
+         this.size = size;
+         this.items = NonNullList.withSize(size, ItemStack.EMPTY);
+     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/damagesource/DamageSource.java.patch b/paper-server/patches/sources/net/minecraft/world/damagesource/DamageSource.java.patch
similarity index 97%
rename from paper-server/patches/unapplied/net/minecraft/world/damagesource/DamageSource.java.patch
rename to paper-server/patches/sources/net/minecraft/world/damagesource/DamageSource.java.patch
index 65bbca9531..44ed121c0f 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/damagesource/DamageSource.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/damagesource/DamageSource.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/damagesource/DamageSource.java
 +++ b/net/minecraft/world/damagesource/DamageSource.java
-@@ -21,7 +21,106 @@
+@@ -20,6 +_,105 @@
      private final Entity directEntity;
      @Nullable
      private final Vec3 damageSourcePosition;
@@ -14,7 +14,7 @@
 +    private boolean poison = false;
 +    @Nullable
 +    private Entity customEventDamager = null; // This field is a helper for when causing entity damage is not set by vanilla // Paper - fix DamageSource API
- 
++
 +    public DamageSource sweep() {
 +        this.sweep = true;
 +        return this;
@@ -103,11 +103,10 @@
 +        return damageSource;
 +    }
 +    // CraftBukkit end
-+
+ 
+     @Override
      public String toString() {
-         return "DamageSource (" + this.type().msgId() + ")";
-     }
-@@ -163,4 +262,18 @@
+@@ -134,4 +_,18 @@
      public Holder<DamageType> typeHolder() {
          return this.type;
      }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/damagesource/DamageSources.java.patch b/paper-server/patches/sources/net/minecraft/world/damagesource/DamageSources.java.patch
similarity index 56%
rename from paper-server/patches/unapplied/net/minecraft/world/damagesource/DamageSources.java.patch
rename to paper-server/patches/sources/net/minecraft/world/damagesource/DamageSources.java.patch
index ef590e6374..f13f6629d4 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/damagesource/DamageSources.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/damagesource/DamageSources.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/damagesource/DamageSources.java
 +++ b/net/minecraft/world/damagesource/DamageSources.java
-@@ -43,9 +43,15 @@
+@@ -42,9 +_,15 @@
      private final DamageSource stalagmite;
      private final DamageSource outsideBorder;
      private final DamageSource genericKill;
@@ -8,20 +8,18 @@
 +    private final DamageSource melting;
 +    private final DamageSource poison;
  
-     public DamageSources(RegistryAccess registryManager) {
-         this.damageTypes = registryManager.lookupOrThrow(Registries.DAMAGE_TYPE);
+     public DamageSources(RegistryAccess registry) {
+         this.damageTypes = registry.lookupOrThrow(Registries.DAMAGE_TYPE);
 +        this.melting = this.source(DamageTypes.ON_FIRE).melting();
 +        this.poison = this.source(DamageTypes.MAGIC).poison();
 +        // CraftBukkit end
          this.inFire = this.source(DamageTypes.IN_FIRE);
          this.campfire = this.source(DamageTypes.CAMPFIRE);
          this.lightningBolt = this.source(DamageTypes.LIGHTNING_BOLT);
-@@ -83,7 +89,17 @@
+@@ -84,6 +_,16 @@
+         return new DamageSource(this.damageTypes.getOrThrow(damageTypeKey), causingEntity, directEntity);
+     }
  
-     private DamageSource source(ResourceKey<DamageType> key, @Nullable Entity source, @Nullable Entity attacker) {
-         return new DamageSource(this.damageTypes.getOrThrow(key), source, attacker);
-+    }
-+
 +    // CraftBukkit start
 +    public DamageSource melting() {
 +        return this.melting;
@@ -29,34 +27,24 @@
 +
 +    public DamageSource poison() {
 +        return this.poison;
-     }
++    }
 +    // CraftBukkit end
- 
++
      public DamageSource inFire() {
          return this.inFire;
-@@ -254,7 +270,7 @@
      }
- 
-     public DamageSource explosion(@Nullable Entity source, @Nullable Entity attacker) {
--        return this.source(attacker != null && source != null ? DamageTypes.PLAYER_EXPLOSION : DamageTypes.EXPLOSION, source, attacker);
-+        return this.source(attacker != null && source != null ? DamageTypes.PLAYER_EXPLOSION : DamageTypes.EXPLOSION, source, attacker); // Paper - revert to vanilla
-     }
- 
-     public DamageSource sonicBoom(Entity attacker) {
-@@ -262,9 +278,15 @@
+@@ -261,7 +_,13 @@
      }
  
      public DamageSource badRespawnPointExplosion(Vec3 position) {
 -        return new DamageSource(this.damageTypes.getOrThrow(DamageTypes.BAD_RESPAWN_POINT), position);
 +        // CraftBukkit start
 +        return this.badRespawnPointExplosion(position, null);
-     }
- 
-+    public DamageSource badRespawnPointExplosion(Vec3 vec3d, org.bukkit.block.BlockState blockState) {
-+        return new DamageSource(this.damageTypes.getOrThrow(DamageTypes.BAD_RESPAWN_POINT), vec3d).directBlockState(blockState);
-+        // CraftBukkit end
 +    }
 +
-     public DamageSource outOfBorder() {
-         return this.outsideBorder;
++    public DamageSource badRespawnPointExplosion(Vec3 position, org.bukkit.block.BlockState blockState) {
++        return new DamageSource(this.damageTypes.getOrThrow(DamageTypes.BAD_RESPAWN_POINT), position).directBlockState(blockState);
++        // CraftBukkit end
      }
+ 
+     public DamageSource outOfBorder() {
diff --git a/paper-server/patches/sources/net/minecraft/world/effect/HealOrHarmMobEffect.java.patch b/paper-server/patches/sources/net/minecraft/world/effect/HealOrHarmMobEffect.java.patch
new file mode 100644
index 0000000000..040430b443
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/effect/HealOrHarmMobEffect.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/effect/HealOrHarmMobEffect.java
++++ b/net/minecraft/world/effect/HealOrHarmMobEffect.java
+@@ -16,7 +_,7 @@
+     @Override
+     public boolean applyEffectTick(ServerLevel level, LivingEntity entity, int amplifier) {
+         if (this.isHarm == entity.isInvertedHealAndHarm()) {
+-            entity.heal(Math.max(4 << amplifier, 0));
++            entity.heal(Math.max(4 << amplifier, 0), org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.MAGIC); // CraftBukkit
+         } else {
+             entity.hurtServer(level, entity.damageSources().magic(), 6 << amplifier);
+         }
+@@ -30,7 +_,7 @@
+     ) {
+         if (this.isHarm == entity.isInvertedHealAndHarm()) {
+             int i = (int)(health * (4 << amplifier) + 0.5);
+-            entity.heal(i);
++            entity.heal(i, org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.MAGIC); // CraftBukkit
+         } else {
+             int i = (int)(health * (6 << amplifier) + 0.5);
+             if (source == null) {
diff --git a/paper-server/patches/sources/net/minecraft/world/effect/HungerMobEffect.java.patch b/paper-server/patches/sources/net/minecraft/world/effect/HungerMobEffect.java.patch
new file mode 100644
index 0000000000..0328a5b344
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/effect/HungerMobEffect.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/effect/HungerMobEffect.java
++++ b/net/minecraft/world/effect/HungerMobEffect.java
+@@ -12,7 +_,7 @@
+     @Override
+     public boolean applyEffectTick(ServerLevel level, LivingEntity entity, int amplifier) {
+         if (entity instanceof Player player) {
+-            player.causeFoodExhaustion(0.005F * (amplifier + 1));
++            player.causeFoodExhaustion(0.005F * (amplifier + 1), org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.HUNGER_EFFECT); // CraftBukkit - EntityExhaustionEvent
+         }
+ 
+         return true;
diff --git a/paper-server/patches/sources/net/minecraft/world/effect/InfestedMobEffect.java.patch b/paper-server/patches/sources/net/minecraft/world/effect/InfestedMobEffect.java.patch
new file mode 100644
index 0000000000..05442e7403
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/effect/InfestedMobEffect.java.patch
@@ -0,0 +1,15 @@
+--- a/net/minecraft/world/effect/InfestedMobEffect.java
++++ b/net/minecraft/world/effect/InfestedMobEffect.java
+@@ -44,7 +_,11 @@
+             Vector3f vector3f = entity.getLookAngle().toVector3f().mul(0.3F).mul(1.0F, 1.5F, 1.0F).rotateY(f1);
+             silverfish.moveTo(x, y, z, level.getRandom().nextFloat() * 360.0F, 0.0F);
+             silverfish.setDeltaMovement(new Vec3(vector3f));
+-            level.addFreshEntity(silverfish);
++            // CraftBukkit start
++            if (!level.addFreshEntity(silverfish, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.POTION_EFFECT)) {
++                return;
++            }
++            // CraftBukkit end
+             silverfish.playSound(SoundEvents.SILVERFISH_HURT);
+         }
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/effect/MobEffectUtil.java.patch b/paper-server/patches/sources/net/minecraft/world/effect/MobEffectUtil.java.patch
new file mode 100644
index 0000000000..16be8c5818
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/effect/MobEffectUtil.java.patch
@@ -0,0 +1,48 @@
+--- a/net/minecraft/world/effect/MobEffectUtil.java
++++ b/net/minecraft/world/effect/MobEffectUtil.java
+@@ -47,18 +_,38 @@
+     public static List<ServerPlayer> addEffectToPlayersAround(
+         ServerLevel level, @Nullable Entity source, Vec3 pos, double radius, MobEffectInstance effect, int duration
+     ) {
++        // CraftBukkit start
++        return MobEffectUtil.addEffectToPlayersAround(level, source, pos, radius, effect, duration, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.UNKNOWN);
++    }
++
++    public static List<ServerPlayer> addEffectToPlayersAround(ServerLevel level, @Nullable Entity source, Vec3 pos, double radius, MobEffectInstance effect, int duration, org.bukkit.event.entity.EntityPotionEffectEvent.Cause cause) {
++        // Paper start - Add ElderGuardianAppearanceEvent
++        return addEffectToPlayersAround(level, source, pos, radius, effect, duration, cause, null);
++    }
++
++    public static List<ServerPlayer> addEffectToPlayersAround(ServerLevel level, @Nullable Entity source, Vec3 pos, double radius, MobEffectInstance effect, int duration, org.bukkit.event.entity.EntityPotionEffectEvent.Cause cause, @Nullable java.util.function.Predicate<ServerPlayer> playerPredicate) {
++        // Paper end - Add ElderGuardianAppearanceEvent
++        // CraftBukkit end
+         Holder<MobEffect> effect1 = effect.getEffect();
+         List<ServerPlayer> players = level.getPlayers(
+-            serverPlayer -> serverPlayer.gameMode.isSurvival()
++            // Paper start - Add ElderGuardianAppearanceEvent
++            serverPlayer -> {
++            final boolean condition = serverPlayer.gameMode.isSurvival()
+                 && (source == null || !source.isAlliedTo(serverPlayer))
+                 && pos.closerThan(serverPlayer.position(), radius)
+                 && (
+-                    !serverPlayer.hasEffect(effect1)
+-                        || serverPlayer.getEffect(effect1).getAmplifier() < effect.getAmplifier()
+-                        || serverPlayer.getEffect(effect1).endsWithin(duration - 1)
+-                )
+-        );
+-        players.forEach(serverPlayer -> serverPlayer.addEffect(new MobEffectInstance(effect), source));
++                !serverPlayer.hasEffect(effect1)
++                    || serverPlayer.getEffect(effect1).getAmplifier() < effect.getAmplifier()
++                    || serverPlayer.getEffect(effect1).endsWithin(duration - 1)
++            );
++            if (condition) {
++                return playerPredicate == null || playerPredicate.test(serverPlayer); // Only test the player AFTER it is true
++            } else {
++                return false;
++            }
++        });
++        // Paper end - Add ElderGuardianAppearanceEvent
++        players.forEach(serverPlayer -> serverPlayer.addEffect(new MobEffectInstance(effect), source, cause)); // CraftBukkit
+         return players;
+     }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/effect/OozingMobEffect.java.patch b/paper-server/patches/sources/net/minecraft/world/effect/OozingMobEffect.java.patch
new file mode 100644
index 0000000000..33ed7473ee
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/effect/OozingMobEffect.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/effect/OozingMobEffect.java
++++ b/net/minecraft/world/effect/OozingMobEffect.java
+@@ -49,7 +_,7 @@
+         if (slime != null) {
+             slime.setSize(2, true);
+             slime.moveTo(x, y, z, level.getRandom().nextFloat() * 360.0F, 0.0F);
+-            level.addFreshEntity(slime);
++            level.addFreshEntity(slime, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.POTION_EFFECT); // CraftBukkit
+         }
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/effect/PoisonMobEffect.java.patch b/paper-server/patches/sources/net/minecraft/world/effect/PoisonMobEffect.java.patch
new file mode 100644
index 0000000000..66061ffa2d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/effect/PoisonMobEffect.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/effect/PoisonMobEffect.java
++++ b/net/minecraft/world/effect/PoisonMobEffect.java
+@@ -13,7 +_,7 @@
+     @Override
+     public boolean applyEffectTick(ServerLevel level, LivingEntity entity, int amplifier) {
+         if (entity.getHealth() > 1.0F) {
+-            entity.hurtServer(level, entity.damageSources().magic(), 1.0F);
++            entity.hurtServer(level, entity.damageSources().poison(), 1.0F); // CraftBukkit - DamageSource.MAGIC -> CraftEventFactory.POISON
+         }
+ 
+         return true;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/effect/RegenerationMobEffect.java.patch b/paper-server/patches/sources/net/minecraft/world/effect/RegenerationMobEffect.java.patch
similarity index 83%
rename from paper-server/patches/unapplied/net/minecraft/world/effect/RegenerationMobEffect.java.patch
rename to paper-server/patches/sources/net/minecraft/world/effect/RegenerationMobEffect.java.patch
index 9a24fc0330..c7b4805bbb 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/effect/RegenerationMobEffect.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/effect/RegenerationMobEffect.java.patch
@@ -1,8 +1,8 @@
 --- a/net/minecraft/world/effect/RegenerationMobEffect.java
 +++ b/net/minecraft/world/effect/RegenerationMobEffect.java
-@@ -12,7 +12,7 @@
+@@ -11,7 +_,7 @@
      @Override
-     public boolean applyEffectTick(ServerLevel world, LivingEntity entity, int amplifier) {
+     public boolean applyEffectTick(ServerLevel level, LivingEntity entity, int amplifier) {
          if (entity.getHealth() < entity.getMaxHealth()) {
 -            entity.heal(1.0F);
 +            entity.heal(1.0F, org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.MAGIC_REGEN); // CraftBukkit
diff --git a/paper-server/patches/sources/net/minecraft/world/effect/SaturationMobEffect.java.patch b/paper-server/patches/sources/net/minecraft/world/effect/SaturationMobEffect.java.patch
new file mode 100644
index 0000000000..d9dff00176
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/effect/SaturationMobEffect.java.patch
@@ -0,0 +1,19 @@
+--- a/net/minecraft/world/effect/SaturationMobEffect.java
++++ b/net/minecraft/world/effect/SaturationMobEffect.java
+@@ -12,7 +_,15 @@
+     @Override
+     public boolean applyEffectTick(ServerLevel level, LivingEntity entity, int amplifier) {
+         if (entity instanceof Player player) {
+-            player.getFoodData().eat(amplifier + 1, 1.0F);
++            // CraftBukkit start
++            int oldFoodLevel = player.getFoodData().foodLevel;
++            org.bukkit.event.entity.FoodLevelChangeEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callFoodLevelChangeEvent(player, amplifier + 1 + oldFoodLevel);
++            if (!event.isCancelled()) {
++                player.getFoodData().eat(event.getFoodLevel() - oldFoodLevel, 1.0F);
++            }
++
++            ((org.bukkit.craftbukkit.entity.CraftPlayer) player.getBukkitEntity()).sendHealthUpdate();
++            // CraftBukkit end
+         }
+ 
+         return true;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/effect/WeavingMobEffect.java.patch b/paper-server/patches/sources/net/minecraft/world/effect/WeavingMobEffect.java.patch
similarity index 56%
rename from paper-server/patches/unapplied/net/minecraft/world/effect/WeavingMobEffect.java.patch
rename to paper-server/patches/sources/net/minecraft/world/effect/WeavingMobEffect.java.patch
index 8beb585d3f..66fd20e36b 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/effect/WeavingMobEffect.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/effect/WeavingMobEffect.java.patch
@@ -1,24 +1,24 @@
 --- a/net/minecraft/world/effect/WeavingMobEffect.java
 +++ b/net/minecraft/world/effect/WeavingMobEffect.java
-@@ -25,11 +25,11 @@
+@@ -25,11 +_,11 @@
      @Override
-     public void onMobRemoved(ServerLevel world, LivingEntity entity, int amplifier, Entity.RemovalReason reason) {
-         if (reason == Entity.RemovalReason.KILLED && (entity instanceof Player || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING))) {
--            this.spawnCobwebsRandomlyAround(world, entity.getRandom(), entity.blockPosition());
-+            this.spawnCobwebsRandomlyAround(world, entity.getRandom(), entity.blockPosition(), entity); // Paper - Fire EntityChangeBlockEvent in more places
+     public void onMobRemoved(ServerLevel level, LivingEntity entity, int amplifier, Entity.RemovalReason reason) {
+         if (reason == Entity.RemovalReason.KILLED && (entity instanceof Player || level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING))) {
+-            this.spawnCobwebsRandomlyAround(level, entity.getRandom(), entity.blockPosition());
++            this.spawnCobwebsRandomlyAround(level, entity.getRandom(), entity.blockPosition(), entity); // Paper - Fire EntityChangeBlockEvent in more places
          }
      }
  
--    private void spawnCobwebsRandomlyAround(ServerLevel world, RandomSource random, BlockPos pos) {
-+    private void spawnCobwebsRandomlyAround(ServerLevel world, RandomSource random, BlockPos pos, LivingEntity entity) { // Paper - Fire EntityChangeBlockEvent in more places
+-    private void spawnCobwebsRandomlyAround(ServerLevel level, RandomSource random, BlockPos pos) {
++    private void spawnCobwebsRandomlyAround(ServerLevel level, RandomSource random, BlockPos pos, LivingEntity entity) { // Paper - Fire EntityChangeBlockEvent in more places
          Set<BlockPos> set = Sets.newHashSet();
          int i = this.maxCobwebs.applyAsInt(random);
  
-@@ -46,6 +46,7 @@
+@@ -46,6 +_,7 @@
          }
  
-         for (BlockPos blockPos3 : set) {
-+            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(entity, blockPos3, Blocks.COBWEB.defaultBlockState())) continue; // Paper - Fire EntityChangeBlockEvent in more places
-             world.setBlock(blockPos3, Blocks.COBWEB.defaultBlockState(), 3);
-             world.levelEvent(3018, blockPos3, 0);
+         for (BlockPos blockPosx : set) {
++            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(entity, blockPosx, Blocks.COBWEB.defaultBlockState())) continue; // Paper - Fire EntityChangeBlockEvent in more places
+             level.setBlock(blockPosx, Blocks.COBWEB.defaultBlockState(), 3);
+             level.levelEvent(3018, blockPosx, 0);
          }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/AgeableMob.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/AgeableMob.java.patch
new file mode 100644
index 0000000000..a0e42a63dd
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/AgeableMob.java.patch
@@ -0,0 +1,43 @@
+--- a/net/minecraft/world/entity/AgeableMob.java
++++ b/net/minecraft/world/entity/AgeableMob.java
+@@ -20,6 +_,7 @@
+     protected int age;
+     protected int forcedAge;
+     protected int forcedAgeTimer;
++    public boolean ageLocked; // CraftBukkit
+ 
+     protected AgeableMob(EntityType<? extends AgeableMob> entityType, Level level) {
+         super(entityType, level);
+@@ -66,6 +_,7 @@
+     }
+ 
+     public void ageUp(int amount, boolean forced) {
++        if (this.ageLocked) return; // Paper - Honor ageLock
+         int age = this.getAge();
+         age += amount * 20;
+         if (age > 0) {
+@@ -104,6 +_,7 @@
+         super.addAdditionalSaveData(compound);
+         compound.putInt("Age", this.getAge());
+         compound.putInt("ForcedAge", this.forcedAge);
++        compound.putBoolean("AgeLocked", this.ageLocked); // CraftBukkit
+     }
+ 
+     @Override
+@@ -111,6 +_,7 @@
+         super.readAdditionalSaveData(compound);
+         this.setAge(compound.getInt("Age"));
+         this.forcedAge = compound.getInt("ForcedAge");
++        this.ageLocked = compound.getBoolean("AgeLocked"); // CraftBukkit
+     }
+ 
+     @Override
+@@ -125,7 +_,7 @@
+     @Override
+     public void aiStep() {
+         super.aiStep();
+-        if (this.level().isClientSide) {
++        if (this.level().isClientSide || this.ageLocked) { // CraftBukkit
+             if (this.forcedAgeTimer > 0) {
+                 if (this.forcedAgeTimer % 4 == 0) {
+                     this.level().addParticle(ParticleTypes.HAPPY_VILLAGER, this.getRandomX(1.0), this.getRandomY() + 0.5, this.getRandomZ(1.0), 0.0, 0.0, 0.0);
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/AreaEffectCloud.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/AreaEffectCloud.java.patch
new file mode 100644
index 0000000000..ae128ef1a7
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/AreaEffectCloud.java.patch
@@ -0,0 +1,81 @@
+--- a/net/minecraft/world/entity/AreaEffectCloud.java
++++ b/net/minecraft/world/entity/AreaEffectCloud.java
+@@ -47,7 +_,7 @@
+     public float radiusOnUse;
+     public float radiusPerTick;
+     @Nullable
+-    private LivingEntity owner;
++    private net.minecraft.world.entity.LivingEntity owner;
+     @Nullable
+     public UUID ownerUUID;
+ 
+@@ -177,7 +_,7 @@
+ 
+     private void serverTick(ServerLevel level) {
+         if (this.tickCount >= this.waitTime + this.duration) {
+-            this.discard();
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+         } else {
+             boolean isWaiting = this.isWaiting();
+             boolean flag = this.tickCount < this.waitTime;
+@@ -190,7 +_,7 @@
+                 if (this.radiusPerTick != 0.0F) {
+                     radius += this.radiusPerTick;
+                     if (radius < 0.5F) {
+-                        this.discard();
++                        this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+                         return;
+                     }
+ 
+@@ -220,6 +_,7 @@
+                         list.addAll(this.potionContents.customEffects());
+                         List<LivingEntity> entitiesOfClass = this.level().getEntitiesOfClass(LivingEntity.class, this.getBoundingBox());
+                         if (!entitiesOfClass.isEmpty()) {
++                            List<org.bukkit.entity.LivingEntity> entities = new java.util.ArrayList<>(); // CraftBukkit
+                             for (LivingEntity livingEntity : entitiesOfClass) {
+                                 if (!this.victims.containsKey(livingEntity)
+                                     && livingEntity.isAffectedByPotions()
+@@ -228,6 +_,17 @@
+                                     double d1 = livingEntity.getZ() - this.getZ();
+                                     double d2 = d * d + d1 * d1;
+                                     if (d2 <= radius * radius) {
++                                        // CraftBukkit start
++                                        entities.add((org.bukkit.entity.LivingEntity) livingEntity.getBukkitEntity());
++                                    }
++                                }
++                            }
++                            org.bukkit.event.entity.AreaEffectCloudApplyEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callAreaEffectCloudApplyEvent(this, entities);
++                            if (!event.isCancelled()) {
++                                for (org.bukkit.entity.LivingEntity entity : event.getAffectedEntities()) {
++                                    if (entity instanceof org.bukkit.craftbukkit.entity.CraftLivingEntity) {
++                                        net.minecraft.world.entity.LivingEntity livingEntity = ((org.bukkit.craftbukkit.entity.CraftLivingEntity) entity).getHandle();
++                                        // CraftBukkit end
+                                         this.victims.put(livingEntity, this.tickCount + this.reapplicationDelay);
+ 
+                                         for (MobEffectInstance mobEffectInstance1 : list) {
+@@ -236,14 +_,14 @@
+                                                     .value()
+                                                     .applyInstantenousEffect(level, this, this.getOwner(), livingEntity, mobEffectInstance1.getAmplifier(), 0.5);
+                                             } else {
+-                                                livingEntity.addEffect(new MobEffectInstance(mobEffectInstance1), this);
++                                                livingEntity.addEffect(new MobEffectInstance(mobEffectInstance1), this, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.AREA_EFFECT_CLOUD); // CraftBukkit
+                                             }
+                                         }
+ 
+                                         if (this.radiusOnUse != 0.0F) {
+                                             radius += this.radiusOnUse;
+                                             if (radius < 0.5F) {
+-                                                this.discard();
++                                                this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+                                                 return;
+                                             }
+ 
+@@ -253,7 +_,7 @@
+                                         if (this.durationOnUse != 0) {
+                                             this.duration = this.duration + this.durationOnUse;
+                                             if (this.duration <= 0) {
+-                                                this.discard();
++                                                this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+                                                 return;
+                                             }
+                                         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ConversionParams.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ConversionParams.java.patch
similarity index 88%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/ConversionParams.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/ConversionParams.java.patch
index 3673cd3667..2be9008a50 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ConversionParams.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ConversionParams.java.patch
@@ -1,8 +1,8 @@
 --- a/net/minecraft/world/entity/ConversionParams.java
 +++ b/net/minecraft/world/entity/ConversionParams.java
-@@ -12,4 +12,11 @@
+@@ -12,4 +_,11 @@
      public interface AfterConversion<T extends Mob> {
-         void finalizeConversion(T convertedEntity);
+         void finalizeConversion(T mob);
      }
 +
 +    // Paper start - entity zap event - allow conversion to be cancelled during finalization
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ConversionType.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ConversionType.java.patch
new file mode 100644
index 0000000000..1bd938bdbe
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ConversionType.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/ConversionType.java
++++ b/net/minecraft/world/entity/ConversionType.java
+@@ -21,7 +_,7 @@
+ 
+                 for (Entity entity : newMob.getPassengers()) {
+                     entity.stopRiding();
+-                    entity.remove(Entity.RemovalReason.DISCARDED);
++                    entity.remove(Entity.RemovalReason.DISCARDED, org.bukkit.event.entity.EntityRemoveEvent.Cause.TRANSFORMATION); // CraftBukkit - add Bukkit remove cause
+                 }
+ 
+                 firstPassenger.startRiding(newMob);
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/Display.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/Display.java.patch
new file mode 100644
index 0000000000..467ae36f2f
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/Display.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/Display.java
++++ b/net/minecraft/world/entity/Display.java
+@@ -213,7 +_,7 @@
+         if (tag.contains("transformation")) {
+             Transformation.EXTENDED_CODEC
+                 .decode(NbtOps.INSTANCE, tag.get("transformation"))
+-                .resultOrPartial(Util.prefix("Display entity", LOGGER::error))
++                .result() // Paper - Hide text display error on spawn
+                 .ifPresent(pair -> this.setTransformation(pair.getFirst()));
+         }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/Entity.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/Entity.java.patch
new file mode 100644
index 0000000000..f1de578de1
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/Entity.java.patch
@@ -0,0 +1,1780 @@
+--- a/net/minecraft/world/entity/Entity.java
++++ b/net/minecraft/world/entity/Entity.java
+@@ -136,6 +_,108 @@
+ import org.slf4j.Logger;
+ 
+ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess, ScoreHolder {
++
++    // CraftBukkit start
++    private static final int CURRENT_LEVEL = 2;
++    public boolean preserveMotion = true; // Paper - Fix Entity Teleportation and cancel velocity if teleported; keep initial motion on first setPositionRotation
++    static boolean isLevelAtLeast(CompoundTag tag, int level) {
++        return tag.contains("Bukkit.updateLevel") && tag.getInt("Bukkit.updateLevel") >= level;
++    }
++
++    // Paper start - Share random for entities to make them more random
++    public static RandomSource SHARED_RANDOM = new RandomRandomSource();
++    private static final class RandomRandomSource extends java.util.Random implements net.minecraft.world.level.levelgen.BitRandomSource {
++        private boolean locked = false;
++
++        @Override
++        public synchronized void setSeed(long seed) {
++            if (locked) {
++                LOGGER.error("Ignoring setSeed on Entity.SHARED_RANDOM", new Throwable());
++            } else {
++                super.setSeed(seed);
++                locked = true;
++            }
++        }
++
++        @Override
++        public RandomSource fork() {
++            return new net.minecraft.world.level.levelgen.LegacyRandomSource(this.nextLong());
++        }
++
++        @Override
++        public net.minecraft.world.level.levelgen.PositionalRandomFactory forkPositional() {
++            return new net.minecraft.world.level.levelgen.LegacyRandomSource.LegacyPositionalRandomFactory(this.nextLong());
++        }
++
++        // these below are added to fix reobf issues that I don't wanna deal with right now
++        @Override
++        public int next(int bits) {
++            return super.next(bits);
++        }
++
++        @Override
++        public int nextInt(int origin, int bound) {
++            return net.minecraft.world.level.levelgen.BitRandomSource.super.nextInt(origin, bound);
++        }
++
++        @Override
++        public long nextLong() {
++            return net.minecraft.world.level.levelgen.BitRandomSource.super.nextLong();
++        }
++
++        @Override
++        public int nextInt() {
++            return net.minecraft.world.level.levelgen.BitRandomSource.super.nextInt();
++        }
++
++        @Override
++        public int nextInt(int bound) {
++            return net.minecraft.world.level.levelgen.BitRandomSource.super.nextInt(bound);
++        }
++
++        @Override
++        public boolean nextBoolean() {
++            return net.minecraft.world.level.levelgen.BitRandomSource.super.nextBoolean();
++        }
++
++        @Override
++        public float nextFloat() {
++            return net.minecraft.world.level.levelgen.BitRandomSource.super.nextFloat();
++        }
++
++        @Override
++        public double nextDouble() {
++            return net.minecraft.world.level.levelgen.BitRandomSource.super.nextDouble();
++        }
++
++        @Override
++        public double nextGaussian() {
++            return super.nextGaussian();
++        }
++    }
++    // Paper end - Share random for entities to make them more random
++    public org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason spawnReason; // Paper - Entity#getEntitySpawnReason
++
++    private org.bukkit.craftbukkit.entity.CraftEntity bukkitEntity;
++
++    public org.bukkit.craftbukkit.entity.CraftEntity getBukkitEntity() {
++        if (this.bukkitEntity == null) {
++            // Paper start - Folia schedulers
++            synchronized (this) {
++                if (this.bukkitEntity == null) {
++                    return this.bukkitEntity = org.bukkit.craftbukkit.entity.CraftEntity.getEntity(this.level.getCraftServer(), this);
++                }
++            }
++            // Paper end - Folia schedulers
++        }
++        return this.bukkitEntity;
++    }
++    // Paper start
++    public org.bukkit.craftbukkit.entity.CraftEntity getBukkitEntityRaw() {
++        return this.bukkitEntity;
++    }
++    // Paper end
++
+     private static final Logger LOGGER = LogUtils.getLogger();
+     public static final String ID_TAG = "id";
+     public static final String PASSENGERS_TAG = "Passengers";
+@@ -196,7 +_,7 @@
+     public double zOld;
+     public boolean noPhysics;
+     private boolean wasOnFire;
+-    public final RandomSource random = RandomSource.create();
++    public final RandomSource random = SHARED_RANDOM; // Paper - Share random for entities to make them more random
+     public int tickCount;
+     private int remainingFireTicks = -this.getFireImmuneTicks();
+     public boolean wasTouchingWater;
+@@ -233,7 +_,7 @@
+     protected UUID uuid = Mth.createInsecureUUID(this.random);
+     protected String stringUUID = this.uuid.toString();
+     private boolean hasGlowingTag;
+-    private final Set<String> tags = Sets.newHashSet();
++    private final Set<String> tags = new io.papermc.paper.util.SizeLimitedSet<>(new it.unimi.dsi.fastutil.objects.ObjectOpenHashSet<>(), MAX_ENTITY_TAG_COUNT); // Paper - fully limit tag size - replace set impl
+     private final double[] pistonDeltas = new double[]{0.0, 0.0, 0.0};
+     private long pistonDeltasGameTime;
+     private EntityDimensions dimensions;
+@@ -250,6 +_,63 @@
+     private final List<Entity.Movement> movementThisTick = new ArrayList<>();
+     private final Set<BlockState> blocksInside = new ReferenceArraySet<>();
+     private final LongSet visitedBlocks = new LongOpenHashSet();
++    // CraftBukkit start
++    public boolean forceDrops;
++    public boolean persist = true;
++    public boolean visibleByDefault = true;
++    public boolean valid;
++    public boolean inWorld = false;
++    public boolean generation;
++    public int maxAirTicks = this.getDefaultMaxAirSupply(); // CraftBukkit - SPIGOT-6907: re-implement LivingEntity#setMaximumAir()
++    @Nullable // Paper - Refresh ProjectileSource for projectiles
++    public org.bukkit.projectiles.ProjectileSource projectileSource; // For projectiles only
++    public boolean lastDamageCancelled; // SPIGOT-5339, SPIGOT-6252, SPIGOT-6777: Keep track if the event was canceled
++    public boolean persistentInvisibility = false;
++    public BlockPos lastLavaContact;
++    // Marks an entity, that it was removed by a plugin via Entity#remove
++    // Main use case currently is for SPIGOT-7487, preventing dropping of leash when leash is removed
++    public boolean pluginRemoved = false;
++    protected int numCollisions = 0; // Paper - Cap entity collisions
++    public boolean fromNetherPortal; // Paper - Add option to nerf pigmen from nether portals
++    public boolean spawnedViaMobSpawner; // Paper - Yes this name is similar to above, upstream took the better one
++    // Paper start - Entity origin API
++    @javax.annotation.Nullable
++    private org.bukkit.util.Vector origin;
++    @javax.annotation.Nullable
++    private UUID originWorld;
++    public boolean freezeLocked = false; // Paper - Freeze Tick Lock API
++    public boolean fixedPose = false; // Paper - Expand Pose API
++    private final int despawnTime; // Paper - entity despawn time limit
++    public final io.papermc.paper.entity.activation.ActivationType activationType = io.papermc.paper.entity.activation.ActivationType.activationTypeFor(this); // Paper - EAR 2/tracking ranges
++
++    public void setOrigin(@javax.annotation.Nonnull org.bukkit.Location location) {
++        this.origin = location.toVector();
++        this.originWorld = location.getWorld().getUID();
++    }
++
++    @javax.annotation.Nullable
++    public org.bukkit.util.Vector getOriginVector() {
++        return this.origin != null ? this.origin.clone() : null;
++    }
++
++    @javax.annotation.Nullable
++    public UUID getOriginWorld() {
++        return this.originWorld;
++    }
++    // Paper end - Entity origin API
++    public float getBukkitYaw() {
++        return this.yRot;
++    }
++
++    public boolean isChunkLoaded() {
++        return this.level.hasChunk((int) Math.floor(this.getX()) >> 4, (int) Math.floor(this.getZ()) >> 4);
++    }
++    // CraftBukkit end
++    // Paper start
++    public final AABB getBoundingBoxAt(double x, double y, double z) {
++        return this.dimensions.makeBoundingBox(x, y, z);
++    }
++    // Paper end
+ 
+     public Entity(EntityType<?> entityType, Level level) {
+         this.type = entityType;
+@@ -271,6 +_,7 @@
+         this.entityData = builder.build();
+         this.setPos(0.0, 0.0, 0.0);
+         this.eyeHeight = this.dimensions.eyeHeight();
++        this.despawnTime = type == EntityType.PLAYER ? -1 : level.paperConfig().entities.spawning.despawnTime.getOrDefault(type, io.papermc.paper.configuration.type.number.IntOr.Disabled.DISABLED).or(-1); // Paper - entity despawn time limit
+     }
+ 
+     public boolean isColliding(BlockPos pos, BlockState state) {
+@@ -284,6 +_,12 @@
+         return team != null && team.getColor().getColor() != null ? team.getColor().getColor() : 16777215;
+     }
+ 
++    // CraftBukkit start - SPIGOT-6907: re-implement LivingEntity#setMaximumAir()
++    public int getDefaultMaxAirSupply() {
++        return Entity.TOTAL_AIR_SUPPLY;
++    }
++    // CraftBukkit end
++
+     public boolean isSpectator() {
+         return false;
+     }
+@@ -324,7 +_,7 @@
+     }
+ 
+     public boolean addTag(String tag) {
+-        return this.tags.size() < 1024 && this.tags.add(tag);
++        return this.tags.add(tag); // Paper - fully limit tag size - replace set impl
+     }
+ 
+     public boolean removeTag(String tag) {
+@@ -332,12 +_,18 @@
+     }
+ 
+     public void kill(ServerLevel level) {
+-        this.remove(Entity.RemovalReason.KILLED);
++        this.remove(Entity.RemovalReason.KILLED, org.bukkit.event.entity.EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause
+         this.gameEvent(GameEvent.ENTITY_DIE);
+     }
+ 
+     public final void discard() {
+-        this.remove(Entity.RemovalReason.DISCARDED);
++        // CraftBukkit start - add Bukkit remove cause
++        this.discard(null);
++    }
++
++    public final void discard(org.bukkit.event.entity.EntityRemoveEvent.Cause cause) {
++        this.remove(Entity.RemovalReason.DISCARDED, cause);
++        // CraftBukkit end
+     }
+ 
+     protected abstract void defineSynchedData(SynchedEntityData.Builder builder);
+@@ -346,6 +_,48 @@
+         return this.entityData;
+     }
+ 
++    // CraftBukkit start
++    public void refreshEntityData(ServerPlayer to) {
++        List<SynchedEntityData.DataValue<?>> list = this.entityData.packAll(); // Paper - Update EVERYTHING not just not default
++
++        if (list != null && to.getBukkitEntity().canSee(this.getBukkitEntity())) { // Paper
++            to.connection.send(new net.minecraft.network.protocol.game.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 (player.getBukkitEntity().canSee(this.getBukkitEntity())) {
++            ServerLevel world = (net.minecraft.server.level.ServerLevel)this.level();
++            net.minecraft.server.level.ChunkMap.TrackedEntity tracker = world == null ? null : world.getChunkSource().chunkMap.entityMap.get(this.getId());
++            if (tracker == null) {
++                return;
++            }
++            final net.minecraft.server.level.ServerEntity serverEntity = tracker.serverEntity;
++            final List<net.minecraft.network.protocol.Packet<? super net.minecraft.network.protocol.game.ClientGamePacketListener>> 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<EntityDataAccessor<?>> keys, ServerPlayer to) {
++        if (!to.getBukkitEntity().canSee(this.getBukkitEntity())) {
++            return;
++        }
++
++        final List<SynchedEntityData.DataValue<?>> 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 net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket(this.id, values));
++    }
++    // Paper end
++
+     @Override
+     public boolean equals(Object object) {
+         return object instanceof Entity && ((Entity)object).id == this.id;
+@@ -357,7 +_,13 @@
+     }
+ 
+     public void remove(Entity.RemovalReason reason) {
+-        this.setRemoved(reason);
++        // CraftBukkit start - add Bukkit remove cause
++        this.setRemoved(reason, null);
++    }
++
++    public void remove(Entity.RemovalReason reason, org.bukkit.event.entity.EntityRemoveEvent.Cause eventCause) {
++        this.setRemoved(reason, eventCause);
++        // CraftBukkit end
+     }
+ 
+     public void onClientRemoval() {
+@@ -367,6 +_,17 @@
+     }
+ 
+     public void setPose(Pose pose) {
++        if (this.fixedPose) return; // Paper - Expand Pose API
++        // CraftBukkit start
++        if (pose == this.getPose()) {
++            return;
++        }
++        // Paper start - Don't fire sync event during generation
++        if (!this.generation) {
++            this.level.getCraftServer().getPluginManager().callEvent(new org.bukkit.event.entity.EntityPoseChangeEvent(this.getBukkitEntity(), org.bukkit.entity.Pose.values()[pose.ordinal()]));
++        }
++        // Paper end - Don't fire sync event during generation
++        // CraftBukkit end
+         this.entityData.set(DATA_POSE, pose);
+     }
+ 
+@@ -390,6 +_,32 @@
+     }
+ 
+     public void setRot(float yRot, float xRot) {
++        // CraftBukkit start - yaw was sometimes set to NaN, so we need to set it back to 0
++        if (Float.isNaN(yRot)) {
++            yRot = 0;
++        }
++
++        if (yRot == Float.POSITIVE_INFINITY || yRot == Float.NEGATIVE_INFINITY) {
++            if (this instanceof ServerPlayer) {
++                this.level.getCraftServer().getLogger().warning(this.getScoreboardName() + " was caught trying to crash the server with an invalid yaw");
++                ((org.bukkit.craftbukkit.entity.CraftPlayer) this.getBukkitEntity()).kickPlayer("Infinite yaw (Hacking?)");
++            }
++            yRot = 0;
++        }
++
++        // pitch was sometimes set to NaN, so we need to set it back to 0
++        if (Float.isNaN(xRot)) {
++            xRot = 0;
++        }
++
++        if (xRot == Float.POSITIVE_INFINITY || xRot == Float.NEGATIVE_INFINITY) {
++            if (this instanceof ServerPlayer) {
++                this.level.getCraftServer().getLogger().warning(this.getScoreboardName() + " was caught trying to crash the server with an invalid pitch");
++                ((org.bukkit.craftbukkit.entity.CraftPlayer) this.getBukkitEntity()).kickPlayer("Infinite pitch (Hacking?)");
++            }
++            xRot = 0;
++        }
++        // CraftBukkit end
+         this.setYRot(yRot % 360.0F);
+         this.setXRot(xRot % 360.0F);
+     }
+@@ -399,8 +_,8 @@
+     }
+ 
+     public void setPos(double x, double y, double z) {
+-        this.setPosRaw(x, y, z);
+-        this.setBoundingBox(this.makeBoundingBox());
++        this.setPosRaw(x, y, z, true); // Paper - Block invalid positions and bounding box; force update
++        // this.setBoundingBox(this.makeBoundingBox()); // Paper - Block invalid positions and bounding box; move into setPosRaw
+     }
+ 
+     protected final AABB makeBoundingBox() {
+@@ -430,12 +_,28 @@
+     }
+ 
+     public void tick() {
++        // Paper start - entity despawn time limit
++        if (this.despawnTime >= 0 && this.tickCount >= this.despawnTime) {
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN);
++            return;
++        }
++        // Paper end - entity despawn time limit
+         this.baseTick();
+     }
+ 
++    // CraftBukkit start
++    public void postTick() {
++        // No clean way to break out of ticking once the entity has been copied to a new world, so instead we move the portalling later in the tick cycle
++        if (!(this instanceof ServerPlayer) && this.isAlive()) { // Paper - don't attempt to teleport dead entities
++            this.handlePortal();
++        }
++    }
++    // CraftBukkit end
++
+     public void baseTick() {
+         ProfilerFiller profilerFiller = Profiler.get();
+         profilerFiller.push("entityBaseTick");
++        if (firstTick && this instanceof net.minecraft.world.entity.NeutralMob neutralMob) neutralMob.tickInitialPersistentAnger(level); // Paper - Prevent entity loading causing async lookups
+         this.inBlockState = null;
+         if (this.isPassenger() && this.getVehicle().isRemoved()) {
+             this.stopRiding();
+@@ -445,7 +_,7 @@
+             this.boardingCooldown--;
+         }
+ 
+-        this.handlePortal();
++        if (this instanceof ServerPlayer) this.handlePortal(); // CraftBukkit - // Moved up to postTick
+         if (this.canSpawnSprintParticle()) {
+             this.spawnSprintParticle();
+         }
+@@ -470,7 +_,7 @@
+                     this.setRemainingFireTicks(this.remainingFireTicks - 1);
+                 }
+ 
+-                if (this.getTicksFrozen() > 0) {
++                if (this.getTicksFrozen() > 0 && !this.freezeLocked) { // Paper - Freeze Tick Lock API
+                     this.setTicksFrozen(0);
+                     this.level().levelEvent(null, 1009, this.blockPosition, 1);
+                 }
+@@ -482,6 +_,10 @@
+         if (this.isInLava()) {
+             this.lavaHurt();
+             this.fallDistance *= 0.5F;
++            // CraftBukkit start
++        } else {
++            this.lastLavaContact = null;
++            // CraftBukkit end
+         }
+ 
+         this.checkBelowWorld();
+@@ -502,7 +_,12 @@
+     }
+ 
+     public void checkBelowWorld() {
+-        if (this.getY() < this.level().getMinY() - 64) {
++        if (!this.level.getWorld().isVoidDamageEnabled()) return; // Paper - check if void damage is enabled on the world
++        // Paper start - Configurable nether ceiling damage
++        if (this.getY() < (this.level.getMinY() + this.level.getWorld().getVoidDamageMinBuildHeightOffset()) || (this.level.getWorld().getEnvironment() == org.bukkit.World.Environment.NETHER // Paper - use configured min build height offset
++            && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.test(v -> this.getY() >= v)
++            && (!(this instanceof Player player) || !player.getAbilities().invulnerable))) {
++            // Paper end - Configurable nether ceiling damage
+             this.onBelowWorld();
+         }
+     }
+@@ -531,9 +_,24 @@
+ 
+     public void lavaHurt() {
+         if (!this.fireImmune()) {
+-            this.igniteForSeconds(15.0F);
++            // CraftBukkit start - Fallen in lava TODO: this event spams!
++            if (this instanceof net.minecraft.world.entity.LivingEntity && this.remainingFireTicks <= 0) {
++                // not on fire yet
++                org.bukkit.block.Block damager = (this.lastLavaContact == null) ? null : org.bukkit.craftbukkit.block.CraftBlock.at(this.level, this.lastLavaContact);
++                org.bukkit.entity.Entity damagee = this.getBukkitEntity();
++                org.bukkit.event.entity.EntityCombustEvent combustEvent = new org.bukkit.event.entity.EntityCombustByBlockEvent(damager, damagee, 15);
++                this.level.getCraftServer().getPluginManager().callEvent(combustEvent);
++
++                if (!combustEvent.isCancelled()) {
++                    this.igniteForSeconds(combustEvent.getDuration(), false);
++                }
++            } else {
++                // This will be called every single tick the entity is in lava, so don't throw an event
++                this.igniteForSeconds(15.0F, false);
++            }
++            // CraftBukkit end
+             if (this.level() instanceof ServerLevel serverLevel
+-                && this.hurtServer(serverLevel, this.damageSources().lava(), 4.0F)
++                && this.hurtServer(serverLevel, this.damageSources().lava().directBlock(this.level, this.lastLavaContact), 4.0F) // CraftBukkit - we also don't throw an event unless the object in lava is living, to save on some event calls
+                 && this.shouldPlayLavaHurtSound()
+                 && !this.isSilent()) {
+                 serverLevel.playSound(
+@@ -548,7 +_,23 @@
+     }
+ 
+     public final void igniteForSeconds(float seconds) {
+-        this.igniteForTicks(Mth.floor(seconds * 20.0F));
++        // CraftBukkit start
++        this.igniteForSeconds(seconds, true);
++    }
++
++    public final void igniteForSeconds(float f, boolean callEvent) {
++        if (callEvent) {
++            org.bukkit.event.entity.EntityCombustEvent event = new org.bukkit.event.entity.EntityCombustEvent(this.getBukkitEntity(), f);
++            this.level.getCraftServer().getPluginManager().callEvent(event);
++
++            if (event.isCancelled()) {
++                return;
++            }
++
++            f = event.getDuration();
++        }
++        // CraftBukkit end
++        this.igniteForTicks(Mth.floor(f * 20.0F));
+     }
+ 
+     public void igniteForTicks(int ticks) {
+@@ -570,7 +_,7 @@
+     }
+ 
+     protected void onBelowWorld() {
+-        this.discard();
++        this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.OUT_OF_WORLD); // CraftBukkit - add Bukkit remove cause
+     }
+ 
+     public boolean isFree(double x, double y, double z) {
+@@ -626,7 +_,43 @@
+         return this.onGround;
+     }
+ 
++    // Paper start - detailed watchdog information
++    public final Object posLock = new Object(); // Paper - log detailed entity tick information
++
++    private Vec3 moveVector;
++    private double moveStartX;
++    private double moveStartY;
++    private double moveStartZ;
++
++    public final Vec3 getMoveVector() {
++        return this.moveVector;
++    }
++
++    public final double getMoveStartX() {
++        return this.moveStartX;
++    }
++
++    public final double getMoveStartY() {
++        return this.moveStartY;
++    }
++
++    public final double getMoveStartZ() {
++        return this.moveStartZ;
++    }
++    // Paper end - detailed watchdog information
++
+     public void move(MoverType type, Vec3 movement) {
++        final Vec3 originalMovement = movement; // Paper - Expose pre-collision velocity
++        // Paper start - detailed watchdog information
++        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread("Cannot move an entity off-main");
++        synchronized (this.posLock) {
++            this.moveStartX = this.getX();
++            this.moveStartY = this.getY();
++            this.moveStartZ = this.getZ();
++            this.moveVector = movement;
++        }
++        try {
++        // Paper end - detailed watchdog information
+         if (this.noPhysics) {
+             this.setPos(this.getX() + movement.x, this.getY() + movement.y, this.getZ() + movement.z);
+         } else {
+@@ -701,6 +_,28 @@
+                     }
+                 }
+ 
++                // CraftBukkit start
++                if (this.horizontalCollision && this.getBukkitEntity() instanceof org.bukkit.entity.Vehicle) {
++                    org.bukkit.entity.Vehicle vehicle = (org.bukkit.entity.Vehicle) this.getBukkitEntity();
++                    org.bukkit.block.Block bl = this.level.getWorld().getBlockAt(Mth.floor(this.getX()), Mth.floor(this.getY()), Mth.floor(this.getZ()));
++
++                    if (movement.x > vec3.x) {
++                        bl = bl.getRelative(org.bukkit.block.BlockFace.EAST);
++                    } else if (movement.x < vec3.x) {
++                        bl = bl.getRelative(org.bukkit.block.BlockFace.WEST);
++                    } else if (movement.z > vec3.z) {
++                        bl = bl.getRelative(org.bukkit.block.BlockFace.SOUTH);
++                    } else if (movement.z < vec3.z) {
++                        bl = bl.getRelative(org.bukkit.block.BlockFace.NORTH);
++                    }
++
++                    if (!bl.getType().isAir()) {
++                        org.bukkit.event.vehicle.VehicleBlockCollisionEvent event = new org.bukkit.event.vehicle.VehicleBlockCollisionEvent(vehicle, bl, org.bukkit.craftbukkit.util.CraftVector.toBukkit(originalMovement)); // Paper - Expose pre-collision velocity
++                        this.level.getCraftServer().getPluginManager().callEvent(event);
++                    }
++                }
++                // CraftBukkit end
++
+                 if (!this.level().isClientSide() || this.isControlledByLocalInstance()) {
+                     Entity.MovementEmission movementEmission = this.getMovementEmission();
+                     if (movementEmission.emitsAnything() && !this.isPassenger()) {
+@@ -713,6 +_,13 @@
+                 profilerFiller.pop();
+             }
+         }
++        // Paper start - detailed watchdog information
++        } finally {
++            synchronized (this.posLock) { // Paper
++                this.moveVector = null;
++            } // Paper
++        }
++        // Paper end - detailed watchdog information
+     }
+ 
+     private void applyMovementEmissionAndPlaySound(Entity.MovementEmission movementEmission, Vec3 movement, BlockPos pos, BlockState state) {
+@@ -850,7 +_,7 @@
+     }
+ 
+     protected BlockPos getOnPos(float yOffset) {
+-        if (this.mainSupportingBlockPos.isPresent()) {
++        if (this.mainSupportingBlockPos.isPresent() && this.level().getChunkIfLoadedImmediately(this.mainSupportingBlockPos.get()) != null) { // Paper - ensure no loads
+             BlockPos blockPos = this.mainSupportingBlockPos.get();
+             if (!(yOffset > 1.0E-5F)) {
+                 return blockPos;
+@@ -1049,6 +_,20 @@
+         return SoundEvents.GENERIC_SPLASH;
+     }
+ 
++    // CraftBukkit start - Add delegate methods
++    public SoundEvent getSwimSound0() {
++        return this.getSwimSound();
++    }
++
++    public SoundEvent getSwimSplashSound0() {
++        return this.getSwimSplashSound();
++    }
++
++    public SoundEvent getSwimHighSpeedSplashSound0() {
++        return this.getSwimHighSpeedSplashSound();
++    }
++    // CraftBukkit end
++
+     public void recordMovementThroughBlocks(Vec3 oldPosition, Vec3 position) {
+         this.movementThisTick.add(new Entity.Movement(oldPosition, position));
+     }
+@@ -1485,6 +_,7 @@
+         this.setXRot(Mth.clamp(xRot, -90.0F, 90.0F) % 360.0F);
+         this.yRotO = this.getYRot();
+         this.xRotO = this.getXRot();
++        this.setYHeadRot(yRot); // Paper - Update head rotation
+     }
+ 
+     public void absMoveTo(double x, double y, double z) {
+@@ -1494,6 +_,7 @@
+         this.yo = y;
+         this.zo = d1;
+         this.setPos(d, y, d1);
++        if (this.valid) this.level.getChunk((int) Math.floor(this.getX()) >> 4, (int) Math.floor(this.getZ()) >> 4); // CraftBukkit
+     }
+ 
+     public void moveTo(Vec3 vec) {
+@@ -1513,11 +_,19 @@
+     }
+ 
+     public void moveTo(double x, double y, double z, float yRot, float xRot) {
++        // Paper start - Fix Entity Teleportation and cancel velocity if teleported
++        if (!preserveMotion) {
++            this.deltaMovement = Vec3.ZERO;
++        } else {
++            this.preserveMotion = false;
++        }
++        // Paper end - Fix Entity Teleportation and cancel velocity if teleported
+         this.setPosRaw(x, y, z);
+         this.setYRot(yRot);
+         this.setXRot(xRot);
+         this.setOldPosAndRot();
+         this.reapplyPosition();
++        this.setYHeadRot(yRot); // Paper - Update head rotation
+     }
+ 
+     public final void setOldPosAndRot() {
+@@ -1584,6 +_,7 @@
+     public void push(Entity entity) {
+         if (!this.isPassengerOfSameVehicle(entity)) {
+             if (!entity.noPhysics && !this.noPhysics) {
++                if (this.level.paperConfig().collisions.onlyPlayersCollide && !(entity instanceof ServerPlayer || this instanceof ServerPlayer)) return; // Paper - Collision option for requiring a player participant
+                 double d = entity.getX() - this.getX();
+                 double d1 = entity.getZ() - this.getZ();
+                 double max = Mth.absMax(d, d1);
+@@ -1617,7 +_,21 @@
+     }
+ 
+     public void push(double x, double y, double z) {
+-        this.setDeltaMovement(this.getDeltaMovement().add(x, y, z));
++        // Paper start - Add EntityKnockbackByEntityEvent and EntityPushedByEntityAttackEvent
++        this.push(x, y, z, null);
++    }
++
++    public void push(double x, double y, double z, @Nullable Entity pushingEntity) {
++        org.bukkit.util.Vector delta = new org.bukkit.util.Vector(x, y, z);
++        if (pushingEntity != null) {
++            io.papermc.paper.event.entity.EntityPushedByEntityAttackEvent event = new io.papermc.paper.event.entity.EntityPushedByEntityAttackEvent(this.getBukkitEntity(), io.papermc.paper.event.entity.EntityKnockbackEvent.Cause.PUSH, pushingEntity.getBukkitEntity(), delta);
++            if (!event.callEvent()) {
++                return;
++            }
++            delta = event.getKnockback();
++        }
++        this.setDeltaMovement(this.getDeltaMovement().add(delta.getX(), delta.getY(), delta.getZ()));
++        // Paper end - Add EntityKnockbackByEntityEvent and EntityPushedByEntityAttackEvent
+         this.hasImpulse = true;
+     }
+ 
+@@ -1724,8 +_,20 @@
+     }
+ 
+     public boolean isPushable() {
++        // Paper start - Climbing should not bypass cramming gamerule
++        return isCollidable(false);
++    }
++
++    public boolean isCollidable(boolean ignoreClimbing) {
++        // Paper end - Climbing should not bypass cramming gamerule
+         return false;
+     }
++
++    // CraftBukkit start - collidable API
++    public boolean canCollideWithBukkit(Entity entity) {
++        return this.isPushable();
++    }
++    // CraftBukkit end
+ 
+     public void awardKillScore(Entity entity, DamageSource damageSource) {
+         if (entity instanceof ServerPlayer) {
+@@ -1752,34 +_,70 @@
+     }
+ 
+     public boolean saveAsPassenger(CompoundTag compound) {
++        // CraftBukkit start - allow excluding certain data when saving
++        return this.saveAsPassenger(compound, true);
++    }
++
++    public boolean saveAsPassenger(CompoundTag compound, boolean includeAll) {
++        // CraftBukkit end
+         if (this.removalReason != null && !this.removalReason.shouldSave()) {
+             return false;
+         } else {
+             String encodeId = this.getEncodeId();
+-            if (encodeId == null) {
++            if (!this.persist || encodeId == null) { // CraftBukkit - persist flag
+                 return false;
+             } else {
+                 compound.putString("id", encodeId);
+-                this.saveWithoutId(compound);
++                this.saveWithoutId(compound, includeAll); // CraftBukkit - pass on includeAll
+                 return true;
+             }
+         }
+     }
++
++    // Paper start - Entity serialization api
++    public boolean serializeEntity(CompoundTag compound) {
++        List<Entity> pass = new java.util.ArrayList<>(this.getPassengers());
++        this.passengers = ImmutableList.of();
++        boolean result = save(compound);
++        this.passengers = ImmutableList.copyOf(pass);
++        return result;
++    }
++    // Paper end - Entity serialization api
+ 
+     public boolean save(CompoundTag compound) {
+         return !this.isPassenger() && this.saveAsPassenger(compound);
+     }
+ 
+     public CompoundTag saveWithoutId(CompoundTag compound) {
++        // CraftBukkit start - allow excluding certain data when saving
++        return this.saveWithoutId(compound, true);
++    }
++
++    public CompoundTag saveWithoutId(CompoundTag compound, boolean includeAll) {
++        // CraftBukkit end
+         try {
+-            if (this.vehicle != null) {
+-                compound.put("Pos", this.newDoubleList(this.vehicle.getX(), this.getY(), this.vehicle.getZ()));
+-            } else {
+-                compound.put("Pos", this.newDoubleList(this.getX(), this.getY(), this.getZ()));
+-            }
++            // CraftBukkit start - selectively save position
++            if (includeAll) {
++                if (this.vehicle != null) {
++                    compound.put("Pos", this.newDoubleList(this.vehicle.getX(), this.getY(), this.vehicle.getZ()));
++                } else {
++                    compound.put("Pos", this.newDoubleList(this.getX(), this.getY(), this.getZ()));
++                }
++             }
++            // CraftBukkit end
+ 
+             Vec3 deltaMovement = this.getDeltaMovement();
+             compound.put("Motion", this.newDoubleList(deltaMovement.x, deltaMovement.y, deltaMovement.z));
++            // CraftBukkit start - Checking for NaN pitch/yaw and resetting to zero
++            // TODO: make sure this is the best way to address this.
++            if (Float.isNaN(this.yRot)) {
++                this.yRot = 0;
++            }
++
++            if (Float.isNaN(this.xRot)) {
++                this.xRot = 0;
++            }
++            // CraftBukkit end
+             compound.put("Rotation", this.newFloatList(this.getYRot(), this.getXRot()));
+             compound.putFloat("FallDistance", this.fallDistance);
+             compound.putShort("Fire", (short)this.remainingFireTicks);
+@@ -1787,7 +_,29 @@
+             compound.putBoolean("OnGround", this.onGround());
+             compound.putBoolean("Invulnerable", this.invulnerable);
+             compound.putInt("PortalCooldown", this.portalCooldown);
+-            compound.putUUID("UUID", this.getUUID());
++            // CraftBukkit start - selectively save uuid and world
++            if (includeAll) {
++                compound.putUUID("UUID", this.getUUID());
++                // PAIL: Check above UUID reads 1.8 properly, ie: UUIDMost / UUIDLeast
++                compound.putLong("WorldUUIDLeast", this.level.getWorld().getUID().getLeastSignificantBits());
++                compound.putLong("WorldUUIDMost", this.level.getWorld().getUID().getMostSignificantBits());
++            }
++            compound.putInt("Bukkit.updateLevel", Entity.CURRENT_LEVEL);
++            if (!this.persist) {
++                compound.putBoolean("Bukkit.persist", this.persist);
++            }
++            if (!this.visibleByDefault) {
++                compound.putBoolean("Bukkit.visibleByDefault", this.visibleByDefault);
++            }
++            if (this.persistentInvisibility) {
++                compound.putBoolean("Bukkit.invisible", this.persistentInvisibility);
++            }
++            // SPIGOT-6907: re-implement LivingEntity#setMaximumAir()
++            if (this.maxAirTicks != this.getDefaultMaxAirSupply()) {
++                compound.putInt("Bukkit.MaxAirSupply", this.getMaxAirSupply());
++            }
++            compound.putInt("Spigot.ticksLived", this.tickCount);
++            // CraftBukkit end
+             Component customName = this.getCustomName();
+             if (customName != null) {
+                 compound.putString("CustomName", Component.Serializer.toJson(customName, this.registryAccess()));
+@@ -1828,13 +_,13 @@
+                 compound.put("Tags", listTag);
+             }
+ 
+-            this.addAdditionalSaveData(compound);
++            this.addAdditionalSaveData(compound, includeAll); // CraftBukkit - pass on includeAll
+             if (this.isVehicle()) {
+                 ListTag listTag = new ListTag();
+ 
+                 for (Entity entity : this.getPassengers()) {
+                     CompoundTag compoundTag = new CompoundTag();
+-                    if (entity.saveAsPassenger(compoundTag)) {
++                    if (entity.saveAsPassenger(compoundTag, includeAll)) { // CraftBukkit - pass on includeAll
+                         listTag.add(compoundTag);
+                     }
+                 }
+@@ -1844,6 +_,33 @@
+                 }
+             }
+ 
++            // CraftBukkit start - stores eventually existing bukkit values
++            if (this.bukkitEntity != null) {
++                this.bukkitEntity.storeBukkitValues(compound);
++            }
++            // CraftBukkit end
++            // Paper start
++            if (this.origin != null) {
++                UUID originWorld = this.originWorld != null ? this.originWorld : this.level != null ? this.level.getWorld().getUID() : null;
++                if (originWorld != null) {
++                    compound.putUUID("Paper.OriginWorld", originWorld);
++                }
++                compound.put("Paper.Origin", this.newDoubleList(this.origin.getX(), this.origin.getY(), this.origin.getZ()));
++            }
++            if (this.spawnReason != null) {
++                compound.putString("Paper.SpawnReason", this.spawnReason.name());
++            }
++            // Save entity's from mob spawner status
++            if (this.spawnedViaMobSpawner) {
++                compound.putBoolean("Paper.FromMobSpawner", true);
++            }
++            if (this.fromNetherPortal) {
++                compound.putBoolean("Paper.FromNetherPortal", true);
++            }
++            if (this.freezeLocked) {
++                compound.putBoolean("Paper.FreezeLock", true);
++            }
++            // Paper end
+             return compound;
+         } catch (Throwable var9) {
+             CrashReport crashReport = CrashReport.forThrowable(var9, "Saving entity NBT");
+@@ -1930,6 +_,69 @@
+             } else {
+                 throw new IllegalStateException("Entity has invalid rotation");
+             }
++            // CraftBukkit start
++            // Spigot start
++            if (this instanceof net.minecraft.world.entity.LivingEntity) {
++                this.tickCount = compound.getInt("Spigot.ticksLived");
++            }
++            // Spigot end
++            this.persist = !compound.contains("Bukkit.persist") || compound.getBoolean("Bukkit.persist");
++            this.visibleByDefault = !compound.contains("Bukkit.visibleByDefault") || compound.getBoolean("Bukkit.visibleByDefault");
++            // SPIGOT-6907: re-implement LivingEntity#setMaximumAir()
++            if (compound.contains("Bukkit.MaxAirSupply")) {
++                this.maxAirTicks = compound.getInt("Bukkit.MaxAirSupply");
++            }
++            // CraftBukkit end
++
++            // CraftBukkit start
++            // Paper - move world parsing/loading to PlayerList#placeNewPlayer
++            this.getBukkitEntity().readBukkitValues(compound);
++            if (compound.contains("Bukkit.invisible")) {
++                boolean bukkitInvisible = compound.getBoolean("Bukkit.invisible");
++                this.setInvisible(bukkitInvisible);
++                this.persistentInvisibility = bukkitInvisible;
++            }
++            // CraftBukkit end
++
++            // Paper start
++            ListTag originTag = compound.getList("Paper.Origin", net.minecraft.nbt.Tag.TAG_DOUBLE);
++            if (!originTag.isEmpty()) {
++                UUID originWorld = null;
++                if (compound.contains("Paper.OriginWorld")) {
++                    originWorld = compound.getUUID("Paper.OriginWorld");
++                } else if (this.level != null) {
++                    originWorld = this.level.getWorld().getUID();
++                }
++                this.originWorld = originWorld;
++                origin = new org.bukkit.util.Vector(originTag.getDouble(0), originTag.getDouble(1), originTag.getDouble(2));
++            }
++
++            spawnedViaMobSpawner = compound.getBoolean("Paper.FromMobSpawner"); // Restore entity's from mob spawner status
++            fromNetherPortal = compound.getBoolean("Paper.FromNetherPortal");
++            if (compound.contains("Paper.SpawnReason")) {
++                String spawnReasonName = compound.getString("Paper.SpawnReason");
++                try {
++                    spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.valueOf(spawnReasonName);
++                } catch (Exception ignored) {
++                    LOGGER.error("Unknown SpawnReason " + spawnReasonName + " for " + this);
++                }
++            }
++            if (spawnReason == null) {
++                if (spawnedViaMobSpawner) {
++                    spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.SPAWNER;
++                } else if (this instanceof Mob && (this instanceof net.minecraft.world.entity.animal.Animal || this instanceof net.minecraft.world.entity.animal.AbstractFish) && !((Mob) this).removeWhenFarAway(0.0)) {
++                    if (!compound.getBoolean("PersistenceRequired")) {
++                        spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.NATURAL;
++                    }
++                }
++            }
++            if (spawnReason == null) {
++                spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT;
++            }
++            if (compound.contains("Paper.FreezeLock")) {
++                freezeLocked = compound.getBoolean("Paper.FreezeLock");
++            }
++            // Paper end
+         } catch (Throwable var17) {
+             CrashReport crashReport = CrashReport.forThrowable(var17, "Loading entity NBT");
+             CrashReportCategory crashReportCategory = crashReport.addCategory("Entity being loaded");
+@@ -1949,6 +_,12 @@
+         return type.canSerialize() && key != null ? key.toString() : null;
+     }
+ 
++    // CraftBukkit start - allow excluding certain data when saving
++    protected void addAdditionalSaveData(CompoundTag tag, boolean includeAll) {
++        this.addAdditionalSaveData(tag);
++    }
++    // CraftBukkit end
++
+     protected abstract void readAdditionalSaveData(CompoundTag tag);
+ 
+     protected abstract void addAdditionalSaveData(CompoundTag tag);
+@@ -1990,11 +_,61 @@
+ 
+     @Nullable
+     public ItemEntity spawnAtLocation(ServerLevel level, ItemStack stack, float yOffset) {
++        // Paper start - Restore vanilla drops behavior
++        return this.spawnAtLocation(level, stack, yOffset, null);
++    }
++    public record DefaultDrop(Item item, org.bukkit.inventory.ItemStack stack, @Nullable java.util.function.Consumer<ItemStack> dropConsumer) {
++        public DefaultDrop(final ItemStack stack, final java.util.function.Consumer<ItemStack> dropConsumer) {
++            this(stack.getItem(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(stack), dropConsumer);
++        }
++
++        public void runConsumer(final java.util.function.Consumer<org.bukkit.inventory.ItemStack> fallback) {
++            if (this.dropConsumer == null || org.bukkit.craftbukkit.inventory.CraftItemType.bukkitToMinecraft(this.stack.getType()) != this.item) {
++                fallback.accept(this.stack);
++            } else {
++                this.dropConsumer.accept(org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(this.stack));
++            }
++        }
++    }
++    @Nullable
++    public ItemEntity spawnAtLocation(ServerLevel level, ItemStack stack, float yOffset, @Nullable java.util.function.Consumer<? super ItemEntity> delayedAddConsumer) {
++        // Paper end - Restore vanilla drops behavior
+         if (stack.isEmpty()) {
+             return null;
+         } else {
+-            ItemEntity itemEntity = new ItemEntity(level, this.getX(), this.getY() + yOffset, this.getZ(), stack);
++            // CraftBukkit start - Capture drops for death event
++            if (this instanceof net.minecraft.world.entity.LivingEntity && !this.forceDrops) {
++                // Paper start - Restore vanilla drops behavior
++                ((net.minecraft.world.entity.LivingEntity) this).drops.add(new net.minecraft.world.entity.Entity.DefaultDrop(stack, itemStack -> {
++                    ItemEntity itemEntity = new ItemEntity(this.level, this.getX(), this.getY() + (double) yOffset, this.getZ(), itemStack); // stack is copied before consumer
++                    itemEntity.setDefaultPickUpDelay();
++                    this.level.addFreshEntity(itemEntity);
++                    if (delayedAddConsumer != null) delayedAddConsumer.accept(itemEntity);
++                }));
++                // Paper end - Restore vanilla drops behavior
++                return null;
++            }
++            // CraftBukkit end
++            ItemEntity itemEntity = new ItemEntity(level, this.getX(), this.getY() + yOffset, this.getZ(), stack.copy()); // Paper - copy so we can destroy original
++            stack.setCount(0); // Paper - destroy this item - if this ever leaks due to game bugs, ensure it doesn't dupe
++
+             itemEntity.setDefaultPickUpDelay();
++            itemEntity.setDefaultPickUpDelay(); // Paper - diff on change (in dropConsumer)
++            // Paper start - Call EntityDropItemEvent
++            return this.spawnAtLocation(level, itemEntity);
++        }
++    }
++    @Nullable
++    public ItemEntity spawnAtLocation(ServerLevel level, ItemEntity itemEntity) {
++        {
++            // Paper end - Call EntityDropItemEvent
++            // CraftBukkit start
++            org.bukkit.event.entity.EntityDropItemEvent event = new org.bukkit.event.entity.EntityDropItemEvent(this.getBukkitEntity(), (org.bukkit.entity.Item) itemEntity.getBukkitEntity());
++            org.bukkit.Bukkit.getPluginManager().callEvent(event);
++            if (event.isCancelled()) {
++                return null;
++            }
++            // CraftBukkit end
+             level.addFreshEntity(itemEntity);
+             return itemEntity;
+         }
+@@ -2028,7 +_,16 @@
+         if (this.isAlive() && this instanceof Leashable leashable) {
+             if (leashable.getLeashHolder() == player) {
+                 if (!this.level().isClientSide()) {
+-                    if (player.hasInfiniteMaterials()) {
++                    // CraftBukkit start - fire PlayerUnleashEntityEvent
++                    // Paper start - Expand EntityUnleashEvent
++                    org.bukkit.event.player.PlayerUnleashEntityEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerUnleashEntityEvent(this, player, hand, !player.hasInfiniteMaterials());
++                    if (event.isCancelled()) {
++                        // Paper end - Expand EntityUnleashEvent
++                        ((ServerPlayer) player).connection.send(new net.minecraft.network.protocol.game.ClientboundSetEntityLinkPacket(this, leashable.getLeashHolder()));
++                        return InteractionResult.PASS;
++                    }
++                    // CraftBukkit end
++                    if (!event.isDropLeash()) { // Paper - Expand EntityUnleashEvent
+                         leashable.removeLeash();
+                     } else {
+                         leashable.dropLeash();
+@@ -2043,6 +_,13 @@
+             ItemStack itemInHand = player.getItemInHand(hand);
+             if (itemInHand.is(Items.LEAD) && leashable.canHaveALeashAttachedToIt()) {
+                 if (!this.level().isClientSide()) {
++                    // CraftBukkit start - fire PlayerLeashEntityEvent
++                    if (org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerLeashEntityEvent(this, player, player, hand).isCancelled()) {
++                        ((ServerPlayer) player).connection.send(new net.minecraft.network.protocol.game.ClientboundSetEntityLinkPacket(this, leashable.getLeashHolder()));
++                        player.containerMenu.sendAllDataToRemote(); // Paper - Fix inventory desync
++                        return InteractionResult.PASS;
++                    }
++                    // CraftBukkit end
+                     leashable.setLeashedTo(player, true);
+                 }
+ 
+@@ -2116,11 +_,11 @@
+     }
+ 
+     public boolean startRiding(Entity vehicle, boolean force) {
+-        if (vehicle == this.vehicle) {
++        if (vehicle == this.vehicle || vehicle.level != this.level) { // Paper - Ensure entity passenger world matches ridden entity (bad plugins)
+             return false;
+         } else if (!vehicle.couldAcceptPassenger()) {
+             return false;
+-        } else if (!this.level().isClientSide() && !vehicle.type.canSerialize()) {
++        } else if (!force && !this.level().isClientSide() && !vehicle.type.canSerialize()) { // SPIGOT-7947: Allow force riding all entities
+             return false;
+         } else {
+             for (Entity entity = vehicle; entity.vehicle != null; entity = entity.vehicle) {
+@@ -2130,6 +_,27 @@
+             }
+ 
+             if (force || this.canRide(vehicle) && vehicle.canAddPassenger(this)) {
++                // CraftBukkit start
++                if (vehicle.getBukkitEntity() instanceof org.bukkit.entity.Vehicle && this.getBukkitEntity() instanceof org.bukkit.entity.LivingEntity) {
++                    org.bukkit.event.vehicle.VehicleEnterEvent event = new org.bukkit.event.vehicle.VehicleEnterEvent((org.bukkit.entity.Vehicle) vehicle.getBukkitEntity(), this.getBukkitEntity());
++                    // Suppress during worldgen
++                    if (this.valid) {
++                        org.bukkit.Bukkit.getPluginManager().callEvent(event);
++                    }
++                    if (event.isCancelled()) {
++                        return false;
++                    }
++                }
++
++                org.bukkit.event.entity.EntityMountEvent event = new org.bukkit.event.entity.EntityMountEvent(this.getBukkitEntity(), vehicle.getBukkitEntity());
++                // Suppress during worldgen
++                if (this.valid) {
++                    org.bukkit.Bukkit.getPluginManager().callEvent(event);
++                }
++                if (event.isCancelled()) {
++                    return false;
++                }
++                // CraftBukkit end
+                 if (this.isPassenger()) {
+                     this.stopRiding();
+                 }
+@@ -2158,15 +_,26 @@
+     }
+ 
+     public void removeVehicle() {
++        // Paper start - Force entity dismount during teleportation
++        this.removeVehicle(false);
++    }
++    public void removeVehicle(boolean suppressCancellation) {
++        // Paper end - Force entity dismount during teleportation
+         if (this.vehicle != null) {
+             Entity entity = this.vehicle;
+             this.vehicle = null;
+-            entity.removePassenger(this);
++            if (!entity.removePassenger(this, suppressCancellation)) this.vehicle = entity; // CraftBukkit // Paper - Force entity dismount during teleportation
+         }
+     }
+ 
+     public void stopRiding() {
+-        this.removeVehicle();
++        // Paper start - Force entity dismount during teleportation
++        this.stopRiding(false);
++    }
++
++    public void stopRiding(boolean suppressCancellation) {
++        this.removeVehicle(suppressCancellation);
++        // Paper end - Force entity dismount during teleportation
+     }
+ 
+     protected void addPassenger(Entity passenger) {
+@@ -2190,10 +_,43 @@
+         }
+     }
+ 
+-    protected void removePassenger(Entity passenger) {
++    // Paper start - Force entity dismount during teleportation
++    protected boolean removePassenger(Entity passenger) {
++        return removePassenger(passenger, false);
++    }
++    protected boolean removePassenger(Entity passenger, boolean suppressCancellation) { // CraftBukkit
++        // Paper end - Force entity dismount during teleportation
+         if (passenger.getVehicle() == this) {
+             throw new IllegalStateException("Use x.stopRiding(y), not y.removePassenger(x)");
+         } else {
++            // CraftBukkit start
++            org.bukkit.craftbukkit.entity.CraftEntity craft = (org.bukkit.craftbukkit.entity.CraftEntity) passenger.getBukkitEntity().getVehicle();
++            Entity orig = craft == null ? null : craft.getHandle();
++            if (this.getBukkitEntity() instanceof org.bukkit.entity.Vehicle && passenger.getBukkitEntity() instanceof org.bukkit.entity.LivingEntity) {
++                org.bukkit.event.vehicle.VehicleExitEvent event = new org.bukkit.event.vehicle.VehicleExitEvent(
++                        (org.bukkit.entity.Vehicle) this.getBukkitEntity(),
++                        (org.bukkit.entity.LivingEntity) passenger.getBukkitEntity(), !suppressCancellation // Paper - Force entity dismount during teleportation
++                );
++                // Suppress during worldgen
++                if (this.valid) {
++                    org.bukkit.Bukkit.getPluginManager().callEvent(event);
++                }
++                org.bukkit.craftbukkit.entity.CraftEntity craftn = (org.bukkit.craftbukkit.entity.CraftEntity) passenger.getBukkitEntity().getVehicle();
++                Entity n = craftn == null ? null : craftn.getHandle();
++                if (event.isCancelled() || n != orig) {
++                    return false;
++                }
++            }
++
++            org.bukkit.event.entity.EntityDismountEvent event = new org.bukkit.event.entity.EntityDismountEvent(passenger.getBukkitEntity(), this.getBukkitEntity(), !suppressCancellation); // Paper - Force entity dismount during teleportation
++            // Suppress during worldgen
++            if (this.valid) {
++                org.bukkit.Bukkit.getPluginManager().callEvent(event);
++            }
++            if (event.isCancelled()) {
++                return false;
++            }
++            // CraftBukkit end
+             if (this.passengers.size() == 1 && this.passengers.get(0) == passenger) {
+                 this.passengers = ImmutableList.of();
+             } else {
+@@ -2203,6 +_,7 @@
+             passenger.boardingCooldown = 60;
+             this.gameEvent(GameEvent.ENTITY_DISMOUNT, passenger);
+         }
++        return true; // CraftBukkit
+     }
+ 
+     protected boolean canAddPassenger(Entity passenger) {
+@@ -2295,8 +_,8 @@
+                     TeleportTransition portalDestination = this.portalProcess.getPortalDestination(serverLevel, this);
+                     if (portalDestination != null) {
+                         ServerLevel level = portalDestination.newLevel();
+-                        if (serverLevel.getServer().isLevelEnabled(level)
+-                            && (level.dimension() == serverLevel.dimension() || this.canTeleport(serverLevel, level))) {
++                        if (this instanceof ServerPlayer // CraftBukkit - always call event for players
++                            || (level != null && (level.dimension() == serverLevel.dimension() || this.canTeleport(serverLevel, level)))) { // CraftBukkit
+                             this.teleport(portalDestination);
+                         }
+                     }
+@@ -2377,7 +_,7 @@
+     }
+ 
+     public boolean isCrouching() {
+-        return this.hasPose(Pose.CROUCHING);
++        return this.hasPose(net.minecraft.world.entity.Pose.CROUCHING);
+     }
+ 
+     public boolean isSprinting() {
+@@ -2393,7 +_,7 @@
+     }
+ 
+     public boolean isVisuallySwimming() {
+-        return this.hasPose(Pose.SWIMMING);
++        return this.hasPose(net.minecraft.world.entity.Pose.SWIMMING);
+     }
+ 
+     public boolean isVisuallyCrawling() {
+@@ -2401,6 +_,13 @@
+     }
+ 
+     public void setSwimming(boolean swimming) {
++        // CraftBukkit start
++        if (this.valid && this.isSwimming() != swimming && this instanceof net.minecraft.world.entity.LivingEntity) {
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.callToggleSwimEvent((net.minecraft.world.entity.LivingEntity) this, swimming).isCancelled()) {
++                return;
++            }
++        }
++        // CraftBukkit end
+         this.setSharedFlag(4, swimming);
+     }
+ 
+@@ -2439,6 +_,7 @@
+ 
+     @Nullable
+     public PlayerTeam getTeam() {
++        if (!this.level().paperConfig().scoreboards.allowNonPlayerEntitiesOnScoreboards && !(this instanceof Player)) { return null; } // Paper - Perf: Disable Scoreboards for non players by default
+         return this.level().getScoreboard().getPlayersTeam(this.getScoreboardName());
+     }
+ 
+@@ -2455,7 +_,11 @@
+     }
+ 
+     public void setInvisible(boolean invisible) {
+-        this.setSharedFlag(5, invisible);
++        // CraftBukkit - start
++        if (!this.persistentInvisibility) { // Prevent Minecraft from removing our invisibility flag
++            this.setSharedFlag(5, invisible);
++        }
++        // CraftBukkit - end
+     }
+ 
+     public boolean getSharedFlag(int flag) {
+@@ -2472,7 +_,7 @@
+     }
+ 
+     public int getMaxAirSupply() {
+-        return 300;
++        return this.maxAirTicks; // CraftBukkit - SPIGOT-6907: re-implement LivingEntity#setMaximumAir()
+     }
+ 
+     public int getAirSupply() {
+@@ -2480,7 +_,18 @@
+     }
+ 
+     public void setAirSupply(int air) {
+-        this.entityData.set(DATA_AIR_SUPPLY_ID, air);
++        // CraftBukkit start
++        org.bukkit.event.entity.EntityAirChangeEvent event = new org.bukkit.event.entity.EntityAirChangeEvent(this.getBukkitEntity(), air);
++        // Suppress during worldgen
++        if (this.valid) {
++            event.getEntity().getServer().getPluginManager().callEvent(event);
++        }
++        if (event.isCancelled() && this.getAirSupply() != air) {
++            this.entityData.markDirty(Entity.DATA_AIR_SUPPLY_ID);
++            return;
++        }
++        this.entityData.set(Entity.DATA_AIR_SUPPLY_ID, event.getAmount());
++        // CraftBukkit end
+     }
+ 
+     public int getTicksFrozen() {
+@@ -2506,11 +_,43 @@
+ 
+     public void thunderHit(ServerLevel level, LightningBolt lightning) {
+         this.setRemainingFireTicks(this.remainingFireTicks + 1);
++        // CraftBukkit start
++        final org.bukkit.entity.Entity thisBukkitEntity = this.getBukkitEntity();
++        final org.bukkit.entity.Entity stormBukkitEntity = lightning.getBukkitEntity();
++        final org.bukkit.plugin.PluginManager pluginManager = org.bukkit.Bukkit.getPluginManager();
++        // CraftBukkit end
+         if (this.remainingFireTicks == 0) {
+-            this.igniteForSeconds(8.0F);
+-        }
+-
+-        this.hurtServer(level, this.damageSources().lightningBolt(), 5.0F);
++            // CraftBukkit start - Call a combust event when lightning strikes
++            org.bukkit.event.entity.EntityCombustByEntityEvent entityCombustEvent = new org.bukkit.event.entity.EntityCombustByEntityEvent(stormBukkitEntity, thisBukkitEntity, 8.0F);
++            pluginManager.callEvent(entityCombustEvent);
++            if (!entityCombustEvent.isCancelled()) {
++                this.igniteForSeconds(entityCombustEvent.getDuration(), false);
++            // Paper start - fix EntityCombustEvent cancellation
++            } else {
++                this.setRemainingFireTicks(this.remainingFireTicks - 1);
++            // Paper end - fix EntityCombustEvent cancellation
++            }
++            // CraftBukkit end
++        }
++
++        // CraftBukkit start
++        if (thisBukkitEntity instanceof org.bukkit.entity.Hanging) {
++            org.bukkit.event.hanging.HangingBreakByEntityEvent hangingEvent = new org.bukkit.event.hanging.HangingBreakByEntityEvent((org.bukkit.entity.Hanging) thisBukkitEntity, stormBukkitEntity);
++            pluginManager.callEvent(hangingEvent);
++
++            if (hangingEvent.isCancelled()) {
++                return;
++            }
++        }
++
++        if (this.fireImmune()) {
++            return;
++        }
++
++        if (!this.hurtServer(level, this.damageSources().lightningBolt().customEventDamager(lightning), 5.0F)) { // Paper - fix DamageSource API
++            return;
++        }
++        // CraftBukkit end
+     }
+ 
+     public void onAboveBubbleCol(boolean downwards) {
+@@ -2636,26 +_,30 @@
+         return this.removalReason != null
+             ? String.format(
+                 Locale.ROOT,
+-                "%s['%s'/%d, l='%s', x=%.2f, y=%.2f, z=%.2f, removed=%s]",
++                "%s['%s'/%d, uuid='%s', l='%s', x=%.2f, y=%.2f, z=%.2f, cpos=%s, tl=%d, v=%b, removed=%s]", // Paper - add more info
+                 this.getClass().getSimpleName(),
+                 this.getName().getString(),
+                 this.id,
++                this.uuid, // Paper - add more info
+                 string,
+                 this.getX(),
+                 this.getY(),
+                 this.getZ(),
++                this.chunkPosition(), this.tickCount, this.valid, // Paper - add more info
+                 this.removalReason
+             )
+             : String.format(
+                 Locale.ROOT,
+-                "%s['%s'/%d, l='%s', x=%.2f, y=%.2f, z=%.2f]",
++                "%s['%s'/%d, uuid='%s', l='%s', x=%.2f, y=%.2f, z=%.2f, cpos=%s, tl=%d, v=%b]", // Paper - add more info
+                 this.getClass().getSimpleName(),
+                 this.getName().getString(),
+                 this.id,
++                this.uuid, // Paper - add more info
+                 string,
+                 this.getX(),
+                 this.getY(),
+-                this.getZ()
++                this.getZ(),
++                this.chunkPosition(), this.tickCount, this.valid // Paper - add more info
+             );
+     }
+ 
+@@ -2679,6 +_,13 @@
+     }
+ 
+     public void restoreFrom(Entity entity) {
++        // Paper start - Forward CraftEntity in teleport command
++        org.bukkit.craftbukkit.entity.CraftEntity bukkitEntity = entity.bukkitEntity;
++        if (bukkitEntity != null) {
++            bukkitEntity.setHandle(this);
++            this.bukkitEntity = bukkitEntity;
++        }
++        // Paper end - Forward CraftEntity in teleport command
+         CompoundTag compoundTag = entity.saveWithoutId(new CompoundTag());
+         compoundTag.remove("Dimension");
+         this.load(compoundTag);
+@@ -2688,7 +_,56 @@
+ 
+     @Nullable
+     public Entity teleport(TeleportTransition teleportTransition) {
++        // Paper start - Fix item duplication and teleport issues
++        if ((!this.isAlive() || !this.valid) && (teleportTransition.newLevel() != this.level)) {
++            LOGGER.warn("Illegal Entity Teleport " + this + " to " + teleportTransition.newLevel() + ":" + teleportTransition.position(), new Throwable());
++            return null;
++        }
++        // Paper end - Fix item duplication and teleport issues
+         if (this.level() instanceof ServerLevel serverLevel && !this.isRemoved()) {
++            // CraftBukkit start
++            PositionMoveRotation absolutePosition = PositionMoveRotation.calculateAbsolute(PositionMoveRotation.of(this), PositionMoveRotation.of(teleportTransition), teleportTransition.relatives());
++            Vec3 velocity = absolutePosition.deltaMovement(); // Paper
++            org.bukkit.Location to = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(absolutePosition.position(), teleportTransition.newLevel().getWorld(), absolutePosition.yRot(), absolutePosition.xRot());
++            // Paper start - gateway-specific teleport event
++            final org.bukkit.event.entity.EntityTeleportEvent teleEvent;
++            if (this.portalProcess != null && this.portalProcess.isSamePortal(((net.minecraft.world.level.block.EndGatewayBlock) net.minecraft.world.level.block.Blocks.END_GATEWAY)) && this.level.getBlockEntity(this.portalProcess.getEntryPosition()) instanceof net.minecraft.world.level.block.entity.TheEndGatewayBlockEntity theEndGatewayBlockEntity) {
++                teleEvent = new com.destroystokyo.paper.event.entity.EntityTeleportEndGatewayEvent(this.getBukkitEntity(), this.getBukkitEntity().getLocation(), to, new org.bukkit.craftbukkit.block.CraftEndGateway(to.getWorld(), theEndGatewayBlockEntity));
++                teleEvent.callEvent();
++            } else {
++                teleEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTeleportEvent(this, to);
++            }
++            // Paper end - gateway-specific teleport event
++            if (teleEvent.isCancelled() || teleEvent.getTo() == null) {
++                return null;
++            }
++            if (!to.equals(teleEvent.getTo())) {
++                to = teleEvent.getTo();
++                teleportTransition = new TeleportTransition(((org.bukkit.craftbukkit.CraftWorld) to.getWorld()).getHandle(), org.bukkit.craftbukkit.util.CraftLocation.toVec3D(to), Vec3.ZERO, to.getYaw(), to.getPitch(), teleportTransition.missingRespawnBlock(), teleportTransition.asPassenger(), Set.of(), teleportTransition.postTeleportTransition(), teleportTransition.cause());
++                // Paper start - Call EntityPortalExitEvent
++                velocity = Vec3.ZERO;
++            }
++            if (this.portalProcess != null) { // if in a portal
++                org.bukkit.craftbukkit.entity.CraftEntity bukkitEntity = this.getBukkitEntity();
++                org.bukkit.event.entity.EntityPortalExitEvent event = new org.bukkit.event.entity.EntityPortalExitEvent(
++                    bukkitEntity,
++                    bukkitEntity.getLocation(), to.clone(),
++                    bukkitEntity.getVelocity(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(velocity)
++                );
++                event.callEvent();
++
++                // Only change the target if actually needed, since we reset relative flags
++                if (!event.isCancelled() && event.getTo() != null && (!event.getTo().equals(event.getFrom()) || !event.getAfter().equals(event.getBefore()))) {
++                    to = event.getTo().clone();
++                    velocity = org.bukkit.craftbukkit.util.CraftVector.toNMS(event.getAfter());
++                    teleportTransition = new TeleportTransition(((org.bukkit.craftbukkit.CraftWorld) to.getWorld()).getHandle(), org.bukkit.craftbukkit.util.CraftLocation.toVec3D(to), velocity, to.getYaw(), to.getPitch(), teleportTransition.missingRespawnBlock(), teleportTransition.asPassenger(), Set.of(), teleportTransition.postTeleportTransition(), teleportTransition.cause());
++                }
++            }
++            if (this.isRemoved()) {
++                return null;
++            }
++            // Paper end - Call EntityPortalExitEvent
++            // CraftBukkit end
+             ServerLevel level = teleportTransition.newLevel();
+             boolean flag = level.dimension() != serverLevel.dimension();
+             if (!teleportTransition.asPassenger()) {
+@@ -2737,10 +_,19 @@
+             profilerFiller.pop();
+             return null;
+         } else {
++            // Paper start - Fix item duplication and teleport issues
++            if (this instanceof Leashable leashable) {
++                leashable.dropLeash(); // Paper drop lead
++            }
++            // Paper end - Fix item duplication and teleport issues
+             entityx.restoreFrom(this);
+             this.removeAfterChangingDimensions();
++            // CraftBukkit start - Forward the CraftEntity to the new entity
++            //this.getBukkitEntity().setHandle(entity);
++            //entity.bukkitEntity = this.getBukkitEntity(); // Paper - forward CraftEntity in teleport command; moved to Entity#restoreFrom
++            // CraftBukkit end
+             entityx.teleportSetPosition(PositionMoveRotation.of(teleportTransition), teleportTransition.relatives());
+-            level.addDuringTeleport(entityx);
++            if (this.inWorld) level.addDuringTeleport(entityx); // CraftBukkit - Don't spawn the new entity if the current entity isn't spawned
+ 
+             for (Entity entity2 : list) {
+                 entity2.startRiding(entityx, true);
+@@ -2814,9 +_,17 @@
+     }
+ 
+     protected void removeAfterChangingDimensions() {
+-        this.setRemoved(Entity.RemovalReason.CHANGED_DIMENSION);
+-        if (this instanceof Leashable leashable) {
+-            leashable.removeLeash();
++        this.setRemoved(Entity.RemovalReason.CHANGED_DIMENSION, null); // CraftBukkit - add Bukkit remove cause
++        if (this instanceof Leashable leashable && leashable.isLeashed()) { // Paper - only call if it is leashed
++            // Paper start - Expand EntityUnleashEvent
++            final org.bukkit.event.entity.EntityUnleashEvent event = new org.bukkit.event.entity.EntityUnleashEvent(this.getBukkitEntity(), org.bukkit.event.entity.EntityUnleashEvent.UnleashReason.UNKNOWN, false); // CraftBukkit
++            event.callEvent();
++            if (!event.isDropLeash()) {
++                leashable.removeLeash();
++            } else {
++                leashable.dropLeash();
++            }
++            // Paper end - Expand EntityUnleashEvent
+         }
+     }
+ 
+@@ -2824,11 +_,34 @@
+         return PortalShape.getRelativePosition(portal, axis, this.position(), this.getDimensions(this.getPose()));
+     }
+ 
++    // CraftBukkit start
++    public org.bukkit.craftbukkit.event.CraftPortalEvent callPortalEvent(Entity entity, org.bukkit.Location exit, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause, int searchRadius, int creationRadius) {
++        org.bukkit.entity.Entity bukkitEntity = entity.getBukkitEntity();
++        org.bukkit.Location enter = bukkitEntity.getLocation();
++
++        // Paper start
++        final org.bukkit.PortalType portalType = switch (cause) {
++            case END_PORTAL -> org.bukkit.PortalType.ENDER;
++            case NETHER_PORTAL -> org.bukkit.PortalType.NETHER;
++            case END_GATEWAY -> org.bukkit.PortalType.END_GATEWAY; // not actually used yet
++            default -> org.bukkit.PortalType.CUSTOM;
++        };
++        org.bukkit.event.entity.EntityPortalEvent event = new org.bukkit.event.entity.EntityPortalEvent(bukkitEntity, enter, exit, searchRadius, true, creationRadius, portalType);
++        // Paper end
++        event.getEntity().getServer().getPluginManager().callEvent(event);
++        if (event.isCancelled() || event.getTo() == null || event.getTo().getWorld() == null || !entity.isAlive()) {
++            return null;
++        }
++        return new org.bukkit.craftbukkit.event.CraftPortalEvent(event);
++    }
++    // CraftBukkit end
++
+     public boolean canUsePortal(boolean allowPassengers) {
+         return (allowPassengers || !this.isPassenger()) && this.isAlive();
+     }
+ 
+     public boolean canTeleport(Level fromLevel, Level toLevel) {
++        if (!this.isAlive() || !this.valid) return false; // Paper - Fix item duplication and teleport issues
+         if (fromLevel.dimension() == Level.END && toLevel.dimension() == Level.OVERWORLD) {
+             for (Entity entity : this.getPassengers()) {
+                 if (entity instanceof ServerPlayer serverPlayer && !serverPlayer.seenCredits) {
+@@ -2936,9 +_,14 @@
+         return this.entityData.get(DATA_CUSTOM_NAME_VISIBLE);
+     }
+ 
+-    public boolean teleportTo(ServerLevel level, double x, double y, double z, Set<Relative> relativeMovements, float yaw, float pitch, boolean setCamera) {
++    // CraftBukkit start
++    public final boolean teleportTo(ServerLevel level, double x, double y, double z, Set<Relative> relativeMovements, float yaw, float pitch, boolean setCamera) {
++        return this.teleportTo(level, x, y, z, relativeMovements, yaw, pitch, setCamera, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.UNKNOWN);
++    }
++    public boolean teleportTo(ServerLevel level, double x, double y, double z, Set<Relative> relativeMovements, float yaw, float pitch, boolean setCamera, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause) {
++        // CraftBukkit end
+         float f = Mth.clamp(pitch, -90.0F, 90.0F);
+-        Entity entity = this.teleport(new TeleportTransition(level, new Vec3(x, y, z), Vec3.ZERO, yaw, f, relativeMovements, TeleportTransition.DO_NOTHING));
++        Entity entity = this.teleport(new TeleportTransition(level, new Vec3(x, y, z), Vec3.ZERO, yaw, f, relativeMovements, TeleportTransition.DO_NOTHING, cause)); // CraftBukkit
+         return entity != null;
+     }
+ 
+@@ -3052,7 +_,26 @@
+     }
+ 
+     public final void setBoundingBox(AABB bb) {
+-        this.bb = bb;
++        // CraftBukkit start - block invalid bounding boxes
++        double minX = bb.minX,
++                minY = bb.minY,
++                minZ = bb.minZ,
++                maxX = bb.maxX,
++                maxY = bb.maxY,
++                maxZ = bb.maxZ;
++        double len = bb.maxX - bb.minX;
++        if (len < 0) maxX = minX;
++        if (len > 64) maxX = minX + 64.0;
++
++        len = bb.maxY - bb.minY;
++        if (len < 0) maxY = minY;
++        if (len > 64) maxY = minY + 64.0;
++
++        len = bb.maxZ - bb.minZ;
++        if (len < 0) maxZ = minZ;
++        if (len > 64) maxZ = minZ + 64.0;
++        this.bb = new AABB(minX, minY, minZ, maxX, maxY, maxZ);
++        // CraftBukkit end
+     }
+ 
+     public final float getEyeHeight(Pose pose) {
+@@ -3096,6 +_,12 @@
+     }
+ 
+     public void stopSeenByPlayer(ServerPlayer serverPlayer) {
++        // Paper start - entity tracking events
++        // Since this event cannot be cancelled, we should call it here to catch all "un-tracks"
++        if (io.papermc.paper.event.player.PlayerUntrackEntityEvent.getHandlerList().getRegisteredListeners().length > 0) {
++            new io.papermc.paper.event.player.PlayerUntrackEntityEvent(serverPlayer.getBukkitEntity(), this.getBukkitEntity()).callEvent();
++        }
++        // Paper end - entity tracking events
+     }
+ 
+     public float rotate(Rotation transformRotation) {
+@@ -3129,7 +_,7 @@
+     }
+ 
+     @Nullable
+-    public LivingEntity getControllingPassenger() {
++    public net.minecraft.world.entity.LivingEntity getControllingPassenger() {
+         return null;
+     }
+ 
+@@ -3161,21 +_,32 @@
+     }
+ 
+     private Stream<Entity> getIndirectPassengersStream() {
++        if (this.passengers.isEmpty()) { return Stream.of(); } // Paper - Optimize indirect passenger iteration
+         return this.passengers.stream().flatMap(Entity::getSelfAndPassengers);
+     }
+ 
+     @Override
+     public Stream<Entity> getSelfAndPassengers() {
++        if (this.passengers.isEmpty()) { return Stream.of(this); } // Paper - Optimize indirect passenger iteration
+         return Stream.concat(Stream.of(this), this.getIndirectPassengersStream());
+     }
+ 
+     @Override
+     public Stream<Entity> getPassengersAndSelf() {
++        if (this.passengers.isEmpty()) { return Stream.of(this); } // Paper - Optimize indirect passenger iteration
+         return Stream.concat(this.passengers.stream().flatMap(Entity::getPassengersAndSelf), Stream.of(this));
+     }
+ 
+     public Iterable<Entity> getIndirectPassengers() {
+-        return () -> this.getIndirectPassengersStream().iterator();
++        // Paper start - Optimize indirect passenger iteration
++        if (this.passengers.isEmpty()) { return ImmutableList.of(); }
++        ImmutableList.Builder<Entity> indirectPassengers = ImmutableList.builder();
++        for (Entity passenger : this.passengers) {
++            indirectPassengers.add(passenger);
++            indirectPassengers.addAll(passenger.getIndirectPassengers());
++        }
++        return indirectPassengers.build();
++        // Paper end - Optimize indirect passenger iteration
+     }
+ 
+     public int countPlayerPassengers() {
+@@ -3183,6 +_,7 @@
+     }
+ 
+     public boolean hasExactlyOnePlayerPassenger() {
++        if (this.passengers.isEmpty()) { return false; } // Paper - Optimize indirect passenger iteration
+         return this.countPlayerPassengers() == 1;
+     }
+ 
+@@ -3260,9 +_,38 @@
+         return 1;
+     }
+ 
++    // CraftBukkit start
++    private final CommandSource commandSource = new CommandSource() {
++
++        @Override
++        public void sendSystemMessage(Component message) {
++        }
++
++        @Override
++        public org.bukkit.command.CommandSender getBukkitSender(CommandSourceStack wrapper) {
++            return Entity.this.getBukkitEntity();
++        }
++
++        @Override
++        public boolean acceptsSuccess() {
++            return ((ServerLevel) Entity.this.level()).getGameRules().getBoolean(net.minecraft.world.level.GameRules.RULE_SENDCOMMANDFEEDBACK);
++        }
++
++        @Override
++        public boolean acceptsFailure() {
++            return true;
++        }
++
++        @Override
++        public boolean shouldInformAdmins() {
++            return true;
++        }
++    };
++    // CraftBukkit end
++
+     public CommandSourceStack createCommandSourceStackForNameResolution(ServerLevel level) {
+         return new CommandSourceStack(
+-            CommandSource.NULL, this.position(), this.getRotationVector(), level, 0, this.getName().getString(), this.getDisplayName(), level.getServer(), this
++            this.commandSource, this.position(), this.getRotationVector(), level, 0, this.getName().getString(), this.getDisplayName(), level.getServer(), this // CraftBukkit
+         );
+     }
+ 
+@@ -3320,6 +_,11 @@
+                                     vec3 = vec3.add(flow);
+                                     i++;
+                                 }
++                                // CraftBukkit start - store last lava contact location
++                                if (fluidTag == FluidTags.LAVA) {
++                                    this.lastLavaContact = mutableBlockPos.immutable();
++                                }
++                                // CraftBukkit end
+                             }
+                         }
+                     }
+@@ -3417,7 +_,9 @@
+     }
+ 
+     public void setDeltaMovement(Vec3 deltaMovement) {
++        synchronized (this.posLock) { // Paper - detailed watchdog information
+         this.deltaMovement = deltaMovement;
++        } // Paper - detailed watchdog information
+     }
+ 
+     public void addDeltaMovement(Vec3 addend) {
+@@ -3480,9 +_,43 @@
+         return this.getZ((2.0 * this.random.nextDouble() - 1.0) * scale);
+     }
+ 
++    // Paper start - Block invalid positions and bounding box
++    public static boolean checkPosition(Entity entity, double newX, double newY, double newZ) {
++        if (Double.isFinite(newX) && Double.isFinite(newY) && Double.isFinite(newZ)) {
++            return true;
++        }
++
++        String entityInfo;
++        try {
++            entityInfo = entity.toString();
++        } catch (Exception ex) {
++            entityInfo = "[Entity info unavailable] ";
++        }
++        LOGGER.error("New entity position is invalid! Tried to set invalid position ({},{},{}) for entity {} located at {}, entity info: {}", newX, newY, newZ, entity.getClass().getName(), entity.position, entityInfo, new Throwable());
++        return false;
++    }
+     public final void setPosRaw(double x, double y, double z) {
++        this.setPosRaw(x, y, z, false);
++    }
++    public final void setPosRaw(double x, double y, double z, boolean forceBoundingBoxUpdate) {
++        if (!checkPosition(this, x, y, z)) {
++            return;
++        }
++        // Paper end - Block invalid positions and bounding box
++        // Paper start - Fix MC-4
++        if (this instanceof ItemEntity) {
++            if (io.papermc.paper.configuration.GlobalConfiguration.get().misc.fixEntityPositionDesync) {
++                // encode/decode from ClientboundMoveEntityPacket
++                x = Mth.lfloor(x * 4096.0) * (1 / 4096.0);
++                y = Mth.lfloor(y * 4096.0) * (1 / 4096.0);
++                z = Mth.lfloor(z * 4096.0) * (1 / 4096.0);
++            }
++        }
++        // Paper end - Fix MC-4
+         if (this.position.x != x || this.position.y != y || this.position.z != z) {
++            synchronized (this.posLock) { // Paper - detailed watchdog information
+             this.position = new Vec3(x, y, z);
++            } // Paper - detailed watchdog information
+             int floor = Mth.floor(x);
+             int floor1 = Mth.floor(y);
+             int floor2 = Mth.floor(z);
+@@ -3496,6 +_,12 @@
+ 
+             this.levelCallback.onMove();
+         }
++        // Paper start - Block invalid positions and bounding box; don't allow desync of pos and AABB
++        // hanging has its own special logic
++        if (!(this instanceof net.minecraft.world.entity.decoration.HangingEntity) && (forceBoundingBoxUpdate || this.position.x != x || this.position.y != y || this.position.z != z)) {
++            this.setBoundingBox(this.makeBoundingBox());
++        }
++        // Paper end - Block invalid positions and bounding box
+     }
+ 
+     public void checkDespawn() {
+@@ -3583,6 +_,15 @@
+ 
+     @Override
+     public final void setRemoved(Entity.RemovalReason removalReason) {
++        // CraftBukkit start - add Bukkit remove cause
++        this.setRemoved(removalReason, null);
++    }
++
++    @Override
++    public final void setRemoved(Entity.RemovalReason removalReason, org.bukkit.event.entity.EntityRemoveEvent.Cause cause) {
++        org.bukkit.craftbukkit.event.CraftEventFactory.callEntityRemoveEvent(this, cause);
++        // CraftBukkit end
++        final boolean alreadyRemoved = this.removalReason != null; // Paper - Folia schedulers
+         if (this.removalReason == null) {
+             this.removalReason = removalReason;
+         }
+@@ -3594,12 +_,28 @@
+         this.getPassengers().forEach(Entity::stopRiding);
+         this.levelCallback.onRemove(removalReason);
+         this.onRemoval(removalReason);
++        // Paper start - Folia schedulers
++        if (!(this instanceof ServerPlayer) && removalReason != RemovalReason.CHANGED_DIMENSION && !alreadyRemoved) {
++            // Players need to be special cased, because they are regularly removed from the world
++            this.retireScheduler();
++        }
++        // Paper end - Folia schedulers
+     }
+ 
+     public void unsetRemoved() {
+         this.removalReason = null;
+     }
+ 
++    // Paper start - Folia schedulers
++    /**
++     * Invoked only when the entity is truly removed from the server, never to be added to any world.
++     */
++    public final void retireScheduler() {
++        // we need to force create the bukkit entity so that the scheduler can be retired...
++        this.getBukkitEntity().taskScheduler.retire();
++    }
++    // Paper end - Folia schedulers
++
+     @Override
+     public void setLevelCallback(EntityInLevelCallback levelCallback) {
+         this.levelCallback = levelCallback;
+@@ -3723,4 +_,14 @@
+             return this.save;
+         }
+     }
++
++    // Paper start - Expose entity id counter
++    public static int nextEntityId() {
++        return ENTITY_COUNTER.incrementAndGet();
++    }
++
++    public boolean isTicking() {
++        return ((ServerLevel) this.level).isPositionEntityTicking(this.blockPosition());
++    }
++    // Paper end - Expose entity id counter
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/EntitySelector.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/EntitySelector.java.patch
new file mode 100644
index 0000000000..dcb0fd0d64
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/EntitySelector.java.patch
@@ -0,0 +1,75 @@
+--- a/net/minecraft/world/entity/EntitySelector.java
++++ b/net/minecraft/world/entity/EntitySelector.java
+@@ -16,6 +_,23 @@
+     public static final Predicate<Entity> NO_SPECTATORS = entity -> !entity.isSpectator();
+     public static final Predicate<Entity> CAN_BE_COLLIDED_WITH = NO_SPECTATORS.and(Entity::canBeCollidedWith);
+     public static final Predicate<Entity> CAN_BE_PICKED = NO_SPECTATORS.and(Entity::isPickable);
++    // Paper start - Ability to control player's insomnia and phantoms
++    public static Predicate<Player> IS_INSOMNIAC = (player) -> {
++        net.minecraft.server.level.ServerPlayer serverPlayer = (net.minecraft.server.level.ServerPlayer) player;
++        int playerInsomniaTicks = serverPlayer.level().paperConfig().entities.behavior.playerInsomniaStartTicks;
++
++        if (playerInsomniaTicks <= 0) {
++            return false;
++        }
++
++        return net.minecraft.util.Mth.clamp(serverPlayer.getStats().getValue(net.minecraft.stats.Stats.CUSTOM.get(net.minecraft.stats.Stats.TIME_SINCE_REST)), 1, Integer.MAX_VALUE) >= playerInsomniaTicks;
++    };
++    // Paper end - Ability to control player's insomnia and phantoms
++    // Paper start - Affects Spawning API
++    public static final Predicate<Entity> PLAYER_AFFECTS_SPAWNING = (entity) -> {
++        return !entity.isSpectator() && entity.isAlive() && entity instanceof Player player && player.affectsSpawning;
++    };
++    // Paper end - Affects Spawning API
+ 
+     private EntitySelector() {
+     }
+@@ -26,29 +_,34 @@
+     }
+ 
+     public static Predicate<Entity> pushableBy(Entity entity) {
++        // Paper start - Climbing should not bypass cramming gamerule
++        return pushable(entity, false);
++    }
++    public static Predicate<Entity> pushable(Entity entity, boolean ignoreClimbing) {
++        // Paper end - Climbing should not bypass cramming gamerule
+         Team team = entity.getTeam();
+         Team.CollisionRule collisionRule = team == null ? Team.CollisionRule.ALWAYS : team.getCollisionRule();
+         return (Predicate<Entity>)(collisionRule == Team.CollisionRule.NEVER
+             ? Predicates.alwaysFalse()
+             : NO_SPECTATORS.and(
+-                pushedEntity -> {
+-                    if (!pushedEntity.isPushable()) {
++            pushedEntity -> {
++                if (!pushedEntity.isCollidable(ignoreClimbing) || !pushedEntity.canCollideWithBukkit(entity) || !entity.canCollideWithBukkit(pushedEntity)) { // CraftBukkit - collidable API // Paper - Climbing should not bypass cramming gamerule
++                    return false;
++                } else if (!entity.level().isClientSide || pushedEntity instanceof Player && ((Player)pushedEntity).isLocalPlayer()) {
++                    Team team1 = pushedEntity.getTeam();
++                    Team.CollisionRule collisionRule1 = team1 == null ? Team.CollisionRule.ALWAYS : team1.getCollisionRule();
++                    if (collisionRule1 == Team.CollisionRule.NEVER || (pushedEntity instanceof Player && !io.papermc.paper.configuration.GlobalConfiguration.get().collisions.enablePlayerCollisions)) { // Paper - Configurable player collision
+                         return false;
+-                    } else if (!entity.level().isClientSide || pushedEntity instanceof Player && ((Player)pushedEntity).isLocalPlayer()) {
+-                        Team team1 = pushedEntity.getTeam();
+-                        Team.CollisionRule collisionRule1 = team1 == null ? Team.CollisionRule.ALWAYS : team1.getCollisionRule();
+-                        if (collisionRule1 == Team.CollisionRule.NEVER) {
+-                            return false;
+-                        } else {
+-                            boolean flag = team != null && team.isAlliedTo(team1);
+-                            return (collisionRule != Team.CollisionRule.PUSH_OWN_TEAM && collisionRule1 != Team.CollisionRule.PUSH_OWN_TEAM || !flag)
+-                                && (collisionRule != Team.CollisionRule.PUSH_OTHER_TEAMS && collisionRule1 != Team.CollisionRule.PUSH_OTHER_TEAMS || flag);
+-                        }
+                     } else {
+-                        return false;
++                        boolean flag = team != null && team.isAlliedTo(team1);
++                        return (collisionRule != Team.CollisionRule.PUSH_OWN_TEAM && collisionRule1 != Team.CollisionRule.PUSH_OWN_TEAM || !flag)
++                            && (collisionRule != Team.CollisionRule.PUSH_OTHER_TEAMS && collisionRule1 != Team.CollisionRule.PUSH_OTHER_TEAMS || flag);
+                     }
++                } else {
++                    return false;
+                 }
+-            ));
++            }
++        ));
+     }
+ 
+     public static Predicate<Entity> notRiding(Entity entity) {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/EntityType.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/EntityType.java.patch
new file mode 100644
index 0000000000..df8e7e81b0
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/EntityType.java.patch
@@ -0,0 +1,155 @@
+--- a/net/minecraft/world/entity/EntityType.java
++++ b/net/minecraft/world/entity/EntityType.java
+@@ -176,6 +_,7 @@
+ import net.minecraft.world.phys.Vec3;
+ import net.minecraft.world.phys.shapes.Shapes;
+ import net.minecraft.world.phys.shapes.VoxelShape;
++import org.bukkit.event.entity.CreatureSpawnEvent;
+ import org.slf4j.Logger;
+ 
+ public class EntityType<T extends Entity> implements FeatureElement, EntityTypeTest<Entity, T> {
+@@ -215,7 +_,7 @@
+             .fireImmune()
+             .sized(6.0F, 0.5F)
+             .clientTrackingRange(10)
+-            .updateInterval(Integer.MAX_VALUE)
++            .updateInterval(10) // CraftBukkit - SPIGOT-3729: track area effect clouds
+     );
+     public static final EntityType<Armadillo> ARMADILLO = register(
+         "armadillo", EntityType.Builder.of(Armadillo::new, MobCategory.CREATURE).sized(0.7F, 0.65F).eyeHeight(0.26F).clientTrackingRange(10)
+@@ -1132,6 +_,22 @@
+         boolean shouldOffsetY,
+         boolean shouldOffsetYMore
+     ) {
++        // CraftBukkit start
++        return this.spawn(level, spawnedFrom, player, pos, reason, shouldOffsetY, shouldOffsetYMore, reason == EntitySpawnReason.DISPENSER ? org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DISPENSE_EGG : org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.SPAWNER_EGG); // Paper - use correct spawn reason for dispenser spawn eggs
++    }
++
++    @Nullable
++    public T spawn(
++        ServerLevel level,
++        @Nullable ItemStack spawnedFrom,
++        @Nullable Player player,
++        BlockPos pos,
++        EntitySpawnReason reason,
++        boolean shouldOffsetY,
++        boolean shouldOffsetYMore,
++        org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason createSpawnReason
++    ) {
++        // CraftBukkit end
+         Consumer<T> consumer;
+         if (spawnedFrom != null) {
+             consumer = createDefaultStackConfig(level, spawnedFrom, player);
+@@ -1139,7 +_,7 @@
+             consumer = entity -> {};
+         }
+ 
+-        return this.spawn(level, consumer, pos, reason, shouldOffsetY, shouldOffsetYMore);
++        return this.spawn(level, consumer, pos, reason, shouldOffsetY, shouldOffsetYMore, createSpawnReason); // CraftBukkit
+     }
+ 
+     public static <T extends Entity> Consumer<T> createDefaultStackConfig(Level level, ItemStack spawnedFrom, @Nullable Player player) {
+@@ -1159,19 +_,54 @@
+         Consumer<T> consumer, Level level, ItemStack spawnedFrom, @Nullable Player player
+     ) {
+         CustomData customData = spawnedFrom.getOrDefault(DataComponents.ENTITY_DATA, CustomData.EMPTY);
+-        return !customData.isEmpty() ? consumer.andThen(entity -> updateCustomEntityTag(level, player, entity, customData)) : consumer;
++        // CraftBukkit start - SPIGOT-5665
++        return !customData.isEmpty() ? consumer.andThen(entity -> {
++            try {
++                updateCustomEntityTag(level, player, entity, customData);
++            } catch (Throwable t) {
++                EntityType.LOGGER.warn("Error loading spawn egg NBT", t);
++            }
++        }) : consumer;
++        // CraftBukkit end
+     }
+ 
+     @Nullable
+     public T spawn(ServerLevel level, BlockPos pos, EntitySpawnReason reason) {
+-        return this.spawn(level, null, pos, reason, false, false);
++        // CraftBukkit start
++        return this.spawn(level, pos, reason, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT);
++    }
++    @Nullable
++    public T spawn(ServerLevel level, BlockPos pos, EntitySpawnReason reason, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason creatureSpawnReason) {
++        return this.spawn(level, null, pos, reason, false, false, creatureSpawnReason);
++        // CraftBukkit End
+     }
+ 
+     @Nullable
+     public T spawn(ServerLevel level, @Nullable Consumer<T> consumer, BlockPos pos, EntitySpawnReason reason, boolean shouldOffsetY, boolean shouldOffsetYMore) {
++        // CraftBukkit start
++        return this.spawn(level, consumer, pos, reason, shouldOffsetY, shouldOffsetYMore, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT);
++    }
++    @Nullable
++    public T spawn(ServerLevel level, @Nullable Consumer<T> consumer, BlockPos pos, EntitySpawnReason reason, boolean shouldOffsetY, boolean shouldOffsetYMore, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason creatureSpawnReason) {
++        // CraftBukkit end
++        // Paper start - PreCreatureSpawnEvent
++        com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent event = new com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent(
++            io.papermc.paper.util.MCUtil.toLocation(level, pos),
++            org.bukkit.craftbukkit.entity.CraftEntityType.minecraftToBukkit(this),
++            creatureSpawnReason
++        );
++        if (!event.callEvent()) {
++            return null;
++        }
++        // Paper end - PreCreatureSpawnEvent
+         T entity = this.create(level, consumer, pos, reason, shouldOffsetY, shouldOffsetYMore);
+         if (entity != null) {
+-            level.addFreshEntityWithPassengers(entity);
++            // CraftBukkit start
++            level.addFreshEntityWithPassengers(entity, creatureSpawnReason);
++            if (entity.isRemoved()) {
++                return null; // Don't return an entity when CreatureSpawnEvent is canceled
++            }
++            // CraftBukkit end
+             if (entity instanceof Mob mob) {
+                 mob.playAmbientSound();
+             }
+@@ -1225,6 +_,15 @@
+             EntityType<?> entityType = customData.parseEntityType(server.registryAccess(), Registries.ENTITY_TYPE);
+             if (entity.getType() == entityType) {
+                 if (level.isClientSide || !entity.getType().onlyOpCanSetNbt() || player != null && server.getPlayerList().isOp(player.getGameProfile())) {
++                    // Paper start - filter out protected tags
++                    if (player == null || !player.getBukkitEntity().hasPermission("minecraft.nbt.place")) {
++                        customData = customData.update((compound) -> {
++                            for (net.minecraft.commands.arguments.NbtPathArgument.NbtPath tag : level.paperConfig().entities.spawning.filteredEntityTagNbtPaths) {
++                                tag.remove(compound);
++                            }
++                        });
++                    }
++                    // Paper end - filter out protected tags
+                     customData.loadInto(entity);
+                 }
+             }
+@@ -1296,9 +_,19 @@
+     }
+ 
+     public static Optional<Entity> create(CompoundTag tag, Level level, EntitySpawnReason spawnReason) {
++        // Paper start - Don't fire sync event during generation
++        return create(tag, level, spawnReason, false);
++    }
++    public static Optional<Entity> create(CompoundTag tag, Level level, EntitySpawnReason spawnReason, boolean generation) {
++        // Paper end - Don't fire sync event during generation
+         return Util.ifElse(
+             by(tag).map(entityType -> entityType.create(level, spawnReason)),
+-            entity -> entity.load(tag),
++            // Paper start - Don't fire sync event during generation
++            entity -> {
++                if (generation) entity.generation = true; // Paper - Don't fire sync event during generation
++                entity.load(tag);
++            },
++            // Paper end - Don't fire sync event during generation
+             () -> LOGGER.warn("Skipping Entity with id {}", tag.getString("id"))
+         );
+     }
+@@ -1325,7 +_,7 @@
+     }
+ 
+     public static Optional<EntityType<?>> by(CompoundTag tag) {
+-        return BuiltInRegistries.ENTITY_TYPE.getOptional(ResourceLocation.parse(tag.getString("id")));
++        return BuiltInRegistries.ENTITY_TYPE.getOptional(ResourceLocation.tryParse(tag.getString("id"))); // Paper - Validate ResourceLocation
+     }
+ 
+     @Nullable
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ExperienceOrb.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ExperienceOrb.java.patch
similarity index 50%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/ExperienceOrb.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/ExperienceOrb.java.patch
index 2e0228c9d3..69d13b3079 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ExperienceOrb.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ExperienceOrb.java.patch
@@ -1,9 +1,9 @@
 --- a/net/minecraft/world/entity/ExperienceOrb.java
 +++ b/net/minecraft/world/entity/ExperienceOrb.java
-@@ -24,6 +24,13 @@
- import net.minecraft.world.level.entity.EntityTypeTest;
+@@ -24,6 +_,14 @@
  import net.minecraft.world.phys.AABB;
  import net.minecraft.world.phys.Vec3;
+ 
 +// CraftBukkit start
 +import org.bukkit.craftbukkit.event.CraftEventFactory;
 +import org.bukkit.event.entity.EntityRemoveEvent;
@@ -11,20 +11,22 @@
 +import org.bukkit.event.entity.EntityTargetEvent;
 +import org.bukkit.event.player.PlayerExpCooldownChangeEvent;
 +// CraftBukkit end
- 
++
  public class ExperienceOrb extends Entity {
- 
-@@ -37,9 +44,63 @@
+     private static final int LIFETIME = 6000;
+     private static final int ENTITY_SCAN_PERIOD = 20;
+@@ -35,9 +_,63 @@
      public int value;
-     public int count;
+     public int count = 1;
      private Player followingPlayer;
+-
 +    // Paper start
 +    @javax.annotation.Nullable
 +    public java.util.UUID sourceEntityId;
 +    @javax.annotation.Nullable
 +    public java.util.UUID triggerEntityId;
 +    public org.bukkit.entity.ExperienceOrb.SpawnReason spawnReason = org.bukkit.entity.ExperienceOrb.SpawnReason.UNKNOWN;
- 
++
 +    private void loadPaperNBT(CompoundTag tag) {
 +        if (!tag.contains("Paper.ExpData", net.minecraft.nbt.Tag.TAG_COMPOUND)) {
 +            return;
@@ -61,24 +63,24 @@
 +
 +    @io.papermc.paper.annotation.DoNotUse
 +    @Deprecated
-     public ExperienceOrb(Level world, double x, double y, double z, int amount) {
-+        this(world, x, y, z, amount, null, null);
+     public ExperienceOrb(Level level, double x, double y, double z, int value) {
++        this(level, x, y, z, value, null, null);
 +    }
 +
-+    public ExperienceOrb(Level world, double x, double y, double z, int amount, @javax.annotation.Nullable org.bukkit.entity.ExperienceOrb.SpawnReason reason, @javax.annotation.Nullable Entity triggerId) {
-+        this(world, x, y, z, amount, reason, triggerId, null);
++    public ExperienceOrb(Level level, double x, double y, double z, int value, @javax.annotation.Nullable org.bukkit.entity.ExperienceOrb.SpawnReason reason, @javax.annotation.Nullable Entity triggerId) {
++        this(level, x, y, z, value, reason, triggerId, null);
 +    }
 +
-+    public ExperienceOrb(Level world, double x, double y, double z, int amount, @javax.annotation.Nullable org.bukkit.entity.ExperienceOrb.SpawnReason reason, @javax.annotation.Nullable Entity triggerId, @javax.annotation.Nullable Entity sourceId) {
-         this(EntityType.EXPERIENCE_ORB, world);
++    public ExperienceOrb(Level level, double x, double y, double z, int value, @javax.annotation.Nullable org.bukkit.entity.ExperienceOrb.SpawnReason reason, @javax.annotation.Nullable Entity triggerId, @javax.annotation.Nullable Entity sourceId) {
+         this(EntityType.EXPERIENCE_ORB, level);
 +        this.sourceEntityId = sourceId != null ? sourceId.getUUID() : null;
 +        this.triggerEntityId = triggerId != null ? triggerId.getUUID() : null;
 +        this.spawnReason = reason != null ? reason : org.bukkit.entity.ExperienceOrb.SpawnReason.UNKNOWN;
 +        // Paper end
          this.setPos(x, y, z);
-         this.setYRot((float) (this.random.nextDouble() * 360.0D));
-         this.setDeltaMovement((this.random.nextDouble() * 0.20000000298023224D - 0.10000000149011612D) * 2.0D, this.random.nextDouble() * 0.2D * 2.0D, (this.random.nextDouble() * 0.20000000298023224D - 0.10000000149011612D) * 2.0D);
-@@ -68,6 +129,7 @@
+         this.setYRot((float)(this.random.nextDouble() * 360.0));
+         this.setDeltaMovement(
+@@ -67,6 +_,7 @@
      @Override
      public void tick() {
          super.tick();
@@ -86,7 +88,7 @@
          this.xo = this.getX();
          this.yo = this.getY();
          this.zo = this.getZ();
-@@ -93,7 +155,22 @@
+@@ -92,7 +_,22 @@
              this.followingPlayer = null;
          }
  
@@ -107,161 +109,156 @@
 +
 +        if (this.followingPlayer != null && !cancelled) {
 +            // CraftBukkit end
-             Vec3 vec3d = new Vec3(this.followingPlayer.getX() - this.getX(), this.followingPlayer.getY() + (double) this.followingPlayer.getEyeHeight() / 2.0D - this.getY(), this.followingPlayer.getZ() - this.getZ());
-             double d0 = vec3d.lengthSqr();
+             Vec3 vec3 = new Vec3(
+                 this.followingPlayer.getX() - this.getX(),
+                 this.followingPlayer.getY() + this.followingPlayer.getEyeHeight() / 2.0 - this.getY(),
+@@ -120,7 +_,7 @@
  
-@@ -121,7 +198,7 @@
- 
-         ++this.age;
+         this.age++;
          if (this.age >= 6000) {
 -            this.discard();
 +            this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
          }
- 
-     }
-@@ -150,18 +227,27 @@
      }
  
-     public static void award(ServerLevel world, Vec3 pos, int amount) {
+@@ -143,16 +_,25 @@
+     }
+ 
+     public static void award(ServerLevel level, Vec3 pos, int amount) {
 +        // Paper start - add reasons for orbs
-+        award(world, pos, amount, null, null, null);
++        award(level, pos, amount, null, null, null);
 +    }
-+    public static void award(ServerLevel world, Vec3 pos, int amount, org.bukkit.entity.ExperienceOrb.SpawnReason reason, Entity triggerId) {
-+        award(world, pos, amount, reason, triggerId, null);
++    public static void award(ServerLevel level, Vec3 pos, int amount, org.bukkit.entity.ExperienceOrb.SpawnReason reason, Entity triggerId) {
++        award(level, pos, amount, reason, triggerId, null);
 +    }
-+    public static void award(ServerLevel world, Vec3 pos, int amount, org.bukkit.entity.ExperienceOrb.SpawnReason reason, Entity triggerId, Entity sourceId) {
++    public static void award(ServerLevel level, Vec3 pos, int amount, org.bukkit.entity.ExperienceOrb.SpawnReason reason, Entity triggerId, Entity sourceId) {
 +        // Paper end - add reasons for orbs
          while (amount > 0) {
-             int j = ExperienceOrb.getExperienceValue(amount);
- 
-             amount -= j;
-             if (!ExperienceOrb.tryMergeToExisting(world, pos, j)) {
--                world.addFreshEntity(new ExperienceOrb(world, pos.x(), pos.y(), pos.z(), j));
-+                world.addFreshEntity(new ExperienceOrb(world, pos.x(), pos.y(), pos.z(), j, reason, triggerId, sourceId)); // Paper - add reason
+             int experienceValue = getExperienceValue(amount);
+             amount -= experienceValue;
+             if (!tryMergeToExisting(level, pos, experienceValue)) {
+-                level.addFreshEntity(new ExperienceOrb(level, pos.x(), pos.y(), pos.z(), experienceValue));
++                level.addFreshEntity(new ExperienceOrb(level, pos.x(), pos.y(), pos.z(), experienceValue, reason, triggerId, sourceId)); // Paper - add reason
              }
          }
- 
      }
  
-     private static boolean tryMergeToExisting(ServerLevel world, Vec3 pos, int amount) {
+     private static boolean tryMergeToExisting(ServerLevel level, Vec3 pos, int amount) {
 +        // Paper - TODO some other event for this kind of merge
-         AABB axisalignedbb = AABB.ofSize(pos, 1.0D, 1.0D, 1.0D);
-         int j = world.getRandom().nextInt(40);
-         List<ExperienceOrb> list = world.getEntities(EntityTypeTest.forClass(ExperienceOrb.class), axisalignedbb, (entityexperienceorb) -> {
-@@ -188,9 +274,14 @@
+         AABB aabb = AABB.ofSize(pos, 1.0, 1.0, 1.0);
+         int randomInt = level.getRandom().nextInt(40);
+         List<ExperienceOrb> entities = level.getEntities(EntityTypeTest.forClass(ExperienceOrb.class), aabb, orb -> canMerge(orb, randomInt, amount));
+@@ -175,9 +_,14 @@
      }
  
-     private void merge(ExperienceOrb other) {
+     private void merge(ExperienceOrb orb) {
 +        // Paper start - call orb merge event
-+        if (!new com.destroystokyo.paper.event.entity.ExperienceOrbMergeEvent((org.bukkit.entity.ExperienceOrb) this.getBukkitEntity(), (org.bukkit.entity.ExperienceOrb) other.getBukkitEntity()).callEvent()) {
++        if (!new com.destroystokyo.paper.event.entity.ExperienceOrbMergeEvent((org.bukkit.entity.ExperienceOrb) this.getBukkitEntity(), (org.bukkit.entity.ExperienceOrb) orb.getBukkitEntity()).callEvent()) {
 +            return;
 +        }
 +        // Paper end - call orb merge event
-         this.count += other.count;
-         this.age = Math.min(this.age, other.age);
--        other.discard();
-+        other.discard(EntityRemoveEvent.Cause.MERGE); // CraftBukkit - add Bukkit remove cause
+         this.count = this.count + orb.count;
+         this.age = Math.min(this.age, orb.age);
+-        orb.discard();
++        orb.discard(EntityRemoveEvent.Cause.MERGE); // CraftBukkit - add Bukkit remove cause
      }
  
      private void setUnderwaterMovement() {
-@@ -215,7 +306,7 @@
+@@ -202,7 +_,7 @@
              this.markHurt();
-             this.health = (int) ((float) this.health - amount);
+             this.health = (int)(this.health - amount);
              if (this.health <= 0) {
 -                this.discard();
 +                this.discard(EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause
              }
  
              return true;
-@@ -226,33 +317,35 @@
-     public void addAdditionalSaveData(CompoundTag nbt) {
-         nbt.putShort("Health", (short) this.health);
-         nbt.putShort("Age", (short) this.age);
--        nbt.putShort("Value", (short) this.value);
-+        nbt.putInt("Value", this.value); // Paper - save as Integer
-         nbt.putInt("Count", this.count);
-+        this.savePaperNBT(nbt); // Paper
+@@ -213,32 +_,34 @@
+     public void addAdditionalSaveData(CompoundTag compound) {
+         compound.putShort("Health", (short)this.health);
+         compound.putShort("Age", (short)this.age);
+-        compound.putShort("Value", (short)this.value);
++        compound.putInt("Value", this.value); // Paper - save as Integer
+         compound.putInt("Count", this.count);
++        this.savePaperNBT(compound); // Paper
      }
  
      @Override
-     public void readAdditionalSaveData(CompoundTag nbt) {
-         this.health = nbt.getShort("Health");
-         this.age = nbt.getShort("Age");
--        this.value = nbt.getShort("Value");
-+        this.value = nbt.getInt("Value"); // Paper - load as Integer
-         this.count = Math.max(nbt.getInt("Count"), 1);
-+        this.loadPaperNBT(nbt); // Paper
+     public void readAdditionalSaveData(CompoundTag compound) {
+         this.health = compound.getShort("Health");
+         this.age = compound.getShort("Age");
+-        this.value = compound.getShort("Value");
++        this.value = compound.getInt("Value"); // Paper - load as Integer
+         this.count = Math.max(compound.getInt("Count"), 1);
++        this.loadPaperNBT(compound); // Paper
      }
  
      @Override
-     public void playerTouch(Player player) {
-         if (player instanceof ServerPlayer entityplayer) {
--            if (player.takeXpDelay == 0) {
--                player.takeXpDelay = 2;
-+            if (player.takeXpDelay == 0 && new com.destroystokyo.paper.event.player.PlayerPickupExperienceEvent(entityplayer.getBukkitEntity(), (org.bukkit.entity.ExperienceOrb) this.getBukkitEntity()).callEvent()) { // Paper - PlayerPickupExperienceEvent
-+                player.takeXpDelay = CraftEventFactory.callPlayerXpCooldownEvent(player, 2, PlayerExpCooldownChangeEvent.ChangeReason.PICKUP_ORB).getNewCooldown(); // CraftBukkit - entityhuman.takeXpDelay = 2;
-                 player.take(this, 1);
-                 int i = this.repairPlayerItems(entityplayer, this.value);
- 
+     public void playerTouch(Player entity) {
+         if (entity instanceof ServerPlayer serverPlayer) {
+-            if (entity.takeXpDelay == 0) {
+-                entity.takeXpDelay = 2;
++            if (entity.takeXpDelay == 0 && new com.destroystokyo.paper.event.player.PlayerPickupExperienceEvent(serverPlayer.getBukkitEntity(), (org.bukkit.entity.ExperienceOrb) this.getBukkitEntity()).callEvent()) { // Paper - PlayerPickupExperienceEvent
++                entity.takeXpDelay = CraftEventFactory.callPlayerXpCooldownEvent(entity, 2, PlayerExpCooldownChangeEvent.ChangeReason.PICKUP_ORB).getNewCooldown(); // CraftBukkit - entityhuman.takeXpDelay = 2;
+                 entity.take(this, 1);
+                 int i = this.repairPlayerItems(serverPlayer, this.value);
                  if (i > 0) {
--                    player.giveExperiencePoints(i);
-+                    player.giveExperiencePoints(CraftEventFactory.callPlayerExpChangeEvent(player, this).getAmount()); // CraftBukkit - this.value -> event.getAmount() // Paper - supply experience orb object
+-                    entity.giveExperiencePoints(i);
++                    entity.giveExperiencePoints(CraftEventFactory.callPlayerExpChangeEvent(entity, this).getAmount()); // CraftBukkit - this.value -> event.getAmount() // Paper - supply experience orb object
                  }
  
-                 --this.count;
+                 this.count--;
                  if (this.count == 0) {
 -                    this.discard();
 +                    this.discard(EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
                  }
              }
- 
-@@ -266,12 +359,23 @@
-             ItemStack itemstack = ((EnchantedItemInUse) optional.get()).itemStack();
-             int j = EnchantmentHelper.modifyDurabilityToRepairFromXp(player.serverLevel(), itemstack, amount);
-             int k = Math.min(j, itemstack.getDamageValue());
+         }
+@@ -252,10 +_,21 @@
+             ItemStack itemStack = randomItemWith.get().itemStack();
+             int i = EnchantmentHelper.modifyDurabilityToRepairFromXp(player.serverLevel(), itemStack, value);
+             int min = Math.min(i, itemStack.getDamageValue());
 +            // CraftBukkit start
 +            // Paper start - mending event
-+            final int consumedExperience = k > 0 ? k * amount / j : 0;
-+            org.bukkit.event.player.PlayerItemMendEvent event = CraftEventFactory.callPlayerItemMendEvent(player, this, itemstack, optional.get().inSlot(), k, consumedExperience);
++            final int consumedExperience = min > 0 ? min * value / i : 0;
++            org.bukkit.event.player.PlayerItemMendEvent event = CraftEventFactory.callPlayerItemMendEvent(player, this, itemStack, randomItemWith.get().inSlot(), min, consumedExperience);
 +            // Paper end - mending event
-+            k = event.getRepairAmount();
++            min = event.getRepairAmount();
 +            if (event.isCancelled()) {
-+                return amount;
++                return value;
 +            }
 +            // CraftBukkit end
- 
-             itemstack.setDamageValue(itemstack.getDamageValue() - k);
-             if (k > 0) {
--                int l = amount - k * amount / j;
-+                int l = amount - k * amount / j; // Paper - diff on change - expand PlayerMendEvents
- 
-                 if (l > 0) {
+             itemStack.setDamageValue(itemStack.getDamageValue() - min);
+             if (min > 0) {
+-                int i1 = value - min * value / i;
++                int i1 = value - min * value / i; // Paper - diff on change - expand PlayerMendEvents
+                 if (i1 > 0) {
 +                    // this.value = l; // CraftBukkit - update exp value of orb for PlayerItemMendEvent calls // Paper - the value field should not be mutated here because it doesn't take "count" into account
-                     return this.repairPlayerItems(player, l);
+                     return this.repairPlayerItems(player, i1);
                  }
              }
-@@ -291,6 +395,24 @@
+@@ -295,6 +_,24 @@
      }
  
-     public static int getExperienceValue(int value) {
+     public static int getExperienceValue(int expValue) {
 +        // CraftBukkit start
-+        if (value > 162670129) return value - 100000;
-+        if (value > 81335063) return 81335063;
-+        if (value > 40667527) return 40667527;
-+        if (value > 20333759) return 20333759;
-+        if (value > 10166857) return 10166857;
-+        if (value > 5083423) return 5083423;
-+        if (value > 2541701) return 2541701;
-+        if (value > 1270849) return 1270849;
-+        if (value > 635413) return 635413;
-+        if (value > 317701) return 317701;
-+        if (value > 158849) return 158849;
-+        if (value > 79423) return 79423;
-+        if (value > 39709) return 39709;
-+        if (value > 19853) return 19853;
-+        if (value > 9923) return 9923;
-+        if (value > 4957) return 4957;
++        if (expValue > 162670129) return expValue - 100000;
++        if (expValue > 81335063) return 81335063;
++        if (expValue > 40667527) return 40667527;
++        if (expValue > 20333759) return 20333759;
++        if (expValue > 10166857) return 10166857;
++        if (expValue > 5083423) return 5083423;
++        if (expValue > 2541701) return 2541701;
++        if (expValue > 1270849) return 1270849;
++        if (expValue > 635413) return 635413;
++        if (expValue > 317701) return 317701;
++        if (expValue > 158849) return 158849;
++        if (expValue > 79423) return 79423;
++        if (expValue > 39709) return 39709;
++        if (expValue > 19853) return 19853;
++        if (expValue > 9923) return 9923;
++        if (expValue > 4957) return 4957;
 +        // CraftBukkit end
-         return value >= 2477 ? 2477 : (value >= 1237 ? 1237 : (value >= 617 ? 617 : (value >= 307 ? 307 : (value >= 149 ? 149 : (value >= 73 ? 73 : (value >= 37 ? 37 : (value >= 17 ? 17 : (value >= 7 ? 7 : (value >= 3 ? 3 : 1)))))))));
-     }
- 
+         if (expValue >= 2477) {
+             return 2477;
+         } else if (expValue >= 1237) {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/Interaction.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/Interaction.java.patch
new file mode 100644
index 0000000000..3d9857c808
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/Interaction.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/entity/Interaction.java
++++ b/net/minecraft/world/entity/Interaction.java
+@@ -130,9 +_,16 @@
+     @Override
+     public boolean skipAttackInteraction(Entity entity) {
+         if (entity instanceof Player player) {
++            // CraftBukkit start
++            DamageSource source = player.damageSources().playerAttack(player);
++            org.bukkit.event.entity.EntityDamageEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callNonLivingEntityDamageEvent(this, source, 1.0F, false);
++            if (event.isCancelled()) {
++                return true;
++            }
++            // CraftBukkit end
+             this.attack = new Interaction.PlayerAction(player.getUUID(), this.level().getGameTime());
+             if (player instanceof ServerPlayer serverPlayer) {
+-                CriteriaTriggers.PLAYER_HURT_ENTITY.trigger(serverPlayer, this, player.damageSources().generic(), 1.0F, 1.0F, false);
++                CriteriaTriggers.PLAYER_HURT_ENTITY.trigger(serverPlayer, this, player.damageSources().generic(), 1.0F, (float) event.getFinalDamage(), false); // CraftBukkit // Paper - use correct source and fix taken/dealt param order
+             }
+ 
+             return !this.getResponse();
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ItemBasedSteering.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ItemBasedSteering.java.patch
similarity index 84%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/ItemBasedSteering.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/ItemBasedSteering.java.patch
index 2f4eab4bc8..cd34eb69a7 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ItemBasedSteering.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ItemBasedSteering.java.patch
@@ -1,7 +1,7 @@
 --- a/net/minecraft/world/entity/ItemBasedSteering.java
 +++ b/net/minecraft/world/entity/ItemBasedSteering.java
-@@ -53,6 +53,14 @@
-         return (Integer) this.entityData.get(this.boostTimeAccessor);
+@@ -51,6 +_,14 @@
+         return this.entityData.get(this.boostTimeAccessor);
      }
  
 +    // CraftBukkit add setBoostTicks(int)
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/Leashable.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/Leashable.java.patch
new file mode 100644
index 0000000000..691174efb1
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/Leashable.java.patch
@@ -0,0 +1,98 @@
+--- a/net/minecraft/world/entity/Leashable.java
++++ b/net/minecraft/world/entity/Leashable.java
+@@ -56,7 +_,13 @@
+     @Nullable
+     private static Leashable.LeashData readLeashDataInternal(CompoundTag tag) {
+         if (tag.contains("leash", 10)) {
+-            return new Leashable.LeashData(Either.left(tag.getCompound("leash").getUUID("UUID")));
++            // Paper start
++            final CompoundTag leashTag = tag.getCompound("leash");
++            if (!leashTag.hasUUID("UUID")) {
++                return null;
++            }
++            return new Leashable.LeashData(Either.left(leashTag.getUUID("UUID")));
++            // Paper end
+         } else {
+             if (tag.contains("leash", 11)) {
+                 Either<UUID, BlockPos> either = NbtUtils.readBlockPos(tag, "leash").<Either<UUID, BlockPos>>map(Either::right).orElse(null);
+@@ -72,6 +_,11 @@
+     default void writeLeashData(CompoundTag tag, @Nullable Leashable.LeashData leashData) {
+         if (leashData != null) {
+             Either<UUID, BlockPos> either = leashData.delayedLeashInfo;
++            // CraftBukkit start - SPIGOT-7487: Don't save (and possible drop) leash, when the holder was removed by a plugin
++            if (leashData.leashHolder != null && leashData.leashHolder.pluginRemoved) {
++                return;
++            }
++            // CraftBukkit end
+             if (leashData.leashHolder instanceof LeashFenceKnotEntity leashFenceKnotEntity) {
+                 either = Either.right(leashFenceKnotEntity.getPos());
+             } else if (leashData.leashHolder != null) {
+@@ -104,7 +_,9 @@
+             }
+ 
+             if (entity.tickCount > 100) {
++                entity.forceDrops = true; // CraftBukkit
+                 entity.spawnAtLocation(serverLevel, Items.LEAD);
++                entity.forceDrops = false; // CraftBukkit
+                 entity.setLeashData(null);
+             }
+         }
+@@ -128,7 +_,9 @@
+             entity.onLeashRemoved();
+             if (entity.level() instanceof ServerLevel serverLevel) {
+                 if (dropItem) {
++                    entity.forceDrops = true; // CraftBukkit
+                     entity.spawnAtLocation(serverLevel, Items.LEAD);
++                    entity.forceDrops = false; // CraftBukkit
+                 }
+ 
+                 if (broadcastPacket) {
+@@ -146,7 +_,15 @@
+ 
+         if (leashData != null && leashData.leashHolder != null) {
+             if (!entity.isAlive() || !leashData.leashHolder.isAlive()) {
+-                if (level.getGameRules().getBoolean(GameRules.RULE_DOENTITYDROPS)) {
++                // Paper start - Expand EntityUnleashEvent
++                final org.bukkit.event.entity.EntityUnleashEvent event = new org.bukkit.event.entity.EntityUnleashEvent(
++                    entity.getBukkitEntity(),
++                    !entity.isAlive() ? org.bukkit.event.entity.EntityUnleashEvent.UnleashReason.PLAYER_UNLEASH : org.bukkit.event.entity.EntityUnleashEvent.UnleashReason.HOLDER_GONE,
++                    level.getGameRules().getBoolean(GameRules.RULE_DOENTITYDROPS) && !entity.pluginRemoved
++                );
++                event.callEvent();
++                if (event.isDropLeash()) { // CraftBukkit - SPIGOT-7487: Don't drop leash, when the holder was removed by a plugin
++                    // Paper end - Expand EntityUnleashEvent
+                     entity.dropLeash();
+                 } else {
+                     entity.removeLeash();
+@@ -160,7 +_,7 @@
+                     return;
+                 }
+ 
+-                if (f > 10.0) {
++                if (f > entity.level().paperConfig().misc.maxLeashDistance.or(LEASH_TOO_FAR_DIST)) { // Paper - Configurable max leash distance
+                     entity.leashTooFarBehaviour();
+                 } else if (f > 6.0) {
+                     entity.elasticRangeLeashBehaviour(leashHolder, f);
+@@ -177,7 +_,21 @@
+     }
+ 
+     default void leashTooFarBehaviour() {
+-        this.dropLeash();
++        // CraftBukkit start
++        boolean dropLeash = true; // Paper
++        if (this instanceof Entity entity) {
++            // Paper start - Expand EntityUnleashEvent
++            final org.bukkit.event.entity.EntityUnleashEvent event = new org.bukkit.event.entity.EntityUnleashEvent(entity.getBukkitEntity(), org.bukkit.event.entity.EntityUnleashEvent.UnleashReason.DISTANCE, true);
++            if (!event.callEvent()) return;
++            dropLeash = event.isDropLeash();
++        }
++        // CraftBukkit end
++        if (dropLeash) {
++            this.dropLeash();
++        } else {
++            this.removeLeash();
++        }
++        // Paper end - Expand EntityUnleashEvent
+     }
+ 
+     default void closeRangeLeashBehaviour(Entity entity) {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/LightningBolt.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/LightningBolt.java.patch
new file mode 100644
index 0000000000..ffaffc46db
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/LightningBolt.java.patch
@@ -0,0 +1,138 @@
+--- a/net/minecraft/world/entity/LightningBolt.java
++++ b/net/minecraft/world/entity/LightningBolt.java
+@@ -39,6 +_,7 @@
+     private ServerPlayer cause;
+     private final Set<Entity> hitEntities = Sets.newHashSet();
+     private int blocksSetOnFire;
++    public boolean isEffect; // Paper - Properly handle lightning effects api
+ 
+     public LightningBolt(EntityType<? extends LightningBolt> entityType, Level level) {
+         super(entityType, level);
+@@ -76,7 +_,7 @@
+     @Override
+     public void tick() {
+         super.tick();
+-        if (this.life == 2) {
++        if (!this.isEffect && this.life == 2) { // Paper - Properly handle lightning effects api
+             if (this.level().isClientSide()) {
+                 this.level()
+                     .playLocalSound(
+@@ -107,7 +_,7 @@
+                 }
+ 
+                 this.powerLightningRod();
+-                clearCopperOnLightningStrike(this.level(), this.getStrikePosition());
++                clearCopperOnLightningStrike(this.level(), this.getStrikePosition(), this); // Paper - Call EntityChangeBlockEvent
+                 this.gameEvent(GameEvent.LIGHTNING_STRIKE);
+             }
+         }
+@@ -130,7 +_,7 @@
+                     }
+                 }
+ 
+-                this.discard();
++                this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+             } else if (this.life < -this.random.nextInt(10)) {
+                 this.flashes--;
+                 this.life = 1;
+@@ -139,10 +_,10 @@
+             }
+         }
+ 
+-        if (this.life >= 0) {
++        if (this.life >= 0 && !this.isEffect) { // CraftBukkit - add !this.visualOnly // Paper - Properly handle lightning effects api
+             if (!(this.level() instanceof ServerLevel)) {
+                 this.level().setSkyFlashTime(2);
+-            } else if (!this.visualOnly) {
++            } else if (!this.visualOnly && !this.isEffect) { // Paper - Properly handle lightning effects api
+                 List<Entity> entities = this.level()
+                     .getEntities(
+                         this,
+@@ -172,22 +_,30 @@
+             BlockPos blockPos = this.blockPosition();
+             BlockState state = BaseFireBlock.getState(this.level(), blockPos);
+             if (this.level().getBlockState(blockPos).isAir() && state.canSurvive(this.level(), blockPos)) {
+-                this.level().setBlockAndUpdate(blockPos, state);
+-                this.blocksSetOnFire++;
++                // CraftBukkit start - add "!visualOnly"
++                if (!this.visualOnly && !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(this.level(), blockPos, this).isCancelled()) {
++                    this.level().setBlockAndUpdate(blockPos, state);
++                    this.blocksSetOnFire++;
++                }
++                // CraftBukkit end
+             }
+ 
+             for (int i = 0; i < extraIgnitions; i++) {
+                 BlockPos blockPos1 = blockPos.offset(this.random.nextInt(3) - 1, this.random.nextInt(3) - 1, this.random.nextInt(3) - 1);
+                 state = BaseFireBlock.getState(this.level(), blockPos1);
+                 if (this.level().getBlockState(blockPos1).isAir() && state.canSurvive(this.level(), blockPos1)) {
+-                    this.level().setBlockAndUpdate(blockPos1, state);
+-                    this.blocksSetOnFire++;
++                    // CraftBukkit start - add "!visualOnly"
++                    if (!this.visualOnly && !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(this.level(), blockPos1, this).isCancelled()) {
++                        this.level().setBlockAndUpdate(blockPos1, state);
++                        this.blocksSetOnFire++;
++                    }
++                    // CraftBukkit end
+                 }
+             }
+         }
+     }
+ 
+-    private static void clearCopperOnLightningStrike(Level level, BlockPos pos) {
++    private static void clearCopperOnLightningStrike(Level level, BlockPos pos, Entity lightning) { // Paper - Call EntityChangeBlockEvent
+         BlockState blockState = level.getBlockState(pos);
+         BlockPos blockPos;
+         BlockState blockState1;
+@@ -200,22 +_,27 @@
+         }
+ 
+         if (blockState1.getBlock() instanceof WeatheringCopper) {
+-            level.setBlockAndUpdate(blockPos, WeatheringCopper.getFirst(level.getBlockState(blockPos)));
++            // Paper start - Call EntityChangeBlockEvent
++            BlockState newBlockState = WeatheringCopper.getFirst(level.getBlockState(blockPos));
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(lightning, blockPos, newBlockState)) {
++                level.setBlockAndUpdate(blockPos, newBlockState);
++            }
++            // Paper end - Call EntityChangeBlockEvent
+             BlockPos.MutableBlockPos mutableBlockPos = pos.mutable();
+             int i = level.random.nextInt(3) + 3;
+ 
+             for (int i1 = 0; i1 < i; i1++) {
+                 int i2 = level.random.nextInt(8) + 1;
+-                randomWalkCleaningCopper(level, blockPos, mutableBlockPos, i2);
++                randomWalkCleaningCopper(level, blockPos, mutableBlockPos, i2, lightning); // Paper - transmit LightningBolt instance to call EntityChangeBlockEvent
+             }
+         }
+     }
+ 
+-    private static void randomWalkCleaningCopper(Level level, BlockPos pos, BlockPos.MutableBlockPos mutable, int steps) {
++    private static void randomWalkCleaningCopper(Level level, BlockPos pos, BlockPos.MutableBlockPos mutable, int steps, Entity lightning) { // Paper - transmit LightningBolt instance to call EntityChangeBlockEvent
+         mutable.set(pos);
+ 
+         for (int i = 0; i < steps; i++) {
+-            Optional<BlockPos> optional = randomStepCleaningCopper(level, mutable);
++            Optional<BlockPos> optional = randomStepCleaningCopper(level, mutable, lightning); // Paper - transmit LightningBolt instance to call EntityChangeBlockEvent
+             if (optional.isEmpty()) {
+                 break;
+             }
+@@ -224,11 +_,17 @@
+         }
+     }
+ 
+-    private static Optional<BlockPos> randomStepCleaningCopper(Level level, BlockPos pos) {
++    private static Optional<BlockPos> randomStepCleaningCopper(Level level, BlockPos pos, Entity lightning) { // Paper - transmit LightningBolt instance to call EntityChangeBlockEvent
+         for (BlockPos blockPos : BlockPos.randomInCube(level.random, 10, pos, 1)) {
+             BlockState blockState = level.getBlockState(blockPos);
+             if (blockState.getBlock() instanceof WeatheringCopper) {
+-                WeatheringCopper.getPrevious(blockState).ifPresent(blockState1 -> level.setBlockAndUpdate(blockPos, blockState1));
++                // Paper start - call EntityChangeBlockEvent
++                WeatheringCopper.getPrevious(blockState).ifPresent(blockState1 -> {
++                    if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(lightning, blockPos, blockState1)) {
++                        level.setBlockAndUpdate(blockPos, blockState1);
++                    }
++                });
++                // Paper end - call EntityChangeBlockEvent
+                 level.levelEvent(3002, blockPos, -1);
+                 return Optional.of(blockPos);
+             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/LivingEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/LivingEntity.java.patch
similarity index 52%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/LivingEntity.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/LivingEntity.java.patch
index c1e602ad26..6e41390320 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/LivingEntity.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/LivingEntity.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/entity/LivingEntity.java
 +++ b/net/minecraft/world/entity/LivingEntity.java
-@@ -42,6 +42,8 @@
+@@ -42,6 +_,8 @@
  import net.minecraft.core.particles.ParticleOptions;
  import net.minecraft.core.particles.ParticleTypes;
  import net.minecraft.nbt.CompoundTag;
@@ -9,19 +9,10 @@
  import net.minecraft.nbt.ListTag;
  import net.minecraft.nbt.NbtOps;
  import net.minecraft.nbt.Tag;
-@@ -94,7 +96,6 @@
- import net.minecraft.world.entity.animal.Wolf;
- import net.minecraft.world.entity.boss.wither.WitherBoss;
- import net.minecraft.world.entity.item.ItemEntity;
--import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.entity.projectile.AbstractArrow;
- import net.minecraft.world.entity.projectile.Projectile;
- import net.minecraft.world.item.AxeItem;
-@@ -135,6 +136,30 @@
- import net.minecraft.world.scores.PlayerTeam;
+@@ -136,6 +_,29 @@
  import net.minecraft.world.scores.Scoreboard;
  import org.slf4j.Logger;
-+
+ 
 +// CraftBukkit start
 +import java.util.ArrayList;
 +import java.util.HashSet;
@@ -33,7 +24,6 @@
 +import org.bukkit.craftbukkit.attribute.CraftAttributeMap;
 +import org.bukkit.craftbukkit.event.CraftEventFactory;
 +import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.entity.Player;
 +import org.bukkit.event.entity.ArrowBodyCountChangeEvent;
 +import org.bukkit.event.entity.EntityDamageEvent;
 +import org.bukkit.event.entity.EntityDamageEvent.DamageModifier;
@@ -45,31 +35,14 @@
 +import org.bukkit.event.entity.EntityTeleportEvent;
 +import org.bukkit.event.player.PlayerItemConsumeEvent;
 +// CraftBukkit end
- 
++
  public abstract class LivingEntity extends Entity implements Attackable {
- 
-@@ -174,7 +199,7 @@
-     public static final float DEFAULT_BABY_SCALE = 0.5F;
-     public static final String ATTRIBUTES_FIELD = "attributes";
-     public static final Predicate<LivingEntity> PLAYER_NOT_WEARING_DISGUISE_ITEM = (entityliving) -> {
--        if (entityliving instanceof Player entityhuman) {
-+        if (entityliving instanceof net.minecraft.world.entity.player.Player entityhuman) {
-             ItemStack itemstack = entityhuman.getItemBySlot(EquipmentSlot.HEAD);
- 
-             return !itemstack.is(ItemTags.GAZE_DISGUISE_EQUIPMENT);
-@@ -210,7 +235,7 @@
-     public float yHeadRotO;
-     public final ElytraAnimationState elytraAnimationState;
-     @Nullable
--    public Player lastHurtByPlayer;
-+    public net.minecraft.world.entity.player.Player lastHurtByPlayer;
-     public int lastHurtByPlayerTime;
-     protected boolean dead;
-     protected int noActionTime;
-@@ -260,7 +285,30 @@
-     protected boolean skipDropExperience;
-     private final EnumMap<EquipmentSlot, Reference2ObjectMap<Enchantment, Set<EnchantmentLocationBasedEffect>>> activeLocationDependentEnchantments;
-     protected float appliedScale;
+     private static final Logger LOGGER = LogUtils.getLogger();
+     private static final String TAG_ACTIVE_EFFECTS = "active_effects";
+@@ -266,11 +_,29 @@
+         EquipmentSlot.class
+     );
+     protected float appliedScale = 1.0F;
 +    // CraftBukkit start
 +    public int expToDrop;
 +    public ArrayList<DefaultDrop> drops = new ArrayList<>(); // Paper - Restore vanilla drops behavior
@@ -80,86 +53,48 @@
 +    public org.bukkit.craftbukkit.entity.CraftLivingEntity getBukkitLivingEntity() { return (org.bukkit.craftbukkit.entity.CraftLivingEntity) super.getBukkitEntity(); } // Paper
 +    public boolean silentDeath = false; // Paper - mark entity as dying silently for cancellable death event
 +    public net.kyori.adventure.util.TriState frictionState = net.kyori.adventure.util.TriState.NOT_SET; // Paper - Friction API
- 
++
 +    @Override
 +    public float getBukkitYaw() {
 +        return this.getYHeadRot();
 +    }
 +    // CraftBukkit end
-+    // Spigot start
-+    public void inactiveTick()
-+    {
-+        super.inactiveTick();
-+        ++this.noActionTime; // Above all the floats
-+    }
-+    // Spigot end
-+
-     protected LivingEntity(EntityType<? extends LivingEntity> type, Level world) {
-         super(type, world);
-         this.lastHandItemStacks = NonNullList.withSize(2, ItemStack.EMPTY);
-@@ -276,7 +324,9 @@
-         this.activeLocationDependentEnchantments = new EnumMap(EquipmentSlot.class);
-         this.appliedScale = 1.0F;
-         this.attributes = new AttributeMap(DefaultAttributes.getSupplier(type));
+ 
+     protected LivingEntity(EntityType<? extends LivingEntity> entityType, Level level) {
+         super(entityType, level);
+         this.attributes = new AttributeMap(DefaultAttributes.getSupplier(entityType));
 -        this.setHealth(this.getMaxHealth());
 +        this.craftAttributes = new CraftAttributeMap(this.attributes); // CraftBukkit
-+        // CraftBukkit - setHealth(getMaxHealth()) inlined and simplified to skip the instanceof check for EntityPlayer, as getBukkitEntity() is not initialized in constructor
-+        this.entityData.set(LivingEntity.DATA_HEALTH_ID, (float) this.getAttribute(Attributes.MAX_HEALTH).getValue());
++        // CraftBukkit - this.setHealth(this.getMaxHealth()) inlined and simplified to skip the instanceof check for Player, as getBukkitEntity() is not initialized in constructor
++        this.entityData.set(LivingEntity.DATA_HEALTH_ID, this.getMaxHealth());
          this.blocksBuilding = true;
-         this.rotA = (float) ((Math.random() + 1.0D) * 0.009999999776482582D);
+         this.rotA = (float)((Math.random() + 1.0) * 0.01F);
          this.reapplyPosition();
-@@ -356,7 +406,13 @@
-                     double d8 = Math.min((double) (0.2F + f / 15.0F), 2.5D);
-                     int i = (int) (150.0D * d8);
- 
--                    worldserver.sendParticles(new BlockParticleOption(ParticleTypes.BLOCK, state), d2, d3, d4, i, 0.0D, 0.0D, 0.0D, 0.15000000596046448D);
-+                    // CraftBukkit start - visiblity api
-+                    if (this instanceof ServerPlayer) {
-+                        worldserver.sendParticlesSource((ServerPlayer) this, new BlockParticleOption(ParticleTypes.BLOCK, state), false, false, d2, d3, d4, i, 0.0D, 0.0D, 0.0D, 0.15000000596046448D);
-+                    } else {
-+                        worldserver.sendParticles(new BlockParticleOption(ParticleTypes.BLOCK, state), d2, d3, d4, i, 0.0D, 0.0D, 0.0D, 0.15000000596046448D);
-+                    }
-+                    // CraftBukkit end
-                 }
+@@ -360,7 +_,13 @@
+                 float f = Mth.ceil(this.fallDistance - attributeValue);
+                 double min = Math.min((double)(0.2F + f / 15.0F), 2.5);
+                 int i = (int)(150.0 * min);
+-                serverLevel.sendParticles(new BlockParticleOption(ParticleTypes.BLOCK, state), x, y1, z, i, 0.0, 0.0, 0.0, 0.15F);
++                // CraftBukkit start - visiblity api
++                if (this instanceof ServerPlayer) {
++                    serverLevel.sendParticlesSource((ServerPlayer) this, new BlockParticleOption(ParticleTypes.BLOCK, state), false, false, x, y1, z, i, 0.0, 0.0, 0.0, 0.15F);
++                } else {
++                    serverLevel.sendParticles(new BlockParticleOption(ParticleTypes.BLOCK, state), x, y1, z, i, 0.0, 0.0, 0.0, 0.15F);
++                }
++                // CraftBukkit end
              }
          }
-@@ -402,7 +458,7 @@
-         }
  
-         if (this.isAlive()) {
--            boolean flag = this instanceof Player;
-+            boolean flag = this instanceof net.minecraft.world.entity.player.Player;
-             Level world1 = this.level();
-             ServerLevel worldserver1;
-             double d0;
-@@ -424,7 +480,7 @@
-             }
- 
-             if (this.isEyeInFluid(FluidTags.WATER) && !this.level().getBlockState(BlockPos.containing(this.getX(), this.getEyeY(), this.getZ())).is(Blocks.BUBBLE_COLUMN)) {
--                boolean flag1 = !this.canBreatheUnderwater() && !MobEffectUtil.hasWaterBreathing(this) && (!flag || !((Player) this).getAbilities().invulnerable);
-+                boolean flag1 = !this.canBreatheUnderwater() && !MobEffectUtil.hasWaterBreathing(this) && (!flag || !((net.minecraft.world.entity.player.Player) this).getAbilities().invulnerable);
- 
-                 if (flag1) {
-                     this.setAirSupply(this.decreaseAirSupply(this.getAirSupply()));
-@@ -573,7 +629,7 @@
-         ++this.deathTime;
+@@ -566,7 +_,7 @@
+         this.deathTime++;
          if (this.deathTime >= 20 && !this.level().isClientSide() && !this.isRemoved()) {
-             this.level().broadcastEntityEvent(this, (byte) 60);
+             this.level().broadcastEntityEvent(this, (byte)60);
 -            this.remove(Entity.RemovalReason.KILLED);
 +            this.remove(Entity.RemovalReason.KILLED, EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause
          }
- 
-     }
-@@ -629,7 +685,7 @@
-         return this.lastHurtByMobTimestamp;
      }
  
--    public void setLastHurtByPlayer(@Nullable Player attacking) {
-+    public void setLastHurtByPlayer(@Nullable net.minecraft.world.entity.player.Player attacking) {
-         this.lastHurtByPlayer = attacking;
-         this.lastHurtByPlayerTime = this.tickCount;
-     }
-@@ -667,7 +723,7 @@
+@@ -658,7 +_,7 @@
      }
  
      public boolean shouldDiscardFriction() {
@@ -167,69 +102,47 @@
 +        return !this.frictionState.toBooleanOrElse(!this.discardFriction); // Paper - Friction API
      }
  
-     public void setDiscardFriction(boolean noDrag) {
-@@ -679,17 +735,23 @@
+     public void setDiscardFriction(boolean discardFriction) {
+@@ -670,11 +_,16 @@
      }
  
-     public void onEquipItem(EquipmentSlot slot, ItemStack oldStack, ItemStack newStack) {
--        if (!this.level().isClientSide() && !this.isSpectator()) {
--            boolean flag = newStack.isEmpty() && oldStack.isEmpty();
--
--            if (!flag && !ItemStack.isSameItemSameComponents(oldStack, newStack) && !this.firstTick) {
--                Equippable equippable = (Equippable) newStack.get(DataComponents.EQUIPPABLE);
+     public void onEquipItem(EquipmentSlot slot, ItemStack oldItem, ItemStack newItem) {
 +        // CraftBukkit start
-+        this.onEquipItem(slot, oldStack, newStack, false);
++        this.onEquipItem(slot, oldItem, newItem, false);
 +    }
- 
--                if (!this.isSilent() && equippable != null && slot == equippable.slot()) {
--                    this.level().playSeededSound((Player) null, this.getX(), this.getY(), this.getZ(), equippable.equipSound(), this.getSoundSource(), 1.0F, 1.0F, this.random.nextLong());
-+    public void onEquipItem(EquipmentSlot enumitemslot, ItemStack itemstack, ItemStack itemstack1, boolean silent) {
++    public void onEquipItem(EquipmentSlot slot, ItemStack oldItem, ItemStack newItem, boolean silent) {
 +        // CraftBukkit end
-+        if (!this.level().isClientSide() && !this.isSpectator()) {
-+            boolean flag = itemstack1.isEmpty() && itemstack.isEmpty();
-+
-+            if (!flag && !ItemStack.isSameItemSameComponents(itemstack, itemstack1) && !this.firstTick) {
-+                Equippable equippable = (Equippable) itemstack1.get(DataComponents.EQUIPPABLE);
-+
-+                if (!this.isSilent() && equippable != null && enumitemslot == equippable.slot() && !silent) { // CraftBukkit
-+                    this.level().playSeededSound((net.minecraft.world.entity.player.Player) null, this.getX(), this.getY(), this.getZ(), equippable.equipSound(), this.getSoundSource(), 1.0F, 1.0F, this.random.nextLong());
-                 }
- 
--                if (this.doesEmitEquipEvent(slot)) {
-+                if (this.doesEmitEquipEvent(enumitemslot)) {
-                     this.gameEvent(equippable != null ? GameEvent.EQUIP : GameEvent.UNEQUIP);
-                 }
- 
-@@ -699,17 +761,24 @@
+         if (!this.level().isClientSide() && !this.isSpectator()) {
+             boolean flag = newItem.isEmpty() && oldItem.isEmpty();
+             if (!flag && !ItemStack.isSameItemSameComponents(oldItem, newItem) && !this.firstTick) {
+                 Equippable equippable = newItem.get(DataComponents.EQUIPPABLE);
+-                if (!this.isSilent() && equippable != null && slot == equippable.slot()) {
++                if (!this.isSilent() && equippable != null && slot == equippable.slot() && !silent) { // CraftBukkit
+                     this.level()
+                         .playSeededSound(
+                             null, this.getX(), this.getY(), this.getZ(), equippable.equipSound(), this.getSoundSource(), 1.0F, 1.0F, this.random.nextLong()
+@@ -690,11 +_,18 @@
  
      @Override
      public void remove(Entity.RemovalReason reason) {
--        if (reason == Entity.RemovalReason.KILLED || reason == Entity.RemovalReason.DISCARDED) {
 +        // CraftBukkit start - add Bukkit remove cause
 +        this.remove(reason, null);
 +    }
 +
 +    @Override
-+    public void remove(Entity.RemovalReason entity_removalreason, EntityRemoveEvent.Cause cause) {
++    public void remove(Entity.RemovalReason reason, EntityRemoveEvent.Cause eventCause) {
 +        // CraftBukkit end
-+        if (entity_removalreason == Entity.RemovalReason.KILLED || entity_removalreason == Entity.RemovalReason.DISCARDED) {
-             Level world = this.level();
- 
-             if (world instanceof ServerLevel) {
-                 ServerLevel worldserver = (ServerLevel) world;
- 
--                this.triggerOnDeathMobEffects(worldserver, reason);
-+                this.triggerOnDeathMobEffects(worldserver, entity_removalreason);
-             }
+         if ((reason == Entity.RemovalReason.KILLED || reason == Entity.RemovalReason.DISCARDED) && this.level() instanceof ServerLevel serverLevel) {
+             this.triggerOnDeathMobEffects(serverLevel, reason);
          }
  
 -        super.remove(reason);
-+        super.remove(entity_removalreason, cause); // CraftBukkit
++        super.remove(reason, eventCause); // CraftBukkit
          this.brain.clearMemories();
      }
  
-@@ -722,11 +791,17 @@
-             mobeffect.onMobRemoved(world, this, reason);
+@@ -703,11 +_,17 @@
+             mobEffectInstance.onMobRemoved(level, this, removalReason);
          }
  
 +        this.removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.DEATH); // CraftBukkit
@@ -237,82 +150,81 @@
      }
  
      @Override
-     public void addAdditionalSaveData(CompoundTag nbt) {
+     public void addAdditionalSaveData(CompoundTag compound) {
 +        // Paper start - Friction API
 +        if (this.frictionState != net.kyori.adventure.util.TriState.NOT_SET) {
-+            nbt.putString("Paper.FrictionState", this.frictionState.toString());
++            compound.putString("Paper.FrictionState", this.frictionState.toString());
 +        }
 +        // Paper end - Friction API
-         nbt.putFloat("Health", this.getHealth());
-         nbt.putShort("HurtTime", (short) this.hurtTime);
-         nbt.putInt("HurtByTimestamp", this.lastHurtByMobTimestamp);
-@@ -763,7 +838,23 @@
+         compound.putFloat("Health", this.getHealth());
+         compound.putShort("HurtTime", (short)this.hurtTime);
+         compound.putInt("HurtByTimestamp", this.lastHurtByMobTimestamp);
+@@ -736,7 +_,23 @@
  
      @Override
-     public void readAdditionalSaveData(CompoundTag nbt) {
--        this.internalSetAbsorptionAmount(nbt.getFloat("AbsorptionAmount"));
+     public void readAdditionalSaveData(CompoundTag compound) {
+-        this.internalSetAbsorptionAmount(compound.getFloat("AbsorptionAmount"));
 +        // Paper start - Check for NaN
-+        float absorptionAmount = nbt.getFloat("AbsorptionAmount");
++        float absorptionAmount = compound.getFloat("AbsorptionAmount");
 +        if (Float.isNaN(absorptionAmount)) {
 +            absorptionAmount = 0;
 +        }
 +        this.internalSetAbsorptionAmount(absorptionAmount);
 +        // Paper end - Check for NaN
 +        // Paper start - Friction API
-+        if (nbt.contains("Paper.FrictionState")) {
-+            String fs = nbt.getString("Paper.FrictionState");
++        if (compound.contains("Paper.FrictionState")) {
++            String frictionState = compound.getString("Paper.FrictionState");
 +            try {
-+                frictionState = net.kyori.adventure.util.TriState.valueOf(fs);
++                this.frictionState = net.kyori.adventure.util.TriState.valueOf(frictionState);
 +            } catch (Exception ignored) {
-+                LOGGER.error("Unknown friction state " + fs + " for " + this);
++                LOGGER.error("Unknown friction state " + frictionState + " for " + this);
 +            }
 +        }
 +        // Paper end - Friction API
-         if (nbt.contains("attributes", 9) && this.level() != null && !this.level().isClientSide) {
-             this.getAttributes().load(nbt.getList("attributes", 10));
+         if (compound.contains("attributes", 9) && this.level() != null && !this.level().isClientSide) {
+             this.getAttributes().load(compound.getList("attributes", 10));
          }
-@@ -781,6 +872,17 @@
+@@ -753,6 +_,16 @@
              }
          }
  
 +        // CraftBukkit start
-+        if (nbt.contains("Bukkit.MaxHealth")) {
-+            Tag nbtbase = nbt.get("Bukkit.MaxHealth");
-+            if (nbtbase.getId() == 5) {
-+                this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(((FloatTag) nbtbase).getAsDouble());
-+            } else if (nbtbase.getId() == 3) {
-+                this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(((IntTag) nbtbase).getAsDouble());
++        if (compound.contains("Bukkit.MaxHealth")) {
++            Tag maxHealthTag = compound.get("Bukkit.MaxHealth");
++            if (maxHealthTag.getId() == 5) {
++                this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(((FloatTag) maxHealthTag).getAsDouble());
++            } else if (maxHealthTag.getId() == 3) {
++                this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(((IntTag) maxHealthTag).getAsDouble());
 +            }
 +        }
 +        // CraftBukkit end
-+
-         if (nbt.contains("Health", 99)) {
-             this.setHealth(nbt.getFloat("Health"));
+         if (compound.contains("Health", 99)) {
+             this.setHealth(compound.getFloat("Health"));
          }
-@@ -792,6 +894,7 @@
-             String s = nbt.getString("Team");
+@@ -764,6 +_,7 @@
+             String string = compound.getString("Team");
              Scoreboard scoreboard = this.level().getScoreboard();
-             PlayerTeam scoreboardteam = scoreboard.getPlayerTeam(s);
-+            if (!this.level().paperConfig().scoreboards.allowNonPlayerEntitiesOnScoreboards && !(this instanceof net.minecraft.world.entity.player.Player)) { scoreboardteam = null; } // Paper - Perf: Disable Scoreboards for non players by default
-             boolean flag = scoreboardteam != null && scoreboard.addPlayerToTeam(this.getStringUUID(), scoreboardteam);
- 
+             PlayerTeam playerTeam = scoreboard.getPlayerTeam(string);
++            if (!this.level().paperConfig().scoreboards.allowNonPlayerEntitiesOnScoreboards && !(this instanceof net.minecraft.world.entity.player.Player)) { playerTeam = null; } // Paper - Perf: Disable Scoreboards for non players by default
+             boolean flag = playerTeam != null && scoreboard.addPlayerToTeam(this.getStringUUID(), playerTeam);
              if (!flag) {
-@@ -806,11 +909,13 @@
-         if (nbt.contains("SleepingX", 99) && nbt.contains("SleepingY", 99) && nbt.contains("SleepingZ", 99)) {
-             BlockPos blockposition = new BlockPos(nbt.getInt("SleepingX"), nbt.getInt("SleepingY"), nbt.getInt("SleepingZ"));
+                 LOGGER.warn("Unable to add mob to team \"{}\" (that team probably doesn't exist)", string);
+@@ -776,11 +_,13 @@
  
-+            if (this.position().distanceToSqr(blockposition.getX(), blockposition.getY(), blockposition.getZ()) < 16 * 16) { // Paper - The sleeping pos will always also set the actual pos, so a desync suggests something is wrong
-             this.setSleepingPos(blockposition);
-             this.entityData.set(LivingEntity.DATA_POSE, Pose.SLEEPING);
+         if (compound.contains("SleepingX", 99) && compound.contains("SleepingY", 99) && compound.contains("SleepingZ", 99)) {
+             BlockPos blockPos = new BlockPos(compound.getInt("SleepingX"), compound.getInt("SleepingY"), compound.getInt("SleepingZ"));
++            if (this.position().distanceToSqr(blockPos.getX(), blockPos.getY(), blockPos.getZ()) < 16 * 16) { // Paper - The sleeping pos will always also set the actual pos, so a desync suggests something is wrong
+             this.setSleepingPos(blockPos);
+             this.entityData.set(DATA_POSE, Pose.SLEEPING);
              if (!this.firstTick) {
-                 this.setPosToBed(blockposition);
+                 this.setPosToBed(blockPos);
              }
 +            } // Paper - The sleeping pos will always also set the actual pos, so a desync suggests something is wrong
          }
  
-         if (nbt.contains("Brain", 10)) {
-@@ -819,9 +924,32 @@
- 
+         if (compound.contains("Brain", 10)) {
+@@ -788,15 +_,44 @@
+         }
      }
  
 +    // CraftBukkit start
@@ -343,56 +255,44 @@
 +        this.isTickingEffects = true; // CraftBukkit
          try {
              while (iterator.hasNext()) {
-                 Holder<MobEffect> holder = (Holder) iterator.next();
-@@ -831,6 +959,12 @@
-                     this.onEffectUpdated(mobeffect, true, (Entity) null);
-                 })) {
+                 Holder<MobEffect> holder = iterator.next();
+                 MobEffectInstance mobEffectInstance = this.activeEffects.get(holder);
+                 if (!mobEffectInstance.tick(this, () -> this.onEffectUpdated(mobEffectInstance, true, null))) {
                      if (!this.level().isClientSide) {
 +                        // CraftBukkit start
-+                        EntityPotionEffectEvent event = CraftEventFactory.callEntityPotionEffectChangeEvent(this, mobeffect, null, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.EXPIRATION);
++                        EntityPotionEffectEvent event = CraftEventFactory.callEntityPotionEffectChangeEvent(this, mobEffectInstance, null, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.EXPIRATION);
 +                        if (event.isCancelled()) {
 +                            continue;
 +                        }
 +                        // CraftBukkit end
                          iterator.remove();
-                         this.onEffectsRemoved(List.of(mobeffect));
+                         this.onEffectsRemoved(List.of(mobEffectInstance));
                      }
-@@ -841,6 +975,17 @@
-         } catch (ConcurrentModificationException concurrentmodificationexception) {
-             ;
+@@ -807,6 +_,17 @@
+         } catch (ConcurrentModificationException var6) {
          }
+ 
 +        // CraftBukkit start
 +        this.isTickingEffects = false;
-+        for (ProcessableEffect e : this.effectsToProcess) {
-+            if (e.effect != null) {
-+                this.addEffect(e.effect, e.cause);
++        for (ProcessableEffect effect : this.effectsToProcess) {
++            if (effect.effect != null) {
++                this.addEffect(effect.effect, effect.cause);
 +            } else {
-+                this.removeEffect(e.type, e.cause);
++                this.removeEffect(effect.type, effect.cause);
 +            }
 +        }
 +        this.effectsToProcess.clear();
 +        // CraftBukkit end
- 
          if (this.effectsDirty) {
              if (!this.level().isClientSide) {
-@@ -921,7 +1066,7 @@
+                 this.updateInvisibilityStatus();
+@@ -912,15 +_,33 @@
      }
  
-     public boolean canAttack(LivingEntity target) {
--        return target instanceof Player && this.level().getDifficulty() == Difficulty.PEACEFUL ? false : target.canBeSeenAsEnemy();
-+        return target instanceof net.minecraft.world.entity.player.Player && this.level().getDifficulty() == Difficulty.PEACEFUL ? false : target.canBeSeenAsEnemy();
-     }
- 
-     public boolean canBeSeenAsEnemy() {
-@@ -952,17 +1097,36 @@
-         this.entityData.set(LivingEntity.DATA_EFFECT_PARTICLES, List.of());
-     }
- 
-+    // CraftBukkit start
      public boolean removeAllEffects() {
++        // CraftBukkit start
 +        return this.removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.UNKNOWN);
 +    }
-+
 +    public boolean removeAllEffects(EntityPotionEffectEvent.Cause cause) {
 +        // CraftBukkit end
          if (this.level().isClientSide) {
@@ -401,13 +301,12 @@
              return false;
          } else {
 -            Map<Holder<MobEffect>, MobEffectInstance> map = Maps.newHashMap(this.activeEffects);
-+            // CraftBukkit start
-+            List<MobEffectInstance> toRemove = new LinkedList<>();
-+            Iterator<MobEffectInstance> iterator = this.activeEffects.values().iterator();
- 
 -            this.activeEffects.clear();
 -            this.onEffectsRemoved(map.values());
 -            return true;
++            // CraftBukkit start
++            List<MobEffectInstance> toRemove = new LinkedList<>();
++            Iterator<MobEffectInstance> iterator = this.activeEffects.values().iterator();
 +            while (iterator.hasNext()) {
 +                MobEffectInstance effect = iterator.next();
 +                EntityPotionEffectEvent event = CraftEventFactory.callEntityPotionEffectChangeEvent(this, effect, null, cause, EntityPotionEffectEvent.Action.CLEARED);
@@ -425,142 +324,125 @@
          }
      }
  
-@@ -987,24 +1151,63 @@
-         return this.addEffect(effect, (Entity) null);
+@@ -942,21 +_,57 @@
      }
  
+     public final boolean addEffect(MobEffectInstance effectInstance) {
+-        return this.addEffect(effectInstance, null);
++        return this.addEffect(effectInstance, (Entity) null); // CraftBukkit
++    }
++
 +    // CraftBukkit start
-+    public boolean addEffect(MobEffectInstance mobeffect, EntityPotionEffectEvent.Cause cause) {
-+        return this.addEffect(mobeffect, (Entity) null, cause);
++    public boolean addEffect(MobEffectInstance effectInstance, EntityPotionEffectEvent.Cause cause) {
++        return this.addEffect(effectInstance, (Entity) null, cause);
+     }
+ 
+     public boolean addEffect(MobEffectInstance effectInstance, @Nullable Entity entity) {
++        return this.addEffect(effectInstance, entity, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.UNKNOWN);
 +    }
 +
-     public boolean addEffect(MobEffectInstance effect, @Nullable Entity source) {
--        if (!this.canBeAffected(effect)) {
-+        return this.addEffect(effect, source, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.UNKNOWN);
-+    }
-+
-+    public boolean addEffect(MobEffectInstance mobeffect, @Nullable Entity entity, EntityPotionEffectEvent.Cause cause) {
++    public boolean addEffect(MobEffectInstance effectInstance, @Nullable Entity entity, EntityPotionEffectEvent.Cause cause) {
 +        // Paper start - Don't fire sync event during generation
-+        return this.addEffect(mobeffect, entity, cause, true);
++        return this.addEffect(effectInstance, entity, cause, true);
 +    }
-+    public boolean addEffect(MobEffectInstance mobeffect, @Nullable Entity entity, EntityPotionEffectEvent.Cause cause, boolean fireEvent) {
++    public boolean addEffect(MobEffectInstance effectInstance, @Nullable Entity entity, EntityPotionEffectEvent.Cause cause, boolean fireEvent) {
 +        // Paper end - Don't fire sync event during generation
 +        // org.spigotmc.AsyncCatcher.catchOp("effect add"); // Spigot // Paper - move to API
 +        if (this.isTickingEffects) {
-+            this.effectsToProcess.add(new ProcessableEffect(mobeffect, cause));
++            this.effectsToProcess.add(new ProcessableEffect(effectInstance, cause));
 +            return true;
 +        }
 +        // CraftBukkit end
-+
-+        if (!this.canBeAffected(mobeffect)) {
+         if (!this.canBeAffected(effectInstance)) {
              return false;
          } else {
--            MobEffectInstance mobeffect1 = (MobEffectInstance) this.activeEffects.get(effect.getEffect());
-+            MobEffectInstance mobeffect1 = (MobEffectInstance) this.activeEffects.get(mobeffect.getEffect());
+             MobEffectInstance mobEffectInstance = this.activeEffects.get(effectInstance.getEffect());
              boolean flag = false;
- 
 +            // CraftBukkit start
 +            boolean override = false;
-+            if (mobeffect1 != null) {
-+                override = new MobEffectInstance(mobeffect1).update(mobeffect);
++            if (mobEffectInstance != null) {
++                override = new MobEffectInstance(mobEffectInstance).update(effectInstance);
 +            }
 +
 +            if (fireEvent) { // Paper - Don't fire sync event during generation
-+            EntityPotionEffectEvent event = CraftEventFactory.callEntityPotionEffectChangeEvent(this, mobeffect1, mobeffect, cause, override);
++            EntityPotionEffectEvent event = CraftEventFactory.callEntityPotionEffectChangeEvent(this, mobEffectInstance, effectInstance, cause, override);
 +            override = event.isOverride(); // Paper - Don't fire sync event during generation
 +            if (event.isCancelled()) {
 +                return false;
 +            }
 +            } // Paper - Don't fire sync event during generation
 +            // CraftBukkit end
-+
-             if (mobeffect1 == null) {
--                this.activeEffects.put(effect.getEffect(), effect);
--                this.onEffectAdded(effect, source);
-+                this.activeEffects.put(mobeffect.getEffect(), mobeffect);
-+                this.onEffectAdded(mobeffect, entity);
+             if (mobEffectInstance == null) {
+                 this.activeEffects.put(effectInstance.getEffect(), effectInstance);
+                 this.onEffectAdded(effectInstance, entity);
                  flag = true;
--                effect.onEffectAdded(this);
--            } else if (mobeffect1.update(effect)) {
--                this.onEffectUpdated(mobeffect1, true, source);
-+                mobeffect.onEffectAdded(this);
+                 effectInstance.onEffectAdded(this);
+-            } else if (mobEffectInstance.update(effectInstance)) {
 +                // CraftBukkit start
 +            } else if (override) { // Paper - Don't fire sync event during generation
-+                mobeffect1.update(mobeffect);
-+                this.onEffectUpdated(mobeffect1, true, entity);
-+                // CraftBukkit end
++                mobEffectInstance.update(effectInstance);
+                 this.onEffectUpdated(mobEffectInstance, true, entity);
                  flag = true;
              }
+@@ -995,11 +_,37 @@
  
--            effect.onEffectStarted(this);
-+            mobeffect.onEffectStarted(this);
-             return flag;
-         }
-     }
-@@ -1031,14 +1234,40 @@
-         return this.getType().is(EntityTypeTags.INVERTED_HEALING_AND_HARM);
-     }
- 
-+    // CraftBukkit start
      @Nullable
      public MobEffectInstance removeEffectNoUpdate(Holder<MobEffect> effect) {
--        return (MobEffectInstance) this.activeEffects.remove(effect);
+-        return this.activeEffects.remove(effect);
++        // CraftBukkit start
 +        return this.removeEffectNoUpdate(effect, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.UNKNOWN);
-     }
- 
++    }
++
 +    @Nullable
-+    public MobEffectInstance removeEffectNoUpdate(Holder<MobEffect> holder, EntityPotionEffectEvent.Cause cause) {
++    public MobEffectInstance removeEffectNoUpdate(Holder<MobEffect> effect, EntityPotionEffectEvent.Cause cause) {
 +        if (this.isTickingEffects) {
-+            this.effectsToProcess.add(new ProcessableEffect(holder, cause));
++            this.effectsToProcess.add(new ProcessableEffect(effect, cause));
 +            return null;
 +        }
 +
-+        MobEffectInstance effect = this.activeEffects.get(holder);
-+        if (effect == null) {
++        MobEffectInstance effectInstance = this.activeEffects.get(effect);
++        if (effectInstance == null) {
 +            return null;
 +        }
 +
-+        EntityPotionEffectEvent event = CraftEventFactory.callEntityPotionEffectChangeEvent(this, effect, null, cause);
++        EntityPotionEffectEvent event = CraftEventFactory.callEntityPotionEffectChangeEvent(this, effectInstance, null, cause);
 +        if (event.isCancelled()) {
 +            return null;
 +        }
 +
-+        return (MobEffectInstance) this.activeEffects.remove(holder);
-+    }
-+
-     public boolean removeEffect(Holder<MobEffect> effect) {
--        MobEffectInstance mobeffect = this.removeEffectNoUpdate(effect);
-+        return this.removeEffect(effect, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.UNKNOWN);
-+    }
- 
-+    public boolean removeEffect(Holder<MobEffect> holder, EntityPotionEffectEvent.Cause cause) {
-+        MobEffectInstance mobeffect = this.removeEffectNoUpdate(holder, cause);
-+        // CraftBukkit end
-+
-         if (mobeffect != null) {
-             this.onEffectsRemoved(List.of(mobeffect));
-             return true;
-@@ -1142,20 +1371,65 @@
- 
++        return this.activeEffects.remove(effectInstance);
      }
  
-+    // CraftBukkit start - Delegate so we can handle providing a reason for health being regained
-     public void heal(float amount) {
-+        this.heal(amount, EntityRegainHealthEvent.RegainReason.CUSTOM);
+     public boolean removeEffect(Holder<MobEffect> effect) {
+-        MobEffectInstance mobEffectInstance = this.removeEffectNoUpdate(effect);
++        return this.removeEffect(effect, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.UNKNOWN);
 +    }
 +
-+    public void heal(float f, EntityRegainHealthEvent.RegainReason regainReason) {
-+        // Paper start - Forward
-+        heal(f, regainReason, false);
-+    }
-+
-+    public void heal(float f, EntityRegainHealthEvent.RegainReason regainReason, boolean isFastRegen) {
-+        // Paper end
-         float f1 = this.getHealth();
++    public boolean removeEffect(Holder<MobEffect> effect, EntityPotionEffectEvent.Cause cause) {
++        MobEffectInstance mobEffectInstance = this.removeEffectNoUpdate(effect, cause);
++        // CraftBukkit end
+         if (mobEffectInstance != null) {
+             this.onEffectsRemoved(List.of(mobEffectInstance));
+             return true;
+@@ -1080,17 +_,62 @@
+     }
  
-         if (f1 > 0.0F) {
--            this.setHealth(f1 + amount);
-+            EntityRegainHealthEvent event = new EntityRegainHealthEvent(this.getBukkitEntity(), f, regainReason, isFastRegen); // Paper
+     public void heal(float healAmount) {
++        // CraftBukkit start - Delegate so we can handle providing a reason for health being regained
++        this.heal(healAmount, EntityRegainHealthEvent.RegainReason.CUSTOM);
++    }
++
++    public void heal(float healAmount, EntityRegainHealthEvent.RegainReason regainReason) {
++        // Paper start - Forward
++        this.heal(healAmount, regainReason, false);
++    }
++
++    public void heal(float healAmount, EntityRegainHealthEvent.RegainReason regainReason, boolean isFastRegen) {
++        // Paper end - Forward
+         float health = this.getHealth();
+         if (health > 0.0F) {
+-            this.setHealth(health + healAmount);
++            EntityRegainHealthEvent event = new EntityRegainHealthEvent(this.getBukkitEntity(), healAmount, regainReason, isFastRegen); // Paper
 +            // Suppress during worldgen
 +            if (this.valid) {
 +                this.level().getCraftServer().getPluginManager().callEvent(event);
@@ -571,16 +453,15 @@
 +            }
 +            // CraftBukkit end
          }
- 
      }
  
      public float getHealth() {
 +        // CraftBukkit start - Use unscaled health
-+        if (this instanceof ServerPlayer) {
-+            return (float) ((ServerPlayer) this).getBukkitEntity().getHealth();
++        if (this instanceof ServerPlayer serverPlayer) {
++            return (float) serverPlayer.getBukkitEntity().getHealth();
 +        }
 +        // CraftBukkit end
-         return (Float) this.entityData.get(LivingEntity.DATA_HEALTH_ID);
+         return this.entityData.get(DATA_HEALTH_ID);
      }
  
      public void setHealth(float health) {
@@ -604,76 +485,75 @@
 +            return;
 +        }
 +        // CraftBukkit end
-         this.entityData.set(LivingEntity.DATA_HEALTH_ID, Mth.clamp(health, 0.0F, this.getMaxHealth()));
+         this.entityData.set(DATA_HEALTH_ID, Mth.clamp(health, 0.0F, this.getMaxHealth()));
      }
  
-@@ -1167,7 +1441,7 @@
-     public boolean hurtServer(ServerLevel world, DamageSource source, float amount) {
-         if (this.isInvulnerableTo(world, source)) {
+@@ -1102,7 +_,7 @@
+     public boolean hurtServer(ServerLevel level, DamageSource damageSource, float amount) {
+         if (this.isInvulnerableTo(level, damageSource)) {
              return false;
 -        } else if (this.isDeadOrDying()) {
 +        } else if (this.isRemoved() || this.dead || this.getHealth() <= 0.0F) { // CraftBukkit - Don't allow entities that got set to dead/killed elsewhere to get damaged and die
              return false;
-         } else if (source.is(DamageTypeTags.IS_FIRE) && this.hasEffect(MobEffects.FIRE_RESISTANCE)) {
+         } else if (damageSource.is(DamageTypeTags.IS_FIRE) && this.hasEffect(MobEffects.FIRE_RESISTANCE)) {
              return false;
-@@ -1181,11 +1455,12 @@
+@@ -1116,47 +_,71 @@
                  amount = 0.0F;
              }
  
--            float f1 = amount;
+-            float f = amount;
 -            boolean flag = false;
-+            float f1 = amount; final float originalAmount = f1; // Paper - revert to vanilla #hurt - OBFHELPER
-+            boolean flag = amount > 0.0F && this.isDamageSourceBlocked(source); // Copied from below
-             float f2 = 0.0F;
- 
--            if (amount > 0.0F && this.isDamageSourceBlocked(source)) {
++            float f = amount; final float originalAmount = amount; // Paper - revert to vanilla #hurt - OBFHELPER
++            boolean flag = amount > 0.0F && this.isDamageSourceBlocked(damageSource); // Copied from below;
+             float f1 = 0.0F;
+-            if (amount > 0.0F && this.isDamageSourceBlocked(damageSource)) {
 +            // CraftBukkit - Moved into handleEntityDamage(DamageSource, float) for get f and actuallyHurt(DamageSource, float, EntityDamageEvent) for handle damage
-+            if (false && amount > 0.0F && this.isDamageSourceBlocked(source)) {
++            if (false && amount > 0.0F && this.isDamageSourceBlocked(damageSource)) {
                  this.hurtCurrentlyUsedShield(amount);
-                 f2 = amount;
+                 f1 = amount;
                  amount = 0.0F;
-@@ -1202,15 +1477,21 @@
+-                if (!damageSource.is(DamageTypeTags.IS_PROJECTILE) && damageSource.getDirectEntity() instanceof LivingEntity livingEntity) {
++                if (!damageSource.is(DamageTypeTags.IS_PROJECTILE) && damageSource.getDirectEntity() instanceof LivingEntity livingEntity && livingEntity.distanceToSqr(this) <= (200.0D * 200.0D)) { // Paper - Check distance in entity interactions
+                     this.blockUsingShield(livingEntity);
+                 }
+ 
                  flag = true;
              }
  
--            if (source.is(DamageTypeTags.IS_FREEZING) && this.getType().is(EntityTypeTags.FREEZE_HURTS_EXTRA_TYPES)) {
+-            if (damageSource.is(DamageTypeTags.IS_FREEZING) && this.getType().is(EntityTypeTags.FREEZE_HURTS_EXTRA_TYPES)) {
 +            // CraftBukkit - Moved into handleEntityDamage(DamageSource, float) for get f
-+            if (false && source.is(DamageTypeTags.IS_FREEZING) && this.getType().is(EntityTypeTags.FREEZE_HURTS_EXTRA_TYPES)) {
++            if (false && damageSource.is(DamageTypeTags.IS_FREEZING) && this.getType().is(EntityTypeTags.FREEZE_HURTS_EXTRA_TYPES)) {
                  amount *= 5.0F;
              }
  
--            if (source.is(DamageTypeTags.DAMAGES_HELMET) && !this.getItemBySlot(EquipmentSlot.HEAD).isEmpty()) {
+-            if (damageSource.is(DamageTypeTags.DAMAGES_HELMET) && !this.getItemBySlot(EquipmentSlot.HEAD).isEmpty()) {
 +            // CraftBukkit - Moved into handleEntityDamage(DamageSource, float) for get f and actuallyHurt(DamageSource, float, EntityDamageEvent) for handle damage
-+            if (false && source.is(DamageTypeTags.DAMAGES_HELMET) && !this.getItemBySlot(EquipmentSlot.HEAD).isEmpty()) {
-                 this.hurtHelmet(source, amount);
++            if (false && damageSource.is(DamageTypeTags.DAMAGES_HELMET) && !this.getItemBySlot(EquipmentSlot.HEAD).isEmpty()) {
+                 this.hurtHelmet(damageSource, amount);
                  amount *= 0.75F;
              }
  
-+            // CraftBukkit start
-+            EntityDamageEvent event; // Paper - move this into the actual invuln check....
-+            // CraftBukkit end
-+
++            EntityDamageEvent event; // CraftBukkit // Paper - move this into the actual invuln check....
              this.walkAnimation.setSpeed(1.5F);
              if (Float.isNaN(amount) || Float.isInfinite(amount)) {
                  amount = Float.MAX_VALUE;
-@@ -1218,18 +1499,38 @@
+             }
  
              boolean flag1 = true;
- 
--            if ((float) this.invulnerableTime > 10.0F && !source.is(DamageTypeTags.BYPASSES_COOLDOWN)) {
-+            if ((float) this.invulnerableTime > (float) this.invulnerableDuration / 2.0F && !source.is(DamageTypeTags.BYPASSES_COOLDOWN)) { // CraftBukkit - restore use of maxNoDamageTicks
+-            if (this.invulnerableTime > 10.0F && !damageSource.is(DamageTypeTags.BYPASSES_COOLDOWN)) {
++            if (this.invulnerableTime > (float) this.invulnerableDuration / 2.0F && !damageSource.is(DamageTypeTags.BYPASSES_COOLDOWN)) { // CraftBukkit - restore use of maxNoDamageTicks
                  if (amount <= this.lastHurt) {
                      return false;
                  }
  
--                this.actuallyHurt(world, source, amount - this.lastHurt);
+-                this.actuallyHurt(level, damageSource, amount - this.lastHurt);
 +                // Paper start - only call damage event when actuallyHurt will be called - move call logic down
-+                event = this.handleEntityDamage(source, amount, this.lastHurt); // Paper - fix invulnerability reduction in EntityDamageEvent - pass lastDamage reduction
++                event = this.handleEntityDamage(damageSource, amount, this.lastHurt); // Paper - fix invulnerability reduction in EntityDamageEvent - pass lastDamage reduction
 +                amount = computeAmountFromEntityDamageEvent(event);
 +                // Paper end - only call damage event when actuallyHurt will be called - move call logic down
 +
 +                // CraftBukkit start
-+                if (!this.actuallyHurt(world, source, (float) event.getFinalDamage(), event)) { // Paper - fix invulnerability reduction in EntityDamageEvent - no longer subtract lastHurt, that is part of the damage event calc now
++                if (!this.actuallyHurt(level, damageSource, (float) event.getFinalDamage(), event)) { // Paper - fix invulnerability reduction in EntityDamageEvent - no longer subtract lastHurt, that is part of the damage event calc now
 +                    return false;
 +                }
 +                if (this instanceof ServerPlayer && event.getDamage() == 0 && originalAmount == 0) return false; // Paper - revert to vanilla damage - players are not affected by damage that is 0 - skip damage if the vanilla damage is 0 and was not modified by plugins in the event.
@@ -682,45 +562,54 @@
                  flag1 = false;
              } else {
 +                // Paper start - only call damage event when actuallyHurt will be called - move call logic down
-+                event = this.handleEntityDamage(source, amount, 0); // Paper - fix invulnerability reduction in EntityDamageEvent - pass lastDamage reduction (none in this branch)
++                event = this.handleEntityDamage(damageSource, amount, 0); // Paper - fix invulnerability reduction in EntityDamageEvent - pass lastDamage reduction (none in this branch)
 +                amount = computeAmountFromEntityDamageEvent(event);
 +                // Paper end - only call damage event when actuallyHurt will be called - move call logic down
 +                // CraftBukkit start
-+                if (!this.actuallyHurt(world, source, (float) event.getFinalDamage(), event)) {
++                if (!this.actuallyHurt(level, damageSource, (float) event.getFinalDamage(), event)) {
 +                    return false;
 +                }
 +                if (this instanceof ServerPlayer && event.getDamage() == 0 && originalAmount == 0) return false; // Paper - revert to vanilla damage - players are not affected by damage that is 0 - skip damage if the vanilla damage is 0 and was not modified by plugins in the event.
                  this.lastHurt = amount;
 -                this.invulnerableTime = 20;
--                this.actuallyHurt(world, source, amount);
+-                this.actuallyHurt(level, damageSource, amount);
 +                this.invulnerableTime = this.invulnerableDuration; // CraftBukkit - restore use of maxNoDamageTicks
-+                // this.actuallyHurt(worldserver, damagesource, f);
++                // this.actuallyHurt(level, damageSource, amount);
 +                // CraftBukkit end
                  this.hurtDuration = 10;
                  this.hurtTime = this.hurtDuration;
              }
-@@ -1243,7 +1544,7 @@
-                     world.broadcastDamageEvent(this, source);
+@@ -1170,7 +_,7 @@
+                     level.broadcastDamageEvent(this, damageSource);
                  }
  
--                if (!source.is(DamageTypeTags.NO_IMPACT) && (!flag || amount > 0.0F)) {
-+                if (!source.is(DamageTypeTags.NO_IMPACT) && !flag) { // CraftBukkit - Prevent marking hurt if the damage is blocked
+-                if (!damageSource.is(DamageTypeTags.NO_IMPACT) && (!flag || amount > 0.0F)) {
++                if (!damageSource.is(DamageTypeTags.NO_IMPACT) && !flag) { // CraftBukkit - Prevent marking hurt if the damage is blocked
                      this.markHurt();
                  }
  
-@@ -1263,7 +1564,7 @@
-                         d1 = source.getSourcePosition().z() - this.getZ();
+@@ -1185,8 +_,16 @@
+                         d = damageSource.getSourcePosition().x() - this.getX();
+                         d1 = damageSource.getSourcePosition().z() - this.getZ();
                      }
++                    // Paper start - Check distance in entity interactions; see for loop in knockback method
++                    if (Math.abs(d) > 200) {
++                        d = Math.random() - Math.random();
++                    }
++                    if (Math.abs(d1) > 200) {
++                        d1 = Math.random() - Math.random();
++                    }
++                    // Paper end - Check distance in entity interactions
  
--                    this.knockback(0.4000000059604645D, d0, d1);
-+                    this.knockback(0.4000000059604645D, d0, d1, entity1, entity1 == null ? io.papermc.paper.event.entity.EntityKnockbackEvent.Cause.DAMAGE : io.papermc.paper.event.entity.EntityKnockbackEvent.Cause.ENTITY_ATTACK); // CraftBukkit // Paper - knockback events
+-                    this.knockback(0.4F, d, d1);
++                    this.knockback(0.4F, d, d1, damageSource.getDirectEntity(), damageSource.getDirectEntity() == null ? io.papermc.paper.event.entity.EntityKnockbackEvent.Cause.DAMAGE : io.papermc.paper.event.entity.EntityKnockbackEvent.Cause.ENTITY_ATTACK); // CraftBukkit // Paper - knockback events
                      if (!flag) {
-                         this.indicateDamage(d0, d1);
+                         this.indicateDamage(d, d1);
                      }
-@@ -1272,17 +1573,18 @@
+@@ -1195,17 +_,18 @@
  
              if (this.isDeadOrDying()) {
-                 if (!this.checkTotemDeathProtection(source)) {
+                 if (!this.checkTotemDeathProtection(damageSource)) {
 -                    if (flag1) {
 -                        this.makeSound(this.getDeathSound());
 -                    }
@@ -728,43 +617,19 @@
 +                    this.silentDeath = !flag1; // mark entity as dying silently
 +                    // Paper end
  
-                     this.die(source);
+                     this.die(damageSource);
 +                    this.silentDeath = false; // Paper - cancellable death event - reset to default
                  }
              } else if (flag1) {
-                 this.playHurtSound(source);
+                 this.playHurtSound(damageSource);
              }
  
 -            boolean flag2 = !flag || amount > 0.0F;
 +            boolean flag2 = !flag; // CraftBukkit - Ensure to return false if damage is blocked
- 
              if (flag2) {
-                 this.lastDamageSource = source;
-@@ -1329,10 +1631,10 @@
-     }
- 
-     @Nullable
--    protected Player resolvePlayerResponsibleForDamage(DamageSource damageSource) {
-+    protected net.minecraft.world.entity.player.Player resolvePlayerResponsibleForDamage(DamageSource damageSource) {
-         Entity entity = damageSource.getEntity();
- 
--        if (entity instanceof Player entityhuman) {
-+        if (entity instanceof net.minecraft.world.entity.player.Player entityhuman) {
-             this.lastHurtByPlayerTime = 100;
-             this.lastHurtByPlayer = entityhuman;
-             return entityhuman;
-@@ -1342,8 +1644,8 @@
-                     this.lastHurtByPlayerTime = 100;
-                     LivingEntity entityliving = entitywolf.getOwner();
- 
--                    if (entityliving instanceof Player) {
--                        Player entityhuman1 = (Player) entityliving;
-+                    if (entityliving instanceof net.minecraft.world.entity.player.Player) {
-+                        net.minecraft.world.entity.player.Player entityhuman1 = (net.minecraft.world.entity.player.Player) entityliving;
- 
-                         this.lastHurtByPlayer = entityhuman1;
-                     } else {
-@@ -1358,12 +1660,24 @@
+                 this.lastDamageSource = damageSource;
+                 this.lastDamageStamp = this.level().getGameTime();
+@@ -1259,12 +_,24 @@
          }
      }
  
@@ -784,69 +649,67 @@
          attacker.blockedByShield(this);
      }
  
-     protected void blockedByShield(LivingEntity target) {
--        target.knockback(0.5D, target.getX() - this.getX(), target.getZ() - this.getZ());
-+        target.knockback(0.5D, target.getX() - this.getX(), target.getZ() - this.getZ(), this, io.papermc.paper.event.entity.EntityKnockbackEvent.Cause.SHIELD_BLOCK); // CraftBukkit // Paper - fix attacker & knockback events
+     protected void blockedByShield(LivingEntity defender) {
+-        defender.knockback(0.5, defender.getX() - this.getX(), defender.getZ() - this.getZ());
++        defender.knockback(0.5, defender.getX() - this.getX(), defender.getZ() - this.getZ(), this, io.papermc.paper.event.entity.EntityKnockbackEvent.Cause.SHIELD_BLOCK); // CraftBukkit // Paper - fix attacker & knockback events
      }
  
-     private boolean checkTotemDeathProtection(DamageSource source) {
-@@ -1375,20 +1689,39 @@
-             InteractionHand[] aenumhand = InteractionHand.values();
-             int i = aenumhand.length;
+     private boolean checkTotemDeathProtection(DamageSource damageSource) {
+@@ -1274,18 +_,37 @@
+             ItemStack itemStack = null;
+             DeathProtection deathProtection = null;
  
 +            // CraftBukkit start
 +            InteractionHand hand = null;
-+            ItemStack itemstack1 = ItemStack.EMPTY;
-             for (int j = 0; j < i; ++j) {
-                 InteractionHand enumhand = aenumhand[j];
--                ItemStack itemstack1 = this.getItemInHand(enumhand);
-+                itemstack1 = this.getItemInHand(enumhand);
- 
-                 deathprotection = (DeathProtection) itemstack1.get(DataComponents.DEATH_PROTECTION);
-                 if (deathprotection != null) {
-+                    hand = enumhand; // CraftBukkit
-                     itemstack = itemstack1.copy();
--                    itemstack1.shrink(1);
-+                    // itemstack1.subtract(1); // CraftBukkit
++            ItemStack itemInHand = ItemStack.EMPTY;
+             for (InteractionHand interactionHand : InteractionHand.values()) {
+-                ItemStack itemInHand = this.getItemInHand(interactionHand);
++                itemInHand = this.getItemInHand(interactionHand);
+                 deathProtection = itemInHand.get(DataComponents.DEATH_PROTECTION);
+                 if (deathProtection != null) {
++                    hand = interactionHand; // CraftBukkit
+                     itemStack = itemInHand.copy();
+-                    itemInHand.shrink(1);
++                    // itemInHand.shrink(1); // CraftBukkit
                      break;
                  }
              }
  
--            if (itemstack != null) {
--                if (this instanceof ServerPlayer) {
+-            if (itemStack != null) {
+-                if (this instanceof ServerPlayer serverPlayer) {
 +            org.bukkit.inventory.EquipmentSlot handSlot = (hand != null) ? org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand) : null;
 +            EntityResurrectEvent event = new EntityResurrectEvent((org.bukkit.entity.LivingEntity) this.getBukkitEntity(), handSlot);
-+            event.setCancelled(itemstack == null);
++            event.setCancelled(itemStack == null);
 +            this.level().getCraftServer().getPluginManager().callEvent(event);
 +
 +            if (!event.isCancelled()) {
-+                if (!itemstack1.isEmpty() && itemstack != null) { // Paper - only reduce item if actual totem was found
-+                    itemstack1.shrink(1);
++                if (!itemStack.isEmpty() && itemStack != null) { // Paper - only reduce item if actual totem was found
++                    itemStack.shrink(1);
 +                }
 +                // Paper start - fix NPE when pre-cancelled EntityResurrectEvent is uncancelled
 +                // restore the previous behavior in that case by defaulting to vanillas totem of undying efect
-+                if (deathprotection == null) {
-+                    deathprotection = DeathProtection.TOTEM_OF_UNDYING;
++                if (deathProtection == null) {
++                    deathProtection = DeathProtection.TOTEM_OF_UNDYING;
 +                }
 +                // Paper end - fix NPE when pre-cancelled EntityResurrectEvent is uncancelled
-+                if (itemstack != null && this instanceof ServerPlayer) {
++                if (itemStack != null && this instanceof ServerPlayer serverPlayer) {
 +                    // CraftBukkit end
-                     ServerPlayer entityplayer = (ServerPlayer) this;
- 
-                     entityplayer.awardStat(Stats.ITEM_USED.get(itemstack.getItem()));
-@@ -1468,6 +1801,7 @@
+                     serverPlayer.awardStat(Stats.ITEM_USED.get(itemStack.getItem()));
+                     CriteriaTriggers.USED_TOTEM.trigger(serverPlayer, itemStack);
+                     this.gameEvent(GameEvent.ITEM_INTERACT_FINISH);
+@@ -1364,6 +_,7 @@
+         if (!this.isRemoved() && !this.dead) {
              Entity entity = damageSource.getEntity();
-             LivingEntity entityliving = this.getKillCredit();
- 
+             LivingEntity killCredit = this.getKillCredit();
 +            /* // Paper - move down to make death event cancellable - this is the awardKillScore below
-             if (entityliving != null) {
-                 entityliving.awardKillScore(this, damageSource);
+             if (killCredit != null) {
+                 killCredit.awardKillScore(this, damageSource);
              }
-@@ -1477,26 +1811,61 @@
+@@ -1373,68 +_,142 @@
              }
  
              if (!this.level().isClientSide && this.hasCustomName()) {
--                LivingEntity.LOGGER.info("Named entity {} died: {}", this, this.getCombatTracker().getDeathMessage().getString());
+-                LOGGER.info("Named entity {} died: {}", this, this.getCombatTracker().getDeathMessage().getString());
 +                if (org.spigotmc.SpigotConfig.logNamedDeaths) LivingEntity.LOGGER.info("Named entity {} died: {}", this, this.getCombatTracker().getDeathMessage().getString()); // Spigot
              }
 +            */ // Paper - move down to make death event cancellable - this is the awardKillScore below
@@ -854,15 +717,11 @@
              this.dead = true;
 -            this.getCombatTracker().recheckStatus();
 +            // Paper - moved into if below
-             Level world = this.level();
- 
-             if (world instanceof ServerLevel) {
-                 ServerLevel worldserver = (ServerLevel) world;
+             if (this.level() instanceof ServerLevel serverLevel) {
+-                if (entity == null || entity.killedEntity(serverLevel, this)) {
 +                // Paper - move below into if for onKill
- 
--                if (entity == null || entity.killedEntity(worldserver, this)) {
 +                // Paper start
-+                org.bukkit.event.entity.EntityDeathEvent deathEvent = this.dropAllDeathLoot(worldserver, damageSource);
++                org.bukkit.event.entity.EntityDeathEvent deathEvent = this.dropAllDeathLoot(serverLevel, damageSource);
 +                if (deathEvent == null || !deathEvent.isCancelled()) {
 +                    //if (entityliving != null) { // Paper - Fix item duplication and teleport issues; moved to be run earlier in #dropAllDeathLoot before destroying the drop items in CraftEventFactory#callEntityDeathEvent
 +                    //    entityliving.awardKillScore(this, damageSource);
@@ -889,18 +748,18 @@
 +                        entity.killedEntity((ServerLevel) this.level(), this);
 +                    }
                      this.gameEvent(GameEvent.ENTITY_DIE);
--                    this.dropAllDeathLoot(worldserver, damageSource);
+-                    this.dropAllDeathLoot(serverLevel, damageSource);
 +                } else {
 +                    this.dead = false;
 +                    this.setHealth((float) deathEvent.getReviveHealth());
 +                }
 +                // Paper end
-                     this.createWitherRose(entityliving);
+                     this.createWitherRose(killCredit);
                  }
  
 +            // Paper start
 +            if (this.dead) { // Paper
-                 this.level().broadcastEntityEvent(this, (byte) 3);
+                 this.level().broadcastEntityEvent(this, (byte)3);
 -            }
  
              this.setPose(Pose.DYING);
@@ -909,63 +768,56 @@
          }
      }
  
-@@ -1506,20 +1875,28 @@
-         if (world instanceof ServerLevel worldserver) {
-             boolean flag = false;
- 
--            if (adversary instanceof WitherBoss) {
-+            if (this.dead && adversary instanceof WitherBoss) { // Paper
-                 if (worldserver.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) {
-                     BlockPos blockposition = this.blockPosition();
-                     BlockState iblockdata = Blocks.WITHER_ROSE.defaultBlockState();
- 
-                     if (this.level().getBlockState(blockposition).isAir() && iblockdata.canSurvive(this.level(), blockposition)) {
--                        this.level().setBlock(blockposition, iblockdata, 3);
--                        flag = true;
-+                        // CraftBukkit start - call EntityBlockFormEvent for Wither Rose
-+                        flag = org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this.level(), blockposition, iblockdata, 3, this);
-+                        // CraftBukkit end
+     protected void createWitherRose(@Nullable LivingEntity entitySource) {
+         if (this.level() instanceof ServerLevel serverLevel) {
+             boolean var6 = false;
+-            if (entitySource instanceof WitherBoss) {
++            if (this.dead && entitySource instanceof WitherBoss) { // Paper
+                 if (serverLevel.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) {
+                     BlockPos blockPos = this.blockPosition();
+                     BlockState blockState = Blocks.WITHER_ROSE.defaultBlockState();
+                     if (this.level().getBlockState(blockPos).isAir() && blockState.canSurvive(this.level(), blockPos)) {
+-                        this.level().setBlock(blockPos, blockState, 3);
+-                        var6 = true;
++                        var6 = org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this.level(), blockPos, blockState, 3, this); // CraftBukkit - call EntityBlockFormEvent for Wither Rose
                      }
                  }
  
-                 if (!flag) {
-                     ItemEntity entityitem = new ItemEntity(this.level(), this.getX(), this.getY(), this.getZ(), new ItemStack(Items.WITHER_ROSE));
- 
+                 if (!var6) {
+                     ItemEntity itemEntity = new ItemEntity(this.level(), this.getX(), this.getY(), this.getZ(), new ItemStack(Items.WITHER_ROSE));
 +                    // CraftBukkit start
-+                    org.bukkit.event.entity.EntityDropItemEvent event = new org.bukkit.event.entity.EntityDropItemEvent(this.getBukkitEntity(), (org.bukkit.entity.Item) entityitem.getBukkitEntity());
++                    org.bukkit.event.entity.EntityDropItemEvent event = new org.bukkit.event.entity.EntityDropItemEvent(this.getBukkitEntity(), (org.bukkit.entity.Item) itemEntity.getBukkitEntity());
 +                    CraftEventFactory.callEvent(event);
 +                    if (event.isCancelled()) {
 +                        return;
 +                    }
 +                    // CraftBukkit end
-                     this.level().addFreshEntity(entityitem);
+                     this.level().addFreshEntity(itemEntity);
                  }
              }
-@@ -1527,27 +1904,60 @@
          }
      }
  
--    protected void dropAllDeathLoot(ServerLevel world, DamageSource damageSource) {
+-    protected void dropAllDeathLoot(ServerLevel level, DamageSource damageSource) {
 +    // Paper start
 +    protected boolean clearEquipmentSlots = true;
 +    protected Set<EquipmentSlot> clearedEquipmentSlots = new java.util.HashSet<>();
-+    protected org.bukkit.event.entity.EntityDeathEvent dropAllDeathLoot(ServerLevel world, DamageSource damageSource) {
++    protected org.bukkit.event.entity.EntityDeathEvent dropAllDeathLoot(ServerLevel level, DamageSource damageSource) {
 +        // Paper end
          boolean flag = this.lastHurtByPlayerTime > 0;
- 
-+        this.dropEquipment(world); // CraftBukkit - from below
-         if (this.shouldDropLoot() && world.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) {
-             this.dropFromLootTable(world, damageSource, flag);
++        this.dropEquipment(level); // CraftBukkit - from below
+         if (this.shouldDropLoot() && level.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) {
+             this.dropFromLootTable(level, damageSource, flag);
 +            // Paper start
 +            final boolean prev = this.clearEquipmentSlots;
 +            this.clearEquipmentSlots = false;
 +            this.clearedEquipmentSlots.clear();
 +            // Paper end
-             this.dropCustomDeathLoot(world, damageSource, flag);
+             this.dropCustomDeathLoot(level, damageSource, flag);
 +            this.clearEquipmentSlots = prev; // Paper
          }
--
--        this.dropEquipment(world);
+ 
+-        this.dropEquipment(level);
 +        // CraftBukkit start - Call death event // Paper start - call advancement triggers with correct entity equipment
 +        org.bukkit.event.entity.EntityDeathEvent deathEvent = CraftEventFactory.callEntityDeathEvent(this, damageSource, this.drops, () -> {
 +            final LivingEntity entityliving = this.getKillCredit();
@@ -975,83 +827,82 @@
 +        }); // Paper end
 +        this.postDeathDropItems(deathEvent); // Paper
 +        this.drops = new ArrayList<>();
++        // this.dropEquipment(level); // CraftBukkit - moved up
 +        // CraftBukkit end
-+
-+        // this.dropEquipment(worldserver);// CraftBukkit - moved up
-         this.dropExperience(world, damageSource.getEntity());
+         this.dropExperience(level, damageSource.getEntity());
 +        return deathEvent; // Paper
      }
  
-     protected void dropEquipment(ServerLevel world) {}
+     protected void dropEquipment(ServerLevel level) {
+     }
 +    protected void postDeathDropItems(org.bukkit.event.entity.EntityDeathEvent event) {} // Paper - method for post death logic that cannot be ran before the event is potentially cancelled
  
--    protected void dropExperience(ServerLevel world, @Nullable Entity attacker) {
--        if (!this.wasExperienceConsumed() && (this.isAlwaysExperienceDropper() || this.lastHurtByPlayerTime > 0 && this.shouldDropExperience() && world.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT))) {
--            ExperienceOrb.award(world, this.position(), this.getExperienceReward(world, attacker));
-+    public int getExpReward(ServerLevel worldserver, @Nullable Entity entity) { // CraftBukkit
-+        if (!this.wasExperienceConsumed() && (this.isAlwaysExperienceDropper() || this.lastHurtByPlayerTime > 0 && this.shouldDropExperience() && worldserver.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT))) {
-+            return this.getExperienceReward(worldserver, entity); // CraftBukkit            }
-         }
- 
+-    protected void dropExperience(ServerLevel level, @Nullable Entity entity) {
++    public int getExpReward(ServerLevel level, @Nullable Entity entity) { // CraftBukkit
+         if (!this.wasExperienceConsumed()
+             && (
+                 this.isAlwaysExperienceDropper()
+                     || this.lastHurtByPlayerTime > 0 && this.shouldDropExperience() && level.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)
+             )) {
+-            ExperienceOrb.award(level, this.position(), this.getExperienceReward(level, entity));
+-        }
++            return this.getExperienceReward(level, entity); // CraftBukkit
++        }
 +        return 0; // CraftBukkit
-     }
- 
-+    protected void dropExperience(ServerLevel world, @Nullable Entity attacker) {
++    }
++
++    protected void dropExperience(ServerLevel level, @Nullable Entity entity) {
 +        // CraftBukkit start - Update getExpReward() above if the removed if() changes!
 +        if (!(this instanceof net.minecraft.world.entity.boss.enderdragon.EnderDragon)) { // CraftBukkit - SPIGOT-2420: Special case ender dragon will drop the xp over time
-+            ExperienceOrb.award(world, this.position(), this.expToDrop, this instanceof ServerPlayer ? org.bukkit.entity.ExperienceOrb.SpawnReason.PLAYER_DEATH : org.bukkit.entity.ExperienceOrb.SpawnReason.ENTITY_DEATH, attacker, this); // Paper
++            ExperienceOrb.award(level, this.position(), this.expToDrop, this instanceof ServerPlayer ? org.bukkit.entity.ExperienceOrb.SpawnReason.PLAYER_DEATH : org.bukkit.entity.ExperienceOrb.SpawnReason.ENTITY_DEATH, entity, this); // Paper
 +            this.expToDrop = 0;
 +        }
 +        // CraftBukkit end
-+    }
-+
-     protected void dropCustomDeathLoot(ServerLevel world, DamageSource source, boolean causedByPlayer) {}
+     }
  
-     public long getLootTableSeed() {
-@@ -1612,19 +2022,35 @@
+     protected void dropCustomDeathLoot(ServerLevel level, DamageSource damageSource, boolean recentlyHit) {
+@@ -1513,9 +_,14 @@
      }
  
      public void knockback(double strength, double x, double z) {
--        strength *= 1.0D - this.getAttributeValue(Attributes.KNOCKBACK_RESISTANCE);
--        if (strength > 0.0D) {
--            this.hasImpulse = true;
 +        // CraftBukkit start - EntityKnockbackEvent
 +        this.knockback(strength, x, z, null, io.papermc.paper.event.entity.EntityKnockbackEvent.Cause.UNKNOWN); // Paper - knockback events
 +    }
- 
-+    public void knockback(double d0, double d1, double d2, @Nullable Entity attacker, io.papermc.paper.event.entity.EntityKnockbackEvent.Cause cause) { // Paper - knockback events
-+        d0 *= 1.0D - this.getAttributeValue(Attributes.KNOCKBACK_RESISTANCE);
-+        if (true || d0 > 0.0D) { // CraftBukkit - Call event even when force is 0
-+            //this.hasImpulse = true; // CraftBukkit - Move down
 +
-             Vec3 vec3d;
++    public void knockback(double strength, double x, double z, @Nullable Entity attacker, io.papermc.paper.event.entity.EntityKnockbackEvent.Cause eventCause) { // Paper - knockback events
+         strength *= 1.0 - this.getAttributeValue(Attributes.KNOCKBACK_RESISTANCE);
+-        if (!(strength <= 0.0)) {
+-            this.hasImpulse = true;
++        if (true || !(strength <= 0.0)) { // CraftBukkit - Call event even when force is 0
++            // this.hasImpulse = true; // CraftBukkit - Move down
+             Vec3 deltaMovement = this.getDeltaMovement();
  
--            for (vec3d = this.getDeltaMovement(); x * x + z * z < 9.999999747378752E-6D; z = (Math.random() - Math.random()) * 0.01D) {
--                x = (Math.random() - Math.random()) * 0.01D;
-+            for (vec3d = this.getDeltaMovement(); d1 * d1 + d2 * d2 < 9.999999747378752E-6D; d2 = (Math.random() - Math.random()) * 0.01D) {
-+                d1 = (Math.random() - Math.random()) * 0.01D;
+             while (x * x + z * z < 1.0E-5F) {
+@@ -1524,11 +_,22 @@
              }
  
--            Vec3 vec3d1 = (new Vec3(x, 0.0D, z)).normalize().scale(strength);
-+            Vec3 vec3d1 = (new Vec3(d1, 0.0D, d2)).normalize().scale(d0);
- 
--            this.setDeltaMovement(vec3d.x / 2.0D - vec3d1.x, this.onGround() ? Math.min(0.4D, vec3d.y / 2.0D + strength) : vec3d.y, vec3d.z / 2.0D - vec3d1.z);
+             Vec3 vec3 = new Vec3(x, 0.0, z).normalize().scale(strength);
+-            this.setDeltaMovement(
 +            // Paper start - knockback events
-+            Vec3 finalVelocity = new Vec3(vec3d.x / 2.0D - vec3d1.x, this.onGround() ? Math.min(0.4D, vec3d.y / 2.0D + d0) : vec3d.y, vec3d.z / 2.0D - vec3d1.z);
-+            Vec3 diff = finalVelocity.subtract(vec3d);
-+            io.papermc.paper.event.entity.EntityKnockbackEvent event = CraftEventFactory.callEntityKnockbackEvent((org.bukkit.craftbukkit.entity.CraftLivingEntity) this.getBukkitEntity(), attacker, attacker, cause, d0, diff);
++            Vec3 finalVelocity = new Vec3(
+                 deltaMovement.x / 2.0 - vec3.x,
+                 this.onGround() ? Math.min(0.4, deltaMovement.y / 2.0 + strength) : deltaMovement.y,
+                 deltaMovement.z / 2.0 - vec3.z
+             );
++            Vec3 diff = finalVelocity.subtract(deltaMovement);
++            io.papermc.paper.event.entity.EntityKnockbackEvent event = CraftEventFactory.callEntityKnockbackEvent((org.bukkit.craftbukkit.entity.CraftLivingEntity) this.getBukkitEntity(), attacker, attacker, eventCause, strength, diff);
 +            // Paper end - knockback events
 +            if (event.isCancelled()) {
 +                return;
 +            }
 +
 +            this.hasImpulse = true;
-+            this.setDeltaMovement(vec3d.add(event.getKnockback().getX(), event.getKnockback().getY(), event.getKnockback().getZ())); // Paper - knockback events
++            this.setDeltaMovement(deltaMovement.add(event.getKnockback().getX(), event.getKnockback().getY(), event.getKnockback().getZ())); // Paper - knockback events
 +            // CraftBukkit end
          }
      }
  
-@@ -1683,6 +2109,20 @@
+@@ -1584,6 +_,20 @@
          return new LivingEntity.Fallsounds(SoundEvents.GENERIC_SMALL_FALL, SoundEvents.GENERIC_BIG_FALL);
      }
  
@@ -1072,7 +923,7 @@
      public Optional<BlockPos> getLastClimbablePos() {
          return this.lastClimbablePos;
      }
-@@ -1718,7 +2158,7 @@
+@@ -1617,7 +_,7 @@
  
      @Override
      public boolean isAlive() {
@@ -1080,54 +931,50 @@
 +        return !this.isRemoved() && this.getHealth() > 0.0F && !this.dead; // Paper - Check this.dead
      }
  
-     public boolean isLookingAtMe(LivingEntity entity, double d0, boolean flag, boolean visualShape, double... checkedYs) {
-@@ -1757,9 +2197,14 @@
-         int i = this.calculateFallDamage(fallDistance, damageMultiplier);
- 
+     public boolean isLookingAtMe(LivingEntity entity, double tolerance, boolean scaleByDistance, boolean visual, double... yValues) {
+@@ -1651,9 +_,14 @@
+         boolean flag = super.causeFallDamage(fallDistance, multiplier, source);
+         int i = this.calculateFallDamage(fallDistance, multiplier);
          if (i > 0) {
 +            // CraftBukkit start
-+            if (!this.hurtServer((ServerLevel) this.level(), damageSource, (float) i)) {
++            if (!this.hurtServer((ServerLevel) this.level(), source, (float) i)) {
 +                return true;
 +            }
 +            // CraftBukkit end
              this.playSound(this.getFallDamageSound(i), 1.0F, 1.0F);
              this.playBlockFallSound();
--            this.hurt(damageSource, (float) i);
-+            // this.damageEntity(damagesource, (float) i); // CraftBukkit - moved up
+-            this.hurt(source, i);
++            // this.hurt(source, i); // CraftBukkit - moved up
              return true;
          } else {
              return flag;
-@@ -1830,7 +2275,7 @@
+@@ -1718,7 +_,7 @@
  
-     protected float getDamageAfterArmorAbsorb(DamageSource source, float amount) {
-         if (!source.is(DamageTypeTags.BYPASSES_ARMOR)) {
--            this.hurtArmor(source, amount);
-+            // this.hurtArmor(damagesource, f); // CraftBukkit - actuallyHurt(DamageSource, float, EntityDamageEvent) for handle damage
-             amount = CombatRules.getDamageAfterAbsorb(this, amount, source, (float) this.getArmorValue(), (float) this.getAttributeValue(Attributes.ARMOR_TOUGHNESS));
-         }
- 
-@@ -1841,7 +2286,8 @@
-         if (source.is(DamageTypeTags.BYPASSES_EFFECTS)) {
-             return amount;
+     protected float getDamageAfterArmorAbsorb(DamageSource damageSource, float damageAmount) {
+         if (!damageSource.is(DamageTypeTags.BYPASSES_ARMOR)) {
+-            this.hurtArmor(damageSource, damageAmount);
++            // this.hurtArmor(damageSource, damageAmount); // CraftBukkit - actuallyHurt(DamageSource, float, EntityDamageEvent) for damage handling
+             damageAmount = CombatRules.getDamageAfterAbsorb(
+                 this, damageAmount, damageSource, this.getArmorValue(), (float)this.getAttributeValue(Attributes.ARMOR_TOUGHNESS)
+             );
+@@ -1731,7 +_,8 @@
+         if (damageSource.is(DamageTypeTags.BYPASSES_EFFECTS)) {
+             return damageAmount;
          } else {
--            if (this.hasEffect(MobEffects.DAMAGE_RESISTANCE) && !source.is(DamageTypeTags.BYPASSES_RESISTANCE)) {
+-            if (this.hasEffect(MobEffects.DAMAGE_RESISTANCE) && !damageSource.is(DamageTypeTags.BYPASSES_RESISTANCE)) {
 +            // CraftBukkit - Moved to handleEntityDamage(DamageSource, float)
-+            if (false && this.hasEffect(MobEffects.DAMAGE_RESISTANCE) && !source.is(DamageTypeTags.BYPASSES_RESISTANCE)) {
++            if (false && this.hasEffect(MobEffects.DAMAGE_RESISTANCE) && !damageSource.is(DamageTypeTags.BYPASSES_RESISTANCE)) {
                  int i = (this.getEffect(MobEffects.DAMAGE_RESISTANCE).getAmplifier() + 1) * 5;
-                 int j = 25 - i;
-                 float f1 = amount * (float) j;
-@@ -1884,18 +2330,170 @@
+                 int i1 = 25 - i;
+                 float f = damageAmount * i1;
+@@ -1768,24 +_,212 @@
          }
      }
  
--    protected void actuallyHurt(ServerLevel world, DamageSource source, float amount) {
--        if (!this.isInvulnerableTo(world, source)) {
--            amount = this.getDamageAfterArmorAbsorb(source, amount);
--            amount = this.getDamageAfterMagicAbsorb(source, amount);
--            float f1 = amount;
+-    protected void actuallyHurt(ServerLevel level, DamageSource damageSource, float amount) {
 +    // CraftBukkit start
-+    private EntityDamageEvent handleEntityDamage(final DamageSource damagesource, float f, final float invulnerabilityRelatedLastDamage) { // Paper - fix invulnerability reduction in EntityDamageEvent
-+        float originalDamage = f;
++    private EntityDamageEvent handleEntityDamage(final DamageSource damagesource, float amount, final float invulnerabilityRelatedLastDamage) { // Paper - fix invulnerability reduction in EntityDamageEvent
++        float originalDamage = amount;
 +        // Paper start - fix invulnerability reduction in EntityDamageEvent
 +        final com.google.common.base.Function<Double, Double> invulnerabilityReductionEquation = d -> {
 +            if (invulnerabilityRelatedLastDamage == 0) return 0D; // no last damage, no reduction
@@ -1136,13 +983,10 @@
 +            if (d < invulnerabilityRelatedLastDamage) return 0D;
 +            return (double) -invulnerabilityRelatedLastDamage;
 +        };
-+        final float originalInvulnerabilityReduction = invulnerabilityReductionEquation.apply((double) f).floatValue();
-+        f += originalInvulnerabilityReduction;
++        final float originalInvulnerabilityReduction = invulnerabilityReductionEquation.apply((double) amount).floatValue();
++        amount += originalInvulnerabilityReduction;
 +        // Paper end - fix invulnerability reduction in EntityDamageEvent
- 
--            amount = Math.max(amount - this.getAbsorptionAmount(), 0.0F);
--            this.setAbsorptionAmount(this.getAbsorptionAmount() - (f1 - amount));
--            float f2 = f1 - amount;
++
 +        com.google.common.base.Function<Double, Double> freezing = new com.google.common.base.Function<Double, Double>() {
 +            @Override
 +            public Double apply(Double f) {
@@ -1152,9 +996,9 @@
 +                return -0.0;
 +            }
 +        };
-+        float freezingModifier = freezing.apply((double) f).floatValue();
-+        f += freezingModifier;
- 
++        float freezingModifier = freezing.apply((double) amount).floatValue();
++        amount += freezingModifier;
++
 +        com.google.common.base.Function<Double, Double> hardHat = new com.google.common.base.Function<Double, Double>() {
 +            @Override
 +            public Double apply(Double f) {
@@ -1164,8 +1008,8 @@
 +                return -0.0;
 +            }
 +        };
-+        float hardHatModifier = hardHat.apply((double) f).floatValue();
-+        f += hardHatModifier;
++        float hardHatModifier = hardHat.apply((double) amount).floatValue();
++        amount += hardHatModifier;
 +
 +        com.google.common.base.Function<Double, Double> blocking = new com.google.common.base.Function<Double, Double>() {
 +            @Override
@@ -1173,8 +1017,8 @@
 +                return -((LivingEntity.this.isDamageSourceBlocked(damagesource)) ? f : 0.0);
 +            }
 +        };
-+        float blockingModifier = blocking.apply((double) f).floatValue();
-+        f += blockingModifier;
++        float blockingModifier = blocking.apply((double) amount).floatValue();
++        amount += blockingModifier;
 +
 +        com.google.common.base.Function<Double, Double> armor = new com.google.common.base.Function<Double, Double>() {
 +            @Override
@@ -1182,8 +1026,8 @@
 +                return -(f - LivingEntity.this.getDamageAfterArmorAbsorb(damagesource, f.floatValue()));
 +            }
 +        };
-+        float armorModifier = armor.apply((double) f).floatValue();
-+        f += armorModifier;
++        float armorModifier = armor.apply((double) amount).floatValue();
++        amount += armorModifier;
 +
 +        com.google.common.base.Function<Double, Double> resistance = new com.google.common.base.Function<Double, Double>() {
 +            @Override
@@ -1198,8 +1042,8 @@
 +                return -0.0;
 +            }
 +        };
-+        float resistanceModifier = resistance.apply((double) f).floatValue();
-+        f += resistanceModifier;
++        float resistanceModifier = resistance.apply((double) amount).floatValue();
++        amount += resistanceModifier;
 +
 +        com.google.common.base.Function<Double, Double> magic = new com.google.common.base.Function<Double, Double>() {
 +            @Override
@@ -1207,8 +1051,8 @@
 +                return -(f - LivingEntity.this.getDamageAfterMagicAbsorb(damagesource, f.floatValue()));
 +            }
 +        };
-+        float magicModifier = magic.apply((double) f).floatValue();
-+        f += magicModifier;
++        float magicModifier = magic.apply((double) amount).floatValue();
++        amount += magicModifier;
 +
 +        com.google.common.base.Function<Double, Double> absorption = new com.google.common.base.Function<Double, Double>() {
 +            @Override
@@ -1216,7 +1060,7 @@
 +                return -(Math.max(f - Math.max(f - LivingEntity.this.getAbsorptionAmount(), 0.0F), 0.0F));
 +            }
 +        };
-+        float absorptionModifier = absorption.apply((double) f).floatValue();
++        float absorptionModifier = absorption.apply((double) amount).floatValue();
 +
 +        // Paper start - fix invulnerability reduction in EntityDamageEvent
 +        return CraftEventFactory.handleLivingEntityDamageEvent(this, damagesource, originalDamage, freezingModifier, hardHatModifier, blockingModifier, armorModifier, resistanceModifier, magicModifier, absorptionModifier, freezing, hardHat, blocking, armor, resistance, magic, absorption, (damageModifierDoubleMap, damageModifierFunctionMap) -> {
@@ -1226,22 +1070,27 @@
 +        // Paper end - fix invulnerability reduction in EntityDamageEvent
 +    }
 +
-+    protected boolean actuallyHurt(ServerLevel worldserver, final DamageSource damagesource, float f, final EntityDamageEvent event) { // void -> boolean, add final
-+        if (!this.isInvulnerableTo(worldserver, damagesource)) {
++    protected boolean actuallyHurt(ServerLevel level, final DamageSource damageSource, float amount, final EntityDamageEvent event) { // void -> boolean, add final
+         if (!this.isInvulnerableTo(level, damageSource)) {
+-            amount = this.getDamageAfterArmorAbsorb(damageSource, amount);
+-            amount = this.getDamageAfterMagicAbsorb(damageSource, amount);
+-            float var10 = Math.max(amount - this.getAbsorptionAmount(), 0.0F);
+-            this.setAbsorptionAmount(this.getAbsorptionAmount() - (amount - var10));
+-            float f1 = amount - var10;
 +            if (event.isCancelled()) {
 +                return false;
 +            }
 +
-+            if (damagesource.getEntity() instanceof net.minecraft.world.entity.player.Player) {
++            if (damageSource.getEntity() instanceof net.minecraft.world.entity.player.Player) {
 +                // Paper start - PlayerAttackEntityCooldownResetEvent
 +                //((net.minecraft.world.entity.player.Player) damagesource.getEntity()).resetAttackStrengthTicker(); // Moved from EntityHuman in order to make the cooldown reset get called after the damage event is fired
-+                if (damagesource.getEntity() instanceof ServerPlayer) {
-+                    ServerPlayer player = (ServerPlayer) damagesource.getEntity();
++                if (damageSource.getEntity() instanceof ServerPlayer) {
++                    ServerPlayer player = (ServerPlayer) damageSource.getEntity();
 +                    if (new com.destroystokyo.paper.event.player.PlayerAttackEntityCooldownResetEvent(player.getBukkitEntity(), this.getBukkitEntity(), player.getAttackStrengthScale(0F)).callEvent()) {
 +                        player.resetAttackStrengthTicker();
 +                    }
 +                } else {
-+                    ((net.minecraft.world.entity.player.Player) damagesource.getEntity()).resetAttackStrengthTicker();
++                    ((net.minecraft.world.entity.player.Player) damageSource.getEntity()).resetAttackStrengthTicker();
 +                }
 +                // Paper end - PlayerAttackEntityCooldownResetEvent
 +            }
@@ -1252,29 +1101,29 @@
 +                if (f3 > 0.0F && f3 < 3.4028235E37F) {
 +                    if (this instanceof ServerPlayer) {
 +                        ((ServerPlayer) this).awardStat(Stats.DAMAGE_RESISTED, Math.round(f3 * 10.0F));
-+                    } else if (damagesource.getEntity() instanceof ServerPlayer) {
-+                        ((ServerPlayer) damagesource.getEntity()).awardStat(Stats.DAMAGE_DEALT_RESISTED, Math.round(f3 * 10.0F));
++                    } else if (damageSource.getEntity() instanceof ServerPlayer) {
++                        ((ServerPlayer) damageSource.getEntity()).awardStat(Stats.DAMAGE_DEALT_RESISTED, Math.round(f3 * 10.0F));
 +                    }
 +                }
 +            }
 +
 +            // Apply damage to helmet
-+            if (damagesource.is(DamageTypeTags.DAMAGES_HELMET) && !this.getItemBySlot(EquipmentSlot.HEAD).isEmpty()) {
-+                this.hurtHelmet(damagesource, f);
++            if (damageSource.is(DamageTypeTags.DAMAGES_HELMET) && !this.getItemBySlot(EquipmentSlot.HEAD).isEmpty()) {
++                this.hurtHelmet(damageSource, amount);
 +            }
 +
 +            // Apply damage to armor
-+            if (!damagesource.is(DamageTypeTags.BYPASSES_ARMOR)) {
++            if (!damageSource.is(DamageTypeTags.BYPASSES_ARMOR)) {
 +                float armorDamage = (float) (event.getDamage() + event.getDamage(DamageModifier.BLOCKING) + event.getDamage(DamageModifier.HARD_HAT));
-+                this.hurtArmor(damagesource, armorDamage);
++                this.hurtArmor(damageSource, armorDamage);
 +            }
 +
 +            // Apply blocking code // PAIL: steal from above
 +            if (event.getDamage(DamageModifier.BLOCKING) < 0) {
 +                this.hurtCurrentlyUsedShield((float) -event.getDamage(DamageModifier.BLOCKING));
-+                Entity entity = damagesource.getDirectEntity();
++                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 & Check distance in entity interactions
 +                    this.blockUsingShield((LivingEntity) entity);
 +                }
 +            }
@@ -1283,59 +1132,51 @@
 +            float originalDamage = (float) event.getDamage();
 +            float absorptionModifier = (float) -event.getDamage(DamageModifier.ABSORPTION);
 +            this.setAbsorptionAmount(Math.max(this.getAbsorptionAmount() - absorptionModifier, 0.0F));
-+            float f2 = absorptionModifier;
++            float f1 = absorptionModifier;
 +
-+            if (f2 > 0.0F && f2 < 3.4028235E37F && this instanceof net.minecraft.world.entity.player.Player) {
-+                ((net.minecraft.world.entity.player.Player) this).awardStat(Stats.DAMAGE_ABSORBED, Math.round(f2 * 10.0F));
++            if (f1 > 0.0F && f1 < 3.4028235E37F && this instanceof Player player) {
++                player.awardStat(Stats.DAMAGE_ABSORBED, Math.round(f1 * 10.0F));
 +            }
 +            // CraftBukkit end
-+
-             if (f2 > 0.0F && f2 < 3.4028235E37F) {
--                Entity entity = source.getEntity();
-+                Entity entity = damagesource.getEntity();
- 
-                 if (entity instanceof ServerPlayer) {
-                     ServerPlayer entityplayer = (ServerPlayer) entity;
-@@ -1904,13 +2502,48 @@
-                 }
+             if (f1 > 0.0F && f1 < 3.4028235E37F && damageSource.getEntity() instanceof ServerPlayer serverPlayer) {
+                 serverPlayer.awardStat(Stats.DAMAGE_DEALT_ABSORBED, Math.round(f1 * 10.0F));
              }
  
--            if (amount != 0.0F) {
--                this.getCombatTracker().recordDamage(source, amount);
--                this.setHealth(this.getHealth() - amount);
--                this.setAbsorptionAmount(this.getAbsorptionAmount() - amount);
+-            if (var10 != 0.0F) {
+-                this.getCombatTracker().recordDamage(damageSource, var10);
+-                this.setHealth(this.getHealth() - var10);
+-                this.setAbsorptionAmount(this.getAbsorptionAmount() - var10);
 +            // CraftBukkit start
-+            if (f > 0 || !human) {
++            if (amount > 0 || !human) {
 +                if (human) {
 +                    // PAIL: Be sure to drag all this code from the EntityHuman subclass each update.
-+                    ((net.minecraft.world.entity.player.Player) this).causeFoodExhaustion(damagesource.getFoodExhaustion(), org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.DAMAGED); // CraftBukkit - EntityExhaustionEvent
-+                    if (f < 3.4028235E37F) {
-+                        ((net.minecraft.world.entity.player.Player) this).awardStat(Stats.DAMAGE_TAKEN, Math.round(f * 10.0F));
++                    ((net.minecraft.world.entity.player.Player) this).causeFoodExhaustion(damageSource.getFoodExhaustion(), org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.DAMAGED); // CraftBukkit - EntityExhaustionEvent
++                    if (amount < 3.4028235E37F) {
++                        ((net.minecraft.world.entity.player.Player) this).awardStat(Stats.DAMAGE_TAKEN, Math.round(amount * 10.0F));
 +                    }
 +                }
 +                // CraftBukkit end
-+                this.getCombatTracker().recordDamage(damagesource, f);
-+                this.setHealth(this.getHealth() - f);
++                this.getCombatTracker().recordDamage(damageSource, amount);
++                this.setHealth(this.getHealth() - amount);
 +                // CraftBukkit start
 +                if (!human) {
-+                    this.setAbsorptionAmount(this.getAbsorptionAmount() - f);
++                    this.setAbsorptionAmount(this.getAbsorptionAmount() - amount);
 +                }
                  this.gameEvent(GameEvent.ENTITY_DAMAGE);
-+
 +                return true;
 +            } else {
 +                // Duplicate triggers if blocking
 +                if (event.getDamage(DamageModifier.BLOCKING) < 0) {
 +                    if (this instanceof ServerPlayer) {
-+                        CriteriaTriggers.ENTITY_HURT_PLAYER.trigger((ServerPlayer) this, damagesource, originalDamage, f, true); // Paper - fix taken/dealt param order
-+                        f2 = (float) -event.getDamage(DamageModifier.BLOCKING);
-+                        if (f2 > 0.0F && f2 < 3.4028235E37F) {
++                        CriteriaTriggers.ENTITY_HURT_PLAYER.trigger((ServerPlayer) this, damageSource, originalDamage, amount, true); // Paper - fix taken/dealt param order
++                        f1 = (float) -event.getDamage(DamageModifier.BLOCKING);
++                        if (f1 > 0.0F && f1 < 3.4028235E37F) {
 +                            ((ServerPlayer) this).awardStat(Stats.DAMAGE_BLOCKED_BY_SHIELD, Math.round(originalDamage * 10.0F));
 +                        }
 +                    }
 +
-+                    if (damagesource.getEntity() instanceof ServerPlayer) {
-+                        CriteriaTriggers.PLAYER_HURT_ENTITY.trigger((ServerPlayer) damagesource.getEntity(), this, damagesource, originalDamage, f, true); // Paper - fix taken/dealt param order
++                    if (damageSource.getEntity() instanceof ServerPlayer) {
++                        CriteriaTriggers.PLAYER_HURT_ENTITY.trigger((ServerPlayer) damageSource.getEntity(), this, damageSource, originalDamage, amount, true); // Paper - fix taken/dealt param order
 +                    }
 +
 +                    return !io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.skipVanillaDamageTickWhenShieldBlocked; // Paper - this should always return true, however expose an unsupported setting to flip this to false to enable "shield stunning".
@@ -1349,36 +1190,26 @@
      }
  
      public CombatTracker getCombatTracker() {
-@@ -1935,8 +2568,18 @@
+@@ -1814,7 +_,17 @@
      }
  
-     public final void setArrowCount(int stuckArrowCount) {
--        this.entityData.set(LivingEntity.DATA_ARROW_COUNT_ID, stuckArrowCount);
+     public final void setArrowCount(int count) {
+-        this.entityData.set(DATA_ARROW_COUNT_ID, count);
 +        // CraftBukkit start
-+        this.setArrowCount(stuckArrowCount, false);
++        this.setArrowCount(count, false);
 +    }
 +
-+    public final void setArrowCount(int i, boolean flag) {
-+        ArrowBodyCountChangeEvent event = CraftEventFactory.callArrowBodyCountChangeEvent(this, this.getArrowCount(), i, flag);
++    public final void setArrowCount(int count, boolean reset) {
++        ArrowBodyCountChangeEvent event = CraftEventFactory.callArrowBodyCountChangeEvent(this, this.getArrowCount(), count, reset);
 +        if (event.isCancelled()) {
 +            return;
 +        }
-+        this.entityData.set(LivingEntity.DATA_ARROW_COUNT_ID, event.getNewAmount());
++        this.entityData.set(DATA_ARROW_COUNT_ID, event.getNewAmount());
++        // CraftBukkit end
      }
-+    // CraftBukkit end
  
      public final int getStingerCount() {
-         return (Integer) this.entityData.get(LivingEntity.DATA_STINGER_COUNT_ID);
-@@ -1999,7 +2642,7 @@
-                     this.playSound(soundeffect, this.getSoundVolume(), (this.random.nextFloat() - this.random.nextFloat()) * 0.2F + 1.0F);
-                 }
- 
--                if (!(this instanceof Player)) {
-+                if (!(this instanceof net.minecraft.world.entity.player.Player)) {
-                     this.setHealth(0.0F);
-                     this.die(this.damageSources().generic());
-                 }
-@@ -2083,7 +2726,7 @@
+@@ -1957,7 +_,7 @@
  
      @Override
      protected void onBelowWorld() {
@@ -1387,7 +1218,7 @@
      }
  
      protected void updateSwingTime() {
-@@ -2182,6 +2825,12 @@
+@@ -2052,6 +_,12 @@
  
      public abstract ItemStack getItemBySlot(EquipmentSlot slot);
  
@@ -1400,17 +1231,16 @@
      public abstract void setItemSlot(EquipmentSlot slot, ItemStack stack);
  
      public Iterable<ItemStack> getHandSlots() {
-@@ -2292,17 +2941,29 @@
-         return this.hasEffect(MobEffects.JUMP) ? 0.1F * ((float) this.getEffect(MobEffects.JUMP).getAmplifier() + 1.0F) : 0.0F;
+@@ -2158,14 +_,27 @@
+         return this.hasEffect(MobEffects.JUMP) ? 0.1F * (this.getEffect(MobEffects.JUMP).getAmplifier() + 1.0F) : 0.0F;
      }
  
 +    protected long lastJumpTime = 0L; // Paper - Prevent excessive velocity through repeated crits
      @VisibleForTesting
      public void jumpFromGround() {
-         float f = this.getJumpPower();
- 
-         if (f > 1.0E-5F) {
-             Vec3 vec3d = this.getDeltaMovement();
+         float jumpPower = this.getJumpPower();
+         if (!(jumpPower <= 1.0E-5F)) {
+             Vec3 deltaMovement = this.getDeltaMovement();
 +            // Paper start - Prevent excessive velocity through repeated crits
 +            long time = System.nanoTime();
 +            boolean canCrit = true;
@@ -1422,51 +1252,14 @@
 +                }
 +            }
 +            // Paper end - Prevent excessive velocity through repeated crits
- 
-             this.setDeltaMovement(vec3d.x, Math.max((double) f, vec3d.y), vec3d.z);
+             this.setDeltaMovement(deltaMovement.x, Math.max((double)jumpPower, deltaMovement.y), deltaMovement.z);
              if (this.isSprinting()) {
-                 float f1 = this.getYRot() * 0.017453292F;
--
+                 float f = this.getYRot() * (float) (Math.PI / 180.0);
 +                if (canCrit) // Paper - Prevent excessive velocity through repeated crits
-                 this.addDeltaMovement(new Vec3((double) (-Mth.sin(f1)) * 0.2D, 0.0D, (double) Mth.cos(f1) * 0.2D));
+                 this.addDeltaMovement(new Vec3(-Mth.sin(f) * 0.2, 0.0, Mth.cos(f) * 0.2));
              }
  
-@@ -2494,7 +3155,7 @@
- 
-     }
- 
--    private void travelRidden(Player controllingPlayer, Vec3 movementInput) {
-+    private void travelRidden(net.minecraft.world.entity.player.Player controllingPlayer, Vec3 movementInput) {
-         Vec3 vec3d1 = this.getRiddenInput(controllingPlayer, movementInput);
- 
-         this.tickRidden(controllingPlayer, vec3d1);
-@@ -2507,13 +3168,13 @@
- 
-     }
- 
--    protected void tickRidden(Player controllingPlayer, Vec3 movementInput) {}
-+    protected void tickRidden(net.minecraft.world.entity.player.Player controllingPlayer, Vec3 movementInput) {}
- 
--    protected Vec3 getRiddenInput(Player controllingPlayer, Vec3 movementInput) {
-+    protected Vec3 getRiddenInput(net.minecraft.world.entity.player.Player controllingPlayer, Vec3 movementInput) {
-         return movementInput;
-     }
- 
--    protected float getRiddenSpeed(Player controllingPlayer) {
-+    protected float getRiddenSpeed(net.minecraft.world.entity.player.Player controllingPlayer) {
-         return this.getSpeed();
-     }
- 
-@@ -2571,7 +3232,7 @@
-             double d1 = Mth.clamp(motion.z, -0.15000000596046448D, 0.15000000596046448D);
-             double d2 = Math.max(motion.y, -0.15000000596046448D);
- 
--            if (d2 < 0.0D && !this.getInBlockState().is(Blocks.SCAFFOLDING) && this.isSuppressingSlidingDownLadder() && this instanceof Player) {
-+            if (d2 < 0.0D && !this.getInBlockState().is(Blocks.SCAFFOLDING) && this.isSuppressingSlidingDownLadder() && this instanceof net.minecraft.world.entity.player.Player) {
-                 d2 = 0.0D;
-             }
- 
-@@ -2586,7 +3247,7 @@
+@@ -2425,7 +_,7 @@
      }
  
      protected float getFlyingSpeed() {
@@ -1475,7 +1268,7 @@
      }
  
      public float getSpeed() {
-@@ -2634,7 +3295,7 @@
+@@ -2471,7 +_,7 @@
                  }
              }
  
@@ -1484,26 +1277,22 @@
              if (this.tickCount % 20 == 0) {
                  this.getCombatTracker().recheckStatus();
              }
-@@ -2687,38 +3348,16 @@
-         gameprofilerfiller.pop();
-         gameprofilerfiller.push("rangeChecks");
+@@ -2519,37 +_,14 @@
+         profilerFiller.pop();
+         profilerFiller.push("rangeChecks");
  
 -        while (this.getYRot() - this.yRotO < -180.0F) {
 -            this.yRotO -= 360.0F;
 -        }
-+        // Paper start - stop large pitch and yaw changes from crashing the server
-+        this.yRotO += Math.round((this.getYRot() - this.yRotO) / 360.0F) * 360.0F;
- 
+-
 -        while (this.getYRot() - this.yRotO >= 180.0F) {
 -            this.yRotO += 360.0F;
 -        }
-+        this.yBodyRotO += Math.round((this.yBodyRot - this.yBodyRotO) / 360.0F) * 360.0F;
- 
+-
 -        while (this.yBodyRot - this.yBodyRotO < -180.0F) {
 -            this.yBodyRotO -= 360.0F;
 -        }
-+        this.xRotO += Math.round((this.getXRot() - this.xRotO) / 360.0F) * 360.0F;
- 
+-
 -        while (this.yBodyRot - this.yBodyRotO >= 180.0F) {
 -            this.yBodyRotO += 360.0F;
 -        }
@@ -1515,9 +1304,7 @@
 -        while (this.getXRot() - this.xRotO >= 180.0F) {
 -            this.xRotO += 360.0F;
 -        }
-+        this.yHeadRotO += Math.round((this.yHeadRot - this.yHeadRotO) / 360.0F) * 360.0F;
-+        // Paper end
- 
+-
 -        while (this.yHeadRot - this.yHeadRotO < -180.0F) {
 -            this.yHeadRotO -= 360.0F;
 -        }
@@ -1525,83 +1312,75 @@
 -        while (this.yHeadRot - this.yHeadRotO >= 180.0F) {
 -            this.yHeadRotO += 360.0F;
 -        }
--
-         gameprofilerfiller.pop();
++        // Paper start - stop large pitch and yaw changes from crashing the server
++        this.yRotO += Math.round((this.getYRot() - this.yRotO) / 360.0F) * 360.0F;
++
++        this.yBodyRotO += Math.round((this.yBodyRot - this.yBodyRotO) / 360.0F) * 360.0F;
++
++        this.xRotO += Math.round((this.getXRot() - this.xRotO) / 360.0F) * 360.0F;
++
++        this.yHeadRotO += Math.round((this.yHeadRot - this.yHeadRotO) / 360.0F) * 360.0F;
+ 
+         profilerFiller.pop();
          this.animStep += f2;
-         if (this.isFallFlying()) {
-@@ -2741,7 +3380,7 @@
+@@ -2573,7 +_,7 @@
          this.elytraAnimationState.tick();
      }
  
 -    public void detectEquipmentUpdates() {
 +    public void detectEquipmentUpdatesPublic() { // CraftBukkit
          Map<EquipmentSlot, ItemStack> map = this.collectEquipmentChanges();
- 
          if (map != null) {
-@@ -2778,10 +3417,17 @@
-                     throw new MatchException((String) null, (Throwable) null);
-             }
- 
--            ItemStack itemstack2 = itemstack1;
-+            ItemStack itemstack2 = itemstack1; final ItemStack oldEquipment = itemstack2; // Paper - PlayerArmorChangeEvent - obfhelper 
- 
--            itemstack = this.getItemBySlot(enumitemslot);
-+            itemstack = this.getItemBySlot(enumitemslot); final ItemStack newEquipment = itemstack;// Paper - PlayerArmorChangeEvent - obfhelper
-             if (this.equipmentHasChanged(itemstack2, itemstack)) {
+             this.handleHandSwap(map);
+@@ -2595,6 +_,13 @@
+             };
+             ItemStack itemBySlot = this.getItemBySlot(equipmentSlot);
+             if (this.equipmentHasChanged(itemStack, itemBySlot)) {
 +                // Paper start - PlayerArmorChangeEvent
-+                if (this instanceof ServerPlayer && enumitemslot.getType() == EquipmentSlot.Type.HUMANOID_ARMOR) {
-+                    final org.bukkit.inventory.ItemStack oldItem = CraftItemStack.asBukkitCopy(oldEquipment);
-+                    final org.bukkit.inventory.ItemStack newItem = CraftItemStack.asBukkitCopy(newEquipment);
-+                    new com.destroystokyo.paper.event.player.PlayerArmorChangeEvent((Player) this.getBukkitEntity(), com.destroystokyo.paper.event.player.PlayerArmorChangeEvent.SlotType.valueOf(enumitemslot.name()), oldItem, newItem).callEvent();
++                if (this instanceof ServerPlayer && equipmentSlot.getType() == EquipmentSlot.Type.HUMANOID_ARMOR) {
++                    final org.bukkit.inventory.ItemStack oldItem = CraftItemStack.asBukkitCopy(itemStack);
++                    final org.bukkit.inventory.ItemStack newItem = CraftItemStack.asBukkitCopy(itemBySlot);
++                    new com.destroystokyo.paper.event.player.PlayerArmorChangeEvent((org.bukkit.entity.Player) this.getBukkitEntity(), com.destroystokyo.paper.event.player.PlayerArmorChangeEvent.SlotType.valueOf(equipmentSlot.name()), oldItem, newItem).callEvent();
 +                }
 +                // Paper end - PlayerArmorChangeEvent
                  if (map == null) {
                      map = Maps.newEnumMap(EquipmentSlot.class);
                  }
-@@ -2864,7 +3510,7 @@
+@@ -2664,7 +_,7 @@
+                     this.lastBodyItemStack = itemStack;
              }
- 
          });
--        ((ServerLevel) this.level()).getChunkSource().broadcast(this, new ClientboundSetEquipmentPacket(this.getId(), list));
-+        ((ServerLevel) this.level()).getChunkSource().broadcast(this, new ClientboundSetEquipmentPacket(this.getId(), list, true)); // Paper - data sanitization
+-        ((ServerLevel)this.level()).getChunkSource().broadcast(this, new ClientboundSetEquipmentPacket(this.getId(), list));
++        ((ServerLevel)this.level()).getChunkSource().broadcast(this, new ClientboundSetEquipmentPacket(this.getId(), list, true)); // Paper - data sanitization
      }
  
      private ItemStack getLastArmorItem(EquipmentSlot slot) {
-@@ -2974,8 +3620,10 @@
-             } else if (this.isInLava() && (!this.onGround() || d3 > d4)) {
-                 this.jumpInLiquid(FluidTags.LAVA);
-             } else if ((this.onGround() || flag && d3 <= d4) && this.noJumpDelay == 0) {
-+                if (new com.destroystokyo.paper.event.entity.EntityJumpEvent(getBukkitLivingEntity()).callEvent()) { // Paper - Entity Jump API
-                 this.jumpFromGround();
-                 this.noJumpDelay = 10;
-+                } else { this.setJumping(false); } // Paper - Entity Jump API; setJumping(false) stops a potential loop
-             }
-         } else {
-             this.noJumpDelay = 0;
-@@ -3000,7 +3648,7 @@
-         {
-             LivingEntity entityliving = this.getControllingPassenger();
- 
--            if (entityliving instanceof Player entityhuman) {
-+            if (entityliving instanceof net.minecraft.world.entity.player.Player entityhuman) {
-                 if (this.isAlive()) {
-                     this.travelRidden(entityhuman, vec3d1);
-                     break label112;
-@@ -3017,7 +3665,7 @@
+@@ -2765,8 +_,10 @@
+             if (!flag || this.onGround() && !(fluidHeight > fluidJumpThreshold)) {
+                 if (!this.isInLava() || this.onGround() && !(fluidHeight > fluidJumpThreshold)) {
+                     if ((this.onGround() || flag && fluidHeight <= fluidJumpThreshold) && this.noJumpDelay == 0) {
++                        if (new com.destroystokyo.paper.event.entity.EntityJumpEvent(getBukkitLivingEntity()).callEvent()) { // Paper - Entity Jump API
+                         this.jumpFromGround();
+                         this.noJumpDelay = 10;
++                        } else { this.setJumping(false); } // Paper - Entity Jump API; setJumping(false) stops a potential loop
+                     }
+                 } else {
+                     this.jumpInLiquid(FluidTags.LAVA);
+@@ -2805,7 +_,7 @@
          this.calculateEntityAnimation(this instanceof FlyingAnimal);
-         gameprofilerfiller.pop();
-         gameprofilerfiller.push("freezing");
+         profilerFiller.pop();
+         profilerFiller.push("freezing");
 -        if (!this.level().isClientSide && !this.isDeadOrDying()) {
 +        if (!this.level().isClientSide && !this.isDeadOrDying() && !this.freezeLocked) { // Paper - Freeze Tick Lock API
-             int i = this.getTicksFrozen();
- 
+             int ticksFrozen = this.getTicksFrozen();
              if (this.isInPowderSnow && this.canFreeze()) {
-@@ -3046,6 +3694,20 @@
+                 this.setTicksFrozen(Math.min(this.getTicksRequiredToFreeze(), ticksFrozen + 1));
+@@ -2829,6 +_,20 @@
  
          this.pushEntities();
-         gameprofilerfiller.pop();
+         profilerFiller.pop();
 +        // Paper start - Add EntityMoveEvent
-+        if (((ServerLevel) this.level()).hasEntityMoveEvent && !(this instanceof net.minecraft.world.entity.player.Player)) {
++        if (((ServerLevel) this.level()).hasEntityMoveEvent && !(this instanceof Player)) {
 +            if (this.xo != this.getX() || this.yo != this.getY() || this.zo != this.getZ() || this.yRotO != this.getYRot() || this.xRotO != this.getXRot()) {
 +                Location from = new Location(this.level().getWorld(), this.xo, this.yo, this.zo, this.yRotO, this.xRotO);
 +                Location to = new Location(this.level().getWorld(), this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot());
@@ -1614,10 +1393,10 @@
 +            }
 +        }
 +        // Paper end - Add EntityMoveEvent
-         world = this.level();
-         if (world instanceof ServerLevel worldserver) {
-             if (this.isSensitiveToWater() && this.isInWaterRainOrBubble()) {
-@@ -3063,6 +3725,7 @@
+         if (this.level() instanceof ServerLevel serverLevel && this.isSensitiveToWater() && this.isInWaterRainOrBubble()) {
+             this.hurtServer(serverLevel, this.damageSources().drown(), 1.0F);
+         }
+@@ -2842,6 +_,7 @@
          this.checkSlowFallDistance();
          if (!this.level().isClientSide) {
              if (!this.canGlide()) {
@@ -1625,99 +1404,98 @@
                  this.setSharedFlag(7, false);
                  return;
              }
-@@ -3113,12 +3776,26 @@
-         Level world = this.level();
- 
-         if (!(world instanceof ServerLevel worldserver)) {
--            this.level().getEntities(EntityTypeTest.forClass(Player.class), this.getBoundingBox(), EntitySelector.pushableBy(this)).forEach(this::doPush);
-+            this.level().getEntities(EntityTypeTest.forClass(net.minecraft.world.entity.player.Player.class), this.getBoundingBox(), EntitySelector.pushableBy(this)).forEach(this::doPush);
+@@ -2881,9 +_,24 @@
+         if (!(this.level() instanceof ServerLevel serverLevel)) {
+             this.level().getEntities(EntityTypeTest.forClass(Player.class), this.getBoundingBox(), EntitySelector.pushableBy(this)).forEach(this::doPush);
          } else {
--            List list = this.level().getEntities((Entity) this, this.getBoundingBox(), EntitySelector.pushableBy(this));
+-            List<Entity> entities = this.level().getEntities(this, this.getBoundingBox(), EntitySelector.pushableBy(this));
 +            // Paper start - don't run getEntities if we're not going to use its result
 +            if (!this.isPushable()) {
 +                return;
 +            }
++
 +            net.minecraft.world.scores.Team team = this.getTeam();
 +            if (team != null && team.getCollisionRule() == net.minecraft.world.scores.Team.CollisionRule.NEVER) {
 +                return;
 +            }
- 
-+            int i = worldserver.getGameRules().getInt(GameRules.RULE_MAX_ENTITY_CRAMMING);
-+            if (i <= 0 && this.level().paperConfig().collisions.maxEntityCollisions <= 0) {
++
++            int _int = serverLevel.getGameRules().getInt(GameRules.RULE_MAX_ENTITY_CRAMMING);
++            if (_int <= 0 && this.level().paperConfig().collisions.maxEntityCollisions <= 0) {
 +                return;
 +            }
 +            // Paper end - don't run getEntities if we're not going to use its result
-+            List list = this.level().getEntities((Entity) this, this.getBoundingBox(), EntitySelector.pushable(this, this.level().paperConfig().collisions.fixClimbingBypassingCrammingRule)); // Paper - Climbing should not bypass cramming gamerule
-+
-             if (!list.isEmpty()) {
--                int i = worldserver.getGameRules().getInt(GameRules.RULE_MAX_ENTITY_CRAMMING);
++            List<Entity> entities = this.level().getEntities(this, this.getBoundingBox(), EntitySelector.pushable(this, this.level().paperConfig().collisions.fixClimbingBypassingCrammingRule)); // Paper - Climbing should not bypass cramming gamerule
+             if (!entities.isEmpty()) {
+-                int _int = serverLevel.getGameRules().getInt(GameRules.RULE_MAX_ENTITY_CRAMMING);
 +                // Paper - don't run getEntities if we're not going to use its result; moved up
+                 if (_int > 0 && entities.size() > _int - 1 && this.random.nextInt(4) == 0) {
+                     int i = 0;
  
-                 if (i > 0 && list.size() > i - 1 && this.random.nextInt(4) == 0) {
-                     int j = 0;
-@@ -3138,10 +3815,12 @@
+@@ -2898,7 +_,16 @@
+                     }
                  }
  
-                 Iterator iterator1 = list.iterator();
-+                this.numCollisions = Math.max(0, this.numCollisions - this.level().paperConfig().collisions.maxEntityCollisions); // Paper - Cap entity collisions
- 
--                while (iterator1.hasNext()) {
-+                while (iterator1.hasNext() && this.numCollisions < this.level().paperConfig().collisions.maxEntityCollisions) { // Paper - Cap entity collisions
-                     Entity entity1 = (Entity) iterator1.next();
--
-+                    entity1.numCollisions++; // Paper - Cap entity collisions
-+                    this.numCollisions++; // Paper - Cap entity collisions
++                // Paper start - Cap entity collisions
++                this.numCollisions = Math.max(0, this.numCollisions - this.level().paperConfig().collisions.maxEntityCollisions);
+                 for (Entity entity1 : entities) {
++                    if (this.numCollisions >= this.level().paperConfig().collisions.maxEntityCollisions) {
++                        break;
++                    }
++
++                    entity1.numCollisions++;
++                    this.numCollisions++;
++                    // Paper end - Cap entity collisions
                      this.doPush(entity1);
                  }
              }
-@@ -3190,10 +3869,16 @@
+@@ -2941,9 +_,16 @@
  
      @Override
      public void stopRiding() {
 +        // Paper start - Force entity dismount during teleportation
 +        this.stopRiding(false);
 +    }
++
 +    @Override
 +    public void stopRiding(boolean suppressCancellation) {
 +        // Paper end - Force entity dismount during teleportation
-         Entity entity = this.getVehicle();
- 
+         Entity vehicle = this.getVehicle();
 -        super.stopRiding();
--        if (entity != null && entity != this.getVehicle() && !this.level().isClientSide) {
+-        if (vehicle != null && vehicle != this.getVehicle() && !this.level().isClientSide) {
 +        super.stopRiding(suppressCancellation); // Paper - Force entity dismount during teleportation
-+        if (entity != null && entity != this.getVehicle() && !this.level().isClientSide && entity.valid) { // Paper - don't process on world gen
-             this.dismountVehicle(entity);
++        if (vehicle != null && vehicle != this.getVehicle() && !this.level().isClientSide && vehicle.valid) { // Paper - don't process on world gen
+             this.dismountVehicle(vehicle);
          }
- 
-@@ -3258,7 +3943,7 @@
+     }
+@@ -3007,7 +_,7 @@
      }
  
-     public void onItemPickup(ItemEntity item) {
--        Entity entity = item.getOwner();
-+        Entity entity = item.thrower != null ? this.level().getGlobalPlayerByUUID(item.thrower) : null; // Paper - check global player list where appropriate
- 
-         if (entity instanceof ServerPlayer) {
-             CriteriaTriggers.THROWN_ITEM_PICKED_UP_BY_ENTITY.trigger((ServerPlayer) entity, item.getItem(), this);
-@@ -3268,7 +3953,7 @@
- 
-     public void take(Entity item, int count) {
-         if (!item.isRemoved() && !this.level().isClientSide && (item instanceof ItemEntity || item instanceof AbstractArrow || item instanceof ExperienceOrb)) {
--            ((ServerLevel) this.level()).getChunkSource().broadcast(item, new ClientboundTakeItemEntityPacket(item.getId(), this.getId(), count));
-+            ((ServerLevel) this.level()).getChunkSource().broadcastAndSend(this, new ClientboundTakeItemEntityPacket(item.getId(), this.getId(), count)); // Paper - broadcast with collector as source
+     public void onItemPickup(ItemEntity itemEntity) {
+-        Entity owner = itemEntity.getOwner();
++        Entity owner = itemEntity.thrower != null ? this.level().getGlobalPlayerByUUID(itemEntity.thrower) : null; // Paper - check global player list where appropriate
+         if (owner instanceof ServerPlayer) {
+             CriteriaTriggers.THROWN_ITEM_PICKED_UP_BY_ENTITY.trigger((ServerPlayer)owner, itemEntity.getItem(), this);
+         }
+@@ -3017,7 +_,7 @@
+         if (!entity.isRemoved()
+             && !this.level().isClientSide
+             && (entity instanceof ItemEntity || entity instanceof AbstractArrow || entity instanceof ExperienceOrb)) {
+-            ((ServerLevel)this.level()).getChunkSource().broadcast(entity, new ClientboundTakeItemEntityPacket(entity.getId(), this.getId(), amount));
++            ((ServerLevel)this.level()).getChunkSource().broadcastAndSend(this, new ClientboundTakeItemEntityPacket(entity.getId(), this.getId(), amount)); // Paper - broadcast with collector as source
          }
- 
      }
-@@ -3284,7 +3969,8 @@
-             Vec3 vec3d = new Vec3(this.getX(), this.getEyeY(), this.getZ());
-             Vec3 vec3d1 = new Vec3(entity.getX(), entityY, entity.getZ());
  
--            return vec3d1.distanceTo(vec3d) > 128.0D ? false : this.level().clip(new ClipContext(vec3d, vec3d1, shapeType, fluidHandling, this)).getType() == HitResult.Type.MISS;
+@@ -3031,7 +_,8 @@
+         } else {
+             Vec3 vec3 = new Vec3(this.getX(), this.getEyeY(), this.getZ());
+             Vec3 vec31 = new Vec3(entity.getX(), y, entity.getZ());
+-            return !(vec31.distanceTo(vec3) > 128.0) && this.level().clip(new ClipContext(vec3, vec31, block, fluid, this)).getType() == HitResult.Type.MISS;
 +            // Paper - diff on change - used in CraftLivingEntity#hasLineOfSight(Location) and CraftWorld#lineOfSightExists
-+            return vec3d1.distanceToSqr(vec3d) > 128.0D * 128.0D ? false : this.level().clip(new ClipContext(vec3d, vec3d1, shapeType, fluidHandling, this)).getType() == HitResult.Type.MISS; // Paper - Perf: Use distance squared
++            return !(vec31.distanceToSqr(vec3) > 128.0D * 128.0D) && this.level().clip(new ClipContext(vec3, vec31, block, fluid, this)).getType() == HitResult.Type.MISS; // Paper - Perf: Use distance squared
          }
      }
  
-@@ -3305,13 +3991,27 @@
+@@ -3051,13 +_,27 @@
  
      @Override
      public boolean isPickable() {
@@ -1729,6 +1507,7 @@
      @Override
      public boolean isPushable() {
 -        return this.isAlive() && !this.isSpectator() && !this.onClimbable();
+-    }
 +        return this.isCollidable(this.level().paperConfig().collisions.fixClimbingBypassingCrammingRule);
 +    }
 +
@@ -1742,12 +1521,12 @@
 +    @Override
 +    public boolean canCollideWithBukkit(Entity entity) {
 +        return this.isPushable() && this.collides != this.collidableExemptions.contains(entity.getUUID());
-     }
++    }
 +    // CraftBukkit end
  
      @Override
      public float getYHeadRot() {
-@@ -3342,7 +4042,7 @@
+@@ -3088,7 +_,7 @@
      }
  
      public final void setAbsorptionAmount(float absorptionAmount) {
@@ -1756,8 +1535,8 @@
      }
  
      protected void internalSetAbsorptionAmount(float absorptionAmount) {
-@@ -3367,6 +4067,11 @@
-         return ((Byte) this.entityData.get(LivingEntity.DATA_LIVING_ENTITY_FLAGS) & 2) > 0 ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND;
+@@ -3115,6 +_,15 @@
+         return (this.entityData.get(DATA_LIVING_ENTITY_FLAGS) & 2) > 0 ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND;
      }
  
 +    // Paper start - Properly cancel usable items
@@ -1765,10 +1544,28 @@
 +        this.resendPossiblyDesyncedDataValues(java.util.List.of(DATA_LIVING_ENTITY_FLAGS), serverPlayer);
 +    }
 +    // Paper end - Properly cancel usable items
++    // Paper start - lag compensate eating
++    protected long eatStartTime;
++    protected int totalEatTimeTicks;
++    // Paper end - lag compensate eating
      private void updatingUsingItem() {
          if (this.isUsingItem()) {
              if (ItemStack.isSameItem(this.getItemInHand(this.getUsedItemHand()), this.useItem)) {
-@@ -3410,9 +4115,14 @@
+@@ -3128,7 +_,12 @@
+ 
+     protected void updateUsingItem(ItemStack usingItem) {
+         usingItem.onUseTick(this.level(), this, this.getUseItemRemainingTicks());
+-        if (--this.useItemRemaining == 0 && !this.level().isClientSide && !usingItem.useOnRelease()) {
++        // Paper start - lag compensate eating
++        // we add 1 to the expected time to avoid lag compensating when we should not
++        final boolean shouldLagCompensate = this.useItem.has(DataComponents.FOOD) && this.eatStartTime != -1 && (System.nanoTime() - this.eatStartTime) > ((1L + this.totalEatTimeTicks) * 50L * (1000L * 1000L));
++        if ((--this.useItemRemaining == 0 || shouldLagCompensate) && !this.level().isClientSide && !usingItem.useOnRelease()) {
++            this.useItemRemaining = 0;
++            // Paper end - lag compensate eating
+             this.completeUsingItem();
+         }
+     }
+@@ -3154,10 +_,18 @@
      }
  
      public void startUsingItem(InteractionHand hand) {
@@ -1777,54 +1574,70 @@
 +    }
 +    public void startUsingItem(InteractionHand hand, boolean forceUpdate) {
 +        // Paper end - Prevent consuming the wrong itemstack
-         ItemStack itemstack = this.getItemInHand(hand);
- 
--        if (!itemstack.isEmpty() && !this.isUsingItem()) {
-+        if (!itemstack.isEmpty() && !this.isUsingItem() || forceUpdate) { // Paper - Prevent consuming the wrong itemstack
-             this.useItem = itemstack;
-             this.useItemRemaining = itemstack.getUseDuration(this);
+         ItemStack itemInHand = this.getItemInHand(hand);
+-        if (!itemInHand.isEmpty() && !this.isUsingItem()) {
++        if (!itemInHand.isEmpty() && !this.isUsingItem() || forceUpdate) { // Paper - Prevent consuming the wrong itemstack
+             this.useItem = itemInHand;
+-            this.useItemRemaining = itemInHand.getUseDuration(this);
++            // Paper start - lag compensate eating
++            this.useItemRemaining = this.totalEatTimeTicks = itemInHand.getUseDuration(this);
++            this.eatStartTime = System.nanoTime();
++            // Paper end - lag compensate eating
              if (!this.level().isClientSide) {
-@@ -3483,13 +4193,50 @@
+                 this.setLivingEntityFlag(1, true);
+                 this.setLivingEntityFlag(2, hand == InteractionHand.OFF_HAND);
+@@ -3181,7 +_,10 @@
+                 }
+             } else if (!this.isUsingItem() && !this.useItem.isEmpty()) {
+                 this.useItem = ItemStack.EMPTY;
+-                this.useItemRemaining = 0;
++                // Paper start - lag compensate eating
++                this.useItemRemaining = this.totalEatTimeTicks = 0;
++                this.eatStartTime = -1L;
++                // Paper end - lag compensate eating
+             }
+         }
+     }
+@@ -3220,12 +_,49 @@
                  this.releaseUsingItem();
              } else {
                  if (!this.useItem.isEmpty() && this.isUsingItem()) {
--                    ItemStack itemstack = this.useItem.finishUsingItem(this.level(), this);
+-                    ItemStack itemStack = this.useItem.finishUsingItem(this.level(), this);
 +                    this.startUsingItem(this.getUsedItemHand(), true); // Paper - Prevent consuming the wrong itemstack
 +                    // CraftBukkit start - fire PlayerItemConsumeEvent
-+                    ItemStack itemstack;
++                    ItemStack itemStack;
 +                    PlayerItemConsumeEvent event = null; // Paper
-+                    if (this instanceof ServerPlayer entityPlayer) {
++                    if (this instanceof ServerPlayer serverPlayer) {
 +                        org.bukkit.inventory.ItemStack craftItem = CraftItemStack.asBukkitCopy(this.useItem);
-+                        org.bukkit.inventory.EquipmentSlot hand = org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(enumhand);
-+                        event = new PlayerItemConsumeEvent((Player) this.getBukkitEntity(), craftItem, hand); // Paper
++                        org.bukkit.inventory.EquipmentSlot hand = org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(usedItemHand);
++                        event = new PlayerItemConsumeEvent((org.bukkit.entity.Player) this.getBukkitEntity(), craftItem, hand); // Paper
 +                        this.level().getCraftServer().getPluginManager().callEvent(event);
 +
 +                        if (event.isCancelled()) {
 +                            // Update client
 +                            Consumable consumable = this.useItem.get(DataComponents.CONSUMABLE);
 +                            if (consumable != null) {
-+                                consumable.cancelUsingItem(entityPlayer, this.useItem);
++                                consumable.cancelUsingItem(serverPlayer, this.useItem);
 +                            }
-+                            entityPlayer.getBukkitEntity().updateInventory();
-+                            entityPlayer.getBukkitEntity().updateScaledHealth();
++                            serverPlayer.getBukkitEntity().updateInventory();
++                            serverPlayer.getBukkitEntity().updateScaledHealth();
 +                            this.stopUsingItem(); // Paper - event is using an item, clear active item to reset its use
 +                            return;
 +                        }
- 
-+                        itemstack = (craftItem.equals(event.getItem())) ? this.useItem.finishUsingItem(this.level(), this) : CraftItemStack.asNMSCopy(event.getItem()).finishUsingItem(this.level(), this);
++
++                        itemStack = (craftItem.equals(event.getItem())) ? this.useItem.finishUsingItem(this.level(), this) : CraftItemStack.asNMSCopy(event.getItem()).finishUsingItem(this.level(), this);
 +                    } else {
-+                        itemstack = this.useItem.finishUsingItem(this.level(), this);
++                        itemStack = this.useItem.finishUsingItem(this.level(), this);
 +                    }
 +                    // Paper start - save the default replacement item and change it if necessary
-+                    final ItemStack defaultReplacement = itemstack;
++                    final ItemStack defaultReplacement = itemStack;
 +                    if (event != null && event.getReplacement() != null) {
-+                        itemstack = CraftItemStack.asNMSCopy(event.getReplacement());
++                        itemStack = CraftItemStack.asNMSCopy(event.getReplacement());
 +                    }
 +                    // Paper end
 +                    // CraftBukkit end
-+
-                     if (itemstack != this.useItem) {
-                         this.setItemInHand(enumhand, itemstack);
+                     if (itemStack != this.useItem) {
+                         this.setItemInHand(usedItemHand, itemStack);
                      }
  
                      this.stopUsingItem();
@@ -1834,26 +1647,39 @@
 +                    }
 +                    // Paper end
                  }
- 
              }
-@@ -3512,6 +4259,7 @@
+         }
+@@ -3248,6 +_,7 @@
  
      public void releaseUsingItem() {
          if (!this.useItem.isEmpty()) {
-+            if (this instanceof ServerPlayer) new io.papermc.paper.event.player.PlayerStopUsingItemEvent((Player) getBukkitEntity(), useItem.asBukkitMirror(), getTicksUsingItem()).callEvent(); // Paper - Add PlayerStopUsingItemEvent
++            if (this instanceof ServerPlayer) new io.papermc.paper.event.player.PlayerStopUsingItemEvent((org.bukkit.entity.Player) getBukkitEntity(), useItem.asBukkitMirror(), getTicksUsingItem()).callEvent(); // Paper - Add PlayerStopUsingItemEvent
              this.useItem.releaseUsing(this.level(), this, this.getUseItemRemainingTicks());
              if (this.useItem.useOnRelease()) {
                  this.updatingUsingItem();
-@@ -3544,12 +4292,69 @@
-         if (this.isUsingItem() && !this.useItem.isEmpty()) {
-             Item item = this.useItem.getItem();
+@@ -3267,7 +_,10 @@
+         }
  
--            return item.getUseAnimation(this.useItem) != ItemUseAnimation.BLOCK ? null : (item.getUseDuration(this.useItem, this) - this.useItemRemaining < 5 ? null : this.useItem);
-+            return item.getUseAnimation(this.useItem) != ItemUseAnimation.BLOCK ? null : (item.getUseDuration(this.useItem, this) - this.useItemRemaining < getShieldBlockingDelay() ? null : this.useItem); // Paper - Make shield blocking delay configurable
+         this.useItem = ItemStack.EMPTY;
+-        this.useItemRemaining = 0;
++        // Paper start - lag compensate eating
++        this.useItemRemaining = this.totalEatTimeTicks = 0;
++        this.eatStartTime = -1L;
++        // Paper end - lag compensate eating
+     }
+ 
+     public boolean isBlocking() {
+@@ -3281,12 +_,69 @@
+             if (item.getUseAnimation(this.useItem) != ItemUseAnimation.BLOCK) {
+                 return null;
+             } else {
+-                return item.getUseDuration(this.useItem, this) - this.useItemRemaining < 5 ? null : this.useItem;
++                return item.getUseDuration(this.useItem, this) - this.useItemRemaining < this.getShieldBlockingDelay() ? null : this.useItem; // Paper - Make shield blocking delay configurable
+             }
          } else {
              return null;
          }
-+    }
+     }
 +
 +    // Paper start - Make shield blocking delay configurable
 +    public HitResult getRayTrace(int maxDistance, ClipContext.Fluid fluidCollisionOption) {
@@ -1905,56 +1731,45 @@
 +
 +    public int getShieldBlockingDelay() {
 +        return shieldBlockingDelay;
-     }
- 
++    }
++
 +    public void setShieldBlockingDelay(int shieldBlockingDelay) {
 +        this.shieldBlockingDelay = shieldBlockingDelay;
 +    }
 +    // Paper end - Make shield blocking delay configurable
-+
+ 
      public boolean isSuppressingSlidingDownLadder() {
          return this.isShiftKeyDown();
-     }
-@@ -3568,12 +4373,18 @@
+@@ -3306,6 +_,12 @@
      }
  
-     public boolean randomTeleport(double x, double y, double z, boolean particleEffects) {
+     public boolean randomTeleport(double x, double y, double z, boolean broadcastTeleport) {
 +        // CraftBukkit start
-+        return this.randomTeleport(x, y, z, particleEffects, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.UNKNOWN).orElse(false);
++        return this.randomTeleport(x, y, z, broadcastTeleport, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.UNKNOWN).orElse(false);
 +    }
 +
-+    public Optional<Boolean> randomTeleport(double d0, double d1, double d2, boolean flag, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause) {
++    public Optional<Boolean> randomTeleport(double x, double y, double z, boolean broadcastTeleport, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause) {
 +        // CraftBukkit end
-         double d3 = this.getX();
-         double d4 = this.getY();
-         double d5 = this.getZ();
--        double d6 = y;
-+        double d6 = d1;
-         boolean flag1 = false;
--        BlockPos blockposition = BlockPos.containing(x, y, z);
-+        BlockPos blockposition = BlockPos.containing(d0, d1, d2);
-         Level world = this.level();
- 
-         if (world.hasChunkAt(blockposition)) {
-@@ -3592,18 +4403,43 @@
+         double x1 = this.getX();
+         double y1 = this.getY();
+         double z1 = this.getZ();
+@@ -3328,16 +_,39 @@
              }
  
-             if (flag2) {
--                this.teleportTo(x, d6, z);
+             if (flag1) {
+-                this.teleportTo(x, d, z);
 +                // CraftBukkit start - Teleport event
-+                // this.teleportTo(d0, d6, d2);
-+
 +                // first set position, to check if the place to teleport is valid
-+                this.setPos(d0, d6, d2);
-                 if (world.noCollision((Entity) this) && !world.containsAnyLiquid(this.getBoundingBox())) {
-                     flag1 = true;
++                this.setPos(x, d, z);
+                 if (level.noCollision(this) && !level.containsAnyLiquid(this.getBoundingBox())) {
+                     flag = true;
                  }
 +                // now revert and call event if the teleport place is valid
-+                this.setPos(d3, d4, d5);
++                this.setPos(x1, y1, z1);
 +
-+                if (flag1) {
++                if (flag) {
 +                    if (!(this instanceof ServerPlayer)) {
-+                        EntityTeleportEvent teleport = new EntityTeleportEvent(this.getBukkitEntity(), new Location(this.level().getWorld(), d3, d4, d5), new Location(this.level().getWorld(), d0, d6, d2));
++                        EntityTeleportEvent teleport = new EntityTeleportEvent(this.getBukkitEntity(), new Location(this.level().getWorld(), x1, y1, z1), new Location(this.level().getWorld(), x, d, z));
 +                        this.level().getCraftServer().getPluginManager().callEvent(teleport);
 +                        if (!teleport.isCancelled() && teleport.getTo() != null) { // Paper
 +                            Location to = teleport.getTo();
@@ -1964,7 +1779,7 @@
 +                        }
 +                    } else {
 +                        // player teleport event is called in the underlining code
-+                        if (!((ServerPlayer) this).connection.teleport(d0, d6, d2, this.getYRot(), this.getXRot(), cause)) {
++                        if (!((ServerPlayer) this).connection.teleport(x, d, z, this.getYRot(), this.getXRot(), cause)) {
 +                            return Optional.empty();
 +                        }
 +                    }
@@ -1973,19 +1788,16 @@
              }
          }
  
-         if (!flag1) {
--            this.teleportTo(d3, d4, d5);
+         if (!flag) {
+-            this.teleportTo(x1, y1, z1);
 -            return false;
-+            // this.enderTeleportTo(d3, d4, d5); // CraftBukkit - already set the location back
++            // this.teleportTo(x1, y1, z1); // CraftBukkit - already set the location back
 +            return Optional.of(false); // CraftBukkit
          } else {
--            if (particleEffects) {
-+            if (flag) {
-                 world.broadcastEntityEvent(this, (byte) 46);
-             }
- 
-@@ -3613,7 +4449,7 @@
-                 entitycreature.getNavigation().stop();
+             if (broadcastTeleport) {
+                 level.broadcastEntityEvent(this, (byte)46);
+@@ -3347,7 +_,7 @@
+                 pathfinderMob.getNavigation().stop();
              }
  
 -            return true;
@@ -1993,42 +1805,3 @@
          }
      }
  
-@@ -3706,7 +4542,7 @@
-     }
- 
-     public void stopSleeping() {
--        Optional optional = this.getSleepingPos();
-+        Optional<BlockPos> optional = this.getSleepingPos(); // CraftBukkit - decompile error
-         Level world = this.level();
- 
-         java.util.Objects.requireNonNull(world);
-@@ -3718,9 +4554,9 @@
- 
-                 this.level().setBlock(blockposition, (BlockState) iblockdata.setValue(BedBlock.OCCUPIED, false), 3);
-                 Vec3 vec3d = (Vec3) BedBlock.findStandUpPosition(this.getType(), this.level(), blockposition, enumdirection, this.getYRot()).orElseGet(() -> {
--                    BlockPosition blockposition1 = blockposition.above();
-+                    BlockPos blockposition1 = blockposition.above();
- 
--                    return new Vec3D((double) blockposition1.getX() + 0.5D, (double) blockposition1.getY() + 0.1D, (double) blockposition1.getZ() + 0.5D);
-+                    return new Vec3((double) blockposition1.getX() + 0.5D, (double) blockposition1.getY() + 0.1D, (double) blockposition1.getZ() + 0.5D);
-                 });
-                 Vec3 vec3d1 = Vec3.atBottomCenterOf(blockposition).subtract(vec3d).normalize();
-                 float f = (float) Mth.wrapDegrees(Mth.atan2(vec3d1.z, vec3d1.x) * 57.2957763671875D - 90.0D);
-@@ -3740,7 +4576,7 @@
- 
-     @Nullable
-     public Direction getBedOrientation() {
--        BlockPos blockposition = (BlockPos) this.getSleepingPos().orElse((Object) null);
-+        BlockPos blockposition = (BlockPos) this.getSleepingPos().orElse(null); // CraftBukkit - decompile error
- 
-         return blockposition != null ? BedBlock.getBedOrientation(this.level(), blockposition) : null;
-     }
-@@ -3905,7 +4741,7 @@
-     public float maxUpStep() {
-         float f = (float) this.getAttributeValue(Attributes.STEP_HEIGHT);
- 
--        return this.getControllingPassenger() instanceof Player ? Math.max(f, 1.0F) : f;
-+        return this.getControllingPassenger() instanceof net.minecraft.world.entity.player.Player ? Math.max(f, 1.0F) : f;
-     }
- 
-     @Override
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/Mob.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/Mob.java.patch
new file mode 100644
index 0000000000..6e9bf95ddf
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/Mob.java.patch
@@ -0,0 +1,428 @@
+--- a/net/minecraft/world/entity/Mob.java
++++ b/net/minecraft/world/entity/Mob.java
+@@ -84,6 +_,18 @@
+ import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
+ import net.minecraft.world.phys.AABB;
+ 
++// CraftBukkit start
++import org.bukkit.craftbukkit.event.CraftEventFactory;
++import org.bukkit.craftbukkit.entity.CraftLivingEntity;
++import org.bukkit.event.entity.CreatureSpawnEvent;
++import org.bukkit.event.entity.EntityRemoveEvent;
++import org.bukkit.event.entity.EntityTargetLivingEntityEvent;
++import org.bukkit.event.entity.EntityTargetEvent;
++import org.bukkit.event.entity.EntityTransformEvent;
++import org.bukkit.event.entity.EntityUnleashEvent;
++import org.bukkit.event.entity.EntityUnleashEvent.UnleashReason;
++// CraftBukkit end
++
+ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashable, Targeting {
+     private static final EntityDataAccessor<Byte> DATA_MOB_FLAGS_ID = SynchedEntityData.defineId(Mob.class, EntityDataSerializers.BYTE);
+     private static final int MOB_FLAG_NO_AI = 1;
+@@ -112,6 +_,7 @@
+     private final BodyRotationControl bodyRotationControl;
+     protected PathNavigation navigation;
+     public GoalSelector goalSelector;
++    @Nullable public net.minecraft.world.entity.ai.goal.FloatGoal goalFloat; // Paper - Allow nerfed mobs to jump and float
+     public GoalSelector targetSelector;
+     @Nullable
+     private LivingEntity target;
+@@ -131,6 +_,7 @@
+     private Leashable.LeashData leashData;
+     private BlockPos restrictCenter = BlockPos.ZERO;
+     private float restrictRadius = -1.0F;
++    public boolean aware = true; // CraftBukkit
+ 
+     protected Mob(EntityType<? extends Mob> entityType, Level level) {
+         super(entityType, level);
+@@ -150,6 +_,12 @@
+         }
+     }
+ 
++    // CraftBukkit start
++    public void setPersistenceRequired(boolean persistenceRequired) {
++        this.persistenceRequired = persistenceRequired;
++    }
++    // CraftBukkit end
++
+     protected void registerGoals() {
+     }
+ 
+@@ -230,7 +_,40 @@
+     }
+ 
+     public void setTarget(@Nullable LivingEntity target) {
++        // CraftBukkit start - fire event
++        this.setTarget(target, EntityTargetEvent.TargetReason.UNKNOWN, true);
++    }
++
++    public boolean setTarget(LivingEntity target, EntityTargetEvent.TargetReason reason, boolean fireEvent) {
++        if (this.getTarget() == target) {
++            return false;
++        }
++        if (fireEvent) {
++            if (reason == EntityTargetEvent.TargetReason.UNKNOWN && this.getTarget() != null && target == null) {
++                reason = this.getTarget().isAlive() ? EntityTargetEvent.TargetReason.FORGOT_TARGET : EntityTargetEvent.TargetReason.TARGET_DIED;
++            }
++            if (reason == EntityTargetEvent.TargetReason.UNKNOWN) {
++                this.level().getCraftServer().getLogger().log(java.util.logging.Level.WARNING, "Unknown target reason, please report on the issue tracker", new Exception());
++            }
++            CraftLivingEntity ctarget = null;
++            if (target != null) {
++                ctarget = (CraftLivingEntity) target.getBukkitEntity();
++            }
++            EntityTargetLivingEntityEvent event = new EntityTargetLivingEntityEvent(this.getBukkitEntity(), ctarget, reason);
++            this.level().getCraftServer().getPluginManager().callEvent(event);
++            if (event.isCancelled()) {
++                return false;
++            }
++
++            if (event.getTarget() != null) {
++                target = ((CraftLivingEntity) event.getTarget()).getHandle();
++            } else {
++                target = null;
++            }
++        }
+         this.target = target;
++        return true;
++        // CraftBukkit end
+     }
+ 
+     @Override
+@@ -354,6 +_,12 @@
+         return null;
+     }
+ 
++    // CraftBukkit start - Add delegate method
++    public SoundEvent getAmbientSound0() {
++        return this.getAmbientSound();
++    }
++    // CraftBukkit end
++
+     @Override
+     public void addAdditionalSaveData(CompoundTag compound) {
+         super.addAdditionalSaveData(compound);
+@@ -413,13 +_,25 @@
+         if (this.isNoAi()) {
+             compound.putBoolean("NoAI", this.isNoAi());
+         }
++        compound.putBoolean("Bukkit.Aware", this.aware); // CraftBukkit
+     }
+ 
+     @Override
+     public void readAdditionalSaveData(CompoundTag compound) {
+         super.readAdditionalSaveData(compound);
+-        this.setCanPickUpLoot(compound.getBoolean("CanPickUpLoot"));
+-        this.persistenceRequired = compound.getBoolean("PersistenceRequired");
++        // CraftBukkit start - If looting or persistence is false only use it if it was set after we started using it
++        if (compound.contains("CanPickUpLoot", 99)) {
++            boolean data = compound.getBoolean("CanPickUpLoot");
++            if (isLevelAtLeast(compound, 1) || data) {
++                this.setCanPickUpLoot(data);
++            }
++        }
++
++        boolean data = compound.getBoolean("PersistenceRequired");
++        if (isLevelAtLeast(compound, 1) || data) {
++            this.persistenceRequired = data;
++        }
++        // CraftBukkit end
+         if (compound.contains("ArmorItems", 9)) {
+             ListTag list = compound.getList("ArmorItems", 10);
+ 
+@@ -472,13 +_,18 @@
+         this.readLeashData(compound);
+         this.setLeftHanded(compound.getBoolean("LeftHanded"));
+         if (compound.contains("DeathLootTable", 8)) {
+-            this.lootTable = Optional.of(ResourceKey.create(Registries.LOOT_TABLE, ResourceLocation.parse(compound.getString("DeathLootTable"))));
++            this.lootTable = Optional.ofNullable(ResourceLocation.tryParse(compound.getString("DeathLootTable"))).map((rs) -> ResourceKey.create(Registries.LOOT_TABLE, rs)); // Paper - Validate ResourceLocation
+         } else {
+             this.lootTable = Optional.empty();
+         }
+ 
+         this.lootTableSeed = compound.getLong("DeathLootTableSeed");
+         this.setNoAi(compound.getBoolean("NoAI"));
++        // CraftBukkit start
++        if (compound.contains("Bukkit.Aware")) {
++            this.aware = compound.getBoolean("Bukkit.Aware");
++        }
++        // CraftBukkit end
+     }
+ 
+     @Override
+@@ -540,6 +_,11 @@
+                     && !itemEntity.getItem().isEmpty()
+                     && !itemEntity.hasPickUpDelay()
+                     && this.wantsToPickUp(serverLevel, itemEntity.getItem())) {
++                    // Paper start - Item#canEntityPickup
++                    if (!itemEntity.canMobPickup) {
++                        continue;
++                    }
++                    // Paper end - Item#canEntityPickup
+                     this.pickUpItem(serverLevel, itemEntity);
+                 }
+             }
+@@ -554,18 +_,24 @@
+ 
+     protected void pickUpItem(ServerLevel level, ItemEntity entity) {
+         ItemStack item = entity.getItem();
+-        ItemStack itemStack = this.equipItemIfPossible(level, item.copy());
++        ItemStack itemStack = this.equipItemIfPossible(level, item.copy(), entity); // CraftBukkit - add item
+         if (!itemStack.isEmpty()) {
+             this.onItemPickup(entity);
+             this.take(entity, itemStack.getCount());
+             item.shrink(itemStack.getCount());
+             if (item.isEmpty()) {
+-                entity.discard();
++                entity.discard(EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
+             }
+         }
+     }
+ 
+     public ItemStack equipItemIfPossible(ServerLevel level, ItemStack stack) {
++        // CraftBukkit start - add item
++        return this.equipItemIfPossible(level, stack, null);
++    }
++
++    public ItemStack equipItemIfPossible(ServerLevel level, ItemStack stack, ItemEntity entity) {
++        // CraftBukkit end
+         EquipmentSlot equipmentSlotForItem = this.getEquipmentSlotForItem(stack);
+         ItemStack itemBySlot = this.getItemBySlot(equipmentSlotForItem);
+         boolean canReplaceCurrentItem = this.canReplaceCurrentItem(stack, itemBySlot, equipmentSlotForItem);
+@@ -575,10 +_,18 @@
+             canReplaceCurrentItem = itemBySlot.isEmpty();
+         }
+ 
+-        if (canReplaceCurrentItem && this.canHoldItem(stack)) {
++        // CraftBukkit start
++        boolean canPickup = canReplaceCurrentItem && this.canHoldItem(stack);
++        if (entity != null) {
++            canPickup = !org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPickupItemEvent(this, entity, 0, !canPickup).isCancelled();
++        }
++        if (canPickup) {
++            // CraftBukkit end
+             double d = this.getEquipmentDropChance(equipmentSlotForItem);
+             if (!itemBySlot.isEmpty() && Math.max(this.random.nextFloat() - 0.1F, 0.0F) < d) {
++                this.forceDrops = true; // CraftBukkit
+                 this.spawnAtLocation(level, itemBySlot);
++                this.forceDrops = false; // CraftBukkit
+             }
+ 
+             ItemStack itemStack = equipmentSlotForItem.limit(stack);
+@@ -703,22 +_,29 @@
+     @Override
+     public void checkDespawn() {
+         if (this.level().getDifficulty() == Difficulty.PEACEFUL && this.shouldDespawnInPeaceful()) {
+-            this.discard();
++            this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+         } else if (!this.isPersistenceRequired() && !this.requiresCustomPersistence()) {
+-            Entity nearestPlayer = this.level().getNearestPlayer(this, -1.0);
++            Entity nearestPlayer = this.level().findNearbyPlayer(this, -1.0, EntitySelector.PLAYER_AFFECTS_SPAWNING); // Paper - Affects Spawning API
+             if (nearestPlayer != null) {
+-                double d = nearestPlayer.distanceToSqr(this);
+-                int despawnDistance = this.getType().getCategory().getDespawnDistance();
+-                int i = despawnDistance * despawnDistance;
+-                if (d > i && this.removeWhenFarAway(d)) {
+-                    this.discard();
+-                }
++                // Paper start - Configurable despawn distances
++                final io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DespawnRangePair despawnRangePair = this.level().paperConfig().entities.spawning.despawnRanges.get(this.getType().getCategory());
++                final io.papermc.paper.configuration.type.DespawnRange.Shape shape = this.level().paperConfig().entities.spawning.despawnRangeShape;
++                final double dy = Math.abs(nearestPlayer.getY() - this.getY());
++                final double dySqr = Math.pow(dy, 2);
++                final double dxSqr = Math.pow(nearestPlayer.getX() - this.getX(), 2);
++                final double dzSqr = Math.pow(nearestPlayer.getZ() - this.getZ(), 2);
++                final double distanceSquared = dxSqr + dzSqr + dySqr;
++                // Despawn if hard/soft limit is exceeded
++                if (despawnRangePair.hard().shouldDespawn(shape, dxSqr, dySqr, dzSqr, dy) && this.removeWhenFarAway(distanceSquared)) {
++                    this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
++                 }
+ 
+-                int noDespawnDistance = this.getType().getCategory().getNoDespawnDistance();
+-                int i1 = noDespawnDistance * noDespawnDistance;
+-                if (this.noActionTime > 600 && this.random.nextInt(800) == 0 && d > i1 && this.removeWhenFarAway(d)) {
+-                    this.discard();
+-                } else if (d < i1) {
++                if (despawnRangePair.soft().shouldDespawn(shape, dxSqr, dySqr, dzSqr, dy)) {
++                    if (this.noActionTime > 600 && this.random.nextInt(800) == 0 && this.removeWhenFarAway(distanceSquared)) {
++                        this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
++                    }
++                } else {
++                    // Paper end - Configurable despawn distances
+                     this.noActionTime = 0;
+                 }
+             }
+@@ -730,6 +_,15 @@
+     @Override
+     protected final void serverAiStep() {
+         this.noActionTime++;
++        // Paper start - Allow nerfed mobs to jump and float
++        if (!this.aware) {
++            if (goalFloat != null) {
++                if (goalFloat.canUse()) goalFloat.tick();
++                this.getJumpControl().tick();
++            }
++            return;
++        }
++        // Paper end - Allow nerfed mobs to jump and float
+         ProfilerFiller profilerFiller = Profiler.get();
+         profilerFiller.push("sensing");
+         this.sensing.tick();
+@@ -908,26 +_,40 @@
+ 
+     @Override
+     public void setItemSlot(EquipmentSlot slot, ItemStack stack) {
++        // Paper start - Fix silent equipment change
++        setItemSlot(slot, stack, false);
++    }
++
++    @Override
++    public void setItemSlot(EquipmentSlot slot, ItemStack stack, boolean silent) {
++        // Paper end - Fix silent equipment change
+         this.verifyEquippedItem(stack);
+         switch (slot.getType()) {
+             case HAND:
+-                this.onEquipItem(slot, this.handItems.set(slot.getIndex(), stack), stack);
++                this.onEquipItem(slot, this.handItems.set(slot.getIndex(), stack), stack, silent); // Paper - Fix silent equipment change
+                 break;
+             case HUMANOID_ARMOR:
+-                this.onEquipItem(slot, this.armorItems.set(slot.getIndex(), stack), stack);
++                this.onEquipItem(slot, this.armorItems.set(slot.getIndex(), stack), stack, silent); // Paper - Fix silent equipment change
+                 break;
+             case ANIMAL_ARMOR:
+                 ItemStack itemStack = this.bodyArmorItem;
+                 this.bodyArmorItem = stack;
+-                this.onEquipItem(slot, itemStack, stack);
++                this.onEquipItem(slot, itemStack, stack, silent); // Paper - Fix silent equipment change
+         }
+     }
++
++    // Paper start
++    protected boolean shouldSkipLoot(EquipmentSlot slot) { // method to avoid to fallback into the global mob loot logic (i.e fox)
++        return false;
++    }
++    // Paper end
+ 
+     @Override
+     protected void dropCustomDeathLoot(ServerLevel level, DamageSource damageSource, boolean recentlyHit) {
+         super.dropCustomDeathLoot(level, damageSource, recentlyHit);
+ 
+         for (EquipmentSlot equipmentSlot : EquipmentSlot.VALUES) {
++            if (this.shouldSkipLoot(equipmentSlot)) continue; // Paper
+             ItemStack itemBySlot = this.getItemBySlot(equipmentSlot);
+             float equipmentDropChance = this.getEquipmentDropChance(equipmentSlot);
+             if (equipmentDropChance != 0.0F) {
+@@ -951,7 +_,13 @@
+                     }
+ 
+                     this.spawnAtLocation(level, itemBySlot);
++                    if (this.clearEquipmentSlots) { // Paper
+                     this.setItemSlot(equipmentSlot, ItemStack.EMPTY);
++                    // Paper start
++                    } else {
++                        this.clearedEquipmentSlots.add(equipmentSlot);
++                    }
++                    // Paper end
+                 }
+             }
+         }
+@@ -1269,6 +_,22 @@
+     public <T extends Mob> T convertTo(
+         EntityType<T> entityType, ConversionParams conversionParams, EntitySpawnReason spawnReason, ConversionParams.AfterConversion<T> afterConversion
+     ) {
++        return this.convertTo(entityType, conversionParams, spawnReason, afterConversion, EntityTransformEvent.TransformReason.UNKNOWN, CreatureSpawnEvent.SpawnReason.DEFAULT);
++    }
++
++    @Nullable
++    public <T extends Mob> T convertTo(
++        EntityType<T> entityType, ConversionParams conversionParams, EntitySpawnReason spawnReason, ConversionParams.AfterConversion<T> afterConversion, EntityTransformEvent.TransformReason transformReason, CreatureSpawnEvent.SpawnReason creatureSpawnReason
++    ) {
++    // Paper start - entity zap event - allow cancellation of conversion post creation
++        return this.convertTo(entityType, conversionParams, spawnReason, e -> { afterConversion.finalizeConversion(e); return true; }, transformReason, creatureSpawnReason);
++    }
++    @Nullable
++    public <T extends Mob> T convertTo(
++        EntityType<T> entityType, ConversionParams conversionParams, EntitySpawnReason spawnReason, ConversionParams.CancellingAfterConversion<T> afterConversion, EntityTransformEvent.TransformReason transformReason, CreatureSpawnEvent.SpawnReason creatureSpawnReason
++    ) {
++        // Paper end - entity zap event - allow cancellation of conversion post creation
++        // CraftBukkit end
+         if (this.isRemoved()) {
+             return null;
+         } else {
+@@ -1277,13 +_,23 @@
+                 return null;
+             } else {
+                 conversionParams.type().convert(this, mob, conversionParams);
+-                afterConversion.finalizeConversion(mob);
++                if (!afterConversion.finalizeConversionOrCancel(mob)) return null; // Paper - entity zap event - return null if conversion was cancelled
++                // CraftBukkit start
++                if (transformReason == null) {
++                    // Special handling for slime split and pig lightning
++                    return mob;
++                }
++
++                if (CraftEventFactory.callEntityTransformEvent(this, mob, transformReason).isCancelled()) {
++                    return null;
++                }
++                // CraftBukkit end
+                 if (this.level() instanceof ServerLevel serverLevel) {
+-                    serverLevel.addFreshEntity(mob);
++                    serverLevel.addFreshEntity(mob, creatureSpawnReason); // CraftBukkit
+                 }
+ 
+                 if (conversionParams.type().shouldDiscardAfterConversion()) {
+-                    this.discard();
++                    this.discard(EntityRemoveEvent.Cause.TRANSFORMATION); // CraftBukkit - add Bukkit remove cause
+                 }
+ 
+                 return mob;
+@@ -1293,7 +_,20 @@
+ 
+     @Nullable
+     public <T extends Mob> T convertTo(EntityType<T> entityType, ConversionParams coversionParams, ConversionParams.AfterConversion<T> afterConversion) {
+-        return this.convertTo(entityType, coversionParams, EntitySpawnReason.CONVERSION, afterConversion);
++        // CraftBukkit start
++        return this.convertTo(entityType, coversionParams, afterConversion, EntityTransformEvent.TransformReason.UNKNOWN, CreatureSpawnEvent.SpawnReason.DEFAULT);
++    }
++
++    @Nullable
++    public <T extends Mob> T convertTo(EntityType<T> entityType, ConversionParams coversionParams, ConversionParams.AfterConversion<T> afterConversion, EntityTransformEvent.TransformReason transformReason, CreatureSpawnEvent.SpawnReason creatureSpawnReason) {
++    // Paper start - entity zap event - allow cancellation of conversion post creation
++        return this.convertTo(entityType, coversionParams, e -> { afterConversion.finalizeConversion(e); return true; }, transformReason, creatureSpawnReason);
++    }
++    @Nullable
++    public <T extends Mob> T convertTo(EntityType<T> entityType, ConversionParams coversionParams, ConversionParams.CancellingAfterConversion<T> afterConversion, EntityTransformEvent.TransformReason transformReason, CreatureSpawnEvent.SpawnReason creatureSpawnReason) {
++    // Paper start - entity zap event - allow cancellation of conversion post creation
++        return this.convertTo(entityType, coversionParams, EntitySpawnReason.CONVERSION, afterConversion, transformReason, creatureSpawnReason);
++        // CraftBukkit end
+     }
+ 
+     @Nullable
+@@ -1329,7 +_,17 @@
+     public boolean startRiding(Entity entity, boolean force) {
+         boolean flag = super.startRiding(entity, force);
+         if (flag && this.isLeashed()) {
+-            this.dropLeash();
++            // Paper start - Expand EntityUnleashEvent
++            EntityUnleashEvent event = new EntityUnleashEvent(this.getBukkitEntity(), EntityUnleashEvent.UnleashReason.UNKNOWN, true);
++            if (!event.callEvent()) {
++                return flag;
++            }
++            if (event.isDropLeash()) {
++                this.dropLeash();
++            } else {
++                this.removeLeash();
++            }
++            // Paper end - Expand EntityUnleashEvent
+         }
+ 
+         return flag;
+@@ -1412,7 +_,7 @@
+             float knockback = this.getKnockback(source, damageSource);
+             if (knockback > 0.0F && source instanceof LivingEntity livingEntity) {
+                 livingEntity.knockback(
+-                    knockback * 0.5F, Mth.sin(this.getYRot() * (float) (Math.PI / 180.0)), -Mth.cos(this.getYRot() * (float) (Math.PI / 180.0))
++                    knockback * 0.5F, Mth.sin(this.getYRot() * (float) (Math.PI / 180.0)), -Mth.cos(this.getYRot() * (float) (Math.PI / 180.0)), this, io.papermc.paper.event.entity.EntityKnockbackEvent.Cause.ENTITY_ATTACK // CraftBukkit // Paper - knockback events
+                 );
+                 this.setDeltaMovement(this.getDeltaMovement().multiply(0.6, 1.0, 0.6));
+             }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/NeutralMob.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/NeutralMob.java.patch
new file mode 100644
index 0000000000..06fd0f09c7
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/NeutralMob.java.patch
@@ -0,0 +1,69 @@
+--- a/net/minecraft/world/entity/NeutralMob.java
++++ b/net/minecraft/world/entity/NeutralMob.java
+@@ -39,18 +_,11 @@
+             } else {
+                 UUID uuid = tag.getUUID("AngryAt");
+                 this.setPersistentAngerTarget(uuid);
+-                Entity entity = ((ServerLevel)level).getEntity(uuid);
+-                if (entity != null) {
+-                    if (entity instanceof Mob mob) {
+-                        this.setTarget(mob);
+-                        this.setLastHurtByMob(mob);
+-                    }
+-
+-                    if (entity instanceof Player player) {
+-                        this.setTarget(player);
+-                        this.setLastHurtByPlayer(player);
+-                    }
+-                }
++                // Paper - Prevent entity loading causing async lookups; Moved diff to separate method
++                // If this entity already survived its first tick, e.g. is loaded and ticked in sync, actively
++                // tick the initial persistent anger.
++                // If not, let the first tick on the baseTick call the method later down the line.
++                if (this instanceof Entity entity && !entity.firstTick) this.tickInitialPersistentAnger(level);
+             }
+         }
+     }
+@@ -104,7 +_,7 @@
+     default void stopBeingAngry() {
+         this.setLastHurtByMob(null);
+         this.setPersistentAngerTarget(null);
+-        this.setTarget(null);
++        this.setTarget(null, org.bukkit.event.entity.EntityTargetEvent.TargetReason.FORGOT_TARGET, true); // CraftBukkit
+         this.setRemainingPersistentAngerTime(0);
+     }
+ 
+@@ -117,8 +_,33 @@
+ 
+     void setTarget(@Nullable LivingEntity livingEntity);
+ 
++    boolean setTarget(@Nullable LivingEntity entityliving, org.bukkit.event.entity.EntityTargetEvent.TargetReason reason, boolean fireEvent); // CraftBukkit
++
+     boolean canAttack(LivingEntity entity);
+ 
+     @Nullable
+     LivingEntity getTarget();
++
++    // Paper start - Prevent entity loading causing async lookups
++    // Update last hurt when ticking
++    default void tickInitialPersistentAnger(Level level) {
++        UUID uuid = getPersistentAngerTarget();
++        if (uuid == null) {
++            return;
++        }
++
++        Entity entity = ((ServerLevel)level).getEntity(uuid);
++        if (entity != null) {
++            if (entity instanceof Mob mob) {
++                this.setTarget(mob, org.bukkit.event.entity.EntityTargetEvent.TargetReason.UNKNOWN, false); // CraftBukkit
++                this.setLastHurtByMob(mob);
++            }
++
++            if (entity instanceof Player player) {
++                this.setTarget(player, org.bukkit.event.entity.EntityTargetEvent.TargetReason.UNKNOWN, false); // CraftBukkit
++                this.setLastHurtByPlayer(player);
++            }
++        }
++    }
++    // Paper end - Prevent entity loading causing async lookups
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/OminousItemSpawner.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/OminousItemSpawner.java.patch
similarity index 70%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/OminousItemSpawner.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/OminousItemSpawner.java.patch
index c390374ad2..10e7665ce7 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/OminousItemSpawner.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/OminousItemSpawner.java.patch
@@ -1,25 +1,25 @@
 --- a/net/minecraft/world/entity/OminousItemSpawner.java
 +++ b/net/minecraft/world/entity/OminousItemSpawner.java
-@@ -76,7 +76,7 @@
-                     entity = this.spawnProjectile(serverLevel, projectileItem, itemStack);
+@@ -76,7 +_,7 @@
+                     entity = this.spawnProjectile(serverLevel, projectileItem, item);
                  } else {
-                     entity = new ItemEntity(serverLevel, this.getX(), this.getY(), this.getZ(), itemStack);
+                     entity = new ItemEntity(serverLevel, this.getX(), this.getY(), this.getZ(), item);
 -                    serverLevel.addFreshEntity(entity);
 +                    serverLevel.addFreshEntity(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.OMINOUS_ITEM_SPAWNER); // Paper - fixes and addition to spawn reason API
                  }
  
                  serverLevel.levelEvent(3021, this.blockPosition(), 1);
-@@ -90,7 +90,7 @@
-         ProjectileItem.DispenseConfig dispenseConfig = item.createDispenseConfig();
-         dispenseConfig.overrideDispenseEvent().ifPresent(dispenseEvent -> world.levelEvent(dispenseEvent, this.blockPosition(), 0));
+@@ -90,7 +_,7 @@
+         ProjectileItem.DispenseConfig dispenseConfig = projectileItem.createDispenseConfig();
+         dispenseConfig.overrideDispenseEvent().ifPresent(i -> level.levelEvent(i, this.blockPosition(), 0));
          Direction direction = Direction.DOWN;
 -        Projectile projectile = Projectile.spawnProjectileUsingShoot(
 +        Projectile projectile = Projectile.spawnProjectileUsingShootDelayed( // Paper - fixes and addition to spawn reason API
-             item.asProjectile(world, this.position(), stack, direction),
-             world,
+             projectileItem.asProjectile(level, this.position(), stack, direction),
+             level,
              stack,
-@@ -99,7 +99,7 @@
-             (double)direction.getStepZ(),
+@@ -99,7 +_,7 @@
+             direction.getStepZ(),
              dispenseConfig.power(),
              dispenseConfig.uncertainty()
 -        );
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/Shearable.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/Shearable.java.patch
similarity index 65%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/Shearable.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/Shearable.java.patch
index aa779fc65d..177b7c0bc7 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/Shearable.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/Shearable.java.patch
@@ -1,13 +1,14 @@
 --- a/net/minecraft/world/entity/Shearable.java
 +++ b/net/minecraft/world/entity/Shearable.java
-@@ -5,7 +5,15 @@
+@@ -5,7 +_,16 @@
  import net.minecraft.world.item.ItemStack;
  
  public interface Shearable {
-+    default void shear(ServerLevel world, SoundSource soundCategory, ItemStack shears, java.util.List<net.minecraft.world.item.ItemStack> drops) { this.shear(world, soundCategory, shears); } // Paper - Add drops to shear events
-     void shear(ServerLevel world, SoundSource shearedSoundCategory, ItemStack shears);
++    default void shear(ServerLevel level, SoundSource soundSource, ItemStack shears, java.util.List<net.minecraft.world.item.ItemStack> drops) { this.shear(level, soundSource, shears); } // Paper - Add drops to shear events
+     void shear(ServerLevel level, SoundSource soundSource, ItemStack shears);
  
      boolean readyForShearing();
++
 +    net.minecraft.world.level.Level level(); // Shearable API - expose default level needed for shearing.
 +
 +    // Paper start - custom shear drops; ensure all implementing entities override this
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/TamableAnimal.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/TamableAnimal.java.patch
new file mode 100644
index 0000000000..b00c96d54c
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/TamableAnimal.java.patch
@@ -0,0 +1,73 @@
+--- a/net/minecraft/world/entity/TamableAnimal.java
++++ b/net/minecraft/world/entity/TamableAnimal.java
+@@ -84,7 +_,7 @@
+         }
+ 
+         this.orderedToSit = compound.getBoolean("Sitting");
+-        this.setInSittingPose(this.orderedToSit);
++        this.setInSittingPose(this.orderedToSit, false); // Paper - Add EntityToggleSitEvent
+     }
+ 
+     @Override
+@@ -95,8 +_,16 @@
+     @Override
+     public boolean handleLeashAtDistance(Entity leashHolder, float distance) {
+         if (this.isInSittingPose()) {
+-            if (distance > 10.0F) {
+-                this.dropLeash();
++            if (distance > (float) this.level().paperConfig().misc.maxLeashDistance.or(Leashable.LEASH_TOO_FAR_DIST)) { // Paper - Configurable max leash distance
++                // Paper start - Expand EntityUnleashEvent
++                org.bukkit.event.entity.EntityUnleashEvent event = new org.bukkit.event.entity.EntityUnleashEvent(this.getBukkitEntity(), org.bukkit.event.entity.EntityUnleashEvent.UnleashReason.DISTANCE, true);
++                if (!event.callEvent()) return false;
++                if (event.isDropLeash()) {
++                    this.dropLeash();
++                } else {
++                    this.removeLeash();
++                }
++                // Paper end - Expand EntityUnleashEvent
+             }
+ 
+             return false;
+@@ -155,6 +_,12 @@
+     }
+ 
+     public void setInSittingPose(boolean sitting) {
++        // Paper start - Add EntityToggleSitEvent
++        this.setInSittingPose(sitting, true);
++    }
++    public void setInSittingPose(boolean sitting, boolean callEvent) {
++        if (callEvent && !new io.papermc.paper.event.entity.EntityToggleSitEvent(this.getBukkitEntity(), sitting).callEvent()) return;
++        // Paper end - Add EntityToggleSitEvent
+         byte b = this.entityData.get(DATA_FLAGS_ID);
+         if (sitting) {
+             this.entityData.set(DATA_FLAGS_ID, (byte)(b | 1));
+@@ -227,7 +_,12 @@
+         if (this.level() instanceof ServerLevel serverLevel
+             && serverLevel.getGameRules().getBoolean(GameRules.RULE_SHOWDEATHMESSAGES)
+             && this.getOwner() instanceof ServerPlayer serverPlayer) {
+-            serverPlayer.sendSystemMessage(this.getCombatTracker().getDeathMessage());
++            // Paper start - Add TameableDeathMessageEvent
++            io.papermc.paper.event.entity.TameableDeathMessageEvent event = new io.papermc.paper.event.entity.TameableDeathMessageEvent((org.bukkit.entity.Tameable) getBukkitEntity(), io.papermc.paper.adventure.PaperAdventure.asAdventure(this.getCombatTracker().getDeathMessage()));
++            if (event.callEvent()) {
++                serverPlayer.sendSystemMessage(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.deathMessage()));
++            }
++            // Paper end - Add TameableDeathMessageEvent
+         }
+ 
+         super.die(cause);
+@@ -270,7 +_,14 @@
+         if (!this.canTeleportTo(new BlockPos(x, y, z))) {
+             return false;
+         } else {
+-            this.moveTo(x + 0.5, y, z + 0.5, this.getYRot(), this.getXRot());
++            // CraftBukkit start
++            org.bukkit.event.entity.EntityTeleportEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTeleportEvent(this, x + 0.5, y, z + 0.5);
++            if (event.isCancelled() || event.getTo() == null) { // Paper - prevent NP on null event to location
++                return false;
++            }
++            org.bukkit.Location to = event.getTo();
++            this.moveTo(to.getX(), to.getY(), to.getZ(), to.getYaw(), to.getPitch());
++            // CraftBukkit end
+             this.navigation.stop();
+             return true;
+         }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/attributes/AttributeInstance.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/attributes/AttributeInstance.java.patch
new file mode 100644
index 0000000000..c315956cf0
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/attributes/AttributeInstance.java.patch
@@ -0,0 +1,27 @@
+--- a/net/minecraft/world/entity/ai/attributes/AttributeInstance.java
++++ b/net/minecraft/world/entity/ai/attributes/AttributeInstance.java
+@@ -153,20 +_,20 @@
+         double baseValue = this.getBaseValue();
+ 
+         for (AttributeModifier attributeModifier : this.getModifiersOrEmpty(AttributeModifier.Operation.ADD_VALUE)) {
+-            baseValue += attributeModifier.amount();
++            baseValue += attributeModifier.amount(); // Paper - destroy speed API - diff on change
+         }
+ 
+         double d = baseValue;
+ 
+         for (AttributeModifier attributeModifier1 : this.getModifiersOrEmpty(AttributeModifier.Operation.ADD_MULTIPLIED_BASE)) {
+-            d += baseValue * attributeModifier1.amount();
++            d += baseValue * attributeModifier1.amount(); // Paper - destroy speed API - diff on change
+         }
+ 
+         for (AttributeModifier attributeModifier1 : this.getModifiersOrEmpty(AttributeModifier.Operation.ADD_MULTIPLIED_TOTAL)) {
+-            d *= 1.0 + attributeModifier1.amount();
++            d *= 1.0 + attributeModifier1.amount(); // Paper - destroy speed API - diff on change
+         }
+ 
+-        return this.attribute.value().sanitizeValue(d);
++        return this.attribute.value().sanitizeValue(d); // Paper - destroy speed API - diff on change
+     }
+ 
+     private Collection<AttributeModifier> getModifiersOrEmpty(AttributeModifier.Operation operation) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/attributes/AttributeMap.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/attributes/AttributeMap.java.patch
similarity index 96%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/ai/attributes/AttributeMap.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/ai/attributes/AttributeMap.java.patch
index 08b12ec883..f685d112e2 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/attributes/AttributeMap.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/attributes/AttributeMap.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/entity/ai/attributes/AttributeMap.java
 +++ b/net/minecraft/world/entity/ai/attributes/AttributeMap.java
-@@ -162,4 +162,12 @@
+@@ -162,4 +_,12 @@
              }
          }
      }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/attributes/Attributes.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/attributes/Attributes.java.patch
new file mode 100644
index 0000000000..85d0966158
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/attributes/Attributes.java.patch
@@ -0,0 +1,33 @@
+--- a/net/minecraft/world/entity/ai/attributes/Attributes.java
++++ b/net/minecraft/world/entity/ai/attributes/Attributes.java
+@@ -10,7 +_,7 @@
+     public static final Holder<Attribute> ARMOR_TOUGHNESS = register(
+         "armor_toughness", new RangedAttribute("attribute.name.armor_toughness", 0.0, 0.0, 20.0).setSyncable(true)
+     );
+-    public static final Holder<Attribute> ATTACK_DAMAGE = register("attack_damage", new RangedAttribute("attribute.name.attack_damage", 2.0, 0.0, 2048.0));
++    public static final Holder<Attribute> ATTACK_DAMAGE = register("attack_damage", new RangedAttribute("attribute.name.attack_damage", 2.0, 0.0, org.spigotmc.SpigotConfig.attackDamage)); // Spigot
+     public static final Holder<Attribute> ATTACK_KNOCKBACK = register("attack_knockback", new RangedAttribute("attribute.name.attack_knockback", 0.0, 0.0, 5.0));
+     public static final Holder<Attribute> ATTACK_SPEED = register(
+         "attack_speed", new RangedAttribute("attribute.name.attack_speed", 4.0, 0.0, 1024.0).setSyncable(true)
+@@ -49,10 +_,10 @@
+     );
+     public static final Holder<Attribute> LUCK = register("luck", new RangedAttribute("attribute.name.luck", 0.0, -1024.0, 1024.0).setSyncable(true));
+     public static final Holder<Attribute> MAX_ABSORPTION = register(
+-        "max_absorption", new RangedAttribute("attribute.name.max_absorption", 0.0, 0.0, 2048.0).setSyncable(true)
++        "max_absorption", new RangedAttribute("attribute.name.max_absorption", 0.0, 0.0, org.spigotmc.SpigotConfig.maxAbsorption).setSyncable(true) // Spigot
+     );
+     public static final Holder<Attribute> MAX_HEALTH = register(
+-        "max_health", new RangedAttribute("attribute.name.max_health", 20.0, 1.0, 1024.0).setSyncable(true)
++        "max_health", new RangedAttribute("attribute.name.max_health", 20.0, 1.0, org.spigotmc.SpigotConfig.maxHealth).setSyncable(true) // Spigot
+     );
+     public static final Holder<Attribute> MINING_EFFICIENCY = register(
+         "mining_efficiency", new RangedAttribute("attribute.name.mining_efficiency", 0.0, 0.0, 1024.0).setSyncable(true)
+@@ -61,7 +_,7 @@
+         "movement_efficiency", new RangedAttribute("attribute.name.movement_efficiency", 0.0, 0.0, 1.0).setSyncable(true)
+     );
+     public static final Holder<Attribute> MOVEMENT_SPEED = register(
+-        "movement_speed", new RangedAttribute("attribute.name.movement_speed", 0.7, 0.0, 1024.0).setSyncable(true)
++        "movement_speed", new RangedAttribute("attribute.name.movement_speed", 0.7, 0.0, org.spigotmc.SpigotConfig.movementSpeed).setSyncable(true) // Spigot
+     );
+     public static final Holder<Attribute> OXYGEN_BONUS = register(
+         "oxygen_bonus", new RangedAttribute("attribute.name.oxygen_bonus", 0.0, 0.0, 1024.0).setSyncable(true)
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/AcquirePoi.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/AcquirePoi.java.patch
new file mode 100644
index 0000000000..777259cb86
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/AcquirePoi.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/world/entity/ai/behavior/AcquirePoi.java
++++ b/net/minecraft/world/entity/ai/behavior/AcquirePoi.java
+@@ -70,6 +_,7 @@
+                             return false;
+                         } else {
+                             mutableLong.setValue(time + 20L + level.getRandom().nextInt(20));
++                            if (mob.getNavigation().isStuck()) mutableLong.add(200); // Paper - Perf: Wait an additional 10s to check again if they're stuck // TODO Modifies Vanilla behavior, add config option
+                             PoiManager poiManager = level.getPoiManager();
+                             map.long2ObjectEntrySet().removeIf(entry -> !entry.getValue().isStillValid(time));
+                             Predicate<BlockPos> predicate1 = pos -> {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/AssignProfessionFromJobSite.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/AssignProfessionFromJobSite.java.patch
new file mode 100644
index 0000000000..60e6ed37af
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/AssignProfessionFromJobSite.java.patch
@@ -0,0 +1,18 @@
+--- a/net/minecraft/world/entity/ai/behavior/AssignProfessionFromJobSite.java
++++ b/net/minecraft/world/entity/ai/behavior/AssignProfessionFromJobSite.java
+@@ -38,7 +_,14 @@
+                                             .findFirst()
+                                     )
+                                     .ifPresent(profession -> {
+-                                        villager.setVillagerData(villager.getVillagerData().setProfession(profession));
++                                        // CraftBukkit start - Fire VillagerCareerChangeEvent where Villager gets employed
++                                        org.bukkit.event.entity.VillagerCareerChangeEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callVillagerCareerChangeEvent(villager, org.bukkit.craftbukkit.entity.CraftVillager.CraftProfession.minecraftToBukkit(profession), org.bukkit.event.entity.VillagerCareerChangeEvent.ChangeReason.EMPLOYED);
++                                        if (event.isCancelled()) {
++                                            return;
++                                        }
++
++                                        villager.setVillagerData(villager.getVillagerData().setProfession(org.bukkit.craftbukkit.entity.CraftVillager.CraftProfession.bukkitToMinecraft(event.getProfession())));
++                                        // CraftBukkit end
+                                         villager.refreshBrain(level);
+                                     });
+                                 return true;
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/BabyFollowAdult.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/BabyFollowAdult.java.patch
new file mode 100644
index 0000000000..0695aae31f
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/BabyFollowAdult.java.patch
@@ -0,0 +1,23 @@
+--- a/net/minecraft/world/entity/ai/behavior/BabyFollowAdult.java
++++ b/net/minecraft/world/entity/ai/behavior/BabyFollowAdult.java
+@@ -24,8 +_,19 @@
+                     if (!mob.isBaby()) {
+                         return false;
+                     } else {
+-                        AgeableMob ageableMob = instance.get(nearestVisibleAdult);
++                        LivingEntity ageableMob = instance.get(nearestVisibleAdult); // CraftBukkit - type
+                         if (mob.closerThan(ageableMob, followRange.getMaxValue() + 1) && !mob.closerThan(ageableMob, followRange.getMinValue())) {
++                            // CraftBukkit start
++                            org.bukkit.event.entity.EntityTargetLivingEntityEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTargetLivingEvent(mob, ageableMob, org.bukkit.event.entity.EntityTargetEvent.TargetReason.FOLLOW_LEADER);
++                            if (event.isCancelled()) {
++                                return false;
++                            }
++                            if (event.getTarget() == null) {
++                                nearestVisibleAdult.erase();
++                                return true;
++                            }
++                            ageableMob = ((org.bukkit.craftbukkit.entity.CraftLivingEntity) event.getTarget()).getHandle();
++                            // CraftBukkit end
+                             WalkTarget walkTarget1 = new WalkTarget(
+                                 new EntityTracker(ageableMob, false), speedModifier.apply(mob), followRange.getMinValue() - 1
+                             );
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/Behavior.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/Behavior.java.patch
similarity index 53%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/Behavior.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/Behavior.java.patch
index dab10a1e20..5088503a08 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/Behavior.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/Behavior.java.patch
@@ -1,19 +1,17 @@
 --- a/net/minecraft/world/entity/ai/behavior/Behavior.java
 +++ b/net/minecraft/world/entity/ai/behavior/Behavior.java
-@@ -14,6 +14,9 @@
+@@ -14,6 +_,7 @@
      private long endTimestamp;
      private final int minDuration;
      private final int maxDuration;
-+    // Paper start - configurable behavior tick rate and timings
-+    private final String configKey;
-+    // Paper end - configurable behavior tick rate and timings
++    private final String configKey; // Paper - configurable behavior tick rate and timings
  
-     public Behavior(Map<MemoryModuleType<?>, MemoryStatus> requiredMemoryState) {
-         this(requiredMemoryState, 60);
-@@ -27,6 +30,14 @@
-         this.minDuration = minRunTime;
-         this.maxDuration = maxRunTime;
-         this.entryCondition = requiredMemoryState;
+     public Behavior(Map<MemoryModuleType<?>, MemoryStatus> entryCondition) {
+         this(entryCondition, 60);
+@@ -27,6 +_,14 @@
+         this.minDuration = minDuration;
+         this.maxDuration = maxDuration;
+         this.entryCondition = entryCondition;
 +        // Paper start - configurable behavior tick rate and timings
 +        String key = io.papermc.paper.util.MappingEnvironment.reobf() ? io.papermc.paper.util.ObfHelper.INSTANCE.deobfClassName(this.getClass().getName()) : this.getClass().getName();
 +        int lastSeparator = key.lastIndexOf('.');
@@ -25,16 +23,16 @@
      }
  
      @Override
-@@ -36,6 +47,12 @@
+@@ -36,6 +_,12 @@
  
      @Override
-     public final boolean tryStart(ServerLevel world, E entity, long time) {
+     public final boolean tryStart(ServerLevel level, E owner, long gameTime) {
 +        // Paper start - configurable behavior tick rate and timings
-+        int tickRate = java.util.Objects.requireNonNullElse(world.paperConfig().tickRates.behavior.get(entity.getType(), this.configKey), -1);
-+        if (tickRate > -1 && time < this.endTimestamp + tickRate) {
++        int tickRate = java.util.Objects.requireNonNullElse(level.paperConfig().tickRates.behavior.get(owner.getType(), this.configKey), -1);
++        if (tickRate > -1 && gameTime < this.endTimestamp + tickRate) {
 +            return false;
 +        }
 +        // Paper end - configurable behavior tick rate and timings
-         if (this.hasRequiredMemories(entity) && this.checkExtraStartConditions(world, entity)) {
+         if (this.hasRequiredMemories(owner) && this.checkExtraStartConditions(level, owner)) {
              this.status = Behavior.Status.RUNNING;
-             int i = this.minDuration + world.getRandom().nextInt(this.maxDuration + 1 - this.minDuration);
+             int i = this.minDuration + level.getRandom().nextInt(this.maxDuration + 1 - this.minDuration);
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java.patch
new file mode 100644
index 0000000000..77f8530cc4
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java.patch
@@ -0,0 +1,24 @@
+--- a/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java
++++ b/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java
+@@ -80,6 +_,7 @@
+     }
+ 
+     public static void throwItem(LivingEntity entity, ItemStack stack, Vec3 offset, Vec3 speedMultiplier, float yOffset) {
++        if (stack.isEmpty()) return; // CraftBukkit - SPIGOT-4940: no empty loot
+         double d = entity.getEyeY() - yOffset;
+         ItemEntity itemEntity = new ItemEntity(entity.level(), entity.getX(), d, entity.getZ(), stack);
+         itemEntity.setThrower(entity);
+@@ -87,6 +_,13 @@
+         vec3 = vec3.normalize().multiply(speedMultiplier.x, speedMultiplier.y, speedMultiplier.z);
+         itemEntity.setDeltaMovement(vec3);
+         itemEntity.setDefaultPickUpDelay();
++        // CraftBukkit start
++        org.bukkit.event.entity.EntityDropItemEvent event = new org.bukkit.event.entity.EntityDropItemEvent(entity.getBukkitEntity(), (org.bukkit.entity.Item) itemEntity.getBukkitEntity());
++        itemEntity.level().getCraftServer().getPluginManager().callEvent(event);
++        if (event.isCancelled()) {
++            return;
++        }
++        // CraftBukkit end
+         entity.level().addFreshEntity(itemEntity);
+     }
+ 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/GateBehavior.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/GateBehavior.java.patch
similarity index 97%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/GateBehavior.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/GateBehavior.java.patch
index fdb4490626..d7a1d368e0 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/GateBehavior.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/GateBehavior.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/entity/ai/behavior/GateBehavior.java
 +++ b/net/minecraft/world/entity/ai/behavior/GateBehavior.java
-@@ -18,7 +18,7 @@
+@@ -18,7 +_,7 @@
      private final Set<MemoryModuleType<?>> exitErasedMemories;
      private final GateBehavior.OrderPolicy orderPolicy;
      private final GateBehavior.RunningPolicy runningPolicy;
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/GoToWantedItem.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/GoToWantedItem.java.patch
new file mode 100644
index 0000000000..b4c97cad9b
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/GoToWantedItem.java.patch
@@ -0,0 +1,24 @@
+--- a/net/minecraft/world/entity/ai/behavior/GoToWantedItem.java
++++ b/net/minecraft/world/entity/ai/behavior/GoToWantedItem.java
+@@ -35,6 +_,21 @@
+                                 && itemEntity.closerThan(entity, maxDistToWalk)
+                                 && entity.level().getWorldBorder().isWithinBounds(itemEntity.blockPosition())
+                                 && entity.canPickUpLoot()) {
++                                // CraftBukkit start
++                                if (entity instanceof net.minecraft.world.entity.animal.allay.Allay) {
++                                    org.bukkit.event.entity.EntityTargetEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTargetEvent(entity, itemEntity, org.bukkit.event.entity.EntityTargetEvent.TargetReason.CLOSEST_ENTITY);
++
++                                    if (event.isCancelled()) {
++                                        return false;
++                                    }
++                                    if (!(event.getTarget() instanceof org.bukkit.craftbukkit.entity.CraftItem targetItem)) { // Paper - only erase allay memory on non-item targets
++                                        nearestVisibleWantedItem.erase();
++                                        return false; // Paper - only erase allay memory on non-item targets
++                                    }
++
++                                    itemEntity = targetItem.getHandle();
++                                }
++                                // CraftBukkit end
+                                 WalkTarget walkTarget1 = new WalkTarget(new EntityTracker(itemEntity, false), speedModifier, 0);
+                                 lookTarget.set(new EntityTracker(itemEntity, true));
+                                 walkTarget.set(walkTarget1);
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/HarvestFarmland.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/HarvestFarmland.java.patch
new file mode 100644
index 0000000000..32e15ef453
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/HarvestFarmland.java.patch
@@ -0,0 +1,24 @@
+--- a/net/minecraft/world/entity/ai/behavior/HarvestFarmland.java
++++ b/net/minecraft/world/entity/ai/behavior/HarvestFarmland.java
+@@ -110,7 +_,9 @@
+                 Block block = blockState.getBlock();
+                 Block block1 = level.getBlockState(this.aboveFarmlandPos.below()).getBlock();
+                 if (block instanceof CropBlock && ((CropBlock)block).isMaxAge(blockState)) {
++                    if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(owner, this.aboveFarmlandPos, blockState.getFluidState().createLegacyBlock())) { // CraftBukkit // Paper - fix wrong block state
+                     level.destroyBlock(this.aboveFarmlandPos, true, owner);
++                    } // CraftBukkit
+                 }
+ 
+                 if (blockState.isAir() && block1 instanceof FarmBlock && owner.hasFarmSeeds()) {
+@@ -121,9 +_,11 @@
+                         boolean flag = false;
+                         if (!item.isEmpty() && item.is(ItemTags.VILLAGER_PLANTABLE_SEEDS) && item.getItem() instanceof BlockItem blockItem) {
+                             BlockState blockState1 = blockItem.getBlock().defaultBlockState();
++                            if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(owner, this.aboveFarmlandPos, blockState1)) { // CraftBukkit
+                             level.setBlockAndUpdate(this.aboveFarmlandPos, blockState1);
+                             level.gameEvent(GameEvent.BLOCK_PLACE, this.aboveFarmlandPos, GameEvent.Context.of(owner, blockState1));
+                             flag = true;
++                            } // CraftBukkit
+                         }
+ 
+                         if (flag) {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/InteractWithDoor.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/InteractWithDoor.java.patch
new file mode 100644
index 0000000000..05862afb29
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/InteractWithDoor.java.patch
@@ -0,0 +1,28 @@
+--- a/net/minecraft/world/entity/ai/behavior/InteractWithDoor.java
++++ b/net/minecraft/world/entity/ai/behavior/InteractWithDoor.java
+@@ -58,6 +_,12 @@
+                             if (blockState.is(BlockTags.MOB_INTERACTABLE_DOORS, state -> state.getBlock() instanceof DoorBlock)) {
+                                 DoorBlock doorBlock = (DoorBlock)blockState.getBlock();
+                                 if (!doorBlock.isOpen(blockState)) {
++                                    // CraftBukkit start - entities opening doors
++                                    org.bukkit.event.entity.EntityInteractEvent event = new org.bukkit.event.entity.EntityInteractEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(entity.level(), blockPos));
++                                    if (!event.callEvent()) {
++                                        return false;
++                                    }
++                                    // CraftBukkit end - entities opening doors
+                                     doorBlock.setOpen(entity, level, blockState, blockPos, true);
+                                 }
+ 
+@@ -69,6 +_,12 @@
+                             if (blockState1.is(BlockTags.MOB_INTERACTABLE_DOORS, state -> state.getBlock() instanceof DoorBlock)) {
+                                 DoorBlock doorBlock1 = (DoorBlock)blockState1.getBlock();
+                                 if (!doorBlock1.isOpen(blockState1)) {
++                                    // CraftBukkit start - entities opening doors
++                                    org.bukkit.event.entity.EntityInteractEvent event = new org.bukkit.event.entity.EntityInteractEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(entity.level(), blockPos1));
++                                    if (!event.callEvent()) {
++                                        return false;
++                                    }
++                                    // CraftBukkit end - entities opening doors
+                                     doorBlock1.setOpen(entity, level, blockState1, blockPos1, true);
+                                     optional = rememberDoorToClose(doorsToClose, optional, level, blockPos1);
+                                 }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/PrepareRamNearestTarget.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/PrepareRamNearestTarget.java.patch
new file mode 100644
index 0000000000..41b07e6724
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/PrepareRamNearestTarget.java.patch
@@ -0,0 +1,19 @@
+--- a/net/minecraft/world/entity/ai/behavior/PrepareRamNearestTarget.java
++++ b/net/minecraft/world/entity/ai/behavior/PrepareRamNearestTarget.java
+@@ -75,6 +_,16 @@
+             .flatMap(
+                 nearestVisibleLivingEntities -> nearestVisibleLivingEntities.findClosest(livingEntity -> this.ramTargeting.test(level, entity, livingEntity))
+             )
++            // CraftBukkit start
++            .map((livingEntity) -> {
++                org.bukkit.event.entity.EntityTargetEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTargetLivingEvent(entity, livingEntity, (livingEntity instanceof net.minecraft.server.level.ServerPlayer) ? org.bukkit.event.entity.EntityTargetEvent.TargetReason.CLOSEST_PLAYER : org.bukkit.event.entity.EntityTargetEvent.TargetReason.CLOSEST_ENTITY);
++                if (event.isCancelled() || event.getTarget() == null) {
++                    return null;
++                }
++                livingEntity = ((org.bukkit.craftbukkit.entity.CraftLivingEntity) event.getTarget()).getHandle();
++                return livingEntity;
++            })
++            // CraftBukkit end
+             .ifPresent(entity1 -> this.chooseRamPosition(entity, entity1));
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/RamTarget.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/RamTarget.java.patch
new file mode 100644
index 0000000000..0f29b936ec
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/RamTarget.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/ai/behavior/RamTarget.java
++++ b/net/minecraft/world/entity/ai/behavior/RamTarget.java
+@@ -89,7 +_,7 @@
+             float f = 0.25F * (i - i1);
+             float f1 = Mth.clamp(owner.getSpeed() * 1.65F, 0.2F, 3.0F) + f;
+             float f2 = livingEntity.isDamageSourceBlocked(level.damageSources().mobAttack(owner)) ? 0.5F : 1.0F;
+-            livingEntity.knockback(f2 * f1 * this.getKnockbackForce.applyAsDouble(owner), this.ramDirection.x(), this.ramDirection.z());
++            livingEntity.knockback(f2 * f1 * this.getKnockbackForce.applyAsDouble(owner), this.ramDirection.x(), this.ramDirection.z(), owner, io.papermc.paper.event.entity.EntityKnockbackEvent.Cause.ENTITY_ATTACK); // Paper - Add EntityKnockbackByEntityEvent and EntityPushedByEntityAttackEvent
+             this.finishRam(level, owner);
+             level.playSound(null, owner, this.getImpactSound.apply(owner), SoundSource.NEUTRAL, 1.0F, 1.0F);
+         } else if (this.hasRammedHornBreakingBlock(level, owner)) {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/ResetProfession.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/ResetProfession.java.patch
new file mode 100644
index 0000000000..580871ed59
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/ResetProfession.java.patch
@@ -0,0 +1,18 @@
+--- a/net/minecraft/world/entity/ai/behavior/ResetProfession.java
++++ b/net/minecraft/world/entity/ai/behavior/ResetProfession.java
+@@ -18,7 +_,14 @@
+                             && villagerData.getProfession() != VillagerProfession.NITWIT
+                             && villager.getVillagerXp() == 0
+                             && villagerData.getLevel() <= 1) {
+-                            villager.setVillagerData(villager.getVillagerData().setProfession(VillagerProfession.NONE));
++                            // CraftBukkit start
++                            org.bukkit.event.entity.VillagerCareerChangeEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callVillagerCareerChangeEvent(villager, org.bukkit.craftbukkit.entity.CraftVillager.CraftProfession.minecraftToBukkit(VillagerProfession.NONE), org.bukkit.event.entity.VillagerCareerChangeEvent.ChangeReason.LOSING_JOB);
++                            if (event.isCancelled()) {
++                                return false;
++                            }
++
++                            villager.setVillagerData(villager.getVillagerData().setProfession(org.bukkit.craftbukkit.entity.CraftVillager.CraftProfession.bukkitToMinecraft(event.getProfession())));
++                            // CraftBukkit end
+                             villager.refreshBrain(level);
+                             return true;
+                         } else {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/ShufflingList.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/ShufflingList.java.patch
similarity index 92%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/ShufflingList.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/ShufflingList.java.patch
index 3f1d97092e..ddad0d3698 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/ShufflingList.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/ShufflingList.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/entity/ai/behavior/ShufflingList.java
 +++ b/net/minecraft/world/entity/ai/behavior/ShufflingList.java
-@@ -16,12 +16,25 @@
+@@ -16,12 +_,25 @@
  public class ShufflingList<U> implements Iterable<U> {
      protected final List<ShufflingList.WeightedEntry<U>> entries;
      private final RandomSource random = RandomSource.create();
@@ -16,17 +16,17 @@
          this.entries = Lists.newArrayList();
      }
  
-     private ShufflingList(List<ShufflingList.WeightedEntry<U>> list) {
+     private ShufflingList(List<ShufflingList.WeightedEntry<U>> entries) {
 +        // Paper start - Fix Concurrency issue in ShufflingList during worldgen
-+        this(list, true);
++        this(entries, true);
 +    }
-+    private ShufflingList(List<ShufflingList.WeightedEntry<U>> list, boolean isUnsafe) {
++    private ShufflingList(List<ShufflingList.WeightedEntry<U>> entries, boolean isUnsafe) {
 +        this.isUnsafe = isUnsafe;
 +        // Paper end - Fix Concurrency issue in ShufflingList during worldgen
-         this.entries = Lists.newArrayList(list);
+         this.entries = Lists.newArrayList(entries);
      }
  
-@@ -35,9 +48,12 @@
+@@ -35,9 +_,12 @@
      }
  
      public ShufflingList<U> shuffle() {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/SleepInBed.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/SleepInBed.java.patch
similarity index 58%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/SleepInBed.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/SleepInBed.java.patch
index d905f5c1d7..cfe4ec0579 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/SleepInBed.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/SleepInBed.java.patch
@@ -1,12 +1,12 @@
 --- a/net/minecraft/world/entity/ai/behavior/SleepInBed.java
 +++ b/net/minecraft/world/entity/ai/behavior/SleepInBed.java
-@@ -42,7 +42,8 @@
+@@ -42,7 +_,8 @@
                      }
                  }
  
--                BlockState blockState = world.getBlockState(globalPos.pos());
-+                BlockState blockState = world.getBlockStateIfLoaded(globalPos.pos()); // Paper - Prevent sync chunk loads when villagers try to find beds
+-                BlockState blockState = level.getBlockState(globalPos.pos());
++                BlockState blockState = level.getBlockStateIfLoaded(globalPos.pos()); // Paper - Prevent sync chunk loads when villagers try to find beds
 +                if (blockState == null) { return false; } // Paper - Prevent sync chunk loads when villagers try to find beds
-                 return globalPos.pos().closerToCenterThan(entity.position(), 2.0) && blockState.is(BlockTags.BEDS) && !blockState.getValue(BedBlock.OCCUPIED);
+                 return globalPos.pos().closerToCenterThan(owner.position(), 2.0) && blockState.is(BlockTags.BEDS) && !blockState.getValue(BedBlock.OCCUPIED);
              }
          }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/StartAttacking.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/StartAttacking.java.patch
new file mode 100644
index 0000000000..4915d1fd6b
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/StartAttacking.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/entity/ai/behavior/StartAttacking.java
++++ b/net/minecraft/world/entity/ai/behavior/StartAttacking.java
+@@ -27,6 +_,17 @@
+                             if (!entity.canAttack(livingEntity)) {
+                                 return false;
+                             } else {
++                                // CraftBukkit start
++                                org.bukkit.event.entity.EntityTargetEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTargetLivingEvent(entity, livingEntity, (livingEntity instanceof net.minecraft.server.level.ServerPlayer) ? org.bukkit.event.entity.EntityTargetEvent.TargetReason.CLOSEST_PLAYER : org.bukkit.event.entity.EntityTargetEvent.TargetReason.CLOSEST_ENTITY);
++                                if (event.isCancelled()) {
++                                    return false;
++                                }
++                                if (event.getTarget() == null) {
++                                    memoryAccessor.erase();
++                                    return true;
++                                }
++                                livingEntity = ((org.bukkit.craftbukkit.entity.CraftLivingEntity) event.getTarget()).getHandle();
++                                // CraftBukkit end
+                                 memoryAccessor.set(livingEntity);
+                                 memoryAccessor1.erase();
+                                 return true;
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/StopAttackingIfTargetInvalid.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/StopAttackingIfTargetInvalid.java.patch
new file mode 100644
index 0000000000..24a503eca0
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/StopAttackingIfTargetInvalid.java.patch
@@ -0,0 +1,33 @@
+--- a/net/minecraft/world/entity/ai/behavior/StopAttackingIfTargetInvalid.java
++++ b/net/minecraft/world/entity/ai/behavior/StopAttackingIfTargetInvalid.java
+@@ -40,6 +_,30 @@
+                             && !canStopAttacking.test(level, livingEntity)) {
+                             return true;
+                         } else {
++                        // Paper start - better track target change reason
++                        final org.bukkit.event.entity.EntityTargetEvent.TargetReason reason;
++                        if (!entity.canAttack(livingEntity)) {
++                            reason = org.bukkit.event.entity.EntityTargetEvent.TargetReason.TARGET_INVALID;
++                        } else if (canGrowTiredOfTryingToReachTarget && StopAttackingIfTargetInvalid.isTiredOfTryingToReachTarget(entity, instance.tryGet(memoryAccessor1))) {
++                            reason = org.bukkit.event.entity.EntityTargetEvent.TargetReason.FORGOT_TARGET;
++                        } else if (!livingEntity.isAlive()) {
++                            reason = org.bukkit.event.entity.EntityTargetEvent.TargetReason.TARGET_DIED;
++                        } else if (livingEntity.level() != entity.level()) {
++                            reason = org.bukkit.event.entity.EntityTargetEvent.TargetReason.TARGET_OTHER_LEVEL;
++                        } else {
++                            reason = org.bukkit.event.entity.EntityTargetEvent.TargetReason.TARGET_INVALID;
++                        }
++                        // Paper end
++                        // CraftBukkit start
++                        org.bukkit.event.entity.EntityTargetEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTargetLivingEvent(entity, null, reason); // Paper
++                        if (event.isCancelled()) {
++                            return false;
++                        }
++                        if (event.getTarget() != null) {
++                            entity.getBrain().setMemory(MemoryModuleType.ATTACK_TARGET, ((org.bukkit.craftbukkit.entity.CraftLivingEntity) event.getTarget()).getHandle());
++                            return true;
++                        }
++                        // CraftBukkit end
+                             onStopAttacking.accept(level, entity, livingEntity);
+                             memoryAccessor.erase();
+                             return true;
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/TryLaySpawnOnWaterNearLand.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/TryLaySpawnOnWaterNearLand.java.patch
new file mode 100644
index 0000000000..da9c516844
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/TryLaySpawnOnWaterNearLand.java.patch
@@ -0,0 +1,15 @@
+--- a/net/minecraft/world/entity/ai/behavior/TryLaySpawnOnWaterNearLand.java
++++ b/net/minecraft/world/entity/ai/behavior/TryLaySpawnOnWaterNearLand.java
+@@ -33,6 +_,12 @@
+                                     BlockPos blockPos2 = blockPos1.above();
+                                     if (level.getBlockState(blockPos2).isAir()) {
+                                         BlockState blockState = spawnBlock.defaultBlockState();
++                                        // CraftBukkit start
++                                        if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(entity, blockPos2, blockState)) {
++                                            isPregnant.erase();
++                                            return true;
++                                        }
++                                        // CraftBukkit end
+                                         level.setBlock(blockPos2, blockState, 3);
+                                         level.gameEvent(GameEvent.BLOCK_PLACE, blockPos2, GameEvent.Context.of(entity, blockState));
+                                         level.playSound(null, entity, SoundEvents.FROG_LAY_SPAWN, SoundSource.BLOCKS, 1.0F, 1.0F);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/VillagerGoalPackages.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/VillagerGoalPackages.java.patch
similarity index 72%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/VillagerGoalPackages.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/VillagerGoalPackages.java.patch
index c8260c2305..cac3171662 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/VillagerGoalPackages.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/VillagerGoalPackages.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/world/entity/ai/behavior/VillagerGoalPackages.java
 +++ b/net/minecraft/world/entity/ai/behavior/VillagerGoalPackages.java
-@@ -42,7 +42,7 @@
+@@ -42,7 +_,7 @@
              Pair.of(1, new MoveToTargetSink()),
              Pair.of(2, PoiCompetitorScan.create()),
-             Pair.of(3, new LookAndFollowTradingPlayerSink(speed)),
--            Pair.of(5, GoToWantedItem.create(speed, false, 4)),
-+            Pair.of(5, GoToWantedItem.create(villager -> !villager.isSleeping(), speed, false, 4)), // Paper - Fix MC-157464
+             Pair.of(3, new LookAndFollowTradingPlayerSink(speedModifier)),
+-            Pair.of(5, GoToWantedItem.create(speedModifier, false, 4)),
++            Pair.of(5, GoToWantedItem.create(villager -> !villager.isSleeping(), speedModifier, false, 4)), // Paper - Fix MC-157464
              Pair.of(
                  6,
                  AcquirePoi.create(
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/VillagerMakeLove.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/VillagerMakeLove.java.patch
new file mode 100644
index 0000000000..303c80235c
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/VillagerMakeLove.java.patch
@@ -0,0 +1,23 @@
+--- a/net/minecraft/world/entity/ai/behavior/VillagerMakeLove.java
++++ b/net/minecraft/world/entity/ai/behavior/VillagerMakeLove.java
+@@ -111,11 +_,17 @@
+         if (breedOffspring == null) {
+             return Optional.empty();
+         } else {
+-            parent.setAge(6000);
+-            partner.setAge(6000);
+             breedOffspring.setAge(-24000);
+             breedOffspring.moveTo(parent.getX(), parent.getY(), parent.getZ(), 0.0F, 0.0F);
+-            level.addFreshEntityWithPassengers(breedOffspring);
++            // CraftBukkit start - call EntityBreedEvent
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityBreedEvent(breedOffspring, parent, partner, null, null, 0).isCancelled()) {
++                return Optional.empty();
++            }
++            // Move age setting down
++            parent.setAge(6000);
++            partner.setAge(6000);
++            level.addFreshEntityWithPassengers(breedOffspring, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.BREEDING);
++            // CraftBukkit end
+             level.broadcastEntityEvent(breedOffspring, (byte)12);
+             return Optional.of(breedOffspring);
+         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/WorkAtComposter.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/WorkAtComposter.java.patch
similarity index 62%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/WorkAtComposter.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/WorkAtComposter.java.patch
index 3d8d9ce4bd..9f783335fb 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/WorkAtComposter.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/WorkAtComposter.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/world/entity/ai/behavior/WorkAtComposter.java
 +++ b/net/minecraft/world/entity/ai/behavior/WorkAtComposter.java
-@@ -86,7 +86,9 @@
-                 simpleContainer.removeItemType(Items.WHEAT, m);
-                 ItemStack itemStack = simpleContainer.addItem(new ItemStack(Items.BREAD, l));
+@@ -86,7 +_,9 @@
+                 inventory.removeItemType(Items.WHEAT, i3);
+                 ItemStack itemStack = inventory.addItem(new ItemStack(Items.BREAD, min));
                  if (!itemStack.isEmpty()) {
 +                    villager.forceDrops = true; // Paper - Add missing forceDrop toggles
-                     villager.spawnAtLocation(world, itemStack, 0.5F);
+                     villager.spawnAtLocation(level, itemStack, 0.5F);
 +                    villager.forceDrops = false; // Paper - Add missing forceDrop toggles
                  }
              }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/warden/Digging.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/warden/Digging.java.patch
new file mode 100644
index 0000000000..c571e45b5e
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/warden/Digging.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/ai/behavior/warden/Digging.java
++++ b/net/minecraft/world/entity/ai/behavior/warden/Digging.java
+@@ -39,7 +_,7 @@
+     @Override
+     protected void stop(ServerLevel level, E entity, long gameTime) {
+         if (entity.getRemovalReason() == null) {
+-            entity.remove(Entity.RemovalReason.DISCARDED);
++            entity.remove(Entity.RemovalReason.DISCARDED, org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - Add bukkit remove cause
+         }
+     }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/warden/SonicBoom.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/warden/SonicBoom.java.patch
new file mode 100644
index 0000000000..4289534ef3
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/behavior/warden/SonicBoom.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/ai/behavior/warden/SonicBoom.java
++++ b/net/minecraft/world/entity/ai/behavior/warden/SonicBoom.java
+@@ -83,7 +_,7 @@
+                     if (livingEntity.hurtServer(level, level.damageSources().sonicBoom(owner), 10.0F)) {
+                         double d = 0.5 * (1.0 - livingEntity.getAttributeValue(Attributes.KNOCKBACK_RESISTANCE));
+                         double d1 = 2.5 * (1.0 - livingEntity.getAttributeValue(Attributes.KNOCKBACK_RESISTANCE));
+-                        livingEntity.push(vec32.x() * d1, vec32.y() * d, vec32.z() * d1);
++                        livingEntity.push(vec32.x() * d1, vec32.y() * d, vec32.z() * d1, owner); // Paper - Add EntityKnockbackByEntityEvent and EntityPushedByEntityAttackEvent
+                     }
+                 });
+         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/BreakDoorGoal.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/BreakDoorGoal.java.patch
similarity index 98%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/BreakDoorGoal.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/ai/goal/BreakDoorGoal.java.patch
index 8323bfa7ee..9fd1ccd71c 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/BreakDoorGoal.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/BreakDoorGoal.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/entity/ai/goal/BreakDoorGoal.java
 +++ b/net/minecraft/world/entity/ai/goal/BreakDoorGoal.java
-@@ -72,9 +72,16 @@
+@@ -73,9 +_,16 @@
          }
  
          if (this.breakTime == this.getDoorBreakTime() && this.isValidDifficulty(this.mob.level().getDifficulty())) {
@@ -16,5 +16,5 @@
 -            this.mob.level().levelEvent(2001, this.doorPos, Block.getId(this.mob.level().getBlockState(this.doorPos)));
 +            this.mob.level().levelEvent(2001, this.doorPos, Block.getId(oldState)); // Paper - fix MC-263999
          }
- 
      }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/EatBlockGoal.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/EatBlockGoal.java.patch
new file mode 100644
index 0000000000..8cc09baa9d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/EatBlockGoal.java.patch
@@ -0,0 +1,35 @@
+--- a/net/minecraft/world/entity/ai/goal/EatBlockGoal.java
++++ b/net/minecraft/world/entity/ai/goal/EatBlockGoal.java
+@@ -26,6 +_,11 @@
+ 
+     @Override
+     public boolean canUse() {
++        // Paper start - Fix MC-210802
++        if (!((net.minecraft.server.level.ServerLevel) this.level).chunkSource.chunkMap.anyPlayerCloseEnoughForSpawning(this.mob.chunkPosition())) {
++            return false;
++        }
++        // Paper end
+         if (this.mob.getRandom().nextInt(this.mob.isBaby() ? 50 : 1000) != 0) {
+             return false;
+         } else {
+@@ -60,8 +_,9 @@
+         this.eatAnimationTick = Math.max(0, this.eatAnimationTick - 1);
+         if (this.eatAnimationTick == this.adjustedTickDelay(4)) {
+             BlockPos blockPos = this.mob.blockPosition();
+-            if (IS_TALL_GRASS.test(this.level.getBlockState(blockPos))) {
+-                if (getServerLevel(this.level).getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) {
++            final BlockState blockState = this.level.getBlockState(blockPos); // Paper - fix wrong block state
++            if (IS_TALL_GRASS.test(blockState)) { // Paper - fix wrong block state
++                if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this.mob, blockPos, blockState.getFluidState().createLegacyBlock(), !getServerLevel(this.level).getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING))) { // CraftBukkit // Paper - fix wrong block state
+                     this.level.destroyBlock(blockPos, false);
+                 }
+ 
+@@ -69,7 +_,7 @@
+             } else {
+                 BlockPos blockPos1 = blockPos.below();
+                 if (this.level.getBlockState(blockPos1).is(Blocks.GRASS_BLOCK)) {
+-                    if (getServerLevel(this.level).getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) {
++                    if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this.mob, blockPos1, Blocks.DIRT.defaultBlockState(), !getServerLevel(this.level).getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING))) { // CraftBukkit // Paper - Fix wrong block state
+                         this.level.levelEvent(2001, blockPos1, Block.getId(Blocks.GRASS_BLOCK.defaultBlockState()));
+                         this.level.setBlock(blockPos1, Blocks.DIRT.defaultBlockState(), 2);
+                     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/FloatGoal.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/FloatGoal.java.patch
similarity index 59%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/FloatGoal.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/ai/goal/FloatGoal.java.patch
index ed90ccea87..f73e97c903 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/FloatGoal.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/FloatGoal.java.patch
@@ -1,10 +1,10 @@
 --- a/net/minecraft/world/entity/ai/goal/FloatGoal.java
 +++ b/net/minecraft/world/entity/ai/goal/FloatGoal.java
-@@ -9,6 +9,7 @@
+@@ -9,6 +_,7 @@
  
      public FloatGoal(Mob mob) {
          this.mob = mob;
-+        if (mob.getCommandSenderWorld().paperConfig().entities.behavior.spawnerNerfedMobsShouldJump) mob.goalFloat = this; // Paper - Allow nerfed mobs to jump and float
++        if (mob.level().paperConfig().entities.behavior.spawnerNerfedMobsShouldJump) mob.goalFloat = this; // Paper - Allow nerfed mobs to jump and float
          this.setFlags(EnumSet.of(Goal.Flag.JUMP));
          mob.getNavigation().setCanFloat(true);
      }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java.patch
similarity index 51%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java.patch
index 4cdf53ef91..32807a319e 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java
 +++ b/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java
-@@ -72,7 +72,7 @@
+@@ -72,7 +_,7 @@
      public void tick() {
-         boolean bl = this.tamable.shouldTryTeleportToOwner();
-         if (!bl) {
--            this.tamable.getLookControl().setLookAt(this.owner, 10.0F, (float)this.tamable.getMaxHeadXRot());
-+            if (this.tamable.distanceToSqr(this.owner) <= 16 * 16) this.tamable.getLookControl().setLookAt(this.owner, 10.0F, (float)this.tamable.getMaxHeadXRot()); // Paper - Limit pet look distance
+         boolean shouldTryTeleportToOwner = this.tamable.shouldTryTeleportToOwner();
+         if (!shouldTryTeleportToOwner) {
+-            this.tamable.getLookControl().setLookAt(this.owner, 10.0F, this.tamable.getMaxHeadXRot());
++            if (this.tamable.distanceToSqr(this.owner) <= 16 * 16) this.tamable.getLookControl().setLookAt(this.owner, 10.0F, this.tamable.getMaxHeadXRot()); // Paper - Limit pet look distance
          }
  
          if (--this.timeToRecalcPath <= 0) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/Goal.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/Goal.java.patch
similarity index 79%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/Goal.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/ai/goal/Goal.java.patch
index a354e63b19..8760a911ad 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/Goal.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/Goal.java.patch
@@ -1,9 +1,10 @@
 --- a/net/minecraft/world/entity/ai/goal/Goal.java
 +++ b/net/minecraft/world/entity/ai/goal/Goal.java
-@@ -46,6 +46,16 @@
+@@ -46,6 +_,17 @@
          return this.flags;
      }
  
++
 +    // Paper start - Mob Goal API
 +    public boolean hasFlag(final Goal.Flag flag) {
 +        return this.flags.contains(flag);
@@ -14,17 +15,17 @@
 +    }
 +    // Paper end - Mob Goal API
 +
-     protected int adjustedTickDelay(int ticks) {
-         return this.requiresUpdateEveryTick() ? ticks : reducedTickDelay(ticks);
+     protected int adjustedTickDelay(int adjustment) {
+         return this.requiresUpdateEveryTick() ? adjustment : reducedTickDelay(adjustment);
      }
-@@ -62,7 +72,19 @@
-         return (ServerLevel)world;
+@@ -62,7 +_,19 @@
+         return (ServerLevel)level;
      }
  
 +    // Paper start - Mob goal api
 +    private com.destroystokyo.paper.entity.ai.PaperVanillaGoal<?> vanillaGoal;
 +    public <T extends org.bukkit.entity.Mob> com.destroystokyo.paper.entity.ai.Goal<T> asPaperVanillaGoal() {
-+        if(this.vanillaGoal == null) {
++        if (this.vanillaGoal == null) {
 +            this.vanillaGoal = new com.destroystokyo.paper.entity.ai.PaperVanillaGoal<>(this);
 +        }
 +        //noinspection unchecked
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java.patch
new file mode 100644
index 0000000000..16ce65055d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java.patch
@@ -0,0 +1,42 @@
+--- a/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java
++++ b/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java
+@@ -104,6 +_,11 @@
+             }
+ 
+             if (this.ticksSinceReachedGoal > 60) {
++                // CraftBukkit start - Step on eggs
++                if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityInteractEvent(this.removerMob, org.bukkit.craftbukkit.block.CraftBlock.at(level, posWithBlock))) {
++                    return;
++                }
++                // CraftBukkit end
+                 level.removeBlock(posWithBlock, false);
+                 if (!level.isClientSide) {
+                     for (int i = 0; i < 20; i++) {
+@@ -124,13 +_,16 @@
+ 
+     @Nullable
+     private BlockPos getPosWithBlock(BlockPos pos, BlockGetter level) {
+-        if (level.getBlockState(pos).is(this.blockToRemove)) {
++        net.minecraft.world.level.block.state.BlockState block = level.getBlockStateIfLoaded(pos); // Paper - Prevent AI rules from loading chunks
++        if (block == null) return null; // Paper - Prevent AI rules from loading chunks
++        if (block.is(this.blockToRemove)) { // Paper - Prevent AI rules from loading chunks
+             return pos;
+         } else {
+             BlockPos[] blockPoss = new BlockPos[]{pos.below(), pos.west(), pos.east(), pos.north(), pos.south(), pos.below().below()};
+ 
+             for (BlockPos blockPos : blockPoss) {
+-                if (level.getBlockState(blockPos).is(this.blockToRemove)) {
++                net.minecraft.world.level.block.state.BlockState block2 = level.getBlockStateIfLoaded(blockPos); // Paper - Prevent AI rules from loading chunks
++                if (block2 != null && block2.is(this.blockToRemove)) { // Paper - Prevent AI rules from loading chunks
+                     return blockPos;
+                 }
+             }
+@@ -141,7 +_,7 @@
+ 
+     @Override
+     protected boolean isValidTarget(LevelReader level, BlockPos pos) {
+-        ChunkAccess chunk = level.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ()), ChunkStatus.FULL, false);
++        ChunkAccess chunk = level.getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4); // Paper - Prevent AI rules from loading chunks
+         return chunk != null
+             && chunk.getBlockState(pos).is(this.blockToRemove)
+             && chunk.getBlockState(pos.above()).isAir()
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/RunAroundLikeCrazyGoal.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/RunAroundLikeCrazyGoal.java.patch
new file mode 100644
index 0000000000..bab3ed29fe
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/RunAroundLikeCrazyGoal.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/ai/goal/RunAroundLikeCrazyGoal.java
++++ b/net/minecraft/world/entity/ai/goal/RunAroundLikeCrazyGoal.java
+@@ -58,7 +_,7 @@
+             if (firstPassenger instanceof Player player) {
+                 int temper = this.horse.getTemper();
+                 int maxTemper = this.horse.getMaxTemper();
+-                if (maxTemper > 0 && this.horse.getRandom().nextInt(maxTemper) < temper) {
++                if (maxTemper > 0 && this.horse.getRandom().nextInt(maxTemper) < temper && !org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTameEvent(this.horse, ((org.bukkit.craftbukkit.entity.CraftHumanEntity) this.horse.getBukkitEntity().getPassenger()).getHandle()).isCancelled()) { // CraftBukkit - fire EntityTameEvent
+                     this.horse.tameWithName(player);
+                     return;
+                 }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/SitWhenOrderedToGoal.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/SitWhenOrderedToGoal.java.patch
similarity index 96%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/SitWhenOrderedToGoal.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/ai/goal/SitWhenOrderedToGoal.java.patch
index 64830b4a5b..d467f6a9a1 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/SitWhenOrderedToGoal.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/SitWhenOrderedToGoal.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/entity/ai/goal/SitWhenOrderedToGoal.java
 +++ b/net/minecraft/world/entity/ai/goal/SitWhenOrderedToGoal.java
-@@ -22,7 +22,7 @@
+@@ -20,7 +_,7 @@
      @Override
      public boolean canUse() {
          if (!this.mob.isTame()) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/SwellGoal.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/SwellGoal.java.patch
similarity index 60%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/SwellGoal.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/ai/goal/SwellGoal.java.patch
index 885e0fd98c..acde7d31dd 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/SwellGoal.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/SwellGoal.java.patch
@@ -1,17 +1,17 @@
 --- a/net/minecraft/world/entity/ai/goal/SwellGoal.java
 +++ b/net/minecraft/world/entity/ai/goal/SwellGoal.java
-@@ -21,7 +21,14 @@
-         return this.creeper.getSwellDir() > 0 || livingEntity != null && this.creeper.distanceToSqr(livingEntity) < 9.0;
+@@ -21,6 +_,14 @@
+         return this.creeper.getSwellDir() > 0 || target != null && this.creeper.distanceToSqr(target) < 9.0;
      }
  
 +    // Paper start - Fix MC-179072
-     @Override
++    @Override
 +    public boolean canContinueToUse() {
-+        return !net.minecraft.world.entity.EntitySelector.NO_CREATIVE_OR_SPECTATOR.test(this.creeper.getTarget()) && canUse();
++        return !net.minecraft.world.entity.EntitySelector.NO_CREATIVE_OR_SPECTATOR.test(this.creeper.getTarget()) && this.canUse();
 +    }
 +    // Paper end
 +
-+    @Override
++
+     @Override
      public void start() {
          this.creeper.getNavigation().stop();
-         this.target = this.creeper.getTarget();
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/TemptGoal.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/TemptGoal.java.patch
new file mode 100644
index 0000000000..96b15bc9f4
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/TemptGoal.java.patch
@@ -0,0 +1,27 @@
+--- a/net/minecraft/world/entity/ai/goal/TemptGoal.java
++++ b/net/minecraft/world/entity/ai/goal/TemptGoal.java
+@@ -21,7 +_,7 @@
+     private double pRotX;
+     private double pRotY;
+     @Nullable
+-    protected Player player;
++    protected LivingEntity player; // CraftBukkit
+     private int calmDown;
+     private boolean isRunning;
+     private final Predicate<ItemStack> items;
+@@ -44,6 +_,15 @@
+         } else {
+             this.player = getServerLevel(this.mob)
+                 .getNearestPlayer(this.targetingConditions.range(this.mob.getAttributeValue(Attributes.TEMPT_RANGE)), this.mob);
++            // CraftBukkit start
++            if (this.player != null) {
++                org.bukkit.event.entity.EntityTargetLivingEntityEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTargetLivingEvent(this.mob, this.player, org.bukkit.event.entity.EntityTargetEvent.TargetReason.TEMPT);
++                if (event.isCancelled()) {
++                    return false;
++                }
++                this.player = (event.getTarget() == null) ? null : ((org.bukkit.craftbukkit.entity.CraftLivingEntity) event.getTarget()).getHandle();
++            }
++            // CraftBukkit end
+             return this.player != null;
+         }
+     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/DefendVillageTargetGoal.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/target/DefendVillageTargetGoal.java.patch
similarity index 96%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/DefendVillageTargetGoal.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/ai/goal/target/DefendVillageTargetGoal.java.patch
index 3a2b688e7b..8144be0e41 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/DefendVillageTargetGoal.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/target/DefendVillageTargetGoal.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/entity/ai/goal/target/DefendVillageTargetGoal.java
 +++ b/net/minecraft/world/entity/ai/goal/target/DefendVillageTargetGoal.java
-@@ -61,7 +61,7 @@
+@@ -48,7 +_,7 @@
  
      @Override
      public void start() {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/HurtByTargetGoal.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/target/HurtByTargetGoal.java.patch
similarity index 95%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/HurtByTargetGoal.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/ai/goal/target/HurtByTargetGoal.java.patch
index 3b4092a6f3..1fd9b780b6 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/HurtByTargetGoal.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/target/HurtByTargetGoal.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/entity/ai/goal/target/HurtByTargetGoal.java
 +++ b/net/minecraft/world/entity/ai/goal/target/HurtByTargetGoal.java
-@@ -67,7 +67,7 @@
+@@ -59,7 +_,7 @@
  
      @Override
      public void start() {
@@ -9,7 +9,7 @@
          this.targetMob = this.mob.getTarget();
          this.timestamp = this.mob.getLastHurtByMobTimestamp();
          this.unseenMemoryTicks = 300;
-@@ -114,6 +114,6 @@
+@@ -114,6 +_,6 @@
      }
  
      protected void alertOther(Mob mob, LivingEntity target) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/NearestAttackableTargetGoal.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/target/NearestAttackableTargetGoal.java.patch
similarity index 96%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/NearestAttackableTargetGoal.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/ai/goal/target/NearestAttackableTargetGoal.java.patch
index 5d304f4ac8..a6474a3a6b 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/NearestAttackableTargetGoal.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/target/NearestAttackableTargetGoal.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/entity/ai/goal/target/NearestAttackableTargetGoal.java
 +++ b/net/minecraft/world/entity/ai/goal/target/NearestAttackableTargetGoal.java
-@@ -70,7 +70,7 @@
+@@ -73,7 +_,7 @@
  
      @Override
      public void start() {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/OwnerHurtByTargetGoal.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/target/OwnerHurtByTargetGoal.java.patch
similarity index 70%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/OwnerHurtByTargetGoal.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/ai/goal/target/OwnerHurtByTargetGoal.java.patch
index edaee8e182..e411caa1e3 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/OwnerHurtByTargetGoal.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/target/OwnerHurtByTargetGoal.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/world/entity/ai/goal/target/OwnerHurtByTargetGoal.java
 +++ b/net/minecraft/world/entity/ai/goal/target/OwnerHurtByTargetGoal.java
-@@ -38,7 +38,7 @@
+@@ -37,7 +_,7 @@
  
      @Override
      public void start() {
 -        this.mob.setTarget(this.ownerLastHurtBy);
 +        this.mob.setTarget(this.ownerLastHurtBy, org.bukkit.event.entity.EntityTargetEvent.TargetReason.TARGET_ATTACKED_OWNER, true); // CraftBukkit - reason
-         LivingEntity entityliving = this.tameAnimal.getOwner();
- 
-         if (entityliving != null) {
+         LivingEntity owner = this.tameAnimal.getOwner();
+         if (owner != null) {
+             this.timestamp = owner.getLastHurtByMobTimestamp();
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/OwnerHurtTargetGoal.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/target/OwnerHurtTargetGoal.java.patch
similarity index 70%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/OwnerHurtTargetGoal.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/ai/goal/target/OwnerHurtTargetGoal.java.patch
index 696e8ea9fa..d7274fc9a1 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/OwnerHurtTargetGoal.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/target/OwnerHurtTargetGoal.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/world/entity/ai/goal/target/OwnerHurtTargetGoal.java
 +++ b/net/minecraft/world/entity/ai/goal/target/OwnerHurtTargetGoal.java
-@@ -38,7 +38,7 @@
+@@ -37,7 +_,7 @@
  
      @Override
      public void start() {
 -        this.mob.setTarget(this.ownerLastHurt);
 +        this.mob.setTarget(this.ownerLastHurt, org.bukkit.event.entity.EntityTargetEvent.TargetReason.OWNER_ATTACKED_TARGET, true); // CraftBukkit - reason
-         LivingEntity entityliving = this.tameAnimal.getOwner();
- 
-         if (entityliving != null) {
+         LivingEntity owner = this.tameAnimal.getOwner();
+         if (owner != null) {
+             this.timestamp = owner.getLastHurtMobTimestamp();
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/target/TargetGoal.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/target/TargetGoal.java.patch
new file mode 100644
index 0000000000..68b320d4df
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/goal/target/TargetGoal.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/entity/ai/goal/target/TargetGoal.java
++++ b/net/minecraft/world/entity/ai/goal/target/TargetGoal.java
+@@ -63,7 +_,7 @@
+                         }
+                     }
+ 
+-                    this.mob.setTarget(target);
++                    this.mob.setTarget(target, org.bukkit.event.entity.EntityTargetEvent.TargetReason.CLOSEST_ENTITY, true); // CraftBukkit
+                     return true;
+                 }
+             }
+@@ -83,7 +_,7 @@
+ 
+     @Override
+     public void stop() {
+-        this.mob.setTarget(null);
++        this.mob.setTarget(null, org.bukkit.event.entity.EntityTargetEvent.TargetReason.FORGOT_TARGET, true); // CraftBukkit
+         this.targetMob = null;
+     }
+ 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/gossip/GossipContainer.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/gossip/GossipContainer.java.patch
similarity index 96%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/ai/gossip/GossipContainer.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/ai/gossip/GossipContainer.java.patch
index 55d0a4951d..1a74963cdf 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/gossip/GossipContainer.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/gossip/GossipContainer.java.patch
@@ -1,12 +1,13 @@
 --- a/net/minecraft/world/entity/ai/gossip/GossipContainer.java
 +++ b/net/minecraft/world/entity/ai/gossip/GossipContainer.java
-@@ -216,6 +216,43 @@
+@@ -216,6 +_,44 @@
          public void remove(GossipType gossipType) {
              this.entries.removeInt(gossipType);
          }
 +
 +        // Paper start - Add villager reputation API
 +        private static final GossipType[] TYPES = GossipType.values();
++
 +        public com.destroystokyo.paper.entity.villager.Reputation getPaperReputation() {
 +            Map<com.destroystokyo.paper.entity.villager.ReputationType, Integer> map = new java.util.EnumMap<>(com.destroystokyo.paper.entity.villager.ReputationType.class);
 +            for (Object2IntMap.Entry<GossipType> type : this.entries.object2IntEntrySet()) {
@@ -43,4 +44,4 @@
 +        // Paper end - Add villager reputation API
      }
  
-     static record GossipEntry(UUID target, GossipType type, int value) {
+     record GossipEntry(UUID target, GossipType type, int value) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/navigation/FlyingPathNavigation.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/navigation/FlyingPathNavigation.java.patch
similarity index 57%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/ai/navigation/FlyingPathNavigation.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/ai/navigation/FlyingPathNavigation.java.patch
index ac1686a6c3..92e7c74467 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/navigation/FlyingPathNavigation.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/navigation/FlyingPathNavigation.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/world/entity/ai/navigation/FlyingPathNavigation.java
 +++ b/net/minecraft/world/entity/ai/navigation/FlyingPathNavigation.java
-@@ -39,7 +39,7 @@
+@@ -39,7 +_,7 @@
  
      @Override
-     public Path createPath(Entity entity, int distance) {
--        return this.createPath(entity.blockPosition(), distance);
-+        return this.createPath(entity.blockPosition(), entity, distance); // Paper - EntityPathfindEvent
+     public Path createPath(Entity entity, int i) {
+-        return this.createPath(entity.blockPosition(), i);
++        return this.createPath(entity.blockPosition(), entity, i); // Paper - EntityPathfindEvent
      }
  
      @Override
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java.patch
new file mode 100644
index 0000000000..1880055015
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java.patch
@@ -0,0 +1,46 @@
+--- a/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java
++++ b/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java
+@@ -41,7 +_,7 @@
+     }
+ 
+     @Override
+-    public Path createPath(BlockPos pos, int accuracy) {
++    public Path createPath(BlockPos pos, @javax.annotation.Nullable Entity entity, int accuracy) { // Paper - EntityPathfindEvent
+         LevelChunk chunkNow = this.level.getChunkSource().getChunkNow(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ()));
+         if (chunkNow == null) {
+             return null;
+@@ -54,7 +_,7 @@
+                 }
+ 
+                 if (mutableBlockPos.getY() > this.level.getMinY()) {
+-                    return super.createPath(mutableBlockPos.above(), accuracy);
++                    return super.createPath(mutableBlockPos.above(), entity, accuracy); // Paper - EntityPathfindEvent
+                 }
+ 
+                 mutableBlockPos.setY(pos.getY() + 1);
+@@ -67,7 +_,7 @@
+             }
+ 
+             if (!chunkNow.getBlockState(pos).isSolid()) {
+-                return super.createPath(pos, accuracy);
++                return super.createPath(pos, entity, accuracy); // Paper - EntityPathfindEvent
+             } else {
+                 BlockPos.MutableBlockPos mutableBlockPos = pos.mutable().move(Direction.UP);
+ 
+@@ -75,14 +_,14 @@
+                     mutableBlockPos.move(Direction.UP);
+                 }
+ 
+-                return super.createPath(mutableBlockPos.immutable(), accuracy);
++                return super.createPath(mutableBlockPos.immutable(), entity, accuracy); // Paper - EntityPathfindEvent
+             }
+         }
+     }
+ 
+     @Override
+     public Path createPath(Entity entity, int i) {
+-        return this.createPath(entity.blockPosition(), i);
++        return this.createPath(entity.blockPosition(), entity, i); // Paper - EntityPathfindEvent
+     }
+ 
+     private int getSurfaceY() {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/navigation/PathNavigation.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/navigation/PathNavigation.java.patch
similarity index 55%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/ai/navigation/PathNavigation.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/ai/navigation/PathNavigation.java.patch
index 124ed48519..5cc08f5dc8 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/navigation/PathNavigation.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/navigation/PathNavigation.java.patch
@@ -1,64 +1,64 @@
 --- a/net/minecraft/world/entity/ai/navigation/PathNavigation.java
 +++ b/net/minecraft/world/entity/ai/navigation/PathNavigation.java
-@@ -125,8 +125,14 @@
+@@ -125,7 +_,13 @@
  
      @Nullable
-     public Path createPath(BlockPos target, int distance) {
--        return this.createPath(ImmutableSet.of(target), 8, false, distance);
+     public Path createPath(BlockPos pos, int accuracy) {
+-        return this.createPath(ImmutableSet.of(pos), 8, false, accuracy);
 +        // Paper start - EntityPathfindEvent
-+        return this.createPath(target, null, distance);
-     }
-+    @Nullable
-+    public Path createPath(BlockPos target, @Nullable Entity entity, int distance) {
-+        return this.createPath(ImmutableSet.of(target), entity, 8, false, distance);
-+        // Paper end - EntityPathfindEvent
++        return this.createPath(pos, null, accuracy);
 +    }
- 
-     @Nullable
-     public Path createPath(BlockPos target, int minDistance, int maxDistance) {
-@@ -135,7 +141,7 @@
- 
-     @Nullable
-     public Path createPath(Entity entity, int distance) {
--        return this.createPath(ImmutableSet.of(entity.blockPosition()), 16, true, distance);
-+        return this.createPath(ImmutableSet.of(entity.blockPosition()), entity, 16, true, distance); // Paper - EntityPathfindEvent
++    @Nullable
++    public Path createPath(BlockPos target, @Nullable Entity entity, int accuracy) {
++        return this.createPath(ImmutableSet.of(target), entity, 8, false, accuracy);
++        // Paper end - EntityPathfindEvent
      }
  
      @Nullable
-@@ -145,6 +151,17 @@
+@@ -135,7 +_,7 @@
  
      @Nullable
-     protected Path createPath(Set<BlockPos> positions, int range, boolean useHeadPos, int distance, float followRange) {
+     public Path createPath(Entity entity, int accuracy) {
+-        return this.createPath(ImmutableSet.of(entity.blockPosition()), 16, true, accuracy);
++        return this.createPath(ImmutableSet.of(entity.blockPosition()), entity, 16, true, accuracy); // Paper - EntityPathfindEvent
+     }
+ 
+     @Nullable
+@@ -145,6 +_,18 @@
+ 
+     @Nullable
+     protected Path createPath(Set<BlockPos> targets, int regionOffset, boolean offsetUpward, int accuracy, float followRange) {
 +        // Paper start - EntityPathfindEvent
-+        return this.createPath(positions, null, range, useHeadPos, distance, followRange);
++        return this.createPath(targets, null, regionOffset, offsetUpward, accuracy, followRange);
 +    }
 +
 +    @Nullable
-+    protected Path createPath(Set<BlockPos> positions, @Nullable Entity target, int range, boolean useHeadPos, int distance) {
-+        return this.createPath(positions, target, range, useHeadPos, distance, (float) this.mob.getAttributeValue(Attributes.FOLLOW_RANGE));
++    protected Path createPath(Set<BlockPos> targets, @Nullable Entity target, int regionOffset, boolean offsetUpward, int accuracy) {
++        return this.createPath(targets, target, regionOffset, offsetUpward, accuracy, (float) this.mob.getAttributeValue(Attributes.FOLLOW_RANGE));
 +    }
 +
-+    @Nullable protected Path createPath(Set<BlockPos> positions, @Nullable Entity target, int range, boolean useHeadPos, int distance, float followRange) {
++    @Nullable
++    protected Path createPath(Set<BlockPos> targets, @Nullable Entity target, int regionOffset, boolean offsetUpward, int accuracy, float followRange) {
 +        // Paper end - EntityPathfindEvent
-         if (positions.isEmpty()) {
+         if (targets.isEmpty()) {
              return null;
-         } else if (this.mob.getY() < (double)this.level.getMinY()) {
-@@ -154,6 +171,23 @@
-         } else if (this.path != null && !this.path.isDone() && positions.contains(this.targetPos)) {
+         } else if (this.mob.getY() < this.level.getMinY()) {
+@@ -154,6 +_,23 @@
+         } else if (this.path != null && !this.path.isDone() && targets.contains(this.targetPos)) {
              return this.path;
          } else {
 +            // Paper start - EntityPathfindEvent
 +            boolean copiedSet = false;
-+            for (BlockPos possibleTarget : positions) {
-+                if (!this.mob.getCommandSenderWorld().getWorldBorder().isWithinBounds(possibleTarget) || !new com.destroystokyo.paper.event.entity.EntityPathfindEvent(this.mob.getBukkitEntity(), // Paper - don't path out of world border
++            for (BlockPos possibleTarget : targets) {
++                if (!this.mob.level().getWorldBorder().isWithinBounds(possibleTarget) || !new com.destroystokyo.paper.event.entity.EntityPathfindEvent(this.mob.getBukkitEntity(), // Paper - don't path out of world border
 +                    io.papermc.paper.util.MCUtil.toLocation(this.mob.level(), possibleTarget), target == null ? null : target.getBukkitEntity()).callEvent()) {
 +                    if (!copiedSet) {
 +                        copiedSet = true;
-+                        positions = new java.util.HashSet<>(positions);
++                        targets = new java.util.HashSet<>(targets);
 +                    }
 +                    // note: since we copy the set this remove call is safe, since we're iterating over the old copy
-+                    positions.remove(possibleTarget);
-+                    if (positions.isEmpty()) {
++                    targets.remove(possibleTarget);
++                    if (targets.isEmpty()) {
 +                        return null;
 +                    }
 +                }
@@ -66,9 +66,9 @@
 +            // Paper end - EntityPathfindEvent
              ProfilerFiller profilerFiller = Profiler.get();
              profilerFiller.push("pathfind");
-             BlockPos blockPos = useHeadPos ? this.mob.blockPosition().above() : this.mob.blockPosition();
-@@ -175,13 +209,33 @@
-         return this.moveTo(this.createPath(x, y, z, 1), speed);
+             BlockPos blockPos = offsetUpward ? this.mob.blockPosition().above() : this.mob.blockPosition();
+@@ -171,6 +_,11 @@
+         }
      }
  
 +    // Paper start - Perf: Optimise pathfinding
@@ -76,8 +76,10 @@
 +    private int pathfindFailures = 0;
 +    // Paper end - Perf: Optimise pathfinding
 +
-     public boolean moveTo(double x, double y, double z, int distance, double speed) {
-         return this.moveTo(this.createPath(x, y, z, distance), speed);
+     public boolean moveTo(double x, double y, double z, double speed) {
+         return this.moveTo(this.createPath(x, y, z, 1), speed);
+     }
+@@ -180,8 +_,23 @@
      }
  
      public boolean moveTo(Entity entity, double speed) {
@@ -101,4 +103,4 @@
 +        // Paper end - Perf: Optimise pathfinding
      }
  
-     public boolean moveTo(@Nullable Path path, double speed) {
+     public boolean moveTo(@Nullable Path pathentity, double speed) {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/navigation/WallClimberNavigation.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/navigation/WallClimberNavigation.java.patch
new file mode 100644
index 0000000000..ca88f3f478
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/navigation/WallClimberNavigation.java.patch
@@ -0,0 +1,14 @@
+--- a/net/minecraft/world/entity/ai/navigation/WallClimberNavigation.java
++++ b/net/minecraft/world/entity/ai/navigation/WallClimberNavigation.java
+@@ -16,9 +_,9 @@
+     }
+ 
+     @Override
+-    public Path createPath(BlockPos pos, int accuracy) {
++    public Path createPath(BlockPos pos, @Nullable Entity entity, int accuracy) {
+         this.pathToPosition = pos;
+-        return super.createPath(pos, accuracy);
++        return super.createPath(pos, entity, accuracy); // Paper - EntityPathfindEvent
+     }
+ 
+     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/sensing/Sensor.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/sensing/Sensor.java.patch
similarity index 57%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/ai/sensing/Sensor.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/ai/sensing/Sensor.java.patch
index 7d220911a0..c74a750263 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/sensing/Sensor.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/sensing/Sensor.java.patch
@@ -1,14 +1,12 @@
 --- a/net/minecraft/world/entity/ai/sensing/Sensor.java
 +++ b/net/minecraft/world/entity/ai/sensing/Sensor.java
-@@ -29,8 +29,19 @@
+@@ -29,8 +_,17 @@
          .ignoreInvisibilityTesting();
      private final int scanRate;
      private long timeToTick;
-+    // Paper start - configurable sensor tick rate and timings
-+    private final String configKey;
-+    // Paper end
++    private final String configKey; // Paper - configurable sensor tick rate and timings
  
-     public Sensor(int senseInterval) {
+     public Sensor(int scanRate) {
 +        // Paper start - configurable sensor tick rate and timings
 +        String key = io.papermc.paper.util.MappingEnvironment.reobf() ? io.papermc.paper.util.ObfHelper.INSTANCE.deobfClassName(this.getClass().getName()) : this.getClass().getName();
 +        int lastSeparator = key.lastIndexOf('.');
@@ -17,18 +15,15 @@
 +        }
 +        this.configKey = key.toLowerCase(java.util.Locale.ROOT);
 +        // Paper end
-         this.scanRate = senseInterval;
-         this.timeToTick = (long)RANDOM.nextInt(senseInterval);
+         this.scanRate = scanRate;
+         this.timeToTick = RANDOM.nextInt(scanRate);
      }
-@@ -41,8 +52,10 @@
+@@ -41,7 +_,7 @@
  
-     public final void tick(ServerLevel world, E entity) {
+     public final void tick(ServerLevel level, E entity) {
          if (--this.timeToTick <= 0L) {
--            this.timeToTick = (long)this.scanRate;
-+            // Paper start - configurable sensor tick rate and timings
-+            this.timeToTick = java.util.Objects.requireNonNullElse(world.paperConfig().tickRates.sensor.get(entity.getType(), this.configKey), this.scanRate);
+-            this.timeToTick = this.scanRate;
++            this.timeToTick = java.util.Objects.requireNonNullElse(level.paperConfig().tickRates.sensor.get(entity.getType(), this.configKey), this.scanRate); // Paper - configurable sensor tick rate and timings
              this.updateTargetingConditionRanges(entity);
-+            // Paper end
-             this.doTick(world, entity);
+             this.doTick(level, entity);
          }
-     }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/sensing/TemptingSensor.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/sensing/TemptingSensor.java.patch
new file mode 100644
index 0000000000..7ec237343c
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/sensing/TemptingSensor.java.patch
@@ -0,0 +1,36 @@
+--- a/net/minecraft/world/entity/ai/sensing/TemptingSensor.java
++++ b/net/minecraft/world/entity/ai/sensing/TemptingSensor.java
+@@ -16,6 +_,14 @@
+ import net.minecraft.world.entity.player.Player;
+ import net.minecraft.world.item.ItemStack;
+ 
++// CraftBukkit start
++import org.bukkit.craftbukkit.entity.CraftHumanEntity;
++import org.bukkit.craftbukkit.event.CraftEventFactory;
++import org.bukkit.entity.HumanEntity;
++import org.bukkit.event.entity.EntityTargetEvent;
++import org.bukkit.event.entity.EntityTargetLivingEntityEvent;
++// CraftBukkit end
++
+ public class TemptingSensor extends Sensor<PathfinderMob> {
+     private static final TargetingConditions TEMPT_TARGETING = TargetingConditions.forNonCombat().ignoreLineOfSight();
+     private final Predicate<ItemStack> temptations;
+@@ -38,7 +_,17 @@
+             .collect(Collectors.toList());
+         if (!list.isEmpty()) {
+             Player player = list.get(0);
+-            brain.setMemory(MemoryModuleType.TEMPTING_PLAYER, player);
++            // CraftBukkit start
++            EntityTargetLivingEntityEvent event = CraftEventFactory.callEntityTargetLivingEvent(entity, player, EntityTargetEvent.TargetReason.TEMPT);
++            if (event.isCancelled()) {
++                return;
++            }
++            if (event.getTarget() instanceof HumanEntity) {
++                brain.setMemory(MemoryModuleType.TEMPTING_PLAYER, ((CraftHumanEntity) event.getTarget()).getHandle());
++            } else {
++                brain.eraseMemory(MemoryModuleType.TEMPTING_PLAYER);
++            }
++            // CraftBukkit end
+         } else {
+             brain.eraseMemory(MemoryModuleType.TEMPTING_PLAYER);
+         }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ai/village/VillageSiege.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ai/village/VillageSiege.java.patch
new file mode 100644
index 0000000000..983754df6f
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ai/village/VillageSiege.java.patch
@@ -0,0 +1,16 @@
+--- a/net/minecraft/world/entity/ai/village/VillageSiege.java
++++ b/net/minecraft/world/entity/ai/village/VillageSiege.java
+@@ -101,11 +_,12 @@
+                 zombie.finalizeSpawn(level, level.getCurrentDifficultyAt(zombie.blockPosition()), EntitySpawnReason.EVENT, null);
+             } catch (Exception var5) {
+                 LOGGER.warn("Failed to create zombie for village siege at {}", vec3, var5);
++                com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(var5); // Paper - ServerExceptionEvent
+                 return;
+             }
+ 
+             zombie.moveTo(vec3.x, vec3.y, vec3.z, level.random.nextFloat() * 360.0F, 0.0F);
+-            level.addFreshEntityWithPassengers(zombie);
++            level.addFreshEntityWithPassengers(zombie, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.VILLAGE_INVASION); // CraftBukkit
+         }
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/ambient/Bat.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/ambient/Bat.java.patch
new file mode 100644
index 0000000000..2b20441f48
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/ambient/Bat.java.patch
@@ -0,0 +1,45 @@
+--- a/net/minecraft/world/entity/ambient/Bat.java
++++ b/net/minecraft/world/entity/ambient/Bat.java
+@@ -85,7 +_,7 @@
+     }
+ 
+     @Override
+-    public boolean isPushable() {
++    public boolean isCollidable(boolean ignoreClimbing) { // Paper - Climbing should not bypass cramming gamerule
+         return false;
+     }
+ 
+@@ -139,13 +_,13 @@
+                     this.yHeadRot = this.random.nextInt(360);
+                 }
+ 
+-                if (level.getNearestPlayer(BAT_RESTING_TARGETING, this) != null) {
++                if (level.getNearestPlayer(BAT_RESTING_TARGETING, this) != null && org.bukkit.craftbukkit.event.CraftEventFactory.handleBatToggleSleepEvent(this, true)) { // CraftBukkit - Call BatToggleSleepEvent
+                     this.setResting(false);
+                     if (!isSilent) {
+                         level.levelEvent(null, 1025, blockPos, 0);
+                     }
+                 }
+-            } else {
++            } else if (org.bukkit.craftbukkit.event.CraftEventFactory.handleBatToggleSleepEvent(this, true)) { // CraftBukkit - Call BatToggleSleepEvent
+                 this.setResting(false);
+                 if (!isSilent) {
+                     level.levelEvent(null, 1025, blockPos, 0);
+@@ -178,7 +_,7 @@
+             float f1 = Mth.wrapDegrees(f - this.getYRot());
+             this.zza = 0.5F;
+             this.setYRot(this.getYRot() + f1);
+-            if (this.random.nextInt(100) == 0 && level.getBlockState(blockPos1).isRedstoneConductor(level, blockPos1)) {
++            if (this.random.nextInt(100) == 0 && level.getBlockState(blockPos1).isRedstoneConductor(level, blockPos1) && org.bukkit.craftbukkit.event.CraftEventFactory.handleBatToggleSleepEvent(this, false)) { // CraftBukkit - Call BatToggleSleepEvent
+                 this.setResting(true);
+             }
+         }
+@@ -203,7 +_,7 @@
+         if (this.isInvulnerableTo(level, damageSource)) {
+             return false;
+         } else {
+-            if (this.isResting()) {
++            if (this.isResting() && org.bukkit.craftbukkit.event.CraftEventFactory.handleBatToggleSleepEvent(this, true)) { // CraftBukkit - Call BatToggleSleepEvent
+                 this.setResting(false);
+             }
+ 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/AbstractSchoolingFish.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/AbstractSchoolingFish.java.patch
similarity index 59%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/animal/AbstractSchoolingFish.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/animal/AbstractSchoolingFish.java.patch
index 69bd0b2aef..94cfc94803 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/AbstractSchoolingFish.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/AbstractSchoolingFish.java.patch
@@ -1,10 +1,10 @@
 --- a/net/minecraft/world/entity/animal/AbstractSchoolingFish.java
 +++ b/net/minecraft/world/entity/animal/AbstractSchoolingFish.java
-@@ -51,6 +51,7 @@
+@@ -51,6 +_,7 @@
      }
  
      public void stopFollowing() {
-+        if (this.leader == null) return; // Avoid NPE, plugins can now set the leader and certain fish goals might cause this method to be called
++        if (this.leader == null) return; // Paper - Avoid NPE, plugins can now set the leader and certain fish goals might cause this method to be called
          this.leader.removeFollower();
          this.leader = null;
      }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/Animal.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/Animal.java.patch
new file mode 100644
index 0000000000..47de924357
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/Animal.java.patch
@@ -0,0 +1,116 @@
+--- a/net/minecraft/world/entity/animal/Animal.java
++++ b/net/minecraft/world/entity/animal/Animal.java
+@@ -39,6 +_,7 @@
+     public int inLove;
+     @Nullable
+     public UUID loveCause;
++    public ItemStack breedItem; // CraftBukkit - Add breedItem variable
+ 
+     protected Animal(EntityType<? extends Animal> entityType, Level level) {
+         super(entityType, level);
+@@ -78,9 +_,13 @@
+     }
+ 
+     @Override
+-    protected void actuallyHurt(ServerLevel level, DamageSource damageSource, float amount) {
++    // CraftBukkit start - void -> boolean
++    public boolean actuallyHurt(ServerLevel level, DamageSource damageSource, float amount, org.bukkit.event.entity.EntityDamageEvent event) {
++        boolean damageResult = super.actuallyHurt(level, damageSource, amount, event);
++        if (!damageResult) return false;
+         this.resetLove();
+-        super.actuallyHurt(level, damageSource, amount);
++        return true;
++        // CraftBukkit end
+     }
+ 
+     @Override
+@@ -138,8 +_,9 @@
+         if (this.isFood(itemInHand)) {
+             int age = this.getAge();
+             if (!this.level().isClientSide && age == 0 && this.canFallInLove()) {
++                final ItemStack breedCopy = itemInHand.copy(); // Paper - Fix EntityBreedEvent copying
+                 this.usePlayerItem(player, hand, itemInHand);
+-                this.setInLove(player);
++                this.setInLove(player, breedCopy); // Paper - Fix EntityBreedEvent copying
+                 this.playEatingSound();
+                 return InteractionResult.SUCCESS_SERVER;
+             }
+@@ -176,11 +_,26 @@
+         return this.inLove <= 0;
+     }
+ 
++    @Deprecated @io.papermc.paper.annotation.DoNotUse // Paper - Fix EntityBreedEvent copying
+     public void setInLove(@Nullable Player player) {
+-        this.inLove = 600;
++        // Paper start - Fix EntityBreedEvent copying
++        this.setInLove(player, null);
++    }
++    public void setInLove(@Nullable Player player, @Nullable ItemStack breedItemCopy) {
++        if (breedItemCopy != null) this.breedItem = breedItemCopy;
++        // Paper end - Fix EntityBreedEvent copying
++        // CraftBukkit start
++        org.bukkit.event.entity.EntityEnterLoveModeEvent entityEnterLoveModeEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityEnterLoveModeEvent(player, this, 600);
++        if (entityEnterLoveModeEvent.isCancelled()) {
++            this.breedItem = null; // Paper - Fix EntityBreedEvent copying; clear if cancelled
++            return;
++        }
++        this.inLove = entityEnterLoveModeEvent.getTicksInLove();
++        // CraftBukkit end
+         if (player != null) {
+             this.loveCause = player.getUUID();
+         }
++        // Paper - Fix EntityBreedEvent copying; set breed item in better place
+ 
+         this.level().broadcastEntityEvent(this, (byte)18);
+     }
+@@ -220,23 +_,45 @@
+         if (breedOffspring != null) {
+             breedOffspring.setBaby(true);
+             breedOffspring.moveTo(this.getX(), this.getY(), this.getZ(), 0.0F, 0.0F);
+-            this.finalizeSpawnChildFromBreeding(level, mate, breedOffspring);
+-            level.addFreshEntityWithPassengers(breedOffspring);
++            // CraftBukkit start - call EntityBreedEvent
++            ServerPlayer breeder = Optional.ofNullable(this.getLoveCause()).or(() -> Optional.ofNullable(mate.getLoveCause())).orElse(null);
++            int experience = this.getRandom().nextInt(7) + 1;
++            org.bukkit.event.entity.EntityBreedEvent entityBreedEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityBreedEvent(breedOffspring, this, mate, breeder, this.breedItem, experience);
++            if (entityBreedEvent.isCancelled()) {
++                return;
++            }
++            experience = entityBreedEvent.getExperience();
++            this.finalizeSpawnChildFromBreeding(level, mate, breedOffspring, experience);
++            level.addFreshEntityWithPassengers(breedOffspring, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.BREEDING);
++            // CraftBukkit end
+         }
+     }
+ 
+     public void finalizeSpawnChildFromBreeding(ServerLevel level, Animal animal, @Nullable AgeableMob baby) {
+-        Optional.ofNullable(this.getLoveCause()).or(() -> Optional.ofNullable(animal.getLoveCause())).ifPresent(player -> {
++        // CraftBukkit start
++        this.finalizeSpawnChildFromBreeding(level, animal, baby, this.getRandom().nextInt(7) + 1);
++    }
++    public void finalizeSpawnChildFromBreeding(ServerLevel level, Animal animal, @Nullable AgeableMob baby, int experience) {
++        // CraftBukkit end
++        // Paper start
++        ServerPlayer player = this.getLoveCause();
++        if (player == null) player = animal.getLoveCause();
++        if (player != null) {
++            // Paper end
+             player.awardStat(Stats.ANIMALS_BRED);
+             CriteriaTriggers.BRED_ANIMALS.trigger(player, this, animal, baby);
+-        });
++        } // Paper
+         this.setAge(6000);
+         animal.setAge(6000);
+         this.resetLove();
+         animal.resetLove();
+         level.broadcastEntityEvent(this, (byte)18);
+         if (level.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) {
+-            level.addFreshEntity(new ExperienceOrb(level, this.getX(), this.getY(), this.getZ(), this.getRandom().nextInt(7) + 1));
++            // CraftBukkit start - use event experience
++            if (experience > 0) {
++                level.addFreshEntity(new ExperienceOrb(level, this.getX(), this.getY(), this.getZ(), experience, org.bukkit.entity.ExperienceOrb.SpawnReason.BREED, player, baby)); // Paper
++            }
++            // CraftBukkit end
+         }
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/Bee.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/Bee.java.patch
new file mode 100644
index 0000000000..94a18bec62
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/Bee.java.patch
@@ -0,0 +1,147 @@
+--- a/net/minecraft/world/entity/animal/Bee.java
++++ b/net/minecraft/world/entity/animal/Bee.java
+@@ -141,10 +_,26 @@
+     Bee.BeeGoToHiveGoal goToHiveGoal;
+     private Bee.BeeGoToKnownFlowerGoal goToKnownFlowerGoal;
+     private int underWaterTicks;
++    public net.kyori.adventure.util.TriState rollingOverride = net.kyori.adventure.util.TriState.NOT_SET; // Paper - Rolling override
+ 
+     public Bee(EntityType<? extends Bee> entityType, Level level) {
+         super(entityType, level);
+-        this.moveControl = new FlyingMoveControl(this, 20, true);
++        // Paper start - Fix MC-167279
++        class BeeFlyingMoveControl extends FlyingMoveControl {
++            public BeeFlyingMoveControl(final Mob entity, final int maxPitchChange, final boolean noGravity) {
++                super(entity, maxPitchChange, noGravity);
++            }
++
++            @Override
++            public void tick() {
++                if (this.mob.getY() <= Bee.this.level().getMinY()) {
++                    this.mob.setNoGravity(false);
++                }
++                super.tick();
++            }
++        }
++        this.moveControl = new BeeFlyingMoveControl(this, 20, true);
++        // Paper end - Fix MC-167279
+         this.lookControl = new Bee.BeeLookControl(this);
+         this.setPathfindingMalus(PathType.DANGER_FIRE, -1.0F);
+         this.setPathfindingMalus(PathType.WATER, -1.0F);
+@@ -191,12 +_,19 @@
+ 
+     @Override
+     public void addAdditionalSaveData(CompoundTag compound) {
++        // CraftBukkit start - selectively save data
++        this.addAdditionalSaveData(compound, true);
++    }
++
++    @Override
++    public void addAdditionalSaveData(CompoundTag compound, boolean includeAll) {
++        // CraftBukkit end
+         super.addAdditionalSaveData(compound);
+-        if (this.hasHive()) {
++        if (includeAll && this.hasHive()) { // CraftBukkit - selectively save hive
+             compound.put("hive_pos", NbtUtils.writeBlockPos(this.getHivePos()));
+         }
+ 
+-        if (this.hasSavedFlowerPos()) {
++        if (includeAll && this.hasSavedFlowerPos()) { // CraftBukkit - selectively save hive
+             compound.put("flower_pos", NbtUtils.writeBlockPos(this.getSavedFlowerPos()));
+         }
+ 
+@@ -237,7 +_,7 @@
+                 }
+ 
+                 if (i > 0) {
+-                    livingEntity.addEffect(new MobEffectInstance(MobEffects.POISON, i * 20, 0), this);
++                    livingEntity.addEffect(new MobEffectInstance(MobEffects.POISON, i * 20, 0), this, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ATTACK); // CraftBukkit
+                 }
+             }
+ 
+@@ -492,7 +_,11 @@
+         if (this.hivePos == null) {
+             return null;
+         } else {
+-            return this.isTooFarAway(this.hivePos) ? null : this.level().getBlockEntity(this.hivePos, BlockEntityType.BEEHIVE).orElse(null);
++            // Paper start - move over logic to accommodate isTooFarAway with chunk load check
++            return this.isTooFarAway(this.hivePos) || this.level().getChunkIfLoadedImmediately(this.hivePos.getX() >> 4, this.hivePos.getZ() >> 4) == null
++                ? null
++                : this.level().getBlockEntity(this.hivePos, BlockEntityType.BEEHIVE).orElse(null);
++            // Paper end
+         }
+     }
+ 
+@@ -525,6 +_,7 @@
+     }
+ 
+     public void setRolling(boolean isRolling) {
++        isRolling = this.rollingOverride.toBooleanOrElse(isRolling); // Paper - Rolling override
+         this.setFlag(2, isRolling);
+     }
+ 
+@@ -581,7 +_,7 @@
+             if (beeInteractionEffect != null) {
+                 this.usePlayerItem(player, hand, itemInHand);
+                 if (!this.level().isClientSide) {
+-                    this.addEffect(beeInteractionEffect);
++                    this.addEffect(beeInteractionEffect, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.FOOD); // Paper - Add missing effect cause
+                 }
+ 
+                 return InteractionResult.SUCCESS;
+@@ -650,8 +_,9 @@
+         if (this.isInvulnerableTo(level, damageSource)) {
+             return false;
+         } else {
++            if (!super.hurtServer(level, damageSource, amount)) return false; // CraftBukkit - Only stop pollinating if entity was damaged
+             this.beePollinateGoal.stopPollinating();
+-            return super.hurtServer(level, damageSource, amount);
++            return true; // CraftBukkit - Only stop pollinating if entity was damaged
+         }
+     }
+ 
+@@ -772,7 +_,7 @@
+     @VisibleForDebug
+     public class BeeGoToHiveGoal extends Bee.BaseBeeGoal {
+         public static final int MAX_TRAVELLING_TICKS = 2400;
+-        int travellingTicks = Bee.this.level().random.nextInt(10);
++        int travellingTicks = Bee.this.random.nextInt(10); // CraftBukkit - SPIGOT-7495: Give Bees another chance and let them use their own random, avoid concurrency issues
+         private static final int MAX_BLACKLISTED_TARGETS = 3;
+         final List<BlockPos> blacklistedTargets = Lists.newArrayList();
+         @Nullable
+@@ -888,7 +_,7 @@
+ 
+     public class BeeGoToKnownFlowerGoal extends Bee.BaseBeeGoal {
+         private static final int MAX_TRAVELLING_TICKS = 2400;
+-        int travellingTicks = Bee.this.level().random.nextInt(10);
++        int travellingTicks = Bee.this.random.nextInt(10); // CraftBukkit - SPIGOT-7495: Give Bees another chance and let them use their own random, avoid concurrency issues
+ 
+         BeeGoToKnownFlowerGoal() {
+             this.setFlags(EnumSet.of(Goal.Flag.MOVE));
+@@ -986,7 +_,7 @@
+                             }
+                         }
+ 
+-                        if (blockState1 != null) {
++                        if (blockState1 != null && org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(Bee.this, blockPos, blockState1)) { // CraftBukkit
+                             Bee.this.level().levelEvent(2011, blockPos, 15);
+                             Bee.this.level().setBlockAndUpdate(blockPos, blockState1);
+                             Bee.this.incrementNumCropsGrownSincePollination();
+@@ -1010,7 +_,7 @@
+         @Override
+         protected void alertOther(Mob mob, LivingEntity target) {
+             if (mob instanceof Bee && this.mob.hasLineOfSight(target)) {
+-                mob.setTarget(target);
++                mob.setTarget(target, org.bukkit.event.entity.EntityTargetEvent.TargetReason.TARGET_ATTACKED_ENTITY, true); // CraftBukkit - reason
+             }
+         }
+     }
+@@ -1168,7 +_,7 @@
+                     Bee.this.dropFlower();
+                     this.pollinating = false;
+                     Bee.this.remainingCooldownBeforeLocatingNewFlower = 200;
+-                } else {
++                } else if (Bee.this.savedFlowerPos != null) { // Paper - add null check since API can manipulate this
+                     Vec3 vec3 = Vec3.atBottomCenterOf(Bee.this.savedFlowerPos).add(0.0, 0.6F, 0.0);
+                     if (vec3.distanceTo(Bee.this.position()) > 1.0) {
+                         this.hoverPos = vec3;
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/Bucketable.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/Bucketable.java.patch
new file mode 100644
index 0000000000..7d0041cf64
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/Bucketable.java.patch
@@ -0,0 +1,32 @@
+--- a/net/minecraft/world/entity/animal/Bucketable.java
++++ b/net/minecraft/world/entity/animal/Bucketable.java
+@@ -88,9 +_,19 @@
+     static <T extends LivingEntity & Bucketable> Optional<InteractionResult> bucketMobPickup(Player player, InteractionHand hand, T entity) {
+         ItemStack itemInHand = player.getItemInHand(hand);
+         if (itemInHand.getItem() == Items.WATER_BUCKET && entity.isAlive()) {
+-            entity.playSound(entity.getPickupSound(), 1.0F, 1.0F);
++            // CraftBukkit start
++            // entity.playSound(entity.getPickupSound(), 1.0F, 1.0F); // CraftBukkit - moved down
+             ItemStack bucketItemStack = entity.getBucketItemStack();
+             entity.saveToBucketTag(bucketItemStack);
++            org.bukkit.event.player.PlayerBucketEntityEvent playerBucketFishEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerFishBucketEvent(entity, player, itemInHand, bucketItemStack, hand);
++            bucketItemStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(playerBucketFishEvent.getEntityBucket());
++            if (playerBucketFishEvent.isCancelled()) {
++                player.containerMenu.sendAllDataToRemote(); // We need to update inventory to resync client's bucket
++                entity.resendPossiblyDesyncedEntityData((ServerPlayer) player); // Paper
++                return Optional.of(InteractionResult.FAIL);
++            }
++            entity.playSound(entity.getPickupSound(), 1.0F, 1.0F);
++            // CraftBukkit end
+             ItemStack itemStack = ItemUtils.createFilledResult(itemInHand, player, bucketItemStack, false);
+             player.setItemInHand(hand, itemStack);
+             Level level = entity.level();
+@@ -98,7 +_,7 @@
+                 CriteriaTriggers.FILLED_BUCKET.trigger((ServerPlayer)player, bucketItemStack);
+             }
+ 
+-            entity.discard();
++            entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
+             return Optional.of(InteractionResult.SUCCESS);
+         } else {
+             return Optional.empty();
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/Cat.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/Cat.java.patch
new file mode 100644
index 0000000000..f475fefe0a
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/Cat.java.patch
@@ -0,0 +1,75 @@
+--- a/net/minecraft/world/entity/animal/Cat.java
++++ b/net/minecraft/world/entity/animal/Cat.java
+@@ -342,7 +_,7 @@
+         TagKey<CatVariant> tagKey = flag ? CatVariantTags.FULL_MOON_SPAWNS : CatVariantTags.DEFAULT_SPAWNS;
+         BuiltInRegistries.CAT_VARIANT.getRandomElementOf(tagKey, level.getRandom()).ifPresent(this::setVariant);
+         ServerLevel level1 = level.getLevel();
+-        if (level1.structureManager().getStructureWithPieceAt(this.blockPosition(), StructureTags.CATS_SPAWN_AS_BLACK).isValid()) {
++        if (level1.structureManager().getStructureWithPieceAt(this.blockPosition(), StructureTags.CATS_SPAWN_AS_BLACK, level).isValid()) { // Paper - Fix swamp hut cat generation deadlock
+             this.setVariant(BuiltInRegistries.CAT_VARIANT.getOrThrow(CatVariant.ALL_BLACK));
+             this.setPersistenceRequired();
+         }
+@@ -359,6 +_,11 @@
+                 if (item instanceof DyeItem dyeItem) {
+                     DyeColor dyeColor = dyeItem.getDyeColor();
+                     if (dyeColor != this.getCollarColor()) {
++                        // Paper start - Add EntityDyeEvent and CollarColorable interface
++                        final io.papermc.paper.event.entity.EntityDyeEvent event = new io.papermc.paper.event.entity.EntityDyeEvent(this.getBukkitEntity(), org.bukkit.DyeColor.getByWoolData((byte) dyeColor.getId()), ((net.minecraft.server.level.ServerPlayer) player).getBukkitEntity());
++                        if (!event.callEvent()) return InteractionResult.FAIL;
++                        dyeColor = DyeColor.byId(event.getColor().getWoolData());
++                        // Paper end - Add EntityDyeEvent and CollarColorable interface
+                         if (!this.level().isClientSide()) {
+                             this.setCollarColor(dyeColor);
+                             itemInHand.consume(1, player);
+@@ -371,7 +_,7 @@
+                     if (!this.level().isClientSide()) {
+                         this.usePlayerItem(player, hand, itemInHand);
+                         FoodProperties foodProperties = itemInHand.get(DataComponents.FOOD);
+-                        this.heal(foodProperties != null ? foodProperties.nutrition() : 1.0F);
++                        this.heal(foodProperties != null ? foodProperties.nutrition() : 1.0F, org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.EATING); // Paper - Add missing regain reason
+                         this.playEatingSound();
+                     }
+ 
+@@ -433,7 +_,7 @@
+     }
+ 
+     private void tryToTame(Player player) {
+-        if (this.random.nextInt(3) == 0) {
++        if (this.random.nextInt(3) == 0 && !org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTameEvent(this, player).isCancelled()) { // CraftBukkit
+             this.tame(player);
+             this.setOrderedToSit(true);
+             this.level().broadcastEntityEvent(this, (byte)7);
+@@ -567,15 +_,20 @@
+                 .dropFromGiftLootTable(
+                     getServerLevel(this.cat),
+                     BuiltInLootTables.CAT_MORNING_GIFT,
+-                    (serverLevel, itemStack) -> serverLevel.addFreshEntity(
+-                        new ItemEntity(
++                    (serverLevel, itemStack) -> {
++                        // CraftBukkit start
++                        ItemEntity item = new ItemEntity(
+                             serverLevel,
+                             (double)mutableBlockPos.getX() - Mth.sin(this.cat.yBodyRot * (float) (Math.PI / 180.0)),
+                             mutableBlockPos.getY(),
+                             (double)mutableBlockPos.getZ() + Mth.cos(this.cat.yBodyRot * (float) (Math.PI / 180.0)),
+                             itemStack
+-                        )
+-                    )
++                        );
++                        org.bukkit.event.entity.EntityDropItemEvent event = new org.bukkit.event.entity.EntityDropItemEvent(this.cat.getBukkitEntity(), (org.bukkit.entity.Item) item.getBukkitEntity());
++                        if (!event.callEvent()) return;
++                        serverLevel.addFreshEntity(item);
++                        // CraftBukkit end
++                    }
+                 );
+         }
+ 
+@@ -602,7 +_,7 @@
+ 
+     static class CatTemptGoal extends TemptGoal {
+         @Nullable
+-        private Player selectedPlayer;
++        private LivingEntity selectedPlayer; // CraftBukkit
+         private final Cat cat;
+ 
+         public CatTemptGoal(Cat cat, double speedModifier, Predicate<ItemStack> items, boolean canScare) {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/Chicken.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/Chicken.java.patch
new file mode 100644
index 0000000000..a95bdb6673
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/Chicken.java.patch
@@ -0,0 +1,15 @@
+--- a/net/minecraft/world/entity/animal/Chicken.java
++++ b/net/minecraft/world/entity/animal/Chicken.java
+@@ -91,10 +_,12 @@
+ 
+         this.flap = this.flap + this.flapping * 2.0F;
+         if (this.level() instanceof ServerLevel serverLevel && this.isAlive() && !this.isBaby() && !this.isChickenJockey() && --this.eggTime <= 0) {
++            this.forceDrops = true; // CraftBukkit
+             if (this.dropFromGiftLootTable(serverLevel, BuiltInLootTables.CHICKEN_LAY, this::spawnAtLocation)) {
+                 this.playSound(SoundEvents.CHICKEN_EGG, 1.0F, (this.random.nextFloat() - this.random.nextFloat()) * 0.2F + 1.0F);
+                 this.gameEvent(GameEvent.ENTITY_PLACE);
+             }
++            this.forceDrops = false; // CraftBukkit
+ 
+             this.eggTime = this.random.nextInt(6000) + 6000;
+         }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/Cow.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/Cow.java.patch
new file mode 100644
index 0000000000..a5059e85d7
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/Cow.java.patch
@@ -0,0 +1,19 @@
+--- a/net/minecraft/world/entity/animal/Cow.java
++++ b/net/minecraft/world/entity/animal/Cow.java
+@@ -88,8 +_,15 @@
+     public InteractionResult mobInteract(Player player, InteractionHand hand) {
+         ItemStack itemInHand = player.getItemInHand(hand);
+         if (itemInHand.is(Items.BUCKET) && !this.isBaby()) {
++            // CraftBukkit start - Got milk?
++            org.bukkit.event.player.PlayerBucketFillEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerBucketFillEvent((ServerLevel) player.level(), player, this.blockPosition(), this.blockPosition(), null, itemInHand, Items.MILK_BUCKET, hand);
++            if (event.isCancelled()) {
++                player.containerMenu.sendAllDataToRemote(); // Paper - Fix inventory desync
++                return InteractionResult.PASS;
++            }
++            // CraftBukkit end
+             player.playSound(SoundEvents.COW_MILK, 1.0F, 1.0F);
+-            ItemStack itemStack = ItemUtils.createFilledResult(itemInHand, player, Items.MILK_BUCKET.getDefaultInstance());
++            ItemStack itemStack = ItemUtils.createFilledResult(itemInHand, player, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItemStack())); // CraftBukkit
+             player.setItemInHand(hand, itemStack);
+             return InteractionResult.SUCCESS;
+         } else {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/Dolphin.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/Dolphin.java.patch
new file mode 100644
index 0000000000..f0e77a24be
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/Dolphin.java.patch
@@ -0,0 +1,69 @@
+--- a/net/minecraft/world/entity/animal/Dolphin.java
++++ b/net/minecraft/world/entity/animal/Dolphin.java
+@@ -96,6 +_,13 @@
+         return EntityType.DOLPHIN.create(level, EntitySpawnReason.BREEDING);
+     }
+ 
++    // CraftBukkit start - SPIGOT-6907: re-implement LivingEntity#setMaximumAir()
++    @Override
++    public int getDefaultMaxAirSupply() {
++        return TOTAL_AIR_SUPPLY;
++    }
++    // CraftBukkit end
++
+     @Override
+     public float getAgeScale() {
+         return this.isBaby() ? 0.65F : 1.0F;
+@@ -196,7 +_,7 @@
+ 
+     @Override
+     public int getMaxAirSupply() {
+-        return 4800;
++        return this.maxAirTicks; // CraftBukkit - SPIGOT-6907: re-implement LivingEntity#setMaximumAir()
+     }
+ 
+     @Override
+@@ -229,11 +_,15 @@
+         if (this.getItemBySlot(EquipmentSlot.MAINHAND).isEmpty()) {
+             ItemStack item = entity.getItem();
+             if (this.canHoldItem(item)) {
++                // CraftBukkit start - call EntityPickupItemEvent
++                if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPickupItemEvent(this, entity, 0, false).isCancelled()) return;
++                item = entity.getItem(); // CraftBukkit- update ItemStack from event
++                // CraftBukkit end
+                 this.onItemPickup(entity);
+                 this.setItemSlot(EquipmentSlot.MAINHAND, item);
+                 this.setGuaranteedDrop(EquipmentSlot.MAINHAND);
+                 this.take(entity, item.getCount());
+-                entity.discard();
++                entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
+             }
+         }
+     }
+@@ -497,7 +_,7 @@
+ 
+         @Override
+         public void start() {
+-            this.player.addEffect(new MobEffectInstance(MobEffects.DOLPHINS_GRACE, 100), this.dolphin);
++            this.player.addEffect(new MobEffectInstance(MobEffects.DOLPHINS_GRACE, 100), this.dolphin, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.DOLPHIN); // CraftBukkit
+         }
+ 
+         @Override
+@@ -516,7 +_,7 @@
+             }
+ 
+             if (this.player.isSwimming() && this.player.level().random.nextInt(6) == 0) {
+-                this.player.addEffect(new MobEffectInstance(MobEffects.DOLPHINS_GRACE, 100), this.dolphin);
++                this.player.addEffect(new MobEffectInstance(MobEffects.DOLPHINS_GRACE, 100), this.dolphin, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.DOLPHIN); // CraftBukkit
+             }
+         }
+     }
+@@ -586,7 +_,7 @@
+                     0.3F * Mth.cos(Dolphin.this.getYRot() * (float) (Math.PI / 180.0)) * Mth.cos(Dolphin.this.getXRot() * (float) (Math.PI / 180.0))
+                         + Mth.sin(f1) * f2
+                 );
+-                Dolphin.this.level().addFreshEntity(itemEntity);
++                Dolphin.this.spawnAtLocation(getServerLevel(Dolphin.this), itemEntity); // Paper - Call EntityDropItemEvent
+             }
+         }
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/Fox.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/Fox.java.patch
new file mode 100644
index 0000000000..aa6fdd9654
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/Fox.java.patch
@@ -0,0 +1,143 @@
+--- a/net/minecraft/world/entity/animal/Fox.java
++++ b/net/minecraft/world/entity/animal/Fox.java
+@@ -413,7 +_,7 @@
+ 
+         this.setSleeping(compound.getBoolean("Sleeping"));
+         this.setVariant(Fox.Variant.byName(compound.getString("Type")));
+-        this.setSitting(compound.getBoolean("Sitting"));
++        this.setSitting(compound.getBoolean("Sitting"), false); // Paper - Add EntityToggleSitEvent
+         this.setIsCrouching(compound.getBoolean("Crouching"));
+         if (this.level() instanceof ServerLevel) {
+             this.setTargetGoals();
+@@ -425,6 +_,12 @@
+     }
+ 
+     public void setSitting(boolean sitting) {
++        // Paper start - Add EntityToggleSitEvent
++        this.setSitting(sitting, true);
++    }
++    public void setSitting(boolean sitting, boolean fireEvent) {
++        if (fireEvent && !new io.papermc.paper.event.entity.EntityToggleSitEvent(this.getBukkitEntity(), sitting).callEvent()) return;
++        // Paper end - Add EntityToggleSitEvent
+         this.setFlag(1, sitting);
+     }
+ 
+@@ -484,19 +_,20 @@
+             itemEntity.setPickUpDelay(40);
+             itemEntity.setThrower(this);
+             this.playSound(SoundEvents.FOX_SPIT, 1.0F, 1.0F);
+-            this.level().addFreshEntity(itemEntity);
++            this.spawnAtLocation((net.minecraft.server.level.ServerLevel) this.level(), itemEntity); // Paper - Call EntityDropItemEvent
+         }
+     }
+ 
+     private void dropItemStack(ItemStack stack) {
+         ItemEntity itemEntity = new ItemEntity(this.level(), this.getX(), this.getY(), this.getZ(), stack);
+-        this.level().addFreshEntity(itemEntity);
++        this.spawnAtLocation((net.minecraft.server.level.ServerLevel) this.level(), itemEntity); // Paper - Call EntityDropItemEvent
+     }
+ 
+     @Override
+     protected void pickUpItem(ServerLevel level, ItemEntity entity) {
+         ItemStack item = entity.getItem();
+-        if (this.canHoldItem(item)) {
++        if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPickupItemEvent(this, entity, item.getCount() - 1, !this.canHoldItem(item)).isCancelled()) { // CraftBukkit - call EntityPickupItemEvent
++            item = entity.getItem(); // CraftBukkit - update item after event
+             int count = item.getCount();
+             if (count > 1) {
+                 this.dropItemStack(item.split(count - 1));
+@@ -507,7 +_,7 @@
+             this.setItemSlot(EquipmentSlot.MAINHAND, item.split(1));
+             this.setGuaranteedDrop(EquipmentSlot.MAINHAND);
+             this.take(entity, item.getCount());
+-            entity.discard();
++            entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
+             this.ticksSinceEaten = 0;
+         }
+     }
+@@ -671,15 +_,33 @@
+         return this.getTrustedUUIDs().contains(uuid);
+     }
+ 
+-    @Override
+-    protected void dropAllDeathLoot(ServerLevel level, DamageSource damageSource) {
++    // Paper start - handle the bitten item separately like vanilla
++    @Override
++    protected boolean shouldSkipLoot(EquipmentSlot slot) {
++        return slot == EquipmentSlot.MAINHAND;
++    }
++    // Paper end
++
++    @Override
++    // Paper start - Cancellable death event
++    protected org.bukkit.event.entity.EntityDeathEvent dropAllDeathLoot(ServerLevel level, DamageSource damageSource) {
+         ItemStack itemBySlot = this.getItemBySlot(EquipmentSlot.MAINHAND);
+-        if (!itemBySlot.isEmpty()) {
++        boolean releaseMouth = false;
++        if (!itemBySlot.isEmpty() && level.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) { // Fix MC-153010
+             this.spawnAtLocation(level, itemBySlot);
++            releaseMouth = true;
++        }
++
++        org.bukkit.event.entity.EntityDeathEvent deathEvent = super.dropAllDeathLoot(level, damageSource);
++        // Below is code to drop
++        if (deathEvent == null || deathEvent.isCancelled()) return deathEvent;
++
++        if (releaseMouth) {
++            // Paper end
+             this.setItemSlot(EquipmentSlot.MAINHAND, ItemStack.EMPTY);
+         }
+ 
+-        super.dropAllDeathLoot(level, damageSource);
++        return deathEvent; // Paper - Cancellable death event
+     }
+ 
+     public static boolean isPathClear(Fox fox, LivingEntity livingEntity) {
+@@ -853,6 +_,14 @@
+                 if (loveCause1 != null && loveCause != loveCause1) {
+                     fox.addTrustedUUID(loveCause1.getUUID());
+                 }
++                // CraftBukkit start - call EntityBreedEvent
++                fox.setAge(-24000);
++                fox.moveTo(this.animal.getX(), this.animal.getY(), this.animal.getZ(), 0.0F, 0.0F);
++                int experience = this.animal.getRandom().nextInt(7) + 1;
++                org.bukkit.event.entity.EntityBreedEvent entityBreedEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityBreedEvent(fox, this.animal, this.partner, loveCause, this.animal.breedItem, experience);
++                if (entityBreedEvent.isCancelled()) return;
++                experience = entityBreedEvent.getExperience();
++                // CraftBukkit end
+ 
+                 if (serverPlayer != null) {
+                     serverPlayer.awardStat(Stats.ANIMALS_BRED);
+@@ -863,15 +_,17 @@
+                 this.partner.setAge(6000);
+                 this.animal.resetLove();
+                 this.partner.resetLove();
+-                fox.setAge(-24000);
+-                fox.moveTo(this.animal.getX(), this.animal.getY(), this.animal.getZ(), 0.0F, 0.0F);
+-                serverLevel.addFreshEntityWithPassengers(fox);
++                serverLevel.addFreshEntityWithPassengers(fox, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.BREEDING); // CraftBukkit - added SpawnReason
+                 this.level.broadcastEntityEvent(this.animal, (byte)18);
+                 if (serverLevel.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) {
+-                    this.level
+-                        .addFreshEntity(
+-                            new ExperienceOrb(this.level, this.animal.getX(), this.animal.getY(), this.animal.getZ(), this.animal.getRandom().nextInt(7) + 1)
+-                        );
++                    // CraftBukkit start - use event experience
++                    if (experience > 0) {
++                        this.level
++                            .addFreshEntity(
++                                new ExperienceOrb(this.level, this.animal.getX(), this.animal.getY(), this.animal.getZ(), experience, org.bukkit.entity.ExperienceOrb.SpawnReason.BREED, loveCause, fox) // Paper
++                            );
++                    }
++                    // CraftBukkit end
+                 }
+             }
+         }
+@@ -934,6 +_,7 @@
+         private void pickSweetBerries(BlockState state) {
+             int ageValue = state.getValue(SweetBerryBushBlock.AGE);
+             state.setValue(SweetBerryBushBlock.AGE, Integer.valueOf(1));
++            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(Fox.this, this.blockPos, state.setValue(SweetBerryBushBlock.AGE, 1))) return; // CraftBukkit - call EntityChangeBlockEvent
+             int i = 1 + Fox.this.level().random.nextInt(2) + (ageValue == 3 ? 1 : 0);
+             ItemStack itemBySlot = Fox.this.getItemBySlot(EquipmentSlot.MAINHAND);
+             if (itemBySlot.isEmpty()) {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/IronGolem.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/IronGolem.java.patch
new file mode 100644
index 0000000000..3af59e7fb7
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/IronGolem.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/entity/animal/IronGolem.java
++++ b/net/minecraft/world/entity/animal/IronGolem.java
+@@ -104,7 +_,7 @@
+     @Override
+     protected void doPush(Entity entity) {
+         if (entity instanceof Enemy && !(entity instanceof Creeper) && this.getRandom().nextInt(20) == 0) {
+-            this.setTarget((LivingEntity)entity);
++            this.setTarget((LivingEntity)entity, org.bukkit.event.entity.EntityTargetLivingEntityEvent.TargetReason.COLLISION, true); // CraftBukkit - set reason
+         }
+ 
+         super.doPush(entity);
+@@ -303,7 +_,7 @@
+         BlockPos blockPos = this.blockPosition();
+         BlockPos blockPos1 = blockPos.below();
+         BlockState blockState = level.getBlockState(blockPos1);
+-        if (!blockState.entityCanStandOn(level, blockPos1, this)) {
++        if (!blockState.entityCanStandOn(level, blockPos1, this) && !this.level().paperConfig().entities.spawning.ironGolemsCanSpawnInAir) { // Paper - Add option to allow iron golems to spawn in air
+             return false;
+         } else {
+             for (int i = 1; i < 3; i++) {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/MushroomCow.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/MushroomCow.java.patch
new file mode 100644
index 0000000000..adb68967c7
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/MushroomCow.java.patch
@@ -0,0 +1,58 @@
+--- a/net/minecraft/world/entity/animal/MushroomCow.java
++++ b/net/minecraft/world/entity/animal/MushroomCow.java
+@@ -110,7 +_,17 @@
+             return InteractionResult.SUCCESS;
+         } else if (itemInHand.is(Items.SHEARS) && this.readyForShearing()) {
+             if (this.level() instanceof ServerLevel serverLevel) {
+-                this.shear(serverLevel, SoundSource.PLAYERS, itemInHand);
++                // CraftBukkit start
++                // Paper start - custom shear drops
++                java.util.List<ItemStack> drops = this.generateDefaultDrops(serverLevel, itemInHand);
++                org.bukkit.event.player.PlayerShearEntityEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.handlePlayerShearEntityEvent(player, this, itemInHand, hand, drops);
++                if (event != null) {
++                    if (event.isCancelled()) return InteractionResult.PASS;
++                    drops = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getDrops());
++                    // Paper end - custom shear drops
++                }
++                // CraftBukkit end
++                this.shear(serverLevel, SoundSource.PLAYERS, itemInHand, drops); // Paper - custom shear drops
+                 this.gameEvent(GameEvent.SHEAR, player);
+                 itemInHand.hurtAndBreak(1, player, getSlotForHand(hand));
+             }
+@@ -163,15 +_,31 @@
+ 
+     @Override
+     public void shear(ServerLevel level, SoundSource soundSource, ItemStack shears) {
++        // Paper start - custom shear drops
++        this.shear(level, soundSource, shears, this.generateDefaultDrops(level, shears));
++    }
++
++    @Override
++    public java.util.List<ItemStack> generateDefaultDrops(final ServerLevel serverLevel, final ItemStack shears) {
++        final java.util.List<ItemStack> drops = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>();
++        this.dropFromShearingLootTable(serverLevel, BuiltInLootTables.SHEAR_MOOSHROOM, shears, (ignored, stack) -> {
++            for (int i = 0; i < stack.getCount(); ++i) drops.add(stack.copyWithCount(1));
++        });
++        return drops;
++    }
++
++    @Override
++    public void shear(ServerLevel level, SoundSource soundSource, ItemStack shears, java.util.List<ItemStack> drops) {
++        // Paper end
+         level.playSound(null, this, SoundEvents.MOOSHROOM_SHEAR, soundSource, 1.0F, 1.0F);
+         this.convertTo(EntityType.COW, ConversionParams.single(this, false, false), mob -> {
+             level.sendParticles(ParticleTypes.EXPLOSION, this.getX(), this.getY(0.5), this.getZ(), 1, 0.0, 0.0, 0.0, 0.0);
+-            this.dropFromShearingLootTable(level, BuiltInLootTables.SHEAR_MOOSHROOM, shears, (serverLevel, itemStack) -> {
+-                for (int i = 0; i < itemStack.getCount(); i++) {
+-                    serverLevel.addFreshEntity(new ItemEntity(this.level(), this.getX(), this.getY(1.0), this.getZ(), itemStack.copyWithCount(1)));
+-                }
++            // Paper start - custom shear drops; moved drop generation to separate method
++            drops.forEach(drop -> {
++                this.spawnAtLocation(level, new ItemEntity(this.level(), this.getX(), this.getY(1.0), this.getZ(), drop));
+             });
+-        });
++            // Paper end - custom shear drops
++        }, org.bukkit.event.entity.EntityTransformEvent.TransformReason.SHEARED, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.SHEARED); // CraftBukkit
+     }
+ 
+     @Override
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/Ocelot.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/Ocelot.java.patch
new file mode 100644
index 0000000000..0d5245ce20
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/Ocelot.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/entity/animal/Ocelot.java
++++ b/net/minecraft/world/entity/animal/Ocelot.java
+@@ -125,7 +_,7 @@
+ 
+     @Override
+     public boolean removeWhenFarAway(double distanceToClosestPlayer) {
+-        return !this.isTrusting() && this.tickCount > 2400;
++        return !this.isTrusting() && this.tickCount > 2400 && !this.hasCustomName() && !this.isLeashed(); // Paper - honor name and leash
+     }
+ 
+     public static AttributeSupplier.Builder createAttributes() {
+@@ -159,7 +_,7 @@
+         if ((this.temptGoal == null || this.temptGoal.isRunning()) && !this.isTrusting() && this.isFood(itemInHand) && player.distanceToSqr(this) < 9.0) {
+             this.usePlayerItem(player, hand, itemInHand);
+             if (!this.level().isClientSide) {
+-                if (this.random.nextInt(3) == 0) {
++                if (this.random.nextInt(3) == 0 && !org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTameEvent(this, player).isCancelled()) { // CraftBukkit - added event call and isCancelled check
+                     this.setTrusting(true);
+                     this.spawnTrustingParticles(true);
+                     this.level().broadcastEntityEvent(this, (byte)41);
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/Panda.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/Panda.java.patch
new file mode 100644
index 0000000000..3e9a850886
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/Panda.java.patch
@@ -0,0 +1,81 @@
+--- a/net/minecraft/world/entity/animal/Panda.java
++++ b/net/minecraft/world/entity/animal/Panda.java
+@@ -127,6 +_,7 @@
+     }
+ 
+     public void sit(boolean sitting) {
++        if (!new io.papermc.paper.event.entity.EntityToggleSitEvent(this.getBukkitEntity(), sitting).callEvent()) return; // Paper - Add EntityToggleSitEvent
+         this.setFlag(8, sitting);
+     }
+ 
+@@ -516,24 +_,28 @@
+ 
+         for (Panda panda : level.getEntitiesOfClass(Panda.class, this.getBoundingBox().inflate(10.0))) {
+             if (!panda.isBaby() && panda.onGround() && !panda.isInWater() && panda.canPerformAction()) {
++                if (new com.destroystokyo.paper.event.entity.EntityJumpEvent(getBukkitLivingEntity()).callEvent()) { // Paper - Entity Jump API
+                 panda.jumpFromGround();
++                } else { this.setJumping(false); } // Paper - Entity Jump API; setJumping(false) stops a potential loop
+             }
+         }
+ 
+         if (this.level() instanceof ServerLevel serverLevel && serverLevel.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) {
++            this.forceDrops = true; // Paper - Add missing forceDrop toggles
+             this.dropFromGiftLootTable(serverLevel, BuiltInLootTables.PANDA_SNEEZE, this::spawnAtLocation);
++            this.forceDrops = false; // Paper - Add missing forceDrop toggles
+         }
+     }
+ 
+     @Override
+     protected void pickUpItem(ServerLevel level, ItemEntity entity) {
+-        if (this.getItemBySlot(EquipmentSlot.MAINHAND).isEmpty() && canPickUpAndEat(entity)) {
++        if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPickupItemEvent(this, entity, 0, !(this.getItemBySlot(EquipmentSlot.MAINHAND).isEmpty() && Panda.canPickUpAndEat(entity))).isCancelled()) { // CraftBukkit
+             this.onItemPickup(entity);
+             ItemStack item = entity.getItem();
+             this.setItemSlot(EquipmentSlot.MAINHAND, item);
+             this.setGuaranteedDrop(EquipmentSlot.MAINHAND);
+             this.take(entity, item.getCount());
+-            entity.discard();
++            entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
+         }
+     }
+ 
+@@ -624,8 +_,9 @@
+                 this.usePlayerItem(player, hand, itemInHand);
+                 this.ageUp((int)(-this.getAge() / 20 * 0.1F), true);
+             } else if (!this.level().isClientSide && this.getAge() == 0 && this.canFallInLove()) {
++                final ItemStack breedCopy = itemInHand.copy(); // Paper - Fix EntityBreedEvent copying
+                 this.usePlayerItem(player, hand, itemInHand);
+-                this.setInLove(player);
++                this.setInLove(player, breedCopy); // Paper - Fix EntityBreedEvent copying
+             } else {
+                 if (!(this.level() instanceof ServerLevel serverLevel) || this.isSitting() || this.isInWater()) {
+                     return InteractionResult.PASS;
+@@ -635,7 +_,9 @@
+                 this.eat(true);
+                 ItemStack itemBySlot = this.getItemBySlot(EquipmentSlot.MAINHAND);
+                 if (!itemBySlot.isEmpty() && !player.hasInfiniteMaterials()) {
++                    this.forceDrops = true; // Paper - Add missing forceDrop toggles
+                     this.spawnAtLocation(serverLevel, itemBySlot);
++                    this.forceDrops = false; // Paper - Add missing forceDrop toggles
+                 }
+ 
+                 this.setItemSlot(EquipmentSlot.MAINHAND, new ItemStack(itemInHand.getItem(), 1));
+@@ -861,7 +_,7 @@
+         @Override
+         protected void alertOther(Mob mob, LivingEntity target) {
+             if (mob instanceof Panda && mob.isAggressive()) {
+-                mob.setTarget(target);
++                mob.setTarget(target, org.bukkit.event.entity.EntityTargetEvent.TargetReason.TARGET_ATTACKED_ENTITY, true); // CraftBukkit
+             }
+         }
+     }
+@@ -1090,7 +_,9 @@
+         public void stop() {
+             ItemStack itemBySlot = Panda.this.getItemBySlot(EquipmentSlot.MAINHAND);
+             if (!itemBySlot.isEmpty()) {
++                Panda.this.forceDrops = true; // Paper - Add missing forceDrop toggles
+                 Panda.this.spawnAtLocation(getServerLevel(Panda.this.level()), itemBySlot);
++                Panda.this.forceDrops = false; // Paper - Add missing forceDrop toggles
+                 Panda.this.setItemSlot(EquipmentSlot.MAINHAND, ItemStack.EMPTY);
+                 int i = Panda.this.isLazy() ? Panda.this.random.nextInt(50) + 10 : Panda.this.random.nextInt(150) + 10;
+                 this.cooldown = Panda.this.tickCount + i * 20;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Parrot.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/Parrot.java.patch
similarity index 75%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/animal/Parrot.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/animal/Parrot.java.patch
index e42863c2b1..863d2bcb91 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Parrot.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/Parrot.java.patch
@@ -1,24 +1,24 @@
 --- a/net/minecraft/world/entity/animal/Parrot.java
 +++ b/net/minecraft/world/entity/animal/Parrot.java
-@@ -248,7 +248,7 @@
+@@ -257,7 +_,7 @@
              }
  
              if (!this.level().isClientSide) {
 -                if (this.random.nextInt(10) == 0) {
 +                if (this.random.nextInt(10) == 0 && !org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTameEvent(this, player).isCancelled()) { // CraftBukkit
                      this.tame(player);
-                     this.level().broadcastEntityEvent(this, (byte) 7);
+                     this.level().broadcastEntityEvent(this, (byte)7);
                  } else {
-@@ -269,7 +269,7 @@
+@@ -278,7 +_,7 @@
              }
          } else {
-             this.usePlayerItem(player, hand, itemstack);
+             this.usePlayerItem(player, hand, itemInHand);
 -            this.addEffect(new MobEffectInstance(MobEffects.POISON, 900));
 +            this.addEffect(new MobEffectInstance(MobEffects.POISON, 900), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.FOOD); // CraftBukkit
              if (player.isCreative() || !this.isInvulnerable()) {
                  this.hurt(this.damageSources().playerAttack(player), Float.MAX_VALUE);
              }
-@@ -362,8 +362,8 @@
+@@ -373,8 +_,8 @@
      }
  
      @Override
@@ -29,19 +29,18 @@
      }
  
      @Override
-@@ -378,8 +378,14 @@
-         if (this.isInvulnerableTo(world, source)) {
+@@ -389,8 +_,13 @@
+         if (this.isInvulnerableTo(level, damageSource)) {
              return false;
          } else {
 +            // CraftBukkit start
-+            boolean result = super.hurtServer(world, source, amount);
-+            if (!result) {
-+                return result;
++            if (!super.hurtServer(level, damageSource, amount)) {
++                return false;
 +            }
-+            // CraftBukkit end
              this.setOrderedToSit(false);
--            return super.hurtServer(world, source, amount);
-+            return result; // CraftBukkit
+-            return super.hurtServer(level, damageSource, amount);
++            return true;
++            // CraftBukkit
          }
      }
  
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/Pig.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/Pig.java.patch
new file mode 100644
index 0000000000..31833a8584
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/Pig.java.patch
@@ -0,0 +1,18 @@
+--- a/net/minecraft/world/entity/animal/Pig.java
++++ b/net/minecraft/world/entity/animal/Pig.java
+@@ -214,7 +_,14 @@
+                 }
+ 
+                 mob.setPersistenceRequired();
+-            });
++                // CraftBukkit start
++            }, null, null);
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.callPigZapEvent(this, lightning, zombifiedPiglin).isCancelled()) {
++                return;
++            }
++            level.addFreshEntity(zombifiedPiglin, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.LIGHTNING);
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.TRANSFORMATION); // CraftBukkit - add Bukkit remove cause
++            // CraftBukkit end
+             if (zombifiedPiglin == null) {
+                 super.thunderHit(level, lightning);
+             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Pufferfish.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/Pufferfish.java.patch
similarity index 67%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/animal/Pufferfish.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/animal/Pufferfish.java.patch
index 32042a8ba6..bd25f044bc 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Pufferfish.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/Pufferfish.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/entity/animal/Pufferfish.java
 +++ b/net/minecraft/world/entity/animal/Pufferfish.java
-@@ -102,25 +102,39 @@
+@@ -95,24 +_,36 @@
      public void tick() {
          if (!this.level().isClientSide && this.isAlive() && this.isEffectiveAi()) {
              if (this.inflateCounter > 0) {
@@ -17,9 +17,8 @@
 +                    } else { increase = false; } // Paper - Add PufferFishStateChangeEvent
                  }
  
-+                if (increase) { // Paper - Add PufferFishStateChangeEvent
-                 ++this.inflateCounter;
-+                } // Paper - Add PufferFishStateChangeEvent
++                if (increase) // Paper - Add PufferFishStateChangeEvent
+                 this.inflateCounter++;
              } else if (this.getPuffState() != 0) {
 +                boolean increase = true; // Paper - Add PufferFishStateChangeEvent
                  if (this.deflateTimer > 60 && this.getPuffState() == 2) {
@@ -34,27 +33,25 @@
 +                    } else { increase = false; } // Paper - Add PufferFishStateChangeEvent
                  }
  
-+                if (increase) { // Paper - Add PufferFishStateChangeEvent
-                 ++this.deflateTimer;
-+                } // Paper - Add PufferFishStateChangeEvent
++                if (increase) // Paper - Add PufferFishStateChangeEvent
+                 this.deflateTimer++;
              }
          }
- 
-@@ -155,7 +169,7 @@
-         int i = this.getPuffState();
- 
-         if (target.hurtServer(world, this.damageSources().mobAttack(this), (float) (1 + i))) {
--            target.addEffect(new MobEffectInstance(MobEffects.POISON, 60 * i, 0), this);
-+            target.addEffect(new MobEffectInstance(MobEffects.POISON, 60 * i, 0), this, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ATTACK); // CraftBukkit
+@@ -136,7 +_,7 @@
+     private void touch(ServerLevel level, Mob mob) {
+         int puffState = this.getPuffState();
+         if (mob.hurtServer(level, this.damageSources().mobAttack(this), 1 + puffState)) {
+-            mob.addEffect(new MobEffectInstance(MobEffects.POISON, 60 * puffState, 0), this);
++            mob.addEffect(new MobEffectInstance(MobEffects.POISON, 60 * puffState, 0), this, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ATTACK); // CraftBukkit
              this.playSound(SoundEvents.PUFFER_FISH_STING, 1.0F, 1.0F);
          }
- 
-@@ -171,7 +185,7 @@
-                     entityplayer.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.PUFFER_FISH_STING, 0.0F));
-                 }
- 
--                player.addEffect(new MobEffectInstance(MobEffects.POISON, 60 * i, 0), this);
-+                player.addEffect(new MobEffectInstance(MobEffects.POISON, 60 * i, 0), this, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ATTACK); // CraftBukkit
+     }
+@@ -151,7 +_,7 @@
+                 serverPlayer.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.PUFFER_FISH_STING, 0.0F));
              }
-         }
+ 
+-            entity.addEffect(new MobEffectInstance(MobEffects.POISON, 60 * puffState, 0), this);
++            entity.addEffect(new MobEffectInstance(MobEffects.POISON, 60 * puffState, 0), this, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ATTACK); // CraftBukkit
+         }
+     }
  
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/Rabbit.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/Rabbit.java.patch
new file mode 100644
index 0000000000..d5d3f133b3
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/Rabbit.java.patch
@@ -0,0 +1,23 @@
+--- a/net/minecraft/world/entity/animal/Rabbit.java
++++ b/net/minecraft/world/entity/animal/Rabbit.java
+@@ -88,7 +_,7 @@
+         super(entityType, level);
+         this.jumpControl = new Rabbit.RabbitJumpControl(this);
+         this.moveControl = new Rabbit.RabbitMoveControl(this);
+-        this.setSpeedModifier(0.0);
++        //this.setSpeedModifier(0.0); // CraftBukkit
+     }
+ 
+     @Override
+@@ -561,9 +_,11 @@
+                 if (this.canRaid && block instanceof CarrotBlock) {
+                     int ageValue = blockState.getValue(CarrotBlock.AGE);
+                     if (ageValue == 0) {
++                        if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this.rabbit, blockPos, blockState.getFluidState().createLegacyBlock())) return; // CraftBukkit // Paper - fix wrong block state
+                         level.setBlock(blockPos, Blocks.AIR.defaultBlockState(), 2);
+                         level.destroyBlock(blockPos, true, this.rabbit);
+                     } else {
++                        if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this.rabbit, blockPos, blockState.setValue(CarrotBlock.AGE, ageValue - 1))) return; // CraftBukkit // Paper - fix wrong block state
+                         level.setBlock(blockPos, blockState.setValue(CarrotBlock.AGE, Integer.valueOf(ageValue - 1)), 2);
+                         level.gameEvent(GameEvent.BLOCK_CHANGE, blockPos, GameEvent.Context.of(this.rabbit));
+                         level.levelEvent(2001, blockPos, Block.getId(blockState));
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/Sheep.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/Sheep.java.patch
new file mode 100644
index 0000000000..2a1505ea58
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/Sheep.java.patch
@@ -0,0 +1,67 @@
+--- a/net/minecraft/world/entity/animal/Sheep.java
++++ b/net/minecraft/world/entity/animal/Sheep.java
+@@ -158,7 +_,19 @@
+         ItemStack itemInHand = player.getItemInHand(hand);
+         if (itemInHand.is(Items.SHEARS)) {
+             if (this.level() instanceof ServerLevel serverLevel && this.readyForShearing()) {
+-                this.shear(serverLevel, SoundSource.PLAYERS, itemInHand);
++                // CraftBukkit start
++                // Paper start - custom shear drops
++                java.util.List<ItemStack> drops = this.generateDefaultDrops(serverLevel, itemInHand);
++                org.bukkit.event.player.PlayerShearEntityEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.handlePlayerShearEntityEvent(player, this, itemInHand, hand, drops);
++                if (event != null) {
++                    if (event.isCancelled()) {
++                        return InteractionResult.PASS;
++                    }
++                    drops = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getDrops());
++                    // Paper end - custom shear drops
++                }
++                // CraftBukkit end
++                this.shear(serverLevel, SoundSource.PLAYERS, itemInHand, drops); // Paper - custom shear drops
+                 this.gameEvent(GameEvent.SHEAR, player);
+                 itemInHand.hurtAndBreak(1, player, getSlotForHand(hand));
+                 return InteractionResult.SUCCESS_SERVER;
+@@ -172,14 +_,28 @@
+ 
+     @Override
+     public void shear(ServerLevel level, SoundSource soundSource, ItemStack shears) {
++        // Paper start - custom shear drops
++        this.shear(level, soundSource, shears, this.generateDefaultDrops(level, shears));
++    }
++
++    @Override
++    public java.util.List<ItemStack> generateDefaultDrops(final ServerLevel serverLevel, final ItemStack shears) {
++        final java.util.List<ItemStack> drops = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>();
++        this.dropFromShearingLootTable(serverLevel, BuiltInLootTables.SHEAR_SHEEP, shears, (ignored, stack) -> {
++            for (int i = 0; i < stack.getCount(); ++i) drops.add(stack.copyWithCount(1));
++        });
++        return drops;
++    }
++
++    @Override
++    public void shear(ServerLevel level, SoundSource soundSource, ItemStack shears, java.util.List<ItemStack> drops) {
++        // Paper end - custom shear drops
+         level.playSound(null, this, SoundEvents.SHEEP_SHEAR, soundSource, 1.0F, 1.0F);
+-        this.dropFromShearingLootTable(
+-            level,
+-            BuiltInLootTables.SHEAR_SHEEP,
+-            shears,
+-            (serverLevel, itemStack) -> {
+-                for (int i = 0; i < itemStack.getCount(); i++) {
+-                    ItemEntity itemEntity = this.spawnAtLocation(serverLevel, itemStack.copyWithCount(1), 1.0F);
++        drops.forEach(itemStack -> { // Paper - custom drops - loop in generated default drops
++            if (true) { // Paper - custom drops - loop in generated default drops
++                this.forceDrops = true; // CraftBukkit
++                ItemEntity itemEntity = this.spawnAtLocation(level, itemStack, 1.0F); // Paper - custom drops - copy already done above
++                this.forceDrops = false; // CraftBukkit
+                     if (itemEntity != null) {
+                         itemEntity.setDeltaMovement(
+                             itemEntity.getDeltaMovement()
+@@ -287,6 +_,7 @@
+ 
+     @Override
+     public void ate() {
++        if (!new org.bukkit.event.entity.SheepRegrowWoolEvent((org.bukkit.entity.Sheep) this.getBukkitEntity()).callEvent()) return; // CraftBukkit
+         super.ate();
+         this.setSheared(false);
+         if (this.isBaby()) {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/ShoulderRidingEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/ShoulderRidingEntity.java.patch
new file mode 100644
index 0000000000..367ba239ff
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/ShoulderRidingEntity.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/animal/ShoulderRidingEntity.java
++++ b/net/minecraft/world/entity/animal/ShoulderRidingEntity.java
+@@ -19,7 +_,7 @@
+         compoundTag.putString("id", this.getEncodeId());
+         this.saveWithoutId(compoundTag);
+         if (player.setEntityOnShoulder(compoundTag)) {
+-            this.discard();
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
+             return true;
+         } else {
+             return false;
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/SnowGolem.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/SnowGolem.java.patch
new file mode 100644
index 0000000000..e647b528c6
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/SnowGolem.java.patch
@@ -0,0 +1,74 @@
+--- a/net/minecraft/world/entity/animal/SnowGolem.java
++++ b/net/minecraft/world/entity/animal/SnowGolem.java
+@@ -92,7 +_,7 @@
+         super.aiStep();
+         if (this.level() instanceof ServerLevel serverLevel) {
+             if (this.level().getBiome(this.blockPosition()).is(BiomeTags.SNOW_GOLEM_MELTS)) {
+-                this.hurtServer(serverLevel, this.damageSources().onFire(), 1.0F);
++                this.hurtServer(serverLevel, this.damageSources().melting(), 1.0F); // CraftBukkit - DamageSources.ON_FIRE -> CraftEventFactory.MELTING
+             }
+ 
+             if (!serverLevel.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) {
+@@ -107,7 +_,7 @@
+                 int floor2 = Mth.floor(this.getZ() + (i / 2 % 2 * 2 - 1) * 0.25F);
+                 BlockPos blockPos = new BlockPos(floor, floor1, floor2);
+                 if (this.level().getBlockState(blockPos).isAir() && blockState.canSurvive(this.level(), blockPos)) {
+-                    this.level().setBlockAndUpdate(blockPos, blockState);
++                    if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this.level(), blockPos, blockState, this)) continue; // CraftBukkit
+                     this.level().gameEvent(GameEvent.BLOCK_PLACE, blockPos, GameEvent.Context.of(this, blockState));
+                 }
+             }
+@@ -135,7 +_,19 @@
+         ItemStack itemInHand = player.getItemInHand(hand);
+         if (itemInHand.is(Items.SHEARS) && this.readyForShearing()) {
+             if (this.level() instanceof ServerLevel serverLevel) {
+-                this.shear(serverLevel, SoundSource.PLAYERS, itemInHand);
++                // CraftBukkit start
++                // Paper start - custom shear drops
++                java.util.List<ItemStack> drops = this.generateDefaultDrops(serverLevel, itemInHand);
++                org.bukkit.event.player.PlayerShearEntityEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.handlePlayerShearEntityEvent(player, this, itemInHand, hand, drops);
++                if (event != null) {
++                    if (event.isCancelled()) {
++                        return InteractionResult.PASS;
++                    }
++                    drops = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getDrops());
++                    // Paper end - custom shear drops
++                }
++                // CraftBukkit end
++                this.shear(serverLevel, SoundSource.PLAYERS, itemInHand, drops); // Paper - custom shear drops
+                 this.gameEvent(GameEvent.SHEAR, player);
+                 itemInHand.hurtAndBreak(1, player, getSlotForHand(hand));
+             }
+@@ -148,11 +_,29 @@
+ 
+     @Override
+     public void shear(ServerLevel level, SoundSource soundSource, ItemStack shears) {
++        // Paper start - custom shear drops
++        this.shear(level, soundSource, shears, this.generateDefaultDrops(level, shears));
++    }
++
++    @Override
++    public java.util.List<ItemStack> generateDefaultDrops(final ServerLevel serverLevel, final ItemStack shears) {
++        final java.util.List<ItemStack> drops = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>();
++        this.dropFromShearingLootTable(serverLevel, BuiltInLootTables.SHEAR_SNOW_GOLEM, shears, (ignored, stack) -> {
++            drops.add(stack);
++        });
++        return drops;
++    }
++
++    @Override
++    public void shear(ServerLevel level, SoundSource soundSource, ItemStack shears, java.util.List<ItemStack> drops) {
++        // Paper end - custom shear drops
+         level.playSound(null, this, SoundEvents.SNOW_GOLEM_SHEAR, soundSource, 1.0F, 1.0F);
+         this.setPumpkin(false);
+-        this.dropFromShearingLootTable(
+-            level, BuiltInLootTables.SHEAR_SNOW_GOLEM, shears, (serverLevel, itemStack) -> this.spawnAtLocation(serverLevel, itemStack, this.getEyeHeight())
+-        );
++        drops.forEach(itemStack -> { // Paper - custom shear drops
++            this.forceDrops = true; // CraftBukkit
++            this.spawnAtLocation(level, itemStack, this.getEyeHeight());
++            this.forceDrops = false; // CraftBukkit
++        });
+     }
+ 
+     @Override
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/Squid.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/Squid.java.patch
new file mode 100644
index 0000000000..e2fffe7466
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/Squid.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/animal/Squid.java
++++ b/net/minecraft/world/entity/animal/Squid.java
+@@ -46,7 +_,7 @@
+ 
+     public Squid(EntityType<? extends Squid> entityType, Level level) {
+         super(entityType, level);
+-        this.random.setSeed(this.getId());
++        //this.random.setSeed(this.getId()); // Paper - Share random for entities to make them more random
+         this.tentacleSpeed = 1.0F / (this.random.nextFloat() + 1.0F) * 0.2F;
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/Turtle.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/Turtle.java.patch
new file mode 100644
index 0000000000..741323b5b1
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/Turtle.java.patch
@@ -0,0 +1,75 @@
+--- a/net/minecraft/world/entity/animal/Turtle.java
++++ b/net/minecraft/world/entity/animal/Turtle.java
+@@ -303,7 +_,9 @@
+     protected void ageBoundaryReached() {
+         super.ageBoundaryReached();
+         if (!this.isBaby() && this.level() instanceof ServerLevel serverLevel && serverLevel.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) {
++            this.forceDrops = true; // CraftBukkit
+             this.spawnAtLocation(serverLevel, Items.TURTLE_SCUTE, 1);
++            this.forceDrops = false; // CraftBukkit
+         }
+     }
+ 
+@@ -328,7 +_,7 @@
+ 
+     @Override
+     public void thunderHit(ServerLevel level, LightningBolt lightning) {
+-        this.hurtServer(level, this.damageSources().lightningBolt(), Float.MAX_VALUE);
++        this.hurtServer(level, this.damageSources().lightningBolt().customEventDamager(lightning), Float.MAX_VALUE); // CraftBukkit // Paper - fix DamageSource API
+     }
+ 
+     @Override
+@@ -355,6 +_,10 @@
+             if (loveCause == null && this.partner.getLoveCause() != null) {
+                 loveCause = this.partner.getLoveCause();
+             }
++            // Paper start - Add EntityFertilizeEggEvent event
++            io.papermc.paper.event.entity.EntityFertilizeEggEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityFertilizeEggEvent(this.animal, this.partner);
++            if (event.isCancelled()) return;
++            // Paper end - Add EntityFertilizeEggEvent event
+ 
+             if (loveCause != null) {
+                 loveCause.awardStat(Stats.ANIMALS_BRED);
+@@ -368,7 +_,7 @@
+             this.partner.resetLove();
+             RandomSource random = this.animal.getRandom();
+             if (getServerLevel(this.level).getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) {
+-                this.level.addFreshEntity(new ExperienceOrb(this.level, this.animal.getX(), this.animal.getY(), this.animal.getZ(), random.nextInt(7) + 1));
++                if (event.getExperience() > 0) this.level.addFreshEntity(new ExperienceOrb(this.level, this.animal.getX(), this.animal.getY(), this.animal.getZ(), event.getExperience(), org.bukkit.entity.ExperienceOrb.SpawnReason.BREED, loveCause)); // Paper - Add EntityFertilizeEggEvent event
+             }
+         }
+     }
+@@ -392,7 +_,7 @@
+                     this.turtle.hasEgg()
+                         || this.turtle.getRandom().nextInt(reducedTickDelay(700)) == 0
+                             && !this.turtle.getHomePos().closerToCenterThan(this.turtle.position(), 64.0)
+-                );
++                ) && new com.destroystokyo.paper.event.entity.TurtleGoHomeEvent((org.bukkit.entity.Turtle) this.turtle.getBukkitEntity()).callEvent(); // Paper - Turtle API
+         }
+ 
+         @Override
+@@ -500,16 +_,22 @@
+             BlockPos blockPos = this.turtle.blockPosition();
+             if (!this.turtle.isInWater() && this.isReachedTarget()) {
+                 if (this.turtle.layEggCounter < 1) {
+-                    this.turtle.setLayingEgg(true);
++                    this.turtle.setLayingEgg(new com.destroystokyo.paper.event.entity.TurtleStartDiggingEvent((org.bukkit.entity.Turtle) this.turtle.getBukkitEntity(), io.papermc.paper.util.MCUtil.toLocation(this.turtle.level(), this.blockPos)).callEvent()); // Paper - Turtle API
+                 } else if (this.turtle.layEggCounter > this.adjustedTickDelay(200)) {
++                    // Paper start - Turtle API
++                    int eggCount = this.turtle.random.nextInt(4) + 1;
++                    com.destroystokyo.paper.event.entity.TurtleLayEggEvent layEggEvent = new com.destroystokyo.paper.event.entity.TurtleLayEggEvent((org.bukkit.entity.Turtle) this.turtle.getBukkitEntity(), io.papermc.paper.util.MCUtil.toLocation(this.turtle.level(), this.blockPos.above()), eggCount);
++                    if (layEggEvent.callEvent() && org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this.turtle, this.blockPos.above(), Blocks.TURTLE_EGG.defaultBlockState().setValue(TurtleEggBlock.EGGS, layEggEvent.getEggCount()))) {
++                    // Paper end - Turtle API
+                     Level level = this.turtle.level();
+                     level.playSound(null, blockPos, SoundEvents.TURTLE_LAY_EGG, SoundSource.BLOCKS, 0.3F, 0.9F + level.random.nextFloat() * 0.2F);
+                     BlockPos blockPos1 = this.blockPos.above();
+                     BlockState blockState = Blocks.TURTLE_EGG
+                         .defaultBlockState()
+-                        .setValue(TurtleEggBlock.EGGS, Integer.valueOf(this.turtle.random.nextInt(4) + 1));
++                        .setValue(TurtleEggBlock.EGGS, layEggEvent.getEggCount()); // Paper
+                     level.setBlock(blockPos1, blockState, 3);
+                     level.gameEvent(GameEvent.BLOCK_PLACE, blockPos1, GameEvent.Context.of(this.turtle, blockState));
++                    } // CraftBukkit
+                     this.turtle.setHasEgg(false);
+                     this.turtle.setLayingEgg(false);
+                     this.turtle.setInLoveTime(600);
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/WaterAnimal.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/WaterAnimal.java.patch
new file mode 100644
index 0000000000..f871cf2552
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/WaterAnimal.java.patch
@@ -0,0 +1,13 @@
+--- a/net/minecraft/world/entity/animal/WaterAnimal.java
++++ b/net/minecraft/world/entity/animal/WaterAnimal.java
+@@ -70,6 +_,10 @@
+     ) {
+         int seaLevel = level.getSeaLevel();
+         int i = seaLevel - 13;
++        // Paper start - Make water animal spawn height configurable
++        seaLevel = level.getMinecraftWorld().paperConfig().entities.spawning.wateranimalSpawnHeight.maximum.or(seaLevel);
++        i = level.getMinecraftWorld().paperConfig().entities.spawning.wateranimalSpawnHeight.minimum.or(i);
++        // Paper end - Make water animal spawn height configurable
+         return pos.getY() >= i
+             && pos.getY() <= seaLevel
+             && level.getFluidState(pos.below()).is(FluidTags.WATER)
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/Wolf.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/Wolf.java.patch
new file mode 100644
index 0000000000..6bcb88fa10
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/Wolf.java.patch
@@ -0,0 +1,95 @@
+--- a/net/minecraft/world/entity/animal/Wolf.java
++++ b/net/minecraft/world/entity/animal/Wolf.java
+@@ -344,8 +_,9 @@
+         if (this.isInvulnerableTo(level, damageSource)) {
+             return false;
+         } else {
++            if (!super.hurtServer(level, damageSource, amount)) return false; // CraftBukkit
+             this.setOrderedToSit(false);
+-            return super.hurtServer(level, damageSource, amount);
++            return true; // CraftBukkit
+         }
+     }
+ 
+@@ -355,10 +_,11 @@
+     }
+ 
+     @Override
+-    protected void actuallyHurt(ServerLevel level, DamageSource damageSource, float amount) {
++    public boolean actuallyHurt(ServerLevel level, DamageSource damageSource, float amount, org.bukkit.event.entity.EntityDamageEvent event) { // CraftBukkit - void -> boolean
+         if (!this.canArmorAbsorb(damageSource)) {
+-            super.actuallyHurt(level, damageSource, amount);
++            super.actuallyHurt(level, damageSource, amount, event); // CraftBukkit
+         } else {
++            if (event.isCancelled()) return false; // CraftBukkit - SPIGOT-7815: if the damage was cancelled, no need to run the wolf armor behaviour
+             ItemStack bodyArmorItem = this.getBodyArmorItem();
+             int damageValue = bodyArmorItem.getDamageValue();
+             int maxDamage = bodyArmorItem.getMaxDamage();
+@@ -378,6 +_,7 @@
+                 );
+             }
+         }
++        return true; // CraftBukkit // Paper - return false ONLY if event was cancelled
+     }
+ 
+     private boolean canArmorAbsorb(DamageSource damageSource) {
+@@ -388,7 +_,7 @@
+     protected void applyTamingSideEffects() {
+         if (this.isTame()) {
+             this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(40.0);
+-            this.setHealth(40.0F);
++            this.setHealth(this.getMaxHealth()); // CraftBukkit - 40.0 -> getMaxHealth()
+         } else {
+             this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(8.0);
+         }
+@@ -408,7 +_,7 @@
+                 this.usePlayerItem(player, hand, itemInHand);
+                 FoodProperties foodProperties = itemInHand.get(DataComponents.FOOD);
+                 float f = foodProperties != null ? foodProperties.nutrition() : 1.0F;
+-                this.heal(2.0F * f);
++                this.heal(2.0F * f, org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.EATING); // CraftBukkit
+                 return InteractionResult.SUCCESS;
+             }
+ 
+@@ -441,7 +_,7 @@
+                         this.setOrderedToSit(!this.isOrderedToSit());
+                         this.jumping = false;
+                         this.navigation.stop();
+-                        this.setTarget(null);
++                        this.setTarget(null, org.bukkit.event.entity.EntityTargetEvent.TargetReason.FORGOT_TARGET, true); // CraftBukkit - reason
+                         return InteractionResult.SUCCESS.withoutItem();
+                     }
+ 
+@@ -453,7 +_,9 @@
+                 ItemStack bodyArmorItem = this.getBodyArmorItem();
+                 this.setBodyArmorItem(ItemStack.EMPTY);
+                 if (this.level() instanceof ServerLevel serverLevel) {
++                    this.forceDrops = true; // CraftBukkit
+                     this.spawnAtLocation(serverLevel, bodyArmorItem);
++                    this.forceDrops = false; // CraftBukkit
+                 }
+ 
+                 return InteractionResult.SUCCESS;
+@@ -461,6 +_,13 @@
+ 
+             DyeColor dyeColor = dyeItem.getDyeColor();
+             if (dyeColor != this.getCollarColor()) {
++                // Paper start - Add EntityDyeEvent and CollarColorable interface
++                final io.papermc.paper.event.entity.EntityDyeEvent event = new io.papermc.paper.event.entity.EntityDyeEvent(this.getBukkitEntity(), org.bukkit.DyeColor.getByWoolData((byte) dyeColor.getId()), (org.bukkit.entity.Player) player.getBukkitEntity());
++                if (!event.callEvent()) {
++                    return InteractionResult.FAIL;
++                }
++                dyeColor = DyeColor.byId(event.getColor().getWoolData());
++                // Paper end - Add EntityDyeEvent and CollarColorable interface
+                 this.setCollarColor(dyeColor);
+                 itemInHand.consume(1, player);
+                 return InteractionResult.SUCCESS;
+@@ -475,7 +_,7 @@
+     }
+ 
+     private void tryToTame(Player player) {
+-        if (this.random.nextInt(3) == 0) {
++        if (this.random.nextInt(3) == 0 && !org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTameEvent(this, player).isCancelled()) { // CraftBukkit - added event call and isCancelled check.
+             this.tame(player);
+             this.navigation.stop();
+             this.setTarget(null);
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/allay/Allay.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/allay/Allay.java.patch
new file mode 100644
index 0000000000..8653998f89
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/allay/Allay.java.patch
@@ -0,0 +1,83 @@
+--- a/net/minecraft/world/entity/animal/allay/Allay.java
++++ b/net/minecraft/world/entity/animal/allay/Allay.java
+@@ -118,6 +_,7 @@
+     private float dancingAnimationTicks;
+     private float spinningAnimationTicks;
+     private float spinningAnimationTicks0;
++    public boolean forceDancing = false; // CraftBukkit
+ 
+     public Allay(EntityType<? extends Allay> entityType, Level level) {
+         super(entityType, level);
+@@ -131,6 +_,12 @@
+         );
+     }
+ 
++    // CraftBukkit start
++    public void setCanDuplicate(boolean canDuplicate) {
++        this.entityData.set(Allay.DATA_CAN_DUPLICATE, canDuplicate);
++    }
++    // CraftBukkit end
++
+     @Override
+     protected Brain.Provider<Allay> brainProvider() {
+         return Brain.provider(MEMORY_TYPES, SENSOR_TYPES);
+@@ -252,7 +_,7 @@
+     public void aiStep() {
+         super.aiStep();
+         if (!this.level().isClientSide && this.isAlive() && this.tickCount % 10 == 0) {
+-            this.heal(1.0F);
++            this.heal(1.0F, org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.REGEN); // CraftBukkit
+         }
+ 
+         if (this.isDancing() && this.shouldStopDancing() && this.tickCount % 20 == 0) {
+@@ -320,7 +_,12 @@
+         ItemStack itemInHand = player.getItemInHand(hand);
+         ItemStack itemInHand1 = this.getItemInHand(InteractionHand.MAIN_HAND);
+         if (this.isDancing() && itemInHand.is(ItemTags.DUPLICATES_ALLAYS) && this.canDuplicate()) {
+-            this.duplicateAllay();
++            // CraftBukkit start - handle cancel duplication
++            Allay allay = this.duplicateAllay();
++            if (allay == null) {
++                return InteractionResult.SUCCESS;
++            }
++            // CraftBukkit end
+             this.level().broadcastEntityEvent(this, (byte)18);
+             this.level().playSound(player, this, SoundEvents.AMETHYST_BLOCK_CHIME, SoundSource.NEUTRAL, 2.0F, 1.0F);
+             this.removeInteractionItem(player, itemInHand);
+@@ -425,6 +_,7 @@
+     }
+ 
+     private boolean shouldStopDancing() {
++        if (this.forceDancing) {return false;} // CraftBukkit
+         return this.jukeboxPos == null
+             || !this.jukeboxPos.closerToCenterThan(this.position(), GameEvent.JUKEBOX_PLAY.value().notificationRadius())
+             || !this.level().getBlockState(this.jukeboxPos).is(Blocks.JUKEBOX);
+@@ -489,7 +_,7 @@
+                 .ifPresent(data -> this.vibrationData = data);
+         }
+ 
+-        this.duplicationCooldown = compound.getInt("DuplicationCooldown");
++        this.duplicationCooldown = compound.getLong("DuplicationCooldown"); // Paper - Load as long
+         this.entityData.set(DATA_CAN_DUPLICATE, compound.getBoolean("CanDuplicate"));
+     }
+ 
+@@ -508,15 +_,17 @@
+         }
+     }
+ 
+-    public void duplicateAllay() {
++    @Nullable public Allay duplicateAllay() { // CraftBukkit - return allay
+         Allay allay = EntityType.ALLAY.create(this.level(), EntitySpawnReason.BREEDING);
+         if (allay != null) {
+             allay.moveTo(this.position());
+             allay.setPersistenceRequired();
+             allay.resetDuplicationCooldown();
+             this.resetDuplicationCooldown();
+-            this.level().addFreshEntity(allay);
++            this.level().addFreshEntity(allay, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DUPLICATION); // CraftBukkit - reason for duplicated allay
+         }
++
++        return allay; // CraftBukkit
+     }
+ 
+     public void resetDuplicationCooldown() {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/armadillo/Armadillo.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/armadillo/Armadillo.java.patch
new file mode 100644
index 0000000000..28a059aba2
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/armadillo/Armadillo.java.patch
@@ -0,0 +1,47 @@
+--- a/net/minecraft/world/entity/animal/armadillo/Armadillo.java
++++ b/net/minecraft/world/entity/animal/armadillo/Armadillo.java
+@@ -141,10 +_,12 @@
+         ArmadilloAi.updateActivity(this);
+         profilerFiller.pop();
+         if (this.isAlive() && !this.isBaby() && --this.scuteTime <= 0) {
++            this.forceDrops = true; // CraftBukkit
+             if (this.dropFromGiftLootTable(level, BuiltInLootTables.ARMADILLO_SHED, this::spawnAtLocation)) {
+                 this.playSound(SoundEvents.ARMADILLO_SCUTE_DROP, 1.0F, (this.random.nextFloat() - this.random.nextFloat()) * 0.2F + 1.0F);
+                 this.gameEvent(GameEvent.ENTITY_PLACE);
+             }
++            this.forceDrops = false; // CraftBukkit
+ 
+             this.scuteTime = this.pickNextScuteDropTime();
+         }
+@@ -283,8 +_,11 @@
+     }
+ 
+     @Override
+-    protected void actuallyHurt(ServerLevel level, DamageSource damageSource, float amount) {
+-        super.actuallyHurt(level, damageSource, amount);
++    // CraftBukkit start - void -> boolean
++    public boolean actuallyHurt(ServerLevel level, DamageSource damageSource, float amount, org.bukkit.event.entity.EntityDamageEvent event) {
++        boolean damageResult = super.actuallyHurt(level, damageSource, amount, event);
++        if (!damageResult) return false;
++        // CraftBukkit end
+         if (!this.isNoAi() && !this.isDeadOrDying()) {
+             if (damageSource.getEntity() instanceof LivingEntity) {
+                 this.getBrain().setMemoryWithExpiry(MemoryModuleType.DANGER_DETECTED_RECENTLY, true, 80L);
+@@ -295,6 +_,7 @@
+                 this.rollOut();
+             }
+         }
++        return true; // CraftBukkit
+     }
+ 
+     @Override
+@@ -313,7 +_,9 @@
+             return false;
+         } else {
+             if (this.level() instanceof ServerLevel serverLevel) {
++                this.forceDrops = true; // CraftBukkit
+                 this.spawnAtLocation(serverLevel, new ItemStack(Items.ARMADILLO_SCUTE));
++                this.forceDrops = false; // CraftBukkit
+                 this.gameEvent(GameEvent.ENTITY_INTERACT);
+                 this.playSound(SoundEvents.ARMADILLO_BRUSH);
+             }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/axolotl/Axolotl.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/axolotl/Axolotl.java.patch
new file mode 100644
index 0000000000..b39c81cfbc
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/axolotl/Axolotl.java.patch
@@ -0,0 +1,38 @@
+--- a/net/minecraft/world/entity/animal/axolotl/Axolotl.java
++++ b/net/minecraft/world/entity/animal/axolotl/Axolotl.java
+@@ -226,7 +_,7 @@
+ 
+     @Override
+     public int getMaxAirSupply() {
+-        return 6000;
++        return this.maxAirTicks; // CraftBukkit - SPIGOT-6907: re-implement LivingEntity#setMaximumAir()
+     }
+ 
+     @Override
+@@ -426,10 +_,10 @@
+         if (effect == null || effect.endsWithin(2399)) {
+             int i = effect != null ? effect.getDuration() : 0;
+             int min = Math.min(2400, 100 + i);
+-            player.addEffect(new MobEffectInstance(MobEffects.REGENERATION, min, 0), this);
++            player.addEffect(new MobEffectInstance(MobEffects.REGENERATION, min, 0), this, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.AXOLOTL); // CraftBukkit
+         }
+ 
+-        player.removeEffect(MobEffects.DIG_SLOWDOWN);
++        player.removeEffect(MobEffects.DIG_SLOWDOWN, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.AXOLOTL); // Paper - Add missing effect cause
+     }
+ 
+     @Override
+@@ -521,6 +_,13 @@
+     ) {
+         return level.getBlockState(pos.below()).is(BlockTags.AXOLOTLS_SPAWNABLE_ON);
+     }
++
++    // CraftBukkit start - SPIGOT-6907: re-implement LivingEntity#setMaximumAir()
++    @Override
++    public int getDefaultMaxAirSupply() {
++        return Axolotl.AXOLOTL_TOTAL_AIR_SUPPLY;
++    }
++    // CraftBukkit end
+ 
+     public static enum AnimationState {
+         PLAYING_DEAD,
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/camel/Camel.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/camel/Camel.java.patch
similarity index 59%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/animal/camel/Camel.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/animal/camel/Camel.java.patch
index e2b9543713..2b76781cbe 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/camel/Camel.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/camel/Camel.java.patch
@@ -1,59 +1,37 @@
 --- a/net/minecraft/world/entity/animal/camel/Camel.java
 +++ b/net/minecraft/world/entity/animal/camel/Camel.java
-@@ -49,6 +49,9 @@
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.phys.Vec2;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityDamageEvent;
-+// CraftBukkit end
- 
- public class Camel extends AbstractHorse {
- 
-@@ -143,7 +146,7 @@
-         ProfilerFiller gameprofilerfiller = Profiler.get();
- 
-         gameprofilerfiller.push("camelBrain");
--        Brain<?> behaviorcontroller = this.getBrain();
-+        Brain<Camel> behaviorcontroller = (Brain<Camel>) this.getBrain(); // CraftBukkit - decompile error
- 
-         behaviorcontroller.tick(world, this);
-         gameprofilerfiller.pop();
-@@ -386,13 +389,13 @@
+@@ -386,12 +_,12 @@
+         } else {
              boolean flag = this.getHealth() < this.getMaxHealth();
- 
              if (flag) {
 -                this.heal(2.0F);
 +                this.heal(2.0F, org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.EATING); // Paper - Add missing regain reason
              }
  
              boolean flag1 = this.isTamed() && this.getAge() == 0 && this.canFallInLove();
- 
              if (flag1) {
 -                this.setInLove(player);
-+                this.setInLove(player, item.copy()); // Paper - Fix EntityBreedEvent copying
++                this.setInLove(player, stack.copy()); // Paper - Fix EntityBreedEvent copying
              }
  
-             boolean flag2 = this.isBaby();
-@@ -454,9 +457,15 @@
+             boolean isBaby = this.isBaby();
+@@ -451,9 +_,13 @@
      }
  
      @Override
--    protected void actuallyHurt(ServerLevel world, DamageSource source, float amount) {
+-    protected void actuallyHurt(ServerLevel level, DamageSource damageSource, float amount) {
 +    // CraftBukkit start - void -> boolean
-+    public boolean actuallyHurt(ServerLevel worldserver, DamageSource damagesource, float f, EntityDamageEvent event) {
-+        boolean damageResult = super.actuallyHurt(worldserver, damagesource, f, event);
-+        if (!damageResult) {
-+            return false;
-+        }
++    public boolean actuallyHurt(ServerLevel level, DamageSource damageSource, float amount, org.bukkit.event.entity.EntityDamageEvent event) {
++        boolean damageResult = super.actuallyHurt(level, damageSource, amount, event);
++        if (!damageResult) return false;
 +        // CraftBukkit end
          this.standUpInstantly();
--        super.actuallyHurt(world, source, amount);
+-        super.actuallyHurt(level, damageSource, amount);
 +        return true; // CraftBukkit
      }
  
      @Override
-@@ -563,7 +572,7 @@
+@@ -554,7 +_,7 @@
      }
  
      public void sitDown() {
@@ -62,7 +40,7 @@
              this.makeSound(SoundEvents.CAMEL_SIT);
              this.setPose(Pose.SITTING);
              this.gameEvent(GameEvent.ENTITY_ACTION);
-@@ -572,7 +581,7 @@
+@@ -563,7 +_,7 @@
      }
  
      public void standUp() {
@@ -71,7 +49,7 @@
              this.makeSound(SoundEvents.CAMEL_STAND);
              this.setPose(Pose.STANDING);
              this.gameEvent(GameEvent.ENTITY_ACTION);
-@@ -581,6 +590,7 @@
+@@ -572,6 +_,7 @@
      }
  
      public void standUpInstantly() {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/frog/Frog.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/frog/Frog.java.patch
similarity index 59%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/animal/frog/Frog.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/animal/frog/Frog.java.patch
index f849bb1ffc..f46b1c8ba3 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/frog/Frog.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/frog/Frog.java.patch
@@ -1,15 +1,15 @@
 --- a/net/minecraft/world/entity/animal/frog/Frog.java
 +++ b/net/minecraft/world/entity/animal/frog/Frog.java
-@@ -270,7 +270,12 @@
+@@ -270,7 +_,12 @@
  
      @Override
-     public void spawnChildFromBreeding(ServerLevel world, Animal other) {
--        this.finalizeSpawnChildFromBreeding(world, other, null);
+     public void spawnChildFromBreeding(ServerLevel level, Animal mate) {
+-        this.finalizeSpawnChildFromBreeding(level, mate, null);
 +        // Paper start - Add EntityFertilizeEggEvent event
-+        final io.papermc.paper.event.entity.EntityFertilizeEggEvent result = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityFertilizeEggEvent(this, other);
++        final io.papermc.paper.event.entity.EntityFertilizeEggEvent result = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityFertilizeEggEvent(this, mate);
 +        if (result.isCancelled()) return;
 +
-+        this.finalizeSpawnChildFromBreeding(world, other, null, result.getExperience()); // Paper - use craftbukkit call that takes experience amount
++        this.finalizeSpawnChildFromBreeding(level, mate, null, result.getExperience()); // Paper - use craftbukkit call that takes experience amount
 +        // Paper end - Add EntityFertilizeEggEvent event
          this.getBrain().setMemory(MemoryModuleType.IS_PREGNANT, Unit.INSTANCE);
      }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/frog/ShootTongue.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/frog/ShootTongue.java.patch
new file mode 100644
index 0000000000..f503b6b310
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/frog/ShootTongue.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/animal/frog/ShootTongue.java
++++ b/net/minecraft/world/entity/animal/frog/ShootTongue.java
+@@ -96,7 +_,7 @@
+             if (entity.isAlive()) {
+                 frog.doHurtTarget(level, entity);
+                 if (!entity.isAlive()) {
+-                    entity.remove(Entity.RemovalReason.KILLED);
++                    entity.remove(Entity.RemovalReason.KILLED, org.bukkit.event.entity.EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause
+                 }
+             }
+         }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/frog/Tadpole.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/frog/Tadpole.java.patch
new file mode 100644
index 0000000000..215b78b19d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/frog/Tadpole.java.patch
@@ -0,0 +1,84 @@
+--- a/net/minecraft/world/entity/animal/frog/Tadpole.java
++++ b/net/minecraft/world/entity/animal/frog/Tadpole.java
+@@ -62,6 +_,7 @@
+         MemoryModuleType.BREED_TARGET,
+         MemoryModuleType.IS_PANICKING
+     );
++    public boolean ageLocked; // Paper
+ 
+     public Tadpole(EntityType<? extends AbstractFish> entityType, Level level) {
+         super(entityType, level);
+@@ -113,7 +_,7 @@
+     @Override
+     public void aiStep() {
+         super.aiStep();
+-        if (!this.level().isClientSide) {
++        if (!this.level().isClientSide && !this.ageLocked) { // Paper
+             this.setAge(this.age + 1);
+         }
+     }
+@@ -122,12 +_,14 @@
+     public void addAdditionalSaveData(CompoundTag compound) {
+         super.addAdditionalSaveData(compound);
+         compound.putInt("Age", this.age);
++        compound.putBoolean("AgeLocked", this.ageLocked); // Paper
+     }
+ 
+     @Override
+     public void readAdditionalSaveData(CompoundTag compound) {
+         super.readAdditionalSaveData(compound);
+         this.setAge(compound.getInt("Age"));
++        this.ageLocked = compound.getBoolean("AgeLocked"); // Paper
+     }
+ 
+     @Nullable
+@@ -177,7 +_,12 @@
+     @Override
+     public void saveToBucketTag(ItemStack stack) {
+         Bucketable.saveDefaultDataToBucketTag(this, stack);
+-        CustomData.update(DataComponents.BUCKET_ENTITY_DATA, stack, compoundTag -> compoundTag.putInt("Age", this.getAge()));
++        // Paper start - Save tadpole age
++        CustomData.update(DataComponents.BUCKET_ENTITY_DATA, stack, compoundTag -> {
++            compoundTag.putInt("Age", this.getAge());
++            compoundTag.putBoolean("AgeLocked", this.ageLocked);
++        });
++        // Paper end - Save tadpole age
+     }
+ 
+     @Override
+@@ -186,6 +_,7 @@
+         if (tag.contains("Age")) {
+             this.setAge(tag.getInt("Age"));
+         }
++        this.ageLocked = tag.getBoolean("AgeLocked"); // Paper
+     }
+ 
+     @Override
+@@ -217,6 +_,7 @@
+     }
+ 
+     private void ageUp(int offset) {
++        if (this.ageLocked) return; // Paper
+         this.setAge(this.age + offset * 20);
+     }
+ 
+@@ -229,12 +_,17 @@
+ 
+     private void ageUp() {
+         if (this.level() instanceof ServerLevel serverLevel) {
+-            this.convertTo(EntityType.FROG, ConversionParams.single(this, false, false), mob -> {
++            Frog converted = this.convertTo(EntityType.FROG, ConversionParams.single(this, false, false), mob -> { // CraftBukkit
+                 mob.finalizeSpawn(serverLevel, this.level().getCurrentDifficultyAt(mob.blockPosition()), EntitySpawnReason.CONVERSION, null);
+                 mob.setPersistenceRequired();
+                 mob.fudgePositionAfterSizeChange(this.getDimensions(this.getPose()));
+                 this.playSound(SoundEvents.TADPOLE_GROW_UP, 0.15F, 1.0F);
+-            });
++            // CraftBukkit start
++            }, org.bukkit.event.entity.EntityTransformEvent.TransformReason.METAMORPHOSIS, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.METAMORPHOSIS);
++            if (converted == null) {
++                this.setAge(0); // Sets the age to 0 for avoid a loop if the event is canceled
++            }
++            // CraftBukkit end
+         }
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/goat/Goat.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/goat/Goat.java.patch
new file mode 100644
index 0000000000..af11b98e94
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/goat/Goat.java.patch
@@ -0,0 +1,52 @@
+--- a/net/minecraft/world/entity/animal/goat/Goat.java
++++ b/net/minecraft/world/entity/animal/goat/Goat.java
+@@ -231,13 +_,22 @@
+     public InteractionResult mobInteract(Player player, InteractionHand hand) {
+         ItemStack itemInHand = player.getItemInHand(hand);
+         if (itemInHand.is(Items.BUCKET) && !this.isBaby()) {
++            // CraftBukkit start - Got milk?
++            org.bukkit.event.player.PlayerBucketFillEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerBucketFillEvent((ServerLevel) player.level(), player, this.blockPosition(), this.blockPosition(), null, itemInHand, Items.MILK_BUCKET, hand);
++
++            if (event.isCancelled()) {
++                player.containerMenu.sendAllDataToRemote(); // Paper - Fix inventory desync
++                return InteractionResult.PASS;
++            }
++            // CraftBukkit end
+             player.playSound(this.getMilkingSound(), 1.0F, 1.0F);
+-            ItemStack itemStack = ItemUtils.createFilledResult(itemInHand, player, Items.MILK_BUCKET.getDefaultInstance());
++            ItemStack itemStack = ItemUtils.createFilledResult(itemInHand, player, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItemStack())); // CraftBukkit
+             player.setItemInHand(hand, itemStack);
+             return InteractionResult.SUCCESS;
+         } else {
++            boolean isFood = this.isFood(itemInHand); // Paper - track before stack is possibly decreased to 0 (Fixes MC-244739)
+             InteractionResult interactionResult = super.mobInteract(player, hand);
+-            if (interactionResult.consumesAction() && this.isFood(itemInHand)) {
++            if (interactionResult.consumesAction() && isFood) { // Paper
+                 this.playEatingSound();
+             }
+ 
+@@ -350,7 +_,7 @@
+             double d2 = Mth.randomBetween(this.random, -0.2F, 0.2F);
+             ItemEntity itemEntity = new ItemEntity(this.level(), vec3.x(), vec3.y(), vec3.z(), itemStack, d, d1, d2);
+             this.level().addFreshEntity(itemEntity);
+-            return true;
++            return this.spawnAtLocation((net.minecraft.server.level.ServerLevel) this.level(), itemEntity) != null; // Paper - Call EntityDropItemEvent
+         }
+     }
+ 
+@@ -381,4 +_,15 @@
+     ) {
+         return level.getBlockState(pos.below()).is(BlockTags.GOATS_SPAWNABLE_ON) && isBrightEnoughToSpawn(level, pos);
+     }
++
++    // Paper start - Goat ram API
++    public void ram(net.minecraft.world.entity.LivingEntity entity) {
++        Brain<Goat> brain = this.getBrain();
++        brain.setMemory(MemoryModuleType.RAM_TARGET, entity.position());
++        brain.eraseMemory(MemoryModuleType.RAM_COOLDOWN_TICKS);
++        brain.eraseMemory(MemoryModuleType.BREED_TARGET);
++        brain.eraseMemory(MemoryModuleType.TEMPTING_PLAYER);
++        brain.setActiveActivityIfPossible(net.minecraft.world.entity.schedule.Activity.RAM);
++    }
++    // Paper end - Goat ram API
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/horse/AbstractChestedHorse.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/horse/AbstractChestedHorse.java.patch
new file mode 100644
index 0000000000..9fbe146a0c
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/horse/AbstractChestedHorse.java.patch
@@ -0,0 +1,15 @@
+--- a/net/minecraft/world/entity/animal/horse/AbstractChestedHorse.java
++++ b/net/minecraft/world/entity/animal/horse/AbstractChestedHorse.java
+@@ -69,6 +_,12 @@
+         super.dropEquipment(level);
+         if (this.hasChest()) {
+             this.spawnAtLocation(level, Blocks.CHEST);
++            // Paper start - moved to post death logic
++        }
++    }
++    protected void postDeathDropItems(org.bukkit.event.entity.EntityDeathEvent event) {
++        if (this.hasChest() && !event.isCancelled()) {
++            // Paper end - moved to post death logic
+             this.setChest(false);
+         }
+     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/AbstractHorse.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/horse/AbstractHorse.java.patch
similarity index 64%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/AbstractHorse.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/animal/horse/AbstractHorse.java.patch
index e975349f91..2679d44397 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/AbstractHorse.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/horse/AbstractHorse.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/entity/animal/horse/AbstractHorse.java
 +++ b/net/minecraft/world/entity/animal/horse/AbstractHorse.java
-@@ -79,6 +79,17 @@
+@@ -77,6 +_,17 @@
  import net.minecraft.world.phys.Vec3;
  import net.minecraft.world.ticks.ContainerSingleItem;
  
@@ -16,16 +16,15 @@
 +// CraftBukkit end
 +
  public abstract class AbstractHorse extends Animal implements ContainerListener, HasCustomInventoryScreen, OwnableEntity, PlayerRideableJumping, Saddleable {
- 
      public static final int EQUIPMENT_SLOT_OFFSET = 400;
-@@ -166,8 +177,54 @@
-         @Override
+     public static final int CHEST_SLOT_OFFSET = 499;
+@@ -145,7 +_,53 @@
          public boolean stillValid(Player player) {
-             return player.getVehicle() == AbstractHorse.this || player.canInteractWithEntity((Entity) AbstractHorse.this, 4.0D);
-+        }
+             return player.getVehicle() == AbstractHorse.this || player.canInteractWithEntity(AbstractHorse.this, 4.0);
+         }
 +
 +        // CraftBukkit start - add fields and methods
-+        public List<HumanEntity> transaction = new java.util.ArrayList<HumanEntity>();
++        public final List<HumanEntity> transaction = new java.util.ArrayList<>();
 +        private int maxStack = MAX_STACK;
 +
 +        @Override
@@ -34,13 +33,13 @@
 +        }
 +
 +        @Override
-+        public void onOpen(CraftHumanEntity who) {
-+            this.transaction.add(who);
++        public void onOpen(CraftHumanEntity player) {
++            this.transaction.add(player);
 +        }
 +
 +        @Override
-+        public void onClose(CraftHumanEntity who) {
-+            this.transaction.remove(who);
++        public void onClose(CraftHumanEntity player) {
++            this.transaction.remove(player);
 +        }
 +
 +        @Override
@@ -66,14 +65,14 @@
 +        @Override
 +        public Location getLocation() {
 +            return AbstractHorse.this.getBukkitEntity().getLocation();
-         }
++        }
 +        // CraftBukkit end
      };
 +    public int maxDomestication = 100; // CraftBukkit - store max domestication value
  
-     protected AbstractHorse(EntityType<? extends AbstractHorse> type, Level world) {
-         super(type, world);
-@@ -312,7 +369,7 @@
+     protected AbstractHorse(EntityType<? extends AbstractHorse> entityType, Level level) {
+         super(entityType, level);
+@@ -284,7 +_,7 @@
      }
  
      @Override
@@ -82,16 +81,16 @@
          return !this.isVehicle();
      }
  
-@@ -366,7 +423,7 @@
-     public void createInventory() {
-         SimpleContainer inventorysubcontainer = this.inventory;
+@@ -340,7 +_,7 @@
  
+     public void createInventory() {
+         SimpleContainer simpleContainer = this.inventory;
 -        this.inventory = new SimpleContainer(this.getInventorySize());
 +        this.inventory = new SimpleContainer(this.getInventorySize(), (org.bukkit.entity.AbstractHorse) this.getBukkitEntity()); // CraftBukkit
-         if (inventorysubcontainer != null) {
-             inventorysubcontainer.removeListener(this);
-             int i = Math.min(inventorysubcontainer.getContainerSize(), this.inventory.getContainerSize());
-@@ -470,7 +527,7 @@
+         if (simpleContainer != null) {
+             simpleContainer.removeListener(this);
+             int min = Math.min(simpleContainer.getContainerSize(), this.inventory.getContainerSize());
+@@ -448,7 +_,7 @@
      }
  
      public int getMaxTemper() {
@@ -100,21 +99,21 @@
      }
  
      @Override
-@@ -528,7 +585,7 @@
-             b0 = 5;
+@@ -503,7 +_,7 @@
+             i1 = 5;
              if (!this.level().isClientSide && this.isTamed() && this.getAge() == 0 && !this.isInLove()) {
                  flag = true;
 -                this.setInLove(player);
-+                this.setInLove(player, item.copy()); // Paper - Fix EntityBreedEvent copying
++                this.setInLove(player, stack.copy()); // Paper - Fix EntityBreedEvent copying
              }
-         } else if (item.is(Items.GOLDEN_APPLE) || item.is(Items.ENCHANTED_GOLDEN_APPLE)) {
+         } else if (stack.is(Items.GOLDEN_APPLE) || stack.is(Items.ENCHANTED_GOLDEN_APPLE)) {
              f = 10.0F;
-@@ -536,12 +593,12 @@
-             b0 = 10;
+@@ -511,12 +_,12 @@
+             i1 = 10;
              if (!this.level().isClientSide && this.isTamed() && this.getAge() == 0 && !this.isInLove()) {
                  flag = true;
 -                this.setInLove(player);
-+                this.setInLove(player, item.copy()); // Paper - Fix EntityBreedEvent copying
++                this.setInLove(player, stack.copy()); // Paper - Fix EntityBreedEvent copying
              }
          }
  
@@ -124,34 +123,34 @@
              flag = true;
          }
  
-@@ -618,7 +675,7 @@
-         if (world instanceof ServerLevel worldserver) {
-             if (this.isAlive()) {
-                 if (this.random.nextInt(900) == 0 && this.deathTime == 0) {
--                    this.heal(1.0F);
-+                    this.heal(1.0F, EntityRegainHealthEvent.RegainReason.REGEN); // CraftBukkit
-                 }
- 
-                 if (this.canEatGrass()) {
-@@ -720,7 +777,16 @@
+@@ -587,7 +_,7 @@
+         super.aiStep();
+         if (this.level() instanceof ServerLevel serverLevel && this.isAlive()) {
+             if (this.random.nextInt(900) == 0 && this.deathTime == 0) {
+-                this.heal(1.0F);
++                this.heal(1.0F, EntityRegainHealthEvent.RegainReason.REGEN); // CraftBukkit
              }
-         }
  
-+    }
-+
+             if (this.canEatGrass()) {
+@@ -690,6 +_,16 @@
+         }
+     }
+ 
 +    // Paper start - Horse API
 +    public void setMouthOpen(boolean open) {
 +        this.setFlag(FLAG_OPEN_MOUTH, open);
 +    }
++
 +    public boolean isMouthOpen() {
 +        return this.getFlag(FLAG_OPEN_MOUTH);
-     }
++    }
 +    // Paper end - Horse API
- 
++
      @Override
      public InteractionResult mobInteract(Player player, InteractionHand hand) {
-@@ -764,6 +830,11 @@
-         this.setFlag(16, eatingGrass);
+         if (this.isVehicle() || this.isBaby()) {
+@@ -727,6 +_,12 @@
+         this.setFlag(16, eating);
      }
  
 +    // Paper start - Horse API
@@ -159,40 +158,40 @@
 +        this.setFlag(FLAG_STANDING, standing);
 +    }
 +    // Paper end - Horse API
-     public void setStanding(boolean angry) {
-         if (angry) {
++
+     public void setStanding(boolean standing) {
+         if (standing) {
              this.setEating(false);
-@@ -883,6 +954,7 @@
+@@ -838,6 +_,7 @@
          if (this.getOwnerUUID() != null) {
-             nbt.putUUID("Owner", this.getOwnerUUID());
+             compound.putUUID("Owner", this.getOwnerUUID());
          }
-+        nbt.putInt("Bukkit.MaxDomestication", this.maxDomestication); // CraftBukkit
++        compound.putInt("Bukkit.MaxDomestication", this.maxDomestication); // CraftBukkit
  
          if (!this.inventory.getItem(0).isEmpty()) {
-             nbt.put("SaddleItem", this.inventory.getItem(0).save(this.registryAccess()));
-@@ -909,7 +981,12 @@
- 
+             compound.put("SaddleItem", this.inventory.getItem(0).save(this.registryAccess()));
+@@ -862,6 +_,11 @@
          if (uuid != null) {
              this.setOwnerUUID(uuid);
-+        }
-+        // CraftBukkit start
-+        if (nbt.contains("Bukkit.MaxDomestication")) {
-+            this.maxDomestication = nbt.getInt("Bukkit.MaxDomestication");
          }
++        // CraftBukkit start
++        if (compound.contains("Bukkit.MaxDomestication")) {
++            this.maxDomestication = compound.getInt("Bukkit.MaxDomestication");
++        }
 +        // CraftBukkit end
  
-         if (nbt.contains("SaddleItem", 10)) {
-             ItemStack itemstack = (ItemStack) ItemStack.parse(this.registryAccess(), nbt.getCompound("SaddleItem")).orElse(ItemStack.EMPTY);
-@@ -1012,6 +1089,17 @@
+         if (compound.contains("SaddleItem", 10)) {
+             ItemStack itemStack = ItemStack.parse(this.registryAccess(), compound.getCompound("SaddleItem")).orElse(ItemStack.EMPTY);
+@@ -959,6 +_,17 @@
  
      @Override
-     public void handleStartJump(int height) {
+     public void handleStartJump(int jumpPower) {
 +        // CraftBukkit start
 +        float power;
-+        if (height >= 90) {
++        if (jumpPower >= 90) {
 +            power = 1.0F;
 +        } else {
-+            power = 0.4F + 0.4F * (float) height / 90.0F;
++            power = 0.4F + 0.4F * (float) jumpPower / 90.0F;
 +        }
 +        if (!CraftEventFactory.callHorseJumpEvent(this, power)) {
 +            return;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/Llama.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/horse/Llama.java.patch
similarity index 74%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/Llama.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/animal/horse/Llama.java.patch
index df44c55d01..86290f1699 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/Llama.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/horse/Llama.java.patch
@@ -1,14 +1,14 @@
 --- a/net/minecraft/world/entity/animal/horse/Llama.java
 +++ b/net/minecraft/world/entity/animal/horse/Llama.java
-@@ -71,17 +71,23 @@
+@@ -71,17 +_,23 @@
      @Nullable
      private Llama caravanHead;
      @Nullable
 -    private Llama caravanTail;
 +    public Llama caravanTail; // Paper
  
-     public Llama(EntityType<? extends Llama> type, Level world) {
-         super(type, world);
+     public Llama(EntityType<? extends Llama> entityType, Level level) {
+         super(entityType, level);
          this.getNavigation().setRequiredPathLength(40.0F);
 +        this.maxDomestication = 30; // Paper - Missing entity API; configure max temper instead of a hardcoded value
      }
@@ -18,19 +18,19 @@
      }
  
 +    // CraftBukkit start
-+    public void setStrengthPublic(int i) {
-+        this.setStrength(i);
++    public void setStrengthPublic(int strength) {
++        this.setStrength(strength);
 +    }
 +    // CraftBukkit end
      private void setStrength(int strength) {
-         this.entityData.set(Llama.DATA_STRENGTH_ID, Math.max(1, Math.min(5, strength)));
+         this.entityData.set(DATA_STRENGTH_ID, Math.max(1, Math.min(5, strength)));
      }
-@@ -171,12 +177,12 @@
+@@ -168,12 +_,12 @@
              f = 10.0F;
              if (this.isTamed() && this.getAge() == 0 && this.canFallInLove()) {
                  flag = true;
 -                this.setInLove(player);
-+                this.setInLove(player, item.copy()); // Paper - Fix EntityBreedEvent copying
++                this.setInLove(player, stack.copy()); // Paper - Fix EntityBreedEvent copying
              }
          }
  
@@ -40,7 +40,7 @@
              flag = true;
          }
  
-@@ -289,7 +295,7 @@
+@@ -295,7 +_,7 @@
  
      @Override
      public int getMaxTemper() {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/horse/SkeletonHorse.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/horse/SkeletonHorse.java.patch
new file mode 100644
index 0000000000..cdbce96d99
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/horse/SkeletonHorse.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/animal/horse/SkeletonHorse.java
++++ b/net/minecraft/world/entity/animal/horse/SkeletonHorse.java
+@@ -122,7 +_,7 @@
+     public void aiStep() {
+         super.aiStep();
+         if (this.isTrap() && this.trapTime++ >= 18000) {
+-            this.discard();
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+         }
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/horse/SkeletonTrapGoal.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/horse/SkeletonTrapGoal.java.patch
new file mode 100644
index 0000000000..6d15d96e13
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/horse/SkeletonTrapGoal.java.patch
@@ -0,0 +1,48 @@
+--- a/net/minecraft/world/entity/animal/horse/SkeletonTrapGoal.java
++++ b/net/minecraft/world/entity/animal/horse/SkeletonTrapGoal.java
+@@ -18,6 +_,7 @@
+ 
+ public class SkeletonTrapGoal extends Goal {
+     private final SkeletonHorse horse;
++    private java.util.List<org.bukkit.entity.HumanEntity> eligiblePlayers; // Paper
+ 
+     public SkeletonTrapGoal(SkeletonHorse horse) {
+         this.horse = horse;
+@@ -25,12 +_,13 @@
+ 
+     @Override
+     public boolean canUse() {
+-        return this.horse.level().hasNearbyAlivePlayer(this.horse.getX(), this.horse.getY(), this.horse.getZ(), 10.0);
++        return !(this.eligiblePlayers = this.horse.level().findNearbyBukkitPlayers(this.horse.getX(), this.horse.getY(), this.horse.getZ(), 10.0, net.minecraft.world.entity.EntitySelector.PLAYER_AFFECTS_SPAWNING)).isEmpty(); // Paper - Affects Spawning API & SkeletonHorseTrapEvent
+     }
+ 
+     @Override
+     public void tick() {
+         ServerLevel serverLevel = (ServerLevel)this.horse.level();
++        if (!new com.destroystokyo.paper.event.entity.SkeletonHorseTrapEvent((org.bukkit.entity.SkeletonHorse) this.horse.getBukkitEntity(), this.eligiblePlayers).callEvent()) return; // Paper
+         DifficultyInstance currentDifficultyAt = serverLevel.getCurrentDifficultyAt(this.horse.blockPosition());
+         this.horse.setTrap(false);
+         this.horse.setTamed(true);
+@@ -39,11 +_,11 @@
+         if (lightningBolt != null) {
+             lightningBolt.moveTo(this.horse.getX(), this.horse.getY(), this.horse.getZ());
+             lightningBolt.setVisualOnly(true);
+-            serverLevel.addFreshEntity(lightningBolt);
++            serverLevel.strikeLightning(lightningBolt, org.bukkit.event.weather.LightningStrikeEvent.Cause.TRAP); // CraftBukkit
+             Skeleton skeleton = this.createSkeleton(currentDifficultyAt, this.horse);
+             if (skeleton != null) {
+                 skeleton.startRiding(this.horse);
+-                serverLevel.addFreshEntityWithPassengers(skeleton);
++                serverLevel.addFreshEntityWithPassengers(skeleton, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.TRAP); // CraftBukkit
+ 
+                 for (int i = 0; i < 3; i++) {
+                     AbstractHorse abstractHorse = this.createHorse(currentDifficultyAt);
+@@ -52,7 +_,7 @@
+                         if (skeleton1 != null) {
+                             skeleton1.startRiding(abstractHorse);
+                             abstractHorse.push(this.horse.getRandom().triangle(0.0, 1.1485), 0.0, this.horse.getRandom().triangle(0.0, 1.1485));
+-                            serverLevel.addFreshEntityWithPassengers(abstractHorse);
++                            serverLevel.addFreshEntityWithPassengers(abstractHorse, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.JOCKEY); // CraftBukkit
+                         }
+                     }
+                 }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/horse/TraderLlama.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/horse/TraderLlama.java.patch
new file mode 100644
index 0000000000..83a979d52e
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/horse/TraderLlama.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/entity/animal/horse/TraderLlama.java
++++ b/net/minecraft/world/entity/animal/horse/TraderLlama.java
+@@ -89,7 +_,7 @@
+             this.despawnDelay = this.isLeashedToWanderingTrader() ? ((WanderingTrader)this.getLeashHolder()).getDespawnDelay() - 1 : this.despawnDelay - 1;
+             if (this.despawnDelay <= 0) {
+                 this.removeLeash();
+-                this.discard();
++                this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+             }
+         }
+     }
+@@ -148,7 +_,7 @@
+ 
+         @Override
+         public void start() {
+-            this.mob.setTarget(this.ownerLastHurtBy);
++            this.mob.setTarget(this.ownerLastHurtBy, org.bukkit.event.entity.EntityTargetEvent.TargetReason.TARGET_ATTACKED_OWNER, true); // CraftBukkit
+             Entity leashHolder = this.llama.getLeashHolder();
+             if (leashHolder instanceof WanderingTrader) {
+                 this.timestamp = ((WanderingTrader)leashHolder).getLastHurtByMobTimestamp();
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/animal/sniffer/Sniffer.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/animal/sniffer/Sniffer.java.patch
new file mode 100644
index 0000000000..14f23439db
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/animal/sniffer/Sniffer.java.patch
@@ -0,0 +1,36 @@
+--- a/net/minecraft/world/entity/animal/sniffer/Sniffer.java
++++ b/net/minecraft/world/entity/animal/sniffer/Sniffer.java
+@@ -267,6 +_,13 @@
+             this.dropFromGiftLootTable(serverLevel, BuiltInLootTables.SNIFFER_DIGGING, (serverLevel1, itemStack) -> {
+                 ItemEntity itemEntity = new ItemEntity(this.level(), headBlock.getX(), headBlock.getY(), headBlock.getZ(), itemStack);
+                 itemEntity.setDefaultPickUpDelay();
++                // CraftBukkit start - handle EntityDropItemEvent
++                org.bukkit.event.entity.EntityDropItemEvent event = new org.bukkit.event.entity.EntityDropItemEvent(this.getBukkitEntity(), (org.bukkit.entity.Item) itemEntity.getBukkitEntity());
++                org.bukkit.Bukkit.getPluginManager().callEvent(event);
++                if (event.isCancelled()) {
++                    return;
++                }
++                // CraftBukkit end
+                 serverLevel1.addFreshEntity(itemEntity);
+             });
+             this.playSound(SoundEvents.SNIFFER_DROP_SEED, 1.0F, 1.0F);
+@@ -325,12 +_,17 @@
+ 
+     @Override
+     public void spawnChildFromBreeding(ServerLevel level, Animal mate) {
++        // Paper start - Add EntityFertilizeEggEvent event
++        final io.papermc.paper.event.entity.EntityFertilizeEggEvent result = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityFertilizeEggEvent(this, mate);
++        if (result.isCancelled()) return;
++        // Paper end - Add EntityFertilizeEggEvent event
+         ItemStack itemStack = new ItemStack(Items.SNIFFER_EGG);
+         ItemEntity itemEntity = new ItemEntity(level, this.position().x(), this.position().y(), this.position().z(), itemStack);
+         itemEntity.setDefaultPickUpDelay();
+-        this.finalizeSpawnChildFromBreeding(level, mate, null);
++        this.finalizeSpawnChildFromBreeding(level, mate, null, result.getExperience()); // Paper - Add EntityFertilizeEggEvent event
++        if (this.spawnAtLocation(level, itemEntity) != null) { // Paper - Call EntityDropItemEvent
+         this.playSound(SoundEvents.SNIFFER_EGG_PLOP, 1.0F, (this.random.nextFloat() - this.random.nextFloat()) * 0.2F + 0.5F);
+-        level.addFreshEntity(itemEntity);
++        } // Paper - Call EntityDropItemEvent
+     }
+ 
+     @Override
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java.patch
new file mode 100644
index 0000000000..f318988a05
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java.patch
@@ -0,0 +1,79 @@
+--- a/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java
++++ b/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java
+@@ -25,6 +_,7 @@
+     );
+     private static final EntityDataAccessor<Boolean> DATA_SHOW_BOTTOM = SynchedEntityData.defineId(EndCrystal.class, EntityDataSerializers.BOOLEAN);
+     public int time;
++    public boolean generatedByDragonFight = false; // Paper - Fix invulnerable end crystals
+ 
+     public EndCrystal(EntityType<? extends EndCrystal> entityType, Level level) {
+         super(entityType, level);
+@@ -56,9 +_,23 @@
+         if (this.level() instanceof ServerLevel) {
+             BlockPos blockPos = this.blockPosition();
+             if (((ServerLevel)this.level()).getDragonFight() != null && this.level().getBlockState(blockPos).isAir()) {
++                if (!org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(this.level(), blockPos, this).isCancelled()) { // Paper
+                 this.level().setBlockAndUpdate(blockPos, BaseFireBlock.getState(this.level(), blockPos));
+-            }
+-        }
++                } // Paper
++            }
++        }
++
++        // Paper start - Fix invulnerable end crystals
++        if (this.level().paperConfig().unsupportedSettings.fixInvulnerableEndCrystalExploit && this.generatedByDragonFight && this.isInvulnerable()) {
++            if (!java.util.Objects.equals(((ServerLevel) this.level()).uuid, this.getOriginWorld())
++                || ((ServerLevel) this.level()).getDragonFight() == null
++                || ((ServerLevel) this.level()).getDragonFight().respawnStage == null
++                || ((ServerLevel) this.level()).getDragonFight().respawnStage.ordinal() > net.minecraft.world.level.dimension.end.DragonRespawnAnimation.SUMMONING_DRAGON.ordinal()) {
++                this.setInvulnerable(false);
++                this.setBeamTarget(null);
++            }
++        }
++        // Paper end - Fix invulnerable end crystals
+     }
+ 
+     @Override
+@@ -68,6 +_,7 @@
+         }
+ 
+         compound.putBoolean("ShowBottom", this.showsBottom());
++        if (this.generatedByDragonFight) compound.putBoolean("Paper.GeneratedByDragonFight", this.generatedByDragonFight); // Paper - Fix invulnerable end crystals
+     }
+ 
+     @Override
+@@ -76,6 +_,7 @@
+         if (compound.contains("ShowBottom", 1)) {
+             this.setShowBottom(compound.getBoolean("ShowBottom"));
+         }
++        if (compound.contains("Paper.GeneratedByDragonFight", 1)) this.generatedByDragonFight = compound.getBoolean("Paper.GeneratedByDragonFight"); // Paper - Fix invulnerable end crystals
+     }
+ 
+     @Override
+@@ -96,10 +_,24 @@
+             return false;
+         } else {
+             if (!this.isRemoved()) {
+-                this.remove(Entity.RemovalReason.KILLED);
++                // CraftBukkit start - All non-living entities need this
++                if (org.bukkit.craftbukkit.event.CraftEventFactory.handleNonLivingEntityDamageEvent(this, damageSource, amount, false)) {
++                    return false;
++                }
++                // CraftBukkit end
+                 if (!damageSource.is(DamageTypeTags.IS_EXPLOSION)) {
+                     DamageSource damageSource1 = damageSource.getEntity() != null ? this.damageSources().explosion(this, damageSource.getEntity()) : null;
+-                    level.explode(this, damageSource1, null, this.getX(), this.getY(), this.getZ(), 6.0F, false, Level.ExplosionInteraction.BLOCK);
++                    // CraftBukkit start
++                    org.bukkit.event.entity.ExplosionPrimeEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callExplosionPrimeEvent(this, 6.0F, false);
++                    if (event.isCancelled()) {
++                        return false;
++                    }
++
++                    this.remove(Entity.RemovalReason.KILLED, org.bukkit.event.entity.EntityRemoveEvent.Cause.EXPLODE); // Paper - add Bukkit remove cause
++                    level.explode(this, damageSource1, null, this.getX(), this.getY(), this.getZ(), event.getRadius(), event.getFire(), Level.ExplosionInteraction.BLOCK);
++                } else {
++                    this.remove(Entity.RemovalReason.KILLED, org.bukkit.event.entity.EntityRemoveEvent.Cause.DEATH); // Paper - add Bukkit remove cause
++                    // CraftBukkit end
+                 }
+ 
+                 this.onDestroyedBy(level, damageSource);
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java.patch
new file mode 100644
index 0000000000..bb629ff9a6
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java.patch
@@ -0,0 +1,284 @@
+--- a/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java
++++ b/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java
+@@ -86,6 +_,10 @@
+     private final Node[] nodes = new Node[24];
+     private final int[] nodeAdjacency = new int[24];
+     private final BinaryHeap openSet = new BinaryHeap();
++    // Paper start
++    private final net.minecraft.world.level.Explosion explosionSource; // Paper - reusable source for CraftTNTPrimed.getSource()
++    @Nullable private BlockPos podium;
++    // Paper end
+ 
+     public EnderDragon(EntityType<? extends EnderDragon> entityType, Level level) {
+         super(EntityType.ENDER_DRAGON, level);
+@@ -101,6 +_,7 @@
+         this.setHealth(this.getMaxHealth());
+         this.noPhysics = true;
+         this.phaseManager = new EnderDragonPhaseManager(this);
++        this.explosionSource = new net.minecraft.world.level.ServerExplosion(level.getMinecraftWorld(), this, null, null, new Vec3(Double.NaN, Double.NaN, Double.NaN), Float.NaN, true, net.minecraft.world.level.Explosion.BlockInteraction.DESTROY); // Paper
+     }
+ 
+     public void setDragonFight(EndDragonFight dragonFight) {
+@@ -119,6 +_,19 @@
+         return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 200.0);
+     }
+ 
++    // Paper start - Allow changing the EnderDragon podium
++    public BlockPos getPodium() {
++        if (this.podium == null) {
++            return EndPodiumFeature.getLocation(this.getFightOrigin());
++        }
++        return this.podium;
++    }
++
++    public void setPodium(@Nullable BlockPos blockPos) {
++        this.podium = blockPos;
++    }
++    // Paper end - Allow changing the EnderDragon podium
++
+     @Override
+     public boolean isFlapping() {
+         float cos = Mth.cos(this.flapTime * (float) (Math.PI * 2));
+@@ -210,7 +_,7 @@
+                     }
+ 
+                     Vec3 flyTargetLocation = currentPhase.getFlyTargetLocation();
+-                    if (flyTargetLocation != null) {
++                    if (flyTargetLocation != null && currentPhase.getPhase() != EnderDragonPhase.HOVERING) { // CraftBukkit - Don't move when hovering
+                         double d = flyTargetLocation.x - this.getX();
+                         double d1 = flyTargetLocation.y - this.getY();
+                         double d2 = flyTargetLocation.z - this.getZ();
+@@ -369,7 +_,12 @@
+             if (this.nearestCrystal.isRemoved()) {
+                 this.nearestCrystal = null;
+             } else if (this.tickCount % 10 == 0 && this.getHealth() < this.getMaxHealth()) {
+-                this.setHealth(this.getHealth() + 1.0F);
++                // CraftBukkit start
++                org.bukkit.event.entity.EntityRegainHealthEvent event = new org.bukkit.event.entity.EntityRegainHealthEvent(this.getBukkitEntity(), 1.0F, org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.ENDER_CRYSTAL);
++                if (event.callEvent()) {
++                    this.setHealth((float) (this.getHealth() + event.getAmount()));
++                }
++                // CraftBukkit end
+             }
+         }
+ 
+@@ -400,7 +_,7 @@
+                 double d2 = entity.getX() - d;
+                 double d3 = entity.getZ() - d1;
+                 double max = Math.max(d2 * d2 + d3 * d3, 0.1);
+-                entity.push(d2 / max * 4.0, 0.2F, d3 / max * 4.0);
++                entity.push(d2 / max * 4.0, 0.2F, d3 / max * 4.0, this); // Paper - Add EntityKnockbackByEntityEvent and EntityPushedByEntityAttackEvent
+                 if (!this.phaseManager.getCurrentPhase().isSitting() && livingEntity.getLastHurtByMobTimestamp() < entity.tickCount - 2) {
+                     DamageSource damageSource = this.damageSources().mobAttack(this);
+                     entity.hurtServer(level, damageSource, 5.0F);
+@@ -433,6 +_,7 @@
+         int floor5 = Mth.floor(box.maxZ);
+         boolean flag = false;
+         boolean flag1 = false;
++        List<org.bukkit.block.Block> destroyedBlocks = new java.util.ArrayList<>(); // Paper - Create a list to hold all the destroyed blocks
+ 
+         for (int i = floor; i <= floor3; i++) {
+             for (int i1 = floor1; i1 <= floor4; i1++) {
+@@ -441,7 +_,11 @@
+                     BlockState blockState = level.getBlockState(blockPos);
+                     if (!blockState.isAir() && !blockState.is(BlockTags.DRAGON_TRANSPARENT)) {
+                         if (level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) && !blockState.is(BlockTags.DRAGON_IMMUNE)) {
+-                            flag1 = level.removeBlock(blockPos, false) || flag1;
++                            // CraftBukkit start - Add blocks to list rather than destroying them
++                            //flag1 = level.removeBlock(blockPos, false) || flag1;
++                            flag1 = true;
++                            destroyedBlocks.add(org.bukkit.craftbukkit.block.CraftBlock.at(level, blockPos));
++                            // CraftBukkit end
+                         } else {
+                             flag = true;
+                         }
+@@ -450,6 +_,58 @@
+             }
+         }
+ 
++        // CraftBukkit start - Set off an EntityExplodeEvent for the dragon exploding all these blocks
++        // SPIGOT-4882: don't fire event if nothing hit
++        if (!flag1) {
++            return flag;
++        }
++
++        org.bukkit.event.entity.EntityExplodeEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityExplodeEvent(this, destroyedBlocks, 0F, this.explosionSource.getBlockInteraction());
++        if (event.isCancelled()) {
++            // This flag literally means 'Dragon hit something hard' (Obsidian, White Stone or Bedrock) and will cause the dragon to slow down.
++            // We should consider adding an event extension for it, or perhaps returning true if the event is cancelled.
++            return flag;
++        } else if (event.getYield() == 0F) {
++            // Yield zero ==> no drops
++            for (org.bukkit.block.Block block : event.blockList()) {
++                this.level().removeBlock(new BlockPos(block.getX(), block.getY(), block.getZ()), false);
++            }
++        } else {
++            for (org.bukkit.block.Block block : event.blockList()) {
++                org.bukkit.Material blockType = block.getType();
++                if (blockType.isAir()) {
++                    continue;
++                }
++
++                org.bukkit.craftbukkit.block.CraftBlock craftBlock = ((org.bukkit.craftbukkit.block.CraftBlock) block);
++                BlockPos pos = craftBlock.getPosition();
++
++                net.minecraft.world.level.block.Block nmsBlock = craftBlock.getNMS().getBlock();
++                if (nmsBlock.dropFromExplosion(this.explosionSource)) {
++                    net.minecraft.world.level.block.entity.BlockEntity blockEntity = craftBlock.getNMS().hasBlockEntity() ? this.level().getBlockEntity(pos) : null;
++                    net.minecraft.world.level.storage.loot.LootParams.Builder builder = new net.minecraft.world.level.storage.loot.LootParams.Builder((ServerLevel) this.level())
++                        .withParameter(net.minecraft.world.level.storage.loot.parameters.LootContextParams.ORIGIN, Vec3.atCenterOf(pos))
++                        .withParameter(net.minecraft.world.level.storage.loot.parameters.LootContextParams.TOOL, net.minecraft.world.item.ItemStack.EMPTY)
++                        .withParameter(net.minecraft.world.level.storage.loot.parameters.LootContextParams.EXPLOSION_RADIUS, 1.0F / event.getYield())
++                        .withOptionalParameter(net.minecraft.world.level.storage.loot.parameters.LootContextParams.BLOCK_ENTITY, blockEntity);
++
++                    craftBlock.getNMS().getDrops(builder).forEach((stack) -> {
++                        net.minecraft.world.level.block.Block.popResource(this.level(), pos, stack);
++                    });
++                    craftBlock.getNMS().spawnAfterBreak((ServerLevel) this.level(), pos, net.minecraft.world.item.ItemStack.EMPTY, false);
++                }
++                // Paper start - TNTPrimeEvent
++                org.bukkit.block.Block tntBlock = org.bukkit.craftbukkit.block.CraftBlock.at(this.level(), pos);
++                if (!new com.destroystokyo.paper.event.block.TNTPrimeEvent(tntBlock, com.destroystokyo.paper.event.block.TNTPrimeEvent.PrimeReason.EXPLOSION, explosionSource.getIndirectSourceEntity().getBukkitEntity()).callEvent())
++                    continue;
++                // Paper end - TNTPrimeEvent
++                nmsBlock.wasExploded((ServerLevel) this.level(), pos, this.explosionSource);
++
++                this.level().removeBlock(pos, false);
++            }
++        }
++        // CraftBukkit end
++
+         if (flag1) {
+             BlockPos blockPos1 = new BlockPos(
+                 floor + this.random.nextInt(floor3 - floor + 1),
+@@ -507,7 +_,15 @@
+ 
+     @Override
+     public void kill(ServerLevel level) {
+-        this.remove(Entity.RemovalReason.KILLED);
++        // Paper start - Fire entity death event
++        this.silentDeath = true;
++        org.bukkit.event.entity.EntityDeathEvent deathEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityDeathEvent(this, this.damageSources().genericKill());
++        if (deathEvent.isCancelled()) {
++            this.silentDeath = false; // Reset to default if event was cancelled
++            return;
++        }
++        this.remove(Entity.RemovalReason.KILLED, org.bukkit.event.entity.EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause
++        // Paper end - Fire entity death event
+         this.gameEvent(GameEvent.ENTITY_DIE);
+         if (this.dragonFight != null) {
+             this.dragonFight.updateDragon(this);
+@@ -529,18 +_,41 @@
+             this.level().addParticle(ParticleTypes.EXPLOSION_EMITTER, this.getX() + f, this.getY() + 2.0 + f1, this.getZ() + f2, 0.0, 0.0, 0.0);
+         }
+ 
++        // CraftBukkit start - SPIGOT-2420: Moved up to #getExpReward method
++        /*
+         int i = 500;
+         if (this.dragonFight != null && !this.dragonFight.hasPreviouslyKilledDragon()) {
+             i = 12000;
+         }
++         */
++        int i = this.expToDrop;
++        // CraftBukkit end
+ 
+         if (this.level() instanceof ServerLevel serverLevel) {
+-            if (this.dragonDeathTime > 150 && this.dragonDeathTime % 5 == 0 && serverLevel.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) {
+-                ExperienceOrb.award(serverLevel, this.position(), Mth.floor(i * 0.08F));
++            if (this.dragonDeathTime > 150 && this.dragonDeathTime % 5 == 0) { // CraftBukkit - SPIGOT-2420: Already checked for the game rule when calculating the xp
++                ExperienceOrb.award(serverLevel, this.position(), Mth.floor(i * 0.08F), org.bukkit.entity.ExperienceOrb.SpawnReason.ENTITY_DEATH, this.lastHurtByPlayer, this); // Paper
+             }
+ 
+             if (this.dragonDeathTime == 1 && !this.isSilent()) {
+-                serverLevel.globalLevelEvent(1028, this.blockPosition(), 0);
++                // CraftBukkit start - Use relative location for far away sounds
++                // serverLevel.globalLevelEvent(1028, this.blockPosition(), 0);
++                int viewDistance = serverLevel.getCraftServer().getViewDistance() * 16;
++                for (net.minecraft.server.level.ServerPlayer player : serverLevel.getPlayersForGlobalSoundGamerule()) { // Paper - respect global sound events gamerule
++                    double deltaX = this.getX() - player.getX();
++                    double deltaZ = this.getZ() - player.getZ();
++                    double distanceSquared = Mth.square(deltaX) + Mth.square(deltaZ);
++                    final double soundRadiusSquared = serverLevel.getGlobalSoundRangeSquared(config -> config.dragonDeathSoundRadius); // Paper - respect global sound events gamerule
++                    if (!serverLevel.getGameRules().getBoolean(GameRules.RULE_GLOBAL_SOUND_EVENTS) && distanceSquared > soundRadiusSquared) continue; // Paper - respect global sound events gamerule
++                    if (distanceSquared > Mth.square(viewDistance)) {
++                        double deltaLength = Math.sqrt(distanceSquared);
++                        double relativeX = player.getX() + (deltaX / deltaLength) * viewDistance;
++                        double relativeZ = player.getZ() + (deltaZ / deltaLength) * viewDistance;
++                        player.connection.send(new net.minecraft.network.protocol.game.ClientboundLevelEventPacket(net.minecraft.world.level.block.LevelEvent.SOUND_DRAGON_DEATH, new BlockPos((int) relativeX, (int) this.getY(), (int) relativeZ), 0, true));
++                    } else {
++                        player.connection.send(new net.minecraft.network.protocol.game.ClientboundLevelEventPacket(net.minecraft.world.level.block.LevelEvent.SOUND_DRAGON_DEATH, new BlockPos((int) this.getX(), (int) this.getY(), (int) this.getZ()), 0, true));
++                    }
++                }
++                // CraftBukkit end
+             }
+         }
+ 
+@@ -553,15 +_,15 @@
+         }
+ 
+         if (this.dragonDeathTime == 200 && this.level() instanceof ServerLevel serverLevel1) {
+-            if (serverLevel1.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) {
+-                ExperienceOrb.award(serverLevel1, this.position(), Mth.floor(i * 0.2F));
++            if (true) { // Paper - SPIGOT-2420: Already checked for the game rule when calculating the xp
++                ExperienceOrb.award(serverLevel1, this.position(), Mth.floor(i * 0.2F), org.bukkit.entity.ExperienceOrb.SpawnReason.ENTITY_DEATH, this.lastHurtByPlayer, this); // Paper
+             }
+ 
+             if (this.dragonFight != null) {
+                 this.dragonFight.setDragonKilled(this);
+             }
+ 
+-            this.remove(Entity.RemovalReason.KILLED);
++            this.remove(Entity.RemovalReason.KILLED, org.bukkit.event.entity.EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause
+             this.gameEvent(GameEvent.ENTITY_DIE);
+         }
+     }
+@@ -743,6 +_,7 @@
+         super.addAdditionalSaveData(compound);
+         compound.putInt("DragonPhase", this.phaseManager.getCurrentPhase().getPhase().getId());
+         compound.putInt("DragonDeathTime", this.dragonDeathTime);
++        compound.putInt("Bukkit.expToDrop", this.expToDrop); // CraftBukkit - SPIGOT-2420: The ender dragon drops xp over time which can also happen between server starts
+     }
+ 
+     @Override
+@@ -755,6 +_,12 @@
+         if (compound.contains("DragonDeathTime")) {
+             this.dragonDeathTime = compound.getInt("DragonDeathTime");
+         }
++
++        // CraftBukkit start - SPIGOT-2420: The ender dragon drops xp over time which can also happen between server starts
++        if (compound.contains("Bukkit.expToDrop")) {
++            this.expToDrop = compound.getInt("Bukkit.expToDrop");
++        }
++        // CraftBukkit end
+     }
+ 
+     @Override
+@@ -795,7 +_,7 @@
+         EnderDragonPhase<? extends DragonPhaseInstance> phase = currentPhase.getPhase();
+         Vec3 viewVector;
+         if (phase == EnderDragonPhase.LANDING || phase == EnderDragonPhase.TAKEOFF) {
+-            BlockPos heightmapPos = this.level().getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, EndPodiumFeature.getLocation(this.fightOrigin));
++            BlockPos heightmapPos = this.level().getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, this.getPodium()); // Paper - Allow changing the EnderDragon podium
+             float max = Math.max((float)Math.sqrt(heightmapPos.distToCenterSqr(this.position())) / 4.0F, 1.0F);
+             float f = 6.0F / max;
+             float xRot = this.getXRot();
+@@ -883,4 +_,19 @@
+     protected float sanitizeScale(float scale) {
+         return 1.0F;
+     }
++
++    // CraftBukkit start - SPIGOT-2420: Special case, the ender dragon drops 12000 xp for the first kill and 500 xp for every other kill and this over time.
++    @Override
++    public int getExpReward(ServerLevel worldserver, Entity entity) {
++        // CraftBukkit - Moved from #tickDeath method
++        boolean flag = worldserver.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT);
++        int i = 500;
++
++        if (this.dragonFight != null && !this.dragonFight.hasPreviouslyKilledDragon()) {
++            i = 12000;
++        }
++
++        return flag ? i : 0;
++    }
++    // CraftBukkit end
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/phases/DragonDeathPhase.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/phases/DragonDeathPhase.java.patch
new file mode 100644
index 0000000000..3c43f0ec62
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/phases/DragonDeathPhase.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/boss/enderdragon/phases/DragonDeathPhase.java
++++ b/net/minecraft/world/entity/boss/enderdragon/phases/DragonDeathPhase.java
+@@ -34,7 +_,7 @@
+     public void doServerTick(ServerLevel level) {
+         this.time++;
+         if (this.targetLocation == null) {
+-            BlockPos heightmapPos = level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, EndPodiumFeature.getLocation(this.dragon.getFightOrigin()));
++            BlockPos heightmapPos = level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, this.dragon.getPodium()); // Paper - Allow changing the EnderDragon podium
+             this.targetLocation = Vec3.atBottomCenterOf(heightmapPos);
+         }
+ 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonHoldingPatternPhase.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/phases/DragonHoldingPatternPhase.java.patch
similarity index 55%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonHoldingPatternPhase.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/phases/DragonHoldingPatternPhase.java.patch
index f07b2b70e3..9652e080af 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonHoldingPatternPhase.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/phases/DragonHoldingPatternPhase.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/world/entity/boss/enderdragon/phases/DragonHoldingPatternPhase.java
 +++ b/net/minecraft/world/entity/boss/enderdragon/phases/DragonHoldingPatternPhase.java
-@@ -53,7 +53,7 @@
+@@ -53,7 +_,7 @@
  
-     private void findNewTarget(ServerLevel world) {
+     private void findNewTarget(ServerLevel level) {
          if (this.currentPath != null && this.currentPath.isDone()) {
--            BlockPos blockPos = world.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, EndPodiumFeature.getLocation(this.dragon.getFightOrigin()));
-+            BlockPos blockPos = world.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, this.dragon.getPodium()); // Paper - Allow changing the EnderDragon podium
+-            BlockPos heightmapPos = level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, EndPodiumFeature.getLocation(this.dragon.getFightOrigin()));
++            BlockPos heightmapPos = level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, this.dragon.getPodium()); // Paper - Allow changing the EnderDragon podium
              int i = this.dragon.getDragonFight() == null ? 0 : this.dragon.getDragonFight().getCrystalsAlive();
              if (this.dragon.getRandom().nextInt(i + 3) == 0) {
                  this.dragon.getPhaseManager().setPhase(EnderDragonPhase.LANDING_APPROACH);
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/phases/DragonLandingApproachPhase.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/phases/DragonLandingApproachPhase.java.patch
new file mode 100644
index 0000000000..bfc7cb63f7
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/phases/DragonLandingApproachPhase.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/boss/enderdragon/phases/DragonLandingApproachPhase.java
++++ b/net/minecraft/world/entity/boss/enderdragon/phases/DragonLandingApproachPhase.java
+@@ -52,7 +_,7 @@
+     private void findNewTarget(ServerLevel level) {
+         if (this.currentPath == null || this.currentPath.isDone()) {
+             int i = this.dragon.findClosestNode();
+-            BlockPos heightmapPos = level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, EndPodiumFeature.getLocation(this.dragon.getFightOrigin()));
++            BlockPos heightmapPos = level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, this.dragon.getPodium()); // Paper - Allow changing the EnderDragon podium
+             Player nearestPlayer = level.getNearestPlayer(NEAR_EGG_TARGETING, this.dragon, heightmapPos.getX(), heightmapPos.getY(), heightmapPos.getZ());
+             int i1;
+             if (nearestPlayer != null) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonLandingPhase.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/phases/DragonLandingPhase.java.patch
similarity index 70%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonLandingPhase.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/phases/DragonLandingPhase.java.patch
index 0d9035a506..dcadba5126 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonLandingPhase.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/phases/DragonLandingPhase.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/world/entity/boss/enderdragon/phases/DragonLandingPhase.java
 +++ b/net/minecraft/world/entity/boss/enderdragon/phases/DragonLandingPhase.java
-@@ -42,7 +42,7 @@
-     public void doServerTick(ServerLevel world) {
+@@ -50,7 +_,7 @@
+     public void doServerTick(ServerLevel level) {
          if (this.targetLocation == null) {
              this.targetLocation = Vec3.atBottomCenterOf(
--                world.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, EndPodiumFeature.getLocation(this.dragon.getFightOrigin()))
-+                world.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, this.dragon.getPodium()) // Paper - Allow changing the EnderDragon podium
+-                level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, EndPodiumFeature.getLocation(this.dragon.getFightOrigin()))
++                level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, this.dragon.getPodium()) // Paper - Allow changing the EnderDragon podium
              );
          }
  
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonSittingFlamingPhase.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/phases/DragonSittingFlamingPhase.java.patch
similarity index 63%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonSittingFlamingPhase.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/phases/DragonSittingFlamingPhase.java.patch
index 9477328964..61de7efb59 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonSittingFlamingPhase.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/phases/DragonSittingFlamingPhase.java.patch
@@ -1,35 +1,25 @@
 --- a/net/minecraft/world/entity/boss/enderdragon/phases/DragonSittingFlamingPhase.java
 +++ b/net/minecraft/world/entity/boss/enderdragon/phases/DragonSittingFlamingPhase.java
-@@ -10,6 +10,9 @@
- import net.minecraft.world.entity.AreaEffectCloud;
- import net.minecraft.world.entity.boss.enderdragon.EnderDragon;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public class DragonSittingFlamingPhase extends AbstractDragonSittingPhase {
- 
-@@ -86,7 +89,13 @@
+@@ -82,7 +_,13 @@
              this.flame.setDuration(200);
              this.flame.setParticle(ParticleTypes.DRAGON_BREATH);
              this.flame.addEffect(new MobEffectInstance(MobEffects.HARM));
 +            if (new com.destroystokyo.paper.event.entity.EnderDragonFlameEvent((org.bukkit.entity.EnderDragon) this.dragon.getBukkitEntity(), (org.bukkit.entity.AreaEffectCloud) this.flame.getBukkitEntity()).callEvent()) { // Paper - EnderDragon Events
-             world.addFreshEntity(this.flame);
+             level.addFreshEntity(this.flame);
 +            // Paper start - EnderDragon Events
 +            } else {
 +                this.end();
 +            }
 +            // Paper end - EnderDragon Events
          }
- 
      }
-@@ -100,7 +109,7 @@
+ 
+@@ -95,7 +_,7 @@
      @Override
      public void end() {
          if (this.flame != null) {
 -            this.flame.discard();
-+            this.flame.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
++            this.flame.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
              this.flame = null;
          }
- 
+     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonStrafePlayerPhase.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/phases/DragonStrafePlayerPhase.java.patch
similarity index 69%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonStrafePlayerPhase.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/phases/DragonStrafePlayerPhase.java.patch
index 7a5a567fd1..86f2d47cd1 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonStrafePlayerPhase.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/phases/DragonStrafePlayerPhase.java.patch
@@ -1,13 +1,13 @@
 --- a/net/minecraft/world/entity/boss/enderdragon/phases/DragonStrafePlayerPhase.java
 +++ b/net/minecraft/world/entity/boss/enderdragon/phases/DragonStrafePlayerPhase.java
-@@ -79,8 +79,11 @@
+@@ -77,8 +_,11 @@
                          }
  
-                         DragonFireball dragonFireball = new DragonFireball(world, this.dragon, vec34.normalize());
+                         DragonFireball dragonFireball = new DragonFireball(level, this.dragon, vec32.normalize());
 +                        dragonFireball.preserveMotion = true; // Paper - Fix Entity Teleportation and cancel velocity if teleported
-                         dragonFireball.moveTo(o, p, q, 0.0F, 0.0F);
-+                        if (new com.destroystokyo.paper.event.entity.EnderDragonShootFireballEvent((org.bukkit.entity.EnderDragon) dragon.getBukkitEntity(), (org.bukkit.entity.DragonFireball) dragonFireball.getBukkitEntity()).callEvent()) // Paper - EnderDragon Events
-                         world.addFreshEntity(dragonFireball);
+                         dragonFireball.moveTo(d2, d3, d4, 0.0F, 0.0F);
++                        if (new com.destroystokyo.paper.event.entity.EnderDragonShootFireballEvent((org.bukkit.entity.EnderDragon) this.dragon.getBukkitEntity(), (org.bukkit.entity.DragonFireball) dragonFireball.getBukkitEntity()).callEvent()) // Paper - EnderDragon Events
+                         level.addFreshEntity(dragonFireball);
 +                        else dragonFireball.discard(null); // Paper - EnderDragon Events
                          this.fireballCharge = 0;
                          if (this.currentPath != null) {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/phases/DragonTakeoffPhase.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/phases/DragonTakeoffPhase.java.patch
new file mode 100644
index 0000000000..d81a699daa
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/phases/DragonTakeoffPhase.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/boss/enderdragon/phases/DragonTakeoffPhase.java
++++ b/net/minecraft/world/entity/boss/enderdragon/phases/DragonTakeoffPhase.java
+@@ -24,7 +_,7 @@
+     @Override
+     public void doServerTick(ServerLevel level) {
+         if (!this.firstTick && this.currentPath != null) {
+-            BlockPos heightmapPos = level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, EndPodiumFeature.getLocation(this.dragon.getFightOrigin()));
++            BlockPos heightmapPos = level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, this.dragon.getPodium()); // Paper - Allow changing the EnderDragon podium
+             if (!heightmapPos.closerToCenterThan(this.dragon.position(), 10.0)) {
+                 this.dragon.getPhaseManager().setPhase(EnderDragonPhase.HOLDING_PATTERN);
+             }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/phases/EnderDragonPhaseManager.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/phases/EnderDragonPhaseManager.java.patch
new file mode 100644
index 0000000000..c9272090aa
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/phases/EnderDragonPhaseManager.java.patch
@@ -0,0 +1,21 @@
+--- a/net/minecraft/world/entity/boss/enderdragon/phases/EnderDragonPhaseManager.java
++++ b/net/minecraft/world/entity/boss/enderdragon/phases/EnderDragonPhaseManager.java
+@@ -23,6 +_,18 @@
+                 this.currentPhase.end();
+             }
+ 
++            // CraftBukkit start - Call EnderDragonChangePhaseEvent
++            org.bukkit.event.entity.EnderDragonChangePhaseEvent event = new org.bukkit.event.entity.EnderDragonChangePhaseEvent(
++                (org.bukkit.craftbukkit.entity.CraftEnderDragon) this.dragon.getBukkitEntity(),
++                (this.currentPhase == null) ? null : org.bukkit.craftbukkit.entity.CraftEnderDragon.getBukkitPhase(this.currentPhase.getPhase()),
++                org.bukkit.craftbukkit.entity.CraftEnderDragon.getBukkitPhase(phase)
++            );
++            if (!event.callEvent()) {
++                return;
++            }
++            phase = org.bukkit.craftbukkit.entity.CraftEnderDragon.getMinecraftPhase(event.getNewPhase());
++            // CraftBukkit end
++
+             this.currentPhase = this.getPhase((EnderDragonPhase<DragonPhaseInstance>)phase);
+             if (!this.dragon.level().isClientSide) {
+                 this.dragon.getEntityData().set(EnderDragon.DATA_PHASE, phase.getId());
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/boss/wither/WitherBoss.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/boss/wither/WitherBoss.java.patch
new file mode 100644
index 0000000000..936e924c09
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/boss/wither/WitherBoss.java.patch
@@ -0,0 +1,125 @@
+--- a/net/minecraft/world/entity/boss/wither/WitherBoss.java
++++ b/net/minecraft/world/entity/boss/wither/WitherBoss.java
+@@ -69,6 +_,7 @@
+     private final int[] nextHeadUpdate = new int[2];
+     private final int[] idleHeadUpdates = new int[2];
+     private int destroyBlocksTick;
++    private boolean canPortal = false; // Paper
+     public final ServerBossEvent bossEvent = (ServerBossEvent)new ServerBossEvent(
+             this.getDisplayName(), BossEvent.BossBarColor.PURPLE, BossEvent.BossBarOverlay.PROGRESS
+         )
+@@ -260,15 +_,40 @@
+             int i = this.getInvulnerableTicks() - 1;
+             this.bossEvent.setProgress(1.0F - i / 220.0F);
+             if (i <= 0) {
+-                level.explode(this, this.getX(), this.getEyeY(), this.getZ(), 7.0F, false, Level.ExplosionInteraction.MOB);
++                // CraftBukkit start
++                org.bukkit.event.entity.ExplosionPrimeEvent event = new org.bukkit.event.entity.ExplosionPrimeEvent(this.getBukkitEntity(), 7.0F, false);
++                level.getCraftServer().getPluginManager().callEvent(event);
++
++                if (!event.isCancelled()) {
++                    level.explode(this, this.getX(), this.getEyeY(), this.getZ(), event.getRadius(), event.getFire(), Level.ExplosionInteraction.MOB);
++                }
++                // CraftBukkit end
+                 if (!this.isSilent()) {
+-                    level.globalLevelEvent(1023, this.blockPosition(), 0);
++                    // CraftBukkit start - Use relative location for far away sounds
++                    // level.globalLevelEvent(1023, this.blockPosition(), 0);
++                    int viewDistance = level.getCraftServer().getViewDistance() * 16;
++                    for (ServerPlayer player : level.getPlayersForGlobalSoundGamerule()) { // Paper - respect global sound events gamerule
++                        double deltaX = this.getX() - player.getX();
++                        double deltaZ = this.getZ() - player.getZ();
++                        double distanceSquared = Mth.square(deltaX) + Mth.square(deltaZ);
++                        final double soundRadiusSquared = level.getGlobalSoundRangeSquared(config -> config.witherSpawnSoundRadius); // Paper - respect global sound events gamerule
++                        if (!level.getGameRules().getBoolean(GameRules.RULE_GLOBAL_SOUND_EVENTS) && distanceSquared > soundRadiusSquared) continue; // Spigot // Paper - respect global sound events gamerule
++                        if (distanceSquared > Mth.square(viewDistance)) {
++                            double deltaLength = Math.sqrt(distanceSquared);
++                            double relativeX = player.getX() + (deltaX / deltaLength) * viewDistance;
++                            double relativeZ = player.getZ() + (deltaZ / deltaLength) * viewDistance;
++                            player.connection.send(new net.minecraft.network.protocol.game.ClientboundLevelEventPacket(net.minecraft.world.level.block.LevelEvent.SOUND_WITHER_BOSS_SPAWN, new BlockPos((int) relativeX, (int) this.getY(), (int) relativeZ), 0, true));
++                        } else {
++                            player.connection.send(new net.minecraft.network.protocol.game.ClientboundLevelEventPacket(net.minecraft.world.level.block.LevelEvent.SOUND_WITHER_BOSS_SPAWN, this.blockPosition(), 0, true));
++                        }
++                    }
++                    // CraftBukkit end
+                 }
+             }
+ 
+             this.setInvulnerableTicks(i);
+             if (this.tickCount % 10 == 0) {
+-                this.heal(10.0F);
++                this.heal(10.0F, org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.WITHER_SPAWN); // CraftBukkit
+             }
+         } else {
+             super.customServerAiStep(level);
+@@ -305,6 +_,7 @@
+                         );
+                         if (!nearbyEntities.isEmpty()) {
+                             LivingEntity livingEntity1 = nearbyEntities.get(this.random.nextInt(nearbyEntities.size()));
++                            if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTargetLivingEvent(this, livingEntity1, org.bukkit.event.entity.EntityTargetEvent.TargetReason.CLOSEST_ENTITY).isCancelled()) continue; // CraftBukkit
+                             this.setAlternativeTarget(ix, livingEntity1.getId());
+                         }
+                     }
+@@ -334,6 +_,11 @@
+                     )) {
+                         BlockState blockState = level.getBlockState(blockPos);
+                         if (canDestroy(blockState)) {
++                            // CraftBukkit start
++                            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this, blockPos, blockState.getFluidState().createLegacyBlock())) { // Paper - fix wrong block state
++                                continue;
++                            }
++                            // CraftBukkit end
+                             flag = level.destroyBlock(blockPos, true, this) || flag;
+                         }
+                     }
+@@ -345,7 +_,7 @@
+             }
+ 
+             if (this.tickCount % 20 == 0) {
+-                this.heal(1.0F);
++                this.heal(1.0F, org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.REGEN); // CraftBukkit
+             }
+ 
+             this.bossEvent.setProgress(this.getHealth() / this.getMaxHealth());
+@@ -483,16 +_,16 @@
+     @Override
+     protected void dropCustomDeathLoot(ServerLevel level, DamageSource damageSource, boolean recentlyHit) {
+         super.dropCustomDeathLoot(level, damageSource, recentlyHit);
+-        ItemEntity itemEntity = this.spawnAtLocation(level, Items.NETHER_STAR);
++        ItemEntity itemEntity = this.spawnAtLocation(level, new net.minecraft.world.item.ItemStack(Items.NETHER_STAR), 0, ItemEntity::setExtendedLifetime); // Paper - Restore vanilla drops behavior; spawnAtLocation returns null so modify the item entity with a consumer
+         if (itemEntity != null) {
+-            itemEntity.setExtendedLifetime();
++            itemEntity.setExtendedLifetime(); // Paper - diff on change
+         }
+     }
+ 
+     @Override
+     public void checkDespawn() {
+         if (this.level().getDifficulty() == Difficulty.PEACEFUL && this.shouldDespawnInPeaceful()) {
+-            this.discard();
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+         } else {
+             this.noActionTime = 0;
+         }
+@@ -547,12 +_,18 @@
+ 
+     @Override
+     public boolean canUsePortal(boolean allowPassengers) {
+-        return false;
+-    }
++        return this.canPortal; // Paper
++    }
++
++    // Paper start
++    public void setCanTravelThroughPortals(boolean canPortal) {
++        this.canPortal = canPortal;
++    }
++    // Paper end
+ 
+     @Override
+     public boolean canBeAffected(MobEffectInstance potioneffect) {
+-        return !potioneffect.is(MobEffects.WITHER) && super.canBeAffected(potioneffect);
++        return (!potioneffect.is(MobEffects.WITHER) || !this.level().paperConfig().entities.mobEffects.immuneToWitherEffect.wither) && super.canBeAffected(potioneffect);
+     }
+ 
+     class WitherDoNothingGoal extends Goal {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/decoration/ArmorStand.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/decoration/ArmorStand.java.patch
new file mode 100644
index 0000000000..36711a9189
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/decoration/ArmorStand.java.patch
@@ -0,0 +1,377 @@
+--- a/net/minecraft/world/entity/decoration/ArmorStand.java
++++ b/net/minecraft/world/entity/decoration/ArmorStand.java
+@@ -86,9 +_,17 @@
+     public Rotations rightArmPose = DEFAULT_RIGHT_ARM_POSE;
+     public Rotations leftLegPose = DEFAULT_LEFT_LEG_POSE;
+     public Rotations rightLegPose = DEFAULT_RIGHT_LEG_POSE;
++    public boolean canMove = true; // Paper
++    // Paper start - Allow ArmorStands not to tick
++    public boolean canTick = true;
++    public boolean canTickSetByAPI = false;
++    private boolean noTickPoseDirty = false;
++    private boolean noTickEquipmentDirty = false;
++    // Paper end - Allow ArmorStands not to tick
+ 
+     public ArmorStand(EntityType<? extends ArmorStand> entityType, Level level) {
+         super(entityType, level);
++        if (level != null) this.canTick = level.paperConfig().entities.armorStands.tick; // Paper - Allow ArmorStands not to tick
+     }
+ 
+     public ArmorStand(Level level, double x, double y, double z) {
+@@ -100,6 +_,13 @@
+         return createLivingAttributes().add(Attributes.STEP_HEIGHT, 0.0);
+     }
+ 
++    // CraftBukkit start - SPIGOT-3607, SPIGOT-3637
++    @Override
++    public float getBukkitYaw() {
++        return this.getYRot();
++    }
++    // CraftBukkit end
++
+     @Override
+     public void refreshDimensions() {
+         double x = this.getX();
+@@ -159,14 +_,22 @@
+ 
+     @Override
+     public void setItemSlot(EquipmentSlot slot, ItemStack stack) {
++        // CraftBukkit start
++        this.setItemSlot(slot, stack, false);
++    }
++
++    @Override
++    public void setItemSlot(net.minecraft.world.entity.EquipmentSlot slot, ItemStack stack, boolean silent) {
++        // CraftBukkit end
+         this.verifyEquippedItem(stack);
+         switch (slot.getType()) {
+             case HAND:
+-                this.onEquipItem(slot, this.handItems.set(slot.getIndex(), stack), stack);
++                this.onEquipItem(slot, this.handItems.set(slot.getIndex(), stack), stack, silent); // CraftBukkit
+                 break;
+             case HUMANOID_ARMOR:
+-                this.onEquipItem(slot, this.armorItems.set(slot.getIndex(), stack), stack);
++                this.onEquipItem(slot, this.armorItems.set(slot.getIndex(), stack), stack, silent); // CraftBukkit
+         }
++        this.noTickEquipmentDirty = true; // Paper - Allow ArmorStands not to tick; Still update equipment
+     }
+ 
+     @Override
+@@ -196,6 +_,7 @@
+         }
+ 
+         compound.put("Pose", this.writePose());
++        if (this.canTickSetByAPI) compound.putBoolean("Paper.CanTickOverride", this.canTick); // Paper - Allow ArmorStands not to tick
+     }
+ 
+     @Override
+@@ -226,6 +_,12 @@
+         this.setNoBasePlate(compound.getBoolean("NoBasePlate"));
+         this.setMarker(compound.getBoolean("Marker"));
+         this.noPhysics = !this.hasPhysics();
++        // Paper start - Allow ArmorStands not to tick
++        if (compound.contains("Paper.CanTickOverride")) {
++            this.canTick = compound.getBoolean("Paper.CanTickOverride");
++            this.canTickSetByAPI = true;
++        }
++        // Paper end - Allow ArmorStands not to tick
+         CompoundTag compound2 = compound.getCompound("Pose");
+         this.readPose(compound2);
+     }
+@@ -275,7 +_,7 @@
+     }
+ 
+     @Override
+-    public boolean isPushable() {
++    public boolean isCollidable(boolean ignoreClimbing) { // Paper - Climbing should not bypass cramming gamerule
+         return false;
+     }
+ 
+@@ -285,6 +_,7 @@
+ 
+     @Override
+     protected void pushEntities() {
++        if (!this.level().paperConfig().entities.armorStands.doCollisionEntityLookups) return; // Paper - Option to prevent armor stands from doing entity lookups
+         for (Entity entity : this.level().getEntities(this, this.getBoundingBox(), RIDABLE_MINECARTS)) {
+             if (this.distanceToSqr(entity) <= 0.2) {
+                 entity.push(this);
+@@ -357,7 +_,25 @@
+             return false;
+         } else if (itemBySlot.isEmpty() && (this.disabledSlots & 1 << slot.getFilterBit(16)) != 0) {
+             return false;
+-        } else if (player.hasInfiniteMaterials() && itemBySlot.isEmpty() && !stack.isEmpty()) {
++            // CraftBukkit start
++        } else {
++            org.bukkit.inventory.ItemStack armorStandItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemBySlot);
++            org.bukkit.inventory.ItemStack playerHeldItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(stack);
++
++            org.bukkit.entity.Player player1 = (org.bukkit.entity.Player) player.getBukkitEntity();
++            org.bukkit.entity.ArmorStand self = (org.bukkit.entity.ArmorStand) this.getBukkitEntity();
++
++            org.bukkit.inventory.EquipmentSlot slot1 = org.bukkit.craftbukkit.CraftEquipmentSlot.getSlot(slot);
++            org.bukkit.inventory.EquipmentSlot hand1 = org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand);
++            org.bukkit.event.player.PlayerArmorStandManipulateEvent armorStandManipulateEvent = new org.bukkit.event.player.PlayerArmorStandManipulateEvent(player1, self, playerHeldItem, armorStandItem, slot1, hand1);
++            this.level().getCraftServer().getPluginManager().callEvent(armorStandManipulateEvent);
++
++            if (armorStandManipulateEvent.isCancelled()) {
++                return true;
++            }
++
++        if (player.hasInfiniteMaterials() && itemBySlot.isEmpty() && !stack.isEmpty()) {
++            // CraftBukkit end
+             this.setItemSlot(slot, stack.copyWithCount(1));
+             return true;
+         } else if (stack.isEmpty() || stack.getCount() <= 1) {
+@@ -370,6 +_,7 @@
+             this.setItemSlot(slot, stack.split(1));
+             return true;
+         }
++        } // CraftBukkit
+     }
+ 
+     @Override
+@@ -379,15 +_,32 @@
+         } else if (!level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) && damageSource.getEntity() instanceof Mob) {
+             return false;
+         } else if (damageSource.is(DamageTypeTags.BYPASSES_INVULNERABILITY)) {
+-            this.kill(level);
++            // CraftBukkit start
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.handleNonLivingEntityDamageEvent(this, damageSource, amount)) {
++                return false;
++            }
++            this.kill(level, damageSource); // CraftBukkit
++            // CraftBukkit end
+             return false;
+-        } else if (this.isInvulnerableTo(level, damageSource) || this.invisible || this.isMarker()) {
++        } else if (this.isInvulnerableTo(level, damageSource) /*|| this.invisible*/ || this.isMarker()) { // CraftBukkit
+             return false;
+         } else if (damageSource.is(DamageTypeTags.IS_EXPLOSION)) {
+-            this.brokenByAnything(level, damageSource);
+-            this.kill(level);
++            // CraftBukkit start
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.handleNonLivingEntityDamageEvent(this, damageSource, amount, true, this.invisible)) {
++                return false;
++            }
++            // CraftBukkit end
++            // Paper start - avoid duplicate event call
++            org.bukkit.event.entity.EntityDeathEvent event = this.brokenByAnything(level, damageSource);
++            if (!event.isCancelled()) this.kill(level, damageSource, false); // CraftBukkit
++            // Paper end
+             return false;
+         } else if (damageSource.is(DamageTypeTags.IGNITES_ARMOR_STANDS)) {
++            // CraftBukkit start
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.handleNonLivingEntityDamageEvent(this, damageSource, amount, true, this.invisible)) {
++                return false;
++            }
++            // CraftBukkit end
+             if (this.isOnFire()) {
+                 this.causeDamage(level, damageSource, 0.15F);
+             } else {
+@@ -396,9 +_,19 @@
+ 
+             return false;
+         } else if (damageSource.is(DamageTypeTags.BURNS_ARMOR_STANDS) && this.getHealth() > 0.5F) {
++            // CraftBukkit start
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.handleNonLivingEntityDamageEvent(this, damageSource, amount, true, this.invisible)) {
++                return false;
++            }
++            // CraftBukkit end
+             this.causeDamage(level, damageSource, 4.0F);
+             return false;
+         } else {
++            // CraftBukkit start
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.handleNonLivingEntityDamageEvent(this, damageSource, amount, true, this.invisible)) {
++                return false;
++            }
++            // CraftBukkit end
+             boolean isCanBreakArmorStand = damageSource.is(DamageTypeTags.CAN_BREAK_ARMOR_STAND);
+             boolean isAlwaysKillsArmorStands = damageSource.is(DamageTypeTags.ALWAYS_KILLS_ARMOR_STANDS);
+             if (!isCanBreakArmorStand && !isAlwaysKillsArmorStands) {
+@@ -408,7 +_,7 @@
+             } else if (damageSource.isCreativePlayer()) {
+                 this.playBrokenSound();
+                 this.showBreakingParticles();
+-                this.kill(level);
++                this.kill(level, damageSource); // CraftBukkit
+                 return true;
+             } else {
+                 long gameTime = level.getGameTime();
+@@ -417,9 +_,9 @@
+                     this.gameEvent(GameEvent.ENTITY_DAMAGE, damageSource.getEntity());
+                     this.lastHit = gameTime;
+                 } else {
+-                    this.brokenByPlayer(level, damageSource);
++                    org.bukkit.event.entity.EntityDeathEvent event = this.brokenByPlayer(level, damageSource); // Paper
+                     this.showBreakingParticles();
+-                    this.kill(level);
++                    if (!event.isCancelled()) this.kill(level, damageSource, false); // Paper - we still need to kill to follow vanilla logic (emit the game event etc...)
+                 }
+ 
+                 return true;
+@@ -472,28 +_,31 @@
+         health -= damageAmount;
+         if (health <= 0.5F) {
+             this.brokenByAnything(level, damageSource);
+-            this.kill(level);
++            // Paper start - avoid duplicate event call
++            org.bukkit.event.entity.EntityDeathEvent event = this.brokenByAnything(level, damageSource);
++            if (!event.isCancelled()) this.kill(level, damageSource, false); // CraftBukkit
++            // Paper end
+         } else {
+             this.setHealth(health);
+             this.gameEvent(GameEvent.ENTITY_DAMAGE, damageSource.getEntity());
+         }
+     }
+ 
+-    private void brokenByPlayer(ServerLevel level, DamageSource damageSource) {
++    private org.bukkit.event.entity.EntityDeathEvent brokenByPlayer(ServerLevel level, DamageSource damageSource) { // Paper
+         ItemStack itemStack = new ItemStack(Items.ARMOR_STAND);
+         itemStack.set(DataComponents.CUSTOM_NAME, this.getCustomName());
+-        Block.popResource(this.level(), this.blockPosition(), itemStack);
+-        this.brokenByAnything(level, damageSource);
++        this.drops.add(new DefaultDrop(itemStack, stack -> Block.popResource(this.level(), this.blockPosition(), stack))); // CraftBukkit - add to drops // Paper - Restore vanilla drops behavior
++        return this.brokenByAnything(level, damageSource); // Paper
+     }
+ 
+-    private void brokenByAnything(ServerLevel level, DamageSource damageSource) {
++    private org.bukkit.event.entity.EntityDeathEvent brokenByAnything(ServerLevel level, DamageSource damageSource) { // Paper
+         this.playBrokenSound();
+-        this.dropAllDeathLoot(level, damageSource);
++        // this.dropAllDeathLoot(level, damageSource); // CraftBukkit - moved down
+ 
+         for (int i = 0; i < this.handItems.size(); i++) {
+             ItemStack itemStack = this.handItems.get(i);
+             if (!itemStack.isEmpty()) {
+-                Block.popResource(this.level(), this.blockPosition().above(), itemStack);
++                this.drops.add(new DefaultDrop(itemStack, stack -> Block.popResource(this.level(), this.blockPosition().above(), stack))); // CraftBukkit - add to drops // Paper - Restore vanilla drops behavior; mirror so we can destroy it later - though this call site was safe & spawn drops correctly
+                 this.handItems.set(i, ItemStack.EMPTY);
+             }
+         }
+@@ -501,10 +_,11 @@
+         for (int ix = 0; ix < this.armorItems.size(); ix++) {
+             ItemStack itemStack = this.armorItems.get(ix);
+             if (!itemStack.isEmpty()) {
+-                Block.popResource(this.level(), this.blockPosition().above(), itemStack);
++                this.drops.add(new DefaultDrop(itemStack, stack -> Block.popResource(this.level(), this.blockPosition().above(), stack))); // CraftBukkit - add to drops // Paper - Restore vanilla drops behavior; mirror so we can destroy it later - though this call site was safe & spawn drops correctly
+                 this.armorItems.set(ix, ItemStack.EMPTY);
+             }
+         }
++        return this.dropAllDeathLoot(level, damageSource); // CraftBukkit - moved from above // Paper
+     }
+ 
+     private void playBrokenSound() {
+@@ -539,7 +_,28 @@
+ 
+     @Override
+     public void tick() {
++        // Paper start - Allow ArmorStands not to tick
++        if (!this.canTick) {
++            if (this.noTickPoseDirty) {
++                this.noTickPoseDirty = false;
++                this.updatePose();
++            }
++
++            if (this.noTickEquipmentDirty) {
++                this.noTickEquipmentDirty = false;
++                this.detectEquipmentUpdatesPublic();
++            }
++
++            return;
++        }
++        // Paper end - Allow ArmorStands not to tick
+         super.tick();
++        // Paper start - Allow ArmorStands not to tick
++        this.updatePose();
++    }
++
++    public void updatePose() {
++        // Paper end - Allow ArmorStands not to tick
+         Rotations rotations = this.entityData.get(DATA_HEAD_POSE);
+         if (!this.headPose.equals(rotations)) {
+             this.setHeadPose(rotations);
+@@ -587,9 +_,32 @@
+         return this.isSmall();
+     }
+ 
++    // CraftBukkit start
++     @Override
++    public boolean shouldDropExperience() {
++        return true; // MC-157395, SPIGOT-5193 even baby (small) armor stands should drop
++    }
++    // CraftBukkit end
++
+     @Override
+     public void kill(ServerLevel level) {
+-        this.remove(Entity.RemovalReason.KILLED);
++        // CraftBukkit start - pass DamageSource for kill
++        this.kill(level, null);
++    }
++
++    public void kill(ServerLevel level, @Nullable DamageSource damageSource) {
++        // Paper start - make cancellable
++        this.kill(level, damageSource, true);
++    }
++
++    public void kill(ServerLevel level, @Nullable DamageSource damageSource, boolean callEvent) {
++        if (callEvent) {
++            org.bukkit.event.entity.EntityDeathEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityDeathEvent(this, (damageSource == null ? this.damageSources().genericKill() : damageSource), this.drops); // CraftBukkit - call event
++            if (event.isCancelled()) return;
++        }
++        // Paper end
++        this.remove(Entity.RemovalReason.KILLED, org.bukkit.event.entity.EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause
++        // CraftBukkit end
+         this.gameEvent(GameEvent.ENTITY_DIE);
+     }
+ 
+@@ -653,31 +_,37 @@
+     public void setHeadPose(Rotations headPose) {
+         this.headPose = headPose;
+         this.entityData.set(DATA_HEAD_POSE, headPose);
++        this.noTickPoseDirty = true; // Paper - Allow updates when not ticking
+     }
+ 
+     public void setBodyPose(Rotations bodyPose) {
+         this.bodyPose = bodyPose;
+         this.entityData.set(DATA_BODY_POSE, bodyPose);
++        this.noTickPoseDirty = true; // Paper - Allow updates when not ticking
+     }
+ 
+     public void setLeftArmPose(Rotations leftArmPose) {
+         this.leftArmPose = leftArmPose;
+         this.entityData.set(DATA_LEFT_ARM_POSE, leftArmPose);
++        this.noTickPoseDirty = true; // Paper - Allow updates when not ticking
+     }
+ 
+     public void setRightArmPose(Rotations rightArmPose) {
+         this.rightArmPose = rightArmPose;
+         this.entityData.set(DATA_RIGHT_ARM_POSE, rightArmPose);
++        this.noTickPoseDirty = true; // Paper - Allow updates when not ticking
+     }
+ 
+     public void setLeftLegPose(Rotations leftLegPose) {
+         this.leftLegPose = leftLegPose;
+         this.entityData.set(DATA_LEFT_LEG_POSE, leftLegPose);
++        this.noTickPoseDirty = true; // Paper - Allow updates when not ticking
+     }
+ 
+     public void setRightLegPose(Rotations rightLegPose) {
+         this.rightLegPose = rightLegPose;
+         this.entityData.set(DATA_RIGHT_LEG_POSE, rightLegPose);
++        this.noTickPoseDirty = true; // Paper - Allow updates when not ticking
+     }
+ 
+     public Rotations getHeadPose() {
+@@ -809,4 +_,13 @@
+     public boolean canBeSeenByAnyone() {
+         return !this.isInvisible() && !this.isMarker();
+     }
++
++    // Paper start
++    @Override
++    public void move(net.minecraft.world.entity.MoverType type, Vec3 movement) {
++        if (this.canMove) {
++            super.move(type, movement);
++        }
++    }
++    // Paper end
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/decoration/BlockAttachedEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/decoration/BlockAttachedEntity.java.patch
new file mode 100644
index 0000000000..55a016fcd9
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/decoration/BlockAttachedEntity.java.patch
@@ -0,0 +1,111 @@
+--- a/net/minecraft/world/entity/decoration/BlockAttachedEntity.java
++++ b/net/minecraft/world/entity/decoration/BlockAttachedEntity.java
+@@ -20,7 +_,7 @@
+ 
+ public abstract class BlockAttachedEntity extends Entity {
+     private static final Logger LOGGER = LogUtils.getLogger();
+-    private int checkInterval;
++    private int checkInterval; { this.checkInterval = this.getId() % this.level().spigotConfig.hangingTickFrequency; } // Paper - Perf: offset item frame ticking
+     protected BlockPos pos;
+ 
+     protected BlockAttachedEntity(EntityType<? extends BlockAttachedEntity> entityType, Level level) {
+@@ -38,10 +_,26 @@
+     public void tick() {
+         if (this.level() instanceof ServerLevel serverLevel) {
+             this.checkBelowWorld();
+-            if (this.checkInterval++ == 100) {
++            if (this.checkInterval++ == this.level().spigotConfig.hangingTickFrequency) { // Spigot
+                 this.checkInterval = 0;
+                 if (!this.isRemoved() && !this.survives()) {
+-                    this.discard();
++                    // CraftBukkit start - fire break events
++                    final org.bukkit.event.hanging.HangingBreakEvent.RemoveCause cause;
++                    if (!this.level().getBlockState(this.blockPosition()).isAir()) {
++                        // TODO: This feels insufficient to catch 100% of suffocation cases
++                        cause = org.bukkit.event.hanging.HangingBreakEvent.RemoveCause.OBSTRUCTION;
++                    } else {
++                        cause = org.bukkit.event.hanging.HangingBreakEvent.RemoveCause.PHYSICS;
++                    }
++
++                    org.bukkit.event.hanging.HangingBreakEvent event = new org.bukkit.event.hanging.HangingBreakEvent((org.bukkit.entity.Hanging) this.getBukkitEntity(), cause);
++                    this.level().getCraftServer().getPluginManager().callEvent(event);
++
++                    if (this.isRemoved() || event.isCancelled()) {
++                        return;
++                    }
++                    // CraftBukkit end
++                    this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DROP); // CraftBukkit - add Bukkit remove cause
+                     this.dropItem(serverLevel, null);
+                 }
+             }
+@@ -74,6 +_,21 @@
+             return false;
+         } else {
+             if (!this.isRemoved()) {
++                // CraftBukkit start - fire break events
++                Entity damager = (!damageSource.isDirect() && damageSource.getEntity() != null) ? damageSource.getEntity() : damageSource.getDirectEntity(); // Paper - fix DamageSource API
++                org.bukkit.event.hanging.HangingBreakEvent event;
++                if (damager != null) {
++                    event = new org.bukkit.event.hanging.HangingBreakByEntityEvent((org.bukkit.entity.Hanging) this.getBukkitEntity(), damager.getBukkitEntity(), damageSource.is(net.minecraft.tags.DamageTypeTags.IS_EXPLOSION) ? org.bukkit.event.hanging.HangingBreakEvent.RemoveCause.EXPLOSION : org.bukkit.event.hanging.HangingBreakEvent.RemoveCause.ENTITY);
++                } else {
++                    event = new org.bukkit.event.hanging.HangingBreakEvent((org.bukkit.entity.Hanging) this.getBukkitEntity(), damageSource.is(net.minecraft.tags.DamageTypeTags.IS_EXPLOSION) ? org.bukkit.event.hanging.HangingBreakEvent.RemoveCause.EXPLOSION : org.bukkit.event.hanging.HangingBreakEvent.RemoveCause.DEFAULT);
++                }
++
++                this.level().getCraftServer().getPluginManager().callEvent(event);
++
++                if (this.isRemoved() || event.isCancelled()) {
++                    return true;
++                }
++                // CraftBukkit end
+                 this.kill(level);
+                 this.markHurt();
+                 this.dropItem(level, damageSource.getEntity());
+@@ -91,18 +_,36 @@
+     @Override
+     public void move(MoverType type, Vec3 movement) {
+         if (this.level() instanceof ServerLevel serverLevel && !this.isRemoved() && movement.lengthSqr() > 0.0) {
+-            this.kill(serverLevel);
+-            this.dropItem(serverLevel, null);
+-        }
+-    }
+-
+-    @Override
+-    public void push(double x, double y, double z) {
+-        if (this.level() instanceof ServerLevel serverLevel && !this.isRemoved() && x * x + y * y + z * z > 0.0) {
+-            this.kill(serverLevel);
+-            this.dropItem(serverLevel, null);
+-        }
+-    }
++            // CraftBukkit start - fire break events
++            // TODO - Does this need its own cause? Seems to only be triggered by pistons
++            org.bukkit.event.hanging.HangingBreakEvent event = new org.bukkit.event.hanging.HangingBreakEvent((org.bukkit.entity.Hanging) this.getBukkitEntity(), org.bukkit.event.hanging.HangingBreakEvent.RemoveCause.PHYSICS);
++            this.level().getCraftServer().getPluginManager().callEvent(event);
++
++            if (this.isRemoved() || event.isCancelled()) {
++                return;
++            }
++            // CraftBukkit end
++            this.kill(serverLevel);
++            this.dropItem(serverLevel, null);
++        }
++    }
++
++    @Override
++    public void push(double x, double y, double z, @Nullable Entity pushingEntity) { // Paper - override correct overload
++        if (false && this.level() instanceof ServerLevel serverLevel && !this.isRemoved() && x * x + y * y + z * z > 0.0) { // CraftBukkit - not needed
++            this.kill(serverLevel);
++            this.dropItem(serverLevel, null);
++        }
++    }
++
++    // CraftBukkit start - selectively save tile position
++    @Override
++    public void addAdditionalSaveData(CompoundTag nbt, boolean includeAll) {
++        if (includeAll) {
++            this.addAdditionalSaveData(nbt);
++        }
++    }
++    // CraftBukkit end
+ 
+     @Override
+     public void addAdditionalSaveData(CompoundTag tag) {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/decoration/ItemFrame.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/decoration/ItemFrame.java.patch
new file mode 100644
index 0000000000..f6480c2be0
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/decoration/ItemFrame.java.patch
@@ -0,0 +1,127 @@
+--- a/net/minecraft/world/entity/decoration/ItemFrame.java
++++ b/net/minecraft/world/entity/decoration/ItemFrame.java
+@@ -49,6 +_,7 @@
+     private static final float HEIGHT = 0.75F;
+     public float dropChance = 1.0F;
+     public boolean fixed;
++    public @Nullable MapId cachedMapId; // Paper - Perf: Cache map ids on item frames
+ 
+     public ItemFrame(EntityType<? extends ItemFrame> entityType, Level level) {
+         super(entityType, level);
+@@ -88,6 +_,12 @@
+ 
+     @Override
+     protected AABB calculateBoundingBox(BlockPos pos, Direction direction) {
++        // CraftBukkit start - break out BB calc into own method
++        return ItemFrame.calculateBoundingBoxStatic(pos, direction);
++    }
++
++    public static AABB calculateBoundingBoxStatic(BlockPos pos, Direction direction) {
++        // CraftBukkit end
+         float f = 0.46875F;
+         Vec3 vec3 = Vec3.atCenterOf(pos).relative(direction, -0.46875);
+         Direction.Axis axis = direction.getAxis();
+@@ -118,9 +_,9 @@
+     }
+ 
+     @Override
+-    public void push(double x, double y, double z) {
++    public void push(double x, double y, double z, @Nullable Entity pushingEntity) { // Paper - add push source entity param
+         if (!this.fixed) {
+-            super.push(x, y, z);
++            super.push(x, y, z, pushingEntity); // Paper - add push source entity param
+         }
+     }
+ 
+@@ -149,6 +_,18 @@
+             if (this.isInvulnerableToBase(damageSource)) {
+                 return false;
+             } else if (this.shouldDamageDropItem(damageSource)) {
++                // CraftBukkit start - fire EntityDamageEvent
++                if (org.bukkit.craftbukkit.event.CraftEventFactory.handleNonLivingEntityDamageEvent(this, damageSource, amount, false) || this.isRemoved()) {
++                    return true;
++                }
++                // CraftBukkit end
++                // Paper start - Add PlayerItemFrameChangeEvent
++                if (damageSource.getEntity() instanceof Player player) {
++                    var event = new io.papermc.paper.event.player.PlayerItemFrameChangeEvent((org.bukkit.entity.Player) player.getBukkitEntity(), (org.bukkit.entity.ItemFrame) this.getBukkitEntity(), this.getItem().asBukkitCopy(), io.papermc.paper.event.player.PlayerItemFrameChangeEvent.ItemFrameChangeAction.REMOVE);
++                    if (!event.callEvent()) return true; // return true here because you aren't cancelling the damage, just the change
++                    this.setItem(ItemStack.fromBukkitCopy(event.getItemStack()), false);
++                }
++                // Paper end - Add PlayerItemFrameChangeEvent
+                 this.dropItem(level, damageSource.getEntity(), false);
+                 this.gameEvent(GameEvent.BLOCK_CHANGE, damageSource.getEntity());
+                 this.playSound(this.getRemoveItemSound(), 1.0F, 1.0F);
+@@ -234,6 +_,14 @@
+         return this.getEntityData().get(DATA_ITEM);
+     }
+ 
++    // Paper start - Fix MC-123848 (spawn item frame drops above block)
++    @Nullable
++    @Override
++    public net.minecraft.world.entity.item.ItemEntity spawnAtLocation(ServerLevel serverLevel, ItemStack stack) {
++        return this.spawnAtLocation(serverLevel, stack, this.getDirection() == Direction.DOWN ? -0.6F : 0.0F);
++    }
++    // Paper end
++
+     @Nullable
+     public MapId getFramedMapId(ItemStack stack) {
+         return stack.get(DataComponents.MAP_ID);
+@@ -248,13 +_,19 @@
+     }
+ 
+     public void setItem(ItemStack stack, boolean updateNeighbours) {
++        // CraftBukkit start
++        this.setItem(stack, updateNeighbours, true);
++    }
++
++    public void setItem(ItemStack stack, boolean updateNeighbours, boolean playSound) {
++        // CraftBukkit end
+         if (!stack.isEmpty()) {
+             stack = stack.copyWithCount(1);
+         }
+ 
+         this.onItemChanged(stack);
+         this.getEntityData().set(DATA_ITEM, stack);
+-        if (!stack.isEmpty()) {
++        if (!stack.isEmpty() && updateNeighbours && playSound) { // CraftBukkit // Paper - only play sound when update flag is set
+             this.playSound(this.getAddItemSound(), 1.0F, 1.0F);
+         }
+ 
+@@ -280,6 +_,7 @@
+     }
+ 
+     private void onItemChanged(ItemStack item) {
++        this.cachedMapId = item.getComponents().get(net.minecraft.core.component.DataComponents.MAP_ID); // Paper - Perf: Cache map ids on item frames
+         if (!item.isEmpty() && item.getFrame() != this) {
+             item.setEntityRepresentation(this);
+         }
+@@ -359,7 +_,13 @@
+                     if (savedData != null && savedData.isTrackedCountOverLimit(256)) {
+                         return InteractionResult.FAIL;
+                     } else {
+-                        this.setItem(itemInHand);
++                        // Paper start - Add PlayerItemFrameChangeEvent
++                        io.papermc.paper.event.player.PlayerItemFrameChangeEvent event = new io.papermc.paper.event.player.PlayerItemFrameChangeEvent((org.bukkit.entity.Player) player.getBukkitEntity(), (org.bukkit.entity.ItemFrame) this.getBukkitEntity(), itemInHand.asBukkitCopy(), io.papermc.paper.event.player.PlayerItemFrameChangeEvent.ItemFrameChangeAction.PLACE);
++                        if (!event.callEvent()) {
++                            return InteractionResult.FAIL;
++                        }
++                        this.setItem(ItemStack.fromBukkitCopy(event.getItemStack()));
++                        // Paper end - Add PlayerItemFrameChangeEvent
+                         this.gameEvent(GameEvent.BLOCK_CHANGE, player);
+                         itemInHand.consume(1, player);
+                         return InteractionResult.SUCCESS;
+@@ -368,6 +_,13 @@
+                     return InteractionResult.PASS;
+                 }
+             } else {
++                // Paper start - Add PlayerItemFrameChangeEvent
++                io.papermc.paper.event.player.PlayerItemFrameChangeEvent event = new io.papermc.paper.event.player.PlayerItemFrameChangeEvent((org.bukkit.entity.Player) player.getBukkitEntity(), (org.bukkit.entity.ItemFrame) this.getBukkitEntity(), this.getItem().asBukkitCopy(), io.papermc.paper.event.player.PlayerItemFrameChangeEvent.ItemFrameChangeAction.ROTATE);
++                if (!event.callEvent()) {
++                    return InteractionResult.FAIL;
++                }
++                setItem(ItemStack.fromBukkitCopy(event.getItemStack()), false, false);
++                // Paper end - Add PlayerItemFrameChangeEvent
+                 this.playSound(this.getRotateItemSound(), 1.0F, 1.0F);
+                 this.setRotation(this.getRotation() + 1);
+                 this.gameEvent(GameEvent.BLOCK_CHANGE, player);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/decoration/LeashFenceKnotEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/decoration/LeashFenceKnotEntity.java.patch
similarity index 59%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/decoration/LeashFenceKnotEntity.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/decoration/LeashFenceKnotEntity.java.patch
index c569c75fde..56d3dea16c 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/decoration/LeashFenceKnotEntity.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/decoration/LeashFenceKnotEntity.java.patch
@@ -1,35 +1,13 @@
 --- a/net/minecraft/world/entity/decoration/LeashFenceKnotEntity.java
 +++ b/net/minecraft/world/entity/decoration/LeashFenceKnotEntity.java
-@@ -8,9 +8,11 @@
- import net.minecraft.network.protocol.Packet;
- import net.minecraft.network.protocol.game.ClientGamePacketListener;
- import net.minecraft.network.protocol.game.ClientboundAddEntityPacket;
-+import net.minecraft.network.protocol.game.ClientboundSetEntityLinkPacket;
- import net.minecraft.network.syncher.SynchedEntityData;
- import net.minecraft.server.level.ServerEntity;
- import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
- import net.minecraft.sounds.SoundEvents;
- import net.minecraft.tags.BlockTags;
- import net.minecraft.world.InteractionHand;
-@@ -26,6 +28,9 @@
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.phys.AABB;
- import net.minecraft.world.phys.Vec3;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public class LeashFenceKnotEntity extends BlockAttachedEntity {
- 
-@@ -85,6 +90,15 @@
-                 Leashable leashable = (Leashable) iterator.next();
+@@ -81,6 +_,15 @@
  
+             for (Leashable leashable : list) {
                  if (leashable.getLeashHolder() == player) {
 +                    // CraftBukkit start
 +                    if (leashable instanceof Entity leashed) {
-+                        if (CraftEventFactory.callPlayerLeashEntityEvent(leashed, this, player, hand).isCancelled()) {
-+                            ((ServerPlayer) player).connection.send(new ClientboundSetEntityLinkPacket(leashed, leashable.getLeashHolder()));
++                        if (org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerLeashEntityEvent(leashed, this, player, hand).isCancelled()) {
++                            ((net.minecraft.server.level.ServerPlayer) player).connection.send(new net.minecraft.network.protocol.game.ClientboundSetEntityLinkPacket(leashed, leashable.getLeashHolder()));
 +                            flag = true; // Also set true when the event is cancelled otherwise it tries to unleash the entities
 +                            continue;
 +                        }
@@ -38,9 +16,9 @@
                      leashable.setLeashedTo(this, true);
                      flag = true;
                  }
-@@ -93,18 +107,43 @@
-             boolean flag1 = false;
+@@ -88,14 +_,39 @@
  
+             boolean flag1 = false;
              if (!flag) {
 -                this.discard();
 -                if (player.getAbilities().instabuild) {
@@ -49,18 +27,14 @@
 +                boolean die = true;
 +                // CraftBukkit end
 +                if (true || player.getAbilities().instabuild) { // CraftBukkit - Process for non-creative as well
-                     Iterator iterator1 = list.iterator();
- 
-                     while (iterator1.hasNext()) {
-                         Leashable leashable1 = (Leashable) iterator1.next();
- 
+                     for (Leashable leashable1 : list) {
                          if (leashable1.isLeashed() && leashable1.getLeashHolder() == this) {
 -                            leashable1.removeLeash();
 +                            // CraftBukkit start
 +                            boolean dropLeash = !player.hasInfiniteMaterials();
 +                            if (leashable1 instanceof Entity leashed) {
 +                                // Paper start - Expand EntityUnleashEvent
-+                                org.bukkit.event.player.PlayerUnleashEntityEvent event = CraftEventFactory.callPlayerUnleashEntityEvent(leashed, player, hand, dropLeash);
++                                org.bukkit.event.player.PlayerUnleashEntityEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerUnleashEntityEvent(leashed, player, hand, dropLeash);
 +                                dropLeash = event.isDropLeash();
 +                                if (event.isCancelled()) {
 +                                    // Paper end - Expand EntityUnleashEvent
@@ -79,7 +53,7 @@
                      }
 +                    // CraftBukkit start
 +                    if (die) {
-+                        this.discard(EntityRemoveEvent.Cause.DROP); // CraftBukkit - add Bukkit remove cause
++                        this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DROP); // CraftBukkit - add Bukkit remove cause
 +                    }
 +                    // CraftBukkit end
                  }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/decoration/Painting.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/decoration/Painting.java.patch
new file mode 100644
index 0000000000..97a17e6cd7
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/decoration/Painting.java.patch
@@ -0,0 +1,41 @@
+--- a/net/minecraft/world/entity/decoration/Painting.java
++++ b/net/minecraft/world/entity/decoration/Painting.java
+@@ -129,21 +_,31 @@
+ 
+     @Override
+     protected AABB calculateBoundingBox(BlockPos pos, Direction direction) {
++        // CraftBukkit start
++        PaintingVariant variant = (PaintingVariant) this.getVariant().value();
++        return Painting.calculateBoundingBoxStatic(pos, direction, variant.width(), variant.height());
++    }
++
++    public static AABB calculateBoundingBoxStatic(BlockPos pos, Direction direction, int width, int height) {
++        // CraftBukkit end
+         float f = 0.46875F;
+         Vec3 vec3 = Vec3.atCenterOf(pos).relative(direction, -0.46875);
+-        PaintingVariant paintingVariant = this.getVariant().value();
+-        double d = this.offsetForPaintingSize(paintingVariant.width());
+-        double d1 = this.offsetForPaintingSize(paintingVariant.height());
++        // CraftBukkit start
++        double d = Painting.offsetForPaintingSize(width);
++        double d1 = Painting.offsetForPaintingSize(height);
++        // CraftBukkit end
+         Direction counterClockWise = direction.getCounterClockWise();
+         Vec3 vec31 = vec3.relative(counterClockWise, d).relative(Direction.UP, d1);
+         Direction.Axis axis = direction.getAxis();
+-        double d2 = axis == Direction.Axis.X ? 0.0625 : paintingVariant.width();
+-        double d3 = paintingVariant.height();
+-        double d4 = axis == Direction.Axis.Z ? 0.0625 : paintingVariant.width();
++        // CraftBukkit start
++        double d2 = axis == Direction.Axis.X ? 0.0625 : width;
++        double d3 = height;
++        double d4 = axis == Direction.Axis.Z ? 0.0625 : width;
++        // CraftBukkit end
+         return AABB.ofSize(vec31, d2, d3, d4);
+     }
+ 
+-    private double offsetForPaintingSize(int size) {
++    private static double offsetForPaintingSize(int size) { // CraftBukkit - static
+         return size % 2 == 0 ? 0.5 : 0.0;
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/item/FallingBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/item/FallingBlockEntity.java.patch
new file mode 100644
index 0000000000..96b38cb9de
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/item/FallingBlockEntity.java.patch
@@ -0,0 +1,151 @@
+--- a/net/minecraft/world/entity/item/FallingBlockEntity.java
++++ b/net/minecraft/world/entity/item/FallingBlockEntity.java
+@@ -49,6 +_,11 @@
+ import net.minecraft.world.phys.Vec3;
+ import org.slf4j.Logger;
+ 
++// CraftBukkit start
++import org.bukkit.craftbukkit.event.CraftEventFactory;
++import org.bukkit.event.entity.EntityRemoveEvent;
++// CraftBukkit end
++
+ public class FallingBlockEntity extends Entity {
+     private static final Logger LOGGER = LogUtils.getLogger();
+     public BlockState blockState = Blocks.SAND.defaultBlockState();
+@@ -62,6 +_,7 @@
+     public CompoundTag blockData;
+     public boolean forceTickAfterTeleportToDuplicate;
+     protected static final EntityDataAccessor<BlockPos> DATA_START_POS = SynchedEntityData.defineId(FallingBlockEntity.class, EntityDataSerializers.BLOCK_POS);
++    public boolean autoExpire = true; // Paper - Expand FallingBlock API
+ 
+     public FallingBlockEntity(EntityType<? extends FallingBlockEntity> entityType, Level level) {
+         super(entityType, level);
+@@ -89,6 +_,7 @@
+                 ? blockState.setValue(BlockStateProperties.WATERLOGGED, Boolean.valueOf(false))
+                 : blockState
+         );
++        if (!CraftEventFactory.callEntityChangeBlockEvent(fallingBlockEntity, pos, blockState.getFluidState().createLegacyBlock())) return fallingBlockEntity; // CraftBukkit
+         level.setBlock(pos, blockState.getFluidState().createLegacyBlock(), 3);
+         level.addFreshEntity(fallingBlockEntity);
+         return fallingBlockEntity;
+@@ -139,13 +_,22 @@
+     @Override
+     public void tick() {
+         if (this.blockState.isAir()) {
+-            this.discard();
++            this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+         } else {
+             Block block = this.blockState.getBlock();
+             this.time++;
+             this.applyGravity();
+             this.move(MoverType.SELF, this.getDeltaMovement());
+             this.applyEffectsFromBlocks();
++            // Paper start - Configurable falling blocks height nerf
++            if (this.level().paperConfig().fixes.fallingBlockHeightNerf.test(v -> this.getY() > v)) {
++                if (this.dropItem && this.level() instanceof final ServerLevel serverLevel && serverLevel.getGameRules().getBoolean(GameRules.RULE_DOENTITYDROPS)) {
++                    this.spawnAtLocation(serverLevel, block);
++                }
++                this.discard(EntityRemoveEvent.Cause.OUT_OF_WORLD);
++                return;
++            }
++            // Paper end - Configurable falling blocks height nerf
+             this.handlePortal();
+             if (this.level() instanceof ServerLevel serverLevel && (this.isAlive() || this.forceTickAfterTeleportToDuplicate)) {
+                 BlockPos blockPos = this.blockPosition();
+@@ -166,12 +_,12 @@
+                 }
+ 
+                 if (!this.onGround() && !flag1) {
+-                    if (this.time > 100 && (blockPos.getY() <= this.level().getMinY() || blockPos.getY() > this.level().getMaxY()) || this.time > 600) {
++                    if ((this.time > 100 && autoExpire) && (blockPos.getY() <= this.level().getMinY() || blockPos.getY() > this.level().getMaxY()) || (this.time > 600 && autoExpire)) { // Paper - Expand FallingBlock API
+                         if (this.dropItem && serverLevel.getGameRules().getBoolean(GameRules.RULE_DOENTITYDROPS)) {
+                             this.spawnAtLocation(serverLevel, block);
+                         }
+ 
+-                        this.discard();
++                        this.discard(EntityRemoveEvent.Cause.DROP); // CraftBukkit - add Bukkit remove cause
+                     }
+                 } else {
+                     BlockState blockState = this.level().getBlockState(blockPos);
+@@ -189,12 +_,18 @@
+                                     this.blockState = this.blockState.setValue(BlockStateProperties.WATERLOGGED, Boolean.valueOf(true));
+                                 }
+ 
++                                // CraftBukkit start
++                                if (!CraftEventFactory.callEntityChangeBlockEvent(this, blockPos, this.blockState)) {
++                                    this.discard(EntityRemoveEvent.Cause.DESPAWN); // SPIGOT-6586 called before the event in previous versions
++                                    return;
++                                }
++                                // CraftBukkit end
+                                 if (this.level().setBlock(blockPos, this.blockState, 3)) {
+                                     ((ServerLevel)this.level())
+                                         .getChunkSource()
+                                         .chunkMap
+                                         .broadcast(this, new ClientboundBlockUpdatePacket(blockPos, this.level().getBlockState(blockPos)));
+-                                    this.discard();
++                                    this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+                                     if (block instanceof Fallable) {
+                                         ((Fallable)block).onLand(this.level(), blockPos, this.blockState, blockState, this);
+                                     }
+@@ -218,19 +_,19 @@
+                                         }
+                                     }
+                                 } else if (this.dropItem && serverLevel.getGameRules().getBoolean(GameRules.RULE_DOENTITYDROPS)) {
+-                                    this.discard();
++                                    this.discard(EntityRemoveEvent.Cause.DROP); // CraftBukkit - add Bukkit remove cause
+                                     this.callOnBrokenAfterFall(block, blockPos);
+                                     this.spawnAtLocation(serverLevel, block);
+                                 }
+                             } else {
+-                                this.discard();
++                                this.discard(EntityRemoveEvent.Cause.DROP); // CraftBukkit - add Bukkit remove cause
+                                 if (this.dropItem && serverLevel.getGameRules().getBoolean(GameRules.RULE_DOENTITYDROPS)) {
+                                     this.callOnBrokenAfterFall(block, blockPos);
+                                     this.spawnAtLocation(serverLevel, block);
+                                 }
+                             }
+                         } else {
+-                            this.discard();
++                            this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+                             this.callOnBrokenAfterFall(block, blockPos);
+                         }
+                     }
+@@ -290,6 +_,7 @@
+         }
+ 
+         compound.putBoolean("CancelDrop", this.cancelDrop);
++        if (!this.autoExpire) compound.putBoolean("Paper.AutoExpire", false); // Paper - Expand FallingBlock API
+     }
+ 
+     @Override
+@@ -308,7 +_,7 @@
+             this.dropItem = compound.getBoolean("DropItem");
+         }
+ 
+-        if (compound.contains("TileEntityData", 10)) {
++        if (compound.contains("TileEntityData", 10) && !(this.level().paperConfig().entities.spawning.filterBadTileEntityNbtFromFallingBlocks && this.blockState.getBlock() instanceof net.minecraft.world.level.block.GameMasterBlock)) { // Paper - Filter bad block entity nbt data from falling blocks
+             this.blockData = compound.getCompound("TileEntityData").copy();
+         }
+ 
+@@ -316,6 +_,12 @@
+         if (this.blockState.isAir()) {
+             this.blockState = Blocks.SAND.defaultBlockState();
+         }
++
++        // Paper start - Expand FallingBlock API
++        if (compound.contains("Paper.AutoExpire")) {
++            this.autoExpire = compound.getBoolean("Paper.AutoExpire");
++        }
++        // Paper end - Expand FallingBlock API
+     }
+ 
+     public void setHurtsEntities(float fallDamagePerDistance, int fallDamageMax) {
+@@ -372,7 +_,7 @@
+         ResourceKey<Level> resourceKey1 = this.level().dimension();
+         boolean flag = (resourceKey1 == Level.END || resourceKey == Level.END) && resourceKey1 != resourceKey;
+         Entity entity = super.teleport(teleportTransition);
+-        this.forceTickAfterTeleportToDuplicate = entity != null && flag;
++        this.forceTickAfterTeleportToDuplicate = entity != null && flag && io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowUnsafeEndPortalTeleportation; // Paper
+         return entity;
+     }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/item/ItemEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/item/ItemEntity.java.patch
new file mode 100644
index 0000000000..22c2ed163b
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/item/ItemEntity.java.patch
@@ -0,0 +1,269 @@
+--- a/net/minecraft/world/entity/item/ItemEntity.java
++++ b/net/minecraft/world/entity/item/ItemEntity.java
+@@ -49,6 +_,9 @@
+     @Nullable
+     public UUID target;
+     public final float bobOffs;
++    public boolean canMobPickup = true; // Paper - Item#canEntityPickup
++    private int despawnRate = -1; // Paper - Alternative item-despawn-rate
++    public net.kyori.adventure.util.TriState frictionState = net.kyori.adventure.util.TriState.NOT_SET; // Paper - Friction API
+ 
+     public ItemEntity(EntityType<? extends ItemEntity> entityType, Level level) {
+         super(entityType, level);
+@@ -57,7 +_,12 @@
+     }
+ 
+     public ItemEntity(Level level, double posX, double posY, double posZ, ItemStack itemStack) {
+-        this(level, posX, posY, posZ, itemStack, level.random.nextDouble() * 0.2 - 0.1, 0.2, level.random.nextDouble() * 0.2 - 0.1);
++        // Paper start - Don't use level random in entity constructors (to make them thread-safe)
++        this(EntityType.ITEM, level);
++        this.setPos(posX, posY, posZ);
++        this.setDeltaMovement(this.random.nextDouble() * 0.2 - 0.1, 0.2, this.random.nextDouble() * 0.2 - 0.1);
++        this.setItem(itemStack);
++        // Paper end - Don't use level random in entity constructors
+     }
+ 
+     public ItemEntity(Level level, double posX, double posY, double posZ, ItemStack itemStack, double deltaX, double deltaY, double deltaZ) {
+@@ -119,7 +_,7 @@
+     @Override
+     public void tick() {
+         if (this.getItem().isEmpty()) {
+-            this.discard();
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+         } else {
+             super.tick();
+             if (this.pickupDelay > 0 && this.pickupDelay != 32767) {
+@@ -147,11 +_,15 @@
+                 }
+             }
+ 
+-            if (!this.onGround() || this.getDeltaMovement().horizontalDistanceSqr() > 1.0E-5F || (this.tickCount + this.getId()) % 4 == 0) {
++            if (!this.onGround() || this.getDeltaMovement().horizontalDistanceSqr() > 1.0E-5F || (this.tickCount + this.getId()) % 4 == 0) { // Paper - Diff on change; ActivationRange immunity
+                 this.move(MoverType.SELF, this.getDeltaMovement());
+                 this.applyEffectsFromBlocks();
+                 float f = 0.98F;
+-                if (this.onGround()) {
++                // Paper start - Friction API
++                if (frictionState == net.kyori.adventure.util.TriState.FALSE) {
++                    f = 1F;
++                } else if (this.onGround()) {
++                    // Paper end - Friction API
+                     f = this.level().getBlockState(this.getBlockPosBelowThatAffectsMyMovement()).getBlock().getFriction() * 0.98F;
+                 }
+ 
+@@ -184,8 +_,14 @@
+                 }
+             }
+ 
+-            if (!this.level().isClientSide && this.age >= 6000) {
+-                this.discard();
++            if (!this.level().isClientSide && this.age >= this.despawnRate) { // Spigot // Paper - Alternative item-despawn-rate
++                // CraftBukkit start - fire ItemDespawnEvent
++                if (org.bukkit.craftbukkit.event.CraftEventFactory.callItemDespawnEvent(this).isCancelled()) {
++                    this.age = 0;
++                    return;
++                }
++                // CraftBukkit end
++                this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+             }
+         }
+     }
+@@ -210,9 +_,18 @@
+ 
+     private void mergeWithNeighbours() {
+         if (this.isMergable()) {
++            double radius = this.level().spigotConfig.itemMerge; // Spigot
+             for (ItemEntity itemEntity : this.level()
+-                .getEntitiesOfClass(ItemEntity.class, this.getBoundingBox().inflate(0.5, 0.0, 0.5), neighbour -> neighbour != this && neighbour.isMergable())) {
++                .getEntitiesOfClass(ItemEntity.class, this.getBoundingBox().inflate(radius, this.level().paperConfig().entities.behavior.onlyMergeItemsHorizontally ? 0 : radius - 0.5D, radius), neighbour -> neighbour != this && neighbour.isMergable())) { // Spigot // Paper - configuration to only merge items horizontally
+                 if (itemEntity.isMergable()) {
++                    // Paper start - Fix items merging through walls
++                    if (this.level().paperConfig().fixes.fixItemsMergingThroughWalls) {
++                        if (this.level().clipDirect(this.position(), itemEntity.position(),
++                            net.minecraft.world.phys.shapes.CollisionContext.of(this)) == net.minecraft.world.phys.HitResult.Type.BLOCK) {
++                            continue;
++                        }
++                    }
++                    // Paper end - Fix items merging through walls
+                     this.tryToMerge(itemEntity);
+                     if (this.isRemoved()) {
+                         break;
+@@ -224,14 +_,14 @@
+ 
+     private boolean isMergable() {
+         ItemStack item = this.getItem();
+-        return this.isAlive() && this.pickupDelay != 32767 && this.age != -32768 && this.age < 6000 && item.getCount() < item.getMaxStackSize();
++        return this.isAlive() && this.pickupDelay != 32767 && this.age != -32768 && this.age < this.despawnRate && item.getCount() < item.getMaxStackSize(); // Paper - Alternative item-despawn-rate
+     }
+ 
+     private void tryToMerge(ItemEntity itemEntity) {
+         ItemStack item = this.getItem();
+         ItemStack item1 = itemEntity.getItem();
+         if (Objects.equals(this.target, itemEntity.target) && areMergable(item, item1)) {
+-            if (item1.getCount() < item.getCount()) {
++            if (true || item1.getCount() < item.getCount()) { // Spigot
+                 merge(this, item, itemEntity, item1);
+             } else {
+                 merge(itemEntity, item1, this, item);
+@@ -257,11 +_,16 @@
+     }
+ 
+     private static void merge(ItemEntity destinationEntity, ItemStack destinationStack, ItemEntity originEntity, ItemStack originStack) {
++        // CraftBukkit start
++        if (!org.bukkit.craftbukkit.event.CraftEventFactory.callItemMergeEvent(originEntity, destinationEntity)) {
++            return;
++        }
++        // CraftBukkit end
+         merge(destinationEntity, destinationStack, originStack);
+         destinationEntity.pickupDelay = Math.max(destinationEntity.pickupDelay, originEntity.pickupDelay);
+         destinationEntity.age = Math.min(destinationEntity.age, originEntity.age);
+         if (originStack.isEmpty()) {
+-            originEntity.discard();
++            originEntity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.MERGE); // CraftBukkit - add Bukkit remove cause
+         }
+     }
+ 
+@@ -289,12 +_,17 @@
+         } else if (!this.getItem().canBeHurtBy(damageSource)) {
+             return false;
+         } else {
++            // CraftBukkit start
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.handleNonLivingEntityDamageEvent(this, damageSource, amount)) {
++                return false;
++            }
++            // CraftBukkit end
+             this.markHurt();
+             this.health = (int)(this.health - amount);
+             this.gameEvent(GameEvent.ENTITY_DAMAGE, damageSource.getEntity());
+             if (this.health <= 0) {
+                 this.getItem().onDestroyed(this);
+-                this.discard();
++                this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause
+             }
+ 
+             return true;
+@@ -322,6 +_,11 @@
+         if (!this.getItem().isEmpty()) {
+             compound.put("Item", this.getItem().save(this.registryAccess()));
+         }
++        // Paper start - Friction API
++        if (this.frictionState != net.kyori.adventure.util.TriState.NOT_SET) {
++            compound.putString("Paper.FrictionState", this.frictionState.toString());
++        }
++        // Paper end - Friction API
+     }
+ 
+     @Override
+@@ -347,9 +_,19 @@
+         } else {
+             this.setItem(ItemStack.EMPTY);
+         }
++        // Paper start - Friction API
++        if (compound.contains("Paper.FrictionState")) {
++            String fs = compound.getString("Paper.FrictionState");
++            try {
++                frictionState = net.kyori.adventure.util.TriState.valueOf(fs);
++            } catch (Exception ignored) {
++                com.mojang.logging.LogUtils.getLogger().error("Unknown friction state {} for {}", fs, this);
++            }
++        }
++        // Paper end - Friction API
+ 
+         if (this.getItem().isEmpty()) {
+-            this.discard();
++            this.discard(null); // CraftBukkit - add Bukkit remove cause
+         }
+     }
+ 
+@@ -359,10 +_,73 @@
+             ItemStack item = this.getItem();
+             Item item1 = item.getItem();
+             int count = item.getCount();
++            // CraftBukkit start - fire PlayerPickupItemEvent
++            int canHold = entity.getInventory().canHold(item);
++            int remaining = count - canHold;
++            boolean flyAtPlayer = false; // Paper
++
++            // Paper start - PlayerAttemptPickupItemEvent
++            if (this.pickupDelay <= 0) {
++                org.bukkit.event.player.PlayerAttemptPickupItemEvent attemptEvent = new org.bukkit.event.player.PlayerAttemptPickupItemEvent((org.bukkit.entity.Player) entity.getBukkitEntity(), (org.bukkit.entity.Item) this.getBukkitEntity(), remaining);
++                this.level().getCraftServer().getPluginManager().callEvent(attemptEvent);
++
++                flyAtPlayer = attemptEvent.getFlyAtPlayer();
++                if (attemptEvent.isCancelled()) {
++                    if (flyAtPlayer) {
++                        entity.take(this, count);
++                    }
++
++                    return;
++                }
++            }
++
++            if (this.pickupDelay <= 0 && canHold > 0) {
++                item.setCount(canHold);
++                // Call legacy event
++                org.bukkit.event.player.PlayerPickupItemEvent playerEvent = new org.bukkit.event.player.PlayerPickupItemEvent((org.bukkit.entity.Player) entity.getBukkitEntity(), (org.bukkit.entity.Item) this.getBukkitEntity(), remaining);
++                playerEvent.setCancelled(!playerEvent.getPlayer().getCanPickupItems());
++                this.level().getCraftServer().getPluginManager().callEvent(playerEvent);
++                flyAtPlayer = playerEvent.getFlyAtPlayer(); // Paper
++                if (playerEvent.isCancelled()) {
++                    item.setCount(count); // SPIGOT-5294 - restore count
++                    // Paper start
++                    if (flyAtPlayer) {
++                        entity.take(this, count);
++                    }
++                    // Paper end
++                    return;
++                }
++
++                // Call newer event afterwards
++                org.bukkit.event.entity.EntityPickupItemEvent entityEvent = new org.bukkit.event.entity.EntityPickupItemEvent(entity.getBukkitEntity(), (org.bukkit.entity.Item) this.getBukkitEntity(), remaining);
++                entityEvent.setCancelled(!entityEvent.getEntity().getCanPickupItems());
++                this.level().getCraftServer().getPluginManager().callEvent(entityEvent);
++                if (entityEvent.isCancelled()) {
++                    item.setCount(count); // SPIGOT-5294 - restore count
++                    return;
++                }
++
++                // Update the ItemStack if it was changed in the event
++                ItemStack current = this.getItem();
++                if (!item.equals(current)) {
++                    item = current;
++                } else {
++                    item.setCount(canHold + remaining); // = i
++                }
++
++                // Possibly < 0; fix here so we do not have to modify code below
++                this.pickupDelay = 0;
++            } else if (this.pickupDelay == 0) {
++                // ensure that the code below isn't triggered if canHold says we can't pick the items up
++                this.pickupDelay = -1;
++            }
++            // CraftBukkit end
++            // Paper end - PlayerAttemptPickupItemEvent
+             if (this.pickupDelay == 0 && (this.target == null || this.target.equals(entity.getUUID())) && entity.getInventory().add(item)) {
++                if (flyAtPlayer) // Paper - PlayerPickupItemEvent
+                 entity.take(this, count);
+                 if (item.isEmpty()) {
+-                    this.discard();
++                    this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
+                     item.setCount(count);
+                 }
+ 
+@@ -400,6 +_,7 @@
+ 
+     public void setItem(ItemStack stack) {
+         this.getEntityData().set(DATA_ITEM, stack);
++        this.despawnRate = this.level().paperConfig().entities.spawning.altItemDespawnRate.enabled ? this.level().paperConfig().entities.spawning.altItemDespawnRate.items.getOrDefault(stack.getItem(), this.level().spigotConfig.itemDespawnRate) : this.level().spigotConfig.itemDespawnRate; // Paper - Alternative item-despawn-rate
+     }
+ 
+     @Override
+@@ -453,7 +_,7 @@
+ 
+     public void makeFakeItem() {
+         this.setNeverPickUp();
+-        this.age = 5999;
++        this.age = this.despawnRate - 1; // Spigot // Paper - Alternative item-despawn-rate
+     }
+ 
+     public static float getSpin(float age, float bobOffset) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/item/PrimedTnt.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/item/PrimedTnt.java.patch
similarity index 72%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/item/PrimedTnt.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/item/PrimedTnt.java.patch
index 1120fe2bdf..45ed0bc03d 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/item/PrimedTnt.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/item/PrimedTnt.java.patch
@@ -1,36 +1,36 @@
 --- a/net/minecraft/world/entity/item/PrimedTnt.java
 +++ b/net/minecraft/world/entity/item/PrimedTnt.java
-@@ -27,6 +27,12 @@
+@@ -27,6 +_,12 @@
  import net.minecraft.world.level.material.FluidState;
  import net.minecraft.world.level.portal.TeleportTransition;
  
-+// CraftBukkit start;
++// CraftBukkit start
 +import org.bukkit.craftbukkit.event.CraftEventFactory;
 +import org.bukkit.event.entity.EntityRemoveEvent;
 +import org.bukkit.event.entity.ExplosionPrimeEvent;
 +// CraftBukkit end
 +
  public class PrimedTnt extends Entity implements TraceableEntity {
- 
      private static final EntityDataAccessor<Integer> DATA_FUSE_ID = SynchedEntityData.defineId(PrimedTnt.class, EntityDataSerializers.INT);
-@@ -51,6 +57,7 @@
+     private static final EntityDataAccessor<BlockState> DATA_BLOCK_STATE_ID = SynchedEntityData.defineId(PrimedTnt.class, EntityDataSerializers.BLOCK_STATE);
+@@ -50,6 +_,7 @@
      public LivingEntity owner;
      private boolean usedPortal;
-     public float explosionPower;
+     public float explosionPower = 4.0F;
 +    public boolean isIncendiary = false; // CraftBukkit - add field
  
-     public PrimedTnt(EntityType<? extends PrimedTnt> type, Level world) {
-         super(type, world);
-@@ -61,7 +68,7 @@
-     public PrimedTnt(Level world, double x, double y, double z, @Nullable LivingEntity igniter) {
-         this(EntityType.TNT, world);
+     public PrimedTnt(EntityType<? extends PrimedTnt> entityType, Level level) {
+         super(entityType, level);
+@@ -59,7 +_,7 @@
+     public PrimedTnt(Level level, double x, double y, double z, @Nullable LivingEntity owner) {
+         this(EntityType.TNT, level);
          this.setPos(x, y, z);
--        double d3 = world.random.nextDouble() * 6.2831854820251465D;
-+        double d3 = this.random.nextDouble() * 6.2831854820251465D; // Paper - Don't use level random in entity constructors
- 
-         this.setDeltaMovement(-Math.sin(d3) * 0.02D, 0.20000000298023224D, -Math.cos(d3) * 0.02D);
+-        double d = level.random.nextDouble() * (float) (Math.PI * 2);
++        double d = this.random.nextDouble() * (float) (Math.PI * 2); // Paper - Don't use level random in entity constructors
+         this.setDeltaMovement(-Math.sin(d) * 0.02, 0.2F, -Math.cos(d) * 0.02);
          this.setFuse(80);
-@@ -94,10 +101,17 @@
+         this.xo = x;
+@@ -91,10 +_,17 @@
  
      @Override
      public void tick() {
@@ -45,16 +45,16 @@
 +            return;
 +        }
 +        // Paper end - Configurable TNT height nerf
-         this.setDeltaMovement(this.getDeltaMovement().scale(0.98D));
+         this.setDeltaMovement(this.getDeltaMovement().scale(0.98));
          if (this.onGround()) {
-             this.setDeltaMovement(this.getDeltaMovement().multiply(0.7D, -0.5D, 0.7D));
-@@ -107,10 +121,13 @@
- 
+             this.setDeltaMovement(this.getDeltaMovement().multiply(0.7, -0.5, 0.7));
+@@ -103,19 +_,49 @@
+         int i = this.getFuse() - 1;
          this.setFuse(i);
          if (i <= 0) {
 -            this.discard();
 +            // CraftBukkit start - Need to reverse the order of the explosion and the entity death so we have a location for the event
-+            // this.discard();
++            //this.discard();
              if (!this.level().isClientSide) {
                  this.explode();
              }
@@ -63,10 +63,9 @@
          } else {
              this.updateInWaterStateAndDoFluidPushing();
              if (this.level().isClientSide) {
-@@ -118,10 +135,37 @@
+                 this.level().addParticle(ParticleTypes.SMOKE, this.getX(), this.getY() + 0.5, this.getZ(), 0.0, 0.0, 0.0);
              }
          }
- 
 +        // Paper start - Option to prevent TNT from moving in water
 +        if (!this.isRemoved() && this.wasTouchingWater && this.level().paperConfig().fixes.preventTntFromMovingInWater) {
 +            /*
@@ -91,26 +90,35 @@
      }
  
      private void explode() {
--        this.level().explode(this, Explosion.getDefaultDamageSource(this.level(), this), this.usedPortal ? PrimedTnt.USED_PORTAL_DAMAGE_CALCULATOR : null, this.getX(), this.getY(0.0625D), this.getZ(), this.explosionPower, false, Level.ExplosionInteraction.TNT);
 +        // CraftBukkit start
 +        ExplosionPrimeEvent event = CraftEventFactory.callExplosionPrimeEvent((org.bukkit.entity.Explosive) this.getBukkitEntity());
 +        if (event.isCancelled()) {
 +            return;
 +        }
-+        this.level().explode(this, Explosion.getDefaultDamageSource(this.level(), this), this.usedPortal ? PrimedTnt.USED_PORTAL_DAMAGE_CALCULATOR : null, this.getX(), this.getY(0.0625D), this.getZ(), event.getRadius(), event.getFire(), Level.ExplosionInteraction.TNT);
 +        // CraftBukkit end
+         this.level()
+             .explode(
+                 this,
+@@ -124,8 +_,8 @@
+                 this.getX(),
+                 this.getY(0.0625),
+                 this.getZ(),
+-                this.explosionPower,
+-                false,
++                event.getRadius(), // CraftBukkit
++                event.getFire(), // CraftBukkit
+                 Level.ExplosionInteraction.TNT
+             );
      }
- 
-     @Override
-@@ -198,4 +242,11 @@
-     public final boolean hurtServer(ServerLevel world, DamageSource source, float amount) {
+@@ -200,4 +_,11 @@
+     public final boolean hurtServer(ServerLevel level, DamageSource damageSource, float amount) {
          return false;
      }
 +
 +    // Paper start - Option to prevent TNT from moving in water
 +    @Override
 +    public boolean isPushedByFluid() {
-+        return !level().paperConfig().fixes.preventTntFromMovingInWater && super.isPushedByFluid();
++        return !this.level().paperConfig().fixes.preventTntFromMovingInWater && super.isPushedByFluid();
 +    }
 +    // Paper end - Option to prevent TNT from moving in water
  }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/AbstractSkeleton.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/AbstractSkeleton.java.patch
new file mode 100644
index 0000000000..50e3f4e66b
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/AbstractSkeleton.java.patch
@@ -0,0 +1,92 @@
+--- a/net/minecraft/world/entity/monster/AbstractSkeleton.java
++++ b/net/minecraft/world/entity/monster/AbstractSkeleton.java
+@@ -64,6 +_,7 @@
+             AbstractSkeleton.this.setAggressive(true);
+         }
+     };
++    private boolean shouldBurnInDay = true; // Paper - shouldBurnInDay API
+ 
+     protected AbstractSkeleton(EntityType<? extends AbstractSkeleton> entityType, Level level) {
+         super(entityType, level);
+@@ -88,6 +_,16 @@
+         return Monster.createMonsterAttributes().add(Attributes.MOVEMENT_SPEED, 0.25);
+     }
+ 
++    // Paper start - shouldBurnInDay API
++    public boolean shouldBurnInDay() {
++        return this.shouldBurnInDay;
++    }
++
++    public void setShouldBurnInDay(boolean shouldBurnInDay) {
++        this.shouldBurnInDay = shouldBurnInDay;
++    }
++    // Paper end - shouldBurnInDay API
++
+     @Override
+     protected void playStepSound(BlockPos pos, BlockState block) {
+         this.playSound(this.getStepSound(), 0.15F, 1.0F);
+@@ -97,7 +_,7 @@
+ 
+     @Override
+     public void aiStep() {
+-        boolean isSunBurnTick = this.isSunBurnTick();
++        boolean isSunBurnTick = this.shouldBurnInDay && this.isSunBurnTick(); // Paper - shouldBurnInDay API
+         if (isSunBurnTick) {
+             ItemStack itemBySlot = this.getItemBySlot(EquipmentSlot.HEAD);
+             if (!itemBySlot.isEmpty()) {
+@@ -145,7 +_,7 @@
+         this.populateDefaultEquipmentSlots(random, difficulty);
+         this.populateDefaultEquipmentEnchantments(level, random, difficulty);
+         this.reassessWeaponGoal();
+-        this.setCanPickUpLoot(random.nextFloat() < 0.55F * difficulty.getSpecialMultiplier());
++        this.setCanPickUpLoot(this.level().paperConfig().entities.behavior.mobsCanAlwaysPickUpLoot.skeletons || random.nextFloat() < 0.55F * difficulty.getSpecialMultiplier()); // Paper - Add world settings for mobs picking up loot
+         if (this.getItemBySlot(EquipmentSlot.HEAD).isEmpty()) {
+             LocalDate localDate = LocalDate.now();
+             int i = localDate.get(ChronoField.DAY_OF_MONTH);
+@@ -196,9 +_,19 @@
+         double d2 = target.getZ() - this.getZ();
+         double squareRoot = Math.sqrt(d * d + d2 * d2);
+         if (this.level() instanceof ServerLevel serverLevel) {
++            // CraftBukkit start
++            org.bukkit.event.entity.EntityShootBowEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityShootBowEvent(this, this.getMainHandItem(), arrow.getPickupItem(), arrow, net.minecraft.world.InteractionHand.MAIN_HAND, 0.8F, true); // Paper - improve entity shhot bow event - add arrow stack to event
++            if (event.isCancelled()) {
++                event.getProjectile().remove();
++                return;
++            }
++
++            if (event.getProjectile() == arrow.getBukkitEntity()) {
++            // CraftBukkit end
+             Projectile.spawnProjectileUsingShoot(
+                 arrow, serverLevel, projectile, d, d1 + squareRoot * 0.2F, d2, 1.6F, 14 - serverLevel.getDifficulty().getId() * 4
+             );
++            } // CraftBukkit
+         }
+ 
+         this.playSound(SoundEvents.SKELETON_SHOOT, 1.0F, 1.0F / (this.getRandom().nextFloat() * 0.4F + 0.8F));
+@@ -222,11 +_,23 @@
+     public void readAdditionalSaveData(CompoundTag compound) {
+         super.readAdditionalSaveData(compound);
+         this.reassessWeaponGoal();
+-    }
++        // Paper start - shouldBurnInDay API
++        if (compound.contains("Paper.ShouldBurnInDay")) {
++            this.shouldBurnInDay = compound.getBoolean("Paper.ShouldBurnInDay");
++        }
++        // Paper end - shouldBurnInDay API
++    }
++    // Paper start - shouldBurnInDay API
++    @Override
++    public void addAdditionalSaveData(final net.minecraft.nbt.CompoundTag nbt) {
++        super.addAdditionalSaveData(nbt);
++        nbt.putBoolean("Paper.ShouldBurnInDay", this.shouldBurnInDay);
++    }
++    // Paper end - shouldBurnInDay API
+ 
+     @Override
+-    public void setItemSlot(EquipmentSlot slot, ItemStack stack) {
+-        super.setItemSlot(slot, stack);
++    public void setItemSlot(EquipmentSlot slot, ItemStack stack, boolean silent) { // Paper - Fix silent equipment change
++        super.setItemSlot(slot, stack, silent); // Paper - Fix silent equipment change
+         if (!this.level().isClientSide) {
+             this.reassessWeaponGoal();
+         }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/Bogged.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/Bogged.java.patch
new file mode 100644
index 0000000000..63ee02e6cb
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/Bogged.java.patch
@@ -0,0 +1,62 @@
+--- a/net/minecraft/world/entity/monster/Bogged.java
++++ b/net/minecraft/world/entity/monster/Bogged.java
+@@ -72,7 +_,19 @@
+         ItemStack itemInHand = player.getItemInHand(hand);
+         if (itemInHand.is(Items.SHEARS) && this.readyForShearing()) {
+             if (this.level() instanceof ServerLevel serverLevel) {
+-                this.shear(serverLevel, SoundSource.PLAYERS, itemInHand);
++                // CraftBukkit start
++                // Paper start - custom shear drops
++                java.util.List<ItemStack> drops = this.generateDefaultDrops(serverLevel, itemInHand);
++                org.bukkit.event.player.PlayerShearEntityEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.handlePlayerShearEntityEvent(player, this, itemInHand, hand, drops);
++                if (event != null) {
++                    if (event.isCancelled()) {
++                        return InteractionResult.PASS;
++                    }
++                    drops = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getDrops());
++                    // Paper end - custom shear drops
++                }
++                // CraftBukkit end
++                this.shear(serverLevel, SoundSource.PLAYERS, itemInHand, drops); // Paper - custom shear drops
+                 this.gameEvent(GameEvent.SHEAR, player);
+                 itemInHand.hurtAndBreak(1, player, getSlotForHand(hand));
+             }
+@@ -125,15 +_,33 @@
+ 
+     @Override
+     public void shear(ServerLevel level, SoundSource soundSource, ItemStack shears) {
++    // Paper start - custom shear drops
++        this.shear(level, soundSource, shears, this.generateDefaultDrops(level, shears));
++    }
++
++    @Override
++    public java.util.List<ItemStack> generateDefaultDrops(final ServerLevel serverLevel, final ItemStack shears) {
++        final java.util.List<ItemStack> drops = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>();
++        this.dropFromShearingLootTable(serverLevel, BuiltInLootTables.BOGGED_SHEAR, shears, (ignored, stack) -> {
++            drops.add(stack);
++        });
++        return drops;
++    }
++
++    @Override
++    public void shear(ServerLevel level, SoundSource soundSource, ItemStack shears, java.util.List<ItemStack> drops) {
++    // Paper end - custom shear drops
+         level.playSound(null, this, SoundEvents.BOGGED_SHEAR, soundSource, 1.0F, 1.0F);
+-        this.spawnShearedMushrooms(level, shears);
++        this.spawnShearedMushrooms(level, shears, drops); // Paper - custom shear drops
+         this.setSheared(true);
+     }
+ 
+-    private void spawnShearedMushrooms(ServerLevel level, ItemStack stack) {
+-        this.dropFromShearingLootTable(
+-            level, BuiltInLootTables.BOGGED_SHEAR, stack, (serverLevel, itemStack) -> this.spawnAtLocation(serverLevel, itemStack, this.getBbHeight())
+-        );
++    // Paper start - custom shear drops
++    private void spawnShearedMushrooms(ServerLevel level, ItemStack stack, java.util.List<ItemStack> drops) {
++        this.forceDrops = true; // Paper - Add missing forceDrop toggles
++        drops.forEach(drop -> this.spawnAtLocation(level, drop, this.getBbHeight()));
++        this.forceDrops = false; // Paper - Add missing forceDrop toggles
++        // Paper end - custom shear drops
+     }
+ 
+     @Override
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/CaveSpider.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/CaveSpider.java.patch
new file mode 100644
index 0000000000..2b7ce5164e
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/CaveSpider.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/monster/CaveSpider.java
++++ b/net/minecraft/world/entity/monster/CaveSpider.java
+@@ -38,7 +_,7 @@
+                 }
+ 
+                 if (i > 0) {
+-                    ((LivingEntity)source).addEffect(new MobEffectInstance(MobEffects.POISON, i * 20, 0), this);
++                    ((LivingEntity)source).addEffect(new MobEffectInstance(MobEffects.POISON, i * 20, 0), this, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ATTACK); // CraftBukkit
+                 }
+             }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/Creeper.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/Creeper.java.patch
new file mode 100644
index 0000000000..6a3c837696
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/Creeper.java.patch
@@ -0,0 +1,115 @@
+--- a/net/minecraft/world/entity/monster/Creeper.java
++++ b/net/minecraft/world/entity/monster/Creeper.java
+@@ -49,6 +_,7 @@
+     public int maxSwell = 30;
+     public int explosionRadius = 3;
+     private int droppedSkulls;
++    public Entity entityIgniter; // CraftBukkit
+ 
+     public Creeper(EntityType<? extends Creeper> entityType, Level level) {
+         super(entityType, level);
+@@ -121,7 +_,7 @@
+         }
+ 
+         if (compound.getBoolean("ignited")) {
+-            this.ignite();
++            this.entityData.set(DATA_IS_IGNITED, true); // Paper - set directly to avoid firing event
+         }
+     }
+ 
+@@ -204,8 +_,19 @@
+     @Override
+     public void thunderHit(ServerLevel level, LightningBolt lightning) {
+         super.thunderHit(level, lightning);
++        // CraftBukkit start
++        if (org.bukkit.craftbukkit.event.CraftEventFactory.callCreeperPowerEvent(this, lightning, org.bukkit.event.entity.CreeperPowerEvent.PowerCause.LIGHTNING).isCancelled()) {
++            return;
++        }
++        // CraftBukkit end
+         this.entityData.set(DATA_IS_POWERED, true);
+     }
++    // CraftBukkit start
++    public void setPowered(boolean powered) {
++        this.entityData.set(Creeper.DATA_IS_POWERED, powered);
++    }
++    // CraftBukkit end
++
+ 
+     @Override
+     protected InteractionResult mobInteract(Player player, InteractionHand hand) {
+@@ -215,8 +_,9 @@
+             this.level()
+                 .playSound(player, this.getX(), this.getY(), this.getZ(), soundEvent, this.getSoundSource(), 1.0F, this.random.nextFloat() * 0.4F + 0.8F);
+             if (!this.level().isClientSide) {
++                this.entityIgniter = player; // CraftBukkit
+                 this.ignite();
+-                if (!itemInHand.isDamageableItem()) {
++                if (itemInHand.getMaxDamage() == 0) { // CraftBukkit - fix MC-264285: unbreakable flint and steels are completely consumed when igniting a creeper
+                     itemInHand.shrink(1);
+                 } else {
+                     itemInHand.hurtAndBreak(1, player, getSlotForHand(hand));
+@@ -232,18 +_,29 @@
+     public void explodeCreeper() {
+         if (this.level() instanceof ServerLevel serverLevel) {
+             float f = this.isPowered() ? 2.0F : 1.0F;
++            // CraftBukkit start
++            org.bukkit.event.entity.ExplosionPrimeEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callExplosionPrimeEvent(this, this.explosionRadius * f, false);
++            if (!event.isCancelled()) {
++            // CraftBukkit end
+             this.dead = true;
+-            serverLevel.explode(this, this.getX(), this.getY(), this.getZ(), this.explosionRadius * f, Level.ExplosionInteraction.MOB);
++            serverLevel.explode(this, this.getX(), this.getY(), this.getZ(), event.getRadius(), event.getFire(), Level.ExplosionInteraction.MOB); // CraftBukkit // Paper - fix DamageSource API (revert to vanilla, no, just no, don't change this)
+             this.spawnLingeringCloud();
+             this.triggerOnDeathMobEffects(serverLevel, Entity.RemovalReason.KILLED);
+-            this.discard();
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.EXPLODE); // CraftBukkit - add Bukkit remove cause
++            // CraftBukkit start
++            } else {
++                this.swell = 0;
++                this.entityData.set(DATA_IS_IGNITED, false); // Paper
++            }
++            // CraftBukkit end
+         }
+     }
+ 
+     private void spawnLingeringCloud() {
+         Collection<MobEffectInstance> activeEffects = this.getActiveEffects();
+-        if (!activeEffects.isEmpty()) {
++        if (!activeEffects.isEmpty() && !this.level().paperConfig().entities.behavior.disableCreeperLingeringEffect) { // Paper - Option to disable creeper lingering effect
+             AreaEffectCloud areaEffectCloud = new AreaEffectCloud(this.level(), this.getX(), this.getY(), this.getZ());
++            areaEffectCloud.setOwner(this); // CraftBukkit
+             areaEffectCloud.setRadius(2.5F);
+             areaEffectCloud.setRadiusOnUse(-0.5F);
+             areaEffectCloud.setWaitTime(10);
+@@ -254,16 +_,27 @@
+                 areaEffectCloud.addEffect(new MobEffectInstance(mobEffectInstance));
+             }
+ 
+-            this.level().addFreshEntity(areaEffectCloud);
+-        }
+-    }
++            this.level().addFreshEntity(areaEffectCloud, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.EXPLOSION); // CraftBukkit
++        }
++    }
++
++    // Paper start - CreeperIgniteEvent
++    public void setIgnited(boolean ignited) {
++        if (isIgnited() != ignited) {
++            com.destroystokyo.paper.event.entity.CreeperIgniteEvent event = new com.destroystokyo.paper.event.entity.CreeperIgniteEvent((org.bukkit.entity.Creeper) getBukkitEntity(), ignited);
++            if (event.callEvent()) {
++                this.entityData.set(DATA_IS_IGNITED, event.isIgnited());
++            }
++        }
++    }
++    // Paper end - CreeperIgniteEvent
+ 
+     public boolean isIgnited() {
+         return this.entityData.get(DATA_IS_IGNITED);
+     }
+ 
+     public void ignite() {
+-        this.entityData.set(DATA_IS_IGNITED, true);
++        setIgnited(true); // Paper - CreeperIgniteEvent
+     }
+ 
+     public boolean canDropMobsSkull() {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Drowned.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/Drowned.java.patch
similarity index 81%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/monster/Drowned.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/monster/Drowned.java.patch
index 938442254b..f28da7213d 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Drowned.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/Drowned.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/world/entity/monster/Drowned.java
 +++ b/net/minecraft/world/entity/monster/Drowned.java
-@@ -85,7 +85,7 @@
+@@ -85,7 +_,7 @@
          this.goalSelector.addGoal(7, new RandomStrollGoal(this, 1.0));
          this.targetSelector.addGoal(1, new HurtByTargetGoal(this, Drowned.class).setAlertOthers(ZombifiedPiglin.class));
-         this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, (target, world) -> this.okTarget(target)));
+         this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, (entity, level) -> this.okTarget(entity)));
 -        this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false));
-+        if (this.level().spigotConfig.zombieAggressiveTowardsVillager) this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false));  // Paper - Check drowned for villager aggression config
++        if (this.level().spigotConfig.zombieAggressiveTowardsVillager) this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false)); // Paper - Check drowned for villager aggression config
          this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, IronGolem.class, true));
          this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Axolotl.class, true, false));
          this.targetSelector.addGoal(5, new NearestAttackableTargetGoal<>(this, Turtle.class, 10, true, false, Turtle.BABY_ON_LAND_SELECTOR));
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/ElderGuardian.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/ElderGuardian.java.patch
new file mode 100644
index 0000000000..c15aa5deb4
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/ElderGuardian.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/monster/ElderGuardian.java
++++ b/net/minecraft/world/entity/monster/ElderGuardian.java
+@@ -65,7 +_,7 @@
+         super.customServerAiStep(level);
+         if ((this.tickCount + this.getId()) % 1200 == 0) {
+             MobEffectInstance mobEffectInstance = new MobEffectInstance(MobEffects.DIG_SLOWDOWN, 6000, 2);
+-            List<ServerPlayer> list = MobEffectUtil.addEffectToPlayersAround(level, this, this.position(), 50.0, mobEffectInstance, 1200);
++            List<ServerPlayer> list = MobEffectUtil.addEffectToPlayersAround(level, this, this.position(), 50.0, mobEffectInstance, 1200, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ATTACK, (player) -> new io.papermc.paper.event.entity.ElderGuardianAppearanceEvent((org.bukkit.entity.ElderGuardian) this.getBukkitEntity(), player.getBukkitEntity()).callEvent()); // CraftBukkit // Paper - Add ElderGuardianAppearanceEvent
+             list.forEach(
+                 serverPlayer -> serverPlayer.connection
+                     .send(new ClientboundGameEventPacket(ClientboundGameEventPacket.GUARDIAN_ELDER_EFFECT, this.isSilent() ? 0.0F : 1.0F))
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/EnderMan.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/EnderMan.java.patch
new file mode 100644
index 0000000000..d68e68e4c3
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/EnderMan.java.patch
@@ -0,0 +1,140 @@
+--- a/net/minecraft/world/entity/monster/EnderMan.java
++++ b/net/minecraft/world/entity/monster/EnderMan.java
+@@ -117,7 +_,23 @@
+ 
+     @Override
+     public void setTarget(@Nullable LivingEntity livingEntity) {
+-        super.setTarget(livingEntity);
++        // CraftBukkit start - fire event
++        this.setTarget(livingEntity, org.bukkit.event.entity.EntityTargetEvent.TargetReason.UNKNOWN, true);
++    }
++
++    // Paper start - EndermanEscapeEvent
++    private boolean tryEscape(com.destroystokyo.paper.event.entity.EndermanEscapeEvent.Reason reason) {
++        return new com.destroystokyo.paper.event.entity.EndermanEscapeEvent((org.bukkit.craftbukkit.entity.CraftEnderman) this.getBukkitEntity(), reason).callEvent();
++    }
++    // Paper end - EndermanEscapeEvent
++
++    @Override
++    public boolean setTarget(LivingEntity livingEntity, org.bukkit.event.entity.EntityTargetEvent.TargetReason reason, boolean fireEvent) {
++        if (!super.setTarget(livingEntity, reason, fireEvent)) {
++            return false;
++        }
++        livingEntity = this.getTarget();
++        // CraftBukkit end
+         AttributeInstance attribute = this.getAttribute(Attributes.MOVEMENT_SPEED);
+         if (livingEntity == null) {
+             this.targetChangeTime = 0;
+@@ -131,6 +_,7 @@
+                 attribute.addTransientModifier(SPEED_MODIFIER_ATTACKING);
+             }
+         }
++        return true; // CraftBukkit
+     }
+ 
+     @Override
+@@ -212,6 +_,14 @@
+     }
+ 
+     boolean isBeingStaredBy(Player player) {
++        // Paper start - EndermanAttackPlayerEvent
++        final boolean shouldAttack = isBeingStaredBy0(player);
++        final com.destroystokyo.paper.event.entity.EndermanAttackPlayerEvent event = new com.destroystokyo.paper.event.entity.EndermanAttackPlayerEvent((org.bukkit.entity.Enderman) getBukkitEntity(), (org.bukkit.entity.Player) player.getBukkitEntity());
++        event.setCancelled(!shouldAttack);
++        return event.callEvent();
++    }
++    private boolean isBeingStaredBy0(Player player) {
++        // Paper end - EndermanAttackPlayerEvent
+         return LivingEntity.PLAYER_NOT_WEARING_DISGUISE_ITEM.test(player) && this.isLookingAtMe(player, 0.025, true, false, new double[]{this.getEyeY()});
+     }
+ 
+@@ -251,7 +_,7 @@
+             float lightLevelDependentMagicValue = this.getLightLevelDependentMagicValue();
+             if (lightLevelDependentMagicValue > 0.5F
+                 && level.canSeeSky(this.blockPosition())
+-                && this.random.nextFloat() * 30.0F < (lightLevelDependentMagicValue - 0.4F) * 2.0F) {
++                && this.random.nextFloat() * 30.0F < (lightLevelDependentMagicValue - 0.4F) * 2.0F && this.tryEscape(com.destroystokyo.paper.event.entity.EndermanEscapeEvent.Reason.RUNAWAY)) { // Paper - EndermanEscapeEvent
+                 this.setTarget(null);
+                 this.teleport();
+             }
+@@ -372,11 +_,13 @@
+             } else {
+                 boolean flag1 = flag && this.hurtWithCleanWater(level, damageSource, (ThrownPotion)damageSource.getDirectEntity(), amount);
+ 
++                if (this.tryEscape(com.destroystokyo.paper.event.entity.EndermanEscapeEvent.Reason.INDIRECT)) { // Paper - EndermanEscapeEvent
+                 for (int i = 0; i < 64; i++) {
+                     if (this.teleport()) {
+                         return true;
+                     }
+                 }
++                } // Paper - EndermanEscapeEvent
+ 
+                 return flag1;
+             }
+@@ -401,6 +_,16 @@
+         this.entityData.set(DATA_STARED_AT, true);
+     }
+ 
++    // Paper start
++    public void setCreepy(boolean creepy) {
++        this.entityData.set(DATA_CREEPY, creepy);
++    }
++
++    public void setHasBeenStaredAt(boolean hasBeenStaredAt) {
++        this.entityData.set(DATA_STARED_AT, hasBeenStaredAt);
++    }
++    // Paper end
++
+     @Override
+     public boolean requiresCustomPersistence() {
+         return super.requiresCustomPersistence() || this.getCarriedBlock() != null;
+@@ -460,16 +_,19 @@
+             int floor1 = Mth.floor(this.enderman.getY() + random.nextDouble() * 2.0);
+             int floor2 = Mth.floor(this.enderman.getZ() - 1.0 + random.nextDouble() * 2.0);
+             BlockPos blockPos = new BlockPos(floor, floor1, floor2);
+-            BlockState blockState = level.getBlockState(blockPos);
++            BlockState blockState = level.getBlockStateIfLoaded(blockPos); // Paper - Prevent endermen from loading chunks
++            if (blockState == null) return; // Paper - Prevent endermen from loading chunks
+             BlockPos blockPos1 = blockPos.below();
+             BlockState blockState1 = level.getBlockState(blockPos1);
+             BlockState carriedBlock = this.enderman.getCarriedBlock();
+             if (carriedBlock != null) {
+                 carriedBlock = Block.updateFromNeighbourShapes(carriedBlock, this.enderman.level(), blockPos);
+                 if (this.canPlaceBlock(level, blockPos, carriedBlock, blockState, blockState1, blockPos1)) {
++                    if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this.enderman, blockPos, carriedBlock)) { // CraftBukkit - Place event
+                     level.setBlock(blockPos, carriedBlock, 3);
+                     level.gameEvent(GameEvent.BLOCK_PLACE, blockPos, GameEvent.Context.of(this.enderman, carriedBlock));
+                     this.enderman.setCarriedBlock(null);
++                    } // CraftBukkit
+                 }
+             }
+         }
+@@ -567,7 +_,7 @@
+             } else {
+                 if (this.target != null && !this.enderman.isPassenger()) {
+                     if (this.enderman.isBeingStaredBy((Player)this.target)) {
+-                        if (this.target.distanceToSqr(this.enderman) < 16.0) {
++                        if (this.target.distanceToSqr(this.enderman) < 16.0 && this.enderman.tryEscape(com.destroystokyo.paper.event.entity.EndermanEscapeEvent.Reason.STARE)) { // Paper - EndermanEscapeEvent
+                             this.enderman.teleport();
+                         }
+ 
+@@ -606,15 +_,18 @@
+             int floor1 = Mth.floor(this.enderman.getY() + random.nextDouble() * 3.0);
+             int floor2 = Mth.floor(this.enderman.getZ() - 2.0 + random.nextDouble() * 4.0);
+             BlockPos blockPos = new BlockPos(floor, floor1, floor2);
+-            BlockState blockState = level.getBlockState(blockPos);
++            BlockState blockState = level.getBlockStateIfLoaded(blockPos); // Paper - Prevent endermen from loading chunks
++            if (blockState == null) return; // Paper - Prevent endermen from loading chunks
+             Vec3 vec3 = new Vec3(this.enderman.getBlockX() + 0.5, floor1 + 0.5, this.enderman.getBlockZ() + 0.5);
+             Vec3 vec31 = new Vec3(floor + 0.5, floor1 + 0.5, floor2 + 0.5);
+             BlockHitResult blockHitResult = level.clip(new ClipContext(vec3, vec31, ClipContext.Block.OUTLINE, ClipContext.Fluid.NONE, this.enderman));
+             boolean flag = blockHitResult.getBlockPos().equals(blockPos);
+             if (blockState.is(BlockTags.ENDERMAN_HOLDABLE) && flag) {
++                if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this.enderman, blockPos, blockState.getFluidState().createLegacyBlock())) { // CraftBukkit - Place event // Paper - fix wrong block state
+                 level.removeBlock(blockPos, false);
+                 level.gameEvent(GameEvent.BLOCK_DESTROY, blockPos, GameEvent.Context.of(this.enderman, blockState));
+                 this.enderman.setCarriedBlock(blockState.getBlock().defaultBlockState());
++                } // CraftBukkit
+             }
+         }
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/Endermite.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/Endermite.java.patch
new file mode 100644
index 0000000000..4677099a10
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/Endermite.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/monster/Endermite.java
++++ b/net/minecraft/world/entity/monster/Endermite.java
+@@ -121,7 +_,7 @@
+             }
+ 
+             if (this.life >= 2400) {
+-                this.discard();
++                this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+             }
+         }
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/Evoker.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/Evoker.java.patch
new file mode 100644
index 0000000000..5329db11c5
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/Evoker.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/monster/Evoker.java
++++ b/net/minecraft/world/entity/monster/Evoker.java
+@@ -264,7 +_,7 @@
+                         serverLevel.getScoreboard().addPlayerToTeam(vex.getScoreboardName(), team);
+                     }
+ 
+-                    serverLevel.addFreshEntityWithPassengers(vex);
++                    serverLevel.addFreshEntityWithPassengers(vex, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.SPELL); // CraftBukkit - Add SpawnReason
+                     serverLevel.gameEvent(GameEvent.ENTITY_PLACE, blockPos, GameEvent.Context.of(Evoker.this));
+                 }
+             }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/Ghast.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/Ghast.java.patch
new file mode 100644
index 0000000000..20e8a59f9f
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/Ghast.java.patch
@@ -0,0 +1,23 @@
+--- a/net/minecraft/world/entity/monster/Ghast.java
++++ b/net/minecraft/world/entity/monster/Ghast.java
+@@ -63,6 +_,12 @@
+         return this.explosionPower;
+     }
+ 
++    // Paper start
++    public void setExplosionPower(int explosionPower) {
++        this.explosionPower = explosionPower;
++    }
++    // Paper end
++
+     @Override
+     protected boolean shouldDespawnInPeaceful() {
+         return true;
+@@ -277,6 +_,7 @@
+                         }
+ 
+                         LargeFireball largeFireball = new LargeFireball(level, this.ghast, vec3.normalize(), this.ghast.getExplosionPower());
++                        largeFireball.bukkitYield = largeFireball.explosionPower = this.ghast.getExplosionPower(); // CraftBukkit - set bukkitYield when setting explosionPower
+                         largeFireball.setPos(this.ghast.getX() + viewVector.x * 4.0, this.ghast.getY(0.5) + 0.5, largeFireball.getZ() + viewVector.z * 4.0);
+                         level.addFreshEntity(largeFireball);
+                         this.chargeTime = -40;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Guardian.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/Guardian.java.patch
similarity index 60%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/monster/Guardian.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/monster/Guardian.java.patch
index 078be667c0..5dbcc39af7 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Guardian.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/Guardian.java.patch
@@ -1,19 +1,19 @@
 --- a/net/minecraft/world/entity/monster/Guardian.java
 +++ b/net/minecraft/world/entity/monster/Guardian.java
-@@ -60,6 +60,7 @@
+@@ -59,6 +_,7 @@
      private boolean clientSideTouchedGround;
      @Nullable
      public RandomStrollGoal randomStrollGoal;
 +    public Guardian.GuardianAttackGoal guardianAttackGoal; // CraftBukkit - add field
  
-     public Guardian(EntityType<? extends Guardian> type, Level world) {
-         super(type, world);
-@@ -75,7 +76,7 @@
-         MoveTowardsRestrictionGoal pathfindergoalmovetowardsrestriction = new MoveTowardsRestrictionGoal(this, 1.0D);
- 
-         this.randomStrollGoal = new RandomStrollGoal(this, 1.0D, 80);
+     public Guardian(EntityType<? extends Guardian> entityType, Level level) {
+         super(entityType, level);
+@@ -73,7 +_,7 @@
+     protected void registerGoals() {
+         MoveTowardsRestrictionGoal moveTowardsRestrictionGoal = new MoveTowardsRestrictionGoal(this, 1.0);
+         this.randomStrollGoal = new RandomStrollGoal(this, 1.0, 80);
 -        this.goalSelector.addGoal(4, new Guardian.GuardianAttackGoal(this));
 +        this.goalSelector.addGoal(4, this.guardianAttackGoal = new Guardian.GuardianAttackGoal(this)); // CraftBukkit - assign field
-         this.goalSelector.addGoal(5, pathfindergoalmovetowardsrestriction);
+         this.goalSelector.addGoal(5, moveTowardsRestrictionGoal);
          this.goalSelector.addGoal(7, this.randomStrollGoal);
          this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F));
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/Husk.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/Husk.java.patch
new file mode 100644
index 0000000000..78b5ff1c16
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/Husk.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/monster/Husk.java
++++ b/net/minecraft/world/entity/monster/Husk.java
+@@ -57,7 +_,7 @@
+         boolean flag = super.doHurtTarget(level, source);
+         if (flag && this.getMainHandItem().isEmpty() && source instanceof LivingEntity) {
+             float effectiveDifficulty = this.level().getCurrentDifficultyAt(this.blockPosition()).getEffectiveDifficulty();
+-            ((LivingEntity)source).addEffect(new MobEffectInstance(MobEffects.HUNGER, 140 * (int)effectiveDifficulty), this);
++            ((LivingEntity)source).addEffect(new MobEffectInstance(MobEffects.HUNGER, 140 * (int)effectiveDifficulty), this, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ATTACK); // CraftBukkit
+         }
+ 
+         return flag;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Illusioner.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/Illusioner.java.patch
similarity index 69%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/monster/Illusioner.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/monster/Illusioner.java.patch
index b839923b98..288baed666 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Illusioner.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/Illusioner.java.patch
@@ -1,33 +1,26 @@
 --- a/net/minecraft/world/entity/monster/Illusioner.java
 +++ b/net/minecraft/world/entity/monster/Illusioner.java
-@@ -184,7 +184,17 @@
-         Level world = this.level();
- 
-         if (world instanceof ServerLevel worldserver) {
+@@ -180,9 +_,19 @@
+         double d2 = target.getZ() - this.getZ();
+         double squareRoot = Math.sqrt(d * d + d2 * d2);
+         if (this.level() instanceof ServerLevel serverLevel) {
 +            // Paper start - EntityShootBowEvent
-+            org.bukkit.event.entity.EntityShootBowEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityShootBowEvent(this, this.getMainHandItem(), entityarrow.getPickupItem(), entityarrow, target.getUsedItemHand(), 0.8F, true);
++            org.bukkit.event.entity.EntityShootBowEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityShootBowEvent(this, this.getMainHandItem(), mobArrow.getPickupItem(), mobArrow, target.getUsedItemHand(), 0.8F, true);
 +            if (event.isCancelled()) {
 +                event.getProjectile().remove();
 +                return;
 +            }
 +
-+            if (event.getProjectile() == entityarrow.getBukkitEntity()) {
-             Projectile.spawnProjectileUsingShoot(entityarrow, worldserver, itemstack1, d0, d1 + d3 * 0.20000000298023224D, d2, 1.6F, (float) (14 - worldserver.getDifficulty().getId() * 4));
++            if (event.getProjectile() == mobArrow.getBukkitEntity()) {
+             Projectile.spawnProjectileUsingShoot(
+                 mobArrow, serverLevel, projectile, d, d1 + squareRoot * 0.2F, d2, 1.6F, 14 - serverLevel.getDifficulty().getId() * 4
+             );
 +            }
 +            // Paper end - EntityShootBowEvent
          }
  
          this.playSound(SoundEvents.SKELETON_SHOOT, 1.0F, 1.0F / (this.getRandom().nextFloat() * 0.4F + 0.8F));
-@@ -218,7 +228,7 @@
- 
-         @Override
-         protected void performSpellCasting() {
--            Illusioner.this.addEffect(new MobEffectInstance(MobEffects.INVISIBILITY, 1200));
-+            Illusioner.this.addEffect(new MobEffectInstance(MobEffects.INVISIBILITY, 1200), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ILLUSION); // CraftBukkit
-         }
- 
-         @Nullable
-@@ -269,7 +279,7 @@
+@@ -229,7 +_,7 @@
  
          @Override
          protected void performSpellCasting() {
@@ -36,3 +29,12 @@
          }
  
          @Override
+@@ -261,7 +_,7 @@
+ 
+         @Override
+         protected void performSpellCasting() {
+-            Illusioner.this.addEffect(new MobEffectInstance(MobEffects.INVISIBILITY, 1200));
++            Illusioner.this.addEffect(new MobEffectInstance(MobEffects.INVISIBILITY, 1200), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ILLUSION); // CraftBukkit
+         }
+ 
+         @Nullable
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Monster.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/Monster.java.patch
similarity index 67%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/monster/Monster.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/monster/Monster.java.patch
index bae67fa57b..68ded0ff66 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Monster.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/Monster.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/world/entity/monster/Monster.java
 +++ b/net/minecraft/world/entity/monster/Monster.java
-@@ -92,7 +92,7 @@
+@@ -92,7 +_,7 @@
              return false;
          } else {
-             DimensionType dimensionType = world.dimensionType();
+             DimensionType dimensionType = level.dimensionType();
 -            int i = dimensionType.monsterSpawnBlockLightLimit();
-+            int i = world.getLevel().paperConfig().entities.spawning.monsterSpawnMaxLightLevel.or(dimensionType.monsterSpawnBlockLightLimit()); // Paper - Configurable max block light for monster spawning
-             if (i < 15 && world.getBrightness(LightLayer.BLOCK, pos) > i) {
++            int i = level.getLevel().paperConfig().entities.spawning.monsterSpawnMaxLightLevel.or(dimensionType.monsterSpawnBlockLightLimit()); // Paper - Configurable max block light for monster spawning
+             if (i < 15 && level.getBrightness(LightLayer.BLOCK, pos) > i) {
                  return false;
              } else {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/Phantom.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/Phantom.java.patch
new file mode 100644
index 0000000000..3b9061d838
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/Phantom.java.patch
@@ -0,0 +1,62 @@
+--- a/net/minecraft/world/entity/monster/Phantom.java
++++ b/net/minecraft/world/entity/monster/Phantom.java
+@@ -47,6 +_,11 @@
+     Vec3 moveTargetPoint = Vec3.ZERO;
+     public BlockPos anchorPoint = BlockPos.ZERO;
+     Phantom.AttackPhase attackPhase = Phantom.AttackPhase.CIRCLE;
++    // Paper start
++    @Nullable
++    public java.util.UUID spawningEntity;
++    public boolean shouldBurnInDay = true;
++    // Paper end
+ 
+     public Phantom(EntityType<? extends Phantom> entityType, Level level) {
+         super(entityType, level);
+@@ -141,7 +_,7 @@
+ 
+     @Override
+     public void aiStep() {
+-        if (this.isAlive() && this.isSunBurnTick()) {
++        if (this.isAlive() && this.shouldBurnInDay && this.isSunBurnTick()) { // Paper - shouldBurnInDay API
+             this.igniteForSeconds(8.0F);
+         }
+ 
+@@ -165,6 +_,15 @@
+         }
+ 
+         this.setPhantomSize(compound.getInt("Size"));
++
++        // Paper start
++        if (compound.hasUUID("Paper.SpawningEntity")) {
++            this.spawningEntity = compound.getUUID("Paper.SpawningEntity");
++        }
++        if (compound.contains("Paper.ShouldBurnInDay")) {
++            this.shouldBurnInDay = compound.getBoolean("Paper.ShouldBurnInDay");
++        }
++        // Paper end
+     }
+ 
+     @Override
+@@ -174,6 +_,12 @@
+         compound.putInt("AY", this.anchorPoint.getY());
+         compound.putInt("AZ", this.anchorPoint.getZ());
+         compound.putInt("Size", this.getPhantomSize());
++        // Paper start
++        if (this.spawningEntity != null) {
++            compound.putUUID("Paper.SpawningEntity", this.spawningEntity);
++        }
++        compound.putBoolean("Paper.ShouldBurnInDay", this.shouldBurnInDay);
++        // Paper end
+     }
+ 
+     @Override
+@@ -247,7 +_,8 @@
+ 
+                     for (Player player : nearbyPlayers) {
+                         if (Phantom.this.canAttack(serverLevel, player, TargetingConditions.DEFAULT)) {
+-                            Phantom.this.setTarget(player);
++                            if (!level().paperConfig().entities.behavior.phantomsOnlyAttackInsomniacs || EntitySelector.IS_INSOMNIAC.test(player)) // Paper - Add phantom creative and insomniac controls
++                            Phantom.this.setTarget(player, org.bukkit.event.entity.EntityTargetEvent.TargetReason.CLOSEST_PLAYER, true); // CraftBukkit - reason
+                             return true;
+                         }
+                     }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/Pillager.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/Pillager.java.patch
new file mode 100644
index 0000000000..baac9f090b
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/Pillager.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/monster/Pillager.java
++++ b/net/minecraft/world/entity/monster/Pillager.java
+@@ -214,7 +_,7 @@
+             this.onItemPickup(entity);
+             ItemStack itemStack = this.inventory.addItem(item);
+             if (itemStack.isEmpty()) {
+-                entity.discard();
++                entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
+             } else {
+                 item.setCount(itemStack.getCount());
+             }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/Ravager.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/Ravager.java.patch
new file mode 100644
index 0000000000..00100011ed
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/Ravager.java.patch
@@ -0,0 +1,31 @@
+--- a/net/minecraft/world/entity/monster/Ravager.java
++++ b/net/minecraft/world/entity/monster/Ravager.java
+@@ -151,12 +_,19 @@
+                     BlockState blockState = serverLevel.getBlockState(blockPos);
+                     Block block = blockState.getBlock();
+                     if (block instanceof LeavesBlock) {
++                        // CraftBukkit start
++                        if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this, blockPos, blockState.getFluidState().createLegacyBlock())) { // Paper - fix wrong block state
++                            continue;
++                        }
++                        // CraftBukkit end
+                         flag = serverLevel.destroyBlock(blockPos, true, this) || flag;
+                     }
+                 }
+ 
+                 if (!flag && this.onGround()) {
++                    if (new com.destroystokyo.paper.event.entity.EntityJumpEvent(getBukkitLivingEntity()).callEvent()) { // Paper - Entity Jump API
+                     this.jumpFromGround();
++                    } else { this.setJumping(false); } // Paper - Entity Jump API; setJumping(false) stops a potential loop
+                 }
+             }
+ 
+@@ -257,7 +_,7 @@
+         double d = entity.getX() - this.getX();
+         double d1 = entity.getZ() - this.getZ();
+         double max = Math.max(d * d + d1 * d1, 0.001);
+-        entity.push(d / max * 4.0, 0.2, d1 / max * 4.0);
++        entity.push(d / max * 4.0, 0.2, d1 / max * 4.0, this); // Paper - Add EntityKnockbackByEntityEvent and EntityPushedByEntityAttackEvent
+     }
+ 
+     @Override
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/Shulker.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/Shulker.java.patch
new file mode 100644
index 0000000000..8bd9da3693
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/Shulker.java.patch
@@ -0,0 +1,46 @@
+--- a/net/minecraft/world/entity/monster/Shulker.java
++++ b/net/minecraft/world/entity/monster/Shulker.java
+@@ -274,7 +_,13 @@
+ 
+     @Override
+     public void stopRiding() {
+-        super.stopRiding();
++        // Paper start - Force entity dismount during teleportation
++        this.stopRiding(false);
++    }
++    @Override
++    public void stopRiding(boolean suppressCancellation) {
++        super.stopRiding(suppressCancellation);
++        // Paper end - Force entity dismount during teleportation
+         if (this.level().isClientSide) {
+             this.clientOldAttachPosition = this.blockPosition();
+         }
+@@ -387,6 +_,14 @@
+                     && this.level().getWorldBorder().isWithinBounds(blockPos1)
+                     && this.level().noCollision(this, new AABB(blockPos1).deflate(1.0E-6))) {
+                     Direction direction = this.findAttachableSurface(blockPos1);
++                    // CraftBukkit start
++                    org.bukkit.event.entity.EntityTeleportEvent teleportEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTeleportEvent(this, blockPos1.getX(), blockPos1.getY(), blockPos1.getZ());
++                    if (teleportEvent.isCancelled() || teleportEvent.getTo() == null) { // Paper
++                        return false;
++                    } else {
++                        blockPos1 = org.bukkit.craftbukkit.util.CraftLocation.toBlockPosition(teleportEvent.getTo());
++                    }
++                    // CraftBukkit end
+                     if (direction != null) {
+                         this.unRide();
+                         this.setAttachFace(direction);
+@@ -453,7 +_,12 @@
+                 if (shulker != null) {
+                     shulker.setVariant(this.getVariant());
+                     shulker.moveTo(vec3);
+-                    this.level().addFreshEntity(shulker);
++                    // Paper start - Shulker duplicate event
++                    if (!new io.papermc.paper.event.entity.ShulkerDuplicateEvent((org.bukkit.entity.Shulker) shulker.getBukkitEntity(), (org.bukkit.entity.Shulker) this.getBukkitEntity()).callEvent()) {
++                        return;
++                    }
++                    // Paper end - Shulker duplicate event
++                    this.level().addFreshEntity(shulker, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.BREEDING); // CraftBukkit - the mysteries of life
+                 }
+             }
+         }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/Silverfish.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/Silverfish.java.patch
new file mode 100644
index 0000000000..a8c9b55110
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/Silverfish.java.patch
@@ -0,0 +1,40 @@
+--- a/net/minecraft/world/entity/monster/Silverfish.java
++++ b/net/minecraft/world/entity/monster/Silverfish.java
+@@ -119,7 +_,7 @@
+             return true;
+         } else {
+             Player nearestPlayer = level.getNearestPlayer(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, 5.0, true);
+-            return nearestPlayer == null;
++            return !(nearestPlayer != null && !nearestPlayer.affectsSpawning) && nearestPlayer == null; // Paper - Affects Spawning API
+         }
+     }
+ 
+@@ -170,9 +_,14 @@
+                 BlockPos blockPos = BlockPos.containing(this.mob.getX(), this.mob.getY() + 0.5, this.mob.getZ()).relative(this.selectedDirection);
+                 BlockState blockState = levelAccessor.getBlockState(blockPos);
+                 if (InfestedBlock.isCompatibleHostBlock(blockState)) {
++                    // CraftBukkit start
++                    if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this.mob, blockPos, InfestedBlock.infestedStateByHost(blockState))) {
++                        return;
++                    }
++                    // CraftBukkit end
+                     levelAccessor.setBlock(blockPos, InfestedBlock.infestedStateByHost(blockState), 3);
+                     this.mob.spawnAnim();
+-                    this.mob.discard();
++                    this.mob.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.ENTER_BLOCK); // CraftBukkit - add Bukkit remove cause
+                 }
+             }
+         }
+@@ -212,6 +_,12 @@
+                             BlockState blockState = level.getBlockState(blockPos1);
+                             Block block = blockState.getBlock();
+                             if (block instanceof InfestedBlock) {
++                                // CraftBukkit start
++                                BlockState afterState = getServerLevel(level).getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) ? blockState.getFluidState().createLegacyBlock() : ((InfestedBlock) block).hostStateByInfested(level.getBlockState(blockPos1)); // Paper - fix wrong block state
++                                if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this.silverfish, blockPos1, afterState)) { // Paper - fix wrong block state
++                                    continue;
++                                }
++                                // CraftBukkit end
+                                 if (getServerLevel(level).getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) {
+                                     level.destroyBlock(blockPos1, true, this.silverfish);
+                                 } else {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Skeleton.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/Skeleton.java.patch
similarity index 52%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/monster/Skeleton.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/monster/Skeleton.java.patch
index d17fea360f..49812bbeee 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Skeleton.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/Skeleton.java.patch
@@ -1,24 +1,22 @@
 --- a/net/minecraft/world/entity/monster/Skeleton.java
 +++ b/net/minecraft/world/entity/monster/Skeleton.java
-@@ -94,12 +94,19 @@
+@@ -89,11 +_,17 @@
      }
  
      protected void doFreezeConversion() {
--        this.convertTo(EntityType.STRAY, ConversionParams.single(this, true, true), (entityskeletonstray) -> {
-+        final Stray stray = this.convertTo(EntityType.STRAY, ConversionParams.single(this, true, true), (entityskeletonstray) -> { // Paper - Fix issues with mob conversion; reset conversion time to prevent event spam
+-        this.convertTo(EntityType.STRAY, ConversionParams.single(this, true, true), mob -> {
++        final Stray stray = this.convertTo(EntityType.STRAY, ConversionParams.single(this, true, true), mob -> { // Paper - Fix issues with mob conversion; reset conversion time to prevent event spam
              if (!this.isSilent()) {
-                 this.level().levelEvent((Player) null, 1048, this.blockPosition(), 0);
+                 this.level().levelEvent(null, 1048, this.blockPosition(), 0);
              }
- 
 -        });
-+        }, org.bukkit.event.entity.EntityTransformEvent.TransformReason.FROZEN, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.FROZEN);// CraftBukkit - add spawn and transform reasons
-+
-+        // Paper start - Fix issues with mob conversion; reset conversion time to prevent event spam
++        // Paper start - add spawn and transform reasons
++        }, org.bukkit.event.entity.EntityTransformEvent.TransformReason.FROZEN, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.FROZEN);
 +        if (stray == null) {
++            // Reset conversion time to prevent event spam
 +            this.conversionTime = 300;
 +        }
-+        // Paper end - Fix issues with mob conversion
-+
++        // Paper end - add spawn and transform reasons
      }
  
      @Override
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/Slime.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/Slime.java.patch
new file mode 100644
index 0000000000..07a1abbeea
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/Slime.java.patch
@@ -0,0 +1,230 @@
+--- a/net/minecraft/world/entity/monster/Slime.java
++++ b/net/minecraft/world/entity/monster/Slime.java
+@@ -56,6 +_,7 @@
+     public float squish;
+     public float oSquish;
+     private boolean wasOnGround;
++    private boolean canWander = true; // Paper - Slime pathfinder events
+ 
+     public Slime(EntityType<? extends Slime> entityType, Level level) {
+         super(entityType, level);
+@@ -110,6 +_,7 @@
+         super.addAdditionalSaveData(compound);
+         compound.putInt("Size", this.getSize() - 1);
+         compound.putBoolean("wasOnGround", this.wasOnGround);
++        compound.putBoolean("Paper.canWander", this.canWander); // Paper
+     }
+ 
+     @Override
+@@ -117,6 +_,11 @@
+         this.setSize(compound.getInt("Size") + 1, false);
+         super.readAdditionalSaveData(compound);
+         this.wasOnGround = compound.getBoolean("wasOnGround");
++        // Paper start
++        if (compound.contains("Paper.canWander")) {
++            this.canWander = compound.getBoolean("Paper.canWander");
++        }
++        // Paper end
+     }
+ 
+     public boolean isTiny() {
+@@ -197,6 +_,13 @@
+ 
+     @Override
+     public void remove(Entity.RemovalReason reason) {
++        // CraftBukkit start - add Bukkit remove cause
++        this.remove(reason, null);
++    }
++
++    @Override
++    public void remove(Entity.RemovalReason reason, org.bukkit.event.entity.EntityRemoveEvent.Cause eventCause) {
++        // CraftBukkit end
+         int size = this.getSize();
+         if (!this.level().isClientSide && size > 1 && this.isDeadOrDying()) {
+             float width = this.getDimensions(this.getPose()).width();
+@@ -204,18 +_,45 @@
+             int i = size / 2;
+             int i1 = 2 + this.random.nextInt(3);
+             PlayerTeam team = this.getTeam();
++            // CraftBukkit start
++            org.bukkit.event.entity.SlimeSplitEvent event = new org.bukkit.event.entity.SlimeSplitEvent((org.bukkit.entity.Slime) this.getBukkitEntity(), i1);
++            this.level().getCraftServer().getPluginManager().callEvent(event);
++
++            if (!event.isCancelled() && event.getCount() > 0) {
++                i1 = event.getCount();
++            } else {
++                super.remove(reason, eventCause); // CraftBukkit - add Bukkit remove cause
++                return;
++            }
++            java.util.List<net.minecraft.world.entity.LivingEntity> slimes = new java.util.ArrayList<>(i1);
++            // CraftBukkit end
+ 
+             for (int i2 = 0; i2 < i1; i2++) {
+                 float f1 = (i2 % 2 - 0.5F) * f;
+                 float f2 = (i2 / 2 - 0.5F) * f;
+-                this.convertTo(this.getType(), new ConversionParams(ConversionType.SPLIT_ON_DEATH, false, false, team), EntitySpawnReason.TRIGGERED, mob -> {
++                Slime converted = this.convertTo(this.getType(), new ConversionParams(ConversionType.SPLIT_ON_DEATH, false, false, team), EntitySpawnReason.TRIGGERED, (mob) -> { // CraftBukkit
++                    mob.aware = this.aware; // Paper - Fix nerfed slime when splitting
+                     mob.setSize(i, true);
+                     mob.moveTo(this.getX() + f1, this.getY() + 0.5, this.getZ() + f2, this.random.nextFloat() * 360.0F, 0.0F);
+-                });
+-            }
++                // CraftBukkit start
++                }, null, null);
++                if (converted != null) {
++                    slimes.add(converted);
++                }
++                // CraftBukkit end
++            }
++            // CraftBukkit start
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTransformEvent(this, slimes, org.bukkit.event.entity.EntityTransformEvent.TransformReason.SPLIT).isCancelled()) {
++                super.remove(reason, eventCause); // CraftBukkit - add Bukkit remove cause
++                return;
++            }
++            for (LivingEntity living : slimes) {
++                this.level().addFreshEntity(living, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.SLIME_SPLIT); // CraftBukkit - SpawnReason
++            }
++            // CraftBukkit end
+         }
+ 
+-        super.remove(reason);
++        super.remove(reason, eventCause); // CraftBukkit - add Bukkit remove cause
+     }
+ 
+     @Override
+@@ -281,9 +_,13 @@
+                 return checkMobSpawnRules(entityType, level, spawnReason, pos, random);
+             }
+ 
++            // Paper start - Replace rules for Height in Swamp Biome
++            final double maxHeightSwamp = level.getMinecraftWorld().paperConfig().entities.spawning.slimeSpawnHeight.surfaceBiome.maximum;
++            final double minHeightSwamp = level.getMinecraftWorld().paperConfig().entities.spawning.slimeSpawnHeight.surfaceBiome.minimum;
++            // Paper end
+             if (level.getBiome(pos).is(BiomeTags.ALLOWS_SURFACE_SLIME_SPAWNS)
+-                && pos.getY() > 50
+-                && pos.getY() < 70
++                && pos.getY() > minHeightSwamp // Paper - Replace rules for Height in Swamp Biome
++                && pos.getY() < maxHeightSwamp // Paper - Replace rules for Height in Swamp Biome
+                 && random.nextFloat() < 0.5F
+                 && random.nextFloat() < level.getMoonBrightness()
+                 && level.getMaxLocalRawBrightness(pos) <= random.nextInt(8)) {
+@@ -295,8 +_,11 @@
+             }
+ 
+             ChunkPos chunkPos = new ChunkPos(pos);
+-            boolean flag = WorldgenRandom.seedSlimeChunk(chunkPos.x, chunkPos.z, ((WorldGenLevel)level).getSeed(), 987234911L).nextInt(10) == 0;
+-            if (random.nextInt(10) == 0 && flag && pos.getY() < 40) {
++            boolean flag = level.getMinecraftWorld().paperConfig().entities.spawning.allChunksAreSlimeChunks || WorldgenRandom.seedSlimeChunk(chunkPos.x, chunkPos.z, ((WorldGenLevel) level).getSeed(), level.getMinecraftWorld().spigotConfig.slimeSeed).nextInt(10) == 0; // Spigot // Paper
++                // Paper start - Replace rules for Height in Slime Chunks
++                final double maxHeightSlimeChunk = level.getMinecraftWorld().paperConfig().entities.spawning.slimeSpawnHeight.slimeChunk.maximum;
++                if (random.nextInt(10) == 0 && flag && pos.getY() < maxHeightSlimeChunk) {
++                // Paper end - Replace rules for Height in Slime Chunks
+                 return checkMobSpawnRules(entityType, level, spawnReason, pos, random);
+             }
+         }
+@@ -355,6 +_,16 @@
+         return super.getDefaultDimensions(pose).scale(this.getSize());
+     }
+ 
++    // Paper start - Slime pathfinder events
++    public boolean canWander() {
++        return this.canWander;
++    }
++
++    public void setWander(boolean canWander) {
++        this.canWander = canWander;
++    }
++    // Paper end - Slime pathfinder events
++
+     static class SlimeAttackGoal extends Goal {
+         private final Slime slime;
+         private int growTiredTimer;
+@@ -367,7 +_,16 @@
+         @Override
+         public boolean canUse() {
+             LivingEntity target = this.slime.getTarget();
+-            return target != null && this.slime.canAttack(target) && this.slime.getMoveControl() instanceof Slime.SlimeMoveControl;
++
++            // Paper start - Slime pathfinder events
++            if (target == null || !target.isAlive()) {
++                return false;
++            }
++            if (!this.slime.canAttack(target)) {
++                return false;
++            }
++            return this.slime.getMoveControl() instanceof Slime.SlimeMoveControl && this.slime.canWander && new com.destroystokyo.paper.event.entity.SlimeTargetLivingEntityEvent((org.bukkit.entity.Slime) this.slime.getBukkitEntity(), (org.bukkit.entity.LivingEntity) target.getBukkitEntity()).callEvent();
++            // Paper end - Slime pathfinder events
+         }
+ 
+         @Override
+@@ -379,7 +_,16 @@
+         @Override
+         public boolean canContinueToUse() {
+             LivingEntity target = this.slime.getTarget();
+-            return target != null && this.slime.canAttack(target) && --this.growTiredTimer > 0;
++
++            // Paper start - Slime pathfinder events
++            if (target == null || !target.isAlive()) {
++                return false;
++            }
++            if (!this.slime.canAttack(target)) {
++                return false;
++            }
++            return --this.growTiredTimer > 0 && this.slime.canWander && new com.destroystokyo.paper.event.entity.SlimeTargetLivingEntityEvent((org.bukkit.entity.Slime) this.slime.getBukkitEntity(), (org.bukkit.entity.LivingEntity) target.getBukkitEntity()).callEvent();
++            // Paper end - Slime pathfinder events
+         }
+ 
+         @Override
+@@ -398,6 +_,13 @@
+                 slimeMoveControl.setDirection(this.slime.getYRot(), this.slime.isDealsDamage());
+             }
+         }
++
++        // Paper start - Slime pathfinder events; clear timer and target when goal resets
++        public void stop() {
++            this.growTiredTimer = 0;
++            this.slime.setTarget(null);
++        }
++        // Paper end - Slime pathfinder events
+     }
+ 
+     static class SlimeFloatGoal extends Goal {
+@@ -411,7 +_,7 @@
+ 
+         @Override
+         public boolean canUse() {
+-            return (this.slime.isInWater() || this.slime.isInLava()) && this.slime.getMoveControl() instanceof Slime.SlimeMoveControl;
++            return (this.slime.isInWater() || this.slime.isInLava()) && this.slime.getMoveControl() instanceof Slime.SlimeMoveControl && this.slime.canWander && new com.destroystokyo.paper.event.entity.SlimeSwimEvent((org.bukkit.entity.Slime) this.slime.getBukkitEntity()).callEvent(); // Paper - Slime pathfinder events
+         }
+ 
+         @Override
+@@ -441,7 +_,7 @@
+ 
+         @Override
+         public boolean canUse() {
+-            return !this.slime.isPassenger();
++            return !this.slime.isPassenger() && this.slime.canWander && new com.destroystokyo.paper.event.entity.SlimeWanderEvent((org.bukkit.entity.Slime) this.slime.getBukkitEntity()).callEvent(); // Paper - Slime pathfinder events
+         }
+ 
+         @Override
+@@ -519,7 +_,7 @@
+ 
+         @Override
+         public boolean canUse() {
+-            return this.slime.getTarget() == null
++            return this.slime.getTarget() == null && this.slime.canWander // Paper - Slime pathfinder events
+                 && (this.slime.onGround() || this.slime.isInWater() || this.slime.isInLava() || this.slime.hasEffect(MobEffects.LEVITATION))
+                 && this.slime.getMoveControl() instanceof Slime.SlimeMoveControl;
+         }
+@@ -529,6 +_,11 @@
+             if (--this.nextRandomizeTime <= 0) {
+                 this.nextRandomizeTime = this.adjustedTickDelay(40 + this.slime.getRandom().nextInt(60));
+                 this.chosenDegrees = this.slime.getRandom().nextInt(360);
++                // Paper start - Slime pathfinder events
++                com.destroystokyo.paper.event.entity.SlimeChangeDirectionEvent event = new com.destroystokyo.paper.event.entity.SlimeChangeDirectionEvent((org.bukkit.entity.Slime) this.slime.getBukkitEntity(), this.chosenDegrees);
++                if (!this.slime.canWander || !event.callEvent()) return;
++                this.chosenDegrees = event.getNewYaw();
++                // Paper end - Slime pathfinder events
+             }
+ 
+             if (this.slime.getMoveControl() instanceof Slime.SlimeMoveControl slimeMoveControl) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/SpellcasterIllager.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/SpellcasterIllager.java.patch
similarity index 50%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/monster/SpellcasterIllager.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/monster/SpellcasterIllager.java.patch
index 7e02fcf3a8..24fa757b5d 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/SpellcasterIllager.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/SpellcasterIllager.java.patch
@@ -1,21 +1,11 @@
 --- a/net/minecraft/world/entity/monster/SpellcasterIllager.java
 +++ b/net/minecraft/world/entity/monster/SpellcasterIllager.java
-@@ -17,6 +17,9 @@
- import net.minecraft.world.entity.LivingEntity;
- import net.minecraft.world.entity.ai.goal.Goal;
- import net.minecraft.world.level.Level;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+// CraftBukkit end
- 
- public abstract class SpellcasterIllager extends AbstractIllager {
- 
-@@ -159,6 +162,11 @@
+@@ -208,6 +_,11 @@
          public void tick() {
-             --this.attackWarmupDelay;
+             this.attackWarmupDelay--;
              if (this.attackWarmupDelay == 0) {
 +                // CraftBukkit start
-+                if (!CraftEventFactory.handleEntitySpellCastEvent(SpellcasterIllager.this, this.getSpell())) {
++                if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleEntitySpellCastEvent(SpellcasterIllager.this, this.getSpell())) {
 +                    return;
 +                }
 +                // CraftBukkit end
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/Spider.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/Spider.java.patch
new file mode 100644
index 0000000000..16a88af185
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/Spider.java.patch
@@ -0,0 +1,29 @@
+--- a/net/minecraft/world/entity/monster/Spider.java
++++ b/net/minecraft/world/entity/monster/Spider.java
+@@ -79,7 +_,7 @@
+     public void tick() {
+         super.tick();
+         if (!this.level().isClientSide) {
+-            this.setClimbing(this.horizontalCollision);
++            this.setClimbing(this.horizontalCollision && (this.level().paperConfig().entities.behavior.allowSpiderWorldBorderClimbing || !(io.papermc.paper.FeatureHooks.isSpiderCollidingWithWorldBorder(this) && this.level().getWorldBorder().isInsideCloseToBorder(this, this.getBoundingBox())))); // Paper - Add config option for spider worldborder climbing (Inflate by +EPSILON as collision will just barely place us outside border)
+         }
+     }
+ 
+@@ -121,7 +_,7 @@
+ 
+     @Override
+     public boolean canBeAffected(MobEffectInstance potioneffect) {
+-        return !potioneffect.is(MobEffects.POISON) && super.canBeAffected(potioneffect);
++        return (!potioneffect.is(MobEffects.POISON) || !this.level().paperConfig().entities.mobEffects.spidersImmuneToPoisonEffect) && super.canBeAffected(potioneffect); // Paper - Add config for mobs immune to default effects
+     }
+ 
+     public boolean isClimbing() {
+@@ -165,7 +_,7 @@
+         if (spawnGroupData instanceof Spider.SpiderEffectsGroupData spiderEffectsGroupData) {
+             Holder<MobEffect> holder = spiderEffectsGroupData.effect;
+             if (holder != null) {
+-                this.addEffect(new MobEffectInstance(holder, -1));
++                this.addEffect(new MobEffectInstance(holder, -1), null, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.SPIDER_SPAWN, level instanceof net.minecraft.server.level.ServerLevel); // CraftBukkit
+             }
+         }
+ 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Strider.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/Strider.java.patch
similarity index 57%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/monster/Strider.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/monster/Strider.java.patch
index fbc85b5bbe..63f135a7cc 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Strider.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/Strider.java.patch
@@ -1,12 +1,12 @@
 --- a/net/minecraft/world/entity/monster/Strider.java
 +++ b/net/minecraft/world/entity/monster/Strider.java
-@@ -350,7 +350,14 @@
- 
-             boolean flag2 = flag1;
- 
--            this.setSuffocating(!flag || flag2);
+@@ -309,7 +_,14 @@
+                 || blockStateOnLegacy.is(BlockTags.STRIDER_WARM_BLOCKS)
+                 || this.getFluidHeight(FluidTags.LAVA) > 0.0;
+             boolean flag1 = this.getVehicle() instanceof Strider strider && strider.isSuffocating();
+-            this.setSuffocating(!flag || flag1);
 +            // CraftBukkit start
-+            boolean suffocating = !flag || flag2;
++            boolean suffocating = !flag || flag1;
 +            if (suffocating ^ this.isSuffocating()) {
 +                if (org.bukkit.craftbukkit.event.CraftEventFactory.callStriderTemperatureChangeEvent(this, suffocating)) {
 +                    this.setSuffocating(suffocating);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Vex.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/Vex.java.patch
similarity index 52%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/monster/Vex.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/monster/Vex.java.patch
index c2c4e27616..415806e46f 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Vex.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/Vex.java.patch
@@ -1,18 +1,6 @@
 --- a/net/minecraft/world/entity/monster/Vex.java
 +++ b/net/minecraft/world/entity/monster/Vex.java
-@@ -354,7 +354,10 @@
-             for (int i = 0; i < 3; ++i) {
-                 BlockPos blockposition1 = blockposition.offset(Vex.this.random.nextInt(15) - 7, Vex.this.random.nextInt(11) - 5, Vex.this.random.nextInt(15) - 7);
- 
--                if (Vex.this.level().isEmptyBlock(blockposition1)) {
-+                // Paper start - Don't load chunks
-+                final net.minecraft.world.level.block.state.BlockState blockState = Vex.this.level().getBlockStateIfLoaded(blockposition1);
-+                if (blockState != null && blockState.isAir()) {
-+                    // Paper end - Don't load chunks
-                     Vex.this.moveControl.setWantedPosition((double) blockposition1.getX() + 0.5D, (double) blockposition1.getY() + 0.5D, (double) blockposition1.getZ() + 0.5D, 0.25D);
-                     if (Vex.this.getTarget() == null) {
-                         Vex.this.getLookControl().setLookAt((double) blockposition1.getX() + 0.5D, (double) blockposition1.getY() + 0.5D, (double) blockposition1.getZ() + 0.5D, 180.0F, 20.0F);
-@@ -381,7 +384,7 @@
+@@ -296,7 +_,7 @@
  
          @Override
          public void start() {
@@ -21,3 +9,15 @@
              super.start();
          }
      }
+@@ -355,7 +_,10 @@
+ 
+             for (int i = 0; i < 3; i++) {
+                 BlockPos blockPos = boundOrigin.offset(Vex.this.random.nextInt(15) - 7, Vex.this.random.nextInt(11) - 5, Vex.this.random.nextInt(15) - 7);
+-                if (Vex.this.level().isEmptyBlock(blockPos)) {
++                // Paper start - Don't load chunks
++                final net.minecraft.world.level.block.state.BlockState blockState = Vex.this.level().getBlockStateIfLoaded(blockPos);
++                if (blockState != null && blockState.isAir()) {
++                    // Paper end - Don't load chunks
+                     Vex.this.moveControl.setWantedPosition(blockPos.getX() + 0.5, blockPos.getY() + 0.5, blockPos.getZ() + 0.5, 0.25);
+                     if (Vex.this.getTarget() == null) {
+                         Vex.this.getLookControl().setLookAt(blockPos.getX() + 0.5, blockPos.getY() + 0.5, blockPos.getZ() + 0.5, 180.0F, 20.0F);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Vindicator.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/Vindicator.java.patch
similarity index 97%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/monster/Vindicator.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/monster/Vindicator.java.patch
index e652ce7a7e..70543fd76e 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Vindicator.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/Vindicator.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/entity/monster/Vindicator.java
 +++ b/net/minecraft/world/entity/monster/Vindicator.java
-@@ -184,7 +184,7 @@
+@@ -184,7 +_,7 @@
  
      static class VindicatorBreakDoorGoal extends BreakDoorGoal {
          public VindicatorBreakDoorGoal(Mob mob) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Witch.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/Witch.java.patch
similarity index 55%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/monster/Witch.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/monster/Witch.java.patch
index 6939fca599..d7a8e9a59e 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Witch.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/Witch.java.patch
@@ -1,23 +1,22 @@
 --- a/net/minecraft/world/entity/monster/Witch.java
 +++ b/net/minecraft/world/entity/monster/Witch.java
-@@ -124,9 +124,15 @@
- 
+@@ -122,8 +_,14 @@
+                     ItemStack mainHandItem = this.getMainHandItem();
                      this.setItemSlot(EquipmentSlot.MAINHAND, ItemStack.EMPTY);
-                     PotionContents potioncontents = (PotionContents) itemstack.get(DataComponents.POTION_CONTENTS);
+                     PotionContents potionContents = mainHandItem.get(DataComponents.POTION_CONTENTS);
 +                    // Paper start - WitchConsumePotionEvent
-+                    if (itemstack.is(Items.POTION)) {
-+                        com.destroystokyo.paper.event.entity.WitchConsumePotionEvent event = new com.destroystokyo.paper.event.entity.WitchConsumePotionEvent((org.bukkit.entity.Witch) this.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack));
-+                        potioncontents = event.callEvent() ? org.bukkit.craftbukkit.inventory.CraftItemStack.unwrap(event.getPotion()).get(DataComponents.POTION_CONTENTS) : null;
++                    if (mainHandItem.is(Items.POTION)) {
++                        com.destroystokyo.paper.event.entity.WitchConsumePotionEvent event = new com.destroystokyo.paper.event.entity.WitchConsumePotionEvent((org.bukkit.entity.Witch) this.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(mainHandItem));
++                        potionContents = event.callEvent() ? org.bukkit.craftbukkit.inventory.CraftItemStack.unwrap(event.getPotion()).get(DataComponents.POTION_CONTENTS) : null;
 +                    }
 +                    // Paper end - WitchConsumePotionEvent
- 
-                     if (itemstack.is(Items.POTION) && potioncontents != null) {
--                        potioncontents.forEachEffect(this::addEffect);
-+                        potioncontents.forEachEffect((effect) -> this.addEffect(effect, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ATTACK)); // CraftBukkit
+                     if (mainHandItem.is(Items.POTION) && potionContents != null) {
+-                        potionContents.forEachEffect(this::addEffect);
++                        potionContents.forEachEffect((effect) -> this.addEffect(effect, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ATTACK)); // CraftBukkit
                      }
  
                      this.gameEvent(GameEvent.DRINK);
-@@ -146,17 +152,7 @@
+@@ -147,26 +_,7 @@
                  }
  
                  if (holder != null) {
@@ -25,23 +24,30 @@
 -                    this.usingTime = this.getMainHandItem().getUseDuration(this);
 -                    this.setUsingItem(true);
 -                    if (!this.isSilent()) {
--                        this.level().playSound((Player) null, this.getX(), this.getY(), this.getZ(), SoundEvents.WITCH_DRINK, this.getSoundSource(), 1.0F, 0.8F + this.random.nextFloat() * 0.4F);
+-                        this.level()
+-                            .playSound(
+-                                null,
+-                                this.getX(),
+-                                this.getY(),
+-                                this.getZ(),
+-                                SoundEvents.WITCH_DRINK,
+-                                this.getSoundSource(),
+-                                1.0F,
+-                                0.8F + this.random.nextFloat() * 0.4F
+-                            );
 -                    }
 -
--                    AttributeInstance attributemodifiable = this.getAttribute(Attributes.MOVEMENT_SPEED);
--
--                    attributemodifiable.removeModifier(Witch.SPEED_MODIFIER_DRINKING_ID);
--                    attributemodifiable.addTransientModifier(Witch.SPEED_MODIFIER_DRINKING);
+-                    AttributeInstance attribute = this.getAttribute(Attributes.MOVEMENT_SPEED);
+-                    attribute.removeModifier(SPEED_MODIFIER_DRINKING_ID);
+-                    attribute.addTransientModifier(SPEED_MODIFIER_DRINKING);
 +                    this.setDrinkingPotion(PotionContents.createItemStack(Items.POTION, holder)); // Paper - logic moved into setDrinkingPotion, copy exact impl into the method and then comment out
                  }
              }
  
-@@ -166,7 +162,24 @@
-         }
- 
+@@ -178,6 +_,23 @@
          super.aiStep();
-+    }
-+
+     }
+ 
 +    // Paper start - moved to its own method
 +    public void setDrinkingPotion(ItemStack potion) {
 +        potion = org.bukkit.craftbukkit.event.CraftEventFactory.handleWitchReadyPotionEvent(this, potion);
@@ -49,29 +55,30 @@
 +        this.usingTime = this.getMainHandItem().getUseDuration(this);
 +        this.setUsingItem(true);
 +        if (!this.isSilent()) {
-+            this.level().playSound((Player) null, this.getX(), this.getY(), this.getZ(), SoundEvents.WITCH_DRINK, this.getSoundSource(), 1.0F, 0.8F + this.random.nextFloat() * 0.4F);
++            this.level().playSound(null, this.getX(), this.getY(), this.getZ(), SoundEvents.WITCH_DRINK, this.getSoundSource(), 1.0F, 0.8F + this.random.nextFloat() * 0.4F);
 +        }
 +
-+        AttributeInstance attributemodifiable = this.getAttribute(Attributes.MOVEMENT_SPEED);
++        AttributeInstance attribute = this.getAttribute(Attributes.MOVEMENT_SPEED);
 +
-+        attributemodifiable.removeModifier(Witch.SPEED_MODIFIER_DRINKING_ID);
-+        attributemodifiable.addTransientModifier(Witch.SPEED_MODIFIER_DRINKING);
-     }
++        attribute.removeModifier(Witch.SPEED_MODIFIER_DRINKING_ID);
++        attribute.addTransientModifier(Witch.SPEED_MODIFIER_DRINKING);
++    }
 +    // Paper end
- 
++
      @Override
      public SoundEvent getCelebrateSound() {
-@@ -231,6 +244,13 @@
-                 ServerLevel worldserver = (ServerLevel) world;
-                 ItemStack itemstack = PotionContents.createItemStack(Items.SPLASH_POTION, holder);
+         return SoundEvents.WITCH_CELEBRATE;
+@@ -244,6 +_,13 @@
  
+             if (this.level() instanceof ServerLevel serverLevel) {
+                 ItemStack itemStack = PotionContents.createItemStack(Items.SPLASH_POTION, holder);
 +                // Paper start - WitchThrowPotionEvent
-+                com.destroystokyo.paper.event.entity.WitchThrowPotionEvent event = new com.destroystokyo.paper.event.entity.WitchThrowPotionEvent((org.bukkit.entity.Witch) this.getBukkitEntity(), (org.bukkit.entity.LivingEntity) target.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack));
++                com.destroystokyo.paper.event.entity.WitchThrowPotionEvent event = new com.destroystokyo.paper.event.entity.WitchThrowPotionEvent((org.bukkit.entity.Witch) this.getBukkitEntity(), (org.bukkit.entity.LivingEntity) target.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack));
 +                if (!event.callEvent()) {
 +                    return;
 +                }
-+                itemstack = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getPotion());
-+                // Paper end - WitchThrowPotionEvent
-                 Projectile.spawnProjectileUsingShoot(ThrownPotion::new, worldserver, itemstack, this, d0, d1 + d3 * 0.2D, d2, 0.75F, 8.0F);
++                itemStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getPotion());
++                // Paper end - WitchThrowPotionEven
+                 Projectile.spawnProjectileUsingShoot(ThrownPotion::new, serverLevel, itemStack, this, d, d1 + squareRoot * 0.2, d2, 0.75F, 8.0F);
              }
  
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/WitherSkeleton.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/WitherSkeleton.java.patch
new file mode 100644
index 0000000000..9780923155
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/WitherSkeleton.java.patch
@@ -0,0 +1,19 @@
+--- a/net/minecraft/world/entity/monster/WitherSkeleton.java
++++ b/net/minecraft/world/entity/monster/WitherSkeleton.java
+@@ -105,7 +_,7 @@
+             return false;
+         } else {
+             if (source instanceof LivingEntity) {
+-                ((LivingEntity)source).addEffect(new MobEffectInstance(MobEffects.WITHER, 200), this);
++                ((LivingEntity)source).addEffect(new MobEffectInstance(MobEffects.WITHER, 200), this, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ATTACK); // CraftBukkit
+             }
+ 
+             return true;
+@@ -121,6 +_,6 @@
+ 
+     @Override
+     public boolean canBeAffected(MobEffectInstance potioneffect) {
+-        return !potioneffect.is(MobEffects.WITHER) && super.canBeAffected(potioneffect);
++        return potioneffect.is(MobEffects.WITHER) && this.level().paperConfig().entities.mobEffects.immuneToWitherEffect.witherSkeleton ? false : super.canBeAffected(potioneffect); // Paper - Add config for mobs immune to default effects
+     }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/Zombie.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/Zombie.java.patch
new file mode 100644
index 0000000000..c3460fb7a5
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/Zombie.java.patch
@@ -0,0 +1,231 @@
+--- a/net/minecraft/world/entity/monster/Zombie.java
++++ b/net/minecraft/world/entity/monster/Zombie.java
+@@ -68,9 +_,7 @@
+ 
+ public class Zombie extends Monster {
+     private static final ResourceLocation SPEED_MODIFIER_BABY_ID = ResourceLocation.withDefaultNamespace("baby");
+-    private static final AttributeModifier SPEED_MODIFIER_BABY = new AttributeModifier(
+-        SPEED_MODIFIER_BABY_ID, 0.5, AttributeModifier.Operation.ADD_MULTIPLIED_BASE
+-    );
++    private final AttributeModifier babyModifier = new AttributeModifier(Zombie.SPEED_MODIFIER_BABY_ID, this.level().paperConfig().entities.behavior.babyZombieMovementModifier, AttributeModifier.Operation.ADD_MULTIPLIED_BASE); // Paper - Make baby speed configurable
+     private static final ResourceLocation REINFORCEMENT_CALLER_CHARGE_ID = ResourceLocation.withDefaultNamespace("reinforcement_caller_charge");
+     private static final AttributeModifier ZOMBIE_REINFORCEMENT_CALLEE_CHARGE = new AttributeModifier(
+         ResourceLocation.withDefaultNamespace("reinforcement_callee_charge"), -0.05F, AttributeModifier.Operation.ADD_VALUE
+@@ -87,13 +_,15 @@
+     private static final EntityDimensions BABY_DIMENSIONS = EntityType.ZOMBIE.getDimensions().scale(0.5F).withEyeHeight(0.93F);
+     private static final float BREAK_DOOR_CHANCE = 0.1F;
+     public static final Predicate<Difficulty> DOOR_BREAKING_PREDICATE = difficulty -> difficulty == Difficulty.HARD;
+-    private final BreakDoorGoal breakDoorGoal = new BreakDoorGoal(this, DOOR_BREAKING_PREDICATE);
++    private final BreakDoorGoal breakDoorGoal; // Paper - move down
+     private boolean canBreakDoors;
+     private int inWaterTime;
+     public int conversionTime;
++    private boolean shouldBurnInDay = true; // Paper - Add more Zombie API
+ 
+     public Zombie(EntityType<? extends Zombie> entityType, Level level) {
+         super(entityType, level);
++        this.breakDoorGoal = new BreakDoorGoal(this, com.google.common.base.Predicates.in(level.paperConfig().entities.behavior.doorBreakingDifficulty.getOrDefault(entityType, level.paperConfig().entities.behavior.doorBreakingDifficulty.get(EntityType.ZOMBIE)))); // Paper - Configurable door breaking difficulty
+     }
+ 
+     public Zombie(Level level) {
+@@ -102,7 +_,7 @@
+ 
+     @Override
+     protected void registerGoals() {
+-        this.goalSelector.addGoal(4, new Zombie.ZombieAttackTurtleEggGoal(this, 1.0, 3));
++        if (this.level().paperConfig().entities.behavior.zombiesTargetTurtleEggs) this.goalSelector.addGoal(4, new Zombie.ZombieAttackTurtleEggGoal(this, 1.0, 3)); // Paper - Add zombie targets turtle egg config
+         this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F));
+         this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));
+         this.addBehaviourGoals();
+@@ -114,7 +_,7 @@
+         this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0));
+         this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers(ZombifiedPiglin.class));
+         this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true));
+-        this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false));
++        if (this.level().spigotConfig.zombieAggressiveTowardsVillager) this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false)); // Spigot
+         this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, IronGolem.class, true));
+         this.targetSelector.addGoal(5, new NearestAttackableTargetGoal<>(this, Turtle.class, 10, true, false, Turtle.BABY_ON_LAND_SELECTOR));
+     }
+@@ -168,11 +_,16 @@
+ 
+     @Override
+     protected int getBaseExperienceReward(ServerLevel level) {
++        final int previousReward = this.xpReward; // Paper - store previous value to reset after calculating XP reward
+         if (this.isBaby()) {
+             this.xpReward = (int)(this.xpReward * 2.5);
+         }
+ 
+-        return super.getBaseExperienceReward(level);
++        // Paper start - store previous value to reset after calculating XP reward
++        int reward = super.getBaseExperienceReward(level);
++        this.xpReward = previousReward;
++        return reward;
++        // Paper end - store previous value to reset after calculating XP reward
+     }
+ 
+     @Override
+@@ -180,9 +_,9 @@
+         this.getEntityData().set(DATA_BABY_ID, childZombie);
+         if (this.level() != null && !this.level().isClientSide) {
+             AttributeInstance attribute = this.getAttribute(Attributes.MOVEMENT_SPEED);
+-            attribute.removeModifier(SPEED_MODIFIER_BABY_ID);
++            attribute.removeModifier(this.babyModifier.id()); // Paper - Make baby speed configurable
+             if (childZombie) {
+-                attribute.addTransientModifier(SPEED_MODIFIER_BABY);
++                attribute.addTransientModifier(this.babyModifier); // Paper - Make baby speed configurable
+             }
+         }
+     }
+@@ -251,7 +_,14 @@
+         super.aiStep();
+     }
+ 
++    // Paper start - Add more Zombie API
++    public void stopDrowning() {
++        this.conversionTime = -1;
++        this.getEntityData().set(Zombie.DATA_DROWNED_CONVERSION_ID, false);
++    }
++    // Paper end - Add more Zombie API
+     public void startUnderWaterConversion(int conversionTime) {
++        // this.lastTick = MinecraftServer.currentTick; // CraftBukkit // Paper - remove anti tick skipping measures / wall tim
+         this.conversionTime = conversionTime;
+         this.getEntityData().set(DATA_DROWNED_CONVERSION_ID, true);
+     }
+@@ -264,31 +_,49 @@
+     }
+ 
+     protected void convertToZombieType(EntityType<? extends Zombie> entityType) {
+-        this.convertTo(
++        Zombie converted = this.convertTo( // CraftBukkit
+             entityType,
+             ConversionParams.single(this, true, true),
+-            zombie -> zombie.handleAttributes(zombie.level().getCurrentDifficultyAt(zombie.blockPosition()).getSpecialMultiplier())
+-        );
++            // CraftBukkit start
++            zombie -> { zombie.handleAttributes(zombie.level().getCurrentDifficultyAt(zombie.blockPosition()).getSpecialMultiplier()); },
++            org.bukkit.event.entity.EntityTransformEvent.TransformReason.DROWNED, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DROWNED);
++        if (converted == null) {
++            ((org.bukkit.entity.Zombie) this.getBukkitEntity()).setConversionTime(-1); // CraftBukkit - SPIGOT-5208: End conversion to stop event spam
++        }
++        // CraftBukkit end
+     }
+ 
+     @VisibleForTesting
+     public boolean convertVillagerToZombieVillager(ServerLevel level, Villager villager) {
++        // CraftBukkit start
++        return Zombie.convertVillagerToZombieVillager(level, villager, this.blockPosition(), this.isSilent(), org.bukkit.event.entity.EntityTransformEvent.TransformReason.INFECTION, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.INFECTION) != null;
++    }
++
++    public static ZombieVillager convertVillagerToZombieVillager(ServerLevel level, Villager villager, net.minecraft.core.BlockPos blockPosition, boolean silent, org.bukkit.event.entity.EntityTransformEvent.TransformReason transformReason, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason creatureSpawnReason) {
++        // CraftBukkit end
+         ZombieVillager zombieVillager = villager.convertTo(EntityType.ZOMBIE_VILLAGER, ConversionParams.single(villager, true, true), mob -> {
+             mob.finalizeSpawn(level, level.getCurrentDifficultyAt(mob.blockPosition()), EntitySpawnReason.CONVERSION, new Zombie.ZombieGroupData(false, true));
+             mob.setVillagerData(villager.getVillagerData());
+             mob.setGossips(villager.getGossips().store(NbtOps.INSTANCE));
+             mob.setTradeOffers(villager.getOffers().copy());
+             mob.setVillagerXp(villager.getVillagerXp());
+-            if (!this.isSilent()) {
+-                level.levelEvent(null, 1026, this.blockPosition(), 0);
++            // CraftBukkit start
++            if (!silent) {
++                level.levelEvent(null, 1026, blockPosition, 0);
+             }
+-        });
+-        return zombieVillager != null;
++        }, transformReason, creatureSpawnReason);
++        return zombieVillager;
++        // CraftBukkit end
+     }
+ 
+     public boolean isSunSensitive() {
+-        return true;
+-    }
++        return this.shouldBurnInDay; // Paper - Add more Zombie API
++    }
++    // Paper start - Add more Zombie API
++    public void setShouldBurnInDay(boolean shouldBurnInDay) {
++        this.shouldBurnInDay = shouldBurnInDay;
++    }
++    // Paper end - Add more Zombie API
+ 
+     @Override
+     public boolean hurtServer(ServerLevel level, DamageSource damageSource, float amount) {
+@@ -321,13 +_,13 @@
+                     if (SpawnPlacements.isSpawnPositionOk(type, level, blockPos)
+                         && SpawnPlacements.checkSpawnRules(type, level, EntitySpawnReason.REINFORCEMENT, blockPos, level.random)) {
+                         zombie.setPos(i1, i2, i3);
+-                        if (!level.hasNearbyAlivePlayer(i1, i2, i3, 7.0)
++                        if (!level.hasNearbyAlivePlayerThatAffectsSpawning(i1, i2, i3, 7.0) // Paper - affects spawning api
+                             && level.isUnobstructed(zombie)
+                             && level.noCollision(zombie)
+                             && (zombie.canSpawnInLiquids() || !level.containsAnyLiquid(zombie.getBoundingBox()))) {
+-                            zombie.setTarget(target);
++                            zombie.setTarget(target, org.bukkit.event.entity.EntityTargetEvent.TargetReason.REINFORCEMENT_TARGET, true); // CraftBukkit
+                             zombie.finalizeSpawn(level, level.getCurrentDifficultyAt(zombie.blockPosition()), EntitySpawnReason.REINFORCEMENT, null);
+-                            level.addFreshEntityWithPassengers(zombie);
++                            level.addFreshEntityWithPassengers(zombie, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.REINFORCEMENTS); // CraftBukkit
+                             AttributeInstance attribute = this.getAttribute(Attributes.SPAWN_REINFORCEMENTS_CHANCE);
+                             AttributeModifier modifier = attribute.getModifier(REINFORCEMENT_CALLER_CHARGE_ID);
+                             double d = modifier != null ? modifier.amount() : 0.0;
+@@ -352,7 +_,14 @@
+         if (flag) {
+             float effectiveDifficulty = this.level().getCurrentDifficultyAt(this.blockPosition()).getEffectiveDifficulty();
+             if (this.getMainHandItem().isEmpty() && this.isOnFire() && this.random.nextFloat() < effectiveDifficulty * 0.3F) {
+-                source.igniteForSeconds(2 * (int)effectiveDifficulty);
++                // CraftBukkit start
++                org.bukkit.event.entity.EntityCombustByEntityEvent event = new org.bukkit.event.entity.EntityCombustByEntityEvent(this.getBukkitEntity(), source.getBukkitEntity(), (float) (2 * (int)effectiveDifficulty));
++                this.level().getCraftServer().getPluginManager().callEvent(event);
++
++                if (!event.isCancelled()) {
++                    source.igniteForSeconds(event.getDuration(), false);
++                }
++                // CraftBukkit end
+             }
+         }
+ 
+@@ -412,6 +_,7 @@
+         compound.putBoolean("CanBreakDoors", this.canBreakDoors());
+         compound.putInt("InWaterTime", this.isInWater() ? this.inWaterTime : -1);
+         compound.putInt("DrownedConversionTime", this.isUnderWaterConverting() ? this.conversionTime : -1);
++        compound.putBoolean("Paper.ShouldBurnInDay", this.shouldBurnInDay); // Paper - Add more Zombie API
+     }
+ 
+     @Override
+@@ -423,12 +_,18 @@
+         if (compound.contains("DrownedConversionTime", 99) && compound.getInt("DrownedConversionTime") > -1) {
+             this.startUnderWaterConversion(compound.getInt("DrownedConversionTime"));
+         }
++        // Paper start - Add more Zombie API
++        if (compound.contains("Paper.ShouldBurnInDay")) {
++            this.shouldBurnInDay = compound.getBoolean("Paper.ShouldBurnInDay");
++        }
++        // Paper end - Add more Zombie API
+     }
+ 
+     @Override
+     public boolean killedEntity(ServerLevel level, LivingEntity entity) {
+         boolean flag = super.killedEntity(level, entity);
+-        if ((level.getDifficulty() == Difficulty.NORMAL || level.getDifficulty() == Difficulty.HARD) && entity instanceof Villager villager) {
++        final double fallbackChance = level.getDifficulty() == Difficulty.HARD ? 100 : level.getDifficulty() == Difficulty.NORMAL ? 50 : 0; // Paper - Configurable chance of villager zombie infection
++        if (this.random.nextDouble() * 100 < level.paperConfig().entities.behavior.zombieVillagerInfectionChance.or(fallbackChance) && entity instanceof Villager villager) { // Paper - Configurable chance of villager zombie infection
+             if (level.getDifficulty() != Difficulty.HARD && this.random.nextBoolean()) {
+                 return flag;
+             }
+@@ -465,7 +_,7 @@
+         spawnGroupData = super.finalizeSpawn(level, difficulty, spawnReason, spawnGroupData);
+         float specialMultiplier = difficulty.getSpecialMultiplier();
+         if (spawnReason != EntitySpawnReason.CONVERSION) {
+-            this.setCanPickUpLoot(random.nextFloat() < 0.55F * specialMultiplier);
++            this.setCanPickUpLoot(this.level().paperConfig().entities.behavior.mobsCanAlwaysPickUpLoot.zombies || random.nextFloat() < 0.55F * specialMultiplier); // Paper - Add world settings for mobs picking up loot
+         }
+ 
+         if (spawnGroupData == null) {
+@@ -492,7 +_,7 @@
+                             chicken1.finalizeSpawn(level, difficulty, EntitySpawnReason.JOCKEY, null);
+                             chicken1.setChickenJockey(true);
+                             this.startRiding(chicken1);
+-                            level.addFreshEntity(chicken1);
++                            level.addFreshEntity(chicken1, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.MOUNT); // CraftBukkit
+                         }
+                     }
+                 }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/ZombieVillager.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/ZombieVillager.java.patch
new file mode 100644
index 0000000000..d86b3f0005
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/ZombieVillager.java.patch
@@ -0,0 +1,95 @@
+--- a/net/minecraft/world/entity/monster/ZombieVillager.java
++++ b/net/minecraft/world/entity/monster/ZombieVillager.java
+@@ -33,6 +_,7 @@
+ import net.minecraft.world.entity.SlotAccess;
+ import net.minecraft.world.entity.SpawnGroupData;
+ import net.minecraft.world.entity.ai.village.ReputationEventType;
++import net.minecraft.world.entity.npc.Villager;
+ import net.minecraft.world.entity.npc.VillagerData;
+ import net.minecraft.world.entity.npc.VillagerDataHolder;
+ import net.minecraft.world.entity.npc.VillagerProfession;
+@@ -68,6 +_,7 @@
+     @Nullable
+     private MerchantOffers tradeOffers;
+     private int villagerXp;
++    private int lastTick = net.minecraft.server.MinecraftServer.currentTick; // CraftBukkit - add field
+ 
+     public ZombieVillager(EntityType<? extends ZombieVillager> entityType, Level level) {
+         super(entityType, level);
+@@ -147,6 +_,7 @@
+         }
+ 
+         super.tick();
++        this.lastTick = net.minecraft.server.MinecraftServer.currentTick; // CraftBukkit
+     }
+ 
+     @Override
+@@ -183,12 +_,20 @@
+     }
+ 
+     public void startConverting(@Nullable UUID conversionStarter, int villagerConversionTime) {
++    // Paper start - missing entity behaviour api - converting without entity event
++        this.startConverting(conversionStarter, villagerConversionTime, true);
++    }
++
++    public void startConverting(@Nullable UUID conversionStarter, int villagerConversionTime, boolean broadcastEntityEvent) {
++    // Paper end - missing entity behaviour api - converting without entity event
+         this.conversionStarter = conversionStarter;
+         this.villagerConversionTime = villagerConversionTime;
+         this.getEntityData().set(DATA_CONVERTING_ID, true);
+-        this.removeEffect(MobEffects.WEAKNESS);
+-        this.addEffect(new MobEffectInstance(MobEffects.DAMAGE_BOOST, villagerConversionTime, Math.min(this.level().getDifficulty().getId() - 1, 0)));
+-        this.level().broadcastEntityEvent(this, (byte)16);
++        // CraftBukkit start
++        this.removeEffect(MobEffects.WEAKNESS, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.CONVERSION);
++        this.addEffect(new MobEffectInstance(MobEffects.DAMAGE_BOOST, villagerConversionTime, Math.min(this.level().getDifficulty().getId() - 1, 0)), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.CONVERSION);
++        // CraftBukkit end
++        if (broadcastEntityEvent) this.level().broadcastEntityEvent(this, (byte)16); // Paper - missing entity behaviour api - converting without entity event
+     }
+ 
+     @Override
+@@ -213,7 +_,7 @@
+     }
+ 
+     private void finishConversion(ServerLevel serverLevel) {
+-        this.convertTo(
++        Villager converted = this.convertTo( // CraftBukkit
+             EntityType.VILLAGER,
+             ConversionParams.single(this, false, false),
+             villager -> {
+@@ -223,6 +_,7 @@
+                     SlotAccess slot = villager.getSlot(equipmentSlot.getIndex() + 300);
+                     slot.set(this.getItemBySlot(equipmentSlot));
+                 }
++                this.forceDrops = false; // CraftBukkit
+ 
+                 villager.setVillagerData(this.getVillagerData());
+                 if (this.gossips != null) {
+@@ -237,19 +_,24 @@
+                 villager.finalizeSpawn(serverLevel, serverLevel.getCurrentDifficultyAt(villager.blockPosition()), EntitySpawnReason.CONVERSION, null);
+                 villager.refreshBrain(serverLevel);
+                 if (this.conversionStarter != null) {
+-                    Player playerByUuid = serverLevel.getPlayerByUUID(this.conversionStarter);
++                    Player playerByUuid = serverLevel.getGlobalPlayerByUUID(this.conversionStarter); // Paper - check global player list where appropriate
+                     if (playerByUuid instanceof ServerPlayer) {
+                         CriteriaTriggers.CURED_ZOMBIE_VILLAGER.trigger((ServerPlayer)playerByUuid, this, villager);
+                         serverLevel.onReputationEvent(ReputationEventType.ZOMBIE_VILLAGER_CURED, playerByUuid, villager);
+                     }
+                 }
+ 
+-                villager.addEffect(new MobEffectInstance(MobEffects.CONFUSION, 200, 0));
++                villager.addEffect(new MobEffectInstance(MobEffects.CONFUSION, 200, 0), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.CONVERSION); // CraftBukkit
+                 if (!this.isSilent()) {
+                     serverLevel.levelEvent(null, 1027, this.blockPosition(), 0);
+                 }
+-            }
++                // CraftBukkit start
++            }, org.bukkit.event.entity.EntityTransformEvent.TransformReason.CURED, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.CURED // CraftBukkit
+         );
++        if (converted == null) {
++            ((org.bukkit.entity.ZombieVillager) this.getBukkitEntity()).setConversionTime(-1); // SPIGOT-5208: End conversion to stop event spam
++        }
++        // CraftBukkit end
+     }
+ 
+     @VisibleForTesting
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/ZombifiedPiglin.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/ZombifiedPiglin.java.patch
new file mode 100644
index 0000000000..ea135560b2
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/ZombifiedPiglin.java.patch
@@ -0,0 +1,62 @@
+--- a/net/minecraft/world/entity/monster/ZombifiedPiglin.java
++++ b/net/minecraft/world/entity/monster/ZombifiedPiglin.java
+@@ -56,6 +_,7 @@
+     private static final int ALERT_RANGE_Y = 10;
+     private static final UniformInt ALERT_INTERVAL = TimeUtil.rangeOfSeconds(4, 6);
+     private int ticksUntilNextAlert;
++    private HurtByTargetGoal pathfinderGoalHurtByTarget; // Paper - fix PigZombieAngerEvent cancellation
+ 
+     public ZombifiedPiglin(EntityType<? extends ZombifiedPiglin> entityType, Level level) {
+         super(entityType, level);
+@@ -71,7 +_,7 @@
+     protected void addBehaviourGoals() {
+         this.goalSelector.addGoal(2, new ZombieAttackGoal(this, 1.0, false));
+         this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0));
+-        this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers());
++        this.targetSelector.addGoal(1, this.pathfinderGoalHurtByTarget = (new HurtByTargetGoal(this)).setAlertOthers()); // Paper - fix PigZombieAngerEvent cancellation
+         this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, this::isAngryAt));
+         this.targetSelector.addGoal(3, new ResetUniversalAngerTargetGoal<>(this, true));
+     }
+@@ -148,7 +_,7 @@
+             .filter(zombifiedPiglin -> zombifiedPiglin != this)
+             .filter(zombifiedPiglin -> zombifiedPiglin.getTarget() == null)
+             .filter(zombifiedPiglin -> !zombifiedPiglin.isAlliedTo(this.getTarget()))
+-            .forEach(zombifiedPiglin -> zombifiedPiglin.setTarget(this.getTarget()));
++            .forEach(zombifiedPiglin -> zombifiedPiglin.setTarget(this.getTarget(), org.bukkit.event.entity.EntityTargetEvent.TargetReason.TARGET_ATTACKED_NEARBY_ENTITY, true)); // CraftBukkit
+     }
+ 
+     private void playAngerSound() {
+@@ -156,7 +_,7 @@
+     }
+ 
+     @Override
+-    public void setTarget(@Nullable LivingEntity livingEntity) {
++    public boolean setTarget(@Nullable LivingEntity livingEntity, org.bukkit.event.entity.EntityTargetEvent.TargetReason reason, boolean fireEvent) { // CraftBukkit - signature
+         if (this.getTarget() == null && livingEntity != null) {
+             this.playFirstAngerSoundIn = FIRST_ANGER_SOUND_DELAY.sample(this.random);
+             this.ticksUntilNextAlert = ALERT_INTERVAL.sample(this.random);
+@@ -166,12 +_,22 @@
+             this.setLastHurtByPlayer((Player)livingEntity);
+         }
+ 
+-        super.setTarget(livingEntity);
++        return super.setTarget(livingEntity, reason, fireEvent); // CraftBukkit
+     }
+ 
+     @Override
+     public void startPersistentAngerTimer() {
+-        this.setRemainingPersistentAngerTime(PERSISTENT_ANGER_TIME.sample(this.random));
++        // CraftBukkit start
++        net.minecraft.world.entity.Entity entity = ((ServerLevel) this.level()).getEntity(this.getPersistentAngerTarget());
++        org.bukkit.event.entity.PigZombieAngerEvent event = new org.bukkit.event.entity.PigZombieAngerEvent((org.bukkit.entity.PigZombie) this.getBukkitEntity(), (entity == null) ? null : entity.getBukkitEntity(), ZombifiedPiglin.PERSISTENT_ANGER_TIME.sample(this.random));
++        this.level().getCraftServer().getPluginManager().callEvent(event);
++        if (event.isCancelled()) {
++            this.setPersistentAngerTarget(null);
++            this.pathfinderGoalHurtByTarget.stop(); // Paper - fix PigZombieAngerEvent cancellation
++            return;
++        }
++        this.setRemainingPersistentAngerTime(event.getNewAnger());
++        // CraftBukkit end
+     }
+ 
+     public static boolean checkZombifiedPiglinSpawnRules(
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/breeze/Breeze.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/breeze/Breeze.java.patch
similarity index 64%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/monster/breeze/Breeze.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/monster/breeze/Breeze.java.patch
index b3c69c8fbb..b0ccae8fcd 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/breeze/Breeze.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/breeze/Breeze.java.patch
@@ -1,15 +1,6 @@
 --- a/net/minecraft/world/entity/monster/breeze/Breeze.java
 +++ b/net/minecraft/world/entity/monster/breeze/Breeze.java
-@@ -77,7 +77,7 @@
- 
-     @Override
-     public Brain<Breeze> getBrain() {
--        return super.getBrain();
-+        return (Brain<Breeze>) super.getBrain(); // CraftBukkit - decompile error
-     }
- 
-     @Override
-@@ -252,6 +252,7 @@
+@@ -250,6 +_,7 @@
  
      @Override
      public boolean canAttackType(EntityType<?> type) {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/creaking/Creaking.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/creaking/Creaking.java.patch
new file mode 100644
index 0000000000..3c8cc3d167
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/creaking/Creaking.java.patch
@@ -0,0 +1,35 @@
+--- a/net/minecraft/world/entity/monster/creaking/Creaking.java
++++ b/net/minecraft/world/entity/monster/creaking/Creaking.java
+@@ -190,9 +_,9 @@
+     }
+ 
+     @Override
+-    public void push(double x, double y, double z) {
++    public void push(double x, double y, double z, @Nullable Entity pushingEntity) { // Paper - add push source entity param
+         if (this.canMove()) {
+-            super.push(x, y, z);
++            super.push(x, y, z, pushingEntity); // Paper - add push source entity param
+         }
+     }
+ 
+@@ -317,7 +_,7 @@
+         }
+ 
+         this.makeSound(this.getDeathSound());
+-        this.remove(Entity.RemovalReason.DISCARDED);
++        this.remove(Entity.RemovalReason.DISCARDED, null); // CraftBukkit - add Bukkit remove cause
+     }
+ 
+     public void creakingDeathEffects(DamageSource damageSource) {
+@@ -480,9 +_,9 @@
+     }
+ 
+     @Override
+-    public void knockback(double strength, double x, double z) {
++    public void knockback(double strength, double x, double z, @Nullable Entity attacker, io.papermc.paper.event.entity.EntityKnockbackEvent.Cause cause) { // Paper - knockback events
+         if (this.canMove()) {
+-            super.knockback(strength, x, z);
++            super.knockback(strength, x, z, attacker, cause); // Paper - knockback events
+         }
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/hoglin/Hoglin.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/hoglin/Hoglin.java.patch
new file mode 100644
index 0000000000..a7f4ebaaef
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/hoglin/Hoglin.java.patch
@@ -0,0 +1,16 @@
+--- a/net/minecraft/world/entity/monster/hoglin/Hoglin.java
++++ b/net/minecraft/world/entity/monster/hoglin/Hoglin.java
+@@ -262,7 +_,12 @@
+     }
+ 
+     private void finishConversion() {
+-        this.convertTo(EntityType.ZOGLIN, ConversionParams.single(this, true, false), mob -> mob.addEffect(new MobEffectInstance(MobEffects.CONFUSION, 200, 0)));
++        final Entity converted = this.convertTo(EntityType.ZOGLIN, ConversionParams.single(this, true, false),  mob -> {mob.addEffect(new MobEffectInstance(MobEffects.CONFUSION, 200, 0));}, org.bukkit.event.entity.EntityTransformEvent.TransformReason.PIGLIN_ZOMBIFIED, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.PIGLIN_ZOMBIFIED); // CraftBukkit - add spawn and transform reasons
++        // Paper start - Fix issues with mob conversion; reset to prevent event spam
++        if (converted == null) {
++            this.timeInOverworld = 0;
++        }
++        // Paper end - Fix issues with mob conversion
+     }
+ 
+     @Override
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/hoglin/HoglinBase.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/hoglin/HoglinBase.java.patch
new file mode 100644
index 0000000000..273ab55a4d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/hoglin/HoglinBase.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/monster/hoglin/HoglinBase.java
++++ b/net/minecraft/world/entity/monster/hoglin/HoglinBase.java
+@@ -45,7 +_,7 @@
+             double d3 = d * (hoglin.level().random.nextFloat() * 0.5F + 0.2F);
+             Vec3 vec3 = new Vec3(d1, 0.0, d2).normalize().scale(d3).yRot(f);
+             double d4 = d * hoglin.level().random.nextFloat() * 0.5;
+-            target.push(vec3.x, d4, vec3.z);
++            target.push(vec3.x, d4, vec3.z, hoglin); // Paper - Add EntityKnockbackByEntityEvent and EntityPushedByEntityAttackEvent
+             target.hurtMarked = true;
+         }
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/piglin/AbstractPiglin.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/piglin/AbstractPiglin.java.patch
new file mode 100644
index 0000000000..d17978051b
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/piglin/AbstractPiglin.java.patch
@@ -0,0 +1,19 @@
+--- a/net/minecraft/world/entity/monster/piglin/AbstractPiglin.java
++++ b/net/minecraft/world/entity/monster/piglin/AbstractPiglin.java
+@@ -99,9 +_,14 @@
+     }
+ 
+     protected void finishConversion(ServerLevel serverLevel) {
+-        this.convertTo(
+-            EntityType.ZOMBIFIED_PIGLIN, ConversionParams.single(this, true, true), mob -> mob.addEffect(new MobEffectInstance(MobEffects.CONFUSION, 200, 0))
++        net.minecraft.world.entity.Entity converted = this.convertTo( // Paper - Fix issues with mob conversion; reset to prevent event spam
++            EntityType.ZOMBIFIED_PIGLIN, ConversionParams.single(this, true, true), mob -> {mob.addEffect(new MobEffectInstance(MobEffects.CONFUSION, 200, 0));}, org.bukkit.event.entity.EntityTransformEvent.TransformReason.PIGLIN_ZOMBIFIED, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.PIGLIN_ZOMBIFIED // CraftBukkit - add spawn and transform reasons
+         );
++        // Paper start - Fix issues with mob conversion; reset to prevent event spam
++        if (converted == null) {
++            this.timeInOverworld = 0;
++        }
++        // Paper end - Fix issues with mob conversion
+     }
+ 
+     public boolean isAdult() {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/piglin/Piglin.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/piglin/Piglin.java.patch
new file mode 100644
index 0000000000..1178c733ba
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/piglin/Piglin.java.patch
@@ -0,0 +1,103 @@
+--- a/net/minecraft/world/entity/monster/piglin/Piglin.java
++++ b/net/minecraft/world/entity/monster/piglin/Piglin.java
+@@ -59,6 +_,25 @@
+ import net.minecraft.world.level.ServerLevelAccessor;
+ import net.minecraft.world.level.block.Blocks;
+ import net.minecraft.world.level.block.state.BlockState;
++// CraftBukkit start
++import java.util.stream.Collectors;
++import java.util.HashSet;
++import java.util.Set;
++import net.minecraft.core.BlockPos;
++import net.minecraft.core.registries.BuiltInRegistries;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.nbt.ListTag;
++import net.minecraft.nbt.StringTag;
++import net.minecraft.nbt.Tag;
++import net.minecraft.network.syncher.EntityDataAccessor;
++import net.minecraft.network.syncher.EntityDataSerializers;
++import net.minecraft.network.syncher.SynchedEntityData;
++import net.minecraft.resources.ResourceLocation;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.sounds.SoundEvent;
++import net.minecraft.sounds.SoundEvents;
++import net.minecraft.world.item.Item;
++// CraftBukkit end
+ 
+ public class Piglin extends AbstractPiglin implements CrossbowAttackMob, InventoryCarrier {
+     private static final EntityDataAccessor<Boolean> DATA_BABY_ID = SynchedEntityData.defineId(Piglin.class, EntityDataSerializers.BOOLEAN);
+@@ -122,6 +_,10 @@
+         MemoryModuleType.ATE_RECENTLY,
+         MemoryModuleType.NEAREST_REPELLENT
+     );
++    // CraftBukkit start - Custom bartering and interest list
++    public Set<Item> allowedBarterItems = new HashSet<>();
++    public Set<Item> interestItems = new HashSet<>();
++    // CraftBukkit end
+ 
+     public Piglin(EntityType<? extends AbstractPiglin> entityType, Level level) {
+         super(entityType, level);
+@@ -140,6 +_,14 @@
+         }
+ 
+         this.writeInventoryToTag(compound, this.registryAccess());
++        // CraftBukkit start
++        ListTag barterList = new ListTag();
++        this.allowedBarterItems.stream().map(BuiltInRegistries.ITEM::getKey).map(ResourceLocation::toString).map(StringTag::valueOf).forEach(barterList::add);
++        compound.put("Bukkit.BarterList", barterList);
++        ListTag interestList = new ListTag();
++        this.interestItems.stream().map(BuiltInRegistries.ITEM::getKey).map(ResourceLocation::toString).map(StringTag::valueOf).forEach(interestList::add);
++        compound.put("Bukkit.InterestList", interestList);
++        // CraftBukkit end
+     }
+ 
+     @Override
+@@ -148,6 +_,10 @@
+         this.setBaby(compound.getBoolean("IsBaby"));
+         this.setCannotHunt(compound.getBoolean("CannotHunt"));
+         this.readInventoryFromTag(compound, this.registryAccess());
++        // CraftBukkit start
++        this.allowedBarterItems = compound.getList("Bukkit.BarterList", 8).stream().map(Tag::getAsString).map(ResourceLocation::tryParse).map(BuiltInRegistries.ITEM::getValue).collect(Collectors.toCollection(HashSet::new));
++        this.interestItems = compound.getList("Bukkit.InterestList", 8).stream().map(Tag::getAsString).map(ResourceLocation::tryParse).map(BuiltInRegistries.ITEM::getValue).collect(Collectors.toCollection(HashSet::new));
++        // CraftBukkit end
+     }
+ 
+     @VisibleForDebug
+@@ -325,7 +_,9 @@
+     @Override
+     protected void finishConversion(ServerLevel serverLevel) {
+         PiglinAi.cancelAdmiring(serverLevel, this);
++        this.forceDrops = true; // Paper - Add missing forceDrop toggles
+         this.inventory.removeAllItems().forEach(itemStack -> this.spawnAtLocation(serverLevel, itemStack));
++        this.forceDrops = false; // Paper - Add missing forceDrop toggles
+         super.finishConversion(serverLevel);
+     }
+ 
+@@ -400,7 +_,7 @@
+     }
+ 
+     protected void holdInOffHand(ItemStack stack) {
+-        if (stack.is(PiglinAi.BARTERING_ITEM)) {
++        if (stack.is(PiglinAi.BARTERING_ITEM) || this.allowedBarterItems.contains(stack.getItem())) { // CraftBukkit - Changes to accept custom payment items
+             this.setItemSlot(EquipmentSlot.OFFHAND, stack);
+             this.setGuaranteedDrop(EquipmentSlot.OFFHAND);
+         } else {
+@@ -425,15 +_,15 @@
+             return false;
+         } else {
+             TagKey<Item> preferredWeaponType = this.getPreferredWeaponType();
+-            boolean flag = PiglinAi.isLovedItem(newItem) || preferredWeaponType != null && newItem.is(preferredWeaponType);
+-            boolean flag1 = PiglinAi.isLovedItem(currentItem) || preferredWeaponType != null && currentItem.is(preferredWeaponType);
++            boolean flag = PiglinAi.isLovedItem(newItem, this) || preferredWeaponType != null && newItem.is(preferredWeaponType); // CraftBukkit
++            boolean flag1 = PiglinAi.isLovedItem(currentItem, this) || preferredWeaponType != null && currentItem.is(preferredWeaponType); // CraftBukkit
+             return flag && !flag1 || (flag || !flag1) && super.canReplaceCurrentItem(newItem, currentItem, slot);
+         }
+     }
+ 
+     @Override
+     protected void pickUpItem(ServerLevel level, ItemEntity entity) {
+-        this.onItemPickup(entity);
++        // this.onItemPickup(entity); // Paper - EntityPickupItemEvent fixes; call in PiglinAi#pickUpItem after EntityPickupItemEvent is fired
+         PiglinAi.pickUpItem(level, this, entity);
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/piglin/PiglinAi.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/piglin/PiglinAi.java.patch
new file mode 100644
index 0000000000..467622eb81
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/piglin/PiglinAi.java.patch
@@ -0,0 +1,174 @@
+--- a/net/minecraft/world/entity/monster/piglin/PiglinAi.java
++++ b/net/minecraft/world/entity/monster/piglin/PiglinAi.java
+@@ -70,6 +_,13 @@
+ import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets;
+ import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
+ import net.minecraft.world.phys.Vec3;
++// CraftBukkit start
++import java.util.stream.Collectors;
++import org.bukkit.craftbukkit.event.CraftEventFactory;
++import org.bukkit.craftbukkit.inventory.CraftItemStack;
++import org.bukkit.event.entity.EntityRemoveEvent;
++import org.bukkit.event.entity.PiglinBarterEvent;
++// CraftBukkit end
+ 
+ public class PiglinAi {
+     public static final int REPELLENT_DETECTION_RANGE_HORIZONTAL = 8;
+@@ -328,23 +_,32 @@
+     protected static void pickUpItem(ServerLevel level, Piglin piglin, ItemEntity itemEntity) {
+         stopWalking(piglin);
+         ItemStack item;
+-        if (itemEntity.getItem().is(Items.GOLD_NUGGET)) {
++        // CraftBukkit start
++        // Paper start - EntityPickupItemEvent fixes; fix event firing twice
++        if (itemEntity.getItem().is(Items.GOLD_NUGGET)) { // Paper
++            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPickupItemEvent(piglin, itemEntity, 0, false).isCancelled()) return;
++            piglin.onItemPickup(itemEntity); // Paper - moved from Piglin#pickUpItem - call prior to item entity modification
++            // Paper end
+             piglin.take(itemEntity, itemEntity.getItem().getCount());
+             item = itemEntity.getItem();
+-            itemEntity.discard();
+-        } else {
++            itemEntity.discard(EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
++        } else if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPickupItemEvent(piglin, itemEntity, itemEntity.getItem().getCount() - 1, false).isCancelled()) {
++            piglin.onItemPickup(itemEntity); // Paper - EntityPickupItemEvent fixes; moved from Piglin#pickUpItem - call prior to item entity modification
+             piglin.take(itemEntity, 1);
+             item = removeOneItemFromItemEntity(itemEntity);
++        } else {
++            return;
++            // CraftBukkit end
+         }
+ 
+-        if (isLovedItem(item)) {
++        if (isLovedItem(item, piglin)) { // CraftBukkit - Changes to allow for custom payment in bartering
+             piglin.getBrain().eraseMemory(MemoryModuleType.TIME_TRYING_TO_REACH_ADMIRE_ITEM);
+             holdInOffhand(level, piglin, item);
+             admireGoldItem(piglin);
+         } else if (isFood(item) && !hasEatenRecently(piglin)) {
+             eat(piglin);
+         } else {
+-            boolean flag = !piglin.equipItemIfPossible(level, item).equals(ItemStack.EMPTY);
++            boolean flag = !piglin.equipItemIfPossible(level, item, null).equals(ItemStack.EMPTY); // CraftBukkit // Paper - pass null item entity to prevent duplicate pickup item event call - called above.
+             if (!flag) {
+                 putInInventory(piglin, item);
+             }
+@@ -353,7 +_,9 @@
+ 
+     private static void holdInOffhand(ServerLevel level, Piglin piglin, ItemStack stack) {
+         if (isHoldingItemInOffHand(piglin)) {
++            piglin.forceDrops = true; // Paper - Add missing forceDrop toggles
+             piglin.spawnAtLocation(level, piglin.getItemInHand(InteractionHand.OFF_HAND));
++            piglin.forceDrops = false; // Paper - Add missing forceDrop toggles
+         }
+ 
+         piglin.holdInOffHand(stack);
+@@ -363,7 +_,7 @@
+         ItemStack item = itemEntity.getItem();
+         ItemStack itemStack = item.split(1);
+         if (item.isEmpty()) {
+-            itemEntity.discard();
++            itemEntity.discard(EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
+         } else {
+             itemEntity.setItem(item);
+         }
+@@ -375,9 +_,14 @@
+         ItemStack itemInHand = piglin.getItemInHand(InteractionHand.OFF_HAND);
+         piglin.setItemInHand(InteractionHand.OFF_HAND, ItemStack.EMPTY);
+         if (piglin.isAdult()) {
+-            boolean isBarterCurrency = isBarterCurrency(itemInHand);
++            boolean isBarterCurrency = isBarterCurrency(itemInHand, piglin); // CraftBukkit - Changes to allow custom payment for bartering
+             if (barter && isBarterCurrency) {
+-                throwItems(piglin, getBarterResponseItems(piglin));
++                // CraftBukkit start
++                PiglinBarterEvent event = CraftEventFactory.callPiglinBarterEvent(piglin, getBarterResponseItems(piglin), itemInHand);
++                if (!event.isCancelled()) {
++                    throwItems(piglin, event.getOutcome().stream().map(CraftItemStack::asNMSCopy).collect(Collectors.toList()));
++                }
++                // CraftBukkit end
+             } else if (!isBarterCurrency) {
+                 boolean flag = !piglin.equipItemIfPossible(level, itemInHand).isEmpty();
+                 if (!flag) {
+@@ -388,7 +_,7 @@
+             boolean isBarterCurrency = !piglin.equipItemIfPossible(level, itemInHand).isEmpty();
+             if (!isBarterCurrency) {
+                 ItemStack mainHandItem = piglin.getMainHandItem();
+-                if (isLovedItem(mainHandItem)) {
++                if (isLovedItem(mainHandItem, piglin)) { // CraftBukkit - Changes to allow for custom payment in bartering
+                     putInInventory(piglin, mainHandItem);
+                 } else {
+                     throwItems(piglin, Collections.singletonList(mainHandItem));
+@@ -401,7 +_,9 @@
+ 
+     protected static void cancelAdmiring(ServerLevel level, Piglin piglin) {
+         if (isAdmiringItem(piglin) && !piglin.getOffhandItem().isEmpty()) {
++            piglin.forceDrops = true; // Paper - Add missing forceDrop toggles
+             piglin.spawnAtLocation(level, piglin.getOffhandItem());
++            piglin.forceDrops = false; // Paper - Add missing forceDrop toggles
+             piglin.setItemInHand(InteractionHand.OFF_HAND, ItemStack.EMPTY);
+         }
+     }
+@@ -457,7 +_,7 @@
+             return false;
+         } else if (isAdmiringDisabled(piglin) && piglin.getBrain().hasMemoryValue(MemoryModuleType.ATTACK_TARGET)) {
+             return false;
+-        } else if (isBarterCurrency(stack)) {
++        } else if (isBarterCurrency(stack, piglin)) { // CraftBukkit
+             return isNotHoldingLovedItemInOffHand(piglin);
+         } else {
+             boolean canAddToInventory = piglin.canAddToInventory(stack);
+@@ -466,11 +_,16 @@
+             } else if (isFood(stack)) {
+                 return !hasEatenRecently(piglin) && canAddToInventory;
+             } else {
+-                return !isLovedItem(stack) ? piglin.canReplaceCurrentItem(stack) : isNotHoldingLovedItemInOffHand(piglin) && canAddToInventory;
++                return !isLovedItem(stack, piglin) ? piglin.canReplaceCurrentItem(stack) : isNotHoldingLovedItemInOffHand(piglin) && canAddToInventory; // Paper - upstream missed isLovedItem check
+             }
+         }
+     }
+ 
++    // CraftBukkit start - Added method to allow checking for custom payment items
++    protected static boolean isLovedItem(ItemStack item, Piglin piglin) {
++        return PiglinAi.isLovedItem(item) || (piglin.interestItems.contains(item.getItem()) || piglin.allowedBarterItems.contains(item.getItem()));
++    }
++    // CraftBukkit end
+     protected static boolean isLovedItem(ItemStack item) {
+         return item.is(ItemTags.PIGLIN_LOVED);
+     }
+@@ -522,6 +_,7 @@
+     }
+ 
+     public static void angerNearbyPiglins(ServerLevel level, Player player, boolean requireLineOfSight) {
++        if (!player.level().paperConfig().entities.behavior.piglinsGuardChests) return; // Paper - Config option for Piglins guarding chests
+         List<Piglin> entitiesOfClass = player.level().getEntitiesOfClass(Piglin.class, player.getBoundingBox().inflate(16.0));
+         entitiesOfClass.stream().filter(PiglinAi::isIdle).filter(piglin -> !requireLineOfSight || BehaviorUtils.canSee(piglin, player)).forEach(piglin -> {
+             if (level.getGameRules().getBoolean(GameRules.RULE_UNIVERSAL_ANGER)) {
+@@ -546,7 +_,7 @@
+     }
+ 
+     protected static boolean canAdmire(Piglin piglin, ItemStack stack) {
+-        return !isAdmiringDisabled(piglin) && !isAdmiringItem(piglin) && piglin.isAdult() && isBarterCurrency(stack);
++        return !isAdmiringDisabled(piglin) && !isAdmiringItem(piglin) && piglin.isAdult() && isBarterCurrency(stack, piglin); // CraftBukkit
+     }
+ 
+     protected static void wasHurtBy(ServerLevel level, Piglin piglin, LivingEntity entity) {
+@@ -794,6 +_,11 @@
+         return piglin.getBrain().hasMemoryValue(MemoryModuleType.ADMIRING_ITEM);
+     }
+ 
++    // CraftBukkit start - Changes to allow custom payment for bartering
++    private static boolean isBarterCurrency(ItemStack item, Piglin piglin) {
++        return PiglinAi.isBarterCurrency(item) || piglin.allowedBarterItems.contains(item.getItem());
++    }
++    // CraftBukkit end
+     private static boolean isBarterCurrency(ItemStack stack) {
+         return stack.is(BARTERING_ITEM);
+     }
+@@ -831,7 +_,7 @@
+     }
+ 
+     private static boolean isNotHoldingLovedItemInOffHand(Piglin piglin) {
+-        return piglin.getOffhandItem().isEmpty() || !isLovedItem(piglin.getOffhandItem());
++        return piglin.getOffhandItem().isEmpty() || !isLovedItem(piglin.getOffhandItem(), piglin); // CraftBukkit - Changes to allow custom payment for bartering
+     }
+ 
+     public static boolean isZombified(EntityType<?> entityType) {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/warden/AngerManagement.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/warden/AngerManagement.java.patch
new file mode 100644
index 0000000000..12bd7bec5a
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/warden/AngerManagement.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/monster/warden/AngerManagement.java
++++ b/net/minecraft/world/entity/monster/warden/AngerManagement.java
+@@ -146,7 +_,7 @@
+ 
+     public int increaseAnger(Entity entity, int offset) {
+         boolean flag = !this.angerBySuspect.containsKey(entity);
+-        int i = this.angerBySuspect.computeInt(entity, (entity1, integer) -> Math.min(150, (integer == null ? 0 : integer) + offset));
++        int i = this.angerBySuspect.computeInt(entity, (entity1, integer) -> Math.min(150, (integer == null ? 0 : integer) + offset)); // Paper - diff on change (Warden#increaseAngerAt WardenAngerChangeEvent)
+         if (flag) {
+             int i1 = this.angerByUuid.removeInt(entity.getUUID());
+             i += i1;
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/warden/Warden.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/warden/Warden.java.patch
new file mode 100644
index 0000000000..f6da9a7ec3
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/warden/Warden.java.patch
@@ -0,0 +1,27 @@
+--- a/net/minecraft/world/entity/monster/warden/Warden.java
++++ b/net/minecraft/world/entity/monster/warden/Warden.java
+@@ -407,7 +_,7 @@
+ 
+     public static void applyDarknessAround(ServerLevel level, Vec3 pos, @Nullable Entity source, int radius) {
+         MobEffectInstance mobEffectInstance = new MobEffectInstance(MobEffects.DARKNESS, 260, 0, false, false);
+-        MobEffectUtil.addEffectToPlayersAround(level, source, pos, radius, mobEffectInstance, 200);
++        MobEffectUtil.addEffectToPlayersAround(level, source, pos, radius, mobEffectInstance, 200, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.WARDEN); // CraftBukkit - Add EntityPotionEffectEvent.Cause
+     }
+ 
+     @Override
+@@ -469,6 +_,15 @@
+     @VisibleForTesting
+     public void increaseAngerAt(@Nullable Entity entity, int offset, boolean playListeningSound) {
+         if (!this.isNoAi() && this.canTargetEntity(entity)) {
++            // Paper start - Add WardenAngerChangeEvent
++            int activeAnger = this.angerManagement.getActiveAnger(entity);
++            io.papermc.paper.event.entity.WardenAngerChangeEvent event = new io.papermc.paper.event.entity.WardenAngerChangeEvent((org.bukkit.entity.Warden) this.getBukkitEntity(), entity.getBukkitEntity(), activeAnger, Math.min(150, activeAnger + offset));
++            this.level().getCraftServer().getPluginManager().callEvent(event);
++            if (event.isCancelled()) {
++                return;
++            }
++            offset = event.getNewAnger() - activeAnger;
++            // Paper end - Add WardenAngerChangeEvent
+             WardenAi.setDigCooldown(this);
+             boolean flag = !(this.getTarget() instanceof Player);
+             int i = this.angerManagement.increaseAnger(entity, offset);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/npc/AbstractVillager.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/npc/AbstractVillager.java.patch
similarity index 64%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/npc/AbstractVillager.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/npc/AbstractVillager.java.patch
index ba7ad87dd7..06aabb0b2f 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/npc/AbstractVillager.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/npc/AbstractVillager.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/entity/npc/AbstractVillager.java
 +++ b/net/minecraft/world/entity/npc/AbstractVillager.java
-@@ -40,8 +40,21 @@
+@@ -37,7 +_,20 @@
  import net.minecraft.world.phys.Vec3;
  import org.slf4j.Logger;
  
@@ -12,7 +12,6 @@
 +// CraftBukkit end
 +
  public abstract class AbstractVillager extends AgeableMob implements InventoryCarrier, Npc, Merchant {
- 
 +    // CraftBukkit start
 +    @Override
 +    public CraftMerchant getCraftMerchant() {
@@ -22,16 +21,16 @@
      private static final EntityDataAccessor<Integer> DATA_UNHAPPY_COUNTER = SynchedEntityData.defineId(AbstractVillager.class, EntityDataSerializers.INT);
      private static final Logger LOGGER = LogUtils.getLogger();
      public static final int VILLAGER_SLOT_OFFSET = 300;
-@@ -50,7 +63,7 @@
+@@ -46,7 +_,7 @@
      private Player tradingPlayer;
      @Nullable
      protected MerchantOffers offers;
 -    private final SimpleContainer inventory = new SimpleContainer(8);
-+    private final SimpleContainer inventory = new SimpleContainer(8, (org.bukkit.craftbukkit.entity.CraftAbstractVillager) this.getBukkitEntity()); // CraftBukkit add argument
++    private final SimpleContainer inventory = new SimpleContainer(8, (org.bukkit.craftbukkit.entity.CraftAbstractVillager) this.getBukkitEntity()); // CraftBukkit - add argument
  
-     public AbstractVillager(EntityType<? extends AbstractVillager> type, Level world) {
-         super(type, world);
-@@ -101,6 +114,13 @@
+     public AbstractVillager(EntityType<? extends AbstractVillager> entityType, Level level) {
+         super(entityType, level);
+@@ -99,6 +_,13 @@
          return this.tradingPlayer != null;
      }
  
@@ -45,50 +44,40 @@
      @Override
      public MerchantOffers getOffers() {
          if (this.level().isClientSide) {
-@@ -121,11 +141,24 @@
-     @Override
-     public void overrideXp(int experience) {}
+@@ -121,11 +_,24 @@
+     public void overrideXp(int xp) {
+     }
  
 +    // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent
-     @Override
--    public void notifyTrade(MerchantOffer offer) {
--        offer.increaseUses();
-+    public void processTrade(MerchantOffer recipe, @Nullable io.papermc.paper.event.player.PlayerPurchaseEvent event) { // The MerchantRecipe passed in here is the one set by the PlayerPurchaseEvent
++    @Override
++    public void processTrade(MerchantOffer offer, @Nullable io.papermc.paper.event.player.PlayerPurchaseEvent event) { // The MerchantRecipe passed in here is the one set by the PlayerPurchaseEvent
 +        if (event == null || event.willIncreaseTradeUses()) {
-+            recipe.increaseUses();
++            offer.increaseUses();
 +        }
 +        if (event == null || event.isRewardingExp()) {
-+            this.rewardTradeXp(recipe);
++            this.rewardTradeXp(offer);
 +        }
-+        this.notifyTrade(recipe);
++        this.notifyTrade(offer);
 +    }
 +    // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent
 +
-+    @Override
-+    public void notifyTrade(MerchantOffer offer) {
+     @Override
+     public void notifyTrade(MerchantOffer offer) {
+-        offer.increaseUses();
 +        // offer.increaseUses(); // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
          this.ambientSoundTime = -this.getAmbientSoundInterval();
 -        this.rewardTradeXp(offer);
 +        // this.rewardTradeXp(offer); // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
          if (this.tradingPlayer instanceof ServerPlayer) {
-             CriteriaTriggers.TRADE.trigger((ServerPlayer) this.tradingPlayer, this, offer.getResult());
+             CriteriaTriggers.TRADE.trigger((ServerPlayer)this.tradingPlayer, this, offer.getResult());
          }
-@@ -179,7 +212,7 @@
-     public void readAdditionalSaveData(CompoundTag nbt) {
-         super.readAdditionalSaveData(nbt);
-         if (nbt.contains("Offers")) {
--            DataResult dataresult = MerchantOffers.CODEC.parse(this.registryAccess().createSerializationContext(NbtOps.INSTANCE), nbt.get("Offers"));
-+            DataResult<MerchantOffers> dataresult = MerchantOffers.CODEC.parse(this.registryAccess().createSerializationContext(NbtOps.INSTANCE), nbt.get("Offers")); // CraftBukkit - decompile error
-             Logger logger = AbstractVillager.LOGGER;
- 
-             Objects.requireNonNull(logger);
-@@ -246,7 +279,20 @@
-             MerchantOffer merchantrecipe = ((VillagerTrades.ItemListing) arraylist.remove(this.random.nextInt(arraylist.size()))).getOffer(this, this.random);
- 
-             if (merchantrecipe != null) {
--                recipeList.add(merchantrecipe);
+@@ -236,7 +_,20 @@
+         while (i < maxNumbers && !list.isEmpty()) {
+             MerchantOffer offer = list.remove(this.random.nextInt(list.size())).getOffer(this, this.random);
+             if (offer != null) {
+-                givenMerchantOffers.add(offer);
 +                // CraftBukkit start
-+                VillagerAcquireTradeEvent event = new VillagerAcquireTradeEvent((org.bukkit.entity.AbstractVillager) this.getBukkitEntity(), merchantrecipe.asBukkit());
++                VillagerAcquireTradeEvent event = new VillagerAcquireTradeEvent((org.bukkit.entity.AbstractVillager) this.getBukkitEntity(), offer.asBukkit());
 +                // Suppress during worldgen
 +                if (this.valid) {
 +                    Bukkit.getPluginManager().callEvent(event);
@@ -97,10 +86,10 @@
 +                    // Paper start - Fix crash from invalid ingredient list
 +                    final CraftMerchantRecipe craftMerchantRecipe = CraftMerchantRecipe.fromBukkit(event.getRecipe());
 +                    if (craftMerchantRecipe.getIngredients().isEmpty()) return;
-+                    recipeList.add(craftMerchantRecipe.toMinecraft());
++                    givenMerchantOffers.add(craftMerchantRecipe.toMinecraft());
 +                    // Paper end - Fix crash from invalid ingredient list
 +                }
 +                // CraftBukkit end
-                 ++j;
+                 i++;
              }
          }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/npc/CatSpawner.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/npc/CatSpawner.java.patch
similarity index 62%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/npc/CatSpawner.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/npc/CatSpawner.java.patch
index 6d93d621f8..1c1335c6b5 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/npc/CatSpawner.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/npc/CatSpawner.java.patch
@@ -1,12 +1,12 @@
 --- a/net/minecraft/world/entity/npc/CatSpawner.java
 +++ b/net/minecraft/world/entity/npc/CatSpawner.java
-@@ -82,8 +82,8 @@
+@@ -82,8 +_,8 @@
          if (cat == null) {
              return 0;
          } else {
 +            cat.moveTo(pos, 0.0F, 0.0F); // Paper - move up - Fix MC-147659
-             cat.finalizeSpawn(world, world.getCurrentDifficultyAt(pos), EntitySpawnReason.NATURAL, null);
+             cat.finalizeSpawn(serverLevel, serverLevel.getCurrentDifficultyAt(pos), EntitySpawnReason.NATURAL, null);
 -            cat.moveTo(pos, 0.0F, 0.0F);
-             world.addFreshEntityWithPassengers(cat);
+             serverLevel.addFreshEntityWithPassengers(cat);
              return 1;
          }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/npc/ClientSideMerchant.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/npc/ClientSideMerchant.java.patch
new file mode 100644
index 0000000000..b19fe89536
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/npc/ClientSideMerchant.java.patch
@@ -0,0 +1,16 @@
+--- a/net/minecraft/world/entity/npc/ClientSideMerchant.java
++++ b/net/minecraft/world/entity/npc/ClientSideMerchant.java
+@@ -56,6 +_,13 @@
+         return this.source == player;
+     }
+ 
++    // Paper start
++    @Override
++    public org.bukkit.craftbukkit.inventory.CraftMerchant getCraftMerchant() {
++        throw new UnsupportedOperationException();
++    }
++    // Paper end
++
+     @Override
+     public int getVillagerXp() {
+         return this.xp;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/npc/InventoryCarrier.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/npc/InventoryCarrier.java.patch
similarity index 53%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/npc/InventoryCarrier.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/npc/InventoryCarrier.java.patch
index d6c445bea3..7728954a04 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/npc/InventoryCarrier.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/npc/InventoryCarrier.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/entity/npc/InventoryCarrier.java
 +++ b/net/minecraft/world/entity/npc/InventoryCarrier.java
-@@ -8,6 +8,10 @@
+@@ -8,6 +_,10 @@
  import net.minecraft.world.entity.item.ItemEntity;
  import net.minecraft.world.item.ItemStack;
  
@@ -9,27 +9,26 @@
 +// CraftBukkit end
 +
  public interface InventoryCarrier {
- 
      String TAG_INVENTORY = "Inventory";
-@@ -25,13 +29,20 @@
+ 
+@@ -22,12 +_,19 @@
                  return;
              }
  
 +            // CraftBukkit start
-+            ItemStack remaining = new SimpleContainer(inventorysubcontainer).addItem(itemstack);
-+            if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPickupItemEvent(entity, item, remaining.getCount(), false).isCancelled()) {
++            ItemStack remaining = new SimpleContainer(inventory).addItem(item);
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPickupItemEvent(mob, itemEntity, remaining.getCount(), false).isCancelled()) {
 +                return;
 +            }
 +            // CraftBukkit end
 +
-             entity.onItemPickup(item);
-             int i = itemstack.getCount();
-             ItemStack itemstack1 = inventorysubcontainer.addItem(itemstack);
- 
-             entity.take(item, i - itemstack1.getCount());
-             if (itemstack1.isEmpty()) {
--                item.discard();
-+                item.discard(EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
+             mob.onItemPickup(itemEntity);
+             int count = item.getCount();
+             ItemStack itemStack = inventory.addItem(item);
+             mob.take(itemEntity, count - itemStack.getCount());
+             if (itemStack.isEmpty()) {
+-                itemEntity.discard();
++                itemEntity.discard(EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
              } else {
-                 itemstack.setCount(itemstack1.getCount());
+                 item.setCount(itemStack.getCount());
              }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/npc/Villager.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/npc/Villager.java.patch
new file mode 100644
index 0000000000..a3b4672db7
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/npc/Villager.java.patch
@@ -0,0 +1,161 @@
+--- a/net/minecraft/world/entity/npc/Villager.java
++++ b/net/minecraft/world/entity/npc/Villager.java
+@@ -90,6 +_,14 @@
+ import net.minecraft.world.phys.AABB;
+ import org.slf4j.Logger;
+ 
++// CraftBukkit start
++import org.bukkit.Bukkit;
++import org.bukkit.craftbukkit.event.CraftEventFactory;
++import org.bukkit.event.entity.EntityRemoveEvent;
++import org.bukkit.event.entity.EntityTransformEvent;
++import org.bukkit.event.entity.VillagerReplenishTradeEvent;
++// CraftBukkit end
++
+ public class Villager extends AbstractVillager implements ReputationEventHandler, VillagerDataHolder {
+     private static final Logger LOGGER = LogUtils.getLogger();
+     private static final EntityDataAccessor<VillagerData> DATA_VILLAGER_DATA = SynchedEntityData.defineId(Villager.class, EntityDataSerializers.VILLAGER_DATA);
+@@ -275,7 +_,7 @@
+                     this.increaseProfessionLevelOnUpdate = false;
+                 }
+ 
+-                this.addEffect(new MobEffectInstance(MobEffects.REGENERATION, 200, 0));
++                this.addEffect(new MobEffectInstance(MobEffects.REGENERATION, 200, 0), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.VILLAGER_TRADE); // CraftBukkit
+             }
+         }
+ 
+@@ -384,7 +_,13 @@
+         this.updateDemand();
+ 
+         for (MerchantOffer merchantOffer : this.getOffers()) {
+-            merchantOffer.resetUses();
++            // CraftBukkit start
++            VillagerReplenishTradeEvent event = new VillagerReplenishTradeEvent((org.bukkit.entity.Villager) this.getBukkitEntity(), merchantOffer.asBukkit());
++            Bukkit.getPluginManager().callEvent(event);
++            if (!event.isCancelled()) {
++                merchantOffer.resetUses();
++            }
++            // CraftBukkit end
+         }
+ 
+         this.resendOffersToTradingPlayer();
+@@ -445,7 +_,13 @@
+         int i = 2 - this.numberOfRestocksToday;
+         if (i > 0) {
+             for (MerchantOffer merchantOffer : this.getOffers()) {
+-                merchantOffer.resetUses();
++                // CraftBukkit start
++                VillagerReplenishTradeEvent event = new VillagerReplenishTradeEvent((org.bukkit.entity.Villager) this.getBukkitEntity(), merchantOffer.asBukkit());
++                Bukkit.getPluginManager().callEvent(event);
++                if (!event.isCancelled()) {
++                    merchantOffer.resetUses();
++                }
++                // CraftBukkit end
+             }
+         }
+ 
+@@ -466,6 +_,7 @@
+         int playerReputation = this.getPlayerReputation(player);
+         if (playerReputation != 0) {
+             for (MerchantOffer merchantOffer : this.getOffers()) {
++                if (merchantOffer.ignoreDiscounts) continue; // Paper - Add ignore discounts API
+                 merchantOffer.addToSpecialPriceDiff(-Mth.floor(playerReputation * merchantOffer.getPriceMultiplier()));
+             }
+         }
+@@ -475,6 +_,7 @@
+             int amplifier = effect.getAmplifier();
+ 
+             for (MerchantOffer merchantOffer1 : this.getOffers()) {
++                if (merchantOffer1.ignoreDiscounts) continue; // Paper - Add ignore discounts API
+                 double d = 0.3 + 0.0625 * amplifier;
+                 int i = (int)Math.floor(d * merchantOffer1.getBaseCostA().getCount());
+                 merchantOffer1.addToSpecialPriceDiff(-Math.max(i, 1));
+@@ -594,7 +_,7 @@
+         }
+ 
+         if (offer.shouldRewardExp()) {
+-            this.level().addFreshEntity(new ExperienceOrb(this.level(), this.getX(), this.getY() + 0.5, this.getZ(), i));
++            this.level().addFreshEntity(new ExperienceOrb(this.level(), this.getX(), this.getY() + 0.5, this.getZ(), i, org.bukkit.entity.ExperienceOrb.SpawnReason.VILLAGER_TRADE, this.getTradingPlayer(), this)); // Paper
+         }
+     }
+ 
+@@ -612,7 +_,7 @@
+ 
+     @Override
+     public void die(DamageSource cause) {
+-        LOGGER.info("Villager {} died, message: '{}'", this, cause.getLocalizedDeathMessage(this).getString());
++        if (org.spigotmc.SpigotConfig.logVillagerDeaths) LOGGER.info("Villager {} died, message: '{}'", this, cause.getLocalizedDeathMessage(this).getString()); // Spigot
+         Entity entity = cause.getEntity();
+         if (entity != null) {
+             this.tellWitnessesThatIWasMurdered(entity);
+@@ -782,12 +_,19 @@
+     @Override
+     public void thunderHit(ServerLevel level, LightningBolt lightning) {
+         if (level.getDifficulty() != Difficulty.PEACEFUL) {
+-            LOGGER.info("Villager {} was struck by lightning {}.", this, lightning);
++            // Paper - Add EntityZapEvent; move log down, event can cancel
+             Witch witch = this.convertTo(EntityType.WITCH, ConversionParams.single(this, false, false), mob -> {
++                // Paper start - Add EntityZapEvent
++                if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityZapEvent(this, lightning, mob).isCancelled()) {
++                    return false;
++                }
++                if (org.spigotmc.SpigotConfig.logVillagerDeaths) Villager.LOGGER.info("Villager {} was struck by lightning {}.", this, lightning); // Move down
++                // Paper end - Add EntityZapEvent
+                 mob.finalizeSpawn(level, level.getCurrentDifficultyAt(mob.blockPosition()), EntitySpawnReason.CONVERSION, null);
+                 mob.setPersistenceRequired();
+                 this.releaseAllPois();
+-            });
++                return true; // Paper start - Add EntityZapEvent
++            }, EntityTransformEvent.TransformReason.LIGHTNING, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.LIGHTNING); // CraftBukkit
+             if (witch == null) {
+                 super.thunderHit(level, lightning);
+             }
+@@ -827,6 +_,12 @@
+ 
+     @Override
+     protected void updateTrades() {
++        // Paper start - More vanilla friendly methods to update trades
++        updateTrades(TRADES_PER_LEVEL);
++    }
++
++    public boolean updateTrades(int amount) {
++        // Paper end - More vanilla friendly methods to update trades
+         VillagerData villagerData = this.getVillagerData();
+         Int2ObjectMap<VillagerTrades.ItemListing[]> map1;
+         if (this.level().enabledFeatures().contains(FeatureFlags.TRADE_REBALANCE)) {
+@@ -840,9 +_,11 @@
+             VillagerTrades.ItemListing[] itemListings = map1.get(villagerData.getLevel());
+             if (itemListings != null) {
+                 MerchantOffers offers = this.getOffers();
+-                this.addOffersFromItemListings(offers, itemListings, 2);
++                this.addOffersFromItemListings(offers, itemListings, amount); // Paper - More vanilla friendly methods to update trades
++                return true; // Paper - More vanilla friendly methods to update trades
+             }
+         }
++        return false; // Paper - More vanilla friendly methods to update trades
+     }
+ 
+     public void gossip(ServerLevel serverLevel, Villager target, long gameTime) {
+@@ -871,7 +_,7 @@
+             List<Villager> entitiesOfClass = serverLevel.getEntitiesOfClass(Villager.class, aabb);
+             List<Villager> list = entitiesOfClass.stream().filter(villager -> villager.wantsToSpawnGolem(gameTime)).limit(5L).toList();
+             if (list.size() >= minVillagerAmount) {
+-                if (!SpawnUtil.trySpawnMob(
++                if (SpawnUtil.trySpawnMob( // Paper - Set Golem Last Seen to stop it from spawning another one - switch to isPresent
+                         EntityType.IRON_GOLEM,
+                         EntitySpawnReason.MOB_SUMMONED,
+                         serverLevel,
+@@ -880,9 +_,11 @@
+                         8,
+                         6,
+                         SpawnUtil.Strategy.LEGACY_IRON_GOLEM,
+-                        false
++                        false,
++                        org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.VILLAGE_DEFENSE, // CraftBukkit,
++                        () -> {GolemSensor.golemDetected(this);} // Paper - Set Golem Last Seen to stop it from spawning another one
+                     )
+-                    .isEmpty()) {
++                    .isPresent()) { // Paper - Set Golem Last Seen to stop it from spawning another one - switch to isPresent
+                     entitiesOfClass.forEach(GolemSensor::golemDetected);
+                 }
+             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/npc/VillagerTrades.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/npc/VillagerTrades.java.patch
similarity index 79%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/npc/VillagerTrades.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/npc/VillagerTrades.java.patch
index a21a5e780e..9aed42eb71 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/npc/VillagerTrades.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/npc/VillagerTrades.java.patch
@@ -1,12 +1,12 @@
 --- a/net/minecraft/world/entity/npc/VillagerTrades.java
 +++ b/net/minecraft/world/entity/npc/VillagerTrades.java
-@@ -1834,7 +1834,8 @@
+@@ -1844,7 +_,8 @@
                  return null;
              } else {
-                 ServerLevel serverLevel = (ServerLevel)entity.level();
--                BlockPos blockPos = serverLevel.findNearestMapStructure(this.destination, entity.blockPosition(), 100, true);
+                 ServerLevel serverLevel = (ServerLevel)trader.level();
+-                BlockPos blockPos = serverLevel.findNearestMapStructure(this.destination, trader.blockPosition(), 100, true);
 +                if (!serverLevel.paperConfig().environment.treasureMaps.enabled) return null; // Paper - Configurable cartographer treasure maps
-+                BlockPos blockPos = serverLevel.findNearestMapStructure(this.destination, entity.blockPosition(), 100, !serverLevel.paperConfig().environment.treasureMaps.findAlreadyDiscoveredVillager); // Paper - Configurable cartographer treasure maps
++                BlockPos blockPos = serverLevel.findNearestMapStructure(this.destination, trader.blockPosition(), 100, !serverLevel.paperConfig().environment.treasureMaps.findAlreadyDiscoveredVillager); // Paper - Configurable cartographer treasure maps
                  if (blockPos != null) {
                      ItemStack itemStack = MapItem.create(serverLevel, blockPos.getX(), blockPos.getZ(), (byte)2, true, true);
                      MapItem.renderBiomePreviewMap(serverLevel, itemStack);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/npc/WanderingTrader.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/npc/WanderingTrader.java.patch
similarity index 52%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/npc/WanderingTrader.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/npc/WanderingTrader.java.patch
index c3bde269c7..6c74e0007b 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/npc/WanderingTrader.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/npc/WanderingTrader.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/entity/npc/WanderingTrader.java
 +++ b/net/minecraft/world/entity/npc/WanderingTrader.java
-@@ -48,25 +48,38 @@
+@@ -47,11 +_,23 @@
  import net.minecraft.world.phys.Vec3;
  import org.apache.commons.lang3.tuple.Pair;
  
@@ -12,9 +12,8 @@
 +import org.bukkit.event.entity.EntityRemoveEvent;
 +import org.bukkit.event.entity.VillagerAcquireTradeEvent;
 +// CraftBukkit end
- 
-+public class WanderingTrader extends net.minecraft.world.entity.npc.AbstractVillager implements Consumable.OverrideConsumeSound {
 +
++public class WanderingTrader extends net.minecraft.world.entity.npc.AbstractVillager implements Consumable.OverrideConsumeSound {
      private static final int NUMBER_OF_TRADE_OFFERS = 5;
      @Nullable
      private BlockPos wanderTarget;
@@ -24,57 +23,59 @@
 +    public boolean canDrinkMilk = true;
 +    // Paper end - Add more WanderingTrader API
  
-     public WanderingTrader(EntityType<? extends WanderingTrader> type, Level world) {
-         super(type, world);
-+        //this.setDespawnDelay(48000); // CraftBukkit - set default from MobSpawnerTrader // Paper - move back to MobSpawnerTrader - Vanilla behavior is that only traders spawned by it have this value set.
-     }
- 
-     @Override
-     protected void registerGoals() {
-         this.goalSelector.addGoal(0, new FloatGoal(this));
-         this.goalSelector.addGoal(0, new UseItemGoal<>(this, PotionContents.createItemStack(Items.POTION, Potions.INVISIBILITY), SoundEvents.WANDERING_TRADER_DISAPPEARED, (entityvillagertrader) -> {
--            return this.level().isNight() && !entityvillagertrader.isInvisible();
-+            return this.canDrinkPotion && this.level().isNight() && !entityvillagertrader.isInvisible(); // Paper - Add more WanderingTrader API
-         }));
-         this.goalSelector.addGoal(0, new UseItemGoal<>(this, new ItemStack(Items.MILK_BUCKET), SoundEvents.WANDERING_TRADER_REAPPEARED, (entityvillagertrader) -> {
--            return this.level().isDay() && entityvillagertrader.isInvisible();
-+            return this.canDrinkMilk && this.level().isDay() && entityvillagertrader.isInvisible(); // Paper - Add more WanderingTrader API
-         }));
+     public WanderingTrader(EntityType<? extends WanderingTrader> entityType, Level level) {
+         super(entityType, level);
+@@ -67,7 +_,7 @@
+                     this,
+                     PotionContents.createItemStack(Items.POTION, Potions.INVISIBILITY),
+                     SoundEvents.WANDERING_TRADER_DISAPPEARED,
+-                    wanderingTrader -> this.level().isNight() && !wanderingTrader.isInvisible()
++                    wanderingTrader -> this.canDrinkPotion && this.level().isNight() && !wanderingTrader.isInvisible() // Paper - Add more WanderingTrader API
+                 )
+             );
+         this.goalSelector
+@@ -77,7 +_,7 @@
+                     this,
+                     new ItemStack(Items.MILK_BUCKET),
+                     SoundEvents.WANDERING_TRADER_REAPPEARED,
+-                    wanderingTrader -> this.level().isDay() && wanderingTrader.isInvisible()
++                    wanderingTrader -> this.canDrinkMilk && this.level().isDay() && wanderingTrader.isInvisible() // Paper - Add more WanderingTrader API
+                 )
+             );
          this.goalSelector.addGoal(1, new TradeWithPlayerGoal(this));
-         this.goalSelector.addGoal(1, new AvoidEntityGoal<>(this, Zombie.class, 8.0F, 0.5D, 0.5D));
-@@ -137,7 +150,16 @@
-                 MerchantOffer merchantrecipe = villagertrades_imerchantrecipeoption.getOffer(this, this.random);
- 
-                 if (merchantrecipe != null) {
--                    merchantrecipelist.add(merchantrecipe);
+@@ -145,7 +_,16 @@
+                 VillagerTrades.ItemListing itemListing = itemListings1[randomInt];
+                 MerchantOffer offer = itemListing.getOffer(this, this.random);
+                 if (offer != null) {
+-                    offers.add(offer);
 +                    // CraftBukkit start
-+                    VillagerAcquireTradeEvent event = new VillagerAcquireTradeEvent((AbstractVillager) this.getBukkitEntity(), merchantrecipe.asBukkit());
++                    VillagerAcquireTradeEvent event = new VillagerAcquireTradeEvent((AbstractVillager) this.getBukkitEntity(), offer.asBukkit());
 +                    // Suppress during worldgen
 +                    if (this.valid) {
 +                        Bukkit.getPluginManager().callEvent(event);
 +                    }
 +                    if (!event.isCancelled()) {
-+                        merchantrecipelist.add(CraftMerchantRecipe.fromBukkit(event.getRecipe()).toMinecraft());
++                        offers.add(CraftMerchantRecipe.fromBukkit(event.getRecipe()).toMinecraft());
 +                    }
 +                    // CraftBukkit end
                  }
- 
              }
-@@ -190,7 +212,7 @@
+         }
+@@ -189,7 +_,7 @@
+     protected void rewardTradeXp(MerchantOffer offer) {
          if (offer.shouldRewardExp()) {
              int i = 3 + this.random.nextInt(4);
- 
--            this.level().addFreshEntity(new ExperienceOrb(this.level(), this.getX(), this.getY() + 0.5D, this.getZ(), i));
-+            this.level().addFreshEntity(new ExperienceOrb(this.level(), this.getX(), this.getY() + 0.5D, this.getZ(), i, org.bukkit.entity.ExperienceOrb.SpawnReason.VILLAGER_TRADE, this.getTradingPlayer(), this)); // Paper
+-            this.level().addFreshEntity(new ExperienceOrb(this.level(), this.getX(), this.getY() + 0.5, this.getZ(), i));
++            this.level().addFreshEntity(new ExperienceOrb(this.level(), this.getX(), this.getY() + 0.5, this.getZ(), i, org.bukkit.entity.ExperienceOrb.SpawnReason.VILLAGER_TRADE, this.getTradingPlayer(), this)); // Paper
          }
- 
      }
-@@ -244,7 +266,7 @@
+ 
+@@ -241,7 +_,7 @@
  
      private void maybeDespawn() {
          if (this.despawnDelay > 0 && !this.isTrading() && --this.despawnDelay == 0) {
 -            this.discard();
 +            this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
          }
- 
      }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/npc/WanderingTraderSpawner.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/npc/WanderingTraderSpawner.java.patch
new file mode 100644
index 0000000000..032bfcb48c
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/npc/WanderingTraderSpawner.java.patch
@@ -0,0 +1,97 @@
+--- a/net/minecraft/world/entity/npc/WanderingTraderSpawner.java
++++ b/net/minecraft/world/entity/npc/WanderingTraderSpawner.java
+@@ -38,41 +_,51 @@
+ 
+     public WanderingTraderSpawner(ServerLevelData serverLevelData) {
+         this.serverLevelData = serverLevelData;
+-        this.tickDelay = 1200;
+-        this.spawnDelay = serverLevelData.getWanderingTraderSpawnDelay();
+-        this.spawnChance = serverLevelData.getWanderingTraderSpawnChance();
+-        if (this.spawnDelay == 0 && this.spawnChance == 0) {
+-            this.spawnDelay = 24000;
+-            serverLevelData.setWanderingTraderSpawnDelay(this.spawnDelay);
+-            this.spawnChance = 25;
+-            serverLevelData.setWanderingTraderSpawnChance(this.spawnChance);
+-        }
++        // Paper start - Add Wandering Trader spawn rate config options
++        this.tickDelay = Integer.MIN_VALUE;
++        // this.spawnDelay = serverLevelData.getWanderingTraderSpawnDelay();
++        // this.spawnChance = serverLevelData.getWanderingTraderSpawnChance();
++        // if (this.spawnDelay == 0 && this.spawnChance == 0) {
++        //     this.spawnDelay = 24000;
++        //     serverLevelData.setWanderingTraderSpawnDelay(this.spawnDelay);
++        //     this.spawnChance = 25;
++        //     serverLevelData.setWanderingTraderSpawnChance(this.spawnChance);
++        // }
++        // Paper end - Add Wandering Trader spawn rate config options
+     }
+ 
+     @Override
+     public int tick(ServerLevel level, boolean spawnHostiles, boolean spawnPassives) {
++        // Paper start - Add Wandering Trader spawn rate config options
++        if (this.tickDelay == Integer.MIN_VALUE) {
++            this.tickDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength;
++            this.spawnDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnDayLength;
++            this.spawnChance = level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin;
++        }
+         if (!level.getGameRules().getBoolean(GameRules.RULE_DO_TRADER_SPAWNING)) {
+             return 0;
+-        } else if (--this.tickDelay > 0) {
++        } else if (--this.tickDelay - 1 > 0) {
++            this.tickDelay = this.tickDelay - 1;
+             return 0;
+         } else {
+-            this.tickDelay = 1200;
+-            this.spawnDelay -= 1200;
+-            this.serverLevelData.setWanderingTraderSpawnDelay(this.spawnDelay);
++            this.tickDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength;
++            this.spawnDelay = this.spawnDelay - level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength;
++            //this.serverLevelData.setWanderingTraderSpawnDelay(this.spawnDelay); // Paper - We don't need to save this value to disk if it gets set back to a hardcoded value anyways
+             if (this.spawnDelay > 0) {
+                 return 0;
+             } else {
+-                this.spawnDelay = 24000;
++                this.spawnDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnDayLength;
+                 if (!level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) {
+                     return 0;
+                 } else {
+                     int i = this.spawnChance;
+-                    this.spawnChance = Mth.clamp(this.spawnChance + 25, 25, 75);
+-                    this.serverLevelData.setWanderingTraderSpawnChance(this.spawnChance);
++                    this.spawnChance = Mth.clamp(this.spawnChance + level.paperConfig().entities.spawning.wanderingTrader.spawnChanceFailureIncrement, level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin, level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMax);
++                    //this.serverLevelData.setWanderingTraderSpawnChance(this.spawnChance); // Paper - We don't need to save this value to disk if it gets set back to a hardcoded value anyways
+                     if (this.random.nextInt(100) > i) {
+                         return 0;
+                     } else if (this.spawn(level)) {
+-                        this.spawnChance = 25;
++                        this.spawnChance = level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin;
++                        // Paper end - Add Wandering Trader spawn rate config options
+                         return 1;
+                     } else {
+                         return 0;
+@@ -100,14 +_,14 @@
+                     return false;
+                 }
+ 
+-                WanderingTrader wanderingTrader = EntityType.WANDERING_TRADER.spawn(serverLevel, blockPos2, EntitySpawnReason.EVENT);
++                WanderingTrader wanderingTrader = EntityType.WANDERING_TRADER.spawn(serverLevel, trader -> trader.setDespawnDelay(48000), blockPos2, EntitySpawnReason.EVENT, false, false, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.NATURAL); // CraftBukkit // Paper - set despawnTimer before spawn events called
+                 if (wanderingTrader != null) {
+                     for (int i1 = 0; i1 < 2; i1++) {
+                         this.tryToSpawnLlamaFor(serverLevel, wanderingTrader, 4);
+                     }
+ 
+                     this.serverLevelData.setWanderingTraderId(wanderingTrader.getUUID());
+-                    wanderingTrader.setDespawnDelay(48000);
++                    // wanderingTrader.setDespawnDelay(48000); // Paper - moved above, modifiable by plugins on CreatureSpawnEvent
+                     wanderingTrader.setWanderTarget(blockPos1);
+                     wanderingTrader.restrictTo(blockPos1, 16);
+                     return true;
+@@ -121,7 +_,7 @@
+     private void tryToSpawnLlamaFor(ServerLevel serverLevel, WanderingTrader trader, int maxDistance) {
+         BlockPos blockPos = this.findSpawnPositionNear(serverLevel, trader.blockPosition(), maxDistance);
+         if (blockPos != null) {
+-            TraderLlama traderLlama = EntityType.TRADER_LLAMA.spawn(serverLevel, blockPos, EntitySpawnReason.EVENT);
++            TraderLlama traderLlama = EntityType.TRADER_LLAMA.spawn(serverLevel, blockPos, EntitySpawnReason.EVENT, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.NATURAL); // CraftBukkit
+             if (traderLlama != null) {
+                 traderLlama.setLeashedTo(trader, true);
+             }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/player/Inventory.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/player/Inventory.java.patch
new file mode 100644
index 0000000000..af826eead1
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/player/Inventory.java.patch
@@ -0,0 +1,124 @@
+--- a/net/minecraft/world/entity/player/Inventory.java
++++ b/net/minecraft/world/entity/player/Inventory.java
+@@ -36,6 +_,54 @@
+     public final Player player;
+     private int timesChanged;
+ 
++    // CraftBukkit start - add fields and methods
++    public List<org.bukkit.entity.HumanEntity> transaction = new java.util.ArrayList<>();
++    private int maxStack = MAX_STACK;
++
++    public List<ItemStack> getContents() {
++        List<ItemStack> combined = new java.util.ArrayList<>(this.items.size() + this.armor.size() + this.offhand.size());
++        for (List<ItemStack> sub : this.compartments) {
++            combined.addAll(sub);
++        }
++
++        return combined;
++    }
++
++    public List<ItemStack> getArmorContents() {
++        return this.armor;
++    }
++
++    public void onOpen(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.add(player);
++    }
++
++    public void onClose(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.remove(player);
++    }
++
++    public List<org.bukkit.entity.HumanEntity> getViewers() {
++        return this.transaction;
++    }
++
++    public org.bukkit.inventory.InventoryHolder getOwner() {
++        return this.player.getBukkitEntity();
++    }
++
++    @Override
++    public int getMaxStackSize() {
++        return this.maxStack;
++    }
++
++    public void setMaxStackSize(int size) {
++        this.maxStack = size;
++    }
++
++    @Override
++    public org.bukkit.Location getLocation() {
++        return this.player.getBukkitEntity().getLocation();
++    }
++    // CraftBukkit end
++
+     public Inventory(Player player) {
+         this.player = player;
+     }
+@@ -50,10 +_,39 @@
+ 
+     private boolean hasRemainingSpaceForItem(ItemStack destination, ItemStack origin) {
+         return !destination.isEmpty()
+-            && ItemStack.isSameItemSameComponents(destination, origin)
+             && destination.isStackable()
+-            && destination.getCount() < this.getMaxStackSize(destination);
+-    }
++            && destination.getCount() < this.getMaxStackSize(destination)
++            && ItemStack.isSameItemSameComponents(destination, origin); // Paper - check if itemstack is stackable first
++    }
++
++    // CraftBukkit start - Watch method above! :D
++    public int canHold(ItemStack itemStack) {
++        int remains = itemStack.getCount();
++        for (int slot = 0; slot < this.items.size(); ++slot) {
++            ItemStack itemInSlot = this.getItem(slot);
++            if (itemInSlot.isEmpty()) {
++                return itemStack.getCount();
++            }
++
++            if (this.hasRemainingSpaceForItem(itemInSlot, itemStack)) {
++                remains -= (itemInSlot.getMaxStackSize() < this.getMaxStackSize() ? itemInSlot.getMaxStackSize() : this.getMaxStackSize()) - itemInSlot.getCount();
++            }
++            if (remains <= 0) {
++                return itemStack.getCount();
++            }
++        }
++
++        ItemStack itemInOffhand = this.getItem(this.items.size() + this.armor.size());
++        if (this.hasRemainingSpaceForItem(itemInOffhand, itemStack)) {
++            remains -= (itemInOffhand.getMaxStackSize() < this.getMaxStackSize() ? itemInOffhand.getMaxStackSize() : this.getMaxStackSize()) - itemInOffhand.getCount();
++        }
++        if (remains <= 0) {
++            return itemStack.getCount();
++        }
++
++        return itemStack.getCount() - remains;
++    }
++    // CraftBukkit end
+ 
+     public int getFreeSlot() {
+         for (int i = 0; i < this.items.size(); i++) {
+@@ -65,7 +_,10 @@
+         return -1;
+     }
+ 
+-    public void addAndPickItem(ItemStack stack) {
++    // Paper start - Add PlayerPickItemEvent
++    public void addAndPickItem(ItemStack stack, final int targetSlot) {
++        this.selected = targetSlot;
++        // Paper end - Add PlayerPickItemEvent
+         this.selected = this.getSuitableHotbarSlot();
+         if (!this.items.get(this.selected).isEmpty()) {
+             int freeSlot = this.getFreeSlot();
+@@ -77,7 +_,10 @@
+         this.items.set(this.selected, stack);
+     }
+ 
+-    public void pickSlot(int index) {
++    // Paper start - Add PlayerPickItemEvent
++    public void pickSlot(int index, final int targetSlot) {
++        this.selected = targetSlot;
++        // Paper end - Add PlayerPickItemEvent
+         this.selected = this.getSuitableHotbarSlot();
+         ItemStack itemStack = this.items.get(this.selected);
+         this.items.set(this.selected, this.items.get(index));
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/player/Player.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/player/Player.java.patch
new file mode 100644
index 0000000000..4d20a5178a
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/player/Player.java.patch
@@ -0,0 +1,671 @@
+--- a/net/minecraft/world/entity/player/Player.java
++++ b/net/minecraft/world/entity/player/Player.java
+@@ -159,7 +_,7 @@
+     public static final int CLIENT_LOADED_TIMEOUT_TIME = 60;
+     private long timeEntitySatOnShoulder;
+     final Inventory inventory = new Inventory(this);
+-    protected PlayerEnderChestContainer enderChestInventory = new PlayerEnderChestContainer();
++    protected PlayerEnderChestContainer enderChestInventory = new PlayerEnderChestContainer(this); // CraftBukkit - add "this" to constructor
+     public final InventoryMenu inventoryMenu;
+     public AbstractContainerMenu containerMenu;
+     protected FoodData foodData = new FoodData();
+@@ -191,13 +_,25 @@
+     private Optional<GlobalPos> lastDeathLocation = Optional.empty();
+     @Nullable
+     public FishingHook fishing;
+-    protected float hurtDir;
++    public float hurtDir; // Paper - protected -> public
+     @Nullable
+     public Vec3 currentImpulseImpactPos;
+     @Nullable
+     public Entity currentExplosionCause;
+     private boolean ignoreFallDamageFromCurrentImpulse;
+     private int currentImpulseContextResetGraceTime;
++    public boolean affectsSpawning = true; // Paper - Affects Spawning API
++    public net.kyori.adventure.util.TriState flyingFallDamage = net.kyori.adventure.util.TriState.NOT_SET; // Paper - flying fall damage
++
++    // CraftBukkit start
++    public boolean fauxSleeping;
++    public int oldLevel = -1;
++
++    @Override
++    public org.bukkit.craftbukkit.entity.CraftHumanEntity getBukkitEntity() {
++        return (org.bukkit.craftbukkit.entity.CraftHumanEntity) super.getBukkitEntity();
++    }
++    // CraftBukkit end
+ 
+     public Player(Level level, BlockPos pos, float yRot, GameProfile gameProfile) {
+         super(EntityType.PLAYER, level);
+@@ -261,6 +_,13 @@
+ 
+         if (this.isSleeping()) {
+             this.sleepCounter++;
++            // Paper start - Add PlayerDeepSleepEvent
++            if (this.sleepCounter == SLEEP_DURATION) {
++                if (!new io.papermc.paper.event.player.PlayerDeepSleepEvent((org.bukkit.entity.Player) getBukkitEntity()).callEvent()) {
++                    this.sleepCounter = Integer.MIN_VALUE;
++                }
++            }
++            // Paper end - Add PlayerDeepSleepEvent
+             if (this.sleepCounter > 100) {
+                 this.sleepCounter = 100;
+             }
+@@ -278,7 +_,7 @@
+         this.updateIsUnderwater();
+         super.tick();
+         if (!this.level().isClientSide && this.containerMenu != null && !this.containerMenu.stillValid(this)) {
+-            this.closeContainer();
++            this.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.CANT_USE); // Paper - Inventory close reason
+             this.containerMenu = this.inventoryMenu;
+         }
+ 
+@@ -365,7 +_,7 @@
+     }
+ 
+     private void turtleHelmetTick() {
+-        this.addEffect(new MobEffectInstance(MobEffects.WATER_BREATHING, 200, 0, false, false, true));
++        this.addEffect(new MobEffectInstance(MobEffects.WATER_BREATHING, 200, 0, false, false, true), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.TURTLE_HELMET); // CraftBukkit
+     }
+ 
+     private boolean isEquipped(Item item) {
+@@ -512,6 +_,18 @@
+         }
+     }
+ 
++    // Paper start - Inventory close reason; unused code, but to keep signatures aligned
++    public void closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason reason) {
++        this.closeContainer();
++        this.containerMenu = this.inventoryMenu;
++    }
++    // Paper end - Inventory close reason
++    // Paper start - special close for unloaded inventory
++    public void closeUnloadedInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason reason) {
++        this.containerMenu = this.inventoryMenu;
++    }
++    // Paper end - special close for unloaded inventory
++
+     public void closeContainer() {
+         this.containerMenu = this.inventoryMenu;
+     }
+@@ -523,8 +_,14 @@
+     public void rideTick() {
+         if (!this.level().isClientSide && this.wantsToStopRiding() && this.isPassenger()) {
+             this.stopRiding();
+-            this.setShiftKeyDown(false);
+-        } else {
++            // CraftBukkit start - SPIGOT-7316: no longer passenger, dismount and return
++            if (!this.isPassenger()) {
++                this.setShiftKeyDown(false);
++                return;
++            }
++        }
++        {
++            // CraftBukkit end
+             super.rideTick();
+             this.oBob = this.bob;
+             this.bob = 0.0F;
+@@ -588,6 +_,7 @@
+         this.playShoulderEntityAmbientSound(this.getShoulderEntityLeft());
+         this.playShoulderEntityAmbientSound(this.getShoulderEntityRight());
+         if (!this.level().isClientSide && (this.fallDistance > 0.5F || this.isInWater()) || this.abilities.flying || this.isSleeping() || this.isInPowderSnow) {
++            if (!this.level().paperConfig().entities.behavior.parrotsAreUnaffectedByPlayerMovement) // Paper - Add option to make parrots stay
+             this.removeEntitiesOnShoulder();
+         }
+     }
+@@ -717,6 +_,13 @@
+ 
+     @Nullable
+     public ItemEntity drop(ItemStack droppedItem, boolean dropAround, boolean includeThrowerName) {
++        // CraftBukkit start - SPIGOT-2942: Add boolean to call event
++        return this.drop(droppedItem, dropAround, includeThrowerName, true);
++    }
++
++    @Nullable
++    public ItemEntity drop(ItemStack droppedItem, boolean dropAround, boolean includeThrowerName, boolean callEvent) {
++        // CraftBukkit end
+         if (!droppedItem.isEmpty() && this.level().isClientSide) {
+             this.swing(InteractionHand.MAIN_HAND);
+         }
+@@ -867,10 +_,10 @@
+             if (this.isDeadOrDying()) {
+                 return false;
+             } else {
+-                this.removeEntitiesOnShoulder();
++                // this.removeEntitiesOnShoulder(); // CraftBukkit - moved down
+                 if (damageSource.scalesWithDifficulty()) {
+                     if (level.getDifficulty() == Difficulty.PEACEFUL) {
+-                        amount = 0.0F;
++                        return false; // CraftBukkit - f = 0.0f -> return false
+                     }
+ 
+                     if (level.getDifficulty() == Difficulty.EASY) {
+@@ -882,7 +_,14 @@
+                     }
+                 }
+ 
+-                return amount != 0.0F && super.hurtServer(level, damageSource, amount);
++                // return amount != 0.0F && super.hurtServer(level, damageSource, amount);
++                // CraftBukkit start - Don't filter out 0 damage
++                boolean damaged = super.hurtServer(level, damageSource, amount);
++                if (damaged) {
++                    this.removeEntitiesOnShoulder();
++                }
++                return damaged;
++                // CraftBukkit end
+             }
+         }
+     }
+@@ -892,7 +_,7 @@
+         super.blockUsingShield(entity);
+         ItemStack itemBlockingWith = this.getItemBlockingWith();
+         if (entity.canDisableShield() && itemBlockingWith != null) {
+-            this.disableShield(itemBlockingWith);
++            this.disableShield(itemBlockingWith, entity); // Paper - Add PlayerShieldDisableEvent
+         }
+     }
+ 
+@@ -902,9 +_,29 @@
+     }
+ 
+     public boolean canHarmPlayer(Player other) {
+-        Team team = this.getTeam();
+-        Team team1 = other.getTeam();
+-        return team == null || !team.isAlliedTo(team1) || team.isAllowFriendlyFire();
++        // CraftBukkit start - Change to check OTHER player's scoreboard team according to API
++        // To summarize this method's logic, it's "Can parameter hurt this"
++        org.bukkit.scoreboard.Team team;
++        if (other instanceof ServerPlayer) {
++            ServerPlayer thatPlayer = (ServerPlayer) other;
++            team = thatPlayer.getBukkitEntity().getScoreboard().getPlayerTeam(thatPlayer.getBukkitEntity());
++            if (team == null || team.allowFriendlyFire()) {
++                return true;
++            }
++        } else {
++            // This should never be called, but is implemented anyway
++            org.bukkit.OfflinePlayer thisPlayer = other.level().getCraftServer().getOfflinePlayer(other.getScoreboardName());
++            team = other.level().getCraftServer().getScoreboardManager().getMainScoreboard().getPlayerTeam(thisPlayer);
++            if (team == null || team.allowFriendlyFire()) {
++                return true;
++            }
++        }
++
++        if (this instanceof ServerPlayer) {
++            return !team.hasPlayer(((ServerPlayer) this).getBukkitEntity());
++        }
++        return !team.hasPlayer(this.level().getCraftServer().getOfflinePlayer(this.getScoreboardName()));
++        // CraftBukkit end
+     }
+ 
+     @Override
+@@ -943,7 +_,12 @@
+     }
+ 
+     @Override
+-    protected void actuallyHurt(ServerLevel level, DamageSource damageSource, float amount) {
++    // CraftBukkit start
++    protected boolean actuallyHurt(ServerLevel level, DamageSource damageSource, float amount, org.bukkit.event.entity.EntityDamageEvent event) { // void -> boolean
++        if (true) {
++            return super.actuallyHurt(level, damageSource, amount, event);
++        }
++        // CraftBukkit end
+         if (!this.isInvulnerableTo(level, damageSource)) {
+             amount = this.getDamageAfterArmorAbsorb(damageSource, amount);
+             amount = this.getDamageAfterMagicAbsorb(damageSource, amount);
+@@ -955,7 +_,7 @@
+             }
+ 
+             if (var8 != 0.0F) {
+-                this.causeFoodExhaustion(damageSource.getFoodExhaustion());
++                this.causeFoodExhaustion(damageSource.getFoodExhaustion(), org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.DAMAGED); // CraftBukkit - EntityExhaustionEvent
+                 this.getCombatTracker().recordDamage(damageSource, var8);
+                 this.setHealth(this.getHealth() - var8);
+                 if (var8 < 3.4028235E37F) {
+@@ -965,6 +_,7 @@
+                 this.gameEvent(GameEvent.ENTITY_DAMAGE);
+             }
+         }
++        return false; // CraftBukkit
+     }
+ 
+     public boolean isTextFilteringEnabled() {
+@@ -1040,13 +_,19 @@
+ 
+     @Override
+     public void removeVehicle() {
+-        super.removeVehicle();
++        // Paper start - Force entity dismount during teleportation
++        this.removeVehicle(false);
++    }
++    @Override
++    public void removeVehicle(boolean suppressCancellation) {
++        super.removeVehicle(suppressCancellation);
++        // Paper end - Force entity dismount during teleportation
+         this.boardingCooldown = 0;
+     }
+ 
+     @Override
+     protected boolean isImmobile() {
+-        return super.isImmobile() || this.isSleeping();
++        return super.isImmobile() || this.isSleeping() || this.isRemoved() || !valid; // Paper - player's who are dead or not in a world shouldn't move...
+     }
+ 
+     @Override
+@@ -1125,8 +_,17 @@
+     }
+ 
+     public void attack(Entity target) {
+-        if (target.isAttackable()) {
+-            if (!target.skipAttackInteraction(this)) {
++        // Paper start - PlayerAttackEntityEvent
++        boolean willAttack = target.isAttackable() && !target.skipAttackInteraction(this); // Vanilla logic
++        io.papermc.paper.event.player.PrePlayerAttackEntityEvent playerAttackEntityEvent = new io.papermc.paper.event.player.PrePlayerAttackEntityEvent(
++            (org.bukkit.entity.Player) this.getBukkitEntity(),
++            target.getBukkitEntity(),
++            willAttack
++        );
++
++        if (playerAttackEntityEvent.callEvent() && willAttack) { // Logic moved to willAttack local variable.
++            {
++        // Paper end - PlayerAttackEntityEvent
+                 float f = this.isAutoSpinAttack() ? this.autoSpinAttackDmg : (float)this.getAttributeValue(Attributes.ATTACK_DAMAGE);
+                 ItemStack weaponItem = this.getWeaponItem();
+                 DamageSource damageSource = Optional.ofNullable(weaponItem.getItem().getDamageSource(this)).orElse(this.damageSources().playerAttack(this));
+@@ -1134,18 +_,25 @@
+                 float attackStrengthScale = this.getAttackStrengthScale(0.5F);
+                 f *= 0.2F + attackStrengthScale * attackStrengthScale * 0.8F;
+                 f1 *= attackStrengthScale;
+-                this.resetAttackStrengthTicker();
++                // this.resetAttackStrengthTicker(); // CraftBukkit - Moved to EntityLiving to reset the cooldown after the damage is dealt
+                 if (target.getType().is(EntityTypeTags.REDIRECTABLE_PROJECTILE)
+-                    && target instanceof Projectile projectile
+-                    && projectile.deflect(ProjectileDeflection.AIM_DEFLECT, this, this, true)) {
+-                    this.level().playSound(null, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_ATTACK_NODAMAGE, this.getSoundSource());
+-                } else {
++                    && target instanceof Projectile projectile) {
++                        // CraftBukkit start
++                        if (org.bukkit.craftbukkit.event.CraftEventFactory.handleNonLivingEntityDamageEvent(target, damageSource, f1, false)) {
++                            return;
++                        }
++                        if (projectile.deflect(ProjectileDeflection.AIM_DEFLECT, this, this, true)) {
++                            this.level().playSound(null, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_ATTACK_NODAMAGE, this.getSoundSource());
++                            return;
++                        }
++                }
++                {
++                    // CraftBukkit end
+                     if (f > 0.0F || f1 > 0.0F) {
+                         boolean flag = attackStrengthScale > 0.9F;
+                         boolean flag1;
+                         if (this.isSprinting() && flag) {
+-                            this.level()
+-                                .playSound(null, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_ATTACK_KNOCKBACK, this.getSoundSource(), 1.0F, 1.0F);
++                            this.sendSoundEffect(this, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_ATTACK_KNOCKBACK, this.getSoundSource(), 1.0F, 1.0F); // Paper - send while respecting visibility
+                             flag1 = true;
+                         } else {
+                             flag1 = false;
+@@ -1161,7 +_,9 @@
+                             && !this.isPassenger()
+                             && target instanceof LivingEntity
+                             && !this.isSprinting();
++                        flag2 = flag2 && !this.level().paperConfig().entities.behavior.disablePlayerCrits; // Paper - Toggleable player crits
+                         if (flag2) {
++                            damageSource = damageSource.critical(true); // Paper start - critical damage API
+                             f *= 1.5F;
+                         }
+ 
+@@ -1188,17 +_,23 @@
+                                 if (target instanceof LivingEntity livingEntity1) {
+                                     livingEntity1.knockback(
+                                         f4 * 0.5F, Mth.sin(this.getYRot() * (float) (Math.PI / 180.0)), -Mth.cos(this.getYRot() * (float) (Math.PI / 180.0))
++                                        , this, io.papermc.paper.event.entity.EntityKnockbackEvent.Cause.ENTITY_ATTACK // Paper - knockback events
+                                     );
+                                 } else {
+                                     target.push(
+                                         -Mth.sin(this.getYRot() * (float) (Math.PI / 180.0)) * f4 * 0.5F,
+                                         0.1,
+                                         Mth.cos(this.getYRot() * (float) (Math.PI / 180.0)) * f4 * 0.5F
++                                        , this // Paper - Add EntityKnockbackByEntityEvent and EntityPushedByEntityAttackEvent
+                                     );
+                                 }
+ 
+                                 this.setDeltaMovement(this.getDeltaMovement().multiply(0.6, 1.0, 0.6));
++                                // Paper start - Configurable sprint interruption on attack
++                                if (!this.level().paperConfig().misc.disableSprintInterruptionOnAttack) {
+                                 this.setSprinting(false);
++                                }
++                                // Paper end - Configurable sprint interruption on attack
+                             }
+ 
+                             if (flag3) {
+@@ -1212,43 +_,62 @@
+                                         && (!(livingEntity2 instanceof ArmorStand) || !((ArmorStand)livingEntity2).isMarker())
+                                         && this.distanceToSqr(livingEntity2) < 9.0) {
+                                         float f6 = this.getEnchantedDamage(livingEntity2, f5, damageSource) * attackStrengthScale;
++                                        // CraftBukkit start - Only apply knockback if the damage hits
++                                        if (!livingEntity2.hurtServer((ServerLevel) this.level(), this.damageSources().playerAttack(this).sweep().critical(flag2), f6)) { // Paper - add critical damage API
++                                            continue;
++                                        }
++                                        // CraftBukkit end
+                                         livingEntity2.knockback(
+                                             0.4F, Mth.sin(this.getYRot() * (float) (Math.PI / 180.0)), -Mth.cos(this.getYRot() * (float) (Math.PI / 180.0))
++                                            , this, io.papermc.paper.event.entity.EntityKnockbackEvent.Cause.SWEEP_ATTACK // CraftBukkit // Paper - knockback events
+                                         );
+-                                        livingEntity2.hurt(damageSource, f6);
++                                        // CraftBukkit - moved up
+                                         if (this.level() instanceof ServerLevel serverLevel) {
+                                             EnchantmentHelper.doPostAttackEffects(serverLevel, livingEntity2, damageSource);
+                                         }
+                                     }
+                                 }
+ 
+-                                this.level()
+-                                    .playSound(null, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_ATTACK_SWEEP, this.getSoundSource(), 1.0F, 1.0F);
++                                this.sendSoundEffect(this, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_ATTACK_SWEEP, this.getSoundSource(), 1.0F, 1.0F); // Paper - send while respecting visibility
+                                 this.sweepAttack();
+                             }
+ 
+                             if (target instanceof ServerPlayer && target.hurtMarked) {
++                                // CraftBukkit start - Add Velocity Event
++                                boolean cancelled = false;
++                                org.bukkit.entity.Player player = (org.bukkit.entity.Player) target.getBukkitEntity();
++                                org.bukkit.util.Vector velocity = org.bukkit.craftbukkit.util.CraftVector.toBukkit(deltaMovement);
++
++                                org.bukkit.event.player.PlayerVelocityEvent event = new org.bukkit.event.player.PlayerVelocityEvent(player, velocity.clone());
++                                this.level().getCraftServer().getPluginManager().callEvent(event);
++
++                                if (event.isCancelled()) {
++                                    cancelled = true;
++                                } else if (!velocity.equals(event.getVelocity())) {
++                                    player.setVelocity(event.getVelocity());
++                                }
++
++                                if (!cancelled) {
+                                 ((ServerPlayer)target).connection.send(new ClientboundSetEntityMotionPacket(target));
+                                 target.hurtMarked = false;
+                                 target.setDeltaMovement(deltaMovement);
++                                }
++                                // CraftBukkit end
+                             }
+ 
+                             if (flag2) {
+-                                this.level()
+-                                    .playSound(null, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_ATTACK_CRIT, this.getSoundSource(), 1.0F, 1.0F);
++                                this.sendSoundEffect(this, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_ATTACK_CRIT, this.getSoundSource(), 1.0F, 1.0F); // Paper - send while respecting visibility
+                                 this.crit(target);
+                             }
+ 
+                             if (!flag2 && !flag3) {
+                                 if (flag) {
+-                                    this.level()
+-                                        .playSound(
+-                                            null, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_ATTACK_STRONG, this.getSoundSource(), 1.0F, 1.0F
++                                    this.sendSoundEffect(
++                                            this, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_ATTACK_STRONG, this.getSoundSource(), 1.0F, 1.0F // Paper - send while respecting visibility
+                                         );
+                                 } else {
+-                                    this.level()
+-                                        .playSound(
+-                                            null, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_ATTACK_WEAK, this.getSoundSource(), 1.0F, 1.0F
++                                    this.sendSoundEffect(
++                                            this, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_ATTACK_WEAK, this.getSoundSource(), 1.0F, 1.0F // Paper - send while respecting visibility
+                                         );
+                                 }
+                             }
+@@ -1296,10 +_,14 @@
+                                 }
+                             }
+ 
+-                            this.causeFoodExhaustion(0.1F);
++                            this.causeFoodExhaustion(this.level().spigotConfig.combatExhaustion, org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.ATTACK); // CraftBukkit - EntityExhaustionEvent // Spigot - Change to use configurable value
+                         } else {
+-                            this.level()
+-                                .playSound(null, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_ATTACK_NODAMAGE, this.getSoundSource(), 1.0F, 1.0F);
++                            this.sendSoundEffect(this, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_ATTACK_NODAMAGE, this.getSoundSource(), 1.0F, 1.0F); // Paper - send while respecting visibility
++                            // CraftBukkit start - resync on cancelled event
++                            if (this instanceof ServerPlayer) {
++                                ((ServerPlayer) this).getBukkitEntity().updateInventory();
++                            }
++                            // CraftBukkit end
+                         }
+                     }
+                 }
+@@ -1316,8 +_,21 @@
+         this.attack(target);
+     }
+ 
++    @io.papermc.paper.annotation.DoNotUse @Deprecated // Paper - Add PlayerShieldDisableEvent
+     public void disableShield(ItemStack stack) {
+-        this.getCooldowns().addCooldown(stack, 100);
++        // Paper start - Add PlayerShieldDisableEvent
++        this.disableShield(stack, null);
++    }
++    public void disableShield(ItemStack stack, @Nullable LivingEntity attacker) {
++        final org.bukkit.entity.Entity finalAttacker = attacker != null ? attacker.getBukkitEntity() : null;
++        if (finalAttacker != null) {
++            final io.papermc.paper.event.player.PlayerShieldDisableEvent shieldDisableEvent = new io.papermc.paper.event.player.PlayerShieldDisableEvent((org.bukkit.entity.Player) getBukkitEntity(), finalAttacker, 100);
++            if (!shieldDisableEvent.callEvent()) return;
++            this.getCooldowns().addCooldown(stack, shieldDisableEvent.getCooldown());
++        } else {
++            this.getCooldowns().addCooldown(stack, 100);
++        }
++        // Paper end - Add PlayerShieldDisableEvent
+         this.stopUsingItem();
+         this.level().broadcastEntityEvent(this, (byte)30);
+     }
+@@ -1341,7 +_,14 @@
+ 
+     @Override
+     public void remove(Entity.RemovalReason reason) {
+-        super.remove(reason);
++        // CraftBukkit start - add Bukkit remove cause
++        this.remove(reason, null);
++    }
++
++    @Override
++    public void remove(Entity.RemovalReason reason, org.bukkit.event.entity.EntityRemoveEvent.Cause eventCause) {
++        super.remove(reason, eventCause);
++        // CraftBukkit end
+         this.inventoryMenu.removed(this);
+         if (this.containerMenu != null && this.hasContainerOpen()) {
+             this.doCloseContainer();
+@@ -1381,6 +_,12 @@
+     }
+ 
+     public Either<Player.BedSleepingProblem, Unit> startSleepInBed(BlockPos bedPos) {
++        // CraftBukkit start
++        return this.startSleepInBed(bedPos, false);
++    }
++
++    public Either<Player.BedSleepingProblem, Unit> startSleepInBed(BlockPos bedPos, boolean force) {
++        // CraftBukkit end
+         this.startSleeping(bedPos);
+         this.sleepCounter = 0;
+         return Either.right(Unit.INSTANCE);
+@@ -1492,7 +_,7 @@
+ 
+     @Override
+     public boolean causeFallDamage(float fallDistance, float multiplier, DamageSource source) {
+-        if (this.abilities.mayfly) {
++        if (this.abilities.mayfly && !this.flyingFallDamage.toBooleanOrElse(false)) { // Paper - flying fall damage
+             return false;
+         } else {
+             if (fallDistance >= 2.0F) {
+@@ -1532,12 +_,24 @@
+     }
+ 
+     public void startFallFlying() {
+-        this.setSharedFlag(7, true);
++        // CraftBukkit start
++        if (!org.bukkit.craftbukkit.event.CraftEventFactory.callToggleGlideEvent(this, true).isCancelled()) {
++            this.setSharedFlag(7, true);
++        } else {
++            // SPIGOT-5542: must toggle like below
++            this.setSharedFlag(7, true);
++            this.setSharedFlag(7, false);
++        }
++        // CraftBukkit end
+     }
+ 
+     public void stopFallFlying() {
++        // CraftBukkit start
++        if (!org.bukkit.craftbukkit.event.CraftEventFactory.callToggleGlideEvent(this, false).isCancelled()) {
+         this.setSharedFlag(7, true);
+         this.setSharedFlag(7, false);
++        }
++        // CraftBukkit end
+     }
+ 
+     @Override
+@@ -1643,15 +_,35 @@
+     public int getXpNeededForNextLevel() {
+         if (this.experienceLevel >= 30) {
+             return 112 + (this.experienceLevel - 30) * 9;
+-        } else {
++        } else { // Paper - diff on change; calculateTotalExperiencePoints
+             return this.experienceLevel >= 15 ? 37 + (this.experienceLevel - 15) * 5 : 7 + this.experienceLevel * 2;
+         }
+     }
+ 
++    // Paper start - send while respecting visibility
++    private static void sendSoundEffect(Player fromEntity, double x, double y, double z, SoundEvent soundEffect, SoundSource soundCategory, float volume, float pitch) {
++        fromEntity.level().playSound(fromEntity, x, y, z, soundEffect, soundCategory, volume, pitch); // This will not send the effect to the entity itself
++        if (fromEntity instanceof ServerPlayer serverPlayer) {
++            serverPlayer.connection.send(new net.minecraft.network.protocol.game.ClientboundSoundPacket(net.minecraft.core.registries.BuiltInRegistries.SOUND_EVENT.wrapAsHolder(soundEffect), soundCategory, x, y, z, volume, pitch, fromEntity.random.nextLong()));
++        }
++    }
++    // Paper end - send while respecting visibility
++
+     public void causeFoodExhaustion(float exhaustion) {
++        // CraftBukkit start
++        this.causeFoodExhaustion(exhaustion, org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.UNKNOWN);
++    }
++
++    public void causeFoodExhaustion(float exhaustion, org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason reason) {
++        // CraftBukkit end
+         if (!this.abilities.invulnerable) {
+             if (!this.level().isClientSide) {
+-                this.foodData.addExhaustion(exhaustion);
++                // CraftBukkit start
++                org.bukkit.event.entity.EntityExhaustionEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerExhaustionEvent(this, reason, exhaustion);
++                if (!event.isCancelled()) {
++                    this.foodData.addExhaustion(event.getExhaustion());
++                }
++                // CraftBukkit end
+             }
+         }
+     }
+@@ -1736,13 +_,20 @@
+ 
+     @Override
+     public void setItemSlot(EquipmentSlot slot, ItemStack stack) {
++        // CraftBukkit start
++        this.setItemSlot(slot, stack, false);
++    }
++
++    @Override
++    public void setItemSlot(EquipmentSlot slot, ItemStack stack, boolean silent) {
++        // CraftBukkit end
+         this.verifyEquippedItem(stack);
+         if (slot == EquipmentSlot.MAINHAND) {
+-            this.onEquipItem(slot, this.inventory.items.set(this.inventory.selected, stack), stack);
++            this.onEquipItem(slot, this.inventory.items.set(this.inventory.selected, stack), stack, silent); // CraftBukkit
+         } else if (slot == EquipmentSlot.OFFHAND) {
+-            this.onEquipItem(slot, this.inventory.offhand.set(0, stack), stack);
++            this.onEquipItem(slot, this.inventory.offhand.set(0, stack), stack, silent); // CraftBukkit
+         } else if (slot.getType() == EquipmentSlot.Type.HUMANOID_ARMOR) {
+-            this.onEquipItem(slot, this.inventory.armor.set(slot.getIndex(), stack), stack);
++            this.onEquipItem(slot, this.inventory.armor.set(slot.getIndex(), stack), stack, silent); // CraftBukkit
+         }
+     }
+ 
+@@ -1783,24 +_,53 @@
+ 
+     public void removeEntitiesOnShoulder() {
+         if (this.timeEntitySatOnShoulder + 20L < this.level().getGameTime()) {
+-            this.respawnEntityOnShoulder(this.getShoulderEntityLeft());
++            // CraftBukkit start
++            if (this.respawnEntityOnShoulder(this.getShoulderEntityLeft())) {
++                this.setShoulderEntityLeft(new CompoundTag());
++            }
++            if (this.respawnEntityOnShoulder(this.getShoulderEntityRight())) {
++                this.setShoulderEntityRight(new CompoundTag());
++            }
++            // CraftBukkit end
++        }
++    }
++
++    // Paper start - release entity api
++    public Entity releaseLeftShoulderEntity() {
++        Entity entity = this.respawnEntityOnShoulder0(this.getShoulderEntityLeft());
++        if (entity != null) {
+             this.setShoulderEntityLeft(new CompoundTag());
+-            this.respawnEntityOnShoulder(this.getShoulderEntityRight());
++        }
++        return entity;
++    }
++
++    public Entity releaseRightShoulderEntity() {
++        Entity entity = this.respawnEntityOnShoulder0(this.getShoulderEntityRight());
++        if (entity != null) {
+             this.setShoulderEntityRight(new CompoundTag());
+         }
++        return entity;
+     }
++    // Paper end - release entity api
+ 
+-    private void respawnEntityOnShoulder(CompoundTag entityCompound) {
++    private boolean respawnEntityOnShoulder(CompoundTag entityCompound) { // CraftBukkit void->boolean
++    // Paper start - release entity api - return entity - overload
++        return this.respawnEntityOnShoulder0(entityCompound) != null;
++    }
++    @Nullable
++    private Entity respawnEntityOnShoulder0(CompoundTag entityCompound) { // CraftBukkit void->boolean
++    // Paper end - release entity api - return entity - overload
+         if (!this.level().isClientSide && !entityCompound.isEmpty()) {
+-            EntityType.create(entityCompound, this.level(), EntitySpawnReason.LOAD).ifPresent(entity -> {
++            return EntityType.create(entityCompound, this.level(), EntitySpawnReason.LOAD).map((entity) -> { // CraftBukkit
+                 if (entity instanceof TamableAnimal) {
+                     ((TamableAnimal)entity).setOwnerUUID(this.uuid);
+                 }
+ 
+                 entity.setPos(this.getX(), this.getY() + 0.7F, this.getZ());
+-                ((ServerLevel)this.level()).addWithUUID(entity);
+-            });
++                return ((ServerLevel)this.level()).addWithUUID(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.SHOULDER_ENTITY) ? entity : null; // CraftBukkit // Paper start - release entity api - return entity
++            }).orElse(null); // CraftBukkit // Paper end - release entity api - return entity
+         }
++        return null; // Paper - return null
+     }
+ 
+     @Override
+@@ -1988,17 +_,28 @@
+         return ImmutableList.of(Pose.STANDING, Pose.CROUCHING, Pose.SWIMMING);
+     }
+ 
++    // Paper start - PlayerReadyArrowEvent
++    protected boolean tryReadyArrow(ItemStack bow, ItemStack itemstack) {
++        return !(this instanceof ServerPlayer) ||
++                new com.destroystokyo.paper.event.player.PlayerReadyArrowEvent(
++                    ((ServerPlayer) this).getBukkitEntity(),
++                    org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(bow),
++                    org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack)
++                ).callEvent();
++    }
++    // Paper end - PlayerReadyArrowEvent
++
+     @Override
+     public ItemStack getProjectile(ItemStack shootable) {
+         if (!(shootable.getItem() instanceof ProjectileWeaponItem)) {
+             return ItemStack.EMPTY;
+         } else {
+-            Predicate<ItemStack> supportedHeldProjectiles = ((ProjectileWeaponItem)shootable.getItem()).getSupportedHeldProjectiles();
++            Predicate<ItemStack> supportedHeldProjectiles = ((ProjectileWeaponItem)shootable.getItem()).getSupportedHeldProjectiles().and(item -> this.tryReadyArrow(shootable, item)); // Paper - PlayerReadyArrowEvent
+             ItemStack heldProjectile = ProjectileWeaponItem.getHeldProjectile(this, supportedHeldProjectiles);
+             if (!heldProjectile.isEmpty()) {
+                 return heldProjectile;
+             } else {
+-                supportedHeldProjectiles = ((ProjectileWeaponItem)shootable.getItem()).getAllSupportedProjectiles();
++                supportedHeldProjectiles = ((ProjectileWeaponItem)shootable.getItem()).getAllSupportedProjectiles().and(item -> this.tryReadyArrow(shootable, item)); // Paper - PlayerReadyArrowEvent
+ 
+                 for (int i = 0; i < this.inventory.getContainerSize(); i++) {
+                     ItemStack item = this.inventory.getItem(i);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/player/ProfilePublicKey.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/player/ProfilePublicKey.java.patch
similarity index 58%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/player/ProfilePublicKey.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/player/ProfilePublicKey.java.patch
index a8a26d91a8..31a265cc65 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/player/ProfilePublicKey.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/player/ProfilePublicKey.java.patch
@@ -1,27 +1,27 @@
 --- a/net/minecraft/world/entity/player/ProfilePublicKey.java
 +++ b/net/minecraft/world/entity/player/ProfilePublicKey.java
-@@ -24,7 +24,7 @@
+@@ -24,7 +_,7 @@
  
-     public static ProfilePublicKey createValidated(SignatureValidator servicesSignatureVerifier, UUID playerUuid, ProfilePublicKey.Data publicKeyData) throws ProfilePublicKey.ValidationException {
-         if (!publicKeyData.validateSignature(servicesSignatureVerifier, playerUuid)) {
+     public static ProfilePublicKey createValidated(SignatureValidator signatureValidator, UUID profileId, ProfilePublicKey.Data data) throws ProfilePublicKey.ValidationException {
+         if (!data.validateSignature(signatureValidator, profileId)) {
 -            throw new ProfilePublicKey.ValidationException(INVALID_SIGNATURE);
 +            throw new ProfilePublicKey.ValidationException(INVALID_SIGNATURE, org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PUBLIC_KEY_SIGNATURE); // Paper - kick event causes
          } else {
-             return new ProfilePublicKey(publicKeyData);
+             return new ProfilePublicKey(data);
          }
-@@ -88,8 +88,16 @@
+@@ -88,8 +_,16 @@
      }
  
      public static class ValidationException extends ThrowingComponent {
 +        public final org.bukkit.event.player.PlayerKickEvent.Cause kickCause; // Paper
 +        @io.papermc.paper.annotation.DoNotUse @Deprecated // Paper
-         public ValidationException(Component messageText) {
+         public ValidationException(Component component) {
 +            // Paper start
-+            this(messageText, org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN);
++            this(component, org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN);
 +        }
-+        public ValidationException(Component messageText, org.bukkit.event.player.PlayerKickEvent.Cause kickCause) {
++        public ValidationException(Component component, org.bukkit.event.player.PlayerKickEvent.Cause kickCause) {
 +            // Paper end
-             super(messageText);
+             super(component);
 +            this.kickCause = kickCause; // Paper
          }
      }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/projectile/AbstractArrow.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/projectile/AbstractArrow.java.patch
new file mode 100644
index 0000000000..cf61cc49d2
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/projectile/AbstractArrow.java.patch
@@ -0,0 +1,268 @@
+--- a/net/minecraft/world/entity/projectile/AbstractArrow.java
++++ b/net/minecraft/world/entity/projectile/AbstractArrow.java
+@@ -33,6 +_,7 @@
+ import net.minecraft.world.entity.OminousItemSpawner;
+ import net.minecraft.world.entity.SlotAccess;
+ import net.minecraft.world.entity.ai.attributes.Attributes;
++import net.minecraft.world.entity.item.ItemEntity;
+ import net.minecraft.world.entity.player.Player;
+ import net.minecraft.world.item.Item;
+ import net.minecraft.world.item.ItemStack;
+@@ -63,16 +_,16 @@
+     protected int inGroundTime;
+     public AbstractArrow.Pickup pickup = AbstractArrow.Pickup.DISALLOWED;
+     public int shakeTime;
+-    private int life;
++    public int life; // Paper - private -> public
+     private double baseDamage = 2.0;
+-    private SoundEvent soundEvent = this.getDefaultHitGroundSoundEvent();
++    public SoundEvent soundEvent = this.getDefaultHitGroundSoundEvent(); // Paper - private -> public
+     @Nullable
+     private IntOpenHashSet piercingIgnoreEntityIds;
+     @Nullable
+     private List<Entity> piercedAndKilledEntities;
+-    private ItemStack pickupItemStack = this.getDefaultPickupItem();
++    public ItemStack pickupItemStack = this.getDefaultPickupItem(); // Paper - private -> public
+     @Nullable
+-    private ItemStack firedFromWeapon = null;
++    public ItemStack firedFromWeapon = null; // Paper - private -> public
+ 
+     protected AbstractArrow(EntityType<? extends AbstractArrow> entityType, Level level) {
+         super(entityType, level);
+@@ -87,7 +_,13 @@
+         ItemStack pickupItemStack,
+         @Nullable ItemStack firedFromWeapon
+     ) {
++        // CraftBukkit start - handle the owner before the rest of things
++        this(entityType, x, y, z, level, pickupItemStack, firedFromWeapon, null);
++    }
++    protected AbstractArrow(EntityType<? extends AbstractArrow> entityType, double x, double y, double z, Level level, ItemStack pickupItemStack, @Nullable ItemStack firedFromWeapon, @Nullable LivingEntity ownerEntity) {
+         this(entityType, level);
++        this.setOwner(ownerEntity);
++        // CraftBukkit end
+         this.pickupItemStack = pickupItemStack.copy();
+         this.setCustomName(pickupItemStack.get(DataComponents.CUSTOM_NAME));
+         Unit unit = pickupItemStack.remove(DataComponents.INTANGIBLE_PROJECTILE);
+@@ -112,8 +_,8 @@
+     protected AbstractArrow(
+         EntityType<? extends AbstractArrow> entityType, LivingEntity owner, Level level, ItemStack pickupItemStack, @Nullable ItemStack firedFromWeapon
+     ) {
+-        this(entityType, owner.getX(), owner.getEyeY() - 0.1F, owner.getZ(), level, pickupItemStack, firedFromWeapon);
+-        this.setOwner(owner);
++        this(entityType, owner.getX(), owner.getEyeY() - 0.1F, owner.getZ(), level, pickupItemStack, firedFromWeapon, owner); // CraftBukkit
++        // this.setOwner(owner); // SPIGOT-7744 - Moved to the above constructor
+     }
+ 
+     public void setSoundEvent(SoundEvent soundEvent) {
+@@ -209,6 +_,7 @@
+                 this.applyEffectsFromBlocks();
+             }
+         } else {
++            if (this.tickCount > 200) this.tickDespawn(); // Paper - tick despawnCounter regardless after 10 seconds
+             this.inGroundTime = 0;
+             Vec3 vec31 = this.position();
+             if (this.isInWater()) {
+@@ -275,12 +_,12 @@
+ 
+             if (entityHitResult == null) {
+                 if (this.isAlive() && hitResult.getType() != HitResult.Type.MISS) {
+-                    this.hitTargetOrDeflectSelf(hitResult);
++                    this.preHitTargetOrDeflectSelf(hitResult); // CraftBukkit - projectile hit event
+                     this.hasImpulse = true;
+                 }
+                 break;
+             } else if (this.isAlive() && !this.noPhysics) {
+-                ProjectileDeflection projectileDeflection = this.hitTargetOrDeflectSelf(entityHitResult);
++                ProjectileDeflection projectileDeflection = this.preHitTargetOrDeflectSelf(entityHitResult); // CraftBukkit - projectile hit event
+                 this.hasImpulse = true;
+                 if (this.getPierceLevel() > 0 && projectileDeflection == ProjectileDeflection.NONE) {
+                     continue;
+@@ -313,6 +_,19 @@
+         }
+     }
+ 
++    // Paper start - Fix cancelling ProjectileHitEvent for piercing arrows
++    @Override
++    public ProjectileDeflection preHitTargetOrDeflectSelf(HitResult hitResult) {
++        if (hitResult instanceof EntityHitResult entityHitResult && this.hitCancelled && this.getPierceLevel() > 0) {
++            if (this.piercingIgnoreEntityIds == null) {
++                this.piercingIgnoreEntityIds = new IntOpenHashSet(5);
++            }
++            this.piercingIgnoreEntityIds.add(entityHitResult.getEntity().getId());
++        }
++        return super.preHitTargetOrDeflectSelf(hitResult);
++    }
++    // Paper end - Fix cancelling ProjectileHitEvent for piercing arrows
++
+     @Override
+     protected double getDefaultGravity() {
+         return 0.05;
+@@ -329,7 +_,7 @@
+         this.life = 0;
+     }
+ 
+-    protected boolean isInGround() {
++    public boolean isInGround() { // Paper - protected -> public
+         return this.entityData.get(IN_GROUND);
+     }
+ 
+@@ -347,8 +_,8 @@
+ 
+     protected void tickDespawn() {
+         this.life++;
+-        if (this.life >= 1200) {
+-            this.discard();
++        if (this.life >= (this.pickup == Pickup.CREATIVE_ONLY ? this.level().paperConfig().entities.spawning.creativeArrowDespawnRate.value() : (this.pickup == Pickup.DISALLOWED ? this.level().paperConfig().entities.spawning.nonPlayerArrowDespawnRate.value() : ((this instanceof ThrownTrident) ? this.level().spigotConfig.tridentDespawnRate : this.level().spigotConfig.arrowDespawnRate)))) { // Spigot // Paper - Configurable non-player arrow despawn rate; TODO: Extract this to init?
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+         }
+     }
+ 
+@@ -375,9 +_,9 @@
+     }
+ 
+     @Override
+-    public void push(double x, double y, double z) {
++    public void push(double x, double y, double z, @Nullable Entity pushingEntity) { // Paper - add push source entity param
+         if (!this.isInGround()) {
+-            super.push(x, y, z);
++            super.push(x, y, z, pushingEntity); // Paper - add push source entity param
+         }
+     }
+ 
+@@ -404,7 +_,7 @@
+             }
+ 
+             if (this.piercingIgnoreEntityIds.size() >= this.getPierceLevel() + 1) {
+-                this.discard();
++                this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
+                 return;
+             }
+ 
+@@ -420,10 +_,17 @@
+             livingEntity.setLastHurtMob(entity);
+         }
+ 
++        if (this.isCritArrow()) damageSource = damageSource.critical(); // Paper - add critical damage API
+         boolean flag = entity.getType() == EntityType.ENDERMAN;
+         int remainingFireTicks = entity.getRemainingFireTicks();
+         if (this.isOnFire() && !flag) {
+-            entity.igniteForSeconds(5.0F);
++            // CraftBukkit start
++            org.bukkit.event.entity.EntityCombustByEntityEvent combustEvent = new org.bukkit.event.entity.EntityCombustByEntityEvent(this.getBukkitEntity(), entity.getBukkitEntity(), 5.0F);
++            org.bukkit.Bukkit.getPluginManager().callEvent(combustEvent);
++            if (!combustEvent.isCancelled()) {
++                entity.igniteForSeconds(combustEvent.getDuration(), false);
++            }
++            // CraftBukkit end
+         }
+ 
+         if (entity.hurtOrSimulate(damageSource, ceil)) {
+@@ -461,7 +_,7 @@
+ 
+             this.playSound(this.soundEvent, 1.0F, 1.2F / (this.random.nextFloat() * 0.2F + 0.9F));
+             if (this.getPierceLevel() <= 0) {
+-                this.discard();
++                this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
+             }
+         } else {
+             entity.setRemainingFireTicks(remainingFireTicks);
+@@ -472,7 +_,7 @@
+                     this.spawnAtLocation(serverLevel2, this.getPickupItem(), 0.1F);
+                 }
+ 
+-                this.discard();
++                this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
+             }
+         }
+     }
+@@ -485,7 +_,7 @@
+             double max = Math.max(0.0, 1.0 - entity.getAttributeValue(Attributes.KNOCKBACK_RESISTANCE));
+             Vec3 vec3 = this.getDeltaMovement().multiply(1.0, 0.0, 1.0).normalize().scale(d * 0.6 * max);
+             if (vec3.lengthSqr() > 0.0) {
+-                entity.push(vec3.x, 0.1, vec3.z);
++                entity.push(vec3.x, 0.1, vec3.z, this); // Paper - Add EntityKnockbackByEntityEvent and EntityPushedByEntityAttackEvent
+             }
+         }
+     }
+@@ -597,7 +_,7 @@
+         this.setPierceLevel(compound.getByte("PierceLevel"));
+         if (compound.contains("SoundEvent", 8)) {
+             this.soundEvent = BuiltInRegistries.SOUND_EVENT
+-                .getOptional(ResourceLocation.parse(compound.getString("SoundEvent")))
++                .getOptional(ResourceLocation.tryParse(compound.getString("SoundEvent"))) // Paper - Validate resource location
+                 .orElse(this.getDefaultHitGroundSoundEvent());
+         }
+ 
+@@ -616,7 +_,14 @@
+ 
+     @Override
+     public void setOwner(@Nullable Entity entity) {
++        // Paper start - Fix PickupStatus getting reset
++        this.setOwner(entity, true);
++    }
++
++    public void setOwner(@Nullable Entity entity, boolean resetPickup) {
++        // Paper end - Fix PickupStatus getting reset
+         super.setOwner(entity);
++        if (!resetPickup) return; // Paper - Fix PickupStatus getting reset
+ 
+         this.pickup = switch (entity) {
+             case Player player when this.pickup == AbstractArrow.Pickup.DISALLOWED -> AbstractArrow.Pickup.ALLOWED;
+@@ -628,9 +_,24 @@
+     @Override
+     public void playerTouch(Player entity) {
+         if (!this.level().isClientSide && (this.isInGround() || this.isNoPhysics()) && this.shakeTime <= 0) {
+-            if (this.tryPickup(entity)) {
++            // CraftBukkit start
++            ItemStack itemstack = this.getPickupItem();
++            if (this.pickup == Pickup.ALLOWED && !itemstack.isEmpty() && entity.getInventory().canHold(itemstack) > 0) {
++                ItemEntity item = new ItemEntity(this.level(), this.getX(), this.getY(), this.getZ(), itemstack);
++                org.bukkit.event.player.PlayerPickupArrowEvent event = new org.bukkit.event.player.PlayerPickupArrowEvent((org.bukkit.entity.Player) entity.getBukkitEntity(), (org.bukkit.entity.Item) item.getBukkitEntity(), (org.bukkit.entity.AbstractArrow) this.getBukkitEntity());
++                // event.setCancelled(!entityhuman.canPickUpLoot); TODO
++                this.level().getCraftServer().getPluginManager().callEvent(event);
++
++                if (event.isCancelled()) {
++                    return;
++                }
++                itemstack = item.getItem();
++            }
++
++            if ((this.pickup == AbstractArrow.Pickup.ALLOWED && entity.getInventory().add(itemstack)) || (this.pickup == AbstractArrow.Pickup.CREATIVE_ONLY && entity.getAbilities().instabuild)) {
++                // CraftBukkit end
+                 entity.take(this, 1);
+-                this.discard();
++                this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
+             }
+         }
+     }
+@@ -643,7 +_,7 @@
+         };
+     }
+ 
+-    protected ItemStack getPickupItem() {
++    public ItemStack getPickupItem() { // Paper - protected -> public
+         return this.pickupItemStack.copy();
+     }
+ 
+@@ -675,7 +_,7 @@
+         this.setFlag(1, critArrow);
+     }
+ 
+-    private void setPierceLevel(byte pierceLevel) {
++    public void setPierceLevel(byte pierceLevel) { // Paper - private -> public
+         this.entityData.set(PIERCE_LEVEL, pierceLevel);
+     }
+ 
+@@ -687,6 +_,12 @@
+             this.entityData.set(ID_FLAGS, (byte)(b & ~id));
+         }
+     }
++
++    // Paper start
++    public void setPickupItemStackPublic(final ItemStack pickupItemStack) {
++        this.setPickupItemStack(pickupItemStack);
++    }
++    // Paper end
+ 
+     protected void setPickupItemStack(ItemStack pickupItemStack) {
+         if (!pickupItemStack.isEmpty()) {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java.patch
new file mode 100644
index 0000000000..0f392f4b0a
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java.patch
@@ -0,0 +1,26 @@
+--- a/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java
++++ b/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java
+@@ -19,6 +_,8 @@
+     public static final double INITAL_ACCELERATION_POWER = 0.1;
+     public static final double DEFLECTION_SCALE = 0.5;
+     public double accelerationPower = 0.1;
++    public float bukkitYield = 1; // CraftBukkit
++    public boolean isIncendiary = true; // CraftBukkit
+ 
+     protected AbstractHurtingProjectile(EntityType<? extends AbstractHurtingProjectile> entityType, Level level) {
+         super(entityType, level);
+@@ -83,12 +_,12 @@
+             }
+ 
+             if (hitResultOnMoveVector.getType() != HitResult.Type.MISS && this.isAlive()) {
+-                this.hitTargetOrDeflectSelf(hitResultOnMoveVector);
++                this.preHitTargetOrDeflectSelf(hitResultOnMoveVector); // CraftBukkit - projectile hit event
+             }
+ 
+             this.createParticleTrail();
+         } else {
+-            this.discard();
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+         }
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/projectile/Arrow.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/projectile/Arrow.java.patch
new file mode 100644
index 0000000000..f23f9c6a7e
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/projectile/Arrow.java.patch
@@ -0,0 +1,17 @@
+--- a/net/minecraft/world/entity/projectile/Arrow.java
++++ b/net/minecraft/world/entity/projectile/Arrow.java
+@@ -121,12 +_,13 @@
+                         mobEffectInstance.isVisible()
+                     ),
+                     effectSource
++                    , org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ARROW // CraftBukkit
+                 );
+             }
+         }
+ 
+         for (MobEffectInstance mobEffectInstance : potionContents.customEffects()) {
+-            living.addEffect(mobEffectInstance, effectSource);
++            living.addEffect(mobEffectInstance, effectSource, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ARROW); // CraftBukkit
+         }
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/projectile/DragonFireball.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/projectile/DragonFireball.java.patch
new file mode 100644
index 0000000000..b04695ec1e
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/projectile/DragonFireball.java.patch
@@ -0,0 +1,16 @@
+--- a/net/minecraft/world/entity/projectile/DragonFireball.java
++++ b/net/minecraft/world/entity/projectile/DragonFireball.java
+@@ -52,9 +_,11 @@
+                     }
+                 }
+ 
++                if (new com.destroystokyo.paper.event.entity.EnderDragonFireballHitEvent((org.bukkit.entity.DragonFireball) this.getBukkitEntity(), entitiesOfClass.stream().map(LivingEntity::getBukkitLivingEntity).collect(java.util.stream.Collectors.toList()), (org.bukkit.entity.AreaEffectCloud) areaEffectCloud.getBukkitEntity()).callEvent()) { // Paper - EnderDragon Events
+                 this.level().levelEvent(2006, this.blockPosition(), this.isSilent() ? -1 : 1);
+-                this.level().addFreshEntity(areaEffectCloud);
+-                this.discard();
++                this.level().addFreshEntity(areaEffectCloud, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.EXPLOSION); // Paper - use correct spawn reason
++                } else areaEffectCloud.discard(null); // Paper - EnderDragon Events
++                this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
+             }
+         }
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/projectile/EvokerFangs.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/projectile/EvokerFangs.java.patch
new file mode 100644
index 0000000000..e980aa3858
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/projectile/EvokerFangs.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/entity/projectile/EvokerFangs.java
++++ b/net/minecraft/world/entity/projectile/EvokerFangs.java
+@@ -109,7 +_,7 @@
+             }
+ 
+             if (--this.lifeTicks < 0) {
+-                this.discard();
++                this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+             }
+         }
+     }
+@@ -118,7 +_,7 @@
+         LivingEntity owner = this.getOwner();
+         if (target.isAlive() && !target.isInvulnerable() && target != owner) {
+             if (owner == null) {
+-                target.hurt(this.damageSources().magic(), 6.0F);
++                target.hurt(this.damageSources().magic().customEventDamager(this), 6.0F); // CraftBukkit // Paper - fix DamageSource API
+             } else {
+                 if (owner.isAlliedTo(target)) {
+                     return;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/EyeOfEnder.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/projectile/EyeOfEnder.java.patch
similarity index 53%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/projectile/EyeOfEnder.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/projectile/EyeOfEnder.java.patch
index 67aa21fe00..450c900de3 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/EyeOfEnder.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/projectile/EyeOfEnder.java.patch
@@ -1,16 +1,6 @@
 --- a/net/minecraft/world/entity/projectile/EyeOfEnder.java
 +++ b/net/minecraft/world/entity/projectile/EyeOfEnder.java
-@@ -17,6 +17,9 @@
- import net.minecraft.world.item.Items;
- import net.minecraft.world.level.Level;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public class EyeOfEnder extends Entity implements ItemSupplier {
- 
-@@ -73,6 +76,11 @@
+@@ -70,6 +_,11 @@
      }
  
      public void signalTo(BlockPos pos) {
@@ -19,10 +9,10 @@
 +    }
 +    public void signalTo(BlockPos pos, boolean update) {
 +        // Paper end - Change EnderEye target without changing other things
-         double d0 = (double) pos.getX();
-         int i = pos.getY();
-         double d1 = (double) pos.getZ();
-@@ -90,8 +98,10 @@
+         double d = pos.getX();
+         int y = pos.getY();
+         double d1 = pos.getZ();
+@@ -86,8 +_,10 @@
              this.tz = d1;
          }
  
@@ -33,24 +23,24 @@
      }
  
      @Override
-@@ -153,7 +163,7 @@
-             ++this.life;
+@@ -161,7 +_,7 @@
+             this.life++;
              if (this.life > 80 && !this.level().isClientSide) {
                  this.playSound(SoundEvents.ENDER_EYE_DEATH, 1.0F, 1.0F);
 -                this.discard();
-+                this.discard(this.surviveAfterDeath ? EntityRemoveEvent.Cause.DROP : EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
++                this.discard(this.surviveAfterDeath ? org.bukkit.event.entity.EntityRemoveEvent.Cause.DROP : org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
                  if (this.surviveAfterDeath) {
                      this.level().addFreshEntity(new ItemEntity(this.level(), this.getX(), this.getY(), this.getZ(), this.getItem()));
                  } else {
-@@ -174,7 +184,12 @@
+@@ -181,7 +_,12 @@
      @Override
-     public void readAdditionalSaveData(CompoundTag nbt) {
-         if (nbt.contains("Item", 10)) {
--            this.setItem((ItemStack) ItemStack.parse(this.registryAccess(), nbt.getCompound("Item")).orElse(this.getDefaultItem()));
+     public void readAdditionalSaveData(CompoundTag compound) {
+         if (compound.contains("Item", 10)) {
+-            this.setItem(ItemStack.parse(this.registryAccess(), compound.getCompound("Item")).orElse(this.getDefaultItem()));
 +            // CraftBukkit start - SPIGOT-6103 summon, see also SPIGOT-5474
-+            ItemStack itemstack = (ItemStack) ItemStack.parse(this.registryAccess(), nbt.getCompound("Item")).orElse(this.getDefaultItem());
-+            if (!itemstack.isEmpty()) {
-+                this.setItem(itemstack);
++            ItemStack itemStack = ItemStack.parse(this.registryAccess(), compound.getCompound("Item")).orElse(this.getDefaultItem());
++            if (!itemStack.isEmpty()) {
++                this.setItem(itemStack);
 +            }
 +            // CraftBukkit end
          } else {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/projectile/Fireball.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/projectile/Fireball.java.patch
new file mode 100644
index 0000000000..96bac378a8
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/projectile/Fireball.java.patch
@@ -0,0 +1,16 @@
+--- a/net/minecraft/world/entity/projectile/Fireball.java
++++ b/net/minecraft/world/entity/projectile/Fireball.java
+@@ -60,7 +_,12 @@
+     public void readAdditionalSaveData(CompoundTag compound) {
+         super.readAdditionalSaveData(compound);
+         if (compound.contains("Item", 10)) {
+-            this.setItem(ItemStack.parse(this.registryAccess(), compound.getCompound("Item")).orElse(this.getDefaultItem()));
++            // CraftBukkit start - SPIGOT-5474 probably came from bugged earlier versions
++            final ItemStack itemStack = ItemStack.parse(this.registryAccess(), compound.getCompound("Item")).orElse(this.getDefaultItem());
++            if (!itemStack.isEmpty()) {
++                this.setItem(itemStack);
++            }
++            // CraftBukkit end
+         } else {
+             this.setItem(this.getDefaultItem());
+         }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/projectile/FireworkRocketEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/projectile/FireworkRocketEntity.java.patch
new file mode 100644
index 0000000000..f0049b5ab4
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/projectile/FireworkRocketEntity.java.patch
@@ -0,0 +1,90 @@
+--- a/net/minecraft/world/entity/projectile/FireworkRocketEntity.java
++++ b/net/minecraft/world/entity/projectile/FireworkRocketEntity.java
+@@ -43,6 +_,7 @@
+     public int lifetime;
+     @Nullable
+     public LivingEntity attachedToEntity;
++    @Nullable public java.util.UUID spawningEntity; // Paper
+ 
+     public FireworkRocketEntity(EntityType<? extends FireworkRocketEntity> entityType, Level level) {
+         super(entityType, level);
+@@ -158,7 +_,7 @@
+         }
+ 
+         if (!this.noPhysics && this.isAlive() && hitResultOnMoveVector.getType() != HitResult.Type.MISS) {
+-            this.hitTargetOrDeflectSelf(hitResultOnMoveVector);
++            this.preHitTargetOrDeflectSelf(hitResultOnMoveVector); // CraftBukkit - projectile hit event
+             this.hasImpulse = true;
+         }
+ 
+@@ -182,7 +_,11 @@
+         }
+ 
+         if (this.life > this.lifetime && this.level() instanceof ServerLevel serverLevel) {
+-            this.explode(serverLevel);
++            // CraftBukkit start
++            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callFireworkExplodeEvent(this).isCancelled()) {
++                this.explode(serverLevel);
++            }
++            // CraftBukkit end
+         }
+     }
+ 
+@@ -190,14 +_,18 @@
+         level.broadcastEntityEvent(this, (byte)17);
+         this.gameEvent(GameEvent.EXPLODE, this.getOwner());
+         this.dealExplosionDamage(level);
+-        this.discard();
++        this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.EXPLODE); // CraftBukkit - add Bukkit remove cause
+     }
+ 
+     @Override
+     protected void onHitEntity(EntityHitResult result) {
+         super.onHitEntity(result);
+         if (this.level() instanceof ServerLevel serverLevel) {
+-            this.explode(serverLevel);
++            // CraftBukkit start
++            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callFireworkExplodeEvent(this).isCancelled()) {
++                this.explode(serverLevel);
++            }
++            // CraftBukkit end
+         }
+     }
+ 
+@@ -206,7 +_,11 @@
+         BlockPos blockPos = new BlockPos(result.getBlockPos());
+         this.level().getBlockState(blockPos).entityInside(this.level(), blockPos, this);
+         if (this.level() instanceof ServerLevel serverLevel && this.hasExplosion()) {
+-            this.explode(serverLevel);
++            // CraftBukkit start
++            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callFireworkExplodeEvent(this).isCancelled()) {
++                this.explode(serverLevel);
++            }
++            // CraftBukkit end
+         }
+ 
+         super.onHitBlock(result);
+@@ -278,6 +_,11 @@
+         compound.putInt("LifeTime", this.lifetime);
+         compound.put("FireworksItem", this.getItem().save(this.registryAccess()));
+         compound.putBoolean("ShotAtAngle", this.entityData.get(DATA_SHOT_AT_ANGLE));
++        // Paper start
++        if (this.spawningEntity != null) {
++            compound.putUUID("SpawningEntity", this.spawningEntity);
++        }
++        // Paper end
+     }
+ 
+     @Override
+@@ -298,6 +_,11 @@
+         if (compound.contains("ShotAtAngle")) {
+             this.entityData.set(DATA_SHOT_AT_ANGLE, compound.getBoolean("ShotAtAngle"));
+         }
++        // Paper start
++        if (compound.hasUUID("SpawningEntity")) {
++            this.spawningEntity = compound.getUUID("SpawningEntity");
++        }
++        // Paper end
+     }
+ 
+     private List<FireworkExplosion> getExplosions() {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/projectile/FishingHook.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/projectile/FishingHook.java.patch
new file mode 100644
index 0000000000..b433002c90
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/projectile/FishingHook.java.patch
@@ -0,0 +1,267 @@
+--- a/net/minecraft/world/entity/projectile/FishingHook.java
++++ b/net/minecraft/world/entity/projectile/FishingHook.java
+@@ -66,10 +_,26 @@
+     private final int luck;
+     private final int lureSpeed;
+ 
++    // CraftBukkit start - Extra variables to enable modification of fishing wait time, values are minecraft defaults
++    public int minWaitTime = 100;
++    public int maxWaitTime = 600;
++    public int minLureTime = 20;
++    public int maxLureTime = 80;
++    public float minLureAngle = 0.0F;
++    public float maxLureAngle = 360.0F;
++    public boolean applyLure = true;
++    public boolean rainInfluenced = true;
++    public boolean skyInfluenced = true;
++    // CraftBukkit end
++
+     private FishingHook(EntityType<? extends FishingHook> entityType, Level level, int luck, int lureSpeed) {
+         super(entityType, level);
+         this.luck = Math.max(0, luck);
+         this.lureSpeed = Math.max(0, lureSpeed);
++        // Paper start - Configurable fishing time ranges
++        this.minWaitTime = level.paperConfig().fishingTimeRange.minimum;
++        this.maxWaitTime = level.paperConfig().fishingTimeRange.maximum;
++        // Paper end - Configurable fishing time ranges
+     }
+ 
+     public FishingHook(EntityType<? extends FishingHook> entityType, Level level) {
+@@ -147,12 +_,12 @@
+         super.tick();
+         Player playerOwner = this.getPlayerOwner();
+         if (playerOwner == null) {
+-            this.discard();
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+         } else if (this.level().isClientSide || !this.shouldStopFishing(playerOwner)) {
+             if (this.onGround()) {
+                 this.life++;
+                 if (this.life >= 1200) {
+-                    this.discard();
++                    this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+                     return;
+                 }
+             } else {
+@@ -251,14 +_,14 @@
+         if (!player.isRemoved() && player.isAlive() && (isFishingRod || isFishingRod1) && !(this.distanceToSqr(player) > 1024.0)) {
+             return false;
+         } else {
+-            this.discard();
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+             return true;
+         }
+     }
+ 
+     private void checkCollision() {
+         HitResult hitResultOnMoveVector = ProjectileUtil.getHitResultOnMoveVector(this, this::canHitEntity);
+-        this.hitTargetOrDeflectSelf(hitResultOnMoveVector);
++        this.preHitTargetOrDeflectSelf(hitResultOnMoveVector);
+     }
+ 
+     @Override
+@@ -289,11 +_,11 @@
+         ServerLevel serverLevel = (ServerLevel)this.level();
+         int i = 1;
+         BlockPos blockPos = pos.above();
+-        if (this.random.nextFloat() < 0.25F && this.level().isRainingAt(blockPos)) {
++        if (this.rainInfluenced && this.random.nextFloat() < 0.25F && this.level().isRainingAt(blockPos)) { // CraftBukkit
+             i++;
+         }
+ 
+-        if (this.random.nextFloat() < 0.5F && !this.level().canSeeSky(blockPos)) {
++        if (this.skyInfluenced && this.random.nextFloat() < 0.5F && !this.level().canSeeSky(blockPos)) { // CraftBukkit
+             i--;
+         }
+ 
+@@ -303,6 +_,10 @@
+                 this.timeUntilLured = 0;
+                 this.timeUntilHooked = 0;
+                 this.getEntityData().set(DATA_BITING, false);
++                // CraftBukkit start
++                org.bukkit.event.player.PlayerFishEvent playerFishEvent = new org.bukkit.event.player.PlayerFishEvent((org.bukkit.entity.Player) this.getPlayerOwner().getBukkitEntity(), null, (org.bukkit.entity.FishHook) this.getBukkitEntity(), org.bukkit.event.player.PlayerFishEvent.State.FAILED_ATTEMPT);
++                this.level().getCraftServer().getPluginManager().callEvent(playerFishEvent);
++                // CraftBukkit end
+             }
+         } else if (this.timeUntilHooked > 0) {
+             this.timeUntilHooked -= i;
+@@ -326,6 +_,13 @@
+                     serverLevel.sendParticles(ParticleTypes.FISHING, d, d1, d2, 0, -f2, 0.01, f1, 1.0);
+                 }
+             } else {
++                // CraftBukkit start
++                org.bukkit.event.player.PlayerFishEvent playerFishEvent = new org.bukkit.event.player.PlayerFishEvent((org.bukkit.entity.Player) this.getPlayerOwner().getBukkitEntity(), null, (org.bukkit.entity.FishHook) this.getBukkitEntity(), org.bukkit.event.player.PlayerFishEvent.State.BITE);
++                this.level().getCraftServer().getPluginManager().callEvent(playerFishEvent);
++                if (playerFishEvent.isCancelled()) {
++                    return;
++                }
++                // CraftBukkit end
+                 this.playSound(SoundEvents.FISHING_BOBBER_SPLASH, 0.25F, 1.0F + (this.random.nextFloat() - this.random.nextFloat()) * 0.4F);
+                 double d3 = this.getY() + 0.5;
+                 serverLevel.sendParticles(
+@@ -377,14 +_,33 @@
+             }
+ 
+             if (this.timeUntilLured <= 0) {
+-                this.fishAngle = Mth.nextFloat(this.random, 0.0F, 360.0F);
+-                this.timeUntilHooked = Mth.nextInt(this.random, 20, 80);
++                // CraftBukkit start - logic to modify fishing wait time, lure time, and lure angle
++                this.fishAngle = Mth.nextFloat(this.random, this.minLureAngle, this.maxLureAngle);
++                this.timeUntilHooked = Mth.nextInt(this.random, this.minLureTime, this.maxLureTime);
++                // CraftBukkit end
++                // Paper start - Add missing fishing event state
++                if (this.getPlayerOwner() != null) {
++                    org.bukkit.event.player.PlayerFishEvent playerFishEvent = new org.bukkit.event.player.PlayerFishEvent((org.bukkit.entity.Player) this.getPlayerOwner().getBukkitEntity(), null, (org.bukkit.entity.FishHook) this.getBukkitEntity(), org.bukkit.event.player.PlayerFishEvent.State.LURED);
++                    if (!playerFishEvent.callEvent()) {
++                        this.timeUntilHooked = 0;
++                        return;
++                    }
++                }
++                // Paper end - Add missing fishing event state
+             }
+         } else {
+-            this.timeUntilLured = Mth.nextInt(this.random, 100, 600);
+-            this.timeUntilLured = this.timeUntilLured - this.lureSpeed;
++            // CraftBukkit start - logic to modify fishing wait time
++            this.resetTimeUntilLured(); // Paper - more projectile api - extract time until lured reset logic
++            // CraftBukkit end
+         }
+     }
++
++    // Paper start - more projectile api - extract time until lured reset logic
++    public void resetTimeUntilLured() {
++        this.timeUntilLured = Mth.nextInt(this.random, this.minWaitTime, this.maxWaitTime);
++        this.timeUntilLured -= (this.applyLure) ? (this.lureSpeed >= this.maxWaitTime ? this.timeUntilLured - 1 : this.lureSpeed ) : 0; // Paper - Fix Lure infinite loop
++    }
++    // Paper end - more projectile api - extract time until lured reset logic
+ 
+     public boolean calculateOpenWater(BlockPos pos) {
+         FishingHook.OpenWaterType openWaterType = FishingHook.OpenWaterType.INVALID;
+@@ -443,15 +_,34 @@
+     public void readAdditionalSaveData(CompoundTag compound) {
+     }
+ 
++
++    // Paper start - Add hand parameter to PlayerFishEvent
++    @Deprecated
++    @io.papermc.paper.annotation.DoNotUse
+     public int retrieve(ItemStack stack) {
++        return this.retrieve(net.minecraft.world.InteractionHand.MAIN_HAND, stack);
++    }
++
++    public int retrieve(net.minecraft.world.InteractionHand hand, ItemStack stack) {
++        // Paper end - Add hand parameter to PlayerFishEvent
+         Player playerOwner = this.getPlayerOwner();
+         if (!this.level().isClientSide && playerOwner != null && !this.shouldStopFishing(playerOwner)) {
+             int i = 0;
+             if (this.hookedIn != null) {
++                // CraftBukkit start
++                org.bukkit.event.player.PlayerFishEvent playerFishEvent = new org.bukkit.event.player.PlayerFishEvent((org.bukkit.entity.Player) playerOwner.getBukkitEntity(), this.hookedIn.getBukkitEntity(), (org.bukkit.entity.FishHook) this.getBukkitEntity(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand), org.bukkit.event.player.PlayerFishEvent.State.CAUGHT_ENTITY); // Paper - Add hand parameter to PlayerFishEvent
++                this.level().getCraftServer().getPluginManager().callEvent(playerFishEvent);
++
++                if (playerFishEvent.isCancelled()) {
++                    return 0;
++                }
++                if (this.hookedIn != null) { // Paper - re-check to see if there is a hooked entity
++                // CraftBukkit end
+                 this.pullEntity(this.hookedIn);
+                 CriteriaTriggers.FISHING_ROD_HOOKED.trigger((ServerPlayer)playerOwner, stack, this, Collections.emptyList());
+                 this.level().broadcastEntityEvent(this, (byte)31);
+                 i = this.hookedIn instanceof ItemEntity ? 3 : 5;
++                } // Paper - re-check to see if there is a hooked entity
+             } else if (this.nibble > 0) {
+                 LootParams lootParams = new LootParams.Builder((ServerLevel)this.level())
+                     .withParameter(LootContextParams.ORIGIN, this.position())
+@@ -465,18 +_,32 @@
+ 
+                 for (ItemStack itemStack : randomItems) {
+                     ItemEntity itemEntity = new ItemEntity(this.level(), this.getX(), this.getY(), this.getZ(), itemStack);
++                    // CraftBukkit start
++                    org.bukkit.event.player.PlayerFishEvent playerFishEvent = new org.bukkit.event.player.PlayerFishEvent((org.bukkit.entity.Player) playerOwner.getBukkitEntity(), itemEntity.getBukkitEntity(), (org.bukkit.entity.FishHook) this.getBukkitEntity(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand), org.bukkit.event.player.PlayerFishEvent.State.CAUGHT_FISH); // Paper - itemEntity may be null // Paper - Add hand parameter to PlayerFishEvent
++                    playerFishEvent.setExpToDrop(this.random.nextInt(6) + 1);
++                    this.level().getCraftServer().getPluginManager().callEvent(playerFishEvent);
++
++                    if (playerFishEvent.isCancelled()) {
++                        return 0;
++                    }
++                    // CraftBukkit end
+                     double d = playerOwner.getX() - this.getX();
+                     double d1 = playerOwner.getY() - this.getY();
+                     double d2 = playerOwner.getZ() - this.getZ();
+                     double d3 = 0.1;
+                     itemEntity.setDeltaMovement(d * 0.1, d1 * 0.1 + Math.sqrt(Math.sqrt(d * d + d1 * d1 + d2 * d2)) * 0.08, d2 * 0.1);
+                     this.level().addFreshEntity(itemEntity);
+-                    playerOwner.level()
+-                        .addFreshEntity(
+-                            new ExperienceOrb(
+-                                playerOwner.level(), playerOwner.getX(), playerOwner.getY() + 0.5, playerOwner.getZ() + 0.5, this.random.nextInt(6) + 1
+-                            )
+-                        );
++                    // CraftBukkit start - this.random.nextInt(6) + 1 -> playerFishEvent.getExpToDrop()
++                    if (playerFishEvent.getExpToDrop() > 0) {
++                        playerOwner.level()
++                            .addFreshEntity(
++                                new ExperienceOrb(
++                                    playerOwner.level(), playerOwner.getX(), playerOwner.getY() + 0.5, playerOwner.getZ() + 0.5, playerFishEvent.getExpToDrop(), org.bukkit.entity.ExperienceOrb.SpawnReason.FISHING, this.getPlayerOwner(), this // Paper
++                                )
++                            );
++                    }
++                    // CraftBukkit end
++
+                     if (itemStack.is(ItemTags.FISHES)) {
+                         playerOwner.awardStat(Stats.FISH_CAUGHT, 1);
+                     }
+@@ -486,10 +_,27 @@
+             }
+ 
+             if (this.onGround()) {
++                // CraftBukkit start
++                org.bukkit.event.player.PlayerFishEvent playerFishEvent = new org.bukkit.event.player.PlayerFishEvent((org.bukkit.entity.Player) playerOwner.getBukkitEntity(), null, (org.bukkit.entity.FishHook) this.getBukkitEntity(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand), org.bukkit.event.player.PlayerFishEvent.State.IN_GROUND); // Paper - Add hand parameter to PlayerFishEvent
++                this.level().getCraftServer().getPluginManager().callEvent(playerFishEvent);
++
++                if (playerFishEvent.isCancelled()) {
++                    return 0;
++                }
++                // CraftBukkit end
+                 i = 2;
+             }
++            // CraftBukkit start
++            if (i == 0) {
++                org.bukkit.event.player.PlayerFishEvent playerFishEvent = new org.bukkit.event.player.PlayerFishEvent((org.bukkit.entity.Player) playerOwner.getBukkitEntity(), null, (org.bukkit.entity.FishHook) this.getBukkitEntity(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand), org.bukkit.event.player.PlayerFishEvent.State.REEL_IN); // Paper - Add hand parameter to PlayerFishEvent
++                this.level().getCraftServer().getPluginManager().callEvent(playerFishEvent);
++                if (playerFishEvent.isCancelled()) {
++                    return 0;
++                }
++            }
++            // CraftBukkit end
+ 
+-            this.discard();
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+             return i;
+         } else {
+             return 0;
+@@ -520,8 +_,15 @@
+ 
+     @Override
+     public void remove(Entity.RemovalReason reason) {
++        // CraftBukkit start - add Bukkit remove cause
++        this.remove(reason, null);
++    }
++
++    @Override
++    public void remove(Entity.RemovalReason reason, org.bukkit.event.entity.EntityRemoveEvent.Cause cause) {
++        // CraftBukkit end
+         this.updateOwnerInfo(null);
+-        super.remove(reason);
++        super.remove(reason, cause); // CraftBukkit - add Bukkit remove cause
+     }
+ 
+     @Override
+@@ -570,7 +_,7 @@
+         if (this.getPlayerOwner() == null) {
+             int data = packet.getData();
+             LOGGER.error("Failed to recreate fishing hook on client. {} (id: {}) is not a valid owner.", this.level().getEntity(data), data);
+-            this.discard();
++            this.discard(null); // CraftBukkit - add Bukkit remove cause
+         }
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/projectile/LargeFireball.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/projectile/LargeFireball.java.patch
new file mode 100644
index 0000000000..d484e6d4eb
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/projectile/LargeFireball.java.patch
@@ -0,0 +1,45 @@
+--- a/net/minecraft/world/entity/projectile/LargeFireball.java
++++ b/net/minecraft/world/entity/projectile/LargeFireball.java
+@@ -18,11 +_,13 @@
+ 
+     public LargeFireball(EntityType<? extends LargeFireball> entityType, Level level) {
+         super(entityType, level);
++        this.isIncendiary = (level instanceof ServerLevel serverLevel) && serverLevel.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // CraftBukkit
+     }
+ 
+     public LargeFireball(Level level, LivingEntity owner, Vec3 movement, int explosionPower) {
+         super(EntityType.FIREBALL, owner, movement, level);
+         this.explosionPower = explosionPower;
++        this.isIncendiary = (level instanceof ServerLevel serverLevel) && serverLevel.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // CraftBukkit
+     }
+ 
+     @Override
+@@ -30,8 +_,16 @@
+         super.onHit(result);
+         if (this.level() instanceof ServerLevel serverLevel) {
+             boolean _boolean = serverLevel.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING);
+-            this.level().explode(this, this.getX(), this.getY(), this.getZ(), this.explosionPower, _boolean, Level.ExplosionInteraction.MOB);
+-            this.discard();
++            // CraftBukkit start - fire ExplosionPrimeEvent
++            org.bukkit.event.entity.ExplosionPrimeEvent event = new org.bukkit.event.entity.ExplosionPrimeEvent((org.bukkit.entity.Explosive) this.getBukkitEntity());
++            this.level().getCraftServer().getPluginManager().callEvent(event);
++
++            if (!event.isCancelled()) {
++                // give 'this' instead of (Entity) null so we know what causes the damage
++                this.level().explode(this, this.getX(), this.getY(), this.getZ(), event.getRadius(), event.getFire(), Level.ExplosionInteraction.MOB);
++            }
++            // CraftBukkit end
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
+         }
+     }
+ 
+@@ -57,7 +_,8 @@
+     public void readAdditionalSaveData(CompoundTag compound) {
+         super.readAdditionalSaveData(compound);
+         if (compound.contains("ExplosionPower", 99)) {
+-            this.explosionPower = compound.getByte("ExplosionPower");
++            // CraftBukkit - set bukkitYield when setting explosionpower
++            this.bukkitYield = this.explosionPower = compound.getByte("ExplosionPower");
+         }
+     }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/projectile/LlamaSpit.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/projectile/LlamaSpit.java.patch
new file mode 100644
index 0000000000..1e27f667a5
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/projectile/LlamaSpit.java.patch
@@ -0,0 +1,31 @@
+--- a/net/minecraft/world/entity/projectile/LlamaSpit.java
++++ b/net/minecraft/world/entity/projectile/LlamaSpit.java
+@@ -43,16 +_,16 @@
+         super.tick();
+         Vec3 deltaMovement = this.getDeltaMovement();
+         HitResult hitResultOnMoveVector = ProjectileUtil.getHitResultOnMoveVector(this, this::canHitEntity);
+-        this.hitTargetOrDeflectSelf(hitResultOnMoveVector);
++        this.preHitTargetOrDeflectSelf(hitResultOnMoveVector); // CraftBukkit - projectile hit event
+         double d = this.getX() + deltaMovement.x;
+         double d1 = this.getY() + deltaMovement.y;
+         double d2 = this.getZ() + deltaMovement.z;
+         this.updateRotation();
+         float f = 0.99F;
+         if (this.level().getBlockStates(this.getBoundingBox()).noneMatch(BlockBehaviour.BlockStateBase::isAir)) {
+-            this.discard();
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+         } else if (this.isInWaterOrBubble()) {
+-            this.discard();
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+         } else {
+             this.setDeltaMovement(deltaMovement.scale(0.99F));
+             this.applyGravity();
+@@ -76,7 +_,7 @@
+     protected void onHitBlock(BlockHitResult result) {
+         super.onHitBlock(result);
+         if (!this.level().isClientSide) {
+-            this.discard();
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
+         }
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/projectile/Projectile.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/projectile/Projectile.java.patch
new file mode 100644
index 0000000000..9052fd22ba
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/projectile/Projectile.java.patch
@@ -0,0 +1,228 @@
+--- a/net/minecraft/world/entity/projectile/Projectile.java
++++ b/net/minecraft/world/entity/projectile/Projectile.java
+@@ -43,6 +_,7 @@
+     public boolean hasBeenShot;
+     @Nullable
+     private Entity lastDeflectedBy;
++    protected boolean hitCancelled = false; // CraftBukkit
+ 
+     Projectile(EntityType<? extends Projectile> entityType, Level level) {
+         super(entityType, level);
+@@ -53,15 +_,36 @@
+             this.ownerUUID = owner.getUUID();
+             this.cachedOwner = owner;
+         }
+-    }
++        // Paper start - Refresh ProjectileSource for projectiles
++        else {
++            this.ownerUUID = null;
++            this.cachedOwner = null;
++            this.projectileSource = null;
++        }
++        // Paper end - Refresh ProjectileSource for projectiles
++        this.refreshProjectileSource(false); // Paper
++    }
++
++    // Paper start - Refresh ProjectileSource for projectiles
++    public void refreshProjectileSource(boolean fillCache) {
++        if (fillCache) {
++            this.getOwner();
++        }
++        if (this.cachedOwner != null && !this.cachedOwner.isRemoved() && this.projectileSource == null && this.cachedOwner.getBukkitEntity() instanceof org.bukkit.projectiles.ProjectileSource projSource) {
++            this.projectileSource = projSource;
++        }
++    }
++    // Paper end - Refresh ProjectileSource for projectiles
+ 
+     @Nullable
+     @Override
+     public Entity getOwner() {
+         if (this.cachedOwner != null && !this.cachedOwner.isRemoved()) {
++            this.refreshProjectileSource(false); // Paper - Refresh ProjectileSource for projectiles
+             return this.cachedOwner;
+         } else if (this.ownerUUID != null) {
+             this.cachedOwner = this.findOwner(this.ownerUUID);
++            this.refreshProjectileSource(false); // Paper - Refresh ProjectileSource for projectiles
+             return this.cachedOwner;
+         } else {
+             return null;
+@@ -98,6 +_,7 @@
+     protected void readAdditionalSaveData(CompoundTag compound) {
+         if (compound.hasUUID("Owner")) {
+             this.setOwnerThroughUUID(compound.getUUID("Owner"));
++            if (this instanceof ThrownEnderpearl && this.level().paperConfig().fixes.disableUnloadedChunkEnderpearlExploit && this.level().paperConfig().misc.legacyEnderPearlBehavior) { this.ownerUUID = null; } // Paper - Reset pearls when they stop being ticked; Don't store shooter name for pearls to block enderpearl travel exploit
+         }
+ 
+         this.leftOwner = compound.getBoolean("LeftOwner");
+@@ -175,13 +_,22 @@
+         float f2 = Mth.cos(y * (float) (Math.PI / 180.0)) * Mth.cos(x * (float) (Math.PI / 180.0));
+         this.shoot(f, f1, f2, velocity, inaccuracy);
+         Vec3 knownMovement = shooter.getKnownMovement();
++        // Paper start - allow disabling relative velocity
++        if (!shooter.level().paperConfig().misc.disableRelativeProjectileVelocity) {
+         this.setDeltaMovement(this.getDeltaMovement().add(knownMovement.x, shooter.onGround() ? 0.0 : knownMovement.y, knownMovement.z));
++        }
++        // Paper end - allow disabling relative velocity
+     }
+ 
+     public static <T extends Projectile> T spawnProjectileFromRotation(
+         Projectile.ProjectileFactory<T> factory, ServerLevel level, ItemStack spawnedFrom, LivingEntity owner, float z, float velocity, float innaccuracy
+     ) {
+-        return spawnProjectile(
++        // Paper start - PlayerLaunchProjectileEvent
++        return spawnProjectileFromRotationDelayed(factory, level, spawnedFrom, owner, z, velocity, innaccuracy).spawn();
++    }
++    public static <T extends Projectile> Delayed<T> spawnProjectileFromRotationDelayed(Projectile.ProjectileFactory<T> factory, ServerLevel level, ItemStack spawnedFrom, LivingEntity owner, float z, float velocity, float innaccuracy) {
++        return spawnProjectileDelayed(
++        // Paper end - PlayerLaunchProjectileEvent
+             factory.create(level, owner, spawnedFrom),
+             level,
+             spawnedFrom,
+@@ -206,7 +_,12 @@
+     public static <T extends Projectile> T spawnProjectileUsingShoot(
+         T projectile, ServerLevel level, ItemStack spawnedFrom, double x, double y, double z, float velocity, float inaccuracy
+     ) {
+-        return spawnProjectile(projectile, level, spawnedFrom, projectile1 -> projectile.shoot(x, y, z, velocity, inaccuracy));
++    // Paper start - fixes and addition to spawn reason API
++        return spawnProjectileUsingShootDelayed(projectile, level, spawnedFrom, x, y, z, velocity, inaccuracy).spawn();
++    }
++    public static <T extends Projectile> Delayed<T> spawnProjectileUsingShootDelayed(T projectile, ServerLevel level, ItemStack spawnedFrom, double x, double y, double z, float velocity, float inaccuracy) {
++        return spawnProjectileDelayed(projectile, level, spawnedFrom, projectile1 -> projectile.shoot(x, y, z, velocity, inaccuracy));
++    // Paper end - fixes and addition to spawn reason API
+     }
+ 
+     public static <T extends Projectile> T spawnProjectile(T projectile, ServerLevel level, ItemStack spawnedFrom) {
+@@ -214,11 +_,45 @@
+     }
+ 
+     public static <T extends Projectile> T spawnProjectile(T projectile, ServerLevel level, ItemStack stack, Consumer<T> adapter) {
++        // Paper start - delayed projectile spawning
++        return spawnProjectileDelayed(projectile, level, stack, adapter).spawn();
++    }
++    public static <T extends Projectile> Delayed<T> spawnProjectileDelayed(T projectile, ServerLevel level, ItemStack stack, Consumer<T> adapter) {
++        // Paper end - delayed projectile spawning
+         adapter.accept(projectile);
+-        level.addFreshEntity(projectile);
+-        projectile.applyOnProjectileSpawned(level, stack);
+-        return projectile;
+-    }
++        return new Delayed<>(projectile, level, stack); // Paper - delayed projectile spawning
++    }
++
++    // Paper start - delayed projectile spawning
++    public record Delayed<T extends Projectile>(
++        T projectile,
++        ServerLevel world,
++        ItemStack projectileStack
++    ) {
++        // Taken from net.minecraft.world.entity.projectile.Projectile.spawnProjectile(T, net.minecraft.server.level.ServerLevel, net.minecraft.world.item.ItemStack, java.util.function.Consumer<T>)
++        public boolean attemptSpawn() {
++            if (!this.world.addFreshEntity(this.projectile)) return false;
++            this.projectile.applyOnProjectileSpawned(this.world, this.projectileStack);
++            return true;
++        }
++
++        public T spawn() {
++            this.attemptSpawn();
++            return this.projectile();
++        }
++
++        public boolean attemptSpawn(final org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) {
++            if (!this.world.addFreshEntity(this.projectile, reason)) return false;
++            this.projectile.applyOnProjectileSpawned(this.world, this.projectileStack);
++            return true;
++        }
++
++        public T spawn(final org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) {
++            this.attemptSpawn(reason);
++            return this.projectile();
++        }
++    }
++    // Paper end - delayed projectile spawning
+ 
+     public void applyOnProjectileSpawned(ServerLevel level, ItemStack spawnedFrom) {
+         EnchantmentHelper.onProjectileSpawned(level, spawnedFrom, this, item -> {});
+@@ -230,6 +_,17 @@
+         }
+     }
+ 
++    // CraftBukkit start - call projectile hit event
++    public ProjectileDeflection preHitTargetOrDeflectSelf(HitResult hitResult) { // Paper - protected -> public
++        org.bukkit.event.entity.ProjectileHitEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callProjectileHitEvent(this, hitResult);
++        this.hitCancelled = event != null && event.isCancelled();
++        if (hitResult.getType() == HitResult.Type.BLOCK || !this.hitCancelled) {
++            return this.hitTargetOrDeflectSelf(hitResult);
++        }
++        return ProjectileDeflection.NONE;
++    }
++    // CraftBukkit end
++
+     protected ProjectileDeflection hitTargetOrDeflectSelf(HitResult hitResult) {
+         if (hitResult.getType() == HitResult.Type.ENTITY) {
+             EntityHitResult entityHitResult = (EntityHitResult)hitResult;
+@@ -261,7 +_,13 @@
+     public boolean deflect(ProjectileDeflection deflection, @Nullable Entity entity, @Nullable Entity owner, boolean deflectedByPlayer) {
+         deflection.deflect(this, entity, this.random);
+         if (!this.level().isClientSide) {
+-            this.setOwner(owner);
++            // Paper start - Fix PickupStatus getting reset
++            if (this instanceof AbstractArrow arrow) {
++                arrow.setOwner(owner, false);
++            } else {
++                this.setOwner(owner);
++            }
++            // Paper end - Fix PickupStatus getting reset
+             this.onDeflection(entity, deflectedByPlayer);
+         }
+ 
+@@ -297,15 +_,35 @@
+     }
+ 
+     protected void onHitBlock(BlockHitResult result) {
++        // CraftBukkit start - cancellable hit event
++        if (this.hitCancelled) {
++            return;
++        }
++        // CraftBukkit end
+         BlockState blockState = this.level().getBlockState(result.getBlockPos());
+         blockState.onProjectileHit(this.level(), blockState, result, this);
+     }
+ 
++    // Paper start
++    public boolean canHitEntityPublic(final Entity target) {
++        return this.canHitEntity(target);
++    }
++    // Paper end
++
+     protected boolean canHitEntity(Entity target) {
+         if (!target.canBeHitByProjectile()) {
+             return false;
+         } else {
+             Entity owner = this.getOwner();
++            // Paper start - Cancel hit for vanished players
++            if (owner instanceof net.minecraft.server.level.ServerPlayer && target instanceof net.minecraft.server.level.ServerPlayer) {
++                org.bukkit.entity.Player collided = (org.bukkit.entity.Player) target.getBukkitEntity();
++                org.bukkit.entity.Player shooter = (org.bukkit.entity.Player) owner.getBukkitEntity();
++                if (!shooter.canSee(collided)) {
++                    return false;
++                }
++            }
++            // Paper end - Cancel hit for vanished players
+             return owner == null || this.leftOwner || !owner.isPassengerOfSameVehicle(target);
+         }
+     }
+@@ -318,13 +_,7 @@
+     }
+ 
+     protected static float lerpRotation(float currentRotation, float targetRotation) {
+-        while (targetRotation - currentRotation < -180.0F) {
+-            currentRotation -= 360.0F;
+-        }
+-
+-        while (targetRotation - currentRotation >= 180.0F) {
+-            currentRotation += 360.0F;
+-        }
++        currentRotation += Math.round((targetRotation - currentRotation) / 360.0F) * 360.0F; // Paper - stop large look changes from crashing the server
+ 
+         return Mth.lerp(0.2F, currentRotation, targetRotation);
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/projectile/ShulkerBullet.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/projectile/ShulkerBullet.java.patch
new file mode 100644
index 0000000000..8f79e335e3
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/projectile/ShulkerBullet.java.patch
@@ -0,0 +1,91 @@
+--- a/net/minecraft/world/entity/projectile/ShulkerBullet.java
++++ b/net/minecraft/world/entity/projectile/ShulkerBullet.java
+@@ -57,7 +_,21 @@
+         this.finalTarget = finalTarget;
+         this.currentMoveDirection = Direction.UP;
+         this.selectNextMoveDirection(axis);
+-    }
++        this.projectileSource = shooter.getBukkitLivingEntity(); // CraftBukkit
++    }
++
++    // CraftBukkit start
++    @Nullable
++    public Entity getTarget() {
++        return this.finalTarget;
++    }
++
++    public void setTarget(Entity finalTarget) {
++        this.finalTarget = finalTarget;
++        this.currentMoveDirection = Direction.UP;
++        this.selectNextMoveDirection(Direction.Axis.X);
++    }
++    // CraftBukkit end
+ 
+     @Override
+     public SoundSource getSoundSource() {
+@@ -187,7 +_,7 @@
+     @Override
+     public void checkDespawn() {
+         if (this.level().getDifficulty() == Difficulty.PEACEFUL) {
+-            this.discard();
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+         }
+     }
+ 
+@@ -233,7 +_,7 @@
+         }
+ 
+         if (hitResult != null && this.isAlive() && hitResult.getType() != HitResult.Type.MISS) {
+-            this.hitTargetOrDeflectSelf(hitResult);
++            this.preHitTargetOrDeflectSelf(hitResult); // CraftBukkit - projectile hit event
+         }
+ 
+         ProjectileUtil.rotateTowardsMovement(this, 0.5F);
+@@ -301,7 +_,7 @@
+             }
+ 
+             if (entity instanceof LivingEntity livingEntity1) {
+-                livingEntity1.addEffect(new MobEffectInstance(MobEffects.LEVITATION, 200), MoreObjects.firstNonNull(owner, this));
++                livingEntity1.addEffect(new MobEffectInstance(MobEffects.LEVITATION, 200), MoreObjects.firstNonNull(owner, this), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ATTACK); // CraftBukkit
+             }
+         }
+     }
+@@ -314,14 +_,20 @@
+     }
+ 
+     private void destroy() {
+-        this.discard();
++        // CraftBukkit start - add Bukkit remove cause
++        this.destroy(null);
++    }
++
++    private void destroy(org.bukkit.event.entity.EntityRemoveEvent.Cause cause) {
++        this.discard(cause);
++        // CraftBukkit end
+         this.level().gameEvent(GameEvent.ENTITY_DAMAGE, this.position(), GameEvent.Context.of(this));
+     }
+ 
+     @Override
+     protected void onHit(HitResult result) {
+         super.onHit(result);
+-        this.destroy();
++        this.destroy(org.bukkit.event.entity.EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
+     }
+ 
+     @Override
+@@ -336,9 +_,14 @@
+ 
+     @Override
+     public boolean hurtServer(ServerLevel level, DamageSource damageSource, float amount) {
++        // CraftBukkit start
++        if (org.bukkit.craftbukkit.event.CraftEventFactory.handleNonLivingEntityDamageEvent(this, damageSource, amount, false)) {
++            return false;
++        }
++        // CraftBukkit end
+         this.playSound(SoundEvents.SHULKER_BULLET_HURT, 1.0F, 1.0F);
+         level.sendParticles(ParticleTypes.CRIT, this.getX(), this.getY(), this.getZ(), 15, 0.2, 0.2, 0.2, 0.0);
+-        this.destroy();
++        this.destroy(org.bukkit.event.entity.EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause
+         return true;
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/projectile/SmallFireball.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/projectile/SmallFireball.java.patch
new file mode 100644
index 0000000000..ceefdcdb81
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/projectile/SmallFireball.java.patch
@@ -0,0 +1,51 @@
+--- a/net/minecraft/world/entity/projectile/SmallFireball.java
++++ b/net/minecraft/world/entity/projectile/SmallFireball.java
+@@ -23,6 +_,11 @@
+ 
+     public SmallFireball(Level level, LivingEntity owner, Vec3 movement) {
+         super(EntityType.SMALL_FIREBALL, owner, movement, level);
++        // CraftBukkit start
++        if (this.getOwner() != null && this.getOwner() instanceof Mob) {
++            this.isIncendiary = (level instanceof ServerLevel serverLevel) && serverLevel.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING);
++        }
++        // CraftBukkit end
+     }
+ 
+     public SmallFireball(Level level, double x, double y, double z, Vec3 movement) {
+@@ -36,7 +_,14 @@
+             Entity var7 = result.getEntity();
+             Entity owner = this.getOwner();
+             int remainingFireTicks = var7.getRemainingFireTicks();
+-            var7.igniteForSeconds(5.0F);
++            // CraftBukkit start - Entity damage by entity event + combust event
++            org.bukkit.event.entity.EntityCombustByEntityEvent event = new org.bukkit.event.entity.EntityCombustByEntityEvent(this.getBukkitEntity(), var7.getBukkitEntity(), 5.0F);
++            var7.level().getCraftServer().getPluginManager().callEvent(event);
++
++            if (!event.isCancelled()) {
++                var7.igniteForSeconds(event.getDuration(), false);
++            }
++            // CraftBukkit end
+             DamageSource damageSource = this.damageSources().fireball(this, owner);
+             if (!var7.hurtServer(serverLevel, damageSource, 5.0F)) {
+                 var7.setRemainingFireTicks(remainingFireTicks);
+@@ -51,9 +_,9 @@
+         super.onHitBlock(result);
+         if (this.level() instanceof ServerLevel serverLevel) {
+             Entity owner = this.getOwner();
+-            if (!(owner instanceof Mob) || serverLevel.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) {
++            if (this.isIncendiary) { // CraftBukkit
+                 BlockPos blockPos = result.getBlockPos().relative(result.getDirection());
+-                if (this.level().isEmptyBlock(blockPos)) {
++                if (this.level().isEmptyBlock(blockPos) && !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(this.level(), blockPos, this).isCancelled()) { // CraftBukkit
+                     this.level().setBlockAndUpdate(blockPos, BaseFireBlock.getState(this.level(), blockPos));
+                 }
+             }
+@@ -64,7 +_,7 @@
+     protected void onHit(HitResult result) {
+         super.onHit(result);
+         if (!this.level().isClientSide) {
+-            this.discard();
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
+         }
+     }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/projectile/Snowball.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/projectile/Snowball.java.patch
new file mode 100644
index 0000000000..5c87f7788c
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/projectile/Snowball.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/projectile/Snowball.java
++++ b/net/minecraft/world/entity/projectile/Snowball.java
+@@ -61,7 +_,7 @@
+         super.onHit(result);
+         if (!this.level().isClientSide) {
+             this.level().broadcastEntityEvent(this, (byte)3);
+-            this.discard();
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
+         }
+     }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/projectile/SpectralArrow.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/projectile/SpectralArrow.java.patch
new file mode 100644
index 0000000000..e7c4ff58ef
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/projectile/SpectralArrow.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/projectile/SpectralArrow.java
++++ b/net/minecraft/world/entity/projectile/SpectralArrow.java
+@@ -38,7 +_,7 @@
+     protected void doPostHurtEffects(LivingEntity living) {
+         super.doPostHurtEffects(living);
+         MobEffectInstance mobEffectInstance = new MobEffectInstance(MobEffects.GLOWING, this.duration, 0);
+-        living.addEffect(mobEffectInstance, this.getEffectSource());
++        living.addEffect(mobEffectInstance, this.getEffectSource(), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ARROW); // CraftBukkit
+     }
+ 
+     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrowableItemProjectile.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/projectile/ThrowableItemProjectile.java.patch
similarity index 77%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrowableItemProjectile.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/projectile/ThrowableItemProjectile.java.patch
index 6471dbc99e..bd84b5d136 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrowableItemProjectile.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/projectile/ThrowableItemProjectile.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/entity/projectile/ThrowableItemProjectile.java
 +++ b/net/minecraft/world/entity/projectile/ThrowableItemProjectile.java
-@@ -34,6 +34,12 @@
+@@ -35,6 +_,12 @@
  
      protected abstract Item getDefaultItem();
  
@@ -12,4 +12,4 @@
 +
      @Override
      public ItemStack getItem() {
-         return (ItemStack) this.getEntityData().get(ThrowableItemProjectile.DATA_ITEM_STACK);
+         return this.getEntityData().get(DATA_ITEM_STACK);
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/projectile/ThrowableProjectile.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/projectile/ThrowableProjectile.java.patch
new file mode 100644
index 0000000000..3a35555ec9
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/projectile/ThrowableProjectile.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/projectile/ThrowableProjectile.java
++++ b/net/minecraft/world/entity/projectile/ThrowableProjectile.java
+@@ -59,7 +_,7 @@
+         this.applyEffectsFromBlocks();
+         super.tick();
+         if (hitResultOnMoveVector.getType() != HitResult.Type.MISS && this.isAlive()) {
+-            this.hitTargetOrDeflectSelf(hitResultOnMoveVector);
++            this.preHitTargetOrDeflectSelf(hitResultOnMoveVector); // CraftBukkit - projectile hit event
+         }
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/projectile/ThrownEgg.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/projectile/ThrownEgg.java.patch
new file mode 100644
index 0000000000..a19b02d297
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/projectile/ThrownEgg.java.patch
@@ -0,0 +1,70 @@
+--- a/net/minecraft/world/entity/projectile/ThrownEgg.java
++++ b/net/minecraft/world/entity/projectile/ThrownEgg.java
+@@ -59,28 +_,62 @@
+     protected void onHit(HitResult result) {
+         super.onHit(result);
+         if (!this.level().isClientSide) {
+-            if (this.random.nextInt(8) == 0) {
++            // CraftBukkit start
++            boolean hatching = this.random.nextInt(8) == 0;
++            if (true) {
++            // CraftBukkit end
+                 int i = 1;
+                 if (this.random.nextInt(32) == 0) {
+                     i = 4;
+                 }
++                // CraftBukkit start
++                org.bukkit.entity.EntityType hatchingType = org.bukkit.entity.EntityType.CHICKEN;
++
++                net.minecraft.world.entity.Entity shooter = this.getOwner();
++                if (!hatching) {
++                    i = 0;
++                }
++                if (shooter instanceof net.minecraft.server.level.ServerPlayer) {
++                    org.bukkit.event.player.PlayerEggThrowEvent event = new org.bukkit.event.player.PlayerEggThrowEvent((org.bukkit.entity.Player) shooter.getBukkitEntity(), (org.bukkit.entity.Egg) this.getBukkitEntity(), hatching, (byte) i, hatchingType);
++                    this.level().getCraftServer().getPluginManager().callEvent(event);
++
++                    i = event.getNumHatches();
++                    hatching = event.isHatching();
++                    hatchingType = event.getHatchingType();
++                    // If hatching is set to false, ensure child count is 0
++                    if (!hatching) {
++                        i = 0;
++                    }
++                }
++                // CraftBukkit end
++                // Paper start - Add ThrownEggHatchEvent
++                com.destroystokyo.paper.event.entity.ThrownEggHatchEvent event = new com.destroystokyo.paper.event.entity.ThrownEggHatchEvent((org.bukkit.entity.Egg) getBukkitEntity(), hatching, (byte) i, hatchingType);
++                event.callEvent();
++                hatching = event.isHatching();
++                i = hatching ? event.getNumHatches() : 0; // If hatching is set to false, ensure child count is 0
++                hatchingType = event.getHatchingType();
++                // Paper end - Add ThrownEggHatchEvent
+ 
+                 for (int i1 = 0; i1 < i; i1++) {
+-                    Chicken chicken = EntityType.CHICKEN.create(this.level(), EntitySpawnReason.TRIGGERED);
++                    net.minecraft.world.entity.Entity chicken = this.level().getWorld().makeEntity(new org.bukkit.Location(this.level().getWorld(), this.getX(), this.getY(), this.getZ(), this.getYRot(), 0.0F), hatchingType.getEntityClass()); // CraftBukkit
+                     if (chicken != null) {
+-                        chicken.setAge(-24000);
++                        // CraftBukkit start
++                        if (chicken.getBukkitEntity() instanceof org.bukkit.entity.Ageable ageable) {
++                            ageable.setBaby();
++                        }
++                        // CraftBukkit end
+                         chicken.moveTo(this.getX(), this.getY(), this.getZ(), this.getYRot(), 0.0F);
+                         if (!chicken.fudgePositionAfterSizeChange(ZERO_SIZED_DIMENSIONS)) {
+                             break;
+                         }
+ 
+-                        this.level().addFreshEntity(chicken);
++                        this.level().addFreshEntity(chicken, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.EGG); // CraftBukkit
+                     }
+                 }
+             }
+ 
+             this.level().broadcastEntityEvent(this, (byte)3);
+-            this.discard();
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
+         }
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/projectile/ThrownEnderpearl.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/projectile/ThrownEnderpearl.java.patch
new file mode 100644
index 0000000000..8fdaf1cd3c
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/projectile/ThrownEnderpearl.java.patch
@@ -0,0 +1,85 @@
+--- a/net/minecraft/world/entity/projectile/ThrownEnderpearl.java
++++ b/net/minecraft/world/entity/projectile/ThrownEnderpearl.java
+@@ -126,11 +_,18 @@
+                 Vec3 vec3 = this.oldPosition();
+                 if (owner instanceof ServerPlayer serverPlayer) {
+                     if (serverPlayer.connection.isAcceptingMessages()) {
++                        // CraftBukkit start
++                        ServerPlayer serverPlayer1 = serverPlayer.teleport(new TeleportTransition(serverLevel, vec3, Vec3.ZERO, 0.0F, 0.0F, Relative.union(Relative.ROTATION, Relative.DELTA), TeleportTransition.DO_NOTHING, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.ENDER_PEARL));
++                        if (serverPlayer1 == null) {
++                            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.HIT);
++                            return;
++                        }
++                        // CraftBukkit end
+                         if (this.random.nextFloat() < 0.05F && serverLevel.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) {
+                             Endermite endermite = EntityType.ENDERMITE.create(serverLevel, EntitySpawnReason.TRIGGERED);
+                             if (endermite != null) {
+                                 endermite.moveTo(owner.getX(), owner.getY(), owner.getZ(), owner.getYRot(), owner.getXRot());
+-                                serverLevel.addFreshEntity(endermite);
++                                serverLevel.addFreshEntity(endermite, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.ENDER_PEARL);
+                             }
+                         }
+ 
+@@ -138,15 +_,17 @@
+                             owner.setPortalCooldown();
+                         }
+ 
+-                        ServerPlayer serverPlayer1 = serverPlayer.teleport(
+-                            new TeleportTransition(
+-                                serverLevel, vec3, Vec3.ZERO, 0.0F, 0.0F, Relative.union(Relative.ROTATION, Relative.DELTA), TeleportTransition.DO_NOTHING
+-                            )
+-                        );
++                        // CraftBukkit start - moved up
++                        // ServerPlayer serverPlayer1 = serverPlayer.teleport(
++                        //     new TeleportTransition(
++                        //         serverLevel, vec3, Vec3.ZERO, 0.0F, 0.0F, Relative.union(Relative.ROTATION, Relative.DELTA), TeleportTransition.DO_NOTHING
++                        //     )
++                        // );
++                        // CraftBukkit end - moved up
+                         if (serverPlayer1 != null) {
+                             serverPlayer1.resetFallDistance();
+                             serverPlayer1.resetCurrentImpulseContext();
+-                            serverPlayer1.hurtServer(serverPlayer.serverLevel(), this.damageSources().enderPearl(), 5.0F);
++                            serverPlayer1.hurtServer(serverPlayer.serverLevel(), this.damageSources().enderPearl().customEventDamager(this), 5.0F); // CraftBukkit // Paper - fix DamageSource API
+                         }
+ 
+                         this.playSound(serverLevel, vec3);
+@@ -162,9 +_,9 @@
+                     this.playSound(serverLevel, vec3);
+                 }
+ 
+-                this.discard();
++                this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
+             } else {
+-                this.discard();
++                this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
+             }
+         }
+     }
+@@ -185,7 +_,7 @@
+         if (owner instanceof ServerPlayer serverPlayer
+             && !owner.isAlive()
+             && serverPlayer.serverLevel().getGameRules().getBoolean(GameRules.RULE_ENDER_PEARLS_VANISH_ON_DEATH)) {
+-            this.discard();
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+         } else {
+             super.tick();
+         }
+@@ -212,7 +_,7 @@
+     public Entity teleport(TeleportTransition teleportTransition) {
+         Entity entity = super.teleport(teleportTransition);
+         if (entity != null) {
+-            entity.placePortalTicket(BlockPos.containing(entity.position()));
++            if (!this.level().paperConfig().misc.legacyEnderPearlBehavior) entity.placePortalTicket(BlockPos.containing(entity.position())); // Paper - Allow using old ender pearl behavior
+         }
+ 
+         return entity;
+@@ -220,7 +_,7 @@
+ 
+     @Override
+     public boolean canTeleport(Level fromLevel, Level toLevel) {
+-        return fromLevel.dimension() == Level.END && toLevel.dimension() == Level.OVERWORLD && this.getOwner() instanceof ServerPlayer serverPlayer
++        return fromLevel.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.END && toLevel.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.OVERWORLD && this.getOwner() instanceof ServerPlayer serverPlayer // CraftBukkit
+             ? super.canTeleport(fromLevel, toLevel) && serverPlayer.seenCredits
+             : super.canTeleport(fromLevel, toLevel);
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/projectile/ThrownExperienceBottle.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/projectile/ThrownExperienceBottle.java.patch
new file mode 100644
index 0000000000..e560b1ef1e
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/projectile/ThrownExperienceBottle.java.patch
@@ -0,0 +1,23 @@
+--- a/net/minecraft/world/entity/projectile/ThrownExperienceBottle.java
++++ b/net/minecraft/world/entity/projectile/ThrownExperienceBottle.java
+@@ -37,10 +_,17 @@
+     protected void onHit(HitResult result) {
+         super.onHit(result);
+         if (this.level() instanceof ServerLevel) {
+-            this.level().levelEvent(2002, this.blockPosition(), -13083194);
++            // CraftBukkit - moved to after event
+             int i = 3 + this.level().random.nextInt(5) + this.level().random.nextInt(5);
+-            ExperienceOrb.award((ServerLevel)this.level(), this.position(), i);
+-            this.discard();
++            // CraftBukkit start
++            org.bukkit.event.entity.ExpBottleEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callExpBottleEvent(this, result, i);
++            i = event.getExperience();
++            if (event.getShowEffect()) {
++                this.level().levelEvent(net.minecraft.world.level.block.LevelEvent.PARTICLES_SPELL_POTION_SPLASH, this.blockPosition(), net.minecraft.world.item.alchemy.PotionContents.BASE_POTION_COLOR);
++            }
++            // CraftBukkit end
++            ExperienceOrb.award((ServerLevel)this.level(), this.position(), i, org.bukkit.entity.ExperienceOrb.SpawnReason.EXP_BOTTLE, this.getOwner(), this); // Paper
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
+         }
+     }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/projectile/ThrownPotion.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/projectile/ThrownPotion.java.patch
new file mode 100644
index 0000000000..0fbeafb100
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/projectile/ThrownPotion.java.patch
@@ -0,0 +1,237 @@
+--- a/net/minecraft/world/entity/projectile/ThrownPotion.java
++++ b/net/minecraft/world/entity/projectile/ThrownPotion.java
+@@ -9,6 +_,7 @@
+ import net.minecraft.core.Holder;
+ import net.minecraft.core.component.DataComponents;
+ import net.minecraft.server.level.ServerLevel;
++import net.minecraft.server.level.ServerPlayer;
+ import net.minecraft.tags.BlockTags;
+ import net.minecraft.world.damagesource.DamageSource;
+ import net.minecraft.world.effect.MobEffect;
+@@ -82,51 +_,87 @@
+     @Override
+     protected void onHit(HitResult result) {
+         super.onHit(result);
++        // Paper start - More projectile API
++        this.splash(result);
++    }
++    public void splash(@Nullable HitResult result) {
++        // Paper end - More projectile API
+         if (this.level() instanceof ServerLevel serverLevel) {
+             ItemStack item = this.getItem();
+             PotionContents potionContents = item.getOrDefault(DataComponents.POTION_CONTENTS, PotionContents.EMPTY);
++            boolean showParticles = true; // Paper - Fix potions splash events
+             if (potionContents.is(Potions.WATER)) {
+-                this.applyWater(serverLevel);
+-            } else if (potionContents.hasEffects()) {
++                showParticles = this.applyWater(serverLevel, result); // Paper - Fix potions splash events
++            } else if (true || potionContents.hasEffects()) { // CraftBukkit - Call event even if no effects to apply
+                 if (this.isLingering()) {
+-                    this.makeAreaOfEffectCloud(potionContents);
++                    showParticles = this.makeAreaOfEffectCloud(potionContents, result); // CraftBukkit - Pass MovingObjectPosition // Paper
+                 } else {
+-                    this.applySplash(
+-                        serverLevel, potionContents.getAllEffects(), result.getType() == HitResult.Type.ENTITY ? ((EntityHitResult)result).getEntity() : null
++                    showParticles = this.applySplash(
++                        serverLevel, potionContents.getAllEffects(), result != null && result.getType() == HitResult.Type.ENTITY ? ((EntityHitResult)result).getEntity() : null, result // CraftBukkit - Pass MovingObjectPosition // Paper - More projectile API
+                     );
+                 }
+             }
+ 
++            if (showParticles) { // Paper - Fix potions splash events
+             int i = potionContents.potion().isPresent() && potionContents.potion().get().value().hasInstantEffects() ? 2007 : 2002;
+             serverLevel.levelEvent(i, this.blockPosition(), potionContents.getColor());
+-            this.discard();
++            } // Paper - Fix potions splash events
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
+         }
+     }
+ 
+-    private void applyWater(ServerLevel level) {
++    private static final Predicate<net.minecraft.world.entity.LivingEntity> APPLY_WATER_GET_ENTITIES_PREDICATE = ThrownPotion.WATER_SENSITIVE_OR_ON_FIRE.or(Axolotl.class::isInstance); // Paper - Fix potions splash events
++
++    private boolean applyWater(ServerLevel level, @Nullable HitResult result) { // Paper - Fix potions splash events
+         AABB aabb = this.getBoundingBox().inflate(4.0, 2.0, 4.0);
+ 
+-        for (LivingEntity livingEntity : this.level().getEntitiesOfClass(LivingEntity.class, aabb, WATER_SENSITIVE_OR_ON_FIRE)) {
++        // Paper start - Fix potions splash events
++        java.util.Map<org.bukkit.entity.LivingEntity, Double> affected = new java.util.HashMap<>();
++        java.util.Set<org.bukkit.entity.LivingEntity> rehydrate = new java.util.HashSet<>();
++        java.util.Set<org.bukkit.entity.LivingEntity> extinguish = new java.util.HashSet<>();
++        for (LivingEntity livingEntity : this.level().getEntitiesOfClass(LivingEntity.class, aabb, APPLY_WATER_GET_ENTITIES_PREDICATE)) {
++            if (livingEntity instanceof Axolotl axolotl) {
++                rehydrate.add(((org.bukkit.entity.Axolotl) axolotl.getBukkitEntity()));
++            }
++            // Paper end - Fix potions splash events
+             double d = this.distanceToSqr(livingEntity);
+             if (d < 16.0) {
+                 if (livingEntity.isSensitiveToWater()) {
+-                    livingEntity.hurtServer(level, this.damageSources().indirectMagic(this, this.getOwner()), 1.0F);
++                    affected.put(livingEntity.getBukkitLivingEntity(), 1.0);
++                    // livingEntity.hurtServer(level, this.damageSources().indirectMagic(this, this.getOwner()), 1.0F);
+                 }
+ 
+                 if (livingEntity.isOnFire() && livingEntity.isAlive()) {
+-                    livingEntity.extinguishFire();
++                    extinguish.add(livingEntity.getBukkitLivingEntity());
++                    // livingEntity.extinguishFire();
+                 }
+             }
+         }
+ 
+-        for (Axolotl axolotl : this.level().getEntitiesOfClass(Axolotl.class, aabb)) {
+-            axolotl.rehydrate();
++        io.papermc.paper.event.entity.WaterBottleSplashEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callWaterBottleSplashEvent(
++            this, result, affected, rehydrate, extinguish
++        );
++        if (!event.isCancelled()) {
++            for (org.bukkit.entity.LivingEntity affectedEntity : event.getToDamage()) {
++                ((org.bukkit.craftbukkit.entity.CraftLivingEntity) affectedEntity).getHandle().hurtServer(level, this.damageSources().indirectMagic(this, this.getOwner()), 1.0F);
++            }
++            for (org.bukkit.entity.LivingEntity toExtinguish : event.getToExtinguish()) {
++                ((org.bukkit.craftbukkit.entity.CraftLivingEntity) toExtinguish).getHandle().extinguishFire();
++            }
++            for (org.bukkit.entity.LivingEntity toRehydrate : event.getToRehydrate()) {
++                if (((org.bukkit.craftbukkit.entity.CraftLivingEntity) toRehydrate).getHandle() instanceof Axolotl axolotl) {
++                    axolotl.rehydrate();
++                }
++            }
++            // Paper end - Fix potions splash events
+         }
++        return !event.isCancelled(); // Paper - Fix potions splash events
+     }
+ 
+-    private void applySplash(ServerLevel level, Iterable<MobEffectInstance> effects, @Nullable Entity entity) {
++    private boolean applySplash(ServerLevel level, Iterable<MobEffectInstance> effects, @Nullable Entity entity, @Nullable HitResult result) { // CraftBukkit - Pass MovingObjectPosition // Paper - Fix potions splash events & More projectile API
+         AABB aabb = this.getBoundingBox().inflate(4.0, 2.0, 4.0);
+         List<LivingEntity> entitiesOfClass = level.getEntitiesOfClass(LivingEntity.class, aabb);
++        java.util.Map<org.bukkit.entity.LivingEntity, Double> affected = new java.util.HashMap<>(); // CraftBukkit
+         if (!entitiesOfClass.isEmpty()) {
+             Entity effectSource = this.getEffectSource();
+ 
+@@ -135,33 +_,57 @@
+                     double d = this.distanceToSqr(livingEntity);
+                     if (d < 16.0) {
+                         double d1;
++                        // Paper - diff on change, used when calling the splash event for water splash potions
+                         if (livingEntity == entity) {
+                             d1 = 1.0;
+                         } else {
+                             d1 = 1.0 - Math.sqrt(d) / 4.0;
+                         }
+ 
+-                        for (MobEffectInstance mobEffectInstance : effects) {
+-                            Holder<MobEffect> effect = mobEffectInstance.getEffect();
+-                            if (effect.value().isInstantenous()) {
+-                                effect.value().applyInstantenousEffect(level, this, this.getOwner(), livingEntity, mobEffectInstance.getAmplifier(), d1);
+-                            } else {
+-                                int i = mobEffectInstance.mapDuration(i1 -> (int)(d1 * i1 + 0.5));
+-                                MobEffectInstance mobEffectInstance1 = new MobEffectInstance(
+-                                    effect, i, mobEffectInstance.getAmplifier(), mobEffectInstance.isAmbient(), mobEffectInstance.isVisible()
+-                                );
+-                                if (!mobEffectInstance1.endsWithin(20)) {
+-                                    livingEntity.addEffect(mobEffectInstance1, effectSource);
+-                                }
+-                            }
+-                        }
+-                    }
+-                }
+-            }
+-        }
++                        affected.put(livingEntity.getBukkitLivingEntity(), d1);
++                    }
++                }
++            }
++        }
++        org.bukkit.event.entity.PotionSplashEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPotionSplashEvent(this, result, affected);
++        if (!event.isCancelled() && entitiesOfClass != null && !entitiesOfClass.isEmpty()) { // do not process effects if there are no effects to process
++            Entity effectSource = this.getEffectSource();
++            for (org.bukkit.entity.LivingEntity victim : event.getAffectedEntities()) {
++                if (!(victim instanceof org.bukkit.craftbukkit.entity.CraftLivingEntity craftLivingEntity)) {
++                    continue;
++                }
++                net.minecraft.world.entity.LivingEntity livingEntity = craftLivingEntity.getHandle();
++                double d1 = event.getIntensity(victim);
++                // CraftBukkit end
++                for (MobEffectInstance mobEffectInstance : effects) {
++                    Holder<MobEffect> effect = mobEffectInstance.getEffect();
++                    // CraftBukkit start - Abide by PVP settings - for players only!
++                    if (!this.level().pvpMode && this.getOwner() instanceof ServerPlayer && livingEntity instanceof ServerPlayer && livingEntity != this.getOwner()) {
++                        MobEffect mobEffect = effect.value();
++                        if (mobEffect == net.minecraft.world.effect.MobEffects.MOVEMENT_SLOWDOWN || mobEffect == net.minecraft.world.effect.MobEffects.DIG_SLOWDOWN || mobEffect == net.minecraft.world.effect.MobEffects.HARM || mobEffect == net.minecraft.world.effect.MobEffects.BLINDNESS
++                                || mobEffect == net.minecraft.world.effect.MobEffects.HUNGER || mobEffect == net.minecraft.world.effect.MobEffects.WEAKNESS || mobEffect == net.minecraft.world.effect.MobEffects.POISON) {
++                            continue;
++                        }
++                    }
++                    // CraftBukkit end
++                    if (effect.value().isInstantenous()) {
++                        effect.value().applyInstantenousEffect(level, this, this.getOwner(), livingEntity, mobEffectInstance.getAmplifier(), d1);
++                    } else {
++                        int i = mobEffectInstance.mapDuration(i1 -> (int)(d1 * i1 + 0.5));
++                        MobEffectInstance mobEffectInstance1 = new MobEffectInstance(
++                            effect, i, mobEffectInstance.getAmplifier(), mobEffectInstance.isAmbient(), mobEffectInstance.isVisible()
++                        );
++                        if (!mobEffectInstance1.endsWithin(20)) {
++                            livingEntity.addEffect(mobEffectInstance1, effectSource, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.POTION_SPLASH); // CraftBukkit
++                        }
++                    }
++                }
++            }
++        }
++        return !event.isCancelled(); // Paper - Fix potions splash events
+     }
+ 
+-    private void makeAreaOfEffectCloud(PotionContents potionContents) {
++    private boolean makeAreaOfEffectCloud(PotionContents potionContents, @Nullable HitResult result) { // CraftBukkit - Pass MovingObjectPosition // Paper - More projectile API
+         AreaEffectCloud areaEffectCloud = new AreaEffectCloud(this.level(), this.getX(), this.getY(), this.getZ());
+         if (this.getOwner() instanceof LivingEntity livingEntity) {
+             areaEffectCloud.setOwner(livingEntity);
+@@ -172,7 +_,16 @@
+         areaEffectCloud.setWaitTime(10);
+         areaEffectCloud.setRadiusPerTick(-areaEffectCloud.getRadius() / areaEffectCloud.getDuration());
+         areaEffectCloud.setPotionContents(potionContents);
+-        this.level().addFreshEntity(areaEffectCloud);
++        boolean noEffects = potionContents.hasEffects(); // Paper - Fix potions splash events
++        // CraftBukkit start
++        org.bukkit.event.entity.LingeringPotionSplashEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callLingeringPotionSplashEvent(this, result, areaEffectCloud);
++        if (!(event.isCancelled() || areaEffectCloud.isRemoved() || (!event.allowsEmptyCreation() && (noEffects && !areaEffectCloud.potionContents.hasEffects())))) { // Paper - don't spawn area effect cloud if the effects were empty and not changed during the event handling
++            this.level().addFreshEntity(areaEffectCloud);
++        } else {
++            areaEffectCloud.discard(null); // CraftBukkit - add Bukkit remove cause
++        }
++        // CraftBukkit end
++        return !event.isCancelled(); // Paper - Fix potions splash events
+     }
+ 
+     public boolean isLingering() {
+@@ -182,13 +_,25 @@
+     private void dowseFire(BlockPos pos) {
+         BlockState blockState = this.level().getBlockState(pos);
+         if (blockState.is(BlockTags.FIRE)) {
+-            this.level().destroyBlock(pos, false, this);
++            // CraftBukkit start
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this, pos, blockState.getFluidState().createLegacyBlock())) { // Paper - fix wrong block state
++                this.level().destroyBlock(pos, false, this);
++            }
++            // CraftBukkit end
+         } else if (AbstractCandleBlock.isLit(blockState)) {
+-            AbstractCandleBlock.extinguish(null, blockState, this.level(), pos);
++            // CraftBukkit start
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this, pos, blockState.setValue(AbstractCandleBlock.LIT, false))) {
++                AbstractCandleBlock.extinguish(null, blockState, this.level(), pos);
++            }
++            // CraftBukkit end
+         } else if (CampfireBlock.isLitCampfire(blockState)) {
+-            this.level().levelEvent(null, 1009, pos, 0);
+-            CampfireBlock.dowse(this.getOwner(), this.level(), pos, blockState);
+-            this.level().setBlockAndUpdate(pos, blockState.setValue(CampfireBlock.LIT, Boolean.valueOf(false)));
++            // CraftBukkit start
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this, pos, blockState.setValue(CampfireBlock.LIT, false))) {
++                this.level().levelEvent(null, 1009, pos, 0);
++                CampfireBlock.dowse(this.getOwner(), this.level(), pos, blockState);
++                this.level().setBlockAndUpdate(pos, blockState.setValue(CampfireBlock.LIT, false));
++            }
++            // CraftBukkit end
+         }
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/projectile/ThrownTrident.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/projectile/ThrownTrident.java.patch
new file mode 100644
index 0000000000..dad0da0b92
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/projectile/ThrownTrident.java.patch
@@ -0,0 +1,65 @@
+--- a/net/minecraft/world/entity/projectile/ThrownTrident.java
++++ b/net/minecraft/world/entity/projectile/ThrownTrident.java
+@@ -32,16 +_,19 @@
+ 
+     public ThrownTrident(EntityType<? extends ThrownTrident> entityType, Level level) {
+         super(entityType, level);
++        this.setBaseDamage(net.minecraft.world.item.TridentItem.BASE_DAMAGE); // Paper - Allow trident custom damage
+     }
+ 
+     public ThrownTrident(Level level, LivingEntity shooter, ItemStack pickupItemStack) {
+         super(EntityType.TRIDENT, shooter, level, pickupItemStack, null);
++        this.setBaseDamage(net.minecraft.world.item.TridentItem.BASE_DAMAGE); // Paper - Allow trident custom damage
+         this.entityData.set(ID_LOYALTY, this.getLoyaltyFromItem(pickupItemStack));
+         this.entityData.set(ID_FOIL, pickupItemStack.hasFoil());
+     }
+ 
+     public ThrownTrident(Level level, double x, double y, double z, ItemStack pickupItemStack) {
+         super(EntityType.TRIDENT, x, y, z, level, pickupItemStack, pickupItemStack);
++        this.setBaseDamage(net.minecraft.world.item.TridentItem.BASE_DAMAGE); // Paper - Allow trident custom damage
+         this.entityData.set(ID_LOYALTY, this.getLoyaltyFromItem(pickupItemStack));
+         this.entityData.set(ID_FOIL, pickupItemStack.hasFoil());
+     }
+@@ -67,10 +_,10 @@
+                     this.spawnAtLocation(serverLevel, this.getPickupItem(), 0.1F);
+                 }
+ 
+-                this.discard();
++                this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DROP); // CraftBukkit - add Bukkit remove cause
+             } else {
+                 if (!(owner instanceof Player) && this.position().distanceTo(owner.getEyePosition()) < owner.getBbWidth() + 1.0) {
+-                    this.discard();
++                    this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+                     return;
+                 }
+ 
+@@ -99,6 +_,20 @@
+         return this.entityData.get(ID_FOIL);
+     }
+ 
++    // Paper start
++    public void setFoil(boolean foil) {
++        this.entityData.set(ThrownTrident.ID_FOIL, foil);
++    }
++
++    public int getLoyalty() {
++        return this.entityData.get(ThrownTrident.ID_LOYALTY);
++    }
++
++    public void setLoyalty(byte loyalty) {
++        this.entityData.set(ThrownTrident.ID_LOYALTY, loyalty);
++    }
++    // Paper end
++
+     @Nullable
+     @Override
+     protected EntityHitResult findHitEntity(Vec3 startVec, Vec3 endVec) {
+@@ -108,7 +_,7 @@
+     @Override
+     protected void onHitEntity(EntityHitResult result) {
+         Entity entity = result.getEntity();
+-        float f = 8.0F;
++        float f = (float) this.getBaseDamage(); // Paper - Allow trident custom damage
+         Entity owner = this.getOwner();
+         DamageSource damageSource = this.damageSources().trident(this, (Entity)(owner == null ? this : owner));
+         if (this.level() instanceof ServerLevel serverLevel) {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/projectile/WitherSkull.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/projectile/WitherSkull.java.patch
new file mode 100644
index 0000000000..d323b77c2b
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/projectile/WitherSkull.java.patch
@@ -0,0 +1,43 @@
+--- a/net/minecraft/world/entity/projectile/WitherSkull.java
++++ b/net/minecraft/world/entity/projectile/WitherSkull.java
+@@ -65,11 +_,11 @@
+                     if (var8.isAlive()) {
+                         EnchantmentHelper.doPostAttackEffects(serverLevel, var8, damageSource);
+                     } else {
+-                        livingEntity.heal(5.0F);
++                        livingEntity.heal(5.0F, org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.WITHER); // CraftBukkit
+                     }
+                 }
+             } else {
+-                flag = var8.hurtServer(serverLevel, this.damageSources().magic(), 5.0F);
++                flag = var8.hurtServer(serverLevel, this.damageSources().magic().customEventDamager(this), 5.0F); // Paper - Fire EntityDamageByEntityEvent for unowned wither skulls // Paper - fix DamageSource API
+             }
+ 
+             if (flag && var8 instanceof LivingEntity livingEntityx) {
+@@ -81,7 +_,7 @@
+                 }
+ 
+                 if (i > 0) {
+-                    livingEntityx.addEffect(new MobEffectInstance(MobEffects.WITHER, 20 * i, 1), this.getEffectSource());
++                    livingEntityx.addEffect(new MobEffectInstance(MobEffects.WITHER, 20 * i, 1), this.getEffectSource(), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ATTACK); // CraftBukkit
+                 }
+             }
+         }
+@@ -91,8 +_,15 @@
+     protected void onHit(HitResult result) {
+         super.onHit(result);
+         if (!this.level().isClientSide) {
+-            this.level().explode(this, this.getX(), this.getY(), this.getZ(), 1.0F, false, Level.ExplosionInteraction.MOB);
+-            this.discard();
++            // CraftBukkit start
++            org.bukkit.event.entity.ExplosionPrimeEvent event = new org.bukkit.event.entity.ExplosionPrimeEvent(this.getBukkitEntity(), 1.0F, false);
++            this.level().getCraftServer().getPluginManager().callEvent(event);
++
++            if (!event.isCancelled()) {
++                this.level().explode(this, this.getX(), this.getY(), this.getZ(), event.getRadius(), event.getFire(), Level.ExplosionInteraction.MOB);
++            }
++            // CraftBukkit end
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
+         }
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/projectile/windcharge/AbstractWindCharge.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/projectile/windcharge/AbstractWindCharge.java.patch
new file mode 100644
index 0000000000..a764fae4a2
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/projectile/windcharge/AbstractWindCharge.java.patch
@@ -0,0 +1,38 @@
+--- a/net/minecraft/world/entity/projectile/windcharge/AbstractWindCharge.java
++++ b/net/minecraft/world/entity/projectile/windcharge/AbstractWindCharge.java
+@@ -85,7 +_,7 @@
+     }
+ 
+     @Override
+-    public void push(double x, double y, double z) {
++    public void push(double x, double y, double z, @Nullable Entity pushingEntity) { // Paper - Add EntityKnockbackByEntityEvent and EntityPushedByEntityAttackEvent
+     }
+ 
+     public abstract void explode(Vec3 pos);
+@@ -98,7 +_,7 @@
+             Vec3 vec3 = Vec3.atLowerCornerOf(unitVec3i).multiply(0.25, 0.25, 0.25);
+             Vec3 vec31 = result.getLocation().add(vec3);
+             this.explode(vec31);
+-            this.discard();
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
+         }
+     }
+ 
+@@ -106,7 +_,7 @@
+     protected void onHit(HitResult result) {
+         super.onHit(result);
+         if (!this.level().isClientSide) {
+-            this.discard();
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
+         }
+     }
+ 
+@@ -140,7 +_,7 @@
+     public void tick() {
+         if (!this.level().isClientSide && this.getBlockY() > this.level().getMaxY() + 30) {
+             this.explode(this.position());
+-            this.discard();
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.OUT_OF_WORLD); // CraftBukkit - add Bukkit remove cause
+         } else {
+             super.tick();
+         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/raid/Raid.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/raid/Raid.java.patch
similarity index 61%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/raid/Raid.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/raid/Raid.java.patch
index 789e719f42..f5c2c2d983 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/raid/Raid.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/raid/Raid.java.patch
@@ -1,30 +1,30 @@
 --- a/net/minecraft/world/entity/raid/Raid.java
 +++ b/net/minecraft/world/entity/raid/Raid.java
-@@ -107,6 +107,11 @@
+@@ -104,6 +_,11 @@
      private Raid.RaidStatus status;
      private int celebrationTicks;
-     private Optional<BlockPos> waveSpawnPos;
+     private Optional<BlockPos> waveSpawnPos = Optional.empty();
 +    // Paper start
 +    private static final String PDC_NBT_KEY = "BukkitValues";
 +    private static final org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry PDC_TYPE_REGISTRY = new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry();
 +    public final org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer persistentDataContainer = new org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer(PDC_TYPE_REGISTRY);
 +    // Paper end
  
-     public Raid(int id, ServerLevel world, BlockPos pos) {
-         this.raidEvent = new ServerBossEvent(Raid.RAID_NAME_COMPONENT, BossEvent.BossBarColor.RED, BossEvent.BossBarOverlay.NOTCHED_10);
-@@ -150,6 +155,11 @@
-                 this.heroesOfTheVillage.add(NbtUtils.loadUUID(nbtbase));
+     public Raid(int id, ServerLevel level, BlockPos center) {
+         this.id = id;
+@@ -136,6 +_,11 @@
+                 this.heroesOfTheVillage.add(NbtUtils.loadUUID(tag));
              }
          }
 +        // Paper start
-+        if (nbt.contains(PDC_NBT_KEY, net.minecraft.nbt.Tag.TAG_COMPOUND)) {
-+            this.persistentDataContainer.putAll(nbt.getCompound(PDC_NBT_KEY));
++        if (compound.contains(PDC_NBT_KEY, net.minecraft.nbt.Tag.TAG_COMPOUND)) {
++            this.persistentDataContainer.putAll(compound.getCompound(PDC_NBT_KEY));
 +        }
 +        // Paper end
- 
      }
  
-@@ -177,6 +187,12 @@
+     public boolean isOver() {
+@@ -162,6 +_,12 @@
          return this.status == Raid.RaidStatus.LOSS;
      }
  
@@ -37,15 +37,15 @@
      public float getTotalHealth() {
          return this.totalHealth;
      }
-@@ -281,6 +297,7 @@
- 
+@@ -252,6 +_,7 @@
+                 boolean flag = this.active;
                  this.active = this.level.hasChunkAt(this.center);
                  if (this.level.getDifficulty() == Difficulty.PEACEFUL) {
 +                    org.bukkit.craftbukkit.event.CraftEventFactory.callRaidStopEvent(this, org.bukkit.event.raid.RaidStopEvent.Reason.PEACE); // CraftBukkit
                      this.stop();
                      return;
                  }
-@@ -300,13 +317,16 @@
+@@ -271,13 +_,16 @@
                  if (!this.level.isVillage(this.center)) {
                      if (this.groupsSpawned > 0) {
                          this.status = Raid.RaidStatus.LOSS;
@@ -56,33 +56,33 @@
                      }
                  }
  
-                 ++this.ticksActive;
+                 this.ticksActive++;
                  if (this.ticksActive >= 48000L) {
 +                    org.bukkit.craftbukkit.event.CraftEventFactory.callRaidStopEvent(this, org.bukkit.event.raid.RaidStopEvent.Reason.TIMEOUT); // CraftBukkit
                      this.stop();
                      return;
                  }
-@@ -374,6 +394,7 @@
+@@ -346,6 +_,7 @@
                      }
  
-                     if (j > 5) {
-+                        org.bukkit.craftbukkit.event.CraftEventFactory.callRaidStopEvent(this, org.bukkit.event.raid.RaidStopEvent.Reason.UNSPAWNABLE);  // CraftBukkit
+                     if (i > 5) {
++                        org.bukkit.craftbukkit.event.CraftEventFactory.callRaidStopEvent(this, org.bukkit.event.raid.RaidStopEvent.Reason.UNSPAWNABLE); // CraftBukkit
                          this.stop();
                          break;
                      }
-@@ -386,6 +407,7 @@
+@@ -357,6 +_,7 @@
+                     } else {
                          this.status = Raid.RaidStatus.VICTORY;
-                         Iterator iterator = this.heroesOfTheVillage.iterator();
  
 +                        List<org.bukkit.entity.Player> winners = new java.util.ArrayList<>(); // CraftBukkit
-                         while (iterator.hasNext()) {
-                             UUID uuid = (UUID) iterator.next();
+                         for (UUID uuid : this.heroesOfTheVillage) {
                              Entity entity = this.level.getEntity(uuid);
-@@ -400,10 +422,12 @@
- 
-                                         entityplayer.awardStat(Stats.RAID_WIN);
-                                         CriteriaTriggers.RAID_WIN.trigger(entityplayer);
-+                                        winners.add(entityplayer.getBukkitEntity()); // CraftBukkit
+                             if (entity instanceof LivingEntity) {
+@@ -368,10 +_,12 @@
+                                     if (livingEntity instanceof ServerPlayer serverPlayer) {
+                                         serverPlayer.awardStat(Stats.RAID_WIN);
+                                         CriteriaTriggers.RAID_WIN.trigger(serverPlayer);
++                                        winners.add(serverPlayer.getBukkitEntity()); // CraftBukkit
                                      }
                                  }
                              }
@@ -91,84 +91,84 @@
                      }
                  }
  
-@@ -411,6 +435,7 @@
+@@ -379,6 +_,7 @@
              } else if (this.isOver()) {
-                 ++this.celebrationTicks;
+                 this.celebrationTicks++;
                  if (this.celebrationTicks >= 600) {
 +                    org.bukkit.craftbukkit.event.CraftEventFactory.callRaidStopEvent(this, org.bukkit.event.raid.RaidStopEvent.Reason.FINISHED); // CraftBukkit
                      this.stop();
                      return;
                  }
-@@ -544,6 +569,10 @@
-         int j = araid_wave.length;
-         int k = 0;
+@@ -491,6 +_,10 @@
+         DifficultyInstance currentDifficultyAt = this.level.getCurrentDifficultyAt(pos);
+         boolean shouldSpawnBonusGroup = this.shouldSpawnBonusGroup();
  
 +        // CraftBukkit start
 +        Raider leader = null;
 +        List<Raider> raiders = new java.util.ArrayList<>();
 +        // CraftBukkit end
-         while (k < j) {
-             Raid.RaiderType raid_wave = araid_wave[k];
-             int l = this.getDefaultNumSpawns(raid_wave, i, flag1) + this.getPotentialBonusSpawns(raid_wave, this.random, i, difficultydamagescaler, flag1);
-@@ -559,9 +588,11 @@
-                             entityraider.setPatrolLeader(true);
-                             this.setLeader(i, entityraider);
-                             flag = true;
-+                            leader = entityraider; // CraftBukkit
-                         }
+         for (Raid.RaiderType raiderType : Raid.RaiderType.VALUES) {
+             int i1 = this.getDefaultNumSpawns(raiderType, i, shouldSpawnBonusGroup)
+                 + this.getPotentialBonusSpawns(raiderType, this.random, i, currentDifficultyAt, shouldSpawnBonusGroup);
+@@ -506,9 +_,11 @@
+                     raider.setPatrolLeader(true);
+                     this.setLeader(i, raider);
+                     flag = true;
++                    leader = raider; // CraftBukkit
+                 }
  
-                         this.joinRaid(i, entityraider, pos, false);
-+                        raiders.add(entityraider); // CraftBukkit
-                         if (raid_wave.entityType == EntityType.RAVAGER) {
-                             Raider entityraider1 = null;
- 
-@@ -580,6 +611,7 @@
-                                 this.joinRaid(i, entityraider1, pos, false);
-                                 entityraider1.moveTo(pos, 0.0F, 0.0F);
-                                 entityraider1.startRiding(entityraider);
-+                                raiders.add(entityraider); // CraftBukkit
-                             }
-                         }
- 
-@@ -597,6 +629,7 @@
-         ++this.groupsSpawned;
+                 this.joinRaid(i, raider, pos, false);
++                raiders.add(raider); // CraftBukkit
+                 if (raiderType.entityType == EntityType.RAVAGER) {
+                     Raider raider1 = null;
+                     if (i == this.getNumGroups(Difficulty.NORMAL)) {
+@@ -526,6 +_,7 @@
+                         this.joinRaid(i, raider1, pos, false);
+                         raider1.moveTo(pos, 0.0F, 0.0F);
+                         raider1.startRiding(raider);
++                        raiders.add(raider); // CraftBukkit
+                     }
+                 }
+             }
+@@ -535,6 +_,7 @@
+         this.groupsSpawned++;
          this.updateBossbar();
          this.setDirty();
 +        org.bukkit.craftbukkit.event.CraftEventFactory.callRaidSpawnWaveEvent(this, leader, raiders); // CraftBukkit
      }
  
-     public void joinRaid(int wave, Raider raider, @Nullable BlockPos pos, boolean existing) {
-@@ -612,7 +645,7 @@
-                 raider.finalizeSpawn(this.level, this.level.getCurrentDifficultyAt(pos), EntitySpawnReason.EVENT, (SpawnGroupData) null);
+     public void joinRaid(int wave, Raider raider, @Nullable BlockPos pos, boolean isRecruited) {
+@@ -549,7 +_,7 @@
+                 raider.finalizeSpawn(this.level, this.level.getCurrentDifficultyAt(pos), EntitySpawnReason.EVENT, null);
                  raider.applyRaidBuffs(this.level, wave, false);
                  raider.setOnGround(true);
 -                this.level.addFreshEntityWithPassengers(raider);
 +                this.level.addFreshEntityWithPassengers(raider, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.RAID); // CraftBukkit
              }
          }
- 
-@@ -839,6 +872,11 @@
+     }
+@@ -770,6 +_,11 @@
          }
  
-         nbt.put("HeroesOfTheVillage", nbttaglist);
+         compound.put("HeroesOfTheVillage", listTag);
 +        // Paper start
 +        if (!this.persistentDataContainer.isEmpty()) {
-+            nbt.put(PDC_NBT_KEY, this.persistentDataContainer.toTagCompound());
++            compound.put(PDC_NBT_KEY, this.persistentDataContainer.toTagCompound());
 +        }
 +        // Paper end
-         return nbt;
+         return compound;
      }
  
-@@ -865,6 +903,12 @@
-         this.heroesOfTheVillage.add(entity.getUUID());
+@@ -802,6 +_,12 @@
+     public void addHeroOfTheVillage(Entity player) {
+         this.heroesOfTheVillage.add(player.getUUID());
      }
- 
++
 +    // CraftBukkit start - a method to get all raiders
 +    public java.util.Collection<Raider> getRaiders() {
 +        return this.groupRaiderMap.values().stream().flatMap(Set::stream).collect(java.util.stream.Collectors.toSet());
 +    }
 +    // CraftBukkit end
-+
-     private static enum RaidStatus {
  
-         ONGOING, VICTORY, LOSS, STOPPED;
+     static enum RaidStatus {
+         ONGOING,
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/raid/Raider.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/raid/Raider.java.patch
new file mode 100644
index 0000000000..59f462b6cc
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/raid/Raider.java.patch
@@ -0,0 +1,54 @@
+--- a/net/minecraft/world/entity/raid/Raider.java
++++ b/net/minecraft/world/entity/raid/Raider.java
+@@ -212,17 +_,24 @@
+         if (this.hasActiveRaid()
+             && !flag
+             && ItemStack.matches(item, Raid.getOminousBannerInstance(this.registryAccess().lookupOrThrow(Registries.BANNER_PATTERN)))) {
++            // Paper start - EntityPickupItemEvent fixes
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPickupItemEvent(this, entity, 0, false).isCancelled()) {
++                return;
++            }
++            // Paper end - EntityPickupItemEvent fixes
+             EquipmentSlot equipmentSlot = EquipmentSlot.HEAD;
+             ItemStack itemBySlot = this.getItemBySlot(equipmentSlot);
+             double d = this.getEquipmentDropChance(equipmentSlot);
+             if (!itemBySlot.isEmpty() && Math.max(this.random.nextFloat() - 0.1F, 0.0F) < d) {
++                this.forceDrops = true; // Paper - Add missing forceDrop toggles
+                 this.spawnAtLocation(level, itemBySlot);
++                this.forceDrops = false; // Paper - Add missing forceDrop toggles
+             }
+ 
+             this.onItemPickup(entity);
+             this.setItemSlot(equipmentSlot, item);
+             this.take(entity, item.getCount());
+-            entity.discard();
++            entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
+             this.getCurrentRaid().setLeader(this.getWave(), this);
+             this.setPatrolLeader(true);
+         } else {
+@@ -296,7 +_,7 @@
+ 
+             for (Raider raider : getServerLevel(this.mob)
+                 .getNearbyEntities(Raider.class, this.shoutTargeting, this.mob, this.mob.getBoundingBox().inflate(8.0, 8.0, 8.0))) {
+-                raider.setTarget(this.mob.getTarget());
++                raider.setTarget(this.mob.getTarget(), org.bukkit.event.entity.EntityTargetEvent.TargetReason.FOLLOW_LEADER, true); // CraftBukkit
+             }
+         }
+ 
+@@ -307,7 +_,7 @@
+             if (target != null) {
+                 for (Raider raider : getServerLevel(this.mob)
+                     .getNearbyEntities(Raider.class, this.shoutTargeting, this.mob, this.mob.getBoundingBox().inflate(8.0, 8.0, 8.0))) {
+-                    raider.setTarget(target);
++                    raider.setTarget(target, org.bukkit.event.entity.EntityTargetEvent.TargetReason.FOLLOW_LEADER, true); // CraftBukkit
+                     raider.setAggressive(true);
+                 }
+ 
+@@ -392,6 +_,7 @@
+         }
+ 
+         private boolean cannotPickUpBanner() {
++            if (!getServerLevel(this.mob).getGameRules().getBoolean(net.minecraft.world.level.GameRules.RULE_MOBGRIEFING) || !this.mob.canPickUpLoot()) return false; // Paper - respect game and entity rules for picking up items
+             if (!this.mob.hasActiveRaid()) {
+                 return true;
+             } else if (this.mob.getCurrentRaid().isOver()) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/raid/Raids.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/raid/Raids.java.patch
similarity index 95%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/raid/Raids.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/raid/Raids.java.patch
index b787b60fa1..89f05b3b2d 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/raid/Raids.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/raid/Raids.java.patch
@@ -1,16 +1,17 @@
 --- a/net/minecraft/world/entity/raid/Raids.java
 +++ b/net/minecraft/world/entity/raid/Raids.java
-@@ -115,11 +115,23 @@
- 
-                 Raid raid = this.getOrCreateRaid(player.serverLevel(), blockposition2);
+@@ -112,11 +_,23 @@
+                 }
  
+                 Raid raid = this.getOrCreateRaid(player.serverLevel(), blockPos);
 +                /* CraftBukkit - moved down
                  if (!raid.isStarted() && !this.raidMap.containsKey(raid.getId())) {
                      this.raidMap.put(raid.getId(), raid);
                  }
-+                */
- 
+-
 -                if (!raid.isStarted() || raid.getRaidOmenLevel() < raid.getMaxRaidOmenLevel()) {
++                */
++
 +                if (!raid.isStarted() || (raid.isInProgress() && raid.getRaidOmenLevel() < raid.getMaxRaidOmenLevel())) { // CraftBukkit - fixed a bug with raid: players could add up Bad Omen level even when the raid had finished
 +                    // CraftBukkit start
 +                    if (!org.bukkit.craftbukkit.event.CraftEventFactory.callRaidTriggerEvent(raid, player)) {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/vehicle/AbstractBoat.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/vehicle/AbstractBoat.java.patch
new file mode 100644
index 0000000000..3d61d83b48
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/vehicle/AbstractBoat.java.patch
@@ -0,0 +1,106 @@
+--- a/net/minecraft/world/entity/vehicle/AbstractBoat.java
++++ b/net/minecraft/world/entity/vehicle/AbstractBoat.java
+@@ -83,6 +_,15 @@
+     private Leashable.LeashData leashData;
+     private final Supplier<Item> dropItem;
+ 
++    // CraftBukkit start
++    // PAIL: Some of these haven't worked since a few updates, and since 1.9 they are less and less applicable.
++    public double maxSpeed = 0.4D;
++    public double occupiedDeceleration = 0.2D;
++    public double unoccupiedDeceleration = -1;
++    public boolean landBoats = false;
++    private org.bukkit.Location lastLocation;
++    // CraftBukkit end
++
+     public AbstractBoat(EntityType<? extends AbstractBoat> entityType, Level level, Supplier<Item> dropItem) {
+         super(entityType, level);
+         this.dropItem = dropItem;
+@@ -124,7 +_,7 @@
+     }
+ 
+     @Override
+-    public boolean isPushable() {
++    public boolean isCollidable(boolean ignoreClimbing) { // Paper - Climbing should not bypass cramming gamerule
+         return true;
+     }
+ 
+@@ -177,11 +_,30 @@
+ 
+     @Override
+     public void push(Entity entity) {
++        if (!this.level().paperConfig().collisions.allowVehicleCollisions && this.level().paperConfig().collisions.onlyPlayersCollide && !(entity instanceof Player)) return; // Paper - Collision option for requiring a player participant
+         if (entity instanceof AbstractBoat) {
+             if (entity.getBoundingBox().minY < this.getBoundingBox().maxY) {
++                // CraftBukkit start
++                if (!this.isPassengerOfSameVehicle(entity)) {
++                    org.bukkit.event.vehicle.VehicleEntityCollisionEvent event = new org.bukkit.event.vehicle.VehicleEntityCollisionEvent(
++                        (org.bukkit.entity.Vehicle) this.getBukkitEntity(),
++                        entity.getBukkitEntity()
++                    );
++                    if (!event.callEvent()) return;
++                }
++                // CraftBukkit end
+                 super.push(entity);
+             }
+         } else if (entity.getBoundingBox().minY <= this.getBoundingBox().minY) {
++            // CraftBukkit start
++            if (!this.isPassengerOfSameVehicle(entity)) {
++                org.bukkit.event.vehicle.VehicleEntityCollisionEvent event = new org.bukkit.event.vehicle.VehicleEntityCollisionEvent(
++                    (org.bukkit.entity.Vehicle) this.getBukkitEntity(),
++                    entity.getBukkitEntity()
++                );
++                if (!event.callEvent()) return;
++            }
++            // CraftBukkit end
+             super.push(entity);
+         }
+     }
+@@ -283,6 +_,18 @@
+             this.setDeltaMovement(Vec3.ZERO);
+         }
+ 
++        // CraftBukkit start
++        org.bukkit.Location to = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(this.position(), this.level().getWorld(), this.getYRot(), this.getXRot());
++        org.bukkit.entity.Vehicle vehicle = (org.bukkit.entity.Vehicle) this.getBukkitEntity();
++
++        new org.bukkit.event.vehicle.VehicleUpdateEvent(vehicle).callEvent();
++
++        if (this.lastLocation != null && !this.lastLocation.equals(to)) {
++            org.bukkit.event.vehicle.VehicleMoveEvent event = new org.bukkit.event.vehicle.VehicleMoveEvent(vehicle, this.lastLocation, to);
++            event.callEvent();
++        }
++        this.lastLocation = vehicle.getLocation();
++        // CraftBukkit end
+         this.applyEffectsFromBlocks();
+         this.applyEffectsFromBlocks();
+         this.tickBubbleColumn();
+@@ -598,7 +_,7 @@
+             this.waterLevel = this.getY(1.0);
+             double d2 = this.getWaterLevelAbove() - this.getBbHeight() + 0.101;
+             if (this.level().noCollision(this, this.getBoundingBox().move(0.0, d2 - this.getY(), 0.0))) {
+-                this.setPos(this.getX(), d2, this.getZ());
++                this.move(MoverType.SELF, new Vec3(0.0D, d2 - this.getY(), 0.0D)); // Paper - Fix some exploit with boats // TODO Still needed?
+                 this.setDeltaMovement(this.getDeltaMovement().multiply(1.0, 0.0, 1.0));
+                 this.lastYd = 0.0;
+             }
+@@ -762,11 +_,18 @@
+ 
+     @Override
+     public void remove(Entity.RemovalReason reason) {
++        // CraftBukkit start - add Bukkit remove cause
++        this.remove(reason, null);
++    }
++
++    @Override
++    public void remove(Entity.RemovalReason reason, org.bukkit.event.entity.EntityRemoveEvent.Cause eventCause) {
++        // CraftBukkit end
+         if (!this.level().isClientSide && reason.shouldDestroy() && this.isLeashed()) {
+             this.dropLeash();
+         }
+ 
+-        super.remove(reason);
++        super.remove(reason, eventCause); // CraftBukkit - add Bukkit remove cause
+     }
+ 
+     @Override
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/vehicle/AbstractChestBoat.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/vehicle/AbstractChestBoat.java.patch
new file mode 100644
index 0000000000..9fd4787f99
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/vehicle/AbstractChestBoat.java.patch
@@ -0,0 +1,101 @@
+--- a/net/minecraft/world/entity/vehicle/AbstractChestBoat.java
++++ b/net/minecraft/world/entity/vehicle/AbstractChestBoat.java
+@@ -66,11 +_,18 @@
+ 
+     @Override
+     public void remove(Entity.RemovalReason reason) {
++        // CraftBukkit start - add Bukkit remove cause
++        this.remove(reason, null);
++    }
++
++    @Override
++    public void remove(Entity.RemovalReason reason, org.bukkit.event.entity.EntityRemoveEvent.Cause cause) {
++        // CraftBukkit end
+         if (!this.level().isClientSide && reason.shouldDestroy()) {
+             Containers.dropContents(this.level(), this, this);
+         }
+ 
+-        super.remove(reason);
++        super.remove(reason, cause); // CraftBukkit - add Bukkit remove cause
+     }
+ 
+     @Override
+@@ -97,8 +_,8 @@
+ 
+     @Override
+     public void openCustomInventoryScreen(Player player) {
+-        player.openMenu(this);
+-        if (player.level() instanceof ServerLevel serverLevel) {
++        // Paper - fix inventory open cancel - moved into below if
++        if (player.level() instanceof ServerLevel serverLevel && player.openMenu(this).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
+             this.gameEvent(GameEvent.CONTAINER_OPEN, player);
+             PiglinAi.angerNearbyPiglins(serverLevel, player, true);
+         }
+@@ -151,7 +_,7 @@
+     @Nullable
+     @Override
+     public AbstractContainerMenu createMenu(int containerId, Inventory playerInventory, Player player) {
+-        if (this.lootTable != null && player.isSpectator()) {
++        if (this.lootTable != null && player.isSpectator()) { // Paper - LootTable API (TODO spectators can open chests that aren't ready to be re-generated but this doesn't support that)
+             return null;
+         } else {
+             this.unpackLootTable(playerInventory.player);
+@@ -198,4 +_,58 @@
+     public void stopOpen(Player player) {
+         this.level().gameEvent(GameEvent.CONTAINER_CLOSE, this.position(), GameEvent.Context.of(player));
+     }
++
++    // Paper start - LootTable API
++    final com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData = new com.destroystokyo.paper.loottable.PaperLootableInventoryData();
++
++    @Override
++    public com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData() {
++        return this.lootableData;
++    }
++    // Paper end - LootTable API
++    // CraftBukkit start
++    public java.util.List<org.bukkit.entity.HumanEntity> transaction = new java.util.ArrayList<>();
++    private int maxStack = MAX_STACK;
++
++    @Override
++    public java.util.List<net.minecraft.world.item.ItemStack> getContents() {
++        return this.itemStacks;
++    }
++
++    @Override
++    public void onOpen(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.add(player);
++    }
++
++    @Override
++    public void onClose(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.remove(player);
++    }
++
++    @Override
++    public java.util.List<org.bukkit.entity.HumanEntity> getViewers() {
++        return this.transaction;
++    }
++
++    @Override
++    public org.bukkit.inventory.InventoryHolder getOwner() {
++        org.bukkit.entity.Entity entity = this.getBukkitEntity();
++        return entity instanceof final org.bukkit.inventory.InventoryHolder inventoryHolder ? inventoryHolder : null;
++    }
++
++    @Override
++    public int getMaxStackSize() {
++        return this.maxStack;
++    }
++
++    @Override
++    public void setMaxStackSize(int size) {
++        this.maxStack = size;
++    }
++
++    @Override
++    public org.bukkit.Location getLocation() {
++        return this.getBukkitEntity().getLocation();
++    }
++    // CraftBukkit end
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/AbstractMinecart.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/vehicle/AbstractMinecart.java.patch
similarity index 58%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/AbstractMinecart.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/vehicle/AbstractMinecart.java.patch
index e7b99cb727..35881f5e7a 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/AbstractMinecart.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/vehicle/AbstractMinecart.java.patch
@@ -1,23 +1,9 @@
 --- a/net/minecraft/world/entity/vehicle/AbstractMinecart.java
 +++ b/net/minecraft/world/entity/vehicle/AbstractMinecart.java
-@@ -42,6 +42,13 @@
- import net.minecraft.world.level.block.state.properties.RailShape;
- import net.minecraft.world.phys.AABB;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.Location;
-+import org.bukkit.craftbukkit.util.CraftLocation;
-+import org.bukkit.entity.Vehicle;
-+import org.bukkit.event.vehicle.VehicleEntityCollisionEvent;
-+import org.bukkit.util.Vector;
-+// CraftBukkit end
- 
- public abstract class AbstractMinecart extends VehicleEntity {
- 
-@@ -76,6 +83,18 @@
-         enummap.put(RailShape.NORTH_EAST, Pair.of(baseblockposition2, baseblockposition1));
+@@ -74,6 +_,17 @@
+         map.put(RailShape.NORTH_WEST, Pair.of(unitVec3i2, unitVec3i));
+         map.put(RailShape.NORTH_EAST, Pair.of(unitVec3i2, unitVec3i1));
      });
- 
 +    // CraftBukkit start
 +    public boolean slowWhenEmpty = true;
 +    private double derailedX = 0.5;
@@ -27,33 +13,23 @@
 +    private double flyingY = 0.95;
 +    private double flyingZ = 0.95;
 +    public Double maxSpeed;
-+    // CraftBukkit end
 +    public net.kyori.adventure.util.TriState frictionState = net.kyori.adventure.util.TriState.NOT_SET; // Paper - Friction API
-+
-     protected AbstractMinecart(EntityType<?> type, Level world) {
-         super(type, world);
-         this.blocksBuilding = true;
-@@ -101,7 +120,7 @@
++    // CraftBukkit end
  
-     @Nullable
-     public static <T extends AbstractMinecart> T createMinecart(Level world, double x, double y, double z, EntityType<T> type, EntitySpawnReason reason, ItemStack stack, @Nullable Player player) {
--        T t0 = (AbstractMinecart) type.create(world, reason);
-+        T t0 = (T) type.create(world, reason); // CraftBukkit - decompile error
- 
-         if (t0 != null) {
-             t0.setInitialPos(x, y, z);
-@@ -139,11 +158,19 @@
+     protected AbstractMinecart(EntityType<?> entityType, Level level) {
+         super(entityType, level);
+@@ -134,11 +_,19 @@
  
      @Override
-     public boolean canCollideWith(Entity other) {
--        return AbstractBoat.canVehicleCollide(this, other);
+     public boolean canCollideWith(Entity entity) {
+-        return AbstractBoat.canVehicleCollide(this, entity);
 +        // Paper start - fix VehicleEntityCollisionEvent not called when colliding with player
-+        boolean collides = AbstractBoat.canVehicleCollide(this, other);
++        boolean collides = AbstractBoat.canVehicleCollide(this, entity);
 +        if (!collides) {
 +            return false;
 +        }
-+        org.bukkit.event.vehicle.VehicleEntityCollisionEvent collisionEvent = new org.bukkit.event.vehicle.VehicleEntityCollisionEvent((org.bukkit.entity.Vehicle) getBukkitEntity(), other.getBukkitEntity());
 +
++        org.bukkit.event.vehicle.VehicleEntityCollisionEvent collisionEvent = new org.bukkit.event.vehicle.VehicleEntityCollisionEvent((org.bukkit.entity.Vehicle) getBukkitEntity(), entity.getBukkitEntity());
 +        return collisionEvent.callEvent();
 +        // Paper end - fix VehicleEntityCollisionEvent not called when colliding with player
      }
@@ -64,7 +40,7 @@
          return true;
      }
  
-@@ -262,6 +289,14 @@
+@@ -239,6 +_,14 @@
  
      @Override
      public void tick() {
@@ -79,7 +55,7 @@
          if (this.getHurtTime() > 0) {
              this.setHurtTime(this.getHurtTime() - 1);
          }
-@@ -271,8 +306,20 @@
+@@ -248,8 +_,20 @@
          }
  
          this.checkBelowWorld();
@@ -88,24 +64,24 @@
          this.behavior.tick();
 +        // CraftBukkit start
 +        org.bukkit.World bworld = this.level().getWorld();
-+        Location from = new Location(bworld, prevX, prevY, prevZ, prevYaw, prevPitch);
-+        Location to = CraftLocation.toBukkit(this.position(), bworld, this.getYRot(), this.getXRot());
-+        Vehicle vehicle = (Vehicle) this.getBukkitEntity();
++        org.bukkit.Location from = new org.bukkit.Location(bworld, prevX, prevY, prevZ, prevYaw, prevPitch);
++        org.bukkit.Location to = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(this.position(), bworld, this.getYRot(), this.getXRot());
++        org.bukkit.entity.Vehicle vehicle = (org.bukkit.entity.Vehicle) this.getBukkitEntity();
 +
-+        this.level().getCraftServer().getPluginManager().callEvent(new org.bukkit.event.vehicle.VehicleUpdateEvent(vehicle));
++        new org.bukkit.event.vehicle.VehicleUpdateEvent(vehicle).callEvent();
 +
 +        if (!from.equals(to)) {
-+            this.level().getCraftServer().getPluginManager().callEvent(new org.bukkit.event.vehicle.VehicleMoveEvent(vehicle, from, to));
++            new org.bukkit.event.vehicle.VehicleMoveEvent(vehicle, from, to).callEvent();
 +        }
 +        // CraftBukkit end
          this.updateInWaterStateAndDoFluidPushing();
          if (this.isInLava()) {
              this.lavaHurt();
-@@ -385,12 +432,16 @@
- 
-         this.setDeltaMovement(Mth.clamp(vec3d.x, -d0, d0), vec3d.y, Mth.clamp(vec3d.z, -d0, d0));
+@@ -360,12 +_,16 @@
+         Vec3 deltaMovement = this.getDeltaMovement();
+         this.setDeltaMovement(Mth.clamp(deltaMovement.x, -maxSpeed, maxSpeed), deltaMovement.y, Mth.clamp(deltaMovement.z, -maxSpeed, maxSpeed));
          if (this.onGround()) {
--            this.setDeltaMovement(this.getDeltaMovement().scale(0.5D));
+-            this.setDeltaMovement(this.getDeltaMovement().scale(0.5));
 +            // CraftBukkit start - replace magic numbers with our variables
 +            this.setDeltaMovement(new Vec3(this.getDeltaMovement().x * this.derailedX, this.getDeltaMovement().y * this.derailedY, this.getDeltaMovement().z * this.derailedZ));
 +            // CraftBukkit end
@@ -113,20 +89,20 @@
  
          this.move(MoverType.SELF, this.getDeltaMovement());
          if (!this.onGround()) {
--            this.setDeltaMovement(this.getDeltaMovement().scale(0.95D));
+-            this.setDeltaMovement(this.getDeltaMovement().scale(0.95));
 +            // CraftBukkit start - replace magic numbers with our variables
 +            this.setDeltaMovement(new Vec3(this.getDeltaMovement().x * this.flyingX, this.getDeltaMovement().y * this.flyingY, this.getDeltaMovement().z * this.flyingZ));
 +            // CraftBukkit end
          }
- 
      }
-@@ -502,6 +553,16 @@
  
-         this.flipped = nbt.getBoolean("FlippedRotation");
-         this.firstTick = nbt.getBoolean("HasTicked");
+@@ -469,6 +_,16 @@
+ 
+         this.flipped = compound.getBoolean("FlippedRotation");
+         this.firstTick = compound.getBoolean("HasTicked");
 +        // Paper start - Friction API
-+        if (nbt.contains("Paper.FrictionState")) {
-+            String fs = nbt.getString("Paper.FrictionState");
++        if (compound.contains("Paper.FrictionState")) {
++            String fs = compound.getString("Paper.FrictionState");
 +            try {
 +                frictionState = net.kyori.adventure.util.TriState.valueOf(fs);
 +            } catch (Exception ignored) {
@@ -137,14 +113,14 @@
      }
  
      @Override
-@@ -514,13 +575,28 @@
+@@ -481,13 +_,27 @@
  
-         nbt.putBoolean("FlippedRotation", this.flipped);
-         nbt.putBoolean("HasTicked", this.firstTick);
+         compound.putBoolean("FlippedRotation", this.flipped);
+         compound.putBoolean("HasTicked", this.firstTick);
 +
 +        // Paper start - Friction API
 +        if (this.frictionState != net.kyori.adventure.util.TriState.NOT_SET) {
-+            nbt.putString("Paper.FrictionState", this.frictionState.toString());
++            compound.putString("Paper.FrictionState", this.frictionState.toString());
 +        }
 +        // Paper end - Friction API
      }
@@ -156,41 +132,41 @@
 +                if (!this.level().paperConfig().collisions.allowVehicleCollisions && this.level().paperConfig().collisions.onlyPlayersCollide && !(entity instanceof Player)) return; // Paper - Collision option for requiring a player participant
                  if (!this.hasPassenger(entity)) {
 +                    // CraftBukkit start
-+                    VehicleEntityCollisionEvent collisionEvent = new VehicleEntityCollisionEvent((Vehicle) this.getBukkitEntity(), entity.getBukkitEntity());
-+                    this.level().getCraftServer().getPluginManager().callEvent(collisionEvent);
-+
-+                    if (collisionEvent.isCancelled()) {
-+                        return;
-+                    }
++                    org.bukkit.event.vehicle.VehicleEntityCollisionEvent collisionEvent = new org.bukkit.event.vehicle.VehicleEntityCollisionEvent(
++                        (org.bukkit.entity.Vehicle) this.getBukkitEntity(),
++                        entity.getBukkitEntity()
++                    );
++                    if (!collisionEvent.callEvent()) return;
 +                    // CraftBukkit end
-                     double d0 = entity.getX() - this.getX();
+                     double d = entity.getX() - this.getX();
                      double d1 = entity.getZ() - this.getZ();
-                     double d2 = d0 * d0 + d1 * d1;
-@@ -645,4 +721,27 @@
+                     double d2 = d * d + d1 * d1;
+@@ -602,4 +_,28 @@
      public boolean isFurnace() {
          return false;
      }
 +
 +    // CraftBukkit start - Methods for getting and setting flying and derailed velocity modifiers
-+    public Vector getFlyingVelocityMod() {
-+        return new Vector(this.flyingX, this.flyingY, this.flyingZ);
++    public org.bukkit.util.Vector getFlyingVelocityMod() {
++        return new org.bukkit.util.Vector(this.flyingX, this.flyingY, this.flyingZ);
 +    }
 +
-+    public void setFlyingVelocityMod(Vector flying) {
++    public void setFlyingVelocityMod(org.bukkit.util.Vector flying) {
 +        this.flyingX = flying.getX();
 +        this.flyingY = flying.getY();
 +        this.flyingZ = flying.getZ();
 +    }
 +
-+    public Vector getDerailedVelocityMod() {
-+        return new Vector(this.derailedX, this.derailedY, this.derailedZ);
++    public org.bukkit.util.Vector getDerailedVelocityMod() {
++        return new org.bukkit.util.Vector(this.derailedX, this.derailedY, this.derailedZ);
 +    }
 +
-+    public void setDerailedVelocityMod(Vector derailed) {
++    public void setDerailedVelocityMod(org.bukkit.util.Vector derailed) {
 +        this.derailedX = derailed.getX();
 +        this.derailedY = derailed.getY();
 +        this.derailedZ = derailed.getZ();
 +    }
 +    // CraftBukkit end
++
 +    public net.minecraft.world.item.Item publicGetDropItem() { return getDropItem(); } // Paper - api to get boat and minecart material - expose public drop item
  }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java.patch
new file mode 100644
index 0000000000..5289f0b57c
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java.patch
@@ -0,0 +1,86 @@
+--- a/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java
++++ b/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java
+@@ -21,10 +_,11 @@
+ import net.minecraft.world.phys.Vec3;
+ 
+ public abstract class AbstractMinecartContainer extends AbstractMinecart implements ContainerEntity {
+-    private NonNullList<ItemStack> itemStacks = NonNullList.withSize(36, ItemStack.EMPTY);
++    private NonNullList<ItemStack> itemStacks = NonNullList.withSize(this.getContainerSize(), ItemStack.EMPTY); // CraftBukkit - SPIGOT-3513
+     @Nullable
+     public ResourceKey<LootTable> lootTable;
+     public long lootTableSeed;
++    private final com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData = new com.destroystokyo.paper.loottable.PaperLootableInventoryData(); // Paper - LootTable API
+ 
+     protected AbstractMinecartContainer(EntityType<?> entityType, Level level) {
+         super(entityType, level);
+@@ -72,11 +_,18 @@
+ 
+     @Override
+     public void remove(Entity.RemovalReason reason) {
++        // CraftBukkit start - add Bukkit remove cause
++        this.remove(reason, null);
++    }
++
++    @Override
++    public void remove(Entity.RemovalReason reason, org.bukkit.event.entity.EntityRemoveEvent.Cause cause) {
++        // CraftBukkit end  - add Bukkit remove cause
+         if (!this.level().isClientSide && reason.shouldDestroy()) {
+             Containers.dropContents(this.level(), this, this);
+         }
+ 
+-        super.remove(reason);
++        super.remove(reason, cause); // CraftBukkit - add Bukkit remove cause
+     }
+ 
+     @Override
+@@ -164,4 +_,50 @@
+     public void clearItemStacks() {
+         this.itemStacks = NonNullList.withSize(this.getContainerSize(), ItemStack.EMPTY);
+     }
++
++    // Paper start - LootTable API
++    @Override
++    public com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData() {
++        return this.lootableData;
++    }
++    // Paper end - LootTable API
++
++    // CraftBukkit start
++    public java.util.List<org.bukkit.entity.HumanEntity> transaction = new java.util.ArrayList<>();
++    private int maxStack = MAX_STACK;
++
++    public java.util.List<net.minecraft.world.item.ItemStack> getContents() {
++        return this.itemStacks;
++    }
++
++    public void onOpen(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.add(player);
++    }
++
++    public void onClose(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.remove(player);
++    }
++
++    public java.util.List<org.bukkit.entity.HumanEntity> getViewers() {
++        return this.transaction;
++    }
++
++    public org.bukkit.inventory.InventoryHolder getOwner() {
++        return this.getBukkitEntity() instanceof final org.bukkit.inventory.InventoryHolder inventoryHolder ? inventoryHolder : null;
++    }
++
++    @Override
++    public int getMaxStackSize() {
++        return this.maxStack;
++    }
++
++    public void setMaxStackSize(int size) {
++        this.maxStack = size;
++    }
++
++    @Override
++    public org.bukkit.Location getLocation() {
++        return this.getBukkitEntity().getLocation();
++    }
++    // CraftBukkit end
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/ContainerEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/vehicle/ContainerEntity.java.patch
similarity index 61%
rename from paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/ContainerEntity.java.patch
rename to paper-server/patches/sources/net/minecraft/world/entity/vehicle/ContainerEntity.java.patch
index 3ac071ee7d..fea198069c 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/ContainerEntity.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/vehicle/ContainerEntity.java.patch
@@ -1,38 +1,38 @@
 --- a/net/minecraft/world/entity/vehicle/ContainerEntity.java
 +++ b/net/minecraft/world/entity/vehicle/ContainerEntity.java
-@@ -62,22 +62,26 @@
-     default void addChestVehicleSaveData(CompoundTag nbt, HolderLookup.Provider registries) {
+@@ -62,22 +_,26 @@
+     default void addChestVehicleSaveData(CompoundTag tag, HolderLookup.Provider levelRegistry) {
          if (this.getContainerLootTable() != null) {
-             nbt.putString("LootTable", this.getContainerLootTable().location().toString());
-+            this.lootableData().saveNbt(nbt); // Paper
+             tag.putString("LootTable", this.getContainerLootTable().location().toString());
++            this.lootableData().saveNbt(tag); // Paper
              if (this.getContainerLootTableSeed() != 0L) {
-                 nbt.putLong("LootTableSeed", this.getContainerLootTableSeed());
+                 tag.putLong("LootTableSeed", this.getContainerLootTableSeed());
              }
 -        } else {
--            ContainerHelper.saveAllItems(nbt, this.getItemStacks(), registries);
+-            ContainerHelper.saveAllItems(tag, this.getItemStacks(), levelRegistry);
          }
-+        ContainerHelper.saveAllItems(nbt, this.getItemStacks(), registries); // Paper - always save the items, table may still remain
++        ContainerHelper.saveAllItems(tag, this.getItemStacks(), levelRegistry); // Paper - always save the items, table may still remain
      }
  
-     default void readChestVehicleSaveData(CompoundTag nbt, HolderLookup.Provider registries) {
+     default void readChestVehicleSaveData(CompoundTag tag, HolderLookup.Provider levelRegistry) {
          this.clearItemStacks();
-         if (nbt.contains("LootTable", 8)) {
--            this.setContainerLootTable(ResourceKey.create(Registries.LOOT_TABLE, ResourceLocation.parse(nbt.getString("LootTable"))));
-+            this.setContainerLootTable(net.minecraft.Optionull.map(ResourceLocation.tryParse(nbt.getString("LootTable")), rl -> ResourceKey.create(Registries.LOOT_TABLE, rl))); // Paper - Validate ResourceLocation
+         if (tag.contains("LootTable", 8)) {
+-            this.setContainerLootTable(ResourceKey.create(Registries.LOOT_TABLE, ResourceLocation.parse(tag.getString("LootTable"))));
++            this.setContainerLootTable(net.minecraft.Optionull.map(ResourceLocation.tryParse(tag.getString("LootTable")), rl -> ResourceKey.create(Registries.LOOT_TABLE, rl))); // Paper - Validate ResourceLocation
 +            // Paper start - LootTable API
 +            if (this.getContainerLootTable() != null) {
-+                this.lootableData().loadNbt(nbt);
++                this.lootableData().loadNbt(tag);
 +            }
 +            // Paper end - LootTable API
-             this.setContainerLootTableSeed(nbt.getLong("LootTableSeed"));
+             this.setContainerLootTableSeed(tag.getLong("LootTableSeed"));
 -        } else {
--            ContainerHelper.loadAllItems(nbt, this.getItemStacks(), registries);
+-            ContainerHelper.loadAllItems(tag, this.getItemStacks(), levelRegistry);
          }
-+        ContainerHelper.loadAllItems(nbt, this.getItemStacks(), registries); // Paper - always save the items, table may still remain
++        ContainerHelper.loadAllItems(tag, this.getItemStacks(), levelRegistry); // Paper - always read the items, table may still remain
      }
  
-     default void chestVehicleDestroyed(DamageSource source, ServerLevel world, Entity vehicle) {
-@@ -91,19 +95,28 @@
+     default void chestVehicleDestroyed(DamageSource damageSource, ServerLevel level, Entity entity) {
+@@ -91,19 +_,27 @@
      }
  
      default InteractionResult interactWithContainerVehicle(Player player) {
@@ -46,10 +46,10 @@
      }
  
      default void unpackChestVehicleLootTable(@Nullable Player player) {
-         MinecraftServer minecraftServer = this.level().getServer();
--        if (this.getContainerLootTable() != null && minecraftServer != null) {
-+        if (minecraftServer != null && this.lootableData().shouldReplenish(this, com.destroystokyo.paper.loottable.PaperLootableInventoryData.ENTITY, player)) { // Paper - LootTable API
-             LootTable lootTable = minecraftServer.reloadableRegistries().getLootTable(this.getContainerLootTable());
+         MinecraftServer server = this.level().getServer();
+-        if (this.getContainerLootTable() != null && server != null) {
++        if (server != null && this.lootableData().shouldReplenish(this, com.destroystokyo.paper.loottable.PaperLootableInventoryData.ENTITY, player)) { // Paper - LootTable API
+             LootTable lootTable = server.reloadableRegistries().getLootTable(this.getContainerLootTable());
              if (player != null) {
                  CriteriaTriggers.GENERATE_LOOT.trigger((ServerPlayer)player, this.getContainerLootTable());
              }
@@ -60,11 +60,10 @@
 +                this.setContainerLootTable(null);
 +            }
 +            // Paper end - LootTable API
-+
              LootParams.Builder builder = new LootParams.Builder((ServerLevel)this.level()).withParameter(LootContextParams.ORIGIN, this.position());
              if (player != null) {
                  builder.withLuck(player.getLuck()).withParameter(LootContextParams.THIS_ENTITY, player);
-@@ -173,4 +186,14 @@
+@@ -173,4 +_,14 @@
      default boolean isChestVehicleStillValid(Player player) {
          return !this.isRemoved() && player.canInteractWithEntity(this.getBoundingBox(), 4.0);
      }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java.patch
new file mode 100644
index 0000000000..7a3a6b08c0
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java.patch
@@ -0,0 +1,24 @@
+--- a/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java
++++ b/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java
+@@ -126,7 +_,7 @@
+                 MinecartCommandBlock.this.position(),
+                 MinecartCommandBlock.this.getRotationVector(),
+                 this.getLevel(),
+-                2,
++                this.getLevel().paperConfig().commandBlocks.permissionsLevel, // Paper - configurable command block perm level
+                 this.getName().getString(),
+                 MinecartCommandBlock.this.getDisplayName(),
+                 this.getLevel().getServer(),
+@@ -138,5 +_,12 @@
+         public boolean isValid() {
+             return !MinecartCommandBlock.this.isRemoved();
+         }
++
++        // CraftBukkit start
++        @Override
++        public org.bukkit.command.CommandSender getBukkitSender(CommandSourceStack wrapper) {
++            return net.minecraft.world.entity.vehicle.MinecartCommandBlock.this.getBukkitEntity();
++        }
++        // CraftBukkit end
+     }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/vehicle/MinecartTNT.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/vehicle/MinecartTNT.java.patch
new file mode 100644
index 0000000000..0f23c30d40
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/vehicle/MinecartTNT.java.patch
@@ -0,0 +1,56 @@
+--- a/net/minecraft/world/entity/vehicle/MinecartTNT.java
++++ b/net/minecraft/world/entity/vehicle/MinecartTNT.java
+@@ -33,6 +_,7 @@
+     public int fuse = -1;
+     public float explosionPowerBase = 4.0F;
+     public float explosionSpeedFactor = 1.0F;
++    public boolean isIncendiary = false; // CraftBukkit - add field
+ 
+     public MinecartTNT(EntityType<? extends MinecartTNT> entityType, Level level) {
+         super(entityType, level);
+@@ -47,6 +_,12 @@
+     public void tick() {
+         super.tick();
+         if (this.fuse > 0) {
++            // Paper start - Configurable TNT height nerf
++            if (this.level().paperConfig().fixes.tntEntityHeightNerf.test(v -> this.getY() > v)) {
++                this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.OUT_OF_WORLD);
++                return;
++            }
++            // Paper end - Configurable TNT height nerf
+             this.fuse--;
+             this.level().addParticle(ParticleTypes.SMOKE, this.getX(), this.getY() + 0.5, this.getZ(), 0.0, 0.0, 0.0);
+         } else if (this.fuse == 0) {
+@@ -101,6 +_,17 @@
+     protected void explode(@Nullable DamageSource damageSource, double radiusModifier) {
+         if (this.level() instanceof ServerLevel serverLevel) {
+             double min = Math.min(Math.sqrt(radiusModifier), 5.0);
++            // CraftBukkit start
++            org.bukkit.event.entity.ExplosionPrimeEvent event = new org.bukkit.event.entity.ExplosionPrimeEvent(
++                this.getBukkitEntity(),
++                (float) (this.explosionPowerBase + this.explosionSpeedFactor * this.random.nextDouble() * 1.5 * min),
++                this.isIncendiary
++            );
++            if (!event.callEvent()) {
++                this.fuse = -1;
++                return;
++            }
++            // CraftBukkit end
+             serverLevel.explode(
+                 this,
+                 damageSource,
+@@ -108,11 +_,11 @@
+                 this.getX(),
+                 this.getY(),
+                 this.getZ(),
+-                (float)(this.explosionPowerBase + this.explosionSpeedFactor * this.random.nextDouble() * 1.5 * min),
+-                false,
++                event.getRadius(), // CraftBukkit - explosion prime event
++                event.getFire(), // CraftBukkit - explosion prime event
+                 Level.ExplosionInteraction.TNT
+             );
+-            this.discard();
++            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.EXPLODE); // CraftBukkit - add Bukkit remove cause
+         }
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/vehicle/NewMinecartBehavior.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/vehicle/NewMinecartBehavior.java.patch
new file mode 100644
index 0000000000..31506e8555
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/vehicle/NewMinecartBehavior.java.patch
@@ -0,0 +1,73 @@
+--- a/net/minecraft/world/entity/vehicle/NewMinecartBehavior.java
++++ b/net/minecraft/world/entity/vehicle/NewMinecartBehavior.java
+@@ -479,6 +_,12 @@
+ 
+     @Override
+     public double getMaxSpeed(ServerLevel level) {
++        // CraftBukkit start
++        Double maxSpeed = this.minecart.maxSpeed;
++        if (maxSpeed != null) {
++            return (this.minecart.isInWater() ? maxSpeed / 2.0D : maxSpeed);
++        }
++        // CraftBukkit end
+         return level.getGameRules().getInt(GameRules.RULE_MINECART_MAX_SPEED) * (this.minecart.isInWater() ? 0.5 : 1.0) / 20.0;
+     }
+ 
+@@ -494,7 +_,8 @@
+ 
+     @Override
+     public double getSlowdownFactor() {
+-        return this.minecart.isVehicle() ? 0.997 : 0.975;
++        if (this.minecart.frictionState == net.kyori.adventure.util.TriState.FALSE) return 1; // Paper
++        return this.minecart.isVehicle() || !this.minecart.slowWhenEmpty ? 0.997 : 0.975; // CraftBukkit - add !this.slowWhenEmpty
+     }
+ 
+     @Override
+@@ -518,6 +_,13 @@
+                         && !(entity instanceof AbstractMinecart)
+                         && !this.minecart.isVehicle()
+                         && !entity.isPassenger()) {
++                        // CraftBukkit start
++                        org.bukkit.event.vehicle.VehicleEntityCollisionEvent collisionEvent = new org.bukkit.event.vehicle.VehicleEntityCollisionEvent(
++                            (org.bukkit.entity.Vehicle) this.minecart.getBukkitEntity(),
++                            entity.getBukkitEntity()
++                        );
++                        if (!collisionEvent.callEvent()) continue;
++                        // CraftBukkit end
+                         boolean flag = entity.startRiding(this.minecart);
+                         if (flag) {
+                             return true;
+@@ -541,6 +_,17 @@
+                         || entity instanceof AbstractMinecart
+                         || this.minecart.isVehicle()
+                         || entity.isPassenger()) {
++                        // CraftBukkit start
++                        if (!this.minecart.isPassengerOfSameVehicle(entity)) {
++                            org.bukkit.event.vehicle.VehicleEntityCollisionEvent collisionEvent = new org.bukkit.event.vehicle.VehicleEntityCollisionEvent(
++                                (org.bukkit.entity.Vehicle) this.minecart.getBukkitEntity(),
++                                entity.getBukkitEntity()
++                            );
++                            if (!collisionEvent.callEvent()) {
++                                continue;
++                            }
++                        }
++                        // CraftBukkit end
+                         entity.push(this.minecart);
+                         flag = true;
+                     }
+@@ -549,6 +_,15 @@
+         } else {
+             for (Entity entity1 : this.level().getEntities(this.minecart, box)) {
+                 if (!this.minecart.hasPassenger(entity1) && entity1.isPushable() && entity1 instanceof AbstractMinecart) {
++                    // CraftBukkit start
++                    org.bukkit.event.vehicle.VehicleEntityCollisionEvent collisionEvent = new org.bukkit.event.vehicle.VehicleEntityCollisionEvent(
++                        (org.bukkit.entity.Vehicle) this.minecart.getBukkitEntity(),
++                        entity1.getBukkitEntity()
++                    );
++                    if (!collisionEvent.callEvent()) {
++                        continue;
++                    }
++                    // CraftBukkit end
+                     entity1.push(this.minecart);
+                     flag = true;
+                 }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/vehicle/OldMinecartBehavior.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/vehicle/OldMinecartBehavior.java.patch
new file mode 100644
index 0000000000..cc3a0cf21d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/vehicle/OldMinecartBehavior.java.patch
@@ -0,0 +1,58 @@
+--- a/net/minecraft/world/entity/vehicle/OldMinecartBehavior.java
++++ b/net/minecraft/world/entity/vehicle/OldMinecartBehavior.java
+@@ -414,8 +_,22 @@
+                         && !(entity instanceof AbstractMinecart)
+                         && !this.minecart.isVehicle()
+                         && !entity.isPassenger()) {
++                        // CraftBukkit start
++                        org.bukkit.event.vehicle.VehicleEntityCollisionEvent collisionEvent = new org.bukkit.event.vehicle.VehicleEntityCollisionEvent(
++                            (org.bukkit.entity.Vehicle) this.minecart.getBukkitEntity(), entity.getBukkitEntity()
++                        );
++                        if (!collisionEvent.callEvent()) continue;
++                        // CraftBukkit end
+                         entity.startRiding(this.minecart);
+                     } else {
++                        // CraftBukkit start
++                        if (!this.minecart.isPassengerOfSameVehicle(entity)) {
++                            org.bukkit.event.vehicle.VehicleEntityCollisionEvent collisionEvent = new org.bukkit.event.vehicle.VehicleEntityCollisionEvent(
++                                (org.bukkit.entity.Vehicle) this.minecart.getBukkitEntity(), entity.getBukkitEntity()
++                            );
++                            if (!collisionEvent.callEvent()) continue;
++                        }
++                        // CraftBukkit end
+                         entity.push(this.minecart);
+                     }
+                 }
+@@ -423,6 +_,12 @@
+         } else {
+             for (Entity entity1 : this.level().getEntities(this.minecart, aabb)) {
+                 if (!this.minecart.hasPassenger(entity1) && entity1.isPushable() && entity1 instanceof AbstractMinecart) {
++                    // CraftBukkit start
++                    org.bukkit.event.vehicle.VehicleEntityCollisionEvent collisionEvent = new org.bukkit.event.vehicle.VehicleEntityCollisionEvent(
++                        (org.bukkit.entity.Vehicle) this.minecart.getBukkitEntity(), entity1.getBukkitEntity()
++                    );
++                    if (!collisionEvent.callEvent()) continue;
++                    // CraftBukkit end
+                     entity1.push(this.minecart);
+                 }
+             }
+@@ -443,11 +_,18 @@
+ 
+     @Override
+     public double getMaxSpeed(ServerLevel level) {
++        // CraftBukkit start
++        Double maxSpeed = this.minecart.maxSpeed;
++        if (maxSpeed != null) {
++            return (this.minecart.isInWater() ? maxSpeed / 2.0D : maxSpeed);
++        }
++        // CraftBukkit end
+         return this.minecart.isInWater() ? 0.2 : 0.4;
+     }
+ 
+     @Override
+     public double getSlowdownFactor() {
+-        return this.minecart.isVehicle() ? 0.997 : 0.96;
++        if (this.minecart.frictionState == net.kyori.adventure.util.TriState.FALSE) return 1; // Paper
++        return this.minecart.isVehicle() || !this.minecart.slowWhenEmpty ? 0.997 : 0.96; // CraftBukkit - add !this.slowWhenEmpty
+     }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/vehicle/VehicleEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/vehicle/VehicleEntity.java.patch
new file mode 100644
index 0000000000..0020b09f51
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/entity/vehicle/VehicleEntity.java.patch
@@ -0,0 +1,42 @@
+--- a/net/minecraft/world/entity/vehicle/VehicleEntity.java
++++ b/net/minecraft/world/entity/vehicle/VehicleEntity.java
+@@ -38,6 +_,14 @@
+         } else if (this.isInvulnerableToBase(damageSource)) {
+             return false;
+         } else {
++            // CraftBukkit start
++            org.bukkit.entity.Vehicle vehicle = (org.bukkit.entity.Vehicle) this.getBukkitEntity();
++            org.bukkit.entity.Entity attacker = (damageSource.getEntity() == null) ? null : damageSource.getEntity().getBukkitEntity();
++
++            org.bukkit.event.vehicle.VehicleDamageEvent event = new org.bukkit.event.vehicle.VehicleDamageEvent(vehicle, attacker, amount);
++            if (!event.callEvent()) return false;
++            amount = (float) event.getDamage();
++            // CraftBukkit end
+             this.setHurtDir(-this.getHurtDir());
+             this.setHurtTime(10);
+             this.markHurt();
+@@ -46,9 +_,23 @@
+             boolean flag = damageSource.getEntity() instanceof Player player && player.getAbilities().instabuild;
+             if ((flag || !(this.getDamage() > 40.0F)) && !this.shouldSourceDestroy(damageSource)) {
+                 if (flag) {
+-                    this.discard();
++                    // CraftBukkit start
++                    org.bukkit.event.vehicle.VehicleDestroyEvent destroyEvent = new org.bukkit.event.vehicle.VehicleDestroyEvent(vehicle, attacker);
++                    if (!destroyEvent.callEvent()) {
++                        this.setDamage(40.0F); // Maximize damage so this doesn't get triggered again right away
++                        return true;
++                    }
++                    // CraftBukkit end
++                    this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause
+                 }
+             } else {
++                // CraftBukkit start
++                org.bukkit.event.vehicle.VehicleDestroyEvent destroyEvent = new org.bukkit.event.vehicle.VehicleDestroyEvent(vehicle, attacker);
++                if (!destroyEvent.callEvent()) {
++                    this.setDamage(40.0F); // Maximize damage so this doesn't get triggered again right away
++                    return true;
++                }
++                // CraftBukkit end
+                 this.destroy(level, damageSource);
+             }
+ 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/food/FoodData.java.patch b/paper-server/patches/sources/net/minecraft/world/food/FoodData.java.patch
similarity index 50%
rename from paper-server/patches/unapplied/net/minecraft/world/food/FoodData.java.patch
rename to paper-server/patches/sources/net/minecraft/world/food/FoodData.java.patch
index 68a35b8aa4..b18d67421a 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/food/FoodData.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/food/FoodData.java.patch
@@ -1,22 +1,6 @@
 --- a/net/minecraft/world/food/FoodData.java
 +++ b/net/minecraft/world/food/FoodData.java
-@@ -1,11 +1,14 @@
- package net.minecraft.world.food;
- 
-+import net.minecraft.world.level.GameRules;
- import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.network.protocol.game.ClientboundSetHealthPacket;
- import net.minecraft.server.level.ServerLevel;
- import net.minecraft.server.level.ServerPlayer;
- import net.minecraft.util.Mth;
- import net.minecraft.world.Difficulty;
--import net.minecraft.world.level.GameRules;
-+import net.minecraft.world.item.ItemStack;
-+// CraftBukkit end
- 
- public class FoodData {
- 
-@@ -13,6 +16,11 @@
+@@ -12,6 +_,11 @@
      public float saturationLevel = 5.0F;
      public float exhaustionLevel;
      private int tickTimer;
@@ -26,33 +10,30 @@
 +    public int starvationRate = 80;
 +    // CraftBukkit end
  
-     public FoodData() {}
- 
-@@ -29,6 +37,20 @@
-         this.add(foodComponent.nutrition(), foodComponent.saturation());
+     private void add(int foodLevel, float saturationLevel) {
+         this.foodLevel = Mth.clamp(foodLevel + this.foodLevel, 0, 20);
+@@ -26,6 +_,17 @@
+         this.add(foodProperties.nutrition(), foodProperties.saturation());
      }
  
 +    // CraftBukkit start
-+    public void eat(FoodProperties foodinfo, ItemStack itemstack, ServerPlayer entityplayer) {
++    public void eat(FoodProperties foodProperties, net.minecraft.world.item.ItemStack stack, ServerPlayer serverPlayer) {
 +        int oldFoodLevel = this.foodLevel;
-+
-+        org.bukkit.event.entity.FoodLevelChangeEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callFoodLevelChangeEvent(entityplayer, foodinfo.nutrition() + oldFoodLevel, itemstack);
-+
++        org.bukkit.event.entity.FoodLevelChangeEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callFoodLevelChangeEvent(serverPlayer, foodProperties.nutrition() + oldFoodLevel, stack);
 +        if (!event.isCancelled()) {
-+            this.add(event.getFoodLevel() - oldFoodLevel, foodinfo.saturation());
++            this.add(event.getFoodLevel() - oldFoodLevel, foodProperties.saturation());
 +        }
-+
-+        entityplayer.getBukkitEntity().sendHealthUpdate();
++        serverPlayer.getBukkitEntity().sendHealthUpdate();
 +    }
 +    // CraftBukkit end
 +
      public void tick(ServerPlayer player) {
-         ServerLevel worldserver = player.serverLevel();
-         Difficulty enumdifficulty = worldserver.getDifficulty();
-@@ -38,7 +60,15 @@
+         ServerLevel serverLevel = player.serverLevel();
+         Difficulty difficulty = serverLevel.getDifficulty();
+@@ -34,29 +_,39 @@
              if (this.saturationLevel > 0.0F) {
                  this.saturationLevel = Math.max(this.saturationLevel - 1.0F, 0.0F);
-             } else if (enumdifficulty != Difficulty.PEACEFUL) {
+             } else if (difficulty != Difficulty.PEACEFUL) {
 -                this.foodLevel = Math.max(this.foodLevel - 1, 0);
 +                // CraftBukkit start
 +                org.bukkit.event.entity.FoodLevelChangeEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callFoodLevelChangeEvent(player, Math.max(this.foodLevel - 1, 0));
@@ -61,28 +42,26 @@
 +                    this.foodLevel = event.getFoodLevel();
 +                }
 +
-+                player.connection.send(new ClientboundSetHealthPacket(player.getBukkitEntity().getScaledHealth(), this.foodLevel, this.saturationLevel));
++                player.connection.send(new net.minecraft.network.protocol.game.ClientboundSetHealthPacket(player.getBukkitEntity().getScaledHealth(), this.foodLevel, this.saturationLevel));
 +                // CraftBukkit end
              }
          }
  
-@@ -46,23 +76,25 @@
- 
-         if (flag && this.saturationLevel > 0.0F && player.isHurt() && this.foodLevel >= 20) {
-             ++this.tickTimer;
+         boolean _boolean = serverLevel.getGameRules().getBoolean(GameRules.RULE_NATURAL_REGENERATION);
+         if (_boolean && this.saturationLevel > 0.0F && player.isHurt() && this.foodLevel >= 20) {
+             this.tickTimer++;
 -            if (this.tickTimer >= 10) {
 +            if (this.tickTimer >= this.saturatedRegenRate) { // CraftBukkit
-                 float f = Math.min(this.saturationLevel, 6.0F);
- 
--                player.heal(f / 6.0F);
--                this.addExhaustion(f);
-+                player.heal(f / 6.0F, org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.SATIATED, true); // CraftBukkit - added RegainReason // Paper - This is fast regen
-+                // this.addExhaustion(f); CraftBukkit - EntityExhaustionEvent
-+                player.causeFoodExhaustion(f, org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.REGEN); // CraftBukkit - EntityExhaustionEvent
+                 float min = Math.min(this.saturationLevel, 6.0F);
+-                player.heal(min / 6.0F);
+-                this.addExhaustion(min);
++                player.heal(min / 6.0F, org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.SATIATED, true); // CraftBukkit - added RegainReason // Paper - This is fast regen
++                // this.addExhaustion(min); CraftBukkit - EntityExhaustionEvent
++                player.causeFoodExhaustion(min, org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.REGEN); // CraftBukkit - EntityExhaustionEvent
                  this.tickTimer = 0;
              }
-         } else if (flag && this.foodLevel >= 18 && player.isHurt()) {
-             ++this.tickTimer;
+         } else if (_boolean && this.foodLevel >= 18 && player.isHurt()) {
+             this.tickTimer++;
 -            if (this.tickTimer >= 80) {
 -                player.heal(1.0F);
 -                this.addExhaustion(6.0F);
@@ -93,9 +72,9 @@
                  this.tickTimer = 0;
              }
          } else if (this.foodLevel <= 0) {
-             ++this.tickTimer;
+             this.tickTimer++;
 -            if (this.tickTimer >= 80) {
 +            if (this.tickTimer >= this.starvationRate) { // CraftBukkit - add regen rate manipulation
-                 if (player.getHealth() > 10.0F || enumdifficulty == Difficulty.HARD || player.getHealth() > 1.0F && enumdifficulty == Difficulty.NORMAL) {
-                     player.hurtServer(worldserver, player.damageSources().starve(), 1.0F);
+                 if (player.getHealth() > 10.0F || difficulty == Difficulty.HARD || player.getHealth() > 1.0F && difficulty == Difficulty.NORMAL) {
+                     player.hurtServer(serverLevel, player.damageSources().starve(), 1.0F);
                  }
diff --git a/paper-server/patches/sources/net/minecraft/world/food/FoodProperties.java.patch b/paper-server/patches/sources/net/minecraft/world/food/FoodProperties.java.patch
new file mode 100644
index 0000000000..d6ade19bb4
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/food/FoodProperties.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/food/FoodProperties.java
++++ b/net/minecraft/world/food/FoodProperties.java
+@@ -41,7 +_,7 @@
+         RandomSource random = entity.getRandom();
+         level.playSound(null, entity.getX(), entity.getY(), entity.getZ(), consumable.sound().value(), SoundSource.NEUTRAL, 1.0F, random.triangle(1.0F, 0.4F));
+         if (entity instanceof Player player) {
+-            player.getFoodData().eat(this);
++            player.getFoodData().eat(this, stack, (net.minecraft.server.level.ServerPlayer) player); // CraftBukkit
+             level.playSound(
+                 null, player.getX(), player.getY(), player.getZ(), SoundEvents.PLAYER_BURP, SoundSource.PLAYERS, 0.5F, Mth.randomBetween(random, 0.9F, 1.0F)
+             );
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/AbstractContainerMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/AbstractContainerMenu.java.patch
new file mode 100644
index 0000000000..e33c611feb
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/AbstractContainerMenu.java.patch
@@ -0,0 +1,280 @@
+--- a/net/minecraft/world/inventory/AbstractContainerMenu.java
++++ b/net/minecraft/world/inventory/AbstractContainerMenu.java
+@@ -19,6 +_,8 @@
+ import net.minecraft.ReportedException;
+ import net.minecraft.core.NonNullList;
+ import net.minecraft.core.registries.BuiltInRegistries;
++import net.minecraft.network.chat.Component;
++import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket;
+ import net.minecraft.server.level.ServerPlayer;
+ import net.minecraft.util.Mth;
+ import net.minecraft.world.Container;
+@@ -63,6 +_,31 @@
+     @Nullable
+     private ContainerSynchronizer synchronizer;
+     private boolean suppressRemoteUpdates;
++    // CraftBukkit start
++    public boolean checkReachable = true;
++    public abstract org.bukkit.inventory.InventoryView getBukkitView();
++    public void transferTo(AbstractContainerMenu other, org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        org.bukkit.inventory.InventoryView source = this.getBukkitView(), destination = other.getBukkitView();
++        ((org.bukkit.craftbukkit.inventory.CraftInventory) source.getTopInventory()).getInventory().onClose(player);
++        ((org.bukkit.craftbukkit.inventory.CraftInventory) source.getBottomInventory()).getInventory().onClose(player);
++        ((org.bukkit.craftbukkit.inventory.CraftInventory) destination.getTopInventory()).getInventory().onOpen(player);
++        ((org.bukkit.craftbukkit.inventory.CraftInventory) destination.getBottomInventory()).getInventory().onOpen(player);
++    }
++    private Component title;
++    public final Component getTitle() {
++        // Paper start - return chat component with empty text instead of throwing error
++        // Preconditions.checkState(this.title != null, "Title not set");
++        if (this.title == null){
++            return Component.literal("");
++        }
++        // Paper end - return chat component with empty text instead of throwing error
++        return this.title;
++    }
++    public final void setTitle(Component title) {
++        com.google.common.base.Preconditions.checkState(this.title == null, "Title already set");
++        this.title = title;
++    }
++    // CraftBukkit end
+ 
+     protected AbstractContainerMenu(@Nullable MenuType<?> menuType, int containerId) {
+         this.menuType = menuType;
+@@ -168,8 +_,18 @@
+ 
+         if (this.synchronizer != null) {
+             this.synchronizer.sendInitialData(this, this.remoteSlots, this.remoteCarried, this.remoteDataSlots.toIntArray());
+-        }
+-    }
++            this.synchronizer.sendOffHandSlotChange(); // Paper - Sync offhand slot in menus; update player's offhand since the offhand slot is not added to the slots for menus but can be changed by swapping from a menu slot
++        }
++    }
++
++    // CraftBukkit start
++    public void broadcastCarriedItem() {
++        this.remoteCarried = this.getCarried().copy();
++        if (this.synchronizer != null) {
++            this.synchronizer.sendCarriedChange(this, this.remoteCarried);
++        }
++    }
++    // CraftBukkit end
+ 
+     public void removeSlotListener(ContainerListener listener) {
+         this.containerListeners.remove(listener);
+@@ -235,7 +_,7 @@
+             this.lastSlots.set(slotIndex, itemStack1);
+ 
+             for (ContainerListener containerListener : this.containerListeners) {
+-                containerListener.slotChanged(this, slotIndex, itemStack1);
++                containerListener.slotChanged(this, slotIndex, itemStack, itemStack1); // Paper - Add PlayerInventorySlotChangeEvent
+             }
+         }
+     }
+@@ -343,6 +_,7 @@
+                     this.resetQuickCraft();
+                 }
+             } else if (this.quickcraftStatus == 1) {
++                if (slotId < 0) return; // Paper - Add slot sanity checks to container clicks
+                 Slot slot = this.slots.get(slotId);
+                 ItemStack carried = this.getCarried();
+                 if (canItemQuickReplace(slot, carried, true)
+@@ -367,6 +_,7 @@
+                     }
+ 
+                     int count = this.getCarried().getCount();
++                    java.util.Map<Integer, ItemStack> draggedSlots = new java.util.HashMap<>(); // CraftBukkit - Store slots from drag in map (raw slot id -> new stack)
+ 
+                     for (Slot slot1 : this.quickcraftSlots) {
+                         ItemStack carried1 = this.getCarried();
+@@ -379,12 +_,48 @@
+                             int min = Math.min(itemStack.getMaxStackSize(), slot1.getMaxStackSize(itemStack));
+                             int min1 = Math.min(getQuickCraftPlaceCount(this.quickcraftSlots, this.quickcraftType, itemStack) + i2, min);
+                             count -= min1 - i2;
+-                            slot1.setByPlayer(itemStack.copyWithCount(min1));
+-                        }
+-                    }
+-
+-                    itemStack.setCount(count);
+-                    this.setCarried(itemStack);
++                            // slot1.setByPlayer(itemStack.copyWithCount(min1));
++                            draggedSlots.put(slot1.index, itemStack.copyWithCount(min1)); // CraftBukkit - Put in map instead of setting
++                        }
++                    }
++
++                    // CraftBukkit start - InventoryDragEvent
++                    org.bukkit.inventory.InventoryView view = this.getBukkitView();
++                    org.bukkit.inventory.ItemStack newcursor = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack);
++                    newcursor.setAmount(count);
++                    java.util.Map<Integer, org.bukkit.inventory.ItemStack> eventmap = new java.util.HashMap<>();
++                    for (java.util.Map.Entry<Integer, ItemStack> ditem : draggedSlots.entrySet()) {
++                        eventmap.put(ditem.getKey(), org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitCopy(ditem.getValue()));
++                    }
++
++                    // It's essential that we set the cursor to the new value here to prevent item duplication if a plugin closes the inventory.
++                    ItemStack oldCursor = this.getCarried();
++                    this.setCarried(org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(newcursor));
++
++                    org.bukkit.event.inventory.InventoryDragEvent event = new org.bukkit.event.inventory.InventoryDragEvent(view, (newcursor.getType() != org.bukkit.Material.AIR ? newcursor : null), org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitCopy(oldCursor), this.quickcraftType == 1, eventmap);
++                    player.level().getCraftServer().getPluginManager().callEvent(event);
++
++                    // Whether or not a change was made to the inventory that requires an update.
++                    boolean needsUpdate = event.getResult() != org.bukkit.event.Event.Result.DEFAULT;
++
++                    if (event.getResult() != org.bukkit.event.Event.Result.DENY) {
++                        for (java.util.Map.Entry<Integer, ItemStack> dslot : draggedSlots.entrySet()) {
++                            view.setItem(dslot.getKey(), org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitCopy(dslot.getValue()));
++                        }
++                        // The only time the carried item will be set to null is if the inventory is closed by the server.
++                        // If the inventory is closed by the server, then the cursor items are dropped.  This is why we change the cursor early.
++                        if (this.getCarried() != null) {
++                            this.setCarried(org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getCursor()));
++                            needsUpdate = true;
++                        }
++                    } else {
++                        this.setCarried(oldCursor);
++                    }
++
++                    if (needsUpdate && player instanceof ServerPlayer) {
++                        this.sendAllDataToRemote();
++                    }
++                    // CraftBukkit end
+                 }
+ 
+                 this.resetQuickCraft();
+@@ -398,8 +_,11 @@
+             if (slotId == -999) {
+                 if (!this.getCarried().isEmpty()) {
+                     if (clickAction == ClickAction.PRIMARY) {
+-                        player.drop(this.getCarried(), true);
++                        // CraftBukkit start
++                        ItemStack carried = this.getCarried();
+                         this.setCarried(ItemStack.EMPTY);
++                        player.drop(carried, true);
++                        // CraftBukkit end
+                     } else {
+                         player.drop(this.getCarried().split(1), true);
+                     }
+@@ -461,8 +_,18 @@
+                 }
+ 
+                 slot.setChanged();
++                // CraftBukkit start - Make sure the client has the right slot contents
++                if (player instanceof ServerPlayer serverPlayer && slot.getMaxStackSize() != 64) {
++                    serverPlayer.connection.send(new ClientboundContainerSetSlotPacket(this.containerId, this.incrementStateId(), slot.index, slot.getItem()));
++                    // Updating a crafting inventory makes the client reset the result slot, have to send it again
++                    if (this.getBukkitView().getType() == org.bukkit.event.inventory.InventoryType.WORKBENCH || this.getBukkitView().getType() == org.bukkit.event.inventory.InventoryType.CRAFTING) {
++                        serverPlayer.connection.send(new ClientboundContainerSetSlotPacket(this.containerId, this.incrementStateId(), 0, this.getSlot(0).getItem()));
++                    }
++                }
++                // CraftBukkit end
+             }
+         } else if (clickType == ClickType.SWAP && (button >= 0 && button < 9 || button == 40)) {
++            if (slotId < 0) return; // Paper - Add slot sanity checks to container clicks
+             ItemStack item = inventory.getItem(button);
+             Slot slot = this.slots.get(slotId);
+             ItemStack carried = slot.getItem();
+@@ -582,8 +_,9 @@
+         if (player instanceof ServerPlayer) {
+             ItemStack carried = this.getCarried();
+             if (!carried.isEmpty()) {
++                this.setCarried(ItemStack.EMPTY); // CraftBukkit - SPIGOT-4556 - from below
+                 dropOrPlaceInInventory(player, carried);
+-                this.setCarried(ItemStack.EMPTY);
++                // this.setCarried(ItemStack.EMPTY); // CraftBukkit - moved up
+             }
+         }
+     }
+@@ -629,6 +_,14 @@
+     public abstract boolean stillValid(Player player);
+ 
+     protected boolean moveItemStackTo(ItemStack stack, int startIndex, int endIndex, boolean reverseDirection) {
++        // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent
++        return this.moveItemStackTo(stack, startIndex, endIndex, reverseDirection, false);
++    }
++    protected boolean moveItemStackTo(ItemStack stack, int startIndex, int endIndex, boolean reverseDirection, boolean isCheck) {
++        if (isCheck) {
++            stack = stack.copy();
++        }
++        // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent
+         boolean flag = false;
+         int i = startIndex;
+         if (reverseDirection) {
+@@ -639,18 +_,27 @@
+             while (!stack.isEmpty() && (reverseDirection ? i >= startIndex : i < endIndex)) {
+                 Slot slot = this.slots.get(i);
+                 ItemStack item = slot.getItem();
++                // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent; clone if only a check
++                if (isCheck) {
++                    item = item.copy();
++                }
++                // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent
+                 if (!item.isEmpty() && ItemStack.isSameItemSameComponents(stack, item)) {
+                     int i1 = item.getCount() + stack.getCount();
+                     int maxStackSize = slot.getMaxStackSize(item);
+                     if (i1 <= maxStackSize) {
+                         stack.setCount(0);
+                         item.setCount(i1);
++                        if (!isCheck) { // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
+                         slot.setChanged();
++                        } // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
+                         flag = true;
+                     } else if (item.getCount() < maxStackSize) {
+                         stack.shrink(maxStackSize - item.getCount());
+                         item.setCount(maxStackSize);
++                         if (!isCheck) { // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
+                         slot.setChanged();
++                        } // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
+                         flag = true;
+                     }
+                 }
+@@ -673,10 +_,21 @@
+             while (reverseDirection ? i >= startIndex : i < endIndex) {
+                 Slot slotx = this.slots.get(i);
+                 ItemStack itemx = slotx.getItem();
++                // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent
++                if (isCheck) {
++                    itemx = itemx.copy();
++                }
++                // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent
+                 if (itemx.isEmpty() && slotx.mayPlace(stack)) {
+                     int i1 = slotx.getMaxStackSize(stack);
++                    // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent
++                    if (isCheck) {
++                        stack.shrink(Math.min(stack.getCount(), i1));
++                    } else {
++                    // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent
+                     slotx.setByPlayer(stack.split(Math.min(stack.getCount(), i1)));
+                     slotx.setChanged();
++                    } // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
+                     flag = true;
+                     break;
+                 }
+@@ -760,6 +_,11 @@
+     }
+ 
+     public ItemStack getCarried() {
++        // CraftBukkit start
++        if (this.carried.isEmpty()) {
++            this.setCarried(ItemStack.EMPTY);
++        }
++        // CraftBukkit end
+         return this.carried;
+     }
+ 
+@@ -808,4 +_,15 @@
+         this.stateId = this.stateId + 1 & 32767;
+         return this.stateId;
+     }
++
++    // Paper start - Add missing InventoryHolders
++    // The reason this is a supplier, is that the createHolder method uses the bukkit InventoryView#getTopInventory to get the inventory in question
++    // and that can't be obtained safely until the AbstractContainerMenu has been fully constructed. Using a supplier lazily
++    // initializes the InventoryHolder safely.
++    protected final Supplier<org.bukkit.inventory.BlockInventoryHolder> createBlockHolder(final ContainerLevelAccess context) {
++        //noinspection ConstantValue
++        com.google.common.base.Preconditions.checkArgument(context != null, "context was null");
++        return () -> context.createBlockHolder(this);
++    }
++    // Paper end - Add missing InventoryHolders
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/AbstractCraftingMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/AbstractCraftingMenu.java.patch
new file mode 100644
index 0000000000..0daa1431a6
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/AbstractCraftingMenu.java.patch
@@ -0,0 +1,23 @@
+--- a/net/minecraft/world/inventory/AbstractCraftingMenu.java
++++ b/net/minecraft/world/inventory/AbstractCraftingMenu.java
+@@ -12,14 +_,17 @@
+ public abstract class AbstractCraftingMenu extends RecipeBookMenu {
+     private final int width;
+     private final int height;
+-    public final CraftingContainer craftSlots;
++    public final TransientCraftingContainer craftSlots; // CraftBukkit
+     public final ResultContainer resultSlots = new ResultContainer();
+ 
+-    public AbstractCraftingMenu(MenuType<?> menuType, int containerId, int width, int height) {
++    public AbstractCraftingMenu(MenuType<?> menuType, int containerId, int width, int height, Inventory playerInventory) { // CraftBukkit
+         super(menuType, containerId);
+         this.width = width;
+         this.height = height;
+-        this.craftSlots = new TransientCraftingContainer(this, width, height);
++        // CraftBukkit start
++        this.craftSlots = new TransientCraftingContainer(this, width, height, playerInventory.player); // CraftBukkit - pass player
++        this.craftSlots.resultInventory = this.resultSlots; // CraftBukkit - let InventoryCrafting know about its result slot
++        // CraftBukkit end
+     }
+ 
+     protected Slot addResultSlot(Player player, int x, int y) {
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/AbstractFurnaceMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/AbstractFurnaceMenu.java.patch
new file mode 100644
index 0000000000..4d7c9ff8ff
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/AbstractFurnaceMenu.java.patch
@@ -0,0 +1,40 @@
+--- a/net/minecraft/world/inventory/AbstractFurnaceMenu.java
++++ b/net/minecraft/world/inventory/AbstractFurnaceMenu.java
+@@ -34,6 +_,21 @@
+     private final RecipeType<? extends AbstractCookingRecipe> recipeType;
+     private final RecipePropertySet acceptedInputs;
+     private final RecipeBookType recipeBookType;
++    // CraftBukkit start
++    private org.bukkit.craftbukkit.inventory.view.CraftFurnaceView bukkitEntity = null;
++    private Inventory player;
++
++    @Override
++    public org.bukkit.craftbukkit.inventory.view.CraftFurnaceView getBukkitView() {
++        if (this.bukkitEntity != null) {
++            return this.bukkitEntity;
++        }
++
++        org.bukkit.craftbukkit.inventory.CraftInventoryFurnace inventory = new org.bukkit.craftbukkit.inventory.CraftInventoryFurnace((net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity) this.container);
++        this.bukkitEntity = new org.bukkit.craftbukkit.inventory.view.CraftFurnaceView(this.player.player.getBukkitEntity(), inventory, this);
++        return this.bukkitEntity;
++    }
++    // CraftBukkit end
+ 
+     protected AbstractFurnaceMenu(
+         MenuType<?> menuType,
+@@ -68,6 +_,7 @@
+         this.addSlot(new Slot(container, 0, 56, 17));
+         this.addSlot(new FurnaceFuelSlot(this, container, 1, 56, 53));
+         this.addSlot(new FurnaceResultSlot(inventory.player, container, 2, 116, 35));
++        this.player = inventory; // CraftBukkit - save player
+         this.addStandardInventorySlots(inventory, 8, 84);
+         this.addDataSlots(data);
+     }
+@@ -85,6 +_,7 @@
+ 
+     @Override
+     public boolean stillValid(Player player) {
++        if (!this.checkReachable) return true; // CraftBukkit
+         return this.container.stillValid(player);
+     }
+ 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/AnvilMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/AnvilMenu.java.patch
similarity index 63%
rename from paper-server/patches/unapplied/net/minecraft/world/inventory/AnvilMenu.java.patch
rename to paper-server/patches/sources/net/minecraft/world/inventory/AnvilMenu.java.patch
index 1c6f48d25b..a1143aed14 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/AnvilMenu.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/AnvilMenu.java.patch
@@ -1,68 +1,55 @@
 --- a/net/minecraft/world/inventory/AnvilMenu.java
 +++ b/net/minecraft/world/inventory/AnvilMenu.java
-@@ -21,6 +21,10 @@
- import net.minecraft.world.level.block.state.BlockState;
- import org.slf4j.Logger;
- 
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.inventory.view.CraftAnvilView;
-+// CraftBukkit end
-+
- public class AnvilMenu extends ItemCombinerMenu {
- 
-     public static final int INPUT_SLOT = 0;
-@@ -45,6 +49,12 @@
+@@ -43,6 +_,12 @@
      private static final int ADDITIONAL_SLOT_X_PLACEMENT = 76;
      private static final int RESULT_SLOT_X_PLACEMENT = 134;
      private static final int SLOT_Y_PLACEMENT = 47;
 +    // CraftBukkit start
 +    public static final int DEFAULT_DENIED_COST = -1;
 +    public int maximumRepairCost = 40;
-+    private CraftAnvilView bukkitEntity;
++    private org.bukkit.craftbukkit.inventory.view.CraftAnvilView bukkitEntity;
 +    // CraftBukkit end
 +    public boolean bypassEnchantmentLevelRestriction = false; // Paper - bypass anvil level restrictions
  
-     public AnvilMenu(int syncId, Inventory inventory) {
-         this(syncId, inventory, ContainerLevelAccess.NULL);
-@@ -72,7 +82,7 @@
+     public AnvilMenu(int containerId, Inventory playerInventory) {
+         this(containerId, playerInventory, ContainerLevelAccess.NULL);
+@@ -68,7 +_,7 @@
  
      @Override
-     protected boolean mayPickup(Player player, boolean present) {
+     protected boolean mayPickup(Player player, boolean hasStack) {
 -        return (player.hasInfiniteMaterials() || player.experienceLevel >= this.cost.get()) && this.cost.get() > 0;
-+        return (player.hasInfiniteMaterials() || player.experienceLevel >= this.cost.get()) && this.cost.get() > AnvilMenu.DEFAULT_DENIED_COST && present; // CraftBukkit - allow cost 0 like a free item
++        return (player.hasInfiniteMaterials() || player.experienceLevel >= this.cost.get()) && this.cost.get() > AnvilMenu.DEFAULT_DENIED_COST && hasStack; // CraftBukkit - allow cost 0 like a free item
      }
  
      @Override
-@@ -94,7 +104,7 @@
+@@ -89,12 +_,22 @@
              this.inputSlots.setItem(1, ItemStack.EMPTY);
          }
  
 -        this.cost.set(0);
 +        this.cost.set(AnvilMenu.DEFAULT_DENIED_COST); // CraftBukkit - use a variable for set a cost for denied item
          this.inputSlots.setItem(0, ItemStack.EMPTY);
-         this.access.execute((world, blockposition) -> {
-             BlockState iblockdata = world.getBlockState(blockposition);
-@@ -102,6 +112,16 @@
-             if (!player.hasInfiniteMaterials() && iblockdata.is(BlockTags.ANVIL) && player.getRandom().nextFloat() < 0.12F) {
-                 BlockState iblockdata1 = AnvilBlock.damage(iblockdata);
- 
+         this.access.execute((level, blockPos) -> {
+             BlockState blockState = level.getBlockState(blockPos);
+             if (!player.hasInfiniteMaterials() && blockState.is(BlockTags.ANVIL) && player.getRandom().nextFloat() < 0.12F) {
+                 BlockState blockState1 = AnvilBlock.damage(blockState);
 +                // Paper start - AnvilDamageEvent
-+                com.destroystokyo.paper.event.block.AnvilDamagedEvent event = new com.destroystokyo.paper.event.block.AnvilDamagedEvent(getBukkitView(), iblockdata1 != null ? org.bukkit.craftbukkit.block.data.CraftBlockData.fromData(iblockdata1) : null);
++                com.destroystokyo.paper.event.block.AnvilDamagedEvent event = new com.destroystokyo.paper.event.block.AnvilDamagedEvent(getBukkitView(), blockState1 != null ? org.bukkit.craftbukkit.block.data.CraftBlockData.fromData(blockState1) : null);
 +                if (!event.callEvent()) {
 +                    return;
 +                } else if (event.getDamageState() == com.destroystokyo.paper.event.block.AnvilDamagedEvent.DamageState.BROKEN) {
-+                    iblockdata1 = null;
++                    blockState1 = null;
 +                } else {
-+                    iblockdata1 = ((org.bukkit.craftbukkit.block.data.CraftBlockData) event.getDamageState().getMaterial().createBlockData()).getState().setValue(AnvilBlock.FACING, iblockdata.getValue(AnvilBlock.FACING));
++                    blockState1 = ((org.bukkit.craftbukkit.block.data.CraftBlockData) event.getDamageState().getMaterial().createBlockData()).getState().setValue(AnvilBlock.FACING, blockState.getValue(AnvilBlock.FACING));
 +                }
 +                // Paper end - AnvilDamageEvent
-                 if (iblockdata1 == null) {
-                     world.removeBlock(blockposition, false);
-                     world.levelEvent(1029, blockposition, 0);
-@@ -143,8 +163,8 @@
-                 if (itemstack1.isDamageableItem() && itemstack.isValidRepairItem(itemstack2)) {
-                     k = Math.min(itemstack1.getDamageValue(), itemstack1.getMaxDamage() / 4);
-                     if (k <= 0) {
+                 if (blockState1 == null) {
+                     level.removeBlock(blockPos, false);
+                     level.levelEvent(1029, blockPos, 0);
+@@ -128,8 +_,8 @@
+                 if (itemStack.isDamageableItem() && item.isValidRepairItem(item1)) {
+                     int min = Math.min(itemStack.getDamageValue(), itemStack.getMaxDamage() / 4);
+                     if (min <= 0) {
 -                        this.resultSlots.setItem(0, ItemStack.EMPTY);
 -                        this.cost.set(0);
 +                        org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareAnvilEvent(this.getBukkitView(), ItemStack.EMPTY); // CraftBukkit
@@ -70,10 +57,10 @@
                          return;
                      }
  
-@@ -158,8 +178,8 @@
-                     this.repairItemCountCost = i1;
+@@ -144,8 +_,8 @@
+                     this.repairItemCountCost = i2;
                  } else {
-                     if (!flag && (!itemstack1.is(itemstack2.getItem()) || !itemstack1.isDamageableItem())) {
+                     if (!hasStoredEnchantments && (!itemStack.is(item1.getItem()) || !itemStack.isDamageableItem())) {
 -                        this.resultSlots.setItem(0, ItemStack.EMPTY);
 -                        this.cost.set(0);
 +                        org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareAnvilEvent(this.getBukkitView(), ItemStack.EMPTY); // CraftBukkit
@@ -81,19 +68,19 @@
                          return;
                      }
  
-@@ -214,7 +234,7 @@
-                             flag2 = true;
-                         } else {
+@@ -191,7 +_,7 @@
                              flag1 = true;
--                            if (i2 > enchantment.getMaxLevel()) {
-+                            if (i2 > enchantment.getMaxLevel() && !this.bypassEnchantmentLevelRestriction) { // Paper - bypass anvil level restrictions
-                                 i2 = enchantment.getMaxLevel();
+                         } else {
+                             flag = true;
+-                            if (intValue > enchantment.getMaxLevel()) {
++                            if (intValue > enchantment.getMaxLevel() && !this.bypassEnchantmentLevelRestriction) { // Paper - bypass anvil level restrictions
+                                 intValue = enchantment.getMaxLevel();
                              }
  
-@@ -233,8 +253,8 @@
+@@ -209,8 +_,8 @@
                      }
  
-                     if (flag2 && !flag1) {
+                     if (flag1 && !flag) {
 -                        this.resultSlots.setItem(0, ItemStack.EMPTY);
 -                        this.cost.set(0);
 +                        org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareAnvilEvent(this.getBukkitView(), ItemStack.EMPTY); // CraftBukkit
@@ -101,14 +88,16 @@
                          return;
                      }
                  }
-@@ -260,14 +280,14 @@
+@@ -235,14 +_,16 @@
              }
  
-             if (b0 == i && b0 > 0) {
+             if (i1 == i && i1 > 0) {
 -                if (this.cost.get() >= 40) {
 -                    this.cost.set(39);
-+                if (this.cost.get() >= this.maximumRepairCost) { // CraftBukkit
-+                    this.cost.set(this.maximumRepairCost - 1); // CraftBukkit
++                // CraftBukkit start
++                if (this.cost.get() >= this.maximumRepairCost) {
++                    this.cost.set(this.maximumRepairCost - 1);
++                // CraftBukkit end
                  }
  
                  this.onlyRenaming = true;
@@ -116,15 +105,15 @@
  
 -            if (this.cost.get() >= 40 && !this.player.getAbilities().instabuild) {
 +            if (this.cost.get() >= this.maximumRepairCost && !this.player.getAbilities().instabuild) { // CraftBukkit
-                 itemstack1 = ItemStack.EMPTY;
+                 itemStack = ItemStack.EMPTY;
              }
  
-@@ -285,12 +305,13 @@
-                 EnchantmentHelper.setEnchantments(itemstack1, itemenchantments_a.toImmutable());
+@@ -260,12 +_,13 @@
+                 EnchantmentHelper.setEnchantments(itemStack, mutable.toImmutable());
              }
  
--            this.resultSlots.setItem(0, itemstack1);
-+            org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareAnvilEvent(this.getBukkitView(), itemstack1); // CraftBukkit
+-            this.resultSlots.setItem(0, itemStack);
++            org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareAnvilEvent(this.getBukkitView(), itemStack); // CraftBukkit
              this.broadcastChanges();
          } else {
 -            this.resultSlots.setItem(0, ItemStack.EMPTY);
@@ -135,8 +124,8 @@
 +        this.sendAllDataToRemote(); // CraftBukkit - SPIGOT-6686, SPIGOT-7931: Always send completed inventory to stay in sync with client
      }
  
-     public static int calculateIncreasedRepairCost(int cost) {
-@@ -313,6 +334,7 @@
+     public static int calculateIncreasedRepairCost(int oldRepairCost) {
+@@ -286,6 +_,7 @@
              }
  
              this.createResult();
@@ -144,21 +133,21 @@
              return true;
          } else {
              return false;
-@@ -329,4 +351,19 @@
+@@ -301,4 +_,19 @@
      public int getCost() {
          return this.cost.get();
      }
 +
 +    // CraftBukkit start
 +    @Override
-+    public CraftAnvilView getBukkitView() {
++    public org.bukkit.craftbukkit.inventory.view.CraftAnvilView getBukkitView() {
 +        if (this.bukkitEntity != null) {
 +            return this.bukkitEntity;
 +        }
 +
 +        org.bukkit.craftbukkit.inventory.CraftInventoryAnvil inventory = new org.bukkit.craftbukkit.inventory.CraftInventoryAnvil(
 +                this.access.getLocation(), this.inputSlots, this.resultSlots);
-+        this.bukkitEntity = new CraftAnvilView(this.player.getBukkitEntity(), inventory, this);
++        this.bukkitEntity = new org.bukkit.craftbukkit.inventory.view.CraftAnvilView(this.player.getBukkitEntity(), inventory, this);
 +        this.bukkitEntity.updateFromLegacy(inventory);
 +        return this.bukkitEntity;
 +    }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/BeaconMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/BeaconMenu.java.patch
similarity index 50%
rename from paper-server/patches/unapplied/net/minecraft/world/inventory/BeaconMenu.java.patch
rename to paper-server/patches/sources/net/minecraft/world/inventory/BeaconMenu.java.patch
index 587f354b49..e0549ad657 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/BeaconMenu.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/BeaconMenu.java.patch
@@ -1,54 +1,61 @@
 --- a/net/minecraft/world/inventory/BeaconMenu.java
 +++ b/net/minecraft/world/inventory/BeaconMenu.java
-@@ -8,10 +8,13 @@
- import net.minecraft.world.Container;
- import net.minecraft.world.SimpleContainer;
- import net.minecraft.world.effect.MobEffect;
-+import net.minecraft.world.entity.player.Inventory;
- import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.block.Blocks;
-+import org.bukkit.craftbukkit.inventory.view.CraftBeaconView;
-+// CraftBukkit end
- 
- public class BeaconMenu extends AbstractContainerMenu {
- 
-@@ -27,6 +30,10 @@
-     private final BeaconMenu.PaymentSlot paymentSlot;
+@@ -22,20 +_,14 @@
+     private static final int USE_ROW_SLOT_START = 28;
+     private static final int USE_ROW_SLOT_END = 37;
+     private static final int NO_EFFECT = 0;
+-    private final Container beacon = new SimpleContainer(1) {
+-        @Override
+-        public boolean canPlaceItem(int slot, ItemStack stack) {
+-            return stack.is(ItemTags.BEACON_PAYMENT_ITEMS);
+-        }
+-
+-        @Override
+-        public int getMaxStackSize() {
+-            return 1;
+-        }
+-    };
+-    private final BeaconMenu.PaymentSlot paymentSlot;
++    private final Container beacon; // Paper - Add missing InventoryHolders Move down
++    private final PaymentSlot paymentSlot;
      private final ContainerLevelAccess access;
      private final ContainerData beaconData;
 +    // CraftBukkit start
-+    private CraftBeaconView bukkitEntity = null;
-+    private Inventory player;
++    private org.bukkit.craftbukkit.inventory.view.CraftBeaconView bukkitEntity = null;
++    private net.minecraft.world.entity.player.Inventory player;
 +    // CraftBukkit end
  
-     public BeaconMenu(int syncId, Container inventory) {
-         this(syncId, inventory, new SimpleContainerData(3), ContainerLevelAccess.NULL);
-@@ -34,7 +41,8 @@
+     public BeaconMenu(int containerId, Container container) {
+         this(containerId, container, new SimpleContainerData(3), ContainerLevelAccess.NULL);
+@@ -43,6 +_,27 @@
  
-     public BeaconMenu(int syncId, Container inventory, ContainerData propertyDelegate, ContainerLevelAccess context) {
-         super(MenuType.BEACON, syncId);
--        this.beacon = new SimpleContainer(this, 1) {
-+        this.player = (Inventory) inventory; // CraftBukkit - TODO: check this
-+        this.beacon = new SimpleContainer(this.createBlockHolder(context), 1) { // CraftBukkit - decompile error // Paper - Add missing InventoryHolders
-             @Override
-             public boolean canPlaceItem(int slot, ItemStack stack) {
-                 return stack.is(ItemTags.BEACON_PAYMENT_ITEMS);
-@@ -44,6 +52,12 @@
-             public int getMaxStackSize() {
-                 return 1;
-             }
+     public BeaconMenu(int containerId, Container container, ContainerData beaconData, ContainerLevelAccess access) {
+         super(MenuType.BEACON, containerId);
++        this.player = (net.minecraft.world.entity.player.Inventory) container; // CraftBukkit - TODO: check this
++        // Paper - Add missing InventoryHolders
++        this.beacon = new SimpleContainer(this.createBlockHolder(access), 1) {
++            @Override
++            public boolean canPlaceItem(int slot, ItemStack stack) {
++                return stack.is(ItemTags.BEACON_PAYMENT_ITEMS);
++            }
++
++            @Override
++            public int getMaxStackSize() {
++                return 1;
++            }
++
 +            // Paper start - Fix inventories returning null Locations
 +            @Override
 +            public org.bukkit.Location getLocation() {
-+                return context.getLocation();
++                return access.getLocation();
 +            }
 +            // Paper end - Fix inventories returning null Locations
-         };
-         checkContainerDataCount(propertyDelegate, 3);
-         this.beaconData = propertyDelegate;
-@@ -69,6 +83,7 @@
++        };
++        // Paper end
+         checkContainerDataCount(beaconData, 3);
+         this.beaconData = beaconData;
+         this.access = access;
+@@ -65,6 +_,7 @@
  
      @Override
      public boolean stillValid(Player player) {
@@ -56,27 +63,27 @@
          return stillValid(this.access, player, Blocks.BEACON);
      }
  
-@@ -148,12 +163,30 @@
-         return BeaconMenu.decodeEffect(this.beaconData.get(2));
+@@ -141,13 +_,30 @@
+     public Holder<MobEffect> getSecondaryEffect() {
+         return decodeEffect(this.beaconData.get(2));
      }
- 
 +    // Paper start - Add PlayerChangeBeaconEffectEvent
 +    private static @Nullable org.bukkit.potion.PotionEffectType convert(Optional<Holder<MobEffect>> optionalEffect) {
 +        return optionalEffect.map(org.bukkit.craftbukkit.potion.CraftPotionEffectType::minecraftHolderToBukkit).orElse(null);
 +    }
 +    // Paper end - Add PlayerChangeBeaconEffectEvent
-+
-     public void updateEffects(Optional<Holder<MobEffect>> primary, Optional<Holder<MobEffect>> secondary) {
+ 
+     public void updateEffects(Optional<Holder<MobEffect>> primaryEffect, Optional<Holder<MobEffect>> secondaryEffect) {
 +        // Paper start - fix MC-174630 - validate secondary power
-+        if (secondary.isPresent() && secondary.get() != net.minecraft.world.effect.MobEffects.REGENERATION && (primary.isPresent() && secondary.get() != primary.get())) {
-+            secondary = Optional.empty();
++        if (secondaryEffect.isPresent() && secondaryEffect.get() != net.minecraft.world.effect.MobEffects.REGENERATION && (primaryEffect.isPresent() && secondaryEffect.get() != primaryEffect.get())) {
++            secondaryEffect = Optional.empty();
 +        }
 +        // Paper end
          if (this.paymentSlot.hasItem()) {
--            this.beaconData.set(1, BeaconMenu.encodeEffect((Holder) primary.orElse((Object) null)));
--            this.beaconData.set(2, BeaconMenu.encodeEffect((Holder) secondary.orElse((Object) null)));
+-            this.beaconData.set(1, encodeEffect(primaryEffect.orElse(null)));
+-            this.beaconData.set(2, encodeEffect(secondaryEffect.orElse(null)));
 +            // Paper start - Add PlayerChangeBeaconEffectEvent
-+            io.papermc.paper.event.player.PlayerChangeBeaconEffectEvent event = new io.papermc.paper.event.player.PlayerChangeBeaconEffectEvent((org.bukkit.entity.Player) this.player.player.getBukkitEntity(), convert(primary), convert(secondary), this.access.getLocation().getBlock());
++            io.papermc.paper.event.player.PlayerChangeBeaconEffectEvent event = new io.papermc.paper.event.player.PlayerChangeBeaconEffectEvent((org.bukkit.entity.Player) this.player.player.getBukkitEntity(), convert(primaryEffect), convert(secondaryEffect), this.access.getLocation().getBlock());
 +            if (event.callEvent()) {
 +                // Paper end - Add PlayerChangeBeaconEffectEvent
 +                this.beaconData.set(1, BeaconMenu.encodeEffect(event.getPrimary() == null ? null : org.bukkit.craftbukkit.potion.CraftPotionEffectType.bukkitToMinecraftHolder(event.getPrimary())));// CraftBukkit - decompile error
@@ -87,22 +94,22 @@
              this.access.execute(Level::blockEntityChanged);
 +            } // Paper end - Add PlayerChangeBeaconEffectEvent
          }
- 
      }
-@@ -178,4 +211,17 @@
+ 
+@@ -170,4 +_,17 @@
              return 1;
          }
      }
 +
 +    // CraftBukkit start
 +    @Override
-+    public CraftBeaconView getBukkitView() {
++    public org.bukkit.craftbukkit.inventory.view.CraftBeaconView getBukkitView() {
 +        if (this.bukkitEntity != null) {
 +            return this.bukkitEntity;
 +        }
 +
 +        org.bukkit.craftbukkit.inventory.CraftInventoryBeacon inventory = new org.bukkit.craftbukkit.inventory.CraftInventoryBeacon(this.beacon);
-+        this.bukkitEntity = new CraftBeaconView(this.player.player.getBukkitEntity(), inventory, this);
++        this.bukkitEntity = new org.bukkit.craftbukkit.inventory.view.CraftBeaconView(this.player.player.getBukkitEntity(), inventory, this);
 +        return this.bukkitEntity;
 +    }
 +    // CraftBukkit end
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/BrewingStandMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/BrewingStandMenu.java.patch
new file mode 100644
index 0000000000..1c0ac6136a
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/BrewingStandMenu.java.patch
@@ -0,0 +1,114 @@
+--- a/net/minecraft/world/inventory/BrewingStandMenu.java
++++ b/net/minecraft/world/inventory/BrewingStandMenu.java
+@@ -33,29 +_,50 @@
+     private final Container brewingStand;
+     public final ContainerData brewingStandData;
+     private final Slot ingredientSlot;
++    // CraftBukkit start
++    private org.bukkit.craftbukkit.inventory.view.CraftBrewingStandView bukkitEntity = null;
++    private Inventory player;
++    // CraftBukkit end
+ 
+     public BrewingStandMenu(int containerId, Inventory playerInventory) {
+-        this(containerId, playerInventory, new SimpleContainer(5), new SimpleContainerData(2));
++        this(containerId, playerInventory, new SimpleContainer(5), new io.papermc.paper.inventory.BrewingSimpleContainerData()); // Paper - Add totalBrewTime
+     }
+ 
+     public BrewingStandMenu(int containerId, Inventory playerInventory, Container brewingStandContainer, ContainerData brewingStandData) {
+         super(MenuType.BREWING_STAND, containerId);
++        this.player = playerInventory; // CraftBukkit
+         checkContainerSize(brewingStandContainer, 5);
+-        checkContainerDataCount(brewingStandData, 2);
++        checkContainerDataCount(brewingStandData, 3); // Paper - Add recipeBrewTime
+         this.brewingStand = brewingStandContainer;
+         this.brewingStandData = brewingStandData;
+         PotionBrewing potionBrewing = playerInventory.player.level().potionBrewing();
+-        this.addSlot(new BrewingStandMenu.PotionSlot(brewingStandContainer, 0, 56, 51));
+-        this.addSlot(new BrewingStandMenu.PotionSlot(brewingStandContainer, 1, 79, 58));
+-        this.addSlot(new BrewingStandMenu.PotionSlot(brewingStandContainer, 2, 102, 51));
++        // Paper start - custom potion mixes
++        this.addSlot(new BrewingStandMenu.PotionSlot(brewingStandContainer, 0, 56, 51, potionBrewing));
++        this.addSlot(new BrewingStandMenu.PotionSlot(brewingStandContainer, 1, 79, 58, potionBrewing));
++        this.addSlot(new BrewingStandMenu.PotionSlot(brewingStandContainer, 2, 102, 51, potionBrewing));
++        // Paper end - custom potion mixes
+         this.ingredientSlot = this.addSlot(new BrewingStandMenu.IngredientsSlot(potionBrewing, brewingStandContainer, 3, 79, 17));
+         this.addSlot(new BrewingStandMenu.FuelSlot(brewingStandContainer, 4, 17, 17));
+-        this.addDataSlots(brewingStandData);
++        // Paper start - Add recipeBrewTime
++        this.addDataSlots(new SimpleContainerData(2) {
++            @Override
++            public int get(final int index) {
++                if (index == 0) return 400 * brewingStandData.get(index) / brewingStandData.get(2);
++                return brewingStandData.get(index);
++            }
++
++            @Override
++            public void set(final int index, final int value) {
++                brewingStandData.set(index, value);
++            }
++        });
++        // Paper end - Add recipeBrewTime
+         this.addStandardInventorySlots(playerInventory, 8, 84);
+     }
+ 
+     @Override
+     public boolean stillValid(Player player) {
++        if (!this.checkReachable) return true; // CraftBukkit
+         return this.brewingStand.stillValid(player);
+     }
+ 
+@@ -75,7 +_,7 @@
+                     if (!this.moveItemStackTo(item, 3, 4, false)) {
+                         return ItemStack.EMPTY;
+                     }
+-                } else if (BrewingStandMenu.PotionSlot.mayPlaceItem(itemStack)) {
++                } else if (BrewingStandMenu.PotionSlot.mayPlaceItem(itemStack, this.player.player.level().potionBrewing())) { // Paper - custom potion mixes
+                     if (!this.moveItemStackTo(item, 0, 3, false)) {
+                         return ItemStack.EMPTY;
+                     }
+@@ -157,13 +_,15 @@
+     }
+ 
+     static class PotionSlot extends Slot {
+-        public PotionSlot(Container container, int slot, int x, int y) {
++        private final PotionBrewing potionBrewing; // Paper - custom potion mixes
++        public PotionSlot(Container container, int slot, int x, int y, PotionBrewing potionBrewing) { // Paper - custom potion mixes
+             super(container, slot, x, y);
++            this.potionBrewing = potionBrewing; // Paper - custom potion mixes
+         }
+ 
+         @Override
+         public boolean mayPlace(ItemStack stack) {
+-            return mayPlaceItem(stack);
++            return mayPlaceItem(stack, this.potionBrewing); // Paper - custom potion mixes
+         }
+ 
+         @Override
+@@ -181,8 +_,8 @@
+             super.onTake(player, stack);
+         }
+ 
+-        public static boolean mayPlaceItem(ItemStack stack) {
+-            return stack.is(Items.POTION) || stack.is(Items.SPLASH_POTION) || stack.is(Items.LINGERING_POTION) || stack.is(Items.GLASS_BOTTLE);
++        public static boolean mayPlaceItem(ItemStack stack, PotionBrewing potionBrewing) { // Paper - custom potion mixes
++            return stack.is(Items.POTION) || stack.is(Items.SPLASH_POTION) || stack.is(Items.LINGERING_POTION) || stack.is(Items.GLASS_BOTTLE) || potionBrewing.isCustomInput(stack); // Paper - Custom Potion Mixes
+         }
+ 
+         @Override
+@@ -190,4 +_,16 @@
+             return BrewingStandMenu.EMPTY_SLOT_POTION;
+         }
+     }
++    // CraftBukkit start
++    @Override
++    public org.bukkit.craftbukkit.inventory.view.CraftBrewingStandView getBukkitView() {
++        if (this.bukkitEntity != null) {
++            return this.bukkitEntity;
++        }
++
++        org.bukkit.craftbukkit.inventory.CraftInventoryBrewer inventory = new org.bukkit.craftbukkit.inventory.CraftInventoryBrewer(this.brewingStand);
++        this.bukkitEntity = new org.bukkit.craftbukkit.inventory.view.CraftBrewingStandView(this.player.player.getBukkitEntity(), inventory, this);
++        return this.bukkitEntity;
++    }
++    // CraftBukkit end
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/CartographyTableMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/CartographyTableMenu.java.patch
new file mode 100644
index 0000000000..2e2aa4a423
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/CartographyTableMenu.java.patch
@@ -0,0 +1,103 @@
+--- a/net/minecraft/world/inventory/CartographyTableMenu.java
++++ b/net/minecraft/world/inventory/CartographyTableMenu.java
+@@ -15,6 +_,21 @@
+ import net.minecraft.world.level.saveddata.maps.MapItemSavedData;
+ 
+ public class CartographyTableMenu extends AbstractContainerMenu {
++    // CraftBukkit start
++    private org.bukkit.craftbukkit.inventory.CraftInventoryView bukkitEntity = null;
++    private org.bukkit.entity.Player player;
++
++    @Override
++    public org.bukkit.craftbukkit.inventory.CraftInventoryView getBukkitView() {
++        if (this.bukkitEntity != null) {
++            return this.bukkitEntity;
++        }
++
++        org.bukkit.craftbukkit.inventory.CraftInventoryCartography inventory = new org.bukkit.craftbukkit.inventory.CraftInventoryCartography(this.container, this.resultContainer);
++        this.bukkitEntity = new org.bukkit.craftbukkit.inventory.CraftInventoryView(this.player, inventory, this);
++        return this.bukkitEntity;
++    }
++    // CraftBukkit end
+     public static final int MAP_SLOT = 0;
+     public static final int ADDITIONAL_SLOT = 1;
+     public static final int RESULT_SLOT = 2;
+@@ -24,20 +_,8 @@
+     private static final int USE_ROW_SLOT_END = 39;
+     private final ContainerLevelAccess access;
+     long lastSoundTime;
+-    public final Container container = new SimpleContainer(2) {
+-        @Override
+-        public void setChanged() {
+-            CartographyTableMenu.this.slotsChanged(this);
+-            super.setChanged();
+-        }
+-    };
+-    private final ResultContainer resultContainer = new ResultContainer() {
+-        @Override
+-        public void setChanged() {
+-            CartographyTableMenu.this.slotsChanged(this);
+-            super.setChanged();
+-        }
+-    };
++    public final Container container; // Paper - Add missing InventoryHolders - move down
++    private final ResultContainer resultContainer; // Paper - Add missing InventoryHolders - move down
+ 
+     public CartographyTableMenu(int containerId, Inventory playerInventory) {
+         this(containerId, playerInventory, ContainerLevelAccess.NULL);
+@@ -45,6 +_,34 @@
+ 
+     public CartographyTableMenu(int containerId, Inventory playerInventory, final ContainerLevelAccess access) {
+         super(MenuType.CARTOGRAPHY_TABLE, containerId);
++        // Paper start - Add missing InventoryHolders - move down
++        this.container = new SimpleContainer(this.createBlockHolder(access), 2) { // Paper - Add missing InventoryHolders
++            @Override
++            public void setChanged() {
++                CartographyTableMenu.this.slotsChanged(this);
++                super.setChanged();
++            }
++            // CraftBukkit start
++            @Override
++            public org.bukkit.Location getLocation() {
++                return access.getLocation();
++            }
++            // CraftBukkit end
++        };
++        this.resultContainer = new ResultContainer(this.createBlockHolder(access)) { // Paper - Add missing InventoryHolders
++            @Override
++            public void setChanged() {
++                // CartographyTableMenu.this.slotsChanged(this); // Paper - Add CartographyItemEvent - do not recompute results if the result slot changes - allows to set the result slot via api
++                super.setChanged();
++            }
++            // CraftBukkit start
++            @Override
++            public org.bukkit.Location getLocation() {
++                return access.getLocation();
++            }
++            // CraftBukkit end
++        };
++        // Paper end - Add missing InventoryHolders - move down
+         this.access = access;
+         this.addSlot(new Slot(this.container, 0, 15, 15) {
+             @Override
+@@ -80,10 +_,12 @@
+             }
+         });
+         this.addStandardInventorySlots(playerInventory, 8, 84);
++        this.player = (org.bukkit.entity.Player) playerInventory.player.getBukkitEntity(); // CraftBukkit
+     }
+ 
+     @Override
+     public boolean stillValid(Player player) {
++        if (!this.checkReachable) return true; // CraftBukkit
+         return stillValid(this.access, player, Blocks.CARTOGRAPHY_TABLE);
+     }
+ 
+@@ -99,6 +_,7 @@
+         } else {
+             this.resultContainer.removeItemNoUpdate(2);
+         }
++        org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareResultEvent(this, RESULT_SLOT); // Paper - Add PrepareResultEvent
+     }
+ 
+     private void setupResultSlot(ItemStack map, ItemStack firstSlotStack, ItemStack resultOutput) {
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/ChestMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/ChestMenu.java.patch
new file mode 100644
index 0000000000..c86ead27cc
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/ChestMenu.java.patch
@@ -0,0 +1,50 @@
+--- a/net/minecraft/world/inventory/ChestMenu.java
++++ b/net/minecraft/world/inventory/ChestMenu.java
+@@ -9,6 +_,29 @@
+ public class ChestMenu extends AbstractContainerMenu {
+     private final Container container;
+     private final int containerRows;
++    // CraftBukkit start
++    private org.bukkit.craftbukkit.inventory.CraftInventoryView bukkitEntity = null;
++    private Inventory player;
++
++    @Override
++    public org.bukkit.craftbukkit.inventory.CraftInventoryView getBukkitView() {
++        if (this.bukkitEntity != null) {
++            return this.bukkitEntity;
++        }
++
++        org.bukkit.craftbukkit.inventory.CraftInventory inventory;
++        if (this.container instanceof Inventory) {
++            inventory = new org.bukkit.craftbukkit.inventory.CraftInventoryPlayer((Inventory) this.container);
++        } else if (this.container instanceof net.minecraft.world.CompoundContainer) {
++            inventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest((net.minecraft.world.CompoundContainer) this.container);
++        } else {
++            inventory = new org.bukkit.craftbukkit.inventory.CraftInventory(this.container);
++        }
++
++        this.bukkitEntity = new org.bukkit.craftbukkit.inventory.CraftInventoryView(this.player.player.getBukkitEntity(), inventory, this);
++        return this.bukkitEntity;
++    }
++    // CraftBukkit end
+ 
+     private ChestMenu(MenuType<?> type, int containerId, Inventory playerInventory, int rows) {
+         this(type, containerId, playerInventory, new SimpleContainer(9 * rows), rows);
+@@ -52,6 +_,9 @@
+         this.container = container;
+         this.containerRows = rows;
+         container.startOpen(playerInventory.player);
++        // CraftBukkit start - Save player
++        this.player = playerInventory;
++        // CraftBukkit end
+         int i = 18;
+         this.addChestGrid(container, 8, 18);
+         int i1 = 18 + this.containerRows * 18 + 13;
+@@ -68,6 +_,7 @@
+ 
+     @Override
+     public boolean stillValid(Player player) {
++        if (!this.checkReachable) return true; // CraftBukkit
+         return this.container.stillValid(player);
+     }
+ 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/ContainerLevelAccess.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/ContainerLevelAccess.java.patch
similarity index 83%
rename from paper-server/patches/unapplied/net/minecraft/world/inventory/ContainerLevelAccess.java.patch
rename to paper-server/patches/sources/net/minecraft/world/inventory/ContainerLevelAccess.java.patch
index 3202223f24..273a80aee1 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/ContainerLevelAccess.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/ContainerLevelAccess.java.patch
@@ -1,9 +1,46 @@
 --- a/net/minecraft/world/inventory/ContainerLevelAccess.java
 +++ b/net/minecraft/world/inventory/ContainerLevelAccess.java
-@@ -8,16 +8,66 @@
+@@ -12,6 +_,12 @@
+         public <T> Optional<T> evaluate(BiFunction<Level, BlockPos, T> levelPosConsumer) {
+             return Optional.empty();
+         }
++        // Paper start - fix menus with empty level accesses
++        @Override
++        public org.bukkit.Location getLocation() {
++            return null;
++        }
++        // Paper end - fix menus with empty level accesses
+     };
  
- public interface ContainerLevelAccess {
+     static ContainerLevelAccess create(final Level level, final BlockPos pos) {
+@@ -20,6 +_,23 @@
+             public <T> Optional<T> evaluate(BiFunction<Level, BlockPos, T> levelPosConsumer) {
+                 return Optional.of(levelPosConsumer.apply(level, pos));
+             }
++            // CraftBukkit start
++            @Override
++            public Level getWorld() {
++                return level;
++            }
++
++            @Override
++            public BlockPos getPosition() {
++                return pos;
++            }
++            // CraftBukkit end
++            // Paper start - Add missing InventoryHolders
++            @Override
++            public boolean isBlock() {
++                return true;
++            }
++            // Paper end - Add missing InventoryHolders
+         };
+     }
  
+@@ -35,4 +_,29 @@
+             return Optional.empty();
+         });
+     }
 +    // CraftBukkit start
 +    default Level getWorld() {
 +        throw new UnsupportedOperationException("Not supported yet.");
@@ -29,41 +66,4 @@
 +        return new org.bukkit.craftbukkit.inventory.CraftBlockInventoryHolder(this, menu.getBukkitView().getTopInventory());
 +    }
 +    // Paper end - Add missing InventoryHolders
-+
-     ContainerLevelAccess NULL = new ContainerLevelAccess() {
-         @Override
-         public <T> Optional<T> evaluate(BiFunction<Level, BlockPos, T> getter) {
-             return Optional.empty();
-         }
-+        // Paper start - fix menus with empty level accesses
-+        @Override
-+        public org.bukkit.Location getLocation() {
-+            return null;
-+        }
-+        // Paper end - fix menus with empty level accesses
-     };
- 
-     static ContainerLevelAccess create(final Level world, final BlockPos pos) {
-         return new ContainerLevelAccess() {
-+            // CraftBukkit start
-             @Override
-+            public Level getWorld() {
-+                return world;
-+            }
-+
-+            @Override
-+            public BlockPos getPosition() {
-+                return pos;
-+            }
-+            // CraftBukkit end
-+            // Paper start - Add missing InventoryHolders
-+            @Override
-+            public boolean isBlock() {
-+                return true;
-+            }
-+            // Paper end - Add missing InventoryHolders
-+
-+            @Override
-             public <T> Optional<T> evaluate(BiFunction<Level, BlockPos, T> getter) {
-                 return Optional.of(getter.apply(world, pos));
-             }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/ContainerListener.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/ContainerListener.java.patch
new file mode 100644
index 0000000000..0f457f3e8b
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/ContainerListener.java.patch
@@ -0,0 +1,13 @@
+--- a/net/minecraft/world/inventory/ContainerListener.java
++++ b/net/minecraft/world/inventory/ContainerListener.java
+@@ -6,4 +_,10 @@
+     void slotChanged(AbstractContainerMenu containerToSend, int dataSlotIndex, ItemStack stack);
+ 
+     void dataChanged(AbstractContainerMenu containerMenu, int dataSlotIndex, int value);
++
++    // Paper start - Add PlayerInventorySlotChangeEvent
++    default void slotChanged(AbstractContainerMenu containerToSend, int dataSlotIndex, ItemStack oldStack, ItemStack stack) {
++        slotChanged(containerToSend, dataSlotIndex, stack);
++    }
++    // Paper end - Add PlayerInventorySlotChangeEvent
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/ContainerSynchronizer.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/ContainerSynchronizer.java.patch
new file mode 100644
index 0000000000..a5f19041a8
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/ContainerSynchronizer.java.patch
@@ -0,0 +1,9 @@
+--- a/net/minecraft/world/inventory/ContainerSynchronizer.java
++++ b/net/minecraft/world/inventory/ContainerSynchronizer.java
+@@ -11,4 +_,6 @@
+     void sendCarriedChange(AbstractContainerMenu containerMenu, ItemStack stack);
+ 
+     void sendDataChange(AbstractContainerMenu container, int id, int value);
++
++    default void sendOffHandSlotChange() {} // Paper - Sync offhand slot in menus
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/CrafterMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/CrafterMenu.java.patch
new file mode 100644
index 0000000000..e5fb558412
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/CrafterMenu.java.patch
@@ -0,0 +1,31 @@
+--- a/net/minecraft/world/inventory/CrafterMenu.java
++++ b/net/minecraft/world/inventory/CrafterMenu.java
+@@ -19,6 +_,20 @@
+     private final ContainerData containerData;
+     private final Player player;
+     private final CraftingContainer container;
++    // CraftBukkit start
++    private org.bukkit.craftbukkit.inventory.view.CraftCrafterView bukkitEntity = null;
++
++    @Override
++    public org.bukkit.craftbukkit.inventory.view.CraftCrafterView getBukkitView() {
++        if (this.bukkitEntity != null) {
++            return this.bukkitEntity;
++        }
++
++        org.bukkit.craftbukkit.inventory.CraftInventoryCrafter inventory = new org.bukkit.craftbukkit.inventory.CraftInventoryCrafter(this.container, this.resultContainer);
++        this.bukkitEntity = new org.bukkit.craftbukkit.inventory.view.CraftCrafterView(this.player.getBukkitEntity(), inventory, this);
++        return this.bukkitEntity;
++    }
++    // CraftBukkit end
+ 
+     public CrafterMenu(int containerId, Inventory playerInventory) {
+         super(MenuType.CRAFTER_3x3, containerId);
+@@ -100,6 +_,7 @@
+ 
+     @Override
+     public boolean stillValid(Player player) {
++        if (!this.checkReachable) return true; // CraftBukkit
+         return this.container.stillValid(player);
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/CraftingContainer.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/CraftingContainer.java.patch
new file mode 100644
index 0000000000..2aef7bff97
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/CraftingContainer.java.patch
@@ -0,0 +1,18 @@
+--- a/net/minecraft/world/inventory/CraftingContainer.java
++++ b/net/minecraft/world/inventory/CraftingContainer.java
+@@ -12,6 +_,15 @@
+ 
+     List<ItemStack> getItems();
+ 
++    // CraftBukkit start
++    default net.minecraft.world.item.crafting.RecipeHolder<net.minecraft.world.item.crafting.CraftingRecipe> getCurrentRecipe() {
++        return null;
++    }
++
++    default void setCurrentRecipe(net.minecraft.world.item.crafting.RecipeHolder<net.minecraft.world.item.crafting.CraftingRecipe> recipe) {
++    }
++    // CraftBukkit end
++
+     default CraftingInput asCraftInput() {
+         return this.asPositionedCraftInput().input();
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/CraftingMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/CraftingMenu.java.patch
new file mode 100644
index 0000000000..d04a6f281e
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/CraftingMenu.java.patch
@@ -0,0 +1,87 @@
+--- a/net/minecraft/world/inventory/CraftingMenu.java
++++ b/net/minecraft/world/inventory/CraftingMenu.java
+@@ -30,13 +_,16 @@
+     public final ContainerLevelAccess access;
+     private final Player player;
+     private boolean placingRecipe;
++    // CraftBukkit start
++    private org.bukkit.craftbukkit.inventory.CraftInventoryView bukkitEntity = null;
++    // CraftBukkit end
+ 
+     public CraftingMenu(int containerId, Inventory playerInventory) {
+         this(containerId, playerInventory, ContainerLevelAccess.NULL);
+     }
+ 
+     public CraftingMenu(int containerId, Inventory playerInventory, ContainerLevelAccess access) {
+-        super(MenuType.CRAFTING, containerId, 3, 3);
++        super(MenuType.CRAFTING, containerId, 3, 3, playerInventory); // CraftBukkit - pass player
+         this.access = access;
+         this.player = playerInventory.player;
+         this.addResultSlot(this.player, 124, 35);
+@@ -55,7 +_,32 @@
+         CraftingInput craftInput = craftSlots.asCraftInput();
+         ServerPlayer serverPlayer = (ServerPlayer)player;
+         ItemStack itemStack = ItemStack.EMPTY;
++        // Paper start - Perf: Improve mass crafting; check last recipe used first
++        /*
++        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 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 change, 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.
++
++        See also: ResultSlot class
++         */
++        if (recipe == null) {
++            recipe = craftSlots.getCurrentRecipe();
++        }
++        // Paper end - Perf: Improve mass crafting; check last recipe used first
+         Optional<RecipeHolder<CraftingRecipe>> recipeFor = level.getServer().getRecipeManager().getRecipeFor(RecipeType.CRAFTING, craftInput, level, recipe);
++        craftSlots.setCurrentRecipe(recipeFor.orElse(null)); // CraftBukkit
+         if (recipeFor.isPresent()) {
+             RecipeHolder<CraftingRecipe> recipeHolder = recipeFor.get();
+             CraftingRecipe craftingRecipe = recipeHolder.value();
+@@ -66,6 +_,7 @@
+                 }
+             }
+         }
++        itemStack = org.bukkit.craftbukkit.event.CraftEventFactory.callPreCraftEvent(craftSlots, resultSlots, itemStack, menu.getBukkitView(), recipeFor.map(RecipeHolder::value).orElse(null) instanceof net.minecraft.world.item.crafting.RepairItemRecipe); // CraftBukkit
+ 
+         resultSlots.setItem(0, itemStack);
+         menu.setRemoteSlot(0, itemStack);
+@@ -102,6 +_,7 @@
+ 
+     @Override
+     public boolean stillValid(Player player) {
++        if (!this.checkReachable) return true; // CraftBukkit
+         return stillValid(this.access, player, Blocks.CRAFTING_TABLE);
+     }
+ 
+@@ -176,4 +_,17 @@
+     protected Player owner() {
+         return this.player;
+     }
++
++    // CraftBukkit start
++    @Override
++    public org.bukkit.craftbukkit.inventory.CraftInventoryView getBukkitView() {
++        if (this.bukkitEntity != null) {
++            return this.bukkitEntity;
++        }
++
++        org.bukkit.craftbukkit.inventory.CraftInventoryCrafting inventory = new org.bukkit.craftbukkit.inventory.CraftInventoryCrafting(this.craftSlots, this.resultSlots);
++        this.bukkitEntity = new org.bukkit.craftbukkit.inventory.CraftInventoryView(this.player.getBukkitEntity(), inventory, this);
++        return this.bukkitEntity;
++    }
++    // CraftBukkit end
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/DispenserMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/DispenserMenu.java.patch
new file mode 100644
index 0000000000..de6614874c
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/DispenserMenu.java.patch
@@ -0,0 +1,49 @@
+--- a/net/minecraft/world/inventory/DispenserMenu.java
++++ b/net/minecraft/world/inventory/DispenserMenu.java
+@@ -13,6 +_,10 @@
+     private static final int USE_ROW_SLOT_START = 36;
+     private static final int USE_ROW_SLOT_END = 45;
+     public final Container dispenser;
++    // CraftBukkit start
++    private org.bukkit.craftbukkit.inventory.CraftInventoryView bukkitEntity = null;
++    private Inventory player;
++    // CraftBukkit end
+ 
+     public DispenserMenu(int containerId, Inventory playerInventory) {
+         this(containerId, playerInventory, new SimpleContainer(9));
+@@ -20,6 +_,9 @@
+ 
+     public DispenserMenu(int containerId, Inventory playerInventory, Container container) {
+         super(MenuType.GENERIC_3x3, containerId);
++        // CraftBukkit start - Save player
++        this.player = playerInventory;
++        // CraftBukkit end
+         checkContainerSize(container, 9);
+         this.dispenser = container;
+         container.startOpen(playerInventory.player);
+@@ -38,6 +_,7 @@
+ 
+     @Override
+     public boolean stillValid(Player player) {
++        if (!this.checkReachable) return true; // CraftBukkit
+         return this.dispenser.stillValid(player);
+     }
+ 
+@@ -77,4 +_,17 @@
+         super.removed(player);
+         this.dispenser.stopOpen(player);
+     }
++
++    // CraftBukkit start
++    @Override
++    public org.bukkit.craftbukkit.inventory.CraftInventoryView getBukkitView() {
++        if (this.bukkitEntity != null) {
++            return this.bukkitEntity;
++        }
++
++        org.bukkit.craftbukkit.inventory.CraftInventory inventory = new org.bukkit.craftbukkit.inventory.CraftInventory(this.dispenser);
++        this.bukkitEntity = new org.bukkit.craftbukkit.inventory.CraftInventoryView(this.player.player.getBukkitEntity(), inventory, this);
++        return this.bukkitEntity;
++    }
++    // CraftBukkit end
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/EnchantmentMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/EnchantmentMenu.java.patch
new file mode 100644
index 0000000000..e37648a0fb
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/EnchantmentMenu.java.patch
@@ -0,0 +1,204 @@
+--- a/net/minecraft/world/inventory/EnchantmentMenu.java
++++ b/net/minecraft/world/inventory/EnchantmentMenu.java
+@@ -31,19 +_,17 @@
+ 
+ public class EnchantmentMenu extends AbstractContainerMenu {
+     static final ResourceLocation EMPTY_SLOT_LAPIS_LAZULI = ResourceLocation.withDefaultNamespace("container/slot/lapis_lazuli");
+-    private final Container enchantSlots = new SimpleContainer(2) {
+-        @Override
+-        public void setChanged() {
+-            super.setChanged();
+-            EnchantmentMenu.this.slotsChanged(this);
+-        }
+-    };
++    private final Container enchantSlots; // Paper - Add missing InventoryHolders - move down
+     private final ContainerLevelAccess access;
+     private final RandomSource random = RandomSource.create();
+     private final DataSlot enchantmentSeed = DataSlot.standalone();
+     public final int[] costs = new int[3];
+     public final int[] enchantClue = new int[]{-1, -1, -1};
+     public final int[] levelClue = new int[]{-1, -1, -1};
++    // CraftBukkit start
++    private org.bukkit.craftbukkit.inventory.view.CraftEnchantmentView bukkitEntity = null;
++    private final org.bukkit.entity.Player player;
++    // CraftBukkit end
+ 
+     public EnchantmentMenu(int containerId, Inventory playerInventory) {
+         this(containerId, playerInventory, ContainerLevelAccess.NULL);
+@@ -51,6 +_,22 @@
+ 
+     public EnchantmentMenu(int containerId, Inventory playerInventory, ContainerLevelAccess access) {
+         super(MenuType.ENCHANTMENT, containerId);
++        // Paper start - Add missing InventoryHolders
++        this.enchantSlots = new SimpleContainer(this.createBlockHolder(access), 2) { // Paper - Add missing InventoryHolders
++            @Override
++            public void setChanged() {
++                super.setChanged();
++                EnchantmentMenu.this.slotsChanged(this);
++            }
++
++            // CraftBukkit start
++            @Override
++            public org.bukkit.Location getLocation() {
++                return access.getLocation();
++            }
++            // CraftBukkit end
++        };
++        // Paper end - Add missing InventoryHolders
+         this.access = access;
+         this.addSlot(new Slot(this.enchantSlots, 0, 15, 47) {
+             @Override
+@@ -80,13 +_,14 @@
+         this.addDataSlot(DataSlot.shared(this.levelClue, 0));
+         this.addDataSlot(DataSlot.shared(this.levelClue, 1));
+         this.addDataSlot(DataSlot.shared(this.levelClue, 2));
++        this.player = (org.bukkit.entity.Player) playerInventory.player.getBukkitEntity(); // CraftBukkit
+     }
+ 
+     @Override
+     public void slotsChanged(Container inventory) {
+         if (inventory == this.enchantSlots) {
+             ItemStack item = inventory.getItem(0);
+-            if (!item.isEmpty() && item.isEnchantable()) {
++            if (!item.isEmpty()) { // CraftBukkit - relax condition
+                 this.access.execute((level, blockPos) -> {
+                     IdMap<Holder<Enchantment>> holderIdMap = level.registryAccess().lookupOrThrow(Registries.ENCHANTMENT).asHolderIdMap();
+                     int i1 = 0;
+@@ -119,6 +_,42 @@
+                         }
+                     }
+ 
++                    // CraftBukkit start
++                    org.bukkit.craftbukkit.inventory.CraftItemStack craftItemStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item);
++                    org.bukkit.enchantments.EnchantmentOffer[] offers = new org.bukkit.enchantments.EnchantmentOffer[3];
++                    for (int j = 0; j < 3; ++j) {
++                        org.bukkit.enchantments.Enchantment enchantment = (this.enchantClue[j] >= 0) ? org.bukkit.craftbukkit.enchantments.CraftEnchantment.minecraftHolderToBukkit(holderIdMap.byId(this.enchantClue[j])) : null;
++                        offers[j] = (enchantment != null) ? new org.bukkit.enchantments.EnchantmentOffer(enchantment, this.levelClue[j], this.costs[j]) : null;
++                    }
++
++                    org.bukkit.event.enchantment.PrepareItemEnchantEvent event = new org.bukkit.event.enchantment.PrepareItemEnchantEvent(this.player, this.getBukkitView(), this.access.getLocation().getBlock(), craftItemStack, offers, i1);
++                    event.setCancelled(!item.isEnchantable());
++                    level.getCraftServer().getPluginManager().callEvent(event);
++
++                    if (event.isCancelled()) {
++                        for (int j = 0; j < 3; ++j) {
++                            this.costs[j] = 0;
++                            this.enchantClue[j] = -1;
++                            this.levelClue[j] = -1;
++                        }
++                        return;
++                    }
++
++                    for (int j = 0; j < 3; j++) {
++                        org.bukkit.enchantments.EnchantmentOffer offer = event.getOffers()[j];
++                        if (offer != null) {
++                            this.costs[j] = offer.getCost();
++                            this.enchantClue[j] = holderIdMap.getId(org.bukkit.craftbukkit.enchantments.CraftEnchantment
++                                .bukkitToMinecraftHolder(offer.getEnchantment()));
++                            this.levelClue[j] = offer.getEnchantmentLevel();
++                        } else {
++                            this.costs[j] = 0;
++                            this.enchantClue[j] = -1;
++                            this.levelClue[j] = -1;
++                        }
++                    }
++                    // CraftBukkit end
++
+                     this.broadcastChanges();
+                 });
+             } else {
+@@ -145,19 +_,51 @@
+                 return false;
+             } else {
+                 this.access.execute((level, blockPos) -> {
+-                    ItemStack itemStack = item;
++                    ItemStack itemStack = item; // Paper - diff on change
+                     List<EnchantmentInstance> enchantmentList = this.getEnchantmentList(level.registryAccess(), item, id, this.costs[id]);
+-                    if (!enchantmentList.isEmpty()) {
++                    // CraftBukkit start
++                    IdMap<Holder<Enchantment>> registry = level.registryAccess().lookupOrThrow(Registries.ENCHANTMENT).asHolderIdMap();
++                    if (true || !enchantmentList.isEmpty()) {
++                        // player.onEnchantmentPerformed(item, i); // Moved down
++                        java.util.Map<org.bukkit.enchantments.Enchantment, Integer> enchants = new java.util.HashMap<>();
++                        for (EnchantmentInstance instance : enchantmentList) {
++                            enchants.put(org.bukkit.craftbukkit.enchantments.CraftEnchantment.minecraftHolderToBukkit(instance.enchantment), instance.level);
++                        }
++                        org.bukkit.craftbukkit.inventory.CraftItemStack craftItemStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack);
++                        org.bukkit.enchantments.Enchantment hintedEnchantment = org.bukkit.craftbukkit.enchantments.CraftEnchantment.minecraftHolderToBukkit(registry.byId(this.enchantClue[id]));
++                        int hintedEnchantmentLevel = this.levelClue[id];
++                        org.bukkit.event.enchantment.EnchantItemEvent event = new org.bukkit.event.enchantment.EnchantItemEvent((org.bukkit.entity.Player) player.getBukkitEntity(), this.getBukkitView(), this.access.getLocation().getBlock(), craftItemStack, this.costs[id], enchants, hintedEnchantment, hintedEnchantmentLevel, id);
++                        level.getCraftServer().getPluginManager().callEvent(event);
++                        int itemLevel = event.getExpLevelCost();
++                        if (event.isCancelled() || (itemLevel > player.experienceLevel && !player.getAbilities().instabuild) || event.getEnchantsToAdd().isEmpty()) {
++                            return;
++                        }
++                        // CraftBukkit end
++                        // Paper start
++                        itemStack = org.bukkit.craftbukkit.inventory.CraftItemStack.getOrCloneOnMutation(craftItemStack, event.getItem());
++                        if (itemStack != item) {
++                            this.enchantSlots.setItem(0, itemStack);
++                        }
++                        if (itemStack.is(Items.BOOK)) {
++                            itemStack = itemStack.transmuteCopy(Items.ENCHANTED_BOOK);
++                            this.enchantSlots.setItem(0, itemStack);
++                        }
++                        // Paper end
++
++                        // CraftBukkit start
++                        for (java.util.Map.Entry<org.bukkit.enchantments.Enchantment, Integer> entry : event.getEnchantsToAdd().entrySet()) {
++                            Holder<Enchantment> nms = org.bukkit.craftbukkit.enchantments.CraftEnchantment.bukkitToMinecraftHolder(entry.getKey());
++                            if (nms == null) {
++                                continue;
++                            }
++
++                            EnchantmentInstance weightedrandomenchant = new EnchantmentInstance(nms, entry.getValue());
++                            itemStack.enchant(weightedrandomenchant.enchantment, weightedrandomenchant.level);
++                        }
+                         player.onEnchantmentPerformed(item, i);
+-                        if (item.is(Items.BOOK)) {
+-                            itemStack = item.transmuteCopy(Items.ENCHANTED_BOOK);
+-                            this.enchantSlots.setItem(0, itemStack);
+-                        }
+-
+-                        for (EnchantmentInstance enchantmentInstance : enchantmentList) {
+-                            itemStack.enchant(enchantmentInstance.enchantment, enchantmentInstance.level);
+-                        }
+-
++                        // CraftBukkit end
++
++                        // CraftBukkit - TODO: let plugins change this
+                         item1.consume(i, player);
+                         if (item1.isEmpty()) {
+                             this.enchantSlots.setItem(1, ItemStack.EMPTY);
+@@ -214,6 +_,7 @@
+ 
+     @Override
+     public boolean stillValid(Player player) {
++        if (!this.checkReachable) return true; // CraftBukkit
+         return stillValid(this.access, player, Blocks.ENCHANTING_TABLE);
+     }
+ 
+@@ -261,4 +_,23 @@
+ 
+         return itemStack;
+     }
++
++    // CraftBukkit start
++    @Override
++    public org.bukkit.craftbukkit.inventory.view.CraftEnchantmentView getBukkitView() {
++        if (this.bukkitEntity != null) {
++            return this.bukkitEntity;
++        }
++
++        org.bukkit.craftbukkit.inventory.CraftInventoryEnchanting inventory = new org.bukkit.craftbukkit.inventory.CraftInventoryEnchanting(this.enchantSlots);
++        this.bukkitEntity = new org.bukkit.craftbukkit.inventory.view.CraftEnchantmentView(this.player, inventory, this);
++        return this.bukkitEntity;
++    }
++    // CraftBukkit end
++
++    // Paper start - add enchantment seed update API
++    public void setEnchantmentSeed(int seed) {
++        this.enchantmentSeed.set(seed);
++    }
++    // Paper end - add enchantment seed update API
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/FurnaceResultSlot.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/FurnaceResultSlot.java.patch
new file mode 100644
index 0000000000..8cf409634d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/FurnaceResultSlot.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/inventory/FurnaceResultSlot.java
++++ b/net/minecraft/world/inventory/FurnaceResultSlot.java
+@@ -45,7 +_,7 @@
+     protected void checkTakeAchievements(ItemStack stack) {
+         stack.onCraftedBy(this.player.level(), this.player, this.removeCount);
+         if (this.player instanceof ServerPlayer serverPlayer && this.container instanceof AbstractFurnaceBlockEntity abstractFurnaceBlockEntity) {
+-            abstractFurnaceBlockEntity.awardUsedRecipesAndPopExperience(serverPlayer);
++            abstractFurnaceBlockEntity.awardUsedRecipesAndPopExperience(serverPlayer, stack, this.removeCount); // CraftBukkit
+         }
+ 
+         this.removeCount = 0;
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/GrindstoneMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/GrindstoneMenu.java.patch
new file mode 100644
index 0000000000..3ef40a1183
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/GrindstoneMenu.java.patch
@@ -0,0 +1,108 @@
+--- a/net/minecraft/world/inventory/GrindstoneMenu.java
++++ b/net/minecraft/world/inventory/GrindstoneMenu.java
+@@ -20,6 +_,21 @@
+ import net.minecraft.world.phys.Vec3;
+ 
+ public class GrindstoneMenu extends AbstractContainerMenu {
++    // CraftBukkit start
++    private org.bukkit.craftbukkit.inventory.CraftInventoryView bukkitEntity = null;
++    private org.bukkit.entity.Player player;
++
++    @Override
++    public org.bukkit.craftbukkit.inventory.CraftInventoryView getBukkitView() {
++        if (this.bukkitEntity != null) {
++            return this.bukkitEntity;
++        }
++
++        org.bukkit.craftbukkit.inventory.CraftInventoryGrindstone inventory = new org.bukkit.craftbukkit.inventory.CraftInventoryGrindstone(this.repairSlots, this.resultSlots);
++        this.bukkitEntity = new org.bukkit.craftbukkit.inventory.CraftInventoryView(this.player, inventory, this);
++        return this.bukkitEntity;
++    }
++    // CraftBukkit end
+     public static final int MAX_NAME_LENGTH = 35;
+     public static final int INPUT_SLOT = 0;
+     public static final int ADDITIONAL_SLOT = 1;
+@@ -28,14 +_,8 @@
+     private static final int INV_SLOT_END = 30;
+     private static final int USE_ROW_SLOT_START = 30;
+     private static final int USE_ROW_SLOT_END = 39;
+-    private final Container resultSlots = new ResultContainer();
+-    final Container repairSlots = new SimpleContainer(2) {
+-        @Override
+-        public void setChanged() {
+-            super.setChanged();
+-            GrindstoneMenu.this.slotsChanged(this);
+-        }
+-    };
++    private final Container resultSlots; // Paper - Add missing InventoryHolders - move down
++    final Container repairSlots; // Paper - Add missing InventoryHolders - move down
+     private final ContainerLevelAccess access;
+ 
+     public GrindstoneMenu(int containerId, Inventory playerInventory) {
+@@ -44,6 +_,22 @@
+ 
+     public GrindstoneMenu(int containerId, Inventory playerInventory, final ContainerLevelAccess access) {
+         super(MenuType.GRINDSTONE, containerId);
++        // Paper start - Add missing InventoryHolders
++        this.resultSlots = new ResultContainer(this.createBlockHolder(access)); // Paper - Add missing InventoryHolders
++        this.repairSlots = new SimpleContainer(this.createBlockHolder(access), 2) { // Paper - Add missing InventoryHolders
++            @Override
++            public void setChanged() {
++                super.setChanged();
++                GrindstoneMenu.this.slotsChanged(this);
++            }
++            // CraftBukkit start
++            @Override
++            public org.bukkit.Location getLocation() {
++                return access.getLocation();
++            }
++            // CraftBukkit end
++        };
++        // Paper end - Add missing InventoryHolders
+         this.access = access;
+         this.addSlot(new Slot(this.repairSlots, 0, 49, 19) {
+             @Override
+@@ -67,7 +_,11 @@
+             public void onTake(Player player, ItemStack stack) {
+                 access.execute((level, blockPos) -> {
+                     if (level instanceof ServerLevel) {
+-                        ExperienceOrb.award((ServerLevel)level, Vec3.atCenterOf(blockPos), this.getExperienceAmount(level));
++                        // Paper start - Fire BlockExpEvent on grindstone use
++                        org.bukkit.event.block.BlockExpEvent event = new org.bukkit.event.block.BlockExpEvent(org.bukkit.craftbukkit.block.CraftBlock.at(level, blockPos), this.getExperienceAmount(level));
++                        event.callEvent();
++                        ExperienceOrb.award((ServerLevel) level, Vec3.atCenterOf(blockPos), event.getExpToDrop(), org.bukkit.entity.ExperienceOrb.SpawnReason.GRINDSTONE, player);
++                        // Paper end - Fire BlockExpEvent on grindstone use
+                     }
+ 
+                     level.levelEvent(1042, blockPos, 0);
+@@ -104,6 +_,7 @@
+             }
+         });
+         this.addStandardInventorySlots(playerInventory, 8, 84);
++        this.player = (org.bukkit.entity.Player) playerInventory.player.getBukkitEntity(); // CraftBukkit
+     }
+ 
+     @Override
+@@ -111,11 +_,13 @@
+         super.slotsChanged(inventory);
+         if (inventory == this.repairSlots) {
+             this.createResult();
++            org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareResultEvent(this, RESULT_SLOT); // Paper - Add PrepareResultEvent
+         }
+     }
+ 
+     private void createResult() {
+-        this.resultSlots.setItem(0, this.computeResult(this.repairSlots.getItem(0), this.repairSlots.getItem(1)));
++        org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareGrindstoneEvent(this.getBukkitView(), this.computeResult(this.repairSlots.getItem(0), this.repairSlots.getItem(1))); // CraftBukkit
++        this.sendAllDataToRemote(); // CraftBukkit - SPIGOT-6686: Always send completed inventory to stay in sync with client
+         this.broadcastChanges();
+     }
+ 
+@@ -201,6 +_,7 @@
+ 
+     @Override
+     public boolean stillValid(Player player) {
++        if (!this.checkReachable) return true; // CraftBukkit
+         return stillValid(this.access, player, Blocks.GRINDSTONE);
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/HopperMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/HopperMenu.java.patch
new file mode 100644
index 0000000000..d6d2f6c1de
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/HopperMenu.java.patch
@@ -0,0 +1,40 @@
+--- a/net/minecraft/world/inventory/HopperMenu.java
++++ b/net/minecraft/world/inventory/HopperMenu.java
+@@ -9,6 +_,21 @@
+ public class HopperMenu extends AbstractContainerMenu {
+     public static final int CONTAINER_SIZE = 5;
+     private final Container hopper;
++    // CraftBukkit start
++    private org.bukkit.craftbukkit.inventory.CraftInventoryView bukkitEntity = null;
++    private Inventory player;
++
++    @Override
++    public org.bukkit.craftbukkit.inventory.CraftInventoryView getBukkitView() {
++        if (this.bukkitEntity != null) {
++            return this.bukkitEntity;
++        }
++
++        org.bukkit.craftbukkit.inventory.CraftInventory inventory = new org.bukkit.craftbukkit.inventory.CraftInventory(this.hopper);
++        this.bukkitEntity = new org.bukkit.craftbukkit.inventory.CraftInventoryView(this.player.player.getBukkitEntity(), inventory, this);
++        return this.bukkitEntity;
++    }
++    // CraftBukkit end
+ 
+     public HopperMenu(int containerId, Inventory playerInventory) {
+         this(containerId, playerInventory, new SimpleContainer(5));
+@@ -17,6 +_,7 @@
+     public HopperMenu(int containerId, Inventory playerInventory, Container container) {
+         super(MenuType.HOPPER, containerId);
+         this.hopper = container;
++        this.player = playerInventory; // CraftBukkit - save player
+         checkContainerSize(container, 5);
+         container.startOpen(playerInventory.player);
+ 
+@@ -29,6 +_,7 @@
+ 
+     @Override
+     public boolean stillValid(Player player) {
++        if (!this.checkReachable) return true; // CraftBukkit
+         return this.hopper.stillValid(player);
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/HorseInventoryMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/HorseInventoryMenu.java.patch
new file mode 100644
index 0000000000..8128aca0d5
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/HorseInventoryMenu.java.patch
@@ -0,0 +1,26 @@
+--- a/net/minecraft/world/inventory/HorseInventoryMenu.java
++++ b/net/minecraft/world/inventory/HorseInventoryMenu.java
+@@ -19,9 +_,23 @@
+     private final AbstractHorse horse;
+     public static final int SLOT_BODY_ARMOR = 1;
+     private static final int SLOT_HORSE_INVENTORY_START = 2;
++    // CraftBukkit start
++    org.bukkit.craftbukkit.inventory.CraftInventoryView bukkitEntity;
++    Inventory player;
++
++    @Override
++    public org.bukkit.inventory.InventoryView getBukkitView() {
++        if (this.bukkitEntity != null) {
++            return this.bukkitEntity;
++        }
++
++        return this.bukkitEntity = new org.bukkit.craftbukkit.inventory.CraftInventoryView(this.player.player.getBukkitEntity(), this.horseContainer.getOwner().getInventory(), this);
++    }
++    // CraftBukkit end
+ 
+     public HorseInventoryMenu(int containerId, Inventory inventory, Container horseContainer, final AbstractHorse horse, int columns) {
+         super(null, containerId);
++        this.player = inventory; // CraftBukkit - save player
+         this.horseContainer = horseContainer;
+         this.armorContainer = horse.getBodyArmorAccess();
+         this.horse = horse;
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/InventoryMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/InventoryMenu.java.patch
new file mode 100644
index 0000000000..678db72493
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/InventoryMenu.java.patch
@@ -0,0 +1,45 @@
+--- a/net/minecraft/world/inventory/InventoryMenu.java
++++ b/net/minecraft/world/inventory/InventoryMenu.java
+@@ -2,6 +_,7 @@
+ 
+ import java.util.List;
+ import java.util.Map;
++import net.minecraft.network.chat.Component;
+ import net.minecraft.resources.ResourceLocation;
+ import net.minecraft.server.level.ServerLevel;
+ import net.minecraft.world.Container;
+@@ -44,9 +_,15 @@
+     private static final EquipmentSlot[] SLOT_IDS = new EquipmentSlot[]{EquipmentSlot.HEAD, EquipmentSlot.CHEST, EquipmentSlot.LEGS, EquipmentSlot.FEET};
+     public final boolean active;
+     private final Player owner;
++    // CraftBukkit start
++    private org.bukkit.craftbukkit.inventory.CraftInventoryView bukkitEntity = null;
++    // CraftBukkit end
+ 
+     public InventoryMenu(Inventory playerInventory, boolean active, final Player owner) {
+-        super(null, 0, 2, 2);
++        // CraftBukkit start
++        super((MenuType) null, 0, 2, 2, playerInventory); // CraftBukkit - save player
++        this.setTitle(Component.translatable("container.crafting")); // SPIGOT-4722: Allocate title for player inventory
++        // CraftBukkit end
+         this.active = active;
+         this.owner = owner;
+         this.addResultSlot(owner, 154, 28);
+@@ -188,4 +_,17 @@
+     protected Player owner() {
+         return this.owner;
+     }
++
++    // CraftBukkit start
++    @Override
++    public org.bukkit.craftbukkit.inventory.CraftInventoryView getBukkitView() {
++        if (this.bukkitEntity != null) {
++            return this.bukkitEntity;
++        }
++
++        org.bukkit.craftbukkit.inventory.CraftInventoryCrafting inventory = new org.bukkit.craftbukkit.inventory.CraftInventoryCrafting(this.craftSlots, this.resultSlots);
++        this.bukkitEntity = new org.bukkit.craftbukkit.inventory.CraftInventoryView(this.owner.getBukkitEntity(), inventory, this);
++        return this.bukkitEntity;
++    }
++    // CraftBukkit end
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/ItemCombinerMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/ItemCombinerMenu.java.patch
similarity index 51%
rename from paper-server/patches/unapplied/net/minecraft/world/inventory/ItemCombinerMenu.java.patch
rename to paper-server/patches/sources/net/minecraft/world/inventory/ItemCombinerMenu.java.patch
index 7d50fbdafb..447c6217c1 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/ItemCombinerMenu.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/ItemCombinerMenu.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/inventory/ItemCombinerMenu.java
 +++ b/net/minecraft/world/inventory/ItemCombinerMenu.java
-@@ -17,12 +17,7 @@
+@@ -15,12 +_,7 @@
      protected final ContainerLevelAccess access;
      protected final Player player;
      protected final Container inputSlots;
@@ -13,11 +13,11 @@
 +    protected final ResultContainer resultSlots; // Paper - Add missing InventoryHolders; delay field init
      private final int resultSlotIndex;
  
-     protected boolean mayPickup(Player player, boolean present) {
-@@ -36,6 +31,14 @@
-     public ItemCombinerMenu(@Nullable MenuType<?> type, int syncId, Inventory playerInventory, ContainerLevelAccess context, ItemCombinerMenuSlotDefinition forgingSlotsManager) {
-         super(type, syncId);
-         this.access = context;
+     protected boolean mayPickup(Player player, boolean hasStack) {
+@@ -36,6 +_,14 @@
+     ) {
+         super(menuType, containerId);
+         this.access = access;
 +        // Paper start - Add missing InventoryHolders; delay field init
 +        this.resultSlots = new ResultContainer(this.createBlockHolder(this.access)) {
 +            @Override
@@ -26,19 +26,10 @@
 +            }
 +        };
 +        // Paper end - Add missing InventoryHolders; delay field init
-         this.player = playerInventory.player;
-         this.inputSlots = this.createContainer(forgingSlotsManager.getNumOfInputSlots());
-         this.resultSlotIndex = forgingSlotsManager.getResultSlotIndex();
-@@ -50,7 +53,7 @@
-         while (iterator.hasNext()) {
-             final ItemCombinerMenuSlotDefinition.SlotDefinition itemcombinermenuslotdefinition_b = (ItemCombinerMenuSlotDefinition.SlotDefinition) iterator.next();
- 
--            this.addSlot(new Slot(this, this.inputSlots, itemcombinermenuslotdefinition_b.slotIndex(), itemcombinermenuslotdefinition_b.x(), itemcombinermenuslotdefinition_b.y()) {
-+            this.addSlot(new Slot(this.inputSlots, itemcombinermenuslotdefinition_b.slotIndex(), itemcombinermenuslotdefinition_b.x(), itemcombinermenuslotdefinition_b.y()) { // CraftBukkit - decompile error
-                 @Override
-                 public boolean mayPlace(ItemStack stack) {
-                     return itemcombinermenuslotdefinition_b.mayPlace().test(stack);
-@@ -82,7 +85,7 @@
+         this.player = inventory.player;
+         this.inputSlots = this.createContainer(slotDefinition.getNumOfInputSlots());
+         this.resultSlotIndex = slotDefinition.getResultSlotIndex();
+@@ -79,7 +_,7 @@
      public abstract void createResult();
  
      private SimpleContainer createContainer(int size) {
@@ -47,19 +38,19 @@
              @Override
              public void setChanged() {
                  super.setChanged();
-@@ -96,6 +99,7 @@
+@@ -93,6 +_,7 @@
          super.slotsChanged(inventory);
          if (inventory == this.inputSlots) {
              this.createResult();
 +            org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareResultEvent(this, this instanceof SmithingMenu ? 3 : 2); // Paper - Add PrepareResultEvent
          }
- 
      }
-@@ -110,6 +114,7 @@
+ 
+@@ -104,6 +_,7 @@
  
      @Override
      public boolean stillValid(Player player) {
 +        if (!this.checkReachable) return true; // CraftBukkit
-         return (Boolean) this.access.evaluate((world, blockposition) -> {
-             return !this.isValidBlock(world.getBlockState(blockposition)) ? false : player.canInteractWithBlock(blockposition, 4.0D);
-         }, true);
+         return this.access
+             .evaluate((level, blockPos) -> !this.isValidBlock(level.getBlockState(blockPos)) ? false : player.canInteractWithBlock(blockPos, 4.0), true);
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/LecternMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/LecternMenu.java.patch
new file mode 100644
index 0000000000..11190b8898
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/LecternMenu.java.patch
@@ -0,0 +1,103 @@
+--- a/net/minecraft/world/inventory/LecternMenu.java
++++ b/net/minecraft/world/inventory/LecternMenu.java
+@@ -14,12 +_,29 @@
+     public static final int BUTTON_PAGE_JUMP_RANGE_START = 100;
+     private final Container lectern;
+     private final ContainerData lecternData;
+-
+-    public LecternMenu(int containerId) {
+-        this(containerId, new SimpleContainer(1), new SimpleContainerData(1));
+-    }
+-
+-    public LecternMenu(int containerId, Container lectern, ContainerData lecternData) {
++    // CraftBukkit start
++    private org.bukkit.craftbukkit.inventory.view.CraftLecternView bukkitEntity = null;
++    private org.bukkit.entity.Player player;
++
++    @Override
++    public org.bukkit.craftbukkit.inventory.view.CraftLecternView getBukkitView() {
++        if (this.bukkitEntity != null) {
++            return this.bukkitEntity;
++        }
++
++        org.bukkit.craftbukkit.inventory.CraftInventoryLectern inventory = new org.bukkit.craftbukkit.inventory.CraftInventoryLectern(this.lectern);
++        this.bukkitEntity = new org.bukkit.craftbukkit.inventory.view.CraftLecternView(this.player, inventory, this);
++        return this.bukkitEntity;
++    }
++    // CraftBukkit end
++
++    // CraftBukkit start - add player inventory
++    public LecternMenu(int containerId, net.minecraft.world.entity.player.Inventory playerinventory) {
++        this(containerId, new SimpleContainer(1), new SimpleContainerData(1), playerinventory);
++    }
++
++    public LecternMenu(int containerId, Container lectern, ContainerData lecternData, net.minecraft.world.entity.player.Inventory playerinventory) {
++        // CraftBukkit end - add player inventory
+         super(MenuType.LECTERN, containerId);
+         checkContainerSize(lectern, 1);
+         checkContainerDataCount(lecternData, 1);
+@@ -33,10 +_,12 @@
+             }
+         });
+         this.addDataSlots(lecternData);
++        this.player = (org.bukkit.entity.Player) playerinventory.player.getBukkitEntity(); // CraftBukkit
+     }
+ 
+     @Override
+     public boolean clickMenuButton(Player player, int id) {
++        io.papermc.paper.event.player.PlayerLecternPageChangeEvent playerLecternPageChangeEvent; org.bukkit.craftbukkit.inventory.CraftInventoryLectern bukkitView; // Paper - Add PlayerLecternPageChangeEvent
+         if (id >= 100) {
+             int i = id - 100;
+             this.setData(0, i);
+@@ -45,12 +_,26 @@
+             switch (id) {
+                 case 1: {
+                     int i = this.lecternData.get(0);
+-                    this.setData(0, i - 1);
++                    // Paper start - Add PlayerLecternPageChangeEvent
++                    bukkitView = (org.bukkit.craftbukkit.inventory.CraftInventoryLectern) getBukkitView().getTopInventory();
++                    playerLecternPageChangeEvent = new io.papermc.paper.event.player.PlayerLecternPageChangeEvent((org.bukkit.entity.Player) player.getBukkitEntity(), bukkitView.getHolder(), bukkitView.getBook(), io.papermc.paper.event.player.PlayerLecternPageChangeEvent.PageChangeDirection.LEFT, i, i - 1);
++                    if (!playerLecternPageChangeEvent.callEvent()) {
++                        return false;
++                    }
++                    this.setData(0, playerLecternPageChangeEvent.getNewPage());
++                    // Paper end - Add PlayerLecternPageChangeEvent
+                     return true;
+                 }
+                 case 2: {
+                     int i = this.lecternData.get(0);
+-                    this.setData(0, i + 1);
++                    // Paper start - Add PlayerLecternPageChangeEvent
++                    bukkitView = (org.bukkit.craftbukkit.inventory.CraftInventoryLectern) getBukkitView().getTopInventory();
++                    playerLecternPageChangeEvent = new io.papermc.paper.event.player.PlayerLecternPageChangeEvent((org.bukkit.entity.Player) player.getBukkitEntity(), bukkitView.getHolder(), bukkitView.getBook(), io.papermc.paper.event.player.PlayerLecternPageChangeEvent.PageChangeDirection.RIGHT, i, i + 1);
++                    if (!playerLecternPageChangeEvent.callEvent()) {
++                        return false;
++                    }
++                    this.setData(0, playerLecternPageChangeEvent.getNewPage());
++                    // Paper end - Add PlayerLecternPageChangeEvent
+                     return true;
+                 }
+                 case 3:
+@@ -58,6 +_,13 @@
+                         return false;
+                     }
+ 
++                    // CraftBukkit start - Event for taking the book
++                    org.bukkit.event.player.PlayerTakeLecternBookEvent event = new org.bukkit.event.player.PlayerTakeLecternBookEvent(this.player, ((org.bukkit.craftbukkit.inventory.CraftInventoryLectern) this.getBukkitView().getTopInventory()).getHolder());
++                    org.bukkit.Bukkit.getServer().getPluginManager().callEvent(event);
++                    if (event.isCancelled()) {
++                        return false;
++                    }
++                    // CraftBukkit end
+                     ItemStack itemStack = this.lectern.removeItemNoUpdate(0);
+                     this.lectern.setChanged();
+                     if (!player.getInventory().add(itemStack)) {
+@@ -84,6 +_,8 @@
+ 
+     @Override
+     public boolean stillValid(Player player) {
++        if (this.lectern instanceof net.minecraft.world.level.block.entity.LecternBlockEntity.LecternInventory && !((net.minecraft.world.level.block.entity.LecternBlockEntity.LecternInventory) this.lectern).getLectern().hasBook()) return false; // CraftBukkit
++        if (!this.checkReachable) return true; // CraftBukkit
+         return this.lectern.stillValid(player);
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/LoomMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/LoomMenu.java.patch
new file mode 100644
index 0000000000..8182e17e77
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/LoomMenu.java.patch
@@ -0,0 +1,143 @@
+--- a/net/minecraft/world/inventory/LoomMenu.java
++++ b/net/minecraft/world/inventory/LoomMenu.java
+@@ -38,21 +_,23 @@
+     private final Slot patternSlot;
+     private final Slot resultSlot;
+     long lastSoundTime;
+-    private final Container inputContainer = new SimpleContainer(3) {
+-        @Override
+-        public void setChanged() {
+-            super.setChanged();
+-            LoomMenu.this.slotsChanged(this);
+-            LoomMenu.this.slotUpdateListener.run();
+-        }
+-    };
+-    private final Container outputContainer = new SimpleContainer(1) {
+-        @Override
+-        public void setChanged() {
+-            super.setChanged();
+-            LoomMenu.this.slotUpdateListener.run();
+-        }
+-    };
++    private final Container inputContainer; // Paper - Add missing InventoryHolders - move down
++    private final Container outputContainer; // Paper - Add missing InventoryHolders - move down
++    // CraftBukkit start
++    private org.bukkit.craftbukkit.inventory.view.CraftLoomView bukkitEntity = null;
++    private org.bukkit.entity.Player player;
++
++    @Override
++    public org.bukkit.craftbukkit.inventory.view.CraftLoomView getBukkitView() {
++        if (this.bukkitEntity != null) {
++            return this.bukkitEntity;
++        }
++
++        org.bukkit.craftbukkit.inventory.CraftInventoryLoom inventory = new org.bukkit.craftbukkit.inventory.CraftInventoryLoom(this.inputContainer, this.outputContainer);
++        this.bukkitEntity = new org.bukkit.craftbukkit.inventory.view.CraftLoomView(this.player, inventory, this);
++        return this.bukkitEntity;
++    }
++    // CraftBukkit end
+ 
+     public LoomMenu(int containerId, Inventory playerInventory) {
+         this(containerId, playerInventory, ContainerLevelAccess.NULL);
+@@ -61,6 +_,28 @@
+     public LoomMenu(int containerId, Inventory playerInventory, final ContainerLevelAccess access) {
+         super(MenuType.LOOM, containerId);
+         this.access = access;
++        // CraftBukkit start
++        this.inputContainer = new SimpleContainer(this.createBlockHolder(access), 3) { // Paper - Add missing InventoryHolders
++            @Override
++            public void setChanged() {
++                super.setChanged();
++                LoomMenu.this.slotsChanged(this);
++                LoomMenu.this.slotUpdateListener.run();
++            }
++        };
++        this.outputContainer =  new SimpleContainer(this.createBlockHolder(access), 1) { // Paper - Add missing InventoryHolders
++            @Override
++            public void setChanged() {
++                super.setChanged();
++                LoomMenu.this.slotUpdateListener.run();
++            }
++
++            @Override
++            public org.bukkit.Location getLocation() {
++                return access.getLocation();
++            }
++        };
++        // CraftBukkit end
+         this.bannerSlot = this.addSlot(new Slot(this.inputContainer, 0, 13, 26) {
+             @Override
+             public boolean mayPlace(ItemStack stack) {
+@@ -106,18 +_,44 @@
+         this.addStandardInventorySlots(playerInventory, 8, 84);
+         this.addDataSlot(this.selectedBannerPatternIndex);
+         this.patternGetter = playerInventory.player.registryAccess().lookupOrThrow(Registries.BANNER_PATTERN);
++        this.player = (org.bukkit.entity.Player) playerInventory.player.getBukkitEntity(); // CraftBukkit
+     }
+ 
+     @Override
+     public boolean stillValid(Player player) {
++        if (!this.checkReachable) return true; // CraftBukkit
+         return stillValid(this.access, player, Blocks.LOOM);
+     }
+ 
+     @Override
+     public boolean clickMenuButton(Player player, int id) {
+         if (id >= 0 && id < this.selectablePatterns.size()) {
+-            this.selectedBannerPatternIndex.set(id);
+-            this.setupResultSlot(this.selectablePatterns.get(id));
++            // Paper start - Add PlayerLoomPatternSelectEvent
++            int selectablePatternIndex = id;
++            io.papermc.paper.event.player.PlayerLoomPatternSelectEvent event = new io.papermc.paper.event.player.PlayerLoomPatternSelectEvent((org.bukkit.entity.Player) player.getBukkitEntity(), ((org.bukkit.craftbukkit.inventory.CraftInventoryLoom) getBukkitView().getTopInventory()), org.bukkit.craftbukkit.block.banner.CraftPatternType.minecraftHolderToBukkit(this.selectablePatterns.get(selectablePatternIndex)));
++            if (!event.callEvent()) {
++                player.containerMenu.sendAllDataToRemote();
++                return false;
++            }
++            final Holder<BannerPattern> eventPattern = org.bukkit.craftbukkit.block.banner.CraftPatternType.bukkitToMinecraftHolder(event.getPatternType());
++            Holder<BannerPattern> selectedPattern = null;
++            for (int i = 0; i < this.selectablePatterns.size(); i++) {
++                final Holder<BannerPattern> holder = this.selectablePatterns.get(i);
++                if (eventPattern.equals(holder)) {
++                    selectablePatternIndex = i;
++                    selectedPattern = holder;
++                    break;
++                }
++            }
++            if (selectedPattern == null) {
++                selectedPattern = eventPattern;
++                selectablePatternIndex = -1;
++            }
++
++            player.containerMenu.sendAllDataToRemote();
++            this.selectedBannerPatternIndex.set(selectablePatternIndex);
++            this.setupResultSlot(java.util.Objects.requireNonNull(selectedPattern, "selectedPattern was null, this is unexpected"));
++            // Paper end - Add PlayerLoomPatternSelectEvent
+             return true;
+         } else {
+             return false;
+@@ -180,7 +_,8 @@
+                 this.resultSlot.set(ItemStack.EMPTY);
+             }
+ 
+-            this.broadcastChanges();
++            // this.broadcastChanges(); // Paper - Add PrepareResultEvent; done below
++            org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareResultEvent(this, 3); // Paper - Add PrepareResultEvent
+         } else {
+             this.resultSlot.set(ItemStack.EMPTY);
+             this.selectablePatterns = List.of();
+@@ -269,7 +_,14 @@
+             itemStack.update(
+                 DataComponents.BANNER_PATTERNS,
+                 BannerPatternLayers.EMPTY,
+-                bannerPatternLayers -> new BannerPatternLayers.Builder().addAll(bannerPatternLayers).add(pattern, dyeColor).build()
++                // CraftBukkit start
++                bannerPatternLayers -> {
++                    if (bannerPatternLayers.layers().size() > 20) {
++                        bannerPatternLayers = new BannerPatternLayers(List.copyOf(bannerPatternLayers.layers().subList(0, 20)));
++                    }
++                    return new BannerPatternLayers.Builder().addAll(bannerPatternLayers).add(pattern, dyeColor).build();
++                }
++                // CraftBukkit end
+             );
+         }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/MenuType.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/MenuType.java.patch
new file mode 100644
index 0000000000..a5767492ae
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/MenuType.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/inventory/MenuType.java
++++ b/net/minecraft/world/inventory/MenuType.java
+@@ -26,7 +_,7 @@
+     public static final MenuType<FurnaceMenu> FURNACE = register("furnace", FurnaceMenu::new);
+     public static final MenuType<GrindstoneMenu> GRINDSTONE = register("grindstone", GrindstoneMenu::new);
+     public static final MenuType<HopperMenu> HOPPER = register("hopper", HopperMenu::new);
+-    public static final MenuType<LecternMenu> LECTERN = register("lectern", (containerId, playerInventory) -> new LecternMenu(containerId));
++    public static final MenuType<LecternMenu> LECTERN = register("lectern", LecternMenu::new); // CraftBukkit
+     public static final MenuType<LoomMenu> LOOM = register("loom", LoomMenu::new);
+     public static final MenuType<MerchantMenu> MERCHANT = register("merchant", MerchantMenu::new);
+     public static final MenuType<ShulkerBoxMenu> SHULKER_BOX = register("shulker_box", ShulkerBoxMenu::new);
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/MerchantContainer.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/MerchantContainer.java.patch
new file mode 100644
index 0000000000..3168efc559
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/MerchantContainer.java.patch
@@ -0,0 +1,48 @@
+--- a/net/minecraft/world/inventory/MerchantContainer.java
++++ b/net/minecraft/world/inventory/MerchantContainer.java
+@@ -17,6 +_,45 @@
+     private MerchantOffer activeOffer;
+     public int selectionHint;
+     private int futureXp;
++    // CraftBukkit start - add fields and methods
++    public java.util.List<org.bukkit.entity.HumanEntity> transaction = new java.util.ArrayList<>();
++    private int maxStack = MAX_STACK;
++
++    public java.util.List<ItemStack> getContents() {
++        return this.itemStacks;
++    }
++
++    public void onOpen(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.add(player);
++    }
++
++    public void onClose(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.remove(player);
++        this.merchant.setTradingPlayer(null); // SPIGOT-4860
++    }
++
++    public java.util.List<org.bukkit.entity.HumanEntity> getViewers() {
++        return this.transaction;
++    }
++
++    @Override
++    public int getMaxStackSize() {
++        return this.maxStack;
++    }
++
++    public void setMaxStackSize(int i) {
++        this.maxStack = i;
++    }
++
++    public org.bukkit.inventory.InventoryHolder getOwner() {
++        return (this.merchant instanceof net.minecraft.world.entity.npc.AbstractVillager abstractVillager) ? (org.bukkit.craftbukkit.entity.CraftAbstractVillager) abstractVillager.getBukkitEntity() : null;
++    }
++
++    @Override
++    public org.bukkit.Location getLocation() {
++        return (this.merchant instanceof net.minecraft.world.entity.npc.AbstractVillager abstractVillager) ? abstractVillager.getBukkitEntity().getLocation() : null; // Paper - Fix inventories returning null Locations
++    }
++    // CraftBukkit end
+ 
+     public MerchantContainer(Merchant merchant) {
+         this.merchant = merchant;
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/MerchantMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/MerchantMenu.java.patch
new file mode 100644
index 0000000000..d3c402326c
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/MerchantMenu.java.patch
@@ -0,0 +1,83 @@
+--- a/net/minecraft/world/inventory/MerchantMenu.java
++++ b/net/minecraft/world/inventory/MerchantMenu.java
+@@ -30,6 +_,18 @@
+     private int merchantLevel;
+     private boolean showProgressBar;
+     private boolean canRestock;
++    // CraftBukkit start
++    private org.bukkit.craftbukkit.inventory.view.CraftMerchantView bukkitEntity = null;
++    private Inventory player;
++
++    @Override
++    public org.bukkit.craftbukkit.inventory.view.CraftMerchantView getBukkitView() {
++        if (this.bukkitEntity == null) {
++            this.bukkitEntity = new org.bukkit.craftbukkit.inventory.view.CraftMerchantView(this.player.player.getBukkitEntity(), new org.bukkit.craftbukkit.inventory.CraftInventoryMerchant(this.trader, this.tradeContainer), this, this.trader);
++        }
++        return this.bukkitEntity;
++    }
++    // CraftBukkit end
+ 
+     public MerchantMenu(int containerId, Inventory playerInventory) {
+         this(containerId, playerInventory, new ClientSideMerchant(playerInventory.player));
+@@ -42,6 +_,7 @@
+         this.addSlot(new Slot(this.tradeContainer, 0, 136, 37));
+         this.addSlot(new Slot(this.tradeContainer, 1, 162, 37));
+         this.addSlot(new MerchantResultSlot(playerInventory.player, trader, this.tradeContainer, 2, 220, 37));
++        this.player = playerInventory; // CraftBukkit - save player
+         this.addStandardInventorySlots(playerInventory, 108, 84);
+     }
+ 
+@@ -105,12 +_,12 @@
+             ItemStack item = slot.getItem();
+             itemStack = item.copy();
+             if (index == 2) {
+-                if (!this.moveItemStackTo(item, 3, 39, true)) {
++                if (!this.moveItemStackTo(item, 3, 39, true, true)) { // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
+                     return ItemStack.EMPTY;
+                 }
+ 
+-                slot.onQuickCraft(item, itemStack);
+-                this.playTradeSound();
++                // slot.onQuickCraft(item, itemStack); // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent; moved to after the non-check moveItemStackTo call
++                // this.playTradeSound();
+             } else if (index != 0 && index != 1) {
+                 if (index >= 3 && index < 30) {
+                     if (!this.moveItemStackTo(item, 30, 39, false)) {
+@@ -123,6 +_,7 @@
+                 return ItemStack.EMPTY;
+             }
+ 
++            if (index != 2) { // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent; moved down for slot 2
+             if (item.isEmpty()) {
+                 slot.setByPlayer(ItemStack.EMPTY);
+             } else {
+@@ -134,13 +_,28 @@
+             }
+ 
+             slot.onTake(player, item);
++            } // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent; handle slot 2
++            if (index == 2) { // is merchant result slot
++                slot.onTake(player, item);
++                if (item.isEmpty()) {
++                    slot.set(ItemStack.EMPTY);
++                    return ItemStack.EMPTY;
++                }
++
++                this.moveItemStackTo(item, 3, 39, true, false); // This should always succeed because it's checked above
++
++                slot.onQuickCraft(item, itemStack);
++                this.playTradeSound();
++                slot.set(ItemStack.EMPTY); // item should ALWAYS be empty
++            }
++            // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent
+         }
+ 
+         return itemStack;
+     }
+ 
+     private void playTradeSound() {
+-        if (!this.trader.isClientSide()) {
++        if (!this.trader.isClientSide() && this.trader instanceof Entity) { // CraftBukkit - SPIGOT-5035
+             Entity entity = (Entity)this.trader;
+             entity.level()
+                 .playLocalSound(entity.getX(), entity.getY(), entity.getZ(), this.trader.getNotifyTradeSound(), SoundSource.NEUTRAL, 1.0F, 1.0F, false);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/MerchantResultSlot.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/MerchantResultSlot.java.patch
similarity index 57%
rename from paper-server/patches/unapplied/net/minecraft/world/inventory/MerchantResultSlot.java.patch
rename to paper-server/patches/sources/net/minecraft/world/inventory/MerchantResultSlot.java.patch
index f363fb2ef5..ceb2902468 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/MerchantResultSlot.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/MerchantResultSlot.java.patch
@@ -1,19 +1,19 @@
 --- a/net/minecraft/world/inventory/MerchantResultSlot.java
 +++ b/net/minecraft/world/inventory/MerchantResultSlot.java
-@@ -47,13 +47,32 @@
+@@ -47,13 +_,32 @@
  
      @Override
      public void onTake(Player player, ItemStack stack) {
 -        this.checkTakeAchievements(stack);
 +        // this.checkTakeAchievements(stack); // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent; move to after event is called and not cancelled
-         MerchantOffer merchantOffer = this.slots.getActiveOffer();
+         MerchantOffer activeOffer = this.slots.getActiveOffer();
 +        // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent
 +        io.papermc.paper.event.player.PlayerPurchaseEvent event = null;
-+        if (merchantOffer != null && player instanceof net.minecraft.server.level.ServerPlayer serverPlayer) {
++        if (activeOffer != null && player instanceof net.minecraft.server.level.ServerPlayer serverPlayer) {
 +            if (this.merchant instanceof net.minecraft.world.entity.npc.AbstractVillager abstractVillager) {
-+                event = new io.papermc.paper.event.player.PlayerTradeEvent(serverPlayer.getBukkitEntity(), (org.bukkit.entity.AbstractVillager) abstractVillager.getBukkitEntity(), merchantOffer.asBukkit(), true, true);
++                event = new io.papermc.paper.event.player.PlayerTradeEvent(serverPlayer.getBukkitEntity(), (org.bukkit.entity.AbstractVillager) abstractVillager.getBukkitEntity(), activeOffer.asBukkit(), true, true);
 +            } else if (this.merchant instanceof org.bukkit.craftbukkit.inventory.CraftMerchantCustom.MinecraftMerchant) {
-+                event = new io.papermc.paper.event.player.PlayerPurchaseEvent(serverPlayer.getBukkitEntity(), merchantOffer.asBukkit(), false, true);
++                event = new io.papermc.paper.event.player.PlayerPurchaseEvent(serverPlayer.getBukkitEntity(), activeOffer.asBukkit(), false, true);
 +            }
 +            if (event != null) {
 +                if (!event.callEvent()) {
@@ -21,17 +21,17 @@
 +                    event.getPlayer().updateInventory();
 +                    return;
 +                }
-+                merchantOffer = org.bukkit.craftbukkit.inventory.CraftMerchantRecipe.fromBukkit(event.getTrade()).toMinecraft();
++                activeOffer = org.bukkit.craftbukkit.inventory.CraftMerchantRecipe.fromBukkit(event.getTrade()).toMinecraft();
 +            }
 +        }
 +        this.checkTakeAchievements(stack);
 +        // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent
-         if (merchantOffer != null) {
-             ItemStack itemStack = this.slots.getItem(0);
-             ItemStack itemStack2 = this.slots.getItem(1);
-             if (merchantOffer.take(itemStack, itemStack2) || merchantOffer.take(itemStack2, itemStack)) {
--                this.merchant.notifyTrade(merchantOffer);
-+                this.merchant.processTrade(merchantOffer, event); // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
+         if (activeOffer != null) {
+             ItemStack item = this.slots.getItem(0);
+             ItemStack item1 = this.slots.getItem(1);
+             if (activeOffer.take(item, item1) || activeOffer.take(item1, item)) {
+-                this.merchant.notifyTrade(activeOffer);
++                this.merchant.processTrade(activeOffer, event); // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
                  player.awardStat(Stats.TRADED_WITH_VILLAGER);
-                 this.slots.setItem(0, itemStack);
-                 this.slots.setItem(1, itemStack2);
+                 this.slots.setItem(0, item);
+                 this.slots.setItem(1, item1);
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/PlayerEnderChestContainer.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/PlayerEnderChestContainer.java.patch
new file mode 100644
index 0000000000..62ebcb22a9
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/PlayerEnderChestContainer.java.patch
@@ -0,0 +1,27 @@
+--- a/net/minecraft/world/inventory/PlayerEnderChestContainer.java
++++ b/net/minecraft/world/inventory/PlayerEnderChestContainer.java
+@@ -12,9 +_,22 @@
+ public class PlayerEnderChestContainer extends SimpleContainer {
+     @Nullable
+     private EnderChestBlockEntity activeChest;
+-
+-    public PlayerEnderChestContainer() {
++    // CraftBukkit start
++    private final Player owner;
++
++    public org.bukkit.inventory.InventoryHolder getBukkitOwner() {
++        return this.owner.getBukkitEntity();
++    }
++
++    @Override
++    public org.bukkit.Location getLocation() {
++        return this.activeChest != null ? org.bukkit.craftbukkit.util.CraftLocation.toBukkit(this.activeChest.getBlockPos(), this.activeChest.getLevel().getWorld()) : null;
++    }
++
++    public PlayerEnderChestContainer(Player owner) {
+         super(27);
++        this.owner = owner;
++        // CraftBukkit end
+     }
+ 
+     public void setActiveChest(EnderChestBlockEntity enderChestBlockEntity) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/ResultContainer.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/ResultContainer.java.patch
similarity index 65%
rename from paper-server/patches/unapplied/net/minecraft/world/inventory/ResultContainer.java.patch
rename to paper-server/patches/sources/net/minecraft/world/inventory/ResultContainer.java.patch
index a26bc470cf..a384ac95a8 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/ResultContainer.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/ResultContainer.java.patch
@@ -1,21 +1,9 @@
 --- a/net/minecraft/world/inventory/ResultContainer.java
 +++ b/net/minecraft/world/inventory/ResultContainer.java
-@@ -9,12 +9,64 @@
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.item.crafting.RecipeHolder;
- 
-+// CraftBukkit start
-+import org.bukkit.Location;
-+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
-+import org.bukkit.entity.HumanEntity;
-+// CraftBukkit end
-+
- public class ResultContainer implements Container, RecipeCraftingHolder {
- 
-     private final NonNullList<ItemStack> itemStacks;
+@@ -12,6 +_,53 @@
+     private final NonNullList<ItemStack> itemStacks = NonNullList.withSize(1, ItemStack.EMPTY);
      @Nullable
      private RecipeHolder<?> recipeUsed;
- 
 +    // CraftBukkit start
 +    private int maxStack = MAX_STACK;
 +
@@ -33,10 +21,10 @@
 +    }
 +
 +    // Don't need a transaction; the InventoryCrafting keeps track of it for us
-+    public void onOpen(CraftHumanEntity who) {}
-+    public void onClose(CraftHumanEntity who) {}
-+    public java.util.List<HumanEntity> getViewers() {
-+        return new java.util.ArrayList<HumanEntity>();
++    public void onOpen(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {}
++    public void onClose(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {}
++    public java.util.List<org.bukkit.entity.HumanEntity> getViewers() {
++        return new java.util.ArrayList<>();
 +    }
 +
 +    @Override
@@ -49,7 +37,7 @@
 +    }
 +
 +    @Override
-+    public Location getLocation() {
++    public org.bukkit.Location getLocation() {
 +        return null;
 +    }
 +    // CraftBukkit end
@@ -57,11 +45,12 @@
 +    private @Nullable java.util.function.Supplier<? extends org.bukkit.inventory.InventoryHolder> holderCreator;
 +    private @Nullable org.bukkit.inventory.InventoryHolder holder;
 +    public ResultContainer(java.util.function.Supplier<? extends org.bukkit.inventory.InventoryHolder> holderCreator) {
-+        this();
 +        this.holderCreator = holderCreator;
 +    }
-+    // Paper end - Add missing InventoryHolders
 +
-     public ResultContainer() {
-         this.itemStacks = NonNullList.withSize(1, ItemStack.EMPTY);
-     }
++    public ResultContainer() {
++    }
++    // Paper end - Add missing InventoryHolders
+ 
+     @Override
+     public int getContainerSize() {
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/ResultSlot.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/ResultSlot.java.patch
new file mode 100644
index 0000000000..c8c1e2c156
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/ResultSlot.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/inventory/ResultSlot.java
++++ b/net/minecraft/world/inventory/ResultSlot.java
+@@ -72,7 +_,7 @@
+     private NonNullList<ItemStack> getRemainingItems(CraftingInput input, Level level) {
+         return level instanceof ServerLevel serverLevel
+             ? serverLevel.recipeAccess()
+-                .getRecipeFor(RecipeType.CRAFTING, input, serverLevel)
++                .getRecipeFor(RecipeType.CRAFTING, input, serverLevel, this.craftSlots.getCurrentRecipe()) // Paper - Perf: Improve mass crafting; check last recipe used first
+                 .map(recipe -> recipe.value().getRemainingItems(input))
+                 .orElseGet(() -> copyAllInputItems(input))
+             : CraftingRecipe.defaultCraftingReminder(input);
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/ShulkerBoxMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/ShulkerBoxMenu.java.patch
new file mode 100644
index 0000000000..a65b4c24dc
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/ShulkerBoxMenu.java.patch
@@ -0,0 +1,39 @@
+--- a/net/minecraft/world/inventory/ShulkerBoxMenu.java
++++ b/net/minecraft/world/inventory/ShulkerBoxMenu.java
+@@ -9,6 +_,20 @@
+ public class ShulkerBoxMenu extends AbstractContainerMenu {
+     private static final int CONTAINER_SIZE = 27;
+     private final Container container;
++    // CraftBukkit start
++    private org.bukkit.craftbukkit.inventory.CraftInventoryView bukkitEntity;
++    private Inventory player;
++
++    @Override
++    public org.bukkit.craftbukkit.inventory.CraftInventoryView getBukkitView() {
++        if (this.bukkitEntity != null) {
++            return this.bukkitEntity;
++        }
++
++        this.bukkitEntity = new org.bukkit.craftbukkit.inventory.CraftInventoryView(this.player.player.getBukkitEntity(), new org.bukkit.craftbukkit.inventory.CraftInventory(this.container), this);
++        return this.bukkitEntity;
++    }
++    // CraftBukkit end
+ 
+     public ShulkerBoxMenu(int containerId, Inventory playerInventory) {
+         this(containerId, playerInventory, new SimpleContainer(27));
+@@ -18,6 +_,7 @@
+         super(MenuType.SHULKER_BOX, containerId);
+         checkContainerSize(container, 27);
+         this.container = container;
++        this.player = playerInventory; // CraftBukkit - save player
+         container.startOpen(playerInventory.player);
+         int i = 3;
+         int i1 = 9;
+@@ -33,6 +_,7 @@
+ 
+     @Override
+     public boolean stillValid(Player player) {
++        if (!this.checkReachable) return true; // CraftBukkit
+         return this.container.stillValid(player);
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/SmithingMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/SmithingMenu.java.patch
new file mode 100644
index 0000000000..1277f3d7ac
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/SmithingMenu.java.patch
@@ -0,0 +1,50 @@
+--- a/net/minecraft/world/inventory/SmithingMenu.java
++++ b/net/minecraft/world/inventory/SmithingMenu.java
+@@ -32,6 +_,9 @@
+     private final RecipePropertySet templateItemTest;
+     private final RecipePropertySet additionItemTest;
+     private final DataSlot hasRecipeError = DataSlot.standalone();
++    // CraftBukkit start
++    private org.bukkit.craftbukkit.inventory.CraftInventoryView bukkitEntity;
++    // CraftBukkit end
+ 
+     public SmithingMenu(int containerId, Inventory playerInventory) {
+         this(containerId, playerInventory, ContainerLevelAccess.NULL);
+@@ -99,6 +_,7 @@
+         if (this.level instanceof ServerLevel) {
+             boolean flag = this.getSlot(0).hasItem() && this.getSlot(1).hasItem() && this.getSlot(2).hasItem() && !this.getSlot(this.getResultSlot()).hasItem();
+             this.hasRecipeError.set(flag ? 1 : 0);
++            org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareResultEvent(this, RESULT_SLOT); // Paper - Add PrepareResultEvent
+         }
+     }
+ 
+@@ -115,7 +_,9 @@
+         recipeFor.ifPresentOrElse(recipe -> {
+             ItemStack itemStack = recipe.value().assemble(smithingRecipeInput, this.level.registryAccess());
+             this.resultSlots.setRecipeUsed((RecipeHolder<?>)recipe);
+-            this.resultSlots.setItem(0, itemStack);
++            // CraftBukkit start
++            org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareSmithingEvent(this.getBukkitView(), itemStack);
++            // CraftBukkit end
+         }, () -> {
+             this.resultSlots.setRecipeUsed(null);
+             this.resultSlots.setItem(0, ItemStack.EMPTY);
+@@ -137,4 +_,18 @@
+     public boolean hasRecipeError() {
+         return this.hasRecipeError.get() > 0;
+     }
++
++    // CraftBukkit start
++    @Override
++    public org.bukkit.craftbukkit.inventory.CraftInventoryView getBukkitView() {
++        if (this.bukkitEntity != null) {
++            return this.bukkitEntity;
++        }
++
++        org.bukkit.craftbukkit.inventory.CraftInventory inventory = new org.bukkit.craftbukkit.inventory.CraftInventorySmithing(
++                this.access.getLocation(), this.inputSlots, this.resultSlots);
++        this.bukkitEntity = new org.bukkit.craftbukkit.inventory.CraftInventoryView(this.player.getBukkitEntity(), inventory, this);
++        return this.bukkitEntity;
++    }
++    // CraftBukkit end
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/StonecutterMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/StonecutterMenu.java.patch
new file mode 100644
index 0000000000..5f9f27b0f0
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/StonecutterMenu.java.patch
@@ -0,0 +1,128 @@
+--- a/net/minecraft/world/inventory/StonecutterMenu.java
++++ b/net/minecraft/world/inventory/StonecutterMenu.java
+@@ -25,7 +_,7 @@
+     private static final int USE_ROW_SLOT_START = 29;
+     private static final int USE_ROW_SLOT_END = 38;
+     private final ContainerLevelAccess access;
+-    final DataSlot selectedRecipeIndex = DataSlot.standalone();
++    final DataSlot selectedRecipeIndex = DataSlot.shared(new int[1], 0); // Paper - Add PlayerStonecutterRecipeSelectEvent
+     private final Level level;
+     private SelectableRecipe.SingleInputSet<StonecutterRecipe> recipesForInput = SelectableRecipe.SingleInputSet.empty();
+     private ItemStack input = ItemStack.EMPTY;
+@@ -33,15 +_,23 @@
+     final Slot inputSlot;
+     final Slot resultSlot;
+     Runnable slotUpdateListener = () -> {};
+-    public final Container container = new SimpleContainer(1) {
+-        @Override
+-        public void setChanged() {
+-            super.setChanged();
+-            StonecutterMenu.this.slotsChanged(this);
+-            StonecutterMenu.this.slotUpdateListener.run();
++    public final Container container; // Paper - Add missing InventoryHolders - move down
++    final ResultContainer resultContainer; // Paper - Add missing InventoryHolders - move down
++    // CraftBukkit start
++    private org.bukkit.craftbukkit.inventory.view.CraftStonecutterView bukkitEntity = null;
++    private final org.bukkit.entity.Player player;
++
++    @Override
++    public org.bukkit.craftbukkit.inventory.view.CraftStonecutterView getBukkitView() {
++        if (this.bukkitEntity != null) {
++            return this.bukkitEntity;
+         }
+-    };
+-    final ResultContainer resultContainer = new ResultContainer();
++
++        org.bukkit.craftbukkit.inventory.CraftInventoryStonecutter inventory = new org.bukkit.craftbukkit.inventory.CraftInventoryStonecutter(this.container, this.resultContainer);
++        this.bukkitEntity = new org.bukkit.craftbukkit.inventory.view.CraftStonecutterView(this.player, inventory, this);
++        return this.bukkitEntity;
++    }
++    // CraftBukkit end
+ 
+     public StonecutterMenu(int containerId, Inventory playerInventory) {
+         this(containerId, playerInventory, ContainerLevelAccess.NULL);
+@@ -51,6 +_,23 @@
+         super(MenuType.STONECUTTER, containerId);
+         this.access = access;
+         this.level = playerInventory.player.level();
++        // Paper start
++        this.container = new SimpleContainer(this.createBlockHolder(access), 1) { // Paper - Add missing InventoryHolders
++            @Override
++            public void setChanged() {
++                super.setChanged();
++                StonecutterMenu.this.slotsChanged(this);
++                StonecutterMenu.this.slotUpdateListener.run();
++            }
++            // CraftBukkit start
++            @Override
++            public org.bukkit.Location getLocation() {
++                return access.getLocation();
++            }
++            // CraftBukkit end
++        };
++        this.resultContainer = new ResultContainer(this.createBlockHolder(access)); // Paper - Add missing InventoryHolders
++        // Paper end
+         this.inputSlot = this.addSlot(new Slot(this.container, 0, 20, 33));
+         this.resultSlot = this.addSlot(new Slot(this.resultContainer, 1, 143, 33) {
+             @Override
+@@ -83,6 +_,7 @@
+         });
+         this.addStandardInventorySlots(playerInventory, 8, 84);
+         this.addDataSlot(this.selectedRecipeIndex);
++        this.player = (org.bukkit.entity.Player) playerInventory.player.getBukkitEntity(); // CraftBukkit
+     }
+ 
+     public int getSelectedRecipeIndex() {
+@@ -103,6 +_,7 @@
+ 
+     @Override
+     public boolean stillValid(Player player) {
++        if (!this.checkReachable) return true; // CraftBukkit
+         return stillValid(this.access, player, Blocks.STONECUTTER);
+     }
+ 
+@@ -112,8 +_,34 @@
+             return false;
+         } else {
+             if (this.isValidRecipeIndex(id)) {
+-                this.selectedRecipeIndex.set(id);
+-                this.setupResultSlot(id);
++                // Paper start - Add PlayerStonecutterRecipeSelectEvent
++                int recipeIndex = id;
++                this.selectedRecipeIndex.set(recipeIndex);
++                this.selectedRecipeIndex.checkAndClearUpdateFlag(); // mark as changed
++                paperEventBlock: if (this.isValidRecipeIndex(id)) {
++                    final Optional<RecipeHolder<StonecutterRecipe>> recipe = this.recipesForInput.entries().get(id).recipe().recipe();
++                    if (recipe.isEmpty()) break paperEventBlock; // The recipe selected does not have an actual server recipe (presumably its the empty one). Cannot call the event, just break.
++
++                    io.papermc.paper.event.player.PlayerStonecutterRecipeSelectEvent event = new io.papermc.paper.event.player.PlayerStonecutterRecipeSelectEvent((org.bukkit.entity.Player) player.getBukkitEntity(), getBukkitView().getTopInventory(), (org.bukkit.inventory.StonecuttingRecipe) recipe.get().toBukkitRecipe());
++                    if (!event.callEvent()) {
++                        player.containerMenu.sendAllDataToRemote();
++                        return false;
++                    }
++
++                    net.minecraft.resources.ResourceLocation key = org.bukkit.craftbukkit.util.CraftNamespacedKey.toMinecraft(event.getStonecuttingRecipe().getKey());
++                    if (!recipe.get().id().location().equals(key)) { // If the recipe did NOT stay the same
++                        for (int newRecipeIndex = 0; newRecipeIndex < this.recipesForInput.entries().size(); newRecipeIndex++) {
++                            if (this.recipesForInput.entries().get(newRecipeIndex).recipe().recipe().filter(r -> r.id().location().equals(key)).isPresent()) {
++                                recipeIndex = newRecipeIndex;
++                                break;
++                            }
++                        }
++                    }
++                }
++                player.containerMenu.sendAllDataToRemote();
++                this.selectedRecipeIndex.set(recipeIndex); // set new index, so that listeners can read it
++                this.setupResultSlot(recipeIndex);
++                // Paper end - Add PlayerStonecutterRecipeSelectEvent
+             }
+ 
+             return true;
+@@ -131,6 +_,7 @@
+             this.input = item.copy();
+             this.setupRecipeList(item);
+         }
++        org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareResultEvent(this, RESULT_SLOT); // Paper - Add PrepareResultEvent
+     }
+ 
+     private void setupRecipeList(ItemStack stack) {
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/TransientCraftingContainer.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/TransientCraftingContainer.java.patch
new file mode 100644
index 0000000000..85f7ca8636
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/TransientCraftingContainer.java.patch
@@ -0,0 +1,71 @@
+--- a/net/minecraft/world/inventory/TransientCraftingContainer.java
++++ b/net/minecraft/world/inventory/TransientCraftingContainer.java
+@@ -13,6 +_,68 @@
+     private final int height;
+     private final AbstractContainerMenu menu;
+ 
++    // CraftBukkit start - add fields
++    public List<org.bukkit.entity.HumanEntity> transaction = new java.util.ArrayList<>();
++    private net.minecraft.world.item.crafting.RecipeHolder<net.minecraft.world.item.crafting.CraftingRecipe> currentRecipe;
++    public net.minecraft.world.Container resultInventory;
++    private Player owner;
++    private int maxStack = MAX_STACK;
++
++    public List<ItemStack> getContents() {
++        return this.items;
++    }
++
++    public void onOpen(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.add(player);
++    }
++
++    public org.bukkit.event.inventory.InventoryType getInvType() {
++        return this.items.size() == 4 ? org.bukkit.event.inventory.InventoryType.CRAFTING : org.bukkit.event.inventory.InventoryType.WORKBENCH;
++    }
++
++    public void onClose(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.remove(player);
++    }
++
++    public List<org.bukkit.entity.HumanEntity> getViewers() {
++        return this.transaction;
++    }
++
++    public org.bukkit.inventory.InventoryHolder getOwner() {
++        return (this.owner == null) ? null : this.owner.getBukkitEntity();
++    }
++
++    @Override
++    public int getMaxStackSize() {
++        return this.maxStack;
++    }
++
++    public void setMaxStackSize(int size) {
++        this.maxStack = size;
++        this.resultInventory.setMaxStackSize(size);
++    }
++
++    @Override
++    public org.bukkit.Location getLocation() {
++        return this.menu instanceof CraftingMenu ? ((CraftingMenu) this.menu).access.getLocation() : this.owner.getBukkitEntity().getLocation();
++    }
++
++    @Override
++    public net.minecraft.world.item.crafting.RecipeHolder<net.minecraft.world.item.crafting.CraftingRecipe> getCurrentRecipe() {
++        return this.currentRecipe;
++    }
++
++    @Override
++    public void setCurrentRecipe(net.minecraft.world.item.crafting.RecipeHolder<net.minecraft.world.item.crafting.CraftingRecipe> currentRecipe) {
++        this.currentRecipe = currentRecipe;
++    }
++
++    public TransientCraftingContainer(AbstractContainerMenu menu, int width, int height, Player player) {
++        this(menu, width, height);
++        this.owner = player;
++    }
++    // CraftBukkit end
++
+     public TransientCraftingContainer(AbstractContainerMenu menu, int width, int height) {
+         this(menu, width, height, NonNullList.withSize(width * height, ItemStack.EMPTY));
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/item/ArmorStandItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/ArmorStandItem.java.patch
new file mode 100644
index 0000000000..34157daf71
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/ArmorStandItem.java.patch
@@ -0,0 +1,15 @@
+--- a/net/minecraft/world/item/ArmorStandItem.java
++++ b/net/minecraft/world/item/ArmorStandItem.java
+@@ -45,6 +_,12 @@
+ 
+                     float f = Mth.floor((Mth.wrapDegrees(context.getRotation() - 180.0F) + 22.5F) / 45.0F) * 45.0F;
+                     armorStand.moveTo(armorStand.getX(), armorStand.getY(), armorStand.getZ(), f, 0.0F);
++                    // CraftBukkit start
++                    if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPlaceEvent(context, armorStand).isCancelled()) {
++                        if (context.getPlayer() != null) context.getPlayer().containerMenu.sendAllDataToRemote(); // Paper - Fix inventory desync
++                        return InteractionResult.FAIL;
++                    }
++                    // CraftBukkit end
+                     serverLevel.addFreshEntityWithPassengers(armorStand);
+                     level.playSound(
+                         null, armorStand.getX(), armorStand.getY(), armorStand.getZ(), SoundEvents.ARMOR_STAND_PLACE, SoundSource.BLOCKS, 0.75F, 0.8F
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/AxeItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/AxeItem.java.patch
similarity index 71%
rename from paper-server/patches/unapplied/net/minecraft/world/item/AxeItem.java.patch
rename to paper-server/patches/sources/net/minecraft/world/item/AxeItem.java.patch
index 858ea3302d..f39b785ebf 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/item/AxeItem.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/item/AxeItem.java.patch
@@ -1,14 +1,14 @@
 --- a/net/minecraft/world/item/AxeItem.java
 +++ b/net/minecraft/world/item/AxeItem.java
-@@ -67,6 +67,11 @@
+@@ -67,6 +_,11 @@
                  return InteractionResult.PASS;
              } else {
-                 ItemStack itemStack = context.getItemInHand();
+                 ItemStack itemInHand = context.getItemInHand();
 +                // Paper start - EntityChangeBlockEvent
-+                if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(player, blockPos, optional.get())) {
++                if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(player, clickedPos, optional.get())) {
 +                    return InteractionResult.PASS;
 +                }
 +                // Paper end
                  if (player instanceof ServerPlayer) {
-                     CriteriaTriggers.ITEM_USED_ON_BLOCK.trigger((ServerPlayer)player, blockPos, itemStack);
+                     CriteriaTriggers.ITEM_USED_ON_BLOCK.trigger((ServerPlayer)player, clickedPos, itemInHand);
                  }
diff --git a/paper-server/patches/sources/net/minecraft/world/item/BlockItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/BlockItem.java.patch
new file mode 100644
index 0000000000..dddf4b93f2
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/BlockItem.java.patch
@@ -0,0 +1,98 @@
+--- a/net/minecraft/world/item/BlockItem.java
++++ b/net/minecraft/world/item/BlockItem.java
+@@ -30,6 +_,11 @@
+ import net.minecraft.world.level.block.state.BlockState;
+ import net.minecraft.world.level.gameevent.GameEvent;
+ import net.minecraft.world.phys.shapes.CollisionContext;
++// CraftBukkit start
++import org.bukkit.craftbukkit.block.CraftBlock;
++import org.bukkit.craftbukkit.block.data.CraftBlockData;
++import org.bukkit.event.block.BlockCanBuildEvent;
++// CraftBukkit end
+ 
+ public class BlockItem extends Item {
+     @Deprecated
+@@ -59,6 +_,14 @@
+                 return InteractionResult.FAIL;
+             } else {
+                 BlockState placementState = this.getPlacementState(blockPlaceContext);
++                // CraftBukkit start - special case for handling block placement with water lilies and snow buckets
++                org.bukkit.block.BlockState bukkitState = null;
++                if (this instanceof PlaceOnWaterBlockItem || this instanceof SolidBucketItem) {
++                    bukkitState = org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(blockPlaceContext.getLevel(), blockPlaceContext.getClickedPos());
++                }
++                final org.bukkit.block.BlockState oldBukkitState = bukkitState != null ? bukkitState : org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(blockPlaceContext.getLevel(), blockPlaceContext.getClickedPos()); // Paper - Reset placed block on exception
++                // CraftBukkit end
++
+                 if (placementState == null) {
+                     return InteractionResult.FAIL;
+                 } else if (!this.placeBlock(blockPlaceContext, placementState)) {
+@@ -71,15 +_,40 @@
+                     BlockState blockState = level.getBlockState(clickedPos);
+                     if (blockState.is(placementState.getBlock())) {
+                         blockState = this.updateBlockStateFromTag(clickedPos, level, itemInHand, blockState);
++                        // Paper start - Reset placed block on exception
++                        try {
+                         this.updateCustomBlockEntityTag(clickedPos, level, player, itemInHand, blockState);
+                         updateBlockEntityComponents(level, clickedPos, itemInHand);
++                        } catch (Exception ex) {
++                            oldBukkitState.update(true, false);
++                            if (player instanceof ServerPlayer serverPlayer) {
++                                org.apache.logging.log4j.LogManager.getLogger().error("Player {} tried placing invalid block", player.getScoreboardName(), ex);
++                                serverPlayer.getBukkitEntity().kickPlayer("Packet processing error");
++                                return InteractionResult.FAIL;
++                            }
++                            throw ex; // Rethrow exception if not placed by a player
++                        }
++                        // Paper end - Reset placed block on exception
+                         blockState.getBlock().setPlacedBy(level, clickedPos, blockState, player, itemInHand);
++                        // CraftBukkit start
++                        if (bukkitState != null) {
++                            org.bukkit.event.block.BlockPlaceEvent placeEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPlaceEvent((net.minecraft.server.level.ServerLevel) level, player, blockPlaceContext.getHand(), bukkitState, clickedPos.getX(), clickedPos.getY(), clickedPos.getZ());
++                            if (placeEvent != null && (placeEvent.isCancelled() || !placeEvent.canBuild())) {
++                                bukkitState.update(true, false);
++
++                                // Paper - if the event is called here, the inventory should be updated
++                                player.containerMenu.sendAllDataToRemote(); // SPIGOT-4541
++                                return InteractionResult.FAIL;
++                            }
++                        }
++                        // CraftBukkit end
+                         if (player instanceof ServerPlayer) {
+                             CriteriaTriggers.PLACED_BLOCK.trigger((ServerPlayer)player, clickedPos, itemInHand);
+                         }
+                     }
+ 
+                     SoundType soundType = blockState.getSoundType();
++                    if (player == null) // Paper - Fix block place logic; reintroduce this for the dispenser (i.e the shulker)
+                     level.playSound(
+                         player,
+                         clickedPos,
+@@ -140,8 +_,16 @@
+     protected boolean canPlace(BlockPlaceContext context, BlockState state) {
+         Player player = context.getPlayer();
+         CollisionContext collisionContext = player == null ? CollisionContext.empty() : CollisionContext.of(player);
+-        return (!this.mustSurvive() || state.canSurvive(context.getLevel(), context.getClickedPos()))
+-            && context.getLevel().isUnobstructed(state, context.getClickedPos(), collisionContext);
++        // CraftBukkit start
++        Level world = context.getLevel(); // Paper - Cancel hit for vanished players
++        boolean canBuild = (!this.mustSurvive() || state.canSurvive(world, context.getClickedPos())) && world.checkEntityCollision(state, player, collisionContext, context.getClickedPos(), true); // Paper - Cancel hit for vanished players
++        org.bukkit.entity.Player bukkitPlayer = (context.getPlayer() instanceof ServerPlayer) ? (org.bukkit.entity.Player) context.getPlayer().getBukkitEntity() : null;
++
++        BlockCanBuildEvent event = new BlockCanBuildEvent(CraftBlock.at(world, context.getClickedPos()), bukkitPlayer, CraftBlockData.fromData(state), canBuild, org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(context.getHand())); // Paper - Expose hand in BlockCanBuildEvent
++        world.getCraftServer().getPluginManager().callEvent(event);
++
++        return event.isBuildable();
++        // CraftBukkit end
+     }
+ 
+     protected boolean mustSurvive() {
+@@ -170,7 +_,7 @@
+                         return false;
+                     }
+ 
+-                    if (!type.onlyOpCanSetNbt() || player != null && player.canUseGameMasterBlocks()) {
++                    if (!type.onlyOpCanSetNbt() || player != null && (player.canUseGameMasterBlocks() || (player.getAbilities().instabuild && player.getBukkitEntity().hasPermission("minecraft.nbt.place")))) { // Spigot - add permission
+                         return customData.loadInto(blockEntity, level.registryAccess());
+                     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/item/BoatItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/BoatItem.java.patch
new file mode 100644
index 0000000000..44050142b3
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/BoatItem.java.patch
@@ -0,0 +1,42 @@
+--- a/net/minecraft/world/item/BoatItem.java
++++ b/net/minecraft/world/item/BoatItem.java
+@@ -30,7 +_,7 @@
+     @Override
+     public InteractionResult use(Level level, Player player, InteractionHand hand) {
+         ItemStack itemInHand = player.getItemInHand(hand);
+-        HitResult playerPovHitResult = getPlayerPOVHitResult(level, player, ClipContext.Fluid.ANY);
++        net.minecraft.world.phys.BlockHitResult playerPovHitResult = getPlayerPOVHitResult(level, player, ClipContext.Fluid.ANY); // Paper
+         if (playerPovHitResult.getType() == HitResult.Type.MISS) {
+             return InteractionResult.PASS;
+         } else {
+@@ -51,6 +_,13 @@
+             }
+ 
+             if (playerPovHitResult.getType() == HitResult.Type.BLOCK) {
++                // CraftBukkit start - Boat placement
++                org.bukkit.event.player.PlayerInteractEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerInteractEvent(player, org.bukkit.event.block.Action.RIGHT_CLICK_BLOCK, playerPovHitResult.getBlockPos(), playerPovHitResult.getDirection(), itemInHand, false, hand, playerPovHitResult.getLocation());
++
++                if (event.isCancelled()) {
++                    return InteractionResult.PASS;
++                }
++                // CraftBukkit end
+                 AbstractBoat boat = this.getBoat(level, playerPovHitResult, itemInHand, player);
+                 if (boat == null) {
+                     return InteractionResult.FAIL;
+@@ -60,7 +_,15 @@
+                         return InteractionResult.FAIL;
+                     } else {
+                         if (!level.isClientSide) {
+-                            level.addFreshEntity(boat);
++                            // CraftBukkit start
++                            if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPlaceEvent(level, playerPovHitResult.getBlockPos(), player.getDirection(), player, boat, hand).isCancelled()) {
++                                return InteractionResult.FAIL;
++                            }
++
++                            if (!level.addFreshEntity(boat)) {
++                                return InteractionResult.PASS;
++                            }
++                            // CraftBukkit end
+                             level.gameEvent(player, GameEvent.ENTITY_PLACE, playerPovHitResult.getLocation());
+                             itemInHand.consume(1, player);
+                         }
diff --git a/paper-server/patches/sources/net/minecraft/world/item/BoneMealItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/BoneMealItem.java.patch
new file mode 100644
index 0000000000..a7ffb1e580
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/BoneMealItem.java.patch
@@ -0,0 +1,30 @@
+--- a/net/minecraft/world/item/BoneMealItem.java
++++ b/net/minecraft/world/item/BoneMealItem.java
+@@ -33,12 +_,17 @@
+ 
+     @Override
+     public InteractionResult useOn(UseOnContext context) {
++        // CraftBukkit start - extract bonemeal application logic to separate, static method
++        return BoneMealItem.applyBonemeal(context);
++    }
++    public static InteractionResult applyBonemeal(UseOnContext context) {
++        // CraftBukkit end
+         Level level = context.getLevel();
+         BlockPos clickedPos = context.getClickedPos();
+         BlockPos blockPos = clickedPos.relative(context.getClickedFace());
+         if (growCrop(context.getItemInHand(), level, clickedPos)) {
+             if (!level.isClientSide) {
+-                context.getPlayer().gameEvent(GameEvent.ITEM_INTERACT_FINISH);
++                if (context.getPlayer() != null) context.getPlayer().gameEvent(GameEvent.ITEM_INTERACT_FINISH); // CraftBukkit - SPIGOT-7518
+                 level.levelEvent(1505, clickedPos, 15);
+             }
+ 
+@@ -48,7 +_,7 @@
+             boolean isFaceSturdy = blockState.isFaceSturdy(level, clickedPos, context.getClickedFace());
+             if (isFaceSturdy && growWaterPlant(context.getItemInHand(), level, blockPos, context.getClickedFace())) {
+                 if (!level.isClientSide) {
+-                    context.getPlayer().gameEvent(GameEvent.ITEM_INTERACT_FINISH);
++                    if (context.getPlayer() != null) context.getPlayer().gameEvent(GameEvent.ITEM_INTERACT_FINISH); // CraftBukkit - SPIGOT-7518
+                     level.levelEvent(1505, blockPos, 15);
+                 }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/item/BucketItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/BucketItem.java.patch
new file mode 100644
index 0000000000..6a14409bf4
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/BucketItem.java.patch
@@ -0,0 +1,92 @@
+--- a/net/minecraft/world/item/BucketItem.java
++++ b/net/minecraft/world/item/BucketItem.java
+@@ -29,6 +_,7 @@
+ import net.minecraft.world.phys.HitResult;
+ 
+ public class BucketItem extends Item implements DispensibleContainerItem {
++    private static @Nullable ItemStack itemLeftInHandAfterPlayerBucketEmptyEvent = null; // Paper - Fix PlayerBucketEmptyEvent result itemstack
+     public final Fluid content;
+ 
+     public BucketItem(Fluid content, Item.Properties properties) {
+@@ -55,12 +_,23 @@
+             } else if (this.content == Fluids.EMPTY) {
+                 BlockState blockState = level.getBlockState(blockPos);
+                 if (blockState.getBlock() instanceof BucketPickup bucketPickup) {
++                    // CraftBukkit start
++                    ItemStack dummyFluid = bucketPickup.pickupBlock(player, org.bukkit.craftbukkit.util.DummyGeneratorAccess.INSTANCE, blockPos, blockState);
++                    if (dummyFluid.isEmpty()) return InteractionResult.FAIL; // Don't fire event if the bucket won't be filled.
++                    org.bukkit.event.player.PlayerBucketFillEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerBucketFillEvent((net.minecraft.server.level.ServerLevel) level, player, blockPos, blockPos, playerPovHitResult.getDirection(), itemInHand, dummyFluid.getItem(), hand);
++
++                    if (event.isCancelled()) {
++                        // ((ServerPlayer) user).connection.send(new ClientboundBlockUpdatePacket(level, blockPos)); // SPIGOT-5163 (see PlayerInteractManager) // Paper - Don't resend blocks
++                        ((ServerPlayer) player).getBukkitEntity().updateInventory(); // SPIGOT-4541
++                        return InteractionResult.FAIL;
++                    }
++                    // CraftBukkit end
+                     ItemStack itemStack = bucketPickup.pickupBlock(player, level, blockPos, blockState);
+                     if (!itemStack.isEmpty()) {
+                         player.awardStat(Stats.ITEM_USED.get(this));
+                         bucketPickup.getPickupSound().ifPresent(soundEvent -> player.playSound(soundEvent, 1.0F, 1.0F));
+                         level.gameEvent(player, GameEvent.FLUID_PICKUP, blockPos);
+-                        ItemStack itemStack1 = ItemUtils.createFilledResult(itemInHand, player, itemStack);
++                        ItemStack itemStack1 = ItemUtils.createFilledResult(itemInHand, player, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItemStack())); // CraftBukkit
+                         if (!level.isClientSide) {
+                             CriteriaTriggers.FILLED_BUCKET.trigger((ServerPlayer)player, itemStack);
+                         }
+@@ -73,7 +_,7 @@
+             } else {
+                 BlockState blockState = level.getBlockState(blockPos);
+                 BlockPos blockPos2 = blockState.getBlock() instanceof LiquidBlockContainer && this.content == Fluids.WATER ? blockPos : blockPos1;
+-                if (this.emptyContents(player, level, blockPos2, playerPovHitResult)) {
++                if (this.emptyContents(player, level, blockPos2, playerPovHitResult, playerPovHitResult.getDirection(), blockPos, itemInHand, hand)) { // CraftBukkit
+                     this.checkExtraContent(player, level, itemInHand, blockPos2);
+                     if (player instanceof ServerPlayer) {
+                         CriteriaTriggers.PLACED_BLOCK.trigger((ServerPlayer)player, blockPos2, itemInHand);
+@@ -90,6 +_,13 @@
+     }
+ 
+     public static ItemStack getEmptySuccessItem(ItemStack bucketStack, Player player) {
++        // Paper start - Fix PlayerBucketEmptyEvent result itemstack
++        if (itemLeftInHandAfterPlayerBucketEmptyEvent != null) {
++            ItemStack itemInHand = itemLeftInHandAfterPlayerBucketEmptyEvent;
++            itemLeftInHandAfterPlayerBucketEmptyEvent = null;
++            return itemInHand;
++        }
++        // Paper end - Fix PlayerBucketEmptyEvent result itemstack
+         return !player.hasInfiniteMaterials() ? new ItemStack(Items.BUCKET) : bucketStack;
+     }
+ 
+@@ -99,6 +_,12 @@
+ 
+     @Override
+     public boolean emptyContents(@Nullable Player player, Level level, BlockPos pos, @Nullable BlockHitResult result) {
++        // CraftBukkit start
++        return this.emptyContents(player, level, pos, result, null, null, null, InteractionHand.MAIN_HAND);
++    }
++
++    public boolean emptyContents(@Nullable Player player, Level level, BlockPos pos, @Nullable BlockHitResult result, Direction enumdirection, BlockPos clicked, ItemStack itemstack, InteractionHand enumhand) {
++        // CraftBukkit end
+         if (!(this.content instanceof FlowingFluid flowingFluid)) {
+             return false;
+         } else {
+@@ -109,8 +_,19 @@
+                 || canBeReplaced
+                 || block instanceof LiquidBlockContainer liquidBlockContainer
+                     && liquidBlockContainer.canPlaceLiquid(player, level, pos, blockState, this.content);
++            // CraftBukkit start
++            if (flag && player != null) {
++                org.bukkit.event.player.PlayerBucketEmptyEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerBucketEmptyEvent((net.minecraft.server.level.ServerLevel) level, player, pos, clicked, enumdirection, itemstack, enumhand);
++                if (event.isCancelled()) {
++                    // ((ServerPlayer) player).connection.send(new ClientboundBlockUpdatePacket(level, pos)); // SPIGOT-4238: needed when looking through entity // Paper - Don't resend blocks
++                    ((ServerPlayer) player).getBukkitEntity().updateInventory(); // SPIGOT-4541
++                    return false;
++                }
++                itemLeftInHandAfterPlayerBucketEmptyEvent = event.getItemStack() != null ? event.getItemStack().equals(org.bukkit.craftbukkit.inventory.CraftItemStack.asNewCraftStack(net.minecraft.world.item.Items.BUCKET)) ? null : org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItemStack()) : ItemStack.EMPTY; // Paper - Fix PlayerBucketEmptyEvent result itemstack
++            }
++            // CraftBukkit end
+             if (!flag) {
+-                return result != null && this.emptyContents(player, level, result.getBlockPos().relative(result.getDirection()), null);
++                return result != null && this.emptyContents(player, level, result.getBlockPos().relative(result.getDirection()), null, enumdirection, clicked, itemstack, enumhand); // CraftBukkit
+             } else if (level.dimensionType().ultraWarm() && this.content.is(FluidTags.WATER)) {
+                 int x = pos.getX();
+                 int y = pos.getY();
diff --git a/paper-server/patches/sources/net/minecraft/world/item/CrossbowItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/CrossbowItem.java.patch
new file mode 100644
index 0000000000..2f24e0047f
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/CrossbowItem.java.patch
@@ -0,0 +1,47 @@
+--- a/net/minecraft/world/item/CrossbowItem.java
++++ b/net/minecraft/world/item/CrossbowItem.java
+@@ -90,7 +_,14 @@
+     public boolean releaseUsing(ItemStack stack, Level level, LivingEntity entity, int timeLeft) {
+         int i = this.getUseDuration(stack, entity) - timeLeft;
+         float powerForTime = getPowerForTime(i, stack, entity);
+-        if (powerForTime >= 1.0F && !isCharged(stack) && tryLoadProjectiles(entity, stack)) {
++        // Paper start - Add EntityLoadCrossbowEvent
++        if (powerForTime >= 1.0F && !isCharged(stack)) {
++            final io.papermc.paper.event.entity.EntityLoadCrossbowEvent event = new io.papermc.paper.event.entity.EntityLoadCrossbowEvent(entity.getBukkitLivingEntity(), stack.asBukkitMirror(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(entity.getUsedItemHand()));
++            if (!event.callEvent() || !tryLoadProjectiles(entity, stack, event.shouldConsumeItem()) || !event.shouldConsumeItem()) {
++                if (entity instanceof ServerPlayer player) player.containerMenu.sendAllDataToRemote();
++                return false;
++            }
++            // Paper end - Add EntityLoadCrossbowEvent
+             CrossbowItem.ChargingSounds chargingSounds = this.getChargingSounds(stack);
+             chargingSounds.end()
+                 .ifPresent(
+@@ -111,8 +_,14 @@
+         }
+     }
+ 
++    @io.papermc.paper.annotation.DoNotUse // Paper - Add EntityLoadCrossbowEvent
+     private static boolean tryLoadProjectiles(LivingEntity shooter, ItemStack crossbowStack) {
+-        List<ItemStack> list = draw(crossbowStack, shooter.getProjectile(crossbowStack), shooter);
++        // Paper start - Add EntityLoadCrossbowEvent
++        return CrossbowItem.tryLoadProjectiles(shooter, crossbowStack, true);
++    }
++    private static boolean tryLoadProjectiles(LivingEntity shooter, ItemStack crossbowStack, boolean consume) {
++        List<ItemStack> list = draw(crossbowStack, shooter.getProjectile(crossbowStack), shooter, consume);
++        // Paper end - Add EntityLoadCrossbowEvent
+         if (!list.isEmpty()) {
+             crossbowStack.set(DataComponents.CHARGED_PROJECTILES, ChargedProjectiles.of(list));
+             return true;
+@@ -164,7 +_,11 @@
+     @Override
+     protected Projectile createProjectile(Level level, LivingEntity shooter, ItemStack weapon, ItemStack ammo, boolean isCrit) {
+         if (ammo.is(Items.FIREWORK_ROCKET)) {
+-            return new FireworkRocketEntity(level, ammo, shooter, shooter.getX(), shooter.getEyeY() - 0.15F, shooter.getZ(), true);
++            // Paper start
++            FireworkRocketEntity entity =  new FireworkRocketEntity(level, ammo, shooter, shooter.getX(), shooter.getEyeY() - 0.15F, shooter.getZ(), true);
++            entity.spawningEntity = shooter.getUUID(); // Paper
++            return entity;
++            // Paper end
+         } else {
+             Projectile projectile = super.createProjectile(level, shooter, weapon, ammo, isCrit);
+             if (projectile instanceof AbstractArrow abstractArrow) {
diff --git a/paper-server/patches/sources/net/minecraft/world/item/DebugStickItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/DebugStickItem.java.patch
new file mode 100644
index 0000000000..8b7cbe2726
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/DebugStickItem.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/item/DebugStickItem.java
++++ b/net/minecraft/world/item/DebugStickItem.java
+@@ -51,7 +_,7 @@
+     public boolean handleInteraction(
+         Player player, BlockState stateClicked, LevelAccessor accessor, BlockPos pos, boolean shouldCycleState, ItemStack debugStack
+     ) {
+-        if (!player.canUseGameMasterBlocks()) {
++        if (!player.canUseGameMasterBlocks() && !(player.getAbilities().instabuild && player.getBukkitEntity().hasPermission("minecraft.debugstick")) && !player.getBukkitEntity().hasPermission("minecraft.debugstick.always")) { // Spigot
+             return false;
+         } else {
+             Holder<Block> blockHolder = stateClicked.getBlockHolder();
diff --git a/paper-server/patches/sources/net/minecraft/world/item/DyeItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/DyeItem.java.patch
new file mode 100644
index 0000000000..b19c70d742
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/DyeItem.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/item/DyeItem.java
++++ b/net/minecraft/world/item/DyeItem.java
+@@ -28,6 +_,17 @@
+             sheep.level().playSound(player, sheep, SoundEvents.DYE_USE, SoundSource.PLAYERS, 1.0F, 1.0F);
+             if (!player.level().isClientSide) {
+                 sheep.setColor(this.dyeColor);
++                // CraftBukkit start
++                byte bColor = (byte) this.dyeColor.getId();
++                org.bukkit.event.entity.SheepDyeWoolEvent event = new org.bukkit.event.entity.SheepDyeWoolEvent((org.bukkit.entity.Sheep) sheep.getBukkitEntity(), org.bukkit.DyeColor.getByWoolData(bColor), (org.bukkit.entity.Player) player.getBukkitEntity());
++                sheep.level().getCraftServer().getPluginManager().callEvent(event);
++
++                if (event.isCancelled()) {
++                    return InteractionResult.PASS;
++                }
++
++                sheep.setColor(DyeColor.byId((byte) event.getColor().getWoolData()));
++                // CraftBukkit end
+                 stack.shrink(1);
+             }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/item/EggItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/EggItem.java.patch
new file mode 100644
index 0000000000..c1b9e1c9dd
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/EggItem.java.patch
@@ -0,0 +1,53 @@
+--- a/net/minecraft/world/item/EggItem.java
++++ b/net/minecraft/world/item/EggItem.java
+@@ -23,22 +_,36 @@
+     @Override
+     public InteractionResult use(Level level, Player player, InteractionHand hand) {
+         ItemStack itemInHand = player.getItemInHand(hand);
+-        level.playSound(
+-            null,
+-            player.getX(),
+-            player.getY(),
+-            player.getZ(),
+-            SoundEvents.EGG_THROW,
+-            SoundSource.PLAYERS,
+-            0.5F,
+-            0.4F / (level.getRandom().nextFloat() * 0.4F + 0.8F)
+-        );
+         if (level instanceof ServerLevel serverLevel) {
+-            Projectile.spawnProjectileFromRotation(ThrownEgg::new, serverLevel, itemInHand, player, 0.0F, PROJECTILE_SHOOT_POWER, 1.0F);
++            // CraftBukkit start
++            // Paper start - PlayerLaunchProjectileEvent
++            final Projectile.Delayed<ThrownEgg> thrownEgg = Projectile.spawnProjectileFromRotationDelayed(ThrownEgg::new, serverLevel, itemInHand, player, 0.0F, EggItem.PROJECTILE_SHOOT_POWER, 1.0F);
++            com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) player.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemInHand), (org.bukkit.entity.Projectile) thrownEgg.projectile().getBukkitEntity());
++            if (event.callEvent() && thrownEgg.attemptSpawn()) {
++                if (event.shouldConsume()) {
++                    itemInHand.consume(1, player);
++                } else {
++                    player.containerMenu.sendAllDataToRemote();
++                }
++                level.playSound(
++                    null,
++                    player.getX(),
++                    player.getY(),
++                    player.getZ(),
++                    SoundEvents.EGG_THROW,
++                    SoundSource.PLAYERS,
++                    0.5F,
++                    0.4F / (level.getRandom().nextFloat() * 0.4F + 0.8F)
++                );
++                player.awardStat(Stats.ITEM_USED.get(this));
++            } else {
++                // Paper end - PlayerLaunchProjectileEvent
++                player.containerMenu.sendAllDataToRemote();
++                return InteractionResult.FAIL;
++            }
++            // CraftBukkit end
+         }
+-
+-        player.awardStat(Stats.ITEM_USED.get(this));
+-        itemInHand.consume(1, player);
++        // Paper - PlayerLaunchProjectileEvent - moved up
+         return InteractionResult.SUCCESS;
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/item/EndCrystalItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/EndCrystalItem.java.patch
new file mode 100644
index 0000000000..f6474104df
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/EndCrystalItem.java.patch
@@ -0,0 +1,30 @@
+--- a/net/minecraft/world/item/EndCrystalItem.java
++++ b/net/minecraft/world/item/EndCrystalItem.java
+@@ -27,7 +_,7 @@
+         if (!blockState.is(Blocks.OBSIDIAN) && !blockState.is(Blocks.BEDROCK)) {
+             return InteractionResult.FAIL;
+         } else {
+-            BlockPos blockPos = clickedPos.above();
++            BlockPos blockPos = clickedPos.above(); final BlockPos aboveBlockPosition = blockPos; // Paper - OBFHELPER
+             if (!level.isEmptyBlock(blockPos)) {
+                 return InteractionResult.FAIL;
+             } else {
+@@ -41,11 +_,17 @@
+                     if (level instanceof ServerLevel) {
+                         EndCrystal endCrystal = new EndCrystal(level, d + 0.5, d1, d2 + 0.5);
+                         endCrystal.setShowBottom(false);
++                        // CraftBukkit start
++                        if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPlaceEvent(context, endCrystal).isCancelled()) {
++                            if (context.getPlayer() != null) context.getPlayer().containerMenu.sendAllDataToRemote(); // Paper - Fix inventory desync
++                            return InteractionResult.FAIL;
++                        }
++                        // CraftBukkit end
+                         level.addFreshEntity(endCrystal);
+                         level.gameEvent(context.getPlayer(), GameEvent.ENTITY_PLACE, blockPos);
+                         EndDragonFight dragonFight = ((ServerLevel)level).getDragonFight();
+                         if (dragonFight != null) {
+-                            dragonFight.tryRespawn();
++                            dragonFight.tryRespawn(aboveBlockPosition); // Paper - Perf: Do crystal-portal proximity check before entity lookup
+                         }
+                     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/item/EnderEyeItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/EnderEyeItem.java.patch
new file mode 100644
index 0000000000..1145ec35de
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/EnderEyeItem.java.patch
@@ -0,0 +1,56 @@
+--- a/net/minecraft/world/item/EnderEyeItem.java
++++ b/net/minecraft/world/item/EnderEyeItem.java
+@@ -42,6 +_,11 @@
+             return InteractionResult.SUCCESS;
+         } else {
+             BlockState blockState1 = blockState.setValue(EndPortalFrameBlock.HAS_EYE, Boolean.valueOf(true));
++            // Paper start
++            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(context.getPlayer(), clickedPos, blockState1)) {
++                return InteractionResult.PASS;
++            }
++            // Paper end
+             Block.pushEntitiesUp(blockState, blockState1, level, clickedPos);
+             level.setBlock(clickedPos, blockState1, 2);
+             level.updateNeighbourForOutputSignal(clickedPos, Blocks.END_PORTAL_FRAME);
+@@ -57,7 +_,27 @@
+                     }
+                 }
+ 
+-                level.globalLevelEvent(1038, blockPos.offset(1, 0, 1), 0);
++                // CraftBukkit start - Use relative location for far away sounds
++                // level.globalLevelEvent(1038, blockPos.offset(1, 0, 1), 0);
++                int viewDistance = level.getCraftServer().getViewDistance() * 16;
++                BlockPos soundPos = blockPos.offset(1, 0, 1);
++                final net.minecraft.server.level.ServerLevel serverLevel = (net.minecraft.server.level.ServerLevel) level; // Paper - respect global sound events gamerule - ensured by isClientSide check above
++                for (ServerPlayer player : serverLevel.getPlayersForGlobalSoundGamerule()) { // Paper - respect global sound events gamerule
++                    double deltaX = soundPos.getX() - player.getX();
++                    double deltaZ = soundPos.getZ() - player.getZ();
++                    double distanceSquared = deltaX * deltaX + deltaZ * deltaZ;
++                    final double soundRadiusSquared = serverLevel.getGlobalSoundRangeSquared(config -> config.endPortalSoundRadius); // Paper - respect global sound events gamerule
++                    if (!serverLevel.getGameRules().getBoolean(net.minecraft.world.level.GameRules.RULE_GLOBAL_SOUND_EVENTS) && distanceSquared > soundRadiusSquared) continue; // Spigot // Paper - respect global sound events gamerule
++                    if (distanceSquared > viewDistance * viewDistance) {
++                        double deltaLength = Math.sqrt(distanceSquared);
++                        double relativeX = player.getX() + (deltaX / deltaLength) * viewDistance;
++                        double relativeZ = player.getZ() + (deltaZ / deltaLength) * viewDistance;
++                        player.connection.send(new net.minecraft.network.protocol.game.ClientboundLevelEventPacket(net.minecraft.world.level.block.LevelEvent.SOUND_END_PORTAL_SPAWN, new BlockPos((int) relativeX, (int) soundPos.getY(), (int) relativeZ), 0, true));
++                    } else {
++                        player.connection.send(new net.minecraft.network.protocol.game.ClientboundLevelEventPacket(net.minecraft.world.level.block.LevelEvent.SOUND_END_PORTAL_SPAWN, soundPos, 0, true));
++                    }
++                }
++                // CraftBukkit end
+             }
+ 
+             return InteractionResult.SUCCESS;
+@@ -87,7 +_,11 @@
+                 eyeOfEnder.setItem(itemInHand);
+                 eyeOfEnder.signalTo(blockPos);
+                 level.gameEvent(GameEvent.PROJECTILE_SHOOT, eyeOfEnder.position(), GameEvent.Context.of(player));
+-                level.addFreshEntity(eyeOfEnder);
++                // CraftBukkit start
++                if (!level.addFreshEntity(eyeOfEnder)) {
++                    return InteractionResult.FAIL;
++                }
++                // CraftBukkit end
+                 if (player instanceof ServerPlayer serverPlayer) {
+                     CriteriaTriggers.USED_ENDER_EYE.trigger(serverPlayer, blockPos);
+                 }
diff --git a/paper-server/patches/sources/net/minecraft/world/item/EnderpearlItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/EnderpearlItem.java.patch
new file mode 100644
index 0000000000..0b1cbebad3
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/EnderpearlItem.java.patch
@@ -0,0 +1,54 @@
+--- a/net/minecraft/world/item/EnderpearlItem.java
++++ b/net/minecraft/world/item/EnderpearlItem.java
+@@ -21,22 +_,38 @@
+     @Override
+     public InteractionResult use(Level level, Player player, InteractionHand hand) {
+         ItemStack itemInHand = player.getItemInHand(hand);
+-        level.playSound(
+-            null,
+-            player.getX(),
+-            player.getY(),
+-            player.getZ(),
+-            SoundEvents.ENDER_PEARL_THROW,
+-            SoundSource.NEUTRAL,
+-            0.5F,
+-            0.4F / (level.getRandom().nextFloat() * 0.4F + 0.8F)
+-        );
+         if (level instanceof ServerLevel serverLevel) {
+-            Projectile.spawnProjectileFromRotation(ThrownEnderpearl::new, serverLevel, itemInHand, player, 0.0F, PROJECTILE_SHOOT_POWER, 1.0F);
++            // CraftBukkit start
++            // Paper start - PlayerLaunchProjectileEvent
++            final Projectile.Delayed<ThrownEnderpearl> thrownEnderpearl = Projectile.spawnProjectileFromRotationDelayed(ThrownEnderpearl::new, serverLevel, itemInHand, player, 0.0F, EnderpearlItem.PROJECTILE_SHOOT_POWER, 1.0F);
++            com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) player.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemInHand), (org.bukkit.entity.Projectile) thrownEnderpearl.projectile().getBukkitEntity());
++            if (event.callEvent() && thrownEnderpearl.attemptSpawn()) {
++                if (event.shouldConsume()) {
++                    itemInHand.consume(1, player);
++                } else {
++                    player.containerMenu.sendAllDataToRemote();
++                }
++
++                level.playSound(
++                    null,
++                    player.getX(),
++                    player.getY(),
++                    player.getZ(),
++                    SoundEvents.ENDER_PEARL_THROW,
++                    SoundSource.NEUTRAL,
++                    0.5F,
++                    0.4F / (level.getRandom().nextFloat() * 0.4F + 0.8F)
++                );
++                player.awardStat(Stats.ITEM_USED.get(this));
++            } else {
++            // Paper end - PlayerLaunchProjectileEvent
++                player.containerMenu.sendAllDataToRemote();
++                return InteractionResult.FAIL;
++            }
+         }
++        // CraftBukkit end
+ 
+-        player.awardStat(Stats.ITEM_USED.get(this));
+-        itemInHand.consume(1, player);
++        // Paper - PlayerLaunchProjectileEvent - moved up
+         return InteractionResult.SUCCESS;
+     }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/item/ExperienceBottleItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/ExperienceBottleItem.java.patch
new file mode 100644
index 0000000000..9ec3ea678d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/ExperienceBottleItem.java.patch
@@ -0,0 +1,52 @@
+--- a/net/minecraft/world/item/ExperienceBottleItem.java
++++ b/net/minecraft/world/item/ExperienceBottleItem.java
+@@ -21,22 +_,36 @@
+     @Override
+     public InteractionResult use(Level level, Player player, InteractionHand hand) {
+         ItemStack itemInHand = player.getItemInHand(hand);
+-        level.playSound(
+-            null,
+-            player.getX(),
+-            player.getY(),
+-            player.getZ(),
+-            SoundEvents.EXPERIENCE_BOTTLE_THROW,
+-            SoundSource.NEUTRAL,
+-            0.5F,
+-            0.4F / (level.getRandom().nextFloat() * 0.4F + 0.8F)
+-        );
++        // Paper - PlayerLaunchProjectileEvent - moved down
+         if (level instanceof ServerLevel serverLevel) {
+-            Projectile.spawnProjectileFromRotation(ThrownExperienceBottle::new, serverLevel, itemInHand, player, -20.0F, 0.7F, 1.0F);
++            // Paper start - PlayerLaunchProjectileEvent
++            final Projectile.Delayed<ThrownExperienceBottle> thrownExperienceBottle = Projectile.spawnProjectileFromRotationDelayed(ThrownExperienceBottle::new, serverLevel, itemInHand, player, -20.0F, 0.7F, 1.0F);
++            com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) player.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemInHand), (org.bukkit.entity.Projectile) thrownExperienceBottle.projectile().getBukkitEntity());
++            if (event.callEvent() && thrownExperienceBottle.attemptSpawn()) {
++                if (event.shouldConsume()) {
++                    itemInHand.consume(1, player);
++                } else {
++                    player.containerMenu.sendAllDataToRemote();
++                }
++
++                level.playSound(
++                    null,
++                    player.getX(),
++                    player.getY(),
++                    player.getZ(),
++                    SoundEvents.EXPERIENCE_BOTTLE_THROW,
++                    SoundSource.NEUTRAL,
++                    0.5F,
++                    0.4F / (level.getRandom().nextFloat() * 0.4F + 0.8F)
++                );
++            } else {
++                player.containerMenu.sendAllDataToRemote();
++                return InteractionResult.FAIL;
++            }
++            // Paper end - PlayerLaunchProjectileEvent
+         }
+ 
+-        player.awardStat(Stats.ITEM_USED.get(this));
+-        itemInHand.consume(1, player);
++        // Paper - PlayerLaunchProjectileEvent - moved up
+         return InteractionResult.SUCCESS;
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/item/FireChargeItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/FireChargeItem.java.patch
new file mode 100644
index 0000000000..a182da8d73
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/FireChargeItem.java.patch
@@ -0,0 +1,31 @@
+--- a/net/minecraft/world/item/FireChargeItem.java
++++ b/net/minecraft/world/item/FireChargeItem.java
+@@ -35,12 +_,28 @@
+         if (!CampfireBlock.canLight(blockState) && !CandleBlock.canLight(blockState) && !CandleCakeBlock.canLight(blockState)) {
+             clickedPos = clickedPos.relative(context.getClickedFace());
+             if (BaseFireBlock.canBePlacedAt(level, clickedPos, context.getHorizontalDirection())) {
++                // CraftBukkit start - fire BlockIgniteEvent
++                if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(level, clickedPos, org.bukkit.event.block.BlockIgniteEvent.IgniteCause.FIREBALL, context.getPlayer()).isCancelled()) {
++                    if (!context.getPlayer().getAbilities().instabuild) {
++                        context.getItemInHand().shrink(1);
++                    }
++                    return InteractionResult.PASS;
++                }
++                // CraftBukkit end
+                 this.playSound(level, clickedPos);
+                 level.setBlockAndUpdate(clickedPos, BaseFireBlock.getState(level, clickedPos));
+                 level.gameEvent(context.getPlayer(), GameEvent.BLOCK_PLACE, clickedPos);
+                 flag = true;
+             }
+         } else {
++            // CraftBukkit start - fire BlockIgniteEvent
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(level, clickedPos, org.bukkit.event.block.BlockIgniteEvent.IgniteCause.FIREBALL, context.getPlayer()).isCancelled()) {
++                if (!context.getPlayer().getAbilities().instabuild) {
++                    context.getItemInHand().shrink(1);
++                }
++                return InteractionResult.PASS;
++            }
++            // CraftBukkit end
+             this.playSound(level, clickedPos);
+             level.setBlockAndUpdate(clickedPos, blockState.setValue(BlockStateProperties.LIT, Boolean.valueOf(true)));
+             level.gameEvent(context.getPlayer(), GameEvent.BLOCK_CHANGE, clickedPos);
diff --git a/paper-server/patches/sources/net/minecraft/world/item/FireworkRocketItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/FireworkRocketItem.java.patch
new file mode 100644
index 0000000000..00153390e9
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/FireworkRocketItem.java.patch
@@ -0,0 +1,53 @@
+--- a/net/minecraft/world/item/FireworkRocketItem.java
++++ b/net/minecraft/world/item/FireworkRocketItem.java
+@@ -33,7 +_,7 @@
+             ItemStack itemInHand = context.getItemInHand();
+             Vec3 clickLocation = context.getClickLocation();
+             Direction clickedFace = context.getClickedFace();
+-            Projectile.spawnProjectile(
++            final Projectile.Delayed<FireworkRocketEntity> fireworkRocketEntity = Projectile.spawnProjectileDelayed( // Paper - PlayerLaunchProjectileEvent
+                 new FireworkRocketEntity(
+                     level,
+                     context.getPlayer(),
+@@ -43,9 +_,14 @@
+                     itemInHand
+                 ),
+                 serverLevel,
+-                itemInHand
++                itemInHand, f -> f.spawningEntity = context.getPlayer() == null ? null : context.getPlayer().getUUID() // Paper - firework api - assign spawning entity uuid
+             );
+-            itemInHand.shrink(1);
++            // Paper start - PlayerLaunchProjectileEvent
++            com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) context.getPlayer().getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemInHand), (org.bukkit.entity.Firework) fireworkRocketEntity.projectile().getBukkitEntity());
++            if (!event.callEvent() || !fireworkRocketEntity.attemptSpawn()) return InteractionResult.PASS;
++            if (event.shouldConsume() && !context.getPlayer().hasInfiniteMaterials()) itemInHand.shrink(1);
++            else context.getPlayer().containerMenu.sendAllDataToRemote();
++            // Paper end - PlayerLaunchProjectileEvent
+         }
+ 
+         return InteractionResult.SUCCESS;
+@@ -56,9 +_,21 @@
+         if (player.isFallFlying()) {
+             ItemStack itemInHand = player.getItemInHand(hand);
+             if (level instanceof ServerLevel serverLevel) {
+-                Projectile.spawnProjectile(new FireworkRocketEntity(level, itemInHand, player), serverLevel, itemInHand);
+-                itemInHand.consume(1, player);
+-                player.awardStat(Stats.ITEM_USED.get(this));
++                // Paper start - PlayerElytraBoostEvent
++                final Projectile.Delayed<FireworkRocketEntity> delayed = Projectile.spawnProjectileDelayed(new FireworkRocketEntity(level, itemInHand, player), serverLevel, itemInHand, f -> f.spawningEntity = player.getUUID()); // Paper - firework api - assign spawning entity uuid
++                com.destroystokyo.paper.event.player.PlayerElytraBoostEvent event = new com.destroystokyo.paper.event.player.PlayerElytraBoostEvent((org.bukkit.entity.Player) player.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemInHand), (org.bukkit.entity.Firework) delayed.projectile().getBukkitEntity(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand));
++                if (event.callEvent() && delayed.attemptSpawn()) {
++                    player.awardStat(Stats.ITEM_USED.get(this)); // Moved up from below
++                    if (event.shouldConsume() && !player.hasInfiniteMaterials()) {
++                        itemInHand.shrink(1); // Moved up from below
++                    } else {
++                        player.containerMenu.sendAllDataToRemote();
++                    }
++                } else {
++                    player.containerMenu.sendAllDataToRemote();
++                }
++                // Moved up consume and changed consume to shrink
++                // Paper end - PlayerElytraBoostEvent
+             }
+ 
+             return InteractionResult.SUCCESS;
diff --git a/paper-server/patches/sources/net/minecraft/world/item/FishingRodItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/FishingRodItem.java.patch
new file mode 100644
index 0000000000..d3a5d6505d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/FishingRodItem.java.patch
@@ -0,0 +1,54 @@
+--- a/net/minecraft/world/item/FishingRodItem.java
++++ b/net/minecraft/world/item/FishingRodItem.java
+@@ -24,7 +_,7 @@
+         ItemStack itemInHand = player.getItemInHand(hand);
+         if (player.fishing != null) {
+             if (!level.isClientSide) {
+-                int i = player.fishing.retrieve(itemInHand);
++                int i = player.fishing.retrieve(hand, itemInHand); // Paper - Add hand parameter to PlayerFishEvent
+                 itemInHand.hurtAndBreak(i, player, LivingEntity.getSlotForHand(hand));
+             }
+ 
+@@ -40,20 +_,31 @@
+             );
+             player.gameEvent(GameEvent.ITEM_INTERACT_FINISH);
+         } else {
+-            level.playSound(
+-                null,
+-                player.getX(),
+-                player.getY(),
+-                player.getZ(),
+-                SoundEvents.FISHING_BOBBER_THROW,
+-                SoundSource.NEUTRAL,
+-                0.5F,
+-                0.4F / (level.getRandom().nextFloat() * 0.4F + 0.8F)
+-            );
++            // CraftBukkit - moved down
+             if (level instanceof ServerLevel serverLevel) {
+                 int i1 = (int)(EnchantmentHelper.getFishingTimeReduction(serverLevel, itemInHand, player) * 20.0F);
+                 int fishingLuckBonus = EnchantmentHelper.getFishingLuckBonus(serverLevel, itemInHand, player);
+-                Projectile.spawnProjectile(new FishingHook(player, level, fishingLuckBonus, i1), serverLevel, itemInHand);
++                // CraftBukkit start
++                FishingHook fishingHook = new FishingHook(player, level, fishingLuckBonus, i1);
++                org.bukkit.event.player.PlayerFishEvent playerFishEvent = new org.bukkit.event.player.PlayerFishEvent((org.bukkit.entity.Player) player.getBukkitEntity(), null, (org.bukkit.entity.FishHook) fishingHook.getBukkitEntity(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand), org.bukkit.event.player.PlayerFishEvent.State.FISHING);
++                level.getCraftServer().getPluginManager().callEvent(playerFishEvent);
++
++                if (playerFishEvent.isCancelled()) {
++                    player.fishing = null;
++                    return InteractionResult.PASS;
++                }
++                level.playSound(
++                    null,
++                    player.getX(),
++                    player.getY(),
++                    player.getZ(),
++                    SoundEvents.FISHING_BOBBER_THROW,
++                    SoundSource.NEUTRAL,
++                    0.5F,
++                    0.4F / (level.getRandom().nextFloat() * 0.4F + 0.8F)
++                );
++                Projectile.spawnProjectile(fishingHook, serverLevel, itemInHand);
++                // CraftBukkit end
+             }
+ 
+             player.awardStat(Stats.ITEM_USED.get(this));
diff --git a/paper-server/patches/sources/net/minecraft/world/item/FlintAndSteelItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/FlintAndSteelItem.java.patch
new file mode 100644
index 0000000000..19ba3b4a20
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/FlintAndSteelItem.java.patch
@@ -0,0 +1,28 @@
+--- a/net/minecraft/world/item/FlintAndSteelItem.java
++++ b/net/minecraft/world/item/FlintAndSteelItem.java
+@@ -32,6 +_,12 @@
+         if (!CampfireBlock.canLight(blockState) && !CandleBlock.canLight(blockState) && !CandleCakeBlock.canLight(blockState)) {
+             BlockPos blockPos = clickedPos.relative(context.getClickedFace());
+             if (BaseFireBlock.canBePlacedAt(level, blockPos, context.getHorizontalDirection())) {
++                // CraftBukkit start - Store the clicked block
++                if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(level, blockPos, org.bukkit.event.block.BlockIgniteEvent.IgniteCause.FLINT_AND_STEEL, player).isCancelled()) {
++                    context.getItemInHand().hurtAndBreak(1, player, LivingEntity.getSlotForHand(context.getHand()));
++                    return InteractionResult.PASS;
++                }
++                // CraftBukkit end
+                 level.playSound(player, blockPos, SoundEvents.FLINTANDSTEEL_USE, SoundSource.BLOCKS, 1.0F, level.getRandom().nextFloat() * 0.4F + 0.8F);
+                 BlockState state = BaseFireBlock.getState(level, blockPos);
+                 level.setBlock(blockPos, state, 11);
+@@ -47,6 +_,12 @@
+                 return InteractionResult.FAIL;
+             }
+         } else {
++            // CraftBukkit start - Store the clicked block
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(level, clickedPos, org.bukkit.event.block.BlockIgniteEvent.IgniteCause.FLINT_AND_STEEL, player).isCancelled()) {
++                context.getItemInHand().hurtAndBreak(1, player, LivingEntity.getSlotForHand(context.getHand()));
++                return InteractionResult.PASS;
++            }
++            // CraftBukkit end
+             level.playSound(player, clickedPos, SoundEvents.FLINTANDSTEEL_USE, SoundSource.BLOCKS, 1.0F, level.getRandom().nextFloat() * 0.4F + 0.8F);
+             level.setBlock(clickedPos, blockState.setValue(BlockStateProperties.LIT, Boolean.valueOf(true)), 11);
+             level.gameEvent(player, GameEvent.BLOCK_CHANGE, clickedPos);
diff --git a/paper-server/patches/sources/net/minecraft/world/item/HangingEntityItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/HangingEntityItem.java.patch
new file mode 100644
index 0000000000..ae35e0397c
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/HangingEntityItem.java.patch
@@ -0,0 +1,22 @@
+--- a/net/minecraft/world/item/HangingEntityItem.java
++++ b/net/minecraft/world/item/HangingEntityItem.java
+@@ -66,6 +_,19 @@
+ 
+             if (hangingEntity.survives()) {
+                 if (!level.isClientSide) {
++                    // CraftBukkit start - fire HangingPlaceEvent
++                    org.bukkit.entity.Player who = player == null ? null : (org.bukkit.entity.Player) player.getBukkitEntity();
++                    org.bukkit.block.Block blockClicked = org.bukkit.craftbukkit.block.CraftBlock.at(level, blockPos);
++                    org.bukkit.block.BlockFace blockFace = org.bukkit.craftbukkit.block.CraftBlock.notchToBlockFace(clickedFace);
++                    org.bukkit.inventory.EquipmentSlot hand = org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(context.getHand());
++
++                    org.bukkit.event.hanging.HangingPlaceEvent event = new org.bukkit.event.hanging.HangingPlaceEvent((org.bukkit.entity.Hanging) hangingEntity.getBukkitEntity(), who, blockClicked, blockFace, hand, org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitCopy(itemInHand));
++                    level.getCraftServer().getPluginManager().callEvent(event);
++
++                    if (event.isCancelled()) {
++                        return InteractionResult.FAIL;
++                    }
++                    // CraftBukkit end
+                     hangingEntity.playPlacementSound();
+                     level.gameEvent(player, GameEvent.ENTITY_PLACE, hangingEntity.position());
+                     level.addFreshEntity(hangingEntity);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/HoneycombItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/HoneycombItem.java.patch
similarity index 71%
rename from paper-server/patches/unapplied/net/minecraft/world/item/HoneycombItem.java.patch
rename to paper-server/patches/sources/net/minecraft/world/item/HoneycombItem.java.patch
index 02d0f745ad..9a4c3ee1c3 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/item/HoneycombItem.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/item/HoneycombItem.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/world/item/HoneycombItem.java
 +++ b/net/minecraft/world/item/HoneycombItem.java
-@@ -74,6 +74,14 @@
-         return getWaxed(blockState).map(state -> {
+@@ -74,6 +_,14 @@
+         return getWaxed(blockState).<InteractionResult>map(blockState1 -> {
              Player player = context.getPlayer();
-             ItemStack itemStack = context.getItemInHand();
+             ItemStack itemInHand = context.getItemInHand();
 +            // Paper start - EntityChangeBlockEvent
-+            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(player, blockPos, state)) {
++            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(player, clickedPos, blockState)) {
 +                if (!player.isCreative()) {
 +                    player.containerMenu.sendAllDataToRemote();
 +                }
@@ -13,5 +13,5 @@
 +            }
 +            // Paper end
              if (player instanceof ServerPlayer serverPlayer) {
-                 CriteriaTriggers.ITEM_USED_ON_BLOCK.trigger(serverPlayer, blockPos, itemStack);
+                 CriteriaTriggers.ITEM_USED_ON_BLOCK.trigger(serverPlayer, clickedPos, itemInHand);
              }
diff --git a/paper-server/patches/sources/net/minecraft/world/item/ItemCooldowns.java.patch b/paper-server/patches/sources/net/minecraft/world/item/ItemCooldowns.java.patch
new file mode 100644
index 0000000000..4b0f5b2897
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/ItemCooldowns.java.patch
@@ -0,0 +1,16 @@
+--- a/net/minecraft/world/item/ItemCooldowns.java
++++ b/net/minecraft/world/item/ItemCooldowns.java
+@@ -56,6 +_,13 @@
+     }
+ 
+     public void addCooldown(ResourceLocation group, int cooldown) {
++        // Paper start - Item cooldown events
++        this.addCooldown(group, cooldown, true);
++    }
++
++    public void addCooldown(ResourceLocation group, int cooldown, boolean callEvent) {
++        // Event called in server override
++        // Paper end - Item cooldown events
+         this.cooldowns.put(group, new ItemCooldowns.CooldownInstance(this.tickCount, this.tickCount + cooldown));
+         this.onCooldownStarted(group, cooldown);
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/item/ItemStack.java.patch b/paper-server/patches/sources/net/minecraft/world/item/ItemStack.java.patch
new file mode 100644
index 0000000000..2bd24dd054
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/ItemStack.java.patch
@@ -0,0 +1,376 @@
+--- a/net/minecraft/world/item/ItemStack.java
++++ b/net/minecraft/world/item/ItemStack.java
+@@ -136,18 +_,35 @@
+             } else {
+                 Holder<Item> holder = ITEM_STREAM_CODEC.decode(buffer);
+                 DataComponentPatch dataComponentPatch = DataComponentPatch.STREAM_CODEC.decode(buffer);
+-                return new ItemStack(holder, varInt, dataComponentPatch);
++                // CraftBukkit start
++                ItemStack stack = new ItemStack(holder, varInt, dataComponentPatch);
++                if (false && !dataComponentPatch.isEmpty()) { // Paper - This is no longer needed with raw NBT being handled in metadata
++                    org.bukkit.craftbukkit.inventory.CraftItemStack.setItemMeta(stack, org.bukkit.craftbukkit.inventory.CraftItemStack.getItemMeta(stack));
++                }
++                return stack;
++                // CraftBukkit end
+             }
+         }
+ 
+         @Override
+         public void encode(RegistryFriendlyByteBuf buffer, ItemStack value) {
+-            if (value.isEmpty()) {
++            if (value.isEmpty() || value.getItem() == null) { // CraftBukkit - NPE fix itemstack.getItem()
+                 buffer.writeVarInt(0);
+             } else {
+                 buffer.writeVarInt(value.getCount());
+                 ITEM_STREAM_CODEC.encode(buffer, value.getItemHolder());
++                // Spigot start - filter
++                // value = value.copy();
++                // CraftItemStack.setItemMeta(value, CraftItemStack.getItemMeta(value)); // Paper - This is no longer with raw NBT being handled in metadata
++                // Paper start - adventure; conditionally render translatable components
++                boolean prev = net.minecraft.network.chat.ComponentSerialization.DONT_RENDER_TRANSLATABLES.get();
++                try {
++                    net.minecraft.network.chat.ComponentSerialization.DONT_RENDER_TRANSLATABLES.set(true);
+                 DataComponentPatch.STREAM_CODEC.encode(buffer, value.components.asPatch());
++                } finally {
++                    net.minecraft.network.chat.ComponentSerialization.DONT_RENDER_TRANSLATABLES.set(prev);
++                }
++                // Paper end - adventure; conditionally render translatable components
+             }
+         }
+     };
+@@ -365,10 +_,171 @@
+             return InteractionResult.PASS;
+         } else {
+             Item item = this.getItem();
+-            InteractionResult interactionResult = item.useOn(context);
++            // CraftBukkit start - handle all block place event logic here
++            DataComponentPatch previousPatch = this.components.asPatch();
++            int oldCount = this.getCount();
++            ServerLevel serverLevel = (ServerLevel) context.getLevel();
++
++            if (!(item instanceof BucketItem/* || item instanceof SolidBucketItem*/)) { // if not bucket // Paper - Fix cancelled powdered snow bucket placement
++                serverLevel.captureBlockStates = true;
++                // special case bonemeal
++                if (item == Items.BONE_MEAL) {
++                    serverLevel.captureTreeGeneration = true;
++                }
++            }
++            InteractionResult interactionResult;
++            try {
++                interactionResult = item.useOn(context);
++            } finally {
++                serverLevel.captureBlockStates = false;
++            }
++            DataComponentPatch newPatch = this.components.asPatch();
++            int newCount = this.getCount();
++            this.setCount(oldCount);
++            this.restorePatch(previousPatch);
++            if (interactionResult.consumesAction() && serverLevel.captureTreeGeneration && !serverLevel.capturedBlockStates.isEmpty()) {
++                serverLevel.captureTreeGeneration = false;
++                org.bukkit.Location location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(clickedPos, serverLevel.getWorld());
++                org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeType;
++                net.minecraft.world.level.block.SaplingBlock.treeType = null;
++                List<org.bukkit.craftbukkit.block.CraftBlockState> blocks = new java.util.ArrayList<>(serverLevel.capturedBlockStates.values());
++                serverLevel.capturedBlockStates.clear();
++                org.bukkit.event.world.StructureGrowEvent structureEvent = null;
++                if (treeType != null) {
++                    boolean isBonemeal = this.getItem() == Items.BONE_MEAL;
++                    structureEvent = new org.bukkit.event.world.StructureGrowEvent(location, treeType, isBonemeal, (org.bukkit.entity.Player) player.getBukkitEntity(), (List<org.bukkit.block.BlockState>) (List<? extends org.bukkit.block.BlockState>) blocks);
++                    org.bukkit.Bukkit.getPluginManager().callEvent(structureEvent);
++                }
++
++                org.bukkit.event.block.BlockFertilizeEvent fertilizeEvent = new org.bukkit.event.block.BlockFertilizeEvent(org.bukkit.craftbukkit.block.CraftBlock.at(serverLevel, clickedPos), (org.bukkit.entity.Player) player.getBukkitEntity(), (List<org.bukkit.block.BlockState>) (List<? extends org.bukkit.block.BlockState>) blocks);
++                fertilizeEvent.setCancelled(structureEvent != null && structureEvent.isCancelled());
++                org.bukkit.Bukkit.getPluginManager().callEvent(fertilizeEvent);
++
++                if (!fertilizeEvent.isCancelled()) {
++                    // Change the stack to its new contents if it hasn't been tampered with.
++                    if (this.getCount() == oldCount && Objects.equals(this.components.asPatch(), previousPatch)) {
++                        this.restorePatch(newPatch);
++                        this.setCount(newCount);
++                    }
++                    for (org.bukkit.craftbukkit.block.CraftBlockState blockstate : blocks) {
++                        // SPIGOT-7572 - Move fix for SPIGOT-7248 to CapturedBlockState, to allow bees in bee nest
++                        org.bukkit.craftbukkit.block.CapturedBlockState.setBlockState(blockstate);
++                        serverLevel.checkCapturedTreeStateForObserverNotify(clickedPos, blockstate); // Paper - notify observers even if grow failed
++                    }
++                    player.awardStat(Stats.ITEM_USED.get(item)); // SPIGOT-7236 - award stat
++                }
++
++                SignItem.openSign = null; // SPIGOT-6758 - Reset on early return
++                return interactionResult;
++            }
++            serverLevel.captureTreeGeneration = false;
+             if (player != null && interactionResult instanceof InteractionResult.Success success && success.wasItemInteraction()) {
+-                player.awardStat(Stats.ITEM_USED.get(item));
++                InteractionHand hand = context.getHand();
++                org.bukkit.event.block.BlockPlaceEvent placeEvent = null;
++                List<org.bukkit.block.BlockState> blocks = new java.util.ArrayList<>(serverLevel.capturedBlockStates.values());
++                serverLevel.capturedBlockStates.clear();
++                if (blocks.size() > 1) {
++                    placeEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callBlockMultiPlaceEvent(serverLevel, player, hand, blocks, clickedPos.getX(), clickedPos.getY(), clickedPos.getZ());
++                } else if (blocks.size() == 1 && item != Items.POWDER_SNOW_BUCKET) { // Paper - Fix cancelled powdered snow bucket placement
++                    placeEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPlaceEvent(serverLevel, player, hand, blocks.getFirst(), clickedPos.getX(), clickedPos.getY(), clickedPos.getZ());
++                }
++
++                if (placeEvent != null && (placeEvent.isCancelled() || !placeEvent.canBuild())) {
++                    interactionResult = InteractionResult.FAIL; // cancel placement
++                    // PAIL: Remove this when MC-99075 fixed
++                    placeEvent.getPlayer().updateInventory();
++                    serverLevel.capturedTileEntities.clear(); // Paper - Allow chests to be placed with NBT data; clear out block entities as chests and such will pop loot
++                    // revert back all captured blocks
++                    serverLevel.preventPoiUpdated = true; // CraftBukkit - SPIGOT-5710
++                    serverLevel.isBlockPlaceCancelled = true; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent
++                    for (org.bukkit.block.BlockState blockstate : blocks) {
++                        blockstate.update(true, false);
++                    }
++                    serverLevel.isBlockPlaceCancelled = false; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent
++                    serverLevel.preventPoiUpdated = false;
++
++                    SignItem.openSign = null; // SPIGOT-6758 - Reset on early return
++                } else {
++                    // Change the stack to its new contents if it hasn't been tampered with.
++                    if (this.getCount() == oldCount && Objects.equals(this.components.asPatch(), previousPatch)) {
++                        this.restorePatch(newPatch);
++                        this.setCount(newCount);
++                    }
++
++                    for (java.util.Map.Entry<BlockPos, net.minecraft.world.level.block.entity.BlockEntity> e : serverLevel.capturedTileEntities.entrySet()) {
++                        serverLevel.setBlockEntity(e.getValue());
++                    }
++
++                    for (org.bukkit.block.BlockState blockstate : blocks) {
++                        int updateFlag = ((org.bukkit.craftbukkit.block.CraftBlockState) blockstate).getFlag();
++                        net.minecraft.world.level.block.state.BlockState oldBlock = ((org.bukkit.craftbukkit.block.CraftBlockState) blockstate).getHandle();
++                        BlockPos newPos = ((org.bukkit.craftbukkit.block.CraftBlockState) blockstate).getPosition();
++                        net.minecraft.world.level.block.state.BlockState block = serverLevel.getBlockState(newPos);
++
++                        if (!(block.getBlock() instanceof net.minecraft.world.level.block.BaseEntityBlock)) { // Containers get placed automatically
++                            block.onPlace(serverLevel, newPos, oldBlock, true, context);
++                        }
++
++                        serverLevel.notifyAndUpdatePhysics(newPos, null, oldBlock, block, serverLevel.getBlockState(newPos), updateFlag, net.minecraft.world.level.block.Block.UPDATE_LIMIT); // send null chunk as chunk.k() returns false by this point
++                    }
++
++                    if (this.item == Items.WITHER_SKELETON_SKULL) { // Special case skulls to allow wither spawns to be cancelled
++                        BlockPos bp = clickedPos;
++                        if (!serverLevel.getBlockState(clickedPos).canBeReplaced()) {
++                            if (!serverLevel.getBlockState(clickedPos).isSolid()) {
++                                bp = null;
++                            } else {
++                                bp = bp.relative(context.getClickedFace());
++                            }
++                        }
++                        if (bp != null) {
++                            net.minecraft.world.level.block.entity.BlockEntity te = serverLevel.getBlockEntity(bp);
++                            if (te instanceof net.minecraft.world.level.block.entity.SkullBlockEntity) {
++                                net.minecraft.world.level.block.WitherSkullBlock.checkSpawn(serverLevel, bp, (net.minecraft.world.level.block.entity.SkullBlockEntity) te);
++                            }
++                        }
++                    }
++
++                    // SPIGOT-4678
++                    if (this.item instanceof SignItem && SignItem.openSign != null) {
++                        try {
++                            if (serverLevel.getBlockEntity(SignItem.openSign) instanceof net.minecraft.world.level.block.entity.SignBlockEntity blockEntity) {
++                                if (serverLevel.getBlockState(SignItem.openSign).getBlock() instanceof net.minecraft.world.level.block.SignBlock signBlock) {
++                                    signBlock.openTextEdit(player, blockEntity, true, io.papermc.paper.event.player.PlayerOpenSignEvent.Cause.PLACE); // CraftBukkit // Paper - Add PlayerOpenSignEvent
++                                }
++                            }
++                        } finally {
++                            SignItem.openSign = null;
++                        }
++                    }
++
++                    // SPIGOT-7315: Moved from BlockBed#setPlacedBy
++                    if (placeEvent != null && this.item instanceof BedItem) {
++                        BlockPos position = ((org.bukkit.craftbukkit.block.CraftBlock) placeEvent.getBlock()).getPosition();
++                        net.minecraft.world.level.block.state.BlockState blockData = serverLevel.getBlockState(position);
++
++                        if (blockData.getBlock() instanceof net.minecraft.world.level.block.BedBlock) {
++                            serverLevel.blockUpdated(position, net.minecraft.world.level.block.Blocks.AIR);
++                            blockData.updateNeighbourShapes(serverLevel, position, 3);
++                        }
++                    }
++
++                    // SPIGOT-1288 - play sound stripped from ItemBlock
++                    if (this.item instanceof BlockItem) {
++                        // Paper start - Fix spigot sound playing for BlockItem ItemStacks
++                        BlockPos position = new net.minecraft.world.item.context.BlockPlaceContext(context).getClickedPos();
++                        net.minecraft.world.level.block.state.BlockState blockData = serverLevel.getBlockState(position);
++                        net.minecraft.world.level.block.SoundType soundeffecttype = blockData.getSoundType();
++                        // Paper end - Fix spigot sound playing for BlockItem ItemStacks
++                        serverLevel.playSound(player, clickedPos, soundeffecttype.getPlaceSound(), net.minecraft.sounds.SoundSource.BLOCKS, (soundeffecttype.getVolume() + 1.0F) / 2.0F, soundeffecttype.getPitch() * 0.8F);
++                    }
++
++                    player.awardStat(Stats.ITEM_USED.get(item));
++                }
+             }
++            serverLevel.capturedTileEntities.clear();
++            serverLevel.capturedBlockStates.clear();
++            // CraftBukkit end
+ 
+             return interactionResult;
+         }
+@@ -469,31 +_,70 @@
+         return this.isDamageableItem() && this.getDamageValue() >= this.getMaxDamage() - 1;
+     }
+ 
+-    public void hurtAndBreak(int damage, ServerLevel level, @Nullable ServerPlayer player, Consumer<Item> onBreak) {
+-        int i = this.processDurabilityChange(damage, level, player);
++    public void hurtAndBreak(int damage, ServerLevel level, @Nullable LivingEntity player, Consumer<Item> onBreak) { // Paper - Add EntityDamageItemEvent
++        // Paper start - add force boolean overload
++        this.hurtAndBreak(damage, level, player, onBreak, false);
++    }
++    public void hurtAndBreak(int damage, ServerLevel level, @Nullable LivingEntity player, Consumer<Item> onBreak, boolean force) { // Paper - Add EntityDamageItemEvent
++        // Paper end
++        final int originalDamage = damage; // Paper - Expand PlayerItemDamageEvent
++        int i = this.processDurabilityChange(damage, level, player, force); // Paper
++        // CraftBukkit start
++        if (player instanceof final ServerPlayer serverPlayer) { // Paper - Add EntityDamageItemEvent
++            org.bukkit.event.player.PlayerItemDamageEvent event = new org.bukkit.event.player.PlayerItemDamageEvent(serverPlayer.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(this), i, originalDamage); // Paper - Add EntityDamageItemEvent
++            event.getPlayer().getServer().getPluginManager().callEvent(event);
++
++            if (i != event.getDamage() || event.isCancelled()) {
++                event.getPlayer().updateInventory();
++            }
++            if (event.isCancelled()) {
++                return;
++            }
++
++            i = event.getDamage();
++            // Paper start - Add EntityDamageItemEvent
++        } else if (player != null) {
++            io.papermc.paper.event.entity.EntityDamageItemEvent event = new io.papermc.paper.event.entity.EntityDamageItemEvent(player.getBukkitLivingEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(this), i);
++            if (!event.callEvent()) {
++                return;
++            }
++            i = event.getDamage();
++            // Paper end - Add EntityDamageItemEvent
++        }
++        // CraftBukkit end
+         if (i != 0) {
+             this.applyDamage(this.getDamageValue() + i, player, onBreak);
+         }
+     }
+ 
+-    private int processDurabilityChange(int damage, ServerLevel level, @Nullable ServerPlayer player) {
++    private int processDurabilityChange(int damage, ServerLevel level, @Nullable LivingEntity player) { // Paper - Add EntityDamageItemEvent
++        // Paper start - itemstack damage api
++        return processDurabilityChange(damage, level, player, false);
++    }
++    private int processDurabilityChange(int damage, ServerLevel level, @Nullable LivingEntity player, boolean force) {
++        // Paper end - itemstack damage api
+         if (!this.isDamageableItem()) {
+             return 0;
+-        } else if (player != null && player.hasInfiniteMaterials()) {
++        } else if (player instanceof ServerPlayer && player.hasInfiniteMaterials() && !force) { // Paper - Add EntityDamageItemEvent
+             return 0;
+         } else {
+             return damage > 0 ? EnchantmentHelper.processDurabilityChange(level, this, damage) : damage;
+         }
+     }
+ 
+-    private void applyDamage(int damage, @Nullable ServerPlayer player, Consumer<Item> onBreak) {
+-        if (player != null) {
+-            CriteriaTriggers.ITEM_DURABILITY_CHANGED.trigger(player, this, damage);
++    private void applyDamage(int damage, @Nullable LivingEntity player, Consumer<Item> onBreak) { // Paper - Add EntityDamageItemEvent
++        if (player instanceof final ServerPlayer serverPlayer) { // Paper - Add EntityDamageItemEvent
++            CriteriaTriggers.ITEM_DURABILITY_CHANGED.trigger(serverPlayer, this, damage); // Paper - Add EntityDamageItemEvent
+         }
+ 
+         this.setDamageValue(damage);
+         if (this.isBroken()) {
+             Item item = this.getItem();
++            // CraftBukkit start - Check for item breaking
++            if (this.count == 1 && player instanceof final ServerPlayer serverPlayer) { // Paper - Add EntityDamageItemEvent
++                org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerItemBreakEvent(serverPlayer, this); // Paper - Add EntityDamageItemEvent
++            }
++            // CraftBukkit end
+             this.shrink(1);
+             onBreak.accept(item);
+         }
+@@ -512,9 +_,14 @@
+     }
+ 
+     public void hurtAndBreak(int amount, LivingEntity entity, EquipmentSlot slot) {
++        // Paper start - add param to skip infinite mats check
++        this.hurtAndBreak(amount, entity, slot, false);
++    }
++    public void hurtAndBreak(int amount, LivingEntity entity, EquipmentSlot slot, boolean force) {
++        // Paper end - add param to skip infinite mats check
+         if (entity.level() instanceof ServerLevel serverLevel) {
+             this.hurtAndBreak(
+-                amount, serverLevel, entity instanceof ServerPlayer serverPlayer ? serverPlayer : null, item -> entity.onEquippedItemBroken(item, slot)
++                amount, serverLevel, entity, item -> {if (slot != null) entity.onEquippedItemBroken(item, slot); }, force // Paper - Add EntityDamageItemEvent & itemstack damage API - do not process entity related callbacks when damaging from API
+             );
+         }
+     }
+@@ -715,6 +_,12 @@
+         return this.getItem().useOnRelease(this);
+     }
+ 
++    // CraftBukkit start
++    public void restorePatch(DataComponentPatch datacomponentpatch) {
++        this.components.restorePatch(datacomponentpatch);
++    }
++    // CraftBukkit end
++
+     @Nullable
+     public <T> T set(DataComponentType<? super T> component, @Nullable T value) {
+         return this.components.set(component, value);
+@@ -748,6 +_,25 @@
+         }
+     }
+ 
++    // Paper start - (this is just a good no conflict location)
++    public org.bukkit.inventory.ItemStack asBukkitMirror() {
++        return org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(this);
++    }
++    public org.bukkit.inventory.ItemStack asBukkitCopy() {
++        return org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(this.copy());
++    }
++    public static ItemStack fromBukkitCopy(org.bukkit.inventory.ItemStack itemstack) {
++        return org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(itemstack);
++    }
++    private org.bukkit.craftbukkit.inventory.CraftItemStack bukkitStack;
++    public org.bukkit.inventory.ItemStack getBukkitStack() {
++        if (bukkitStack == null || bukkitStack.handle != this) {
++            bukkitStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(this);
++        }
++        return bukkitStack;
++    }
++    // Paper end
++
+     public void applyComponents(DataComponentPatch components) {
+         this.components.applyPatch(components);
+         this.getItem().verifyComponentsAfterLoad(this);
+@@ -1016,6 +_,19 @@
+         EnchantmentHelper.forEachModifier(this, equipmentSLot, action);
+     }
+ 
++    // CraftBukkit start
++    @Deprecated
++    public void setItem(Item item) {
++        this.bukkitStack = null; // Paper
++        this.item = item;
++        // Paper start - change base component prototype
++        final DataComponentPatch patch = this.getComponentsPatch();
++        this.components = new PatchedDataComponentMap(this.item.components());
++        this.applyComponents(patch);
++        // Paper end - change base component prototype
++    }
++    // CraftBukkit end
++
+     public Component getDisplayName() {
+         MutableComponent mutableComponent = Component.empty().append(this.getHoverName());
+         if (this.has(DataComponents.CUSTOM_NAME)) {
+@@ -1072,7 +_,7 @@
+     }
+ 
+     public void consume(int amount, @Nullable LivingEntity entity) {
+-        if (entity == null || !entity.hasInfiniteMaterials()) {
++        if ((entity == null || !entity.hasInfiniteMaterials()) && this != ItemStack.EMPTY) { // CraftBukkit
+             this.shrink(amount);
+         }
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/item/ItemUtils.java.patch b/paper-server/patches/sources/net/minecraft/world/item/ItemUtils.java.patch
new file mode 100644
index 0000000000..0f2be6fd1c
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/ItemUtils.java.patch
@@ -0,0 +1,19 @@
+--- a/net/minecraft/world/item/ItemUtils.java
++++ b/net/minecraft/world/item/ItemUtils.java
+@@ -41,7 +_,15 @@
+     public static void onContainerDestroyed(ItemEntity container, Iterable<ItemStack> contents) {
+         Level level = container.level();
+         if (!level.isClientSide) {
+-            contents.forEach(itemStack -> level.addFreshEntity(new ItemEntity(level, container.getX(), container.getY(), container.getZ(), itemStack)));
++            // Paper start - call EntityDropItemEvent
++            contents.forEach(itemStack -> {
++                ItemEntity droppedItem = new ItemEntity(level, container.getX(), container.getY(), container.getZ(), itemStack);
++                org.bukkit.event.entity.EntityDropItemEvent event = new org.bukkit.event.entity.EntityDropItemEvent(container.getBukkitEntity(), (org.bukkit.entity.Item) droppedItem.getBukkitEntity());
++                if (event.callEvent()) {
++                    level.addFreshEntity(droppedItem);
++                }
++            });
++            // Paper end - call EntityDropItemEvent
+         }
+     }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/item/LeadItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/LeadItem.java.patch
new file mode 100644
index 0000000000..284a57db81
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/LeadItem.java.patch
@@ -0,0 +1,70 @@
+--- a/net/minecraft/world/item/LeadItem.java
++++ b/net/minecraft/world/item/LeadItem.java
+@@ -28,23 +_,43 @@
+         if (blockState.is(BlockTags.FENCES)) {
+             Player player = context.getPlayer();
+             if (!level.isClientSide && player != null) {
+-                return bindPlayerMobs(player, level, clickedPos);
++                return bindPlayerMobs(player, level, clickedPos, context.getHand()); // CraftBukkit - Pass hand
+             }
+         }
+ 
+         return InteractionResult.PASS;
+     }
+ 
+-    public static InteractionResult bindPlayerMobs(Player player, Level level, BlockPos pos) {
++    public static InteractionResult bindPlayerMobs(Player player, Level level, BlockPos pos, net.minecraft.world.InteractionHand interactionHand) { // CraftBukkit - Add InteractionHand
+         LeashFenceKnotEntity leashFenceKnotEntity = null;
+         List<Leashable> list = leashableInArea(level, pos, leashable1 -> leashable1.getLeashHolder() == player);
+ 
+-        for (Leashable leashable : list) {
++        for (java.util.Iterator<Leashable> iterator = list.iterator(); iterator.hasNext();) { // Paper - use iterator to remove
++            Leashable leashable = iterator.next(); // Paper - use iterator to remove
+             if (leashFenceKnotEntity == null) {
+                 leashFenceKnotEntity = LeashFenceKnotEntity.getOrCreateKnot(level, pos);
++                // CraftBukkit start - fire HangingPlaceEvent
++                org.bukkit.inventory.EquipmentSlot hand = org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(interactionHand);
++                org.bukkit.event.hanging.HangingPlaceEvent event = new org.bukkit.event.hanging.HangingPlaceEvent((org.bukkit.entity.Hanging) leashFenceKnotEntity.getBukkitEntity(), player != null ? (org.bukkit.entity.Player) player.getBukkitEntity() : null, org.bukkit.craftbukkit.block.CraftBlock.at(level, pos), org.bukkit.block.BlockFace.SELF, hand);
++                level.getCraftServer().getPluginManager().callEvent(event);
++
++                if (event.isCancelled()) {
++                    leashFenceKnotEntity.discard(null); // CraftBukkit - add Bukkit remove cause
++                    return InteractionResult.PASS;
++                }
++                // CraftBukkit end
+                 leashFenceKnotEntity.playPlacementSound();
+             }
+ 
++            // CraftBukkit start
++            if (leashable instanceof Entity leashed) {
++                if (org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerLeashEntityEvent(leashed, leashFenceKnotEntity, player, interactionHand).isCancelled()) {
++                    iterator.remove();
++                    continue;
++                }
++            }
++            // CraftBukkit end
++
+             leashable.setLeashedTo(leashFenceKnotEntity, true);
+         }
+ 
+@@ -52,9 +_,20 @@
+             level.gameEvent(GameEvent.BLOCK_ATTACH, pos, GameEvent.Context.of(player));
+             return InteractionResult.SUCCESS_SERVER;
+         } else {
++            // CraftBukkit start - remove leash if we do not leash any entity because of the cancelled event
++            if (leashFenceKnotEntity != null) {
++                leashFenceKnotEntity.discard(null);
++            }
++            // CraftBukkit end
+             return InteractionResult.PASS;
+         }
+     }
++
++    // CraftBukkit start
++    public static InteractionResult bindPlayerMobs(Player player, Level world, BlockPos pos) {
++        return LeadItem.bindPlayerMobs(player, world, pos, net.minecraft.world.InteractionHand.MAIN_HAND);
++    }
++    // CraftBukkit end
+ 
+     public static List<Leashable> leashableInArea(Level level, BlockPos pos, Predicate<Leashable> predicate) {
+         double d = 7.0;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/LingeringPotionItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/LingeringPotionItem.java.patch
similarity index 54%
rename from paper-server/patches/unapplied/net/minecraft/world/item/LingeringPotionItem.java.patch
rename to paper-server/patches/sources/net/minecraft/world/item/LingeringPotionItem.java.patch
index 04fd231da8..3c0c4271a4 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/item/LingeringPotionItem.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/item/LingeringPotionItem.java.patch
@@ -1,21 +1,21 @@
 --- a/net/minecraft/world/item/LingeringPotionItem.java
 +++ b/net/minecraft/world/item/LingeringPotionItem.java
-@@ -24,6 +24,10 @@
+@@ -24,6 +_,10 @@
  
      @Override
-     public InteractionResult use(Level world, Player user, InteractionHand hand) {
+     public InteractionResult use(Level level, Player player, InteractionHand hand) {
 +        // Paper start - PlayerLaunchProjectileEvent
-+        final InteractionResult wrapper = super.use(world, user, hand);
++        final InteractionResult wrapper = super.use(level, player, hand);
 +        if (wrapper instanceof InteractionResult.Fail) return wrapper;
 +        // Paper end - PlayerLaunchProjectileEvent
-         world.playSound(
+         level.playSound(
              null,
-             user.getX(),
-@@ -34,6 +38,6 @@
+             player.getX(),
+@@ -34,6 +_,6 @@
              0.5F,
-             0.4F / (world.getRandom().nextFloat() * 0.4F + 0.8F)
+             0.4F / (level.getRandom().nextFloat() * 0.4F + 0.8F)
          );
--        return super.use(world, user, hand);
+-        return super.use(level, player, hand);
 +        return wrapper; // Paper - PlayerLaunchProjectileEvent
      }
  }
diff --git a/paper-server/patches/sources/net/minecraft/world/item/MapItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/MapItem.java.patch
new file mode 100644
index 0000000000..c31ec01d7b
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/MapItem.java.patch
@@ -0,0 +1,22 @@
+--- a/net/minecraft/world/item/MapItem.java
++++ b/net/minecraft/world/item/MapItem.java
+@@ -99,8 +_,8 @@
+                             int i9 = (i1 / i + i6 - 64) * i;
+                             int i10 = (i2 / i + i7 - 64) * i;
+                             Multiset<MapColor> multiset = LinkedHashMultiset.create();
+-                            LevelChunk chunk = level.getChunk(SectionPos.blockToSectionCoord(i9), SectionPos.blockToSectionCoord(i10));
+-                            if (!chunk.isEmpty()) {
++                            LevelChunk chunk = level.getChunkIfLoaded(SectionPos.blockToSectionCoord(i9), SectionPos.blockToSectionCoord(i10)); // Paper - Maps shouldn't load chunks
++                            if (chunk != null && !chunk.isEmpty()) { // Paper - Maps shouldn't load chunks
+                                 int i11 = 0;
+                                 double d1 = 0.0;
+                                 if (level.dimensionType().hasCeiling()) {
+@@ -207,7 +_,7 @@
+ 
+                 for (int i5 = 0; i5 < 128; i5++) {
+                     for (int i6 = 0; i6 < 128; i6++) {
+-                        Holder<Biome> biome = serverLevel.getBiome(mutableBlockPos.set((i3 + i6) * i, 0, (i4 + i5) * i));
++                        Holder<Biome> biome = serverLevel.getUncachedNoiseBiome((i3 + i6) * i, 0, (i4 + i5) * i); // Paper - Perf: Use seed based lookup for treasure maps
+                         flags[i5 * 128 + i6] = biome.is(BiomeTags.WATER_ON_MAP_OUTLINES);
+                     }
+                 }
diff --git a/paper-server/patches/sources/net/minecraft/world/item/MinecartItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/MinecartItem.java.patch
new file mode 100644
index 0000000000..1513242757
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/MinecartItem.java.patch
@@ -0,0 +1,17 @@
+--- a/net/minecraft/world/item/MinecartItem.java
++++ b/net/minecraft/world/item/MinecartItem.java
+@@ -57,7 +_,13 @@
+                 }
+ 
+                 if (level instanceof ServerLevel serverLevel) {
+-                    serverLevel.addFreshEntity(abstractMinecart);
++                    // CraftBukkit start
++                    if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPlaceEvent(context, abstractMinecart).isCancelled()) {
++                        if (context.getPlayer() != null) context.getPlayer().containerMenu.sendAllDataToRemote(); // Paper - Fix inventory desync
++                        return InteractionResult.FAIL;
++                    }
++                    // CraftBukkit end
++                    if (!serverLevel.addFreshEntity(abstractMinecart)) return InteractionResult.PASS; // CraftBukkit
+                     serverLevel.gameEvent(
+                         GameEvent.ENTITY_PLACE, clickedPos, GameEvent.Context.of(context.getPlayer(), serverLevel.getBlockState(clickedPos.below()))
+                     );
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/NameTagItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/NameTagItem.java.patch
similarity index 61%
rename from paper-server/patches/unapplied/net/minecraft/world/item/NameTagItem.java.patch
rename to paper-server/patches/sources/net/minecraft/world/item/NameTagItem.java.patch
index 735436752e..f3a064fec1 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/item/NameTagItem.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/item/NameTagItem.java.patch
@@ -1,18 +1,19 @@
 --- a/net/minecraft/world/item/NameTagItem.java
 +++ b/net/minecraft/world/item/NameTagItem.java
-@@ -18,8 +18,13 @@
+@@ -18,8 +_,14 @@
          Component component = stack.get(DataComponents.CUSTOM_NAME);
-         if (component != null && entity.getType().canSerialize() && entity.canBeNameTagged()) {
-             if (!user.level().isClientSide && entity.isAlive()) {
--                entity.setCustomName(component);
--                if (entity instanceof Mob mob) {
+         if (component != null && target.getType().canSerialize() && target.canBeNameTagged()) {
+             if (!player.level().isClientSide && target.isAlive()) {
+-                target.setCustomName(component);
+-                if (target instanceof Mob mob) {
 +                // Paper start - Add PlayerNameEntityEvent
-+                io.papermc.paper.event.player.PlayerNameEntityEvent event = new io.papermc.paper.event.player.PlayerNameEntityEvent(((net.minecraft.server.level.ServerPlayer) user).getBukkitEntity(), entity.getBukkitLivingEntity(), io.papermc.paper.adventure.PaperAdventure.asAdventure(stack.getHoverName()), true);
++                io.papermc.paper.event.player.PlayerNameEntityEvent event = new io.papermc.paper.event.player.PlayerNameEntityEvent(((net.minecraft.server.level.ServerPlayer) player).getBukkitEntity(), target.getBukkitLivingEntity(), io.papermc.paper.adventure.PaperAdventure.asAdventure(stack.getHoverName()), true);
 +                if (!event.callEvent()) return InteractionResult.PASS;
++
 +                LivingEntity newEntity = ((org.bukkit.craftbukkit.entity.CraftLivingEntity) event.getEntity()).getHandle();
 +                newEntity.setCustomName(event.getName() != null ? io.papermc.paper.adventure.PaperAdventure.asVanilla(event.getName()) : null);
 +                if (event.isPersistent() && newEntity instanceof Mob mob) {
-+                // Paper end - Add PlayerNameEntityEvent
++                    // Paper end - Add PlayerNameEntityEvent
                      mob.setPersistenceRequired();
                  }
  
diff --git a/paper-server/patches/sources/net/minecraft/world/item/PotionItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/PotionItem.java.patch
new file mode 100644
index 0000000000..a253469109
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/PotionItem.java.patch
@@ -0,0 +1,15 @@
+--- a/net/minecraft/world/item/PotionItem.java
++++ b/net/minecraft/world/item/PotionItem.java
+@@ -42,6 +_,12 @@
+         PotionContents potionContents = itemInHand.getOrDefault(DataComponents.POTION_CONTENTS, PotionContents.EMPTY);
+         BlockState blockState = level.getBlockState(clickedPos);
+         if (context.getClickedFace() != Direction.DOWN && blockState.is(BlockTags.CONVERTABLE_TO_MUD) && potionContents.is(Potions.WATER)) {
++            // Paper start
++            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(player, clickedPos, Blocks.MUD.defaultBlockState())) {
++                player.containerMenu.sendAllDataToRemote();
++                return InteractionResult.PASS;
++            }
++            // Paper end
+             level.playSound(null, clickedPos, SoundEvents.GENERIC_SPLASH, SoundSource.BLOCKS, 1.0F, 1.0F);
+             player.setItemInHand(context.getHand(), ItemUtils.createFilledResult(itemInHand, player, new ItemStack(Items.GLASS_BOTTLE)));
+             player.awardStat(Stats.ITEM_USED.get(itemInHand.getItem()));
diff --git a/paper-server/patches/sources/net/minecraft/world/item/ProjectileWeaponItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/ProjectileWeaponItem.java.patch
new file mode 100644
index 0000000000..bfc05c761e
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/ProjectileWeaponItem.java.patch
@@ -0,0 +1,59 @@
+--- a/net/minecraft/world/item/ProjectileWeaponItem.java
++++ b/net/minecraft/world/item/ProjectileWeaponItem.java
+@@ -62,12 +_,29 @@
+                 float f4 = f2 + f3 * ((i + 1) / 2) * f1;
+                 f3 = -f3;
+                 int i1 = i;
+-                Projectile.spawnProjectile(
+-                    this.createProjectile(level, shooter, weapon, itemStack, isCrit),
+-                    level,
+-                    itemStack,
+-                    projectile -> this.shootProjectile(shooter, projectile, i1, velocity, inaccuracy, f4, target)
+-                );
++                // CraftBukkit start
++                Projectile projectile = this.createProjectile(level, shooter, weapon, itemStack, isCrit);
++                this.shootProjectile(shooter, projectile, i1, velocity, inaccuracy, f4, target);
++
++                org.bukkit.event.entity.EntityShootBowEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityShootBowEvent(shooter, weapon, itemStack, projectile, hand, velocity, true);
++                if (event.isCancelled()) {
++                    event.getProjectile().remove();
++                    return;
++                }
++
++                if (event.getProjectile() == projectile.getBukkitEntity()) {
++                    if (Projectile.spawnProjectile(
++                        projectile,
++                        level,
++                        itemStack
++                    ).isRemoved()) {
++                        if (shooter instanceof net.minecraft.server.level.ServerPlayer serverPlayer) {
++                            serverPlayer.getBukkitEntity().updateInventory();
++                        }
++                        return;
++                    }
++                }
++                // CraftBukkit end
+                 weapon.hurtAndBreak(this.getDurabilityUse(itemStack), shooter, LivingEntity.getSlotForHand(hand));
+                 if (weapon.isEmpty()) {
+                     break;
+@@ -95,6 +_,11 @@
+     }
+ 
+     protected static List<ItemStack> draw(ItemStack weapon, ItemStack ammo, LivingEntity shooter) {
++        // Paper start
++        return draw(weapon, ammo, shooter, true);
++    }
++    protected static List<ItemStack> draw(ItemStack weapon, ItemStack ammo, LivingEntity shooter, boolean consume) {
++        // Paper end
+         if (ammo.isEmpty()) {
+             return List.of();
+         } else {
+@@ -103,7 +_,7 @@
+             ItemStack itemStack = ammo.copy();
+ 
+             for (int i1 = 0; i1 < i; i1++) {
+-                ItemStack itemStack1 = useAmmo(weapon, i1 == 0 ? ammo : itemStack, shooter, i1 > 0);
++                ItemStack itemStack1 = useAmmo(weapon, i1 == 0 ? ammo : itemStack, shooter, i1 > 0 || !consume); // Paper
+                 if (!itemStack1.isEmpty()) {
+                     list.add(itemStack1);
+                 }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/ServerItemCooldowns.java.patch b/paper-server/patches/sources/net/minecraft/world/item/ServerItemCooldowns.java.patch
similarity index 86%
rename from paper-server/patches/unapplied/net/minecraft/world/item/ServerItemCooldowns.java.patch
rename to paper-server/patches/sources/net/minecraft/world/item/ServerItemCooldowns.java.patch
index b9e17d595b..6e1f3fcec1 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/item/ServerItemCooldowns.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/item/ServerItemCooldowns.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/world/item/ServerItemCooldowns.java
 +++ b/net/minecraft/world/item/ServerItemCooldowns.java
-@@ -11,7 +11,40 @@
+@@ -11,6 +_,39 @@
          this.player = player;
      }
  
 +    // Paper start - Add PlayerItemCooldownEvent
-     @Override
++    @Override
 +    public void addCooldown(ItemStack item, int duration) {
 +        final ResourceLocation cooldownGroup = this.getCooldownGroup(item);
 +        final io.papermc.paper.event.player.PlayerItemCooldownEvent event = new io.papermc.paper.event.player.PlayerItemCooldownEvent(
@@ -37,7 +37,6 @@
 +    }
 +    // Paper end - Add PlayerItemCooldownEvent
 +
-+    @Override
-     protected void onCooldownStarted(ResourceLocation groupId, int duration) {
-         super.onCooldownStarted(groupId, duration);
-         this.player.connection.send(new ClientboundCooldownPacket(groupId, duration));
+     @Override
+     protected void onCooldownStarted(ResourceLocation group, int cooldown) {
+         super.onCooldownStarted(group, cooldown);
diff --git a/paper-server/patches/sources/net/minecraft/world/item/ShovelItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/ShovelItem.java.patch
new file mode 100644
index 0000000000..142d691825
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/ShovelItem.java.patch
@@ -0,0 +1,33 @@
+--- a/net/minecraft/world/item/ShovelItem.java
++++ b/net/minecraft/world/item/ShovelItem.java
+@@ -46,20 +_,29 @@
+             Player player = context.getPlayer();
+             BlockState blockState1 = FLATTENABLES.get(blockState.getBlock());
+             BlockState blockState2 = null;
++            Runnable afterAction = null; // Paper
+             if (blockState1 != null && level.getBlockState(clickedPos.above()).isAir()) {
+-                level.playSound(player, clickedPos, SoundEvents.SHOVEL_FLATTEN, SoundSource.BLOCKS, 1.0F, 1.0F);
++                afterAction = () -> level.playSound(player, clickedPos, SoundEvents.SHOVEL_FLATTEN, SoundSource.BLOCKS, 1.0F, 1.0F); // Paper
+                 blockState2 = blockState1;
+             } else if (blockState.getBlock() instanceof CampfireBlock && blockState.getValue(CampfireBlock.LIT)) {
++                afterAction = () -> { // Paper
+                 if (!level.isClientSide()) {
+                     level.levelEvent(null, 1009, clickedPos, 0);
+                 }
+ 
+                 CampfireBlock.dowse(context.getPlayer(), level, clickedPos, blockState);
++                }; // Paper
+                 blockState2 = blockState.setValue(CampfireBlock.LIT, Boolean.valueOf(false));
+             }
+ 
+             if (blockState2 != null) {
+                 if (!level.isClientSide) {
++                    // Paper start
++                    if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(context.getPlayer(), clickedPos, blockState2)) {
++                        return InteractionResult.PASS;
++                    }
++                    afterAction.run();
++                    // Paper end
+                     level.setBlock(clickedPos, blockState2, 11);
+                     level.gameEvent(GameEvent.BLOCK_CHANGE, clickedPos, GameEvent.Context.of(player, blockState2));
+                     if (player != null) {
diff --git a/paper-server/patches/sources/net/minecraft/world/item/SignItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/SignItem.java.patch
new file mode 100644
index 0000000000..fe3d4a6926
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/SignItem.java.patch
@@ -0,0 +1,22 @@
+--- a/net/minecraft/world/item/SignItem.java
++++ b/net/minecraft/world/item/SignItem.java
+@@ -11,6 +_,7 @@
+ import net.minecraft.world.level.block.state.BlockState;
+ 
+ public class SignItem extends StandingAndWallBlockItem {
++    public static BlockPos openSign; // CraftBukkit
+     public SignItem(Block standingBlock, Block wallBlock, Item.Properties properties) {
+         super(standingBlock, wallBlock, Direction.DOWN, properties);
+     }
+@@ -27,7 +_,10 @@
+             && player != null
+             && level.getBlockEntity(pos) instanceof SignBlockEntity signBlockEntity
+             && level.getBlockState(pos).getBlock() instanceof SignBlock signBlock) {
+-            signBlock.openTextEdit(player, signBlockEntity, true);
++            // CraftBukkit start - SPIGOT-4678
++            // signBlock.openTextEdit(player, signBlockEntity, true);
++            SignItem.openSign = pos;
++            // CraftBukkit end
+         }
+ 
+         return flag;
diff --git a/paper-server/patches/sources/net/minecraft/world/item/SnowballItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/SnowballItem.java.patch
new file mode 100644
index 0000000000..ca1757fbfe
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/SnowballItem.java.patch
@@ -0,0 +1,57 @@
+--- a/net/minecraft/world/item/SnowballItem.java
++++ b/net/minecraft/world/item/SnowballItem.java
+@@ -23,22 +_,41 @@
+     @Override
+     public InteractionResult use(Level level, Player player, InteractionHand hand) {
+         ItemStack itemInHand = player.getItemInHand(hand);
+-        level.playSound(
+-            null,
+-            player.getX(),
+-            player.getY(),
+-            player.getZ(),
+-            SoundEvents.SNOWBALL_THROW,
+-            SoundSource.NEUTRAL,
+-            0.5F,
+-            0.4F / (level.getRandom().nextFloat() * 0.4F + 0.8F)
+-        );
++        // CraftBukkit start - moved down
+         if (level instanceof ServerLevel serverLevel) {
+-            Projectile.spawnProjectileFromRotation(Snowball::new, serverLevel, itemInHand, player, 0.0F, PROJECTILE_SHOOT_POWER, 1.0F);
++            // Paper start - PlayerLaunchProjectileEvent
++            final Projectile.Delayed<Snowball> snowball = Projectile.spawnProjectileFromRotationDelayed(Snowball::new, serverLevel, itemInHand, player, 0.0F, SnowballItem.PROJECTILE_SHOOT_POWER, 1.0F);
++            com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) player.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemInHand), (org.bukkit.entity.Projectile) snowball.projectile().getBukkitEntity());
++            if (event.callEvent() && snowball.attemptSpawn()) {
++                player.awardStat(Stats.ITEM_USED.get(this));
++                if (event.shouldConsume()) {
++                    itemInHand.consume(1, player);
++                } else {
++                    player.containerMenu.sendAllDataToRemote();
++                }
++                // Paper end - PlayerLaunchProjectileEvent
++
++                level.playSound(
++                    null,
++                    player.getX(),
++                    player.getY(),
++                    player.getZ(),
++                    SoundEvents.SNOWBALL_THROW,
++                    SoundSource.NEUTRAL,
++                    0.5F,
++                    0.4F / (level.getRandom().nextFloat() * 0.4F + 0.8F)
++                );
++                // Paper start - PlayerLaunchProjectileEvent - return fail
++            } else {
++                player.containerMenu.sendAllDataToRemote();
++                return InteractionResult.FAIL;
++            }
++            // Paper end- PlayerLaunchProjectileEvent - return fail
++            // CraftBukkit end
+         }
+ 
+-        player.awardStat(Stats.ITEM_USED.get(this));
+-        itemInHand.consume(1, player);
++        // Paper - PlayerLaunchProjectileEvent - moved up
++        // itemInHand.consume(1, player); // CraftBukkit - moved up
+         return InteractionResult.SUCCESS;
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/item/SpawnEggItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/SpawnEggItem.java.patch
new file mode 100644
index 0000000000..03672c4a17
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/SpawnEggItem.java.patch
@@ -0,0 +1,19 @@
+--- a/net/minecraft/world/item/SpawnEggItem.java
++++ b/net/minecraft/world/item/SpawnEggItem.java
+@@ -55,6 +_,7 @@
+             Direction clickedFace = context.getClickedFace();
+             BlockState blockState = level.getBlockState(clickedPos);
+             if (level.getBlockEntity(clickedPos) instanceof Spawner spawner) {
++                if (level.paperConfig().entities.spawning.disableMobSpawnerSpawnEggTransformation) return InteractionResult.FAIL; // Paper - Allow disabling mob spawner spawn egg transformation
+                 EntityType<?> type = this.getType(level.registryAccess(), itemInHand);
+                 spawner.setEntityId(type, level.getRandom());
+                 level.sendBlockUpdated(clickedPos, blockState, blockState, 3);
+@@ -169,7 +_,7 @@
+                     return Optional.empty();
+                 } else {
+                     breedOffspring.moveTo(pos.x(), pos.y(), pos.z(), 0.0F, 0.0F);
+-                    serverLevel.addFreshEntityWithPassengers(breedOffspring);
++                    serverLevel.addFreshEntityWithPassengers(breedOffspring, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.SPAWNER_EGG); // CraftBukkit
+                     breedOffspring.setCustomName(stack.get(DataComponents.CUSTOM_NAME));
+                     stack.consume(1, player);
+                     return Optional.of(breedOffspring);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/SplashPotionItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/SplashPotionItem.java.patch
similarity index 54%
rename from paper-server/patches/unapplied/net/minecraft/world/item/SplashPotionItem.java.patch
rename to paper-server/patches/sources/net/minecraft/world/item/SplashPotionItem.java.patch
index bdf8aca4d5..1744dbddc9 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/item/SplashPotionItem.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/item/SplashPotionItem.java.patch
@@ -1,21 +1,21 @@
 --- a/net/minecraft/world/item/SplashPotionItem.java
 +++ b/net/minecraft/world/item/SplashPotionItem.java
-@@ -14,6 +14,10 @@
+@@ -14,6 +_,10 @@
  
      @Override
-     public InteractionResult use(Level world, Player user, InteractionHand hand) {
+     public InteractionResult use(Level level, Player player, InteractionHand hand) {
 +        // Paper start - PlayerLaunchProjectileEvent
-+        final InteractionResult wrapper = super.use(world, user, hand);
++        final InteractionResult wrapper = super.use(level, player, hand);
 +        if (wrapper instanceof InteractionResult.Fail) return wrapper;
 +        // Paper end - PlayerLaunchProjectileEvent
-         world.playSound(
+         level.playSound(
              null,
-             user.getX(),
-@@ -24,6 +28,6 @@
+             player.getX(),
+@@ -24,6 +_,6 @@
              0.5F,
-             0.4F / (world.getRandom().nextFloat() * 0.4F + 0.8F)
+             0.4F / (level.getRandom().nextFloat() * 0.4F + 0.8F)
          );
--        return super.use(world, user, hand);
+-        return super.use(level, player, hand);
 +        return wrapper; // Paper - PlayerLaunchProjectileEvent
      }
  }
diff --git a/paper-server/patches/sources/net/minecraft/world/item/StandingAndWallBlockItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/StandingAndWallBlockItem.java.patch
new file mode 100644
index 0000000000..e99c04062a
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/StandingAndWallBlockItem.java.patch
@@ -0,0 +1,24 @@
+--- a/net/minecraft/world/item/StandingAndWallBlockItem.java
++++ b/net/minecraft/world/item/StandingAndWallBlockItem.java
+@@ -42,7 +_,20 @@
+             }
+         }
+ 
+-        return blockState != null && level.isUnobstructed(blockState, clickedPos, CollisionContext.empty()) ? blockState : null;
++        // return blockState != null && level.isUnobstructed(blockState, clickedPos, CollisionContext.empty()) ? blockState : null;
++        // CraftBukkit start
++        if (blockState != null) {
++            boolean defaultReturn = level.isUnobstructed(blockState, clickedPos, CollisionContext.empty());
++            org.bukkit.entity.Player player = (context.getPlayer() instanceof net.minecraft.server.level.ServerPlayer serverPlayer) ? serverPlayer.getBukkitEntity() : null;
++
++            org.bukkit.event.block.BlockCanBuildEvent event = new org.bukkit.event.block.BlockCanBuildEvent(org.bukkit.craftbukkit.block.CraftBlock.at(context.getLevel(), clickedPos), player, org.bukkit.craftbukkit.block.data.CraftBlockData.fromData(blockState), defaultReturn, org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(context.getHand())); // Paper - Expose hand in BlockCanBuildEvent
++            context.getLevel().getCraftServer().getPluginManager().callEvent(event);
++
++            return (event.isBuildable()) ? blockState : null;
++        } else {
++            return null;
++        }
++        // CraftBukkit end
+     }
+ 
+     @Override
diff --git a/paper-server/patches/sources/net/minecraft/world/item/ThrowablePotionItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/ThrowablePotionItem.java.patch
new file mode 100644
index 0000000000..220fab6ac3
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/ThrowablePotionItem.java.patch
@@ -0,0 +1,31 @@
+--- a/net/minecraft/world/item/ThrowablePotionItem.java
++++ b/net/minecraft/world/item/ThrowablePotionItem.java
+@@ -22,11 +_,25 @@
+     public InteractionResult use(Level level, Player player, InteractionHand hand) {
+         ItemStack itemInHand = player.getItemInHand(hand);
+         if (level instanceof ServerLevel serverLevel) {
+-            Projectile.spawnProjectileFromRotation(ThrownPotion::new, serverLevel, itemInHand, player, -20.0F, PROJECTILE_SHOOT_POWER, 1.0F);
++            // Paper start - PlayerLaunchProjectileEvent
++            final Projectile.Delayed<ThrownPotion> thrownPotion = Projectile.spawnProjectileFromRotationDelayed(ThrownPotion::new, serverLevel, itemInHand, player, -20.0F, PROJECTILE_SHOOT_POWER, 1.0F);
++            com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) player.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemInHand), (org.bukkit.entity.Projectile) thrownPotion.projectile().getBukkitEntity());
++            if (event.callEvent() && thrownPotion.attemptSpawn()) {
++                if (event.shouldConsume()) {
++                    itemInHand.consume(1, player);
++                } else {
++                    player.containerMenu.sendAllDataToRemote();
++                }
++
++                player.awardStat(Stats.ITEM_USED.get(this));
++            } else {
++                player.containerMenu.sendAllDataToRemote();
++                return InteractionResult.FAIL;
++            }
++            // Paper end - PlayerLaunchProjectileEvent
+         }
+ 
+-        player.awardStat(Stats.ITEM_USED.get(this));
+-        itemInHand.consume(1, player);
++        // Paper - PlayerLaunchProjectileEvent - move up
+         return InteractionResult.SUCCESS;
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/item/TridentItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/TridentItem.java.patch
new file mode 100644
index 0000000000..1e9f3060b3
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/TridentItem.java.patch
@@ -0,0 +1,49 @@
+--- a/net/minecraft/world/item/TridentItem.java
++++ b/net/minecraft/world/item/TridentItem.java
+@@ -87,19 +_,35 @@
+                         .orElse(SoundEvents.TRIDENT_THROW);
+                     player.awardStat(Stats.ITEM_USED.get(this));
+                     if (level instanceof ServerLevel serverLevel) {
+-                        stack.hurtWithoutBreaking(1, player);
++                        // stack.hurtWithoutBreaking(1, player); // CraftBukkit - moved down
+                         if (tridentSpinAttackStrength == 0.0F) {
+-                            ThrownTrident thrownTrident = Projectile.spawnProjectileFromRotation(
++                            Projectile.Delayed<ThrownTrident> tridentDelayed = Projectile.spawnProjectileFromRotationDelayed( // Paper - PlayerLaunchProjectileEvent
+                                 ThrownTrident::new, serverLevel, stack, player, 0.0F, 2.5F, 1.0F
+                             );
++                            // Paper start - PlayerLaunchProjectileEvent
++                            com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) player.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(stack), (org.bukkit.entity.Projectile) tridentDelayed.projectile().getBukkitEntity());
++                            if (!event.callEvent() || !tridentDelayed.attemptSpawn()) {
++                                // CraftBukkit start
++                                // Paper end - PlayerLaunchProjectileEvent
++                                player.containerMenu.sendAllDataToRemote();
++                                return false;
++                            }
++                            ThrownTrident thrownTrident = tridentDelayed.projectile(); // Paper - PlayerLaunchProjectileEvent
++                            if (event.shouldConsume()) stack.hurtWithoutBreaking(1, player); // Paper - PlayerLaunchProjectileEvent
++                            thrownTrident.pickupItemStack = stack.copy(); // SPIGOT-4511 update since damage call moved
++                            // CraftBukkit end
+                             if (player.hasInfiniteMaterials()) {
+                                 thrownTrident.pickup = AbstractArrow.Pickup.CREATIVE_ONLY;
+-                            } else {
++                            } else if (event.shouldConsume()) { // Paper - PlayerLaunchProjectileEvent
+                                 player.getInventory().removeItem(stack);
+                             }
+ 
+                             level.playSound(null, thrownTrident, holder.value(), SoundSource.PLAYERS, 1.0F, 1.0F);
+                             return true;
++                            // CraftBukkit start - SPIGOT-5458 also need in this branch :(
++                        } else {
++                            stack.hurtWithoutBreaking(1, player);
++                            // CraftBukkit end
+                         }
+                     }
+ 
+@@ -113,6 +_,7 @@
+                         f *= tridentSpinAttackStrength / squareRoot;
+                         f1 *= tridentSpinAttackStrength / squareRoot;
+                         f2 *= tridentSpinAttackStrength / squareRoot;
++                        org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerRiptideEvent(player, stack, f, f1, f2); // CraftBukkit
+                         player.push(f, f1, f2);
+                         player.startAutoSpinAttack(20, 8.0F, stack);
+                         if (player.onGround()) {
diff --git a/paper-server/patches/sources/net/minecraft/world/item/WindChargeItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/WindChargeItem.java.patch
new file mode 100644
index 0000000000..26a26944da
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/WindChargeItem.java.patch
@@ -0,0 +1,44 @@
+--- a/net/minecraft/world/item/WindChargeItem.java
++++ b/net/minecraft/world/item/WindChargeItem.java
+@@ -27,7 +_,7 @@
+     public InteractionResult use(Level level, Player player, InteractionHand hand) {
+         ItemStack itemInHand = player.getItemInHand(hand);
+         if (level instanceof ServerLevel serverLevel) {
+-            Projectile.spawnProjectileFromRotation(
++            final Projectile.Delayed<WindCharge> windCharge = Projectile.spawnProjectileFromRotationDelayed( // Paper - PlayerLaunchProjectileEvent
+                 (level1, owner, spawnedFrom) -> new WindCharge(player, level, player.position().x(), player.getEyePosition().y(), player.position().z()),
+                 serverLevel,
+                 itemInHand,
+@@ -36,6 +_,22 @@
+                 PROJECTILE_SHOOT_POWER,
+                 1.0F
+             );
++            // Paper start - PlayerLaunchProjectileEvent
++            com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) player.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemInHand), (org.bukkit.entity.Projectile) windCharge.projectile().getBukkitEntity());
++            if (!event.callEvent() || !windCharge.attemptSpawn()) {
++                player.containerMenu.sendAllDataToRemote();
++                if (player instanceof net.minecraft.server.level.ServerPlayer serverPlayer) {
++                    serverPlayer.connection.send(new net.minecraft.network.protocol.game.ClientboundCooldownPacket(player.getCooldowns().getCooldownGroup(itemInHand), 0)); // prevent visual desync of cooldown on the slot
++                }
++                return InteractionResult.FAIL;
++            }
++
++            player.awardStat(Stats.ITEM_USED.get(this));
++            if (event.shouldConsume()) itemInHand.consume(1, player);
++            else if (!player.hasInfiniteMaterials()) {
++                player.containerMenu.sendAllDataToRemote();
++            }
++            // Paper end - PlayerLaunchProjectileEvent
+         }
+ 
+         level.playSound(
+@@ -48,8 +_,7 @@
+             0.5F,
+             0.4F / (level.getRandom().nextFloat() * 0.4F + 0.8F)
+         );
+-        player.awardStat(Stats.ITEM_USED.get(this));
+-        itemInHand.consume(1, player);
++        // Paper - PlayerLaunchProjectileEvent; moved up
+         return InteractionResult.SUCCESS;
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/item/WrittenBookItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/WrittenBookItem.java.patch
new file mode 100644
index 0000000000..a8a03bfeee
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/WrittenBookItem.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/item/WrittenBookItem.java
++++ b/net/minecraft/world/item/WrittenBookItem.java
+@@ -41,7 +_,7 @@
+ 
+     public static boolean resolveBookComponents(ItemStack bookStack, CommandSourceStack resolvingSource, @Nullable Player resolvingPlayer) {
+         WrittenBookContent writtenBookContent = bookStack.get(DataComponents.WRITTEN_BOOK_CONTENT);
+-        if (writtenBookContent != null && !writtenBookContent.resolved()) {
++        if (io.papermc.paper.configuration.GlobalConfiguration.get().itemValidation.resolveSelectorsInBooks && writtenBookContent != null && !writtenBookContent.resolved()) { // Paper - Disable component selector resolving in books by default
+             WrittenBookContent writtenBookContent1 = writtenBookContent.resolve(resolvingSource, resolvingPlayer);
+             if (writtenBookContent1 != null) {
+                 bookStack.set(DataComponents.WRITTEN_BOOK_CONTENT, writtenBookContent1);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/alchemy/PotionBrewing.java.patch b/paper-server/patches/sources/net/minecraft/world/item/alchemy/PotionBrewing.java.patch
similarity index 76%
rename from paper-server/patches/unapplied/net/minecraft/world/item/alchemy/PotionBrewing.java.patch
rename to paper-server/patches/sources/net/minecraft/world/item/alchemy/PotionBrewing.java.patch
index c9c0b22f57..1ccb0b5432 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/item/alchemy/PotionBrewing.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/item/alchemy/PotionBrewing.java.patch
@@ -1,14 +1,14 @@
 --- a/net/minecraft/world/item/alchemy/PotionBrewing.java
 +++ b/net/minecraft/world/item/alchemy/PotionBrewing.java
-@@ -19,6 +19,7 @@
+@@ -19,6 +_,7 @@
      private final List<Ingredient> containers;
      private final List<PotionBrewing.Mix<Potion>> potionMixes;
      private final List<PotionBrewing.Mix<Item>> containerMixes;
 +    private final it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap<org.bukkit.NamespacedKey, io.papermc.paper.potion.PaperPotionMix> customMixes = new it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap<>(); // Paper - Custom Potion Mixes
  
-     PotionBrewing(List<Ingredient> potionTypes, List<PotionBrewing.Mix<Potion>> potionRecipes, List<PotionBrewing.Mix<Item>> itemRecipes) {
-         this.containers = potionTypes;
-@@ -27,7 +28,7 @@
+     PotionBrewing(List<Ingredient> containers, List<PotionBrewing.Mix<Potion>> potionMixes, List<PotionBrewing.Mix<Item>> containerMixes) {
+         this.containers = containers;
+@@ -27,7 +_,7 @@
      }
  
      public boolean isIngredient(ItemStack stack) {
@@ -17,36 +17,37 @@
      }
  
      private boolean isContainer(ItemStack stack) {
-@@ -71,6 +72,11 @@
+@@ -71,6 +_,11 @@
      }
  
-     public boolean hasMix(ItemStack input, ItemStack ingredient) {
+     public boolean hasMix(ItemStack reagent, ItemStack potionItem) {
 +        // Paper start - Custom Potion Mixes
-+        if (this.hasCustomMix(input, ingredient)) {
++        if (this.hasCustomMix(reagent, potionItem)) {
 +            return true;
 +        }
 +        // Paper end - Custom Potion Mixes
-         return this.isContainer(input) && (this.hasContainerMix(input, ingredient) || this.hasPotionMix(input, ingredient));
+         return this.isContainer(reagent) && (this.hasContainerMix(reagent, potionItem) || this.hasPotionMix(reagent, potionItem));
      }
  
-@@ -103,6 +109,13 @@
-         if (input.isEmpty()) {
-             return input;
+@@ -103,6 +_,13 @@
+         if (potionItem.isEmpty()) {
+             return potionItem;
          } else {
 +            // Paper start - Custom Potion Mixes
 +            for (io.papermc.paper.potion.PaperPotionMix mix : this.customMixes.values()) {
-+                if (mix.input().test(input) && mix.ingredient().test(ingredient)) {
++                if (mix.input().test(potionItem) && mix.ingredient().test(potion)) {
 +                    return mix.result().copy();
 +                }
 +            }
 +            // Paper end - Custom Potion Mixes
-             Optional<Holder<Potion>> optional = input.getOrDefault(DataComponents.POTION_CONTENTS, PotionContents.EMPTY).potion();
+             Optional<Holder<Potion>> optional = potionItem.getOrDefault(DataComponents.POTION_CONTENTS, PotionContents.EMPTY).potion();
              if (optional.isEmpty()) {
-                 return input;
-@@ -190,6 +203,50 @@
+                 return potionItem;
+@@ -189,6 +_,50 @@
+         builder.addMix(Potions.AWKWARD, Items.PHANTOM_MEMBRANE, Potions.SLOW_FALLING);
          builder.addMix(Potions.SLOW_FALLING, Items.REDSTONE, Potions.LONG_SLOW_FALLING);
      }
- 
++
 +    // Paper start - Custom Potion Mixes
 +    public boolean isCustomIngredient(ItemStack stack) {
 +        for (io.papermc.paper.potion.PaperPotionMix mix : this.customMixes.values()) {
@@ -90,7 +91,6 @@
 +        return bootstrap(flags);
 +    }
 +    // Paper end - Custom Potion Mixes
-+
+ 
      public static class Builder {
          private final List<Ingredient> containers = new ArrayList<>();
-         private final List<PotionBrewing.Mix<Potion>> potionMixes = new ArrayList<>();
diff --git a/paper-server/patches/sources/net/minecraft/world/item/alchemy/PotionContents.java.patch b/paper-server/patches/sources/net/minecraft/world/item/alchemy/PotionContents.java.patch
new file mode 100644
index 0000000000..f499dec2ee
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/alchemy/PotionContents.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/item/alchemy/PotionContents.java
++++ b/net/minecraft/world/item/alchemy/PotionContents.java
+@@ -158,7 +_,7 @@
+                 if (mobEffectInstance.getEffect().value().isInstantenous()) {
+                     mobEffectInstance.getEffect().value().applyInstantenousEffect(serverLevel, player1, player1, entity, mobEffectInstance.getAmplifier(), 1.0);
+                 } else {
+-                    entity.addEffect(mobEffectInstance);
++                    entity.addEffect(mobEffectInstance, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.POTION_DRINK); // CraftBukkit
+                 }
+             });
+         }
diff --git a/paper-server/patches/sources/net/minecraft/world/item/component/Consumable.java.patch b/paper-server/patches/sources/net/minecraft/world/item/component/Consumable.java.patch
new file mode 100644
index 0000000000..d39f21455d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/component/Consumable.java.patch
@@ -0,0 +1,39 @@
+--- a/net/minecraft/world/item/component/Consumable.java
++++ b/net/minecraft/world/item/component/Consumable.java
+@@ -84,13 +_,35 @@
+ 
+         stack.getAllOfType(ConsumableListener.class).forEach(consumableListener -> consumableListener.onConsume(level, entity, stack, this));
+         if (!level.isClientSide) {
+-            this.onConsumeEffects.forEach(consumeEffect -> consumeEffect.apply(level, stack, entity));
++            // CraftBukkit start
++            org.bukkit.event.entity.EntityPotionEffectEvent.Cause cause;
++            if (stack.is(net.minecraft.world.item.Items.MILK_BUCKET)) {
++                cause = org.bukkit.event.entity.EntityPotionEffectEvent.Cause.MILK;
++            } else if (stack.is(net.minecraft.world.item.Items.POTION)) {
++                cause = org.bukkit.event.entity.EntityPotionEffectEvent.Cause.POTION_DRINK;
++            } else {
++                cause = org.bukkit.event.entity.EntityPotionEffectEvent.Cause.FOOD;
++            }
++
++            this.onConsumeEffects.forEach(consumeEffect -> consumeEffect.apply(level, stack, entity, cause));
++            // CraftBukkit end
+         }
+ 
+         entity.gameEvent(this.animation == ItemUseAnimation.DRINK ? GameEvent.DRINK : GameEvent.EAT);
+         stack.consume(1, entity);
+         return stack;
+     }
++
++    // CraftBukkit start
++    public void cancelUsingItem(ServerPlayer player, ItemStack stack) {
++        final java.util.List<net.minecraft.network.protocol.Packet<? super net.minecraft.network.protocol.game.ClientGamePacketListener>> packets = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>(); // Paper - properly resend entities - collect packets for bundle
++        stack.getAllOfType(ConsumableListener.class).forEach(listener -> {
++            listener.cancelUsingItem(player, stack, packets); // Paper - properly resend entities - collect packets for bundle
++        });
++        player.server.getPlayerList().sendActiveEffects(player, packets::add); // Paper - properly resend entities - collect packets for bundle
++        player.connection.send(new net.minecraft.network.protocol.game.ClientboundBundlePacket(packets));
++    }
++    // CraftBukkit end
+ 
+     public boolean canConsume(LivingEntity entity, ItemStack stack) {
+         FoodProperties foodProperties = stack.get(DataComponents.FOOD);
diff --git a/paper-server/patches/sources/net/minecraft/world/item/component/ConsumableListener.java.patch b/paper-server/patches/sources/net/minecraft/world/item/component/ConsumableListener.java.patch
new file mode 100644
index 0000000000..a47c2b2a62
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/component/ConsumableListener.java.patch
@@ -0,0 +1,9 @@
+--- a/net/minecraft/world/item/component/ConsumableListener.java
++++ b/net/minecraft/world/item/component/ConsumableListener.java
+@@ -6,4 +_,6 @@
+ 
+ public interface ConsumableListener {
+     void onConsume(Level level, LivingEntity entity, ItemStack stack, Consumable consumable);
++
++    default void cancelUsingItem(net.minecraft.server.level.ServerPlayer player, ItemStack stack, java.util.List<net.minecraft.network.protocol.Packet<? super net.minecraft.network.protocol.game.ClientGamePacketListener>> collectedPackets) {} // CraftBukkit // Paper - properly resend entities - collect packets for bundle
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/component/CustomData.java.patch b/paper-server/patches/sources/net/minecraft/world/item/component/CustomData.java.patch
similarity index 83%
rename from paper-server/patches/unapplied/net/minecraft/world/item/component/CustomData.java.patch
rename to paper-server/patches/sources/net/minecraft/world/item/component/CustomData.java.patch
index 73089c0170..33b96fd4de 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/item/component/CustomData.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/item/component/CustomData.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/item/component/CustomData.java
 +++ b/net/minecraft/world/item/component/CustomData.java
-@@ -34,7 +34,17 @@
+@@ -34,7 +_,17 @@
      private static final Logger LOGGER = LogUtils.getLogger();
      public static final CustomData EMPTY = new CustomData(new CompoundTag());
      private static final String TYPE_TAG = "id";
@@ -16,6 +16,6 @@
 +            }
 +        })
 +        // Paper end - Item serialization as json
-         .xmap(CustomData::new, component -> component.tag);
+         .xmap(CustomData::new, customData -> customData.tag);
      public static final Codec<CustomData> CODEC_WITH_ID = CODEC.validate(
-         component -> component.getUnsafe().contains("id", 8) ? DataResult.success(component) : DataResult.error(() -> "Missing id for entity in: " + component)
+         data -> data.getUnsafe().contains("id", 8) ? DataResult.success(data) : DataResult.error(() -> "Missing id for entity in: " + data)
diff --git a/paper-server/patches/sources/net/minecraft/world/item/component/DeathProtection.java.patch b/paper-server/patches/sources/net/minecraft/world/item/component/DeathProtection.java.patch
new file mode 100644
index 0000000000..b2efb28173
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/component/DeathProtection.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/item/component/DeathProtection.java
++++ b/net/minecraft/world/item/component/DeathProtection.java
+@@ -37,7 +_,7 @@
+ 
+     public void applyEffects(ItemStack stack, LivingEntity entity) {
+         for (ConsumeEffect consumeEffect : this.deathEffects) {
+-            consumeEffect.apply(entity.level(), stack, entity);
++            consumeEffect.apply(entity.level(), stack, entity, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.TOTEM); // CraftBukkit
+         }
+     }
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/component/LodestoneTracker.java.patch b/paper-server/patches/sources/net/minecraft/world/item/component/LodestoneTracker.java.patch
similarity index 70%
rename from paper-server/patches/unapplied/net/minecraft/world/item/component/LodestoneTracker.java.patch
rename to paper-server/patches/sources/net/minecraft/world/item/component/LodestoneTracker.java.patch
index 79d33fc6bf..6a668d9014 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/item/component/LodestoneTracker.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/item/component/LodestoneTracker.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/world/item/component/LodestoneTracker.java
 +++ b/net/minecraft/world/item/component/LodestoneTracker.java
-@@ -29,7 +29,7 @@
+@@ -29,7 +_,7 @@
                  return this;
              } else {
                  BlockPos blockPos = this.target.get().pos();
--                return world.isInWorldBounds(blockPos) && world.getPoiManager().existsAtPosition(PoiTypes.LODESTONE, blockPos)
-+                return world.isInWorldBounds(blockPos) && (!world.hasChunkAt(blockPos) || world.getPoiManager().existsAtPosition(PoiTypes.LODESTONE, blockPos)) // Paper - Prevent compass from loading chunks
+-                return level.isInWorldBounds(blockPos) && level.getPoiManager().existsAtPosition(PoiTypes.LODESTONE, blockPos)
++                return level.isInWorldBounds(blockPos) && (!level.hasChunkAt(blockPos) || level.getPoiManager().existsAtPosition(PoiTypes.LODESTONE, blockPos)) // Paper - Prevent compass from loading chunks
                      ? this
                      : new LodestoneTracker(Optional.empty(), true);
              }
diff --git a/paper-server/patches/sources/net/minecraft/world/item/component/OminousBottleAmplifier.java.patch b/paper-server/patches/sources/net/minecraft/world/item/component/OminousBottleAmplifier.java.patch
new file mode 100644
index 0000000000..6a00c3c6f2
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/component/OminousBottleAmplifier.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/item/component/OminousBottleAmplifier.java
++++ b/net/minecraft/world/item/component/OminousBottleAmplifier.java
+@@ -28,8 +_,15 @@
+ 
+     @Override
+     public void onConsume(Level level, LivingEntity entity, ItemStack stack, Consumable consumable) {
+-        entity.addEffect(new MobEffectInstance(MobEffects.BAD_OMEN, 120000, this.value, false, false, true));
+-    }
++        entity.addEffect(new MobEffectInstance(MobEffects.BAD_OMEN, 120000, this.value, false, false, true)); // Paper - properly resend entities - diff on change for below
++    }
++
++    // Paper start - properly resend entities - collect packets for bundle
++    @Override
++    public void cancelUsingItem(net.minecraft.server.level.ServerPlayer player, ItemStack stack, List<net.minecraft.network.protocol.Packet<? super net.minecraft.network.protocol.game.ClientGamePacketListener>> collectedPackets) {
++        collectedPackets.add(new net.minecraft.network.protocol.game.ClientboundRemoveMobEffectPacket(player.getId(), MobEffects.BAD_OMEN));
++    }
++    // Paper end - properly resend entities - collect packets for bundle
+ 
+     @Override
+     public void addToTooltip(Item.TooltipContext context, Consumer<Component> tooltipAdder, TooltipFlag tooltipFlag) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/component/ResolvableProfile.java.patch b/paper-server/patches/sources/net/minecraft/world/item/component/ResolvableProfile.java.patch
similarity index 58%
rename from paper-server/patches/unapplied/net/minecraft/world/item/component/ResolvableProfile.java.patch
rename to paper-server/patches/sources/net/minecraft/world/item/component/ResolvableProfile.java.patch
index c0d2bc1d79..e12079be3f 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/item/component/ResolvableProfile.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/item/component/ResolvableProfile.java.patch
@@ -1,18 +1,18 @@
 --- a/net/minecraft/world/item/component/ResolvableProfile.java
 +++ b/net/minecraft/world/item/component/ResolvableProfile.java
-@@ -20,9 +20,10 @@
+@@ -20,9 +_,10 @@
          instance -> instance.group(
-                     ExtraCodecs.PLAYER_NAME.optionalFieldOf("name").forGetter(ResolvableProfile::name),
-                     UUIDUtil.CODEC.optionalFieldOf("id").forGetter(ResolvableProfile::id),
-+                    UUIDUtil.STRING_CODEC.lenientOptionalFieldOf("Id").forGetter($ -> Optional.empty()), // Paper
-                     ExtraCodecs.PROPERTY_MAP.optionalFieldOf("properties", new PropertyMap()).forGetter(ResolvableProfile::properties)
-                 )
--                .apply(instance, ResolvableProfile::new)
-+                .apply(instance, (s, uuid, uuid2, propertyMap) -> new ResolvableProfile(s, uuid2.or(() -> uuid), propertyMap)) // Paper
+                 ExtraCodecs.PLAYER_NAME.optionalFieldOf("name").forGetter(ResolvableProfile::name),
+                 UUIDUtil.CODEC.optionalFieldOf("id").forGetter(ResolvableProfile::id),
++                UUIDUtil.STRING_CODEC.lenientOptionalFieldOf("Id").forGetter($ -> Optional.empty()), // Paper
+                 ExtraCodecs.PROPERTY_MAP.optionalFieldOf("properties", new PropertyMap()).forGetter(ResolvableProfile::properties)
+             )
+-            .apply(instance, ResolvableProfile::new)
++            .apply(instance, (name, uuid, uuid2, propertyMap) -> new ResolvableProfile(name, uuid2.or(() -> uuid), propertyMap)) // Paper
      );
      public static final Codec<ResolvableProfile> CODEC = Codec.withAlternative(
          FULL_CODEC, ExtraCodecs.PLAYER_NAME, name -> new ResolvableProfile(Optional.of(name), Optional.empty(), new PropertyMap())
-@@ -49,7 +50,7 @@
+@@ -49,7 +_,7 @@
          if (this.isResolved()) {
              return CompletableFuture.completedFuture(this);
          } else {
@@ -20,4 +20,4 @@
 +            return this.id.isPresent() ? SkullBlockEntity.fetchGameProfile(this.id.get(), this.name.orElse(null)).thenApply(optional -> { // Paper - player profile events
                  GameProfile gameProfile = optional.orElseGet(() -> new GameProfile(this.id.get(), this.name.orElse("")));
                  return new ResolvableProfile(gameProfile);
-             }) : SkullBlockEntity.fetchGameProfile(this.name.orElseThrow()).thenApply(profile -> {
+             }) : SkullBlockEntity.fetchGameProfile(this.name.orElseThrow()).thenApply(optional -> {
diff --git a/paper-server/patches/sources/net/minecraft/world/item/component/SuspiciousStewEffects.java.patch b/paper-server/patches/sources/net/minecraft/world/item/component/SuspiciousStewEffects.java.patch
new file mode 100644
index 0000000000..d84c096c8b
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/component/SuspiciousStewEffects.java.patch
@@ -0,0 +1,18 @@
+--- a/net/minecraft/world/item/component/SuspiciousStewEffects.java
++++ b/net/minecraft/world/item/component/SuspiciousStewEffects.java
+@@ -41,6 +_,15 @@
+         }
+     }
+ 
++    // CraftBukkit start
++    @Override
++    public void cancelUsingItem(net.minecraft.server.level.ServerPlayer player, ItemStack stack, List<net.minecraft.network.protocol.Packet<? super net.minecraft.network.protocol.game.ClientGamePacketListener>> collectedPackets) { // Paper - properly resend entities - collect packets for bundle
++        for (SuspiciousStewEffects.Entry entry : this.effects) {
++            collectedPackets.add(new net.minecraft.network.protocol.game.ClientboundRemoveMobEffectPacket(player.getId(), entry.effect())); // Paper - bundlize packets
++        }
++    }
++    // CraftBukkit end
++
+     @Override
+     public void addToTooltip(Item.TooltipContext context, Consumer<Component> tooltipAdder, TooltipFlag tooltipFlag) {
+         if (tooltipFlag.isCreative()) {
diff --git a/paper-server/patches/sources/net/minecraft/world/item/consume_effects/ApplyStatusEffectsConsumeEffect.java.patch b/paper-server/patches/sources/net/minecraft/world/item/consume_effects/ApplyStatusEffectsConsumeEffect.java.patch
new file mode 100644
index 0000000000..4d29a75fd9
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/consume_effects/ApplyStatusEffectsConsumeEffect.java.patch
@@ -0,0 +1,19 @@
+--- a/net/minecraft/world/item/consume_effects/ApplyStatusEffectsConsumeEffect.java
++++ b/net/minecraft/world/item/consume_effects/ApplyStatusEffectsConsumeEffect.java
+@@ -46,14 +_,14 @@
+     }
+ 
+     @Override
+-    public boolean apply(Level level, ItemStack stack, LivingEntity entity) {
++    public boolean apply(Level level, ItemStack stack, LivingEntity entity, org.bukkit.event.entity.EntityPotionEffectEvent.Cause cause) { // CraftBukkit
+         if (entity.getRandom().nextFloat() >= this.probability) {
+             return false;
+         } else {
+             boolean flag = false;
+ 
+             for (MobEffectInstance mobEffectInstance : this.effects) {
+-                if (entity.addEffect(new MobEffectInstance(mobEffectInstance))) {
++                if (entity.addEffect(new MobEffectInstance(mobEffectInstance), cause)) { // CraftBukkit
+                     flag = true;
+                 }
+             }
diff --git a/paper-server/patches/sources/net/minecraft/world/item/consume_effects/ClearAllStatusEffectsConsumeEffect.java.patch b/paper-server/patches/sources/net/minecraft/world/item/consume_effects/ClearAllStatusEffectsConsumeEffect.java.patch
new file mode 100644
index 0000000000..37064488ba
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/consume_effects/ClearAllStatusEffectsConsumeEffect.java.patch
@@ -0,0 +1,14 @@
+--- a/net/minecraft/world/item/consume_effects/ClearAllStatusEffectsConsumeEffect.java
++++ b/net/minecraft/world/item/consume_effects/ClearAllStatusEffectsConsumeEffect.java
+@@ -18,7 +_,9 @@
+     }
+ 
+     @Override
+-    public boolean apply(Level level, ItemStack stack, LivingEntity entity) {
+-        return entity.removeAllEffects();
++    // CraftBukkit start
++    public boolean apply(Level level, ItemStack stack, LivingEntity entity, org.bukkit.event.entity.EntityPotionEffectEvent.Cause cause) {
++        return entity.removeAllEffects(cause);
++        // CraftBukkit end
+     }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/item/consume_effects/ConsumeEffect.java.patch b/paper-server/patches/sources/net/minecraft/world/item/consume_effects/ConsumeEffect.java.patch
new file mode 100644
index 0000000000..af59bc597a
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/consume_effects/ConsumeEffect.java.patch
@@ -0,0 +1,19 @@
+--- a/net/minecraft/world/item/consume_effects/ConsumeEffect.java
++++ b/net/minecraft/world/item/consume_effects/ConsumeEffect.java
+@@ -19,7 +_,15 @@
+ 
+     ConsumeEffect.Type<? extends ConsumeEffect> getType();
+ 
+-    boolean apply(Level level, ItemStack stack, LivingEntity entity);
++    // CraftBukkit start
++    default boolean apply(Level level, ItemStack stack, LivingEntity entity) {
++        return this.apply(level, stack, entity, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.UNKNOWN);
++    }
++
++    default boolean apply(Level level, ItemStack stack, LivingEntity entity, org.bukkit.event.entity.EntityPotionEffectEvent.Cause cause) {
++        return this.apply(level, stack, entity);
++    }
++    // CraftBukkit end
+ 
+     public record Type<T extends ConsumeEffect>(MapCodec<T> codec, StreamCodec<RegistryFriendlyByteBuf, T> streamCodec) {
+         public static final ConsumeEffect.Type<ApplyStatusEffectsConsumeEffect> APPLY_EFFECTS = register(
diff --git a/paper-server/patches/sources/net/minecraft/world/item/consume_effects/RemoveStatusEffectsConsumeEffect.java.patch b/paper-server/patches/sources/net/minecraft/world/item/consume_effects/RemoveStatusEffectsConsumeEffect.java.patch
new file mode 100644
index 0000000000..9cd988de23
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/consume_effects/RemoveStatusEffectsConsumeEffect.java.patch
@@ -0,0 +1,16 @@
+--- a/net/minecraft/world/item/consume_effects/RemoveStatusEffectsConsumeEffect.java
++++ b/net/minecraft/world/item/consume_effects/RemoveStatusEffectsConsumeEffect.java
+@@ -35,11 +_,11 @@
+     }
+ 
+     @Override
+-    public boolean apply(Level level, ItemStack stack, LivingEntity entity) {
++    public boolean apply(Level level, ItemStack stack, LivingEntity entity, org.bukkit.event.entity.EntityPotionEffectEvent.Cause cause) { // CraftBukkit
+         boolean flag = false;
+ 
+         for (Holder<MobEffect> holder : this.effects) {
+-            if (entity.removeEffect(holder)) {
++            if (entity.removeEffect(holder, cause)) { // CraftBukkit
+                 flag = true;
+             }
+         }
diff --git a/paper-server/patches/sources/net/minecraft/world/item/consume_effects/TeleportRandomlyConsumeEffect.java.patch b/paper-server/patches/sources/net/minecraft/world/item/consume_effects/TeleportRandomlyConsumeEffect.java.patch
new file mode 100644
index 0000000000..36547c65ef
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/consume_effects/TeleportRandomlyConsumeEffect.java.patch
@@ -0,0 +1,17 @@
+--- a/net/minecraft/world/item/consume_effects/TeleportRandomlyConsumeEffect.java
++++ b/net/minecraft/world/item/consume_effects/TeleportRandomlyConsumeEffect.java
+@@ -55,7 +_,13 @@
+             }
+ 
+             Vec3 vec3 = entity.position();
+-            if (entity.randomTeleport(d, d1, d2, true)) {
++            // CraftBukkit start - handle canceled status of teleport event
++            java.util.Optional<Boolean> status = entity.randomTeleport(d, d1, d2, true, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.CHORUS_FRUIT);
++
++            // teleport event was canceled, no more tries
++            if (status.isEmpty()) break;
++            if (status.get()) {
++                // CraftBukkit end
+                 level.gameEvent(GameEvent.TELEPORT, vec3, GameEvent.Context.of(entity));
+                 SoundSource soundSource;
+                 SoundEvent soundEvent;
diff --git a/paper-server/patches/sources/net/minecraft/world/item/crafting/BlastingRecipe.java.patch b/paper-server/patches/sources/net/minecraft/world/item/crafting/BlastingRecipe.java.patch
new file mode 100644
index 0000000000..320c3c5413
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/crafting/BlastingRecipe.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/item/crafting/BlastingRecipe.java
++++ b/net/minecraft/world/item/crafting/BlastingRecipe.java
+@@ -31,4 +_,17 @@
+             case FOOD, MISC -> RecipeBookCategories.BLAST_FURNACE_MISC;
+         };
+     }
++
++    // CraftBukkit start
++    @Override
++    public org.bukkit.inventory.Recipe toBukkitRecipe(org.bukkit.NamespacedKey id) {
++        org.bukkit.craftbukkit.inventory.CraftItemStack result = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(this.result());
++
++        org.bukkit.craftbukkit.inventory.CraftBlastingRecipe recipe = new org.bukkit.craftbukkit.inventory.CraftBlastingRecipe(id, result, org.bukkit.craftbukkit.inventory.CraftRecipe.toBukkit(this.input()), this.experience(), this.cookingTime());
++        recipe.setGroup(this.group());
++        recipe.setCategory(org.bukkit.craftbukkit.inventory.CraftRecipe.getCategory(this.category()));
++
++        return recipe;
++    }
++    // CraftBukkit end
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/item/crafting/CampfireCookingRecipe.java.patch b/paper-server/patches/sources/net/minecraft/world/item/crafting/CampfireCookingRecipe.java.patch
new file mode 100644
index 0000000000..876f5b6f1d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/crafting/CampfireCookingRecipe.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/item/crafting/CampfireCookingRecipe.java
++++ b/net/minecraft/world/item/crafting/CampfireCookingRecipe.java
+@@ -28,4 +_,17 @@
+     public RecipeBookCategory recipeBookCategory() {
+         return RecipeBookCategories.CAMPFIRE;
+     }
++
++    // CraftBukkit start
++    @Override
++    public org.bukkit.inventory.Recipe toBukkitRecipe(org.bukkit.NamespacedKey id) {
++        org.bukkit.craftbukkit.inventory.CraftItemStack result = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(this.result());
++
++        org.bukkit.craftbukkit.inventory.CraftCampfireRecipe recipe = new org.bukkit.craftbukkit.inventory.CraftCampfireRecipe(id, result, org.bukkit.craftbukkit.inventory.CraftRecipe.toBukkit(this.input()), this.experience(), this.cookingTime());
++        recipe.setGroup(this.group());
++        recipe.setCategory(org.bukkit.craftbukkit.inventory.CraftRecipe.getCategory(this.category()));
++
++        return recipe;
++    }
++    // CraftBukkit end
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/item/crafting/CustomRecipe.java.patch b/paper-server/patches/sources/net/minecraft/world/item/crafting/CustomRecipe.java.patch
new file mode 100644
index 0000000000..bbf2d64506
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/crafting/CustomRecipe.java.patch
@@ -0,0 +1,22 @@
+--- a/net/minecraft/world/item/crafting/CustomRecipe.java
++++ b/net/minecraft/world/item/crafting/CustomRecipe.java
+@@ -30,6 +_,19 @@
+     @Override
+     public abstract RecipeSerializer<? extends CustomRecipe> getSerializer();
+ 
++    // CraftBukkit start
++    @Override
++    public org.bukkit.inventory.Recipe toBukkitRecipe(org.bukkit.NamespacedKey id) {
++        org.bukkit.craftbukkit.inventory.CraftItemStack result = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(net.minecraft.world.item.ItemStack.EMPTY);
++
++        org.bukkit.craftbukkit.inventory.CraftComplexRecipe recipe = new org.bukkit.craftbukkit.inventory.CraftComplexRecipe(id, result, this);
++        recipe.setGroup(this.group());
++        recipe.setCategory(org.bukkit.craftbukkit.inventory.CraftRecipe.getCategory(this.category()));
++
++        return recipe;
++    }
++    // CraftBukkit end
++
+     public static class Serializer<T extends CraftingRecipe> implements RecipeSerializer<T> {
+         private final MapCodec<T> codec;
+         private final StreamCodec<RegistryFriendlyByteBuf, T> streamCodec;
diff --git a/paper-server/patches/sources/net/minecraft/world/item/crafting/Ingredient.java.patch b/paper-server/patches/sources/net/minecraft/world/item/crafting/Ingredient.java.patch
new file mode 100644
index 0000000000..6e0c1b88c1
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/crafting/Ingredient.java.patch
@@ -0,0 +1,55 @@
+--- a/net/minecraft/world/item/crafting/Ingredient.java
++++ b/net/minecraft/world/item/crafting/Ingredient.java
+@@ -33,6 +_,25 @@
+     public static final Codec<Ingredient> CODEC = ExtraCodecs.nonEmptyHolderSet(NON_AIR_HOLDER_SET_CODEC)
+         .xmap(Ingredient::new, ingredient -> ingredient.values);
+     private final HolderSet<Item> values;
++    // CraftBukkit start
++    @javax.annotation.Nullable
++    private java.util.List<ItemStack> itemStacks;
++
++    public boolean isExact() {
++        return this.itemStacks != null;
++    }
++
++    @javax.annotation.Nullable
++    public java.util.List<ItemStack> itemStacks() {
++        return this.itemStacks;
++    }
++
++    public static Ingredient ofStacks(java.util.List<ItemStack> stacks) {
++        Ingredient recipe = Ingredient.of(stacks.stream().map(ItemStack::getItem));
++        recipe.itemStacks = stacks;
++        return recipe;
++    }
++    // CraftBukkit end
+ 
+     private Ingredient(HolderSet<Item> values) {
+         values.unwrap().ifRight(list -> {
+@@ -60,6 +_,17 @@
+ 
+     @Override
+     public boolean test(ItemStack stack) {
++        // CraftBukkit start
++        if (this.isExact()) {
++            for (ItemStack itemstack1 : this.itemStacks()) {
++                if (itemstack1.getItem() == stack.getItem() && ItemStack.isSameItemSameComponents(stack, itemstack1)) {
++                    return true;
++                }
++            }
++
++            return false;
++        }
++        // CraftBukkit end
+         return stack.is(this.values);
+     }
+ 
+@@ -70,7 +_,7 @@
+ 
+     @Override
+     public boolean equals(Object other) {
+-        return other instanceof Ingredient ingredient && Objects.equals(this.values, ingredient.values);
++        return other instanceof Ingredient ingredient && Objects.equals(this.values, ingredient.values) && Objects.equals(this.itemStacks, ingredient.itemStacks); // CraftBukkit
+     }
+ 
+     public static Ingredient of(ItemLike item) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/Recipe.java.patch b/paper-server/patches/sources/net/minecraft/world/item/crafting/Recipe.java.patch
similarity index 93%
rename from paper-server/patches/unapplied/net/minecraft/world/item/crafting/Recipe.java.patch
rename to paper-server/patches/sources/net/minecraft/world/item/crafting/Recipe.java.patch
index ad97a94316..4d54b2d585 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/Recipe.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/item/crafting/Recipe.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/item/crafting/Recipe.java
 +++ b/net/minecraft/world/item/crafting/Recipe.java
-@@ -44,4 +44,6 @@
+@@ -44,4 +_,6 @@
      }
  
      RecipeBookCategory recipeBookCategory();
diff --git a/paper-server/patches/sources/net/minecraft/world/item/crafting/RecipeHolder.java.patch b/paper-server/patches/sources/net/minecraft/world/item/crafting/RecipeHolder.java.patch
new file mode 100644
index 0000000000..538297e2aa
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/crafting/RecipeHolder.java.patch
@@ -0,0 +1,15 @@
+--- a/net/minecraft/world/item/crafting/RecipeHolder.java
++++ b/net/minecraft/world/item/crafting/RecipeHolder.java
+@@ -10,6 +_,12 @@
+         ResourceKey.streamCodec(Registries.RECIPE), RecipeHolder::id, Recipe.STREAM_CODEC, RecipeHolder::value, RecipeHolder::new
+     );
+ 
++    // CraftBukkit start
++    public final org.bukkit.inventory.Recipe toBukkitRecipe() {
++        return this.value.toBukkitRecipe(org.bukkit.craftbukkit.util.CraftNamespacedKey.fromMinecraft(this.id.location()));
++    }
++    // CraftBukkit end
++
+     @Override
+     public boolean equals(Object other) {
+         return this == other || other instanceof RecipeHolder<?> recipeHolder && this.id == recipeHolder.id;
diff --git a/paper-server/patches/sources/net/minecraft/world/item/crafting/RecipeManager.java.patch b/paper-server/patches/sources/net/minecraft/world/item/crafting/RecipeManager.java.patch
new file mode 100644
index 0000000000..dcc5613328
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/crafting/RecipeManager.java.patch
@@ -0,0 +1,64 @@
+--- a/net/minecraft/world/item/crafting/RecipeManager.java
++++ b/net/minecraft/world/item/crafting/RecipeManager.java
+@@ -87,7 +_,26 @@
+         LOGGER.info("Loaded {} recipes", object.values().size());
+     }
+ 
++    // CraftBukkit start
++    public void addRecipe(RecipeHolder<?> holder) {
++        org.spigotmc.AsyncCatcher.catchOp("Recipe Add"); // Spigot
++        this.recipes.addRecipe(holder);
++        this.finalizeRecipeLoading();
++    }
++
++    private FeatureFlagSet featureflagset;
++
++    public void finalizeRecipeLoading() {
++        if (this.featureflagset != null) {
++            this.finalizeRecipeLoading(this.featureflagset);
++
++            net.minecraft.server.MinecraftServer.getServer().getPlayerList().reloadRecipes();
++        }
++    }
++
+     public void finalizeRecipeLoading(FeatureFlagSet enabledFeatures) {
++        this.featureflagset = enabledFeatures;
++        // CraftBukkit end
+         List<SelectableRecipe.SingleInputEntry<StonecutterRecipe>> list = new ArrayList<>();
+         List<RecipeManager.IngredientCollector> list1 = RECIPE_PROPERTY_SETS.entrySet()
+             .stream()
+@@ -147,7 +_,10 @@
+     }
+ 
+     public <I extends RecipeInput, T extends Recipe<I>> Optional<RecipeHolder<T>> getRecipeFor(RecipeType<T> recipeType, I input, Level level) {
+-        return this.recipes.getRecipesFor(recipeType, input, level).findFirst();
++        // CraftBukkit start
++        List<RecipeHolder<T>> list = this.recipes.getRecipesFor(recipeType, input, level).toList();
++        return (list.isEmpty()) ? Optional.empty() : Optional.of(list.getLast()); // CraftBukkit - SPIGOT-4638: last recipe gets priority
++        // CraftBukkit end
+     }
+ 
+     public Optional<RecipeHolder<?>> byKey(ResourceKey<Recipe<?>> key) {
+@@ -199,6 +_,22 @@
+         Recipe<?> recipe1 = Recipe.CODEC.parse(registries.createSerializationContext(JsonOps.INSTANCE), json).getOrThrow(JsonParseException::new);
+         return new RecipeHolder<>(recipe, recipe1);
+     }
++
++    // CraftBukkit start
++    public boolean removeRecipe(ResourceKey<Recipe<?>> mcKey) {
++        boolean removed = this.recipes.removeRecipe((ResourceKey<Recipe<RecipeInput>>) (ResourceKey) mcKey); // Paper - generic fix
++        if (removed) {
++            this.finalizeRecipeLoading();
++        }
++
++        return removed;
++    }
++
++    public void clearRecipes() {
++        this.recipes = RecipeMap.create(java.util.Collections.emptyList());
++        this.finalizeRecipeLoading();
++    }
++    // CraftBukkit end
+ 
+     public static <I extends RecipeInput, T extends Recipe<I>> RecipeManager.CachedCheck<I, T> createCheck(final RecipeType<T> recipeType) {
+         return new RecipeManager.CachedCheck<I, T>() {
diff --git a/paper-server/patches/sources/net/minecraft/world/item/crafting/RecipeMap.java.patch b/paper-server/patches/sources/net/minecraft/world/item/crafting/RecipeMap.java.patch
new file mode 100644
index 0000000000..77d44ed126
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/crafting/RecipeMap.java.patch
@@ -0,0 +1,39 @@
+--- a/net/minecraft/world/item/crafting/RecipeMap.java
++++ b/net/minecraft/world/item/crafting/RecipeMap.java
+@@ -30,8 +_,34 @@
+             builder1.put(recipeHolder.id(), recipeHolder);
+         }
+ 
+-        return new RecipeMap(builder.build(), builder1.build());
+-    }
++        // CraftBukkit start - mutable
++        return new RecipeMap(com.google.common.collect.LinkedHashMultimap.create(builder.build()), com.google.common.collect.Maps.newHashMap(builder1.build()));
++    }
++
++    public void addRecipe(RecipeHolder<?> holder) {
++        Collection<RecipeHolder<?>> recipes = this.byType.get(holder.value().getType());
++
++        if (this.byKey.containsKey(holder.id())) {
++            throw new IllegalStateException("Duplicate recipe ignored with ID " + holder.id());
++        } else {
++            recipes.add(holder);
++            this.byKey.put(holder.id(), holder);
++        }
++    }
++    // CraftBukkit end
++
++    // Paper start - replace removeRecipe implementation
++    public <T extends RecipeInput> boolean removeRecipe(ResourceKey<Recipe<T>> mcKey) {
++        //noinspection unchecked
++        final RecipeHolder<Recipe<T>> remove = (RecipeHolder<Recipe<T>>) this.byKey.remove(mcKey);
++        if (remove == null) {
++            return false;
++        }
++        final Collection<? extends RecipeHolder<? extends Recipe<T>>> recipes = this.byType(remove.value().getType());
++        return recipes.remove(remove);
++        // Paper end - why are you using a loop???
++    }
++    // Paper end - replace removeRecipe implementation
+ 
+     public <I extends RecipeInput, T extends Recipe<I>> Collection<RecipeHolder<T>> byType(RecipeType<T> type) {
+         return (Collection)this.byType.get(type);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/ShapedRecipe.java.patch b/paper-server/patches/sources/net/minecraft/world/item/crafting/ShapedRecipe.java.patch
similarity index 57%
rename from paper-server/patches/unapplied/net/minecraft/world/item/crafting/ShapedRecipe.java.patch
rename to paper-server/patches/sources/net/minecraft/world/item/crafting/ShapedRecipe.java.patch
index 05fee5cb09..18e1fb0718 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/ShapedRecipe.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/item/crafting/ShapedRecipe.java.patch
@@ -1,30 +1,16 @@
 --- a/net/minecraft/world/item/crafting/ShapedRecipe.java
 +++ b/net/minecraft/world/item/crafting/ShapedRecipe.java
-@@ -16,6 +16,13 @@
- import net.minecraft.world.item.crafting.display.ShapedCraftingRecipeDisplay;
- import net.minecraft.world.item.crafting.display.SlotDisplay;
- import net.minecraft.world.level.Level;
-+// CraftBukkit start
-+import org.bukkit.NamespacedKey;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.craftbukkit.inventory.CraftRecipe;
-+import org.bukkit.craftbukkit.inventory.CraftShapedRecipe;
-+import org.bukkit.inventory.RecipeChoice;
-+// CraftBukkit end
- 
- public class ShapedRecipe implements CraftingRecipe {
- 
-@@ -39,7 +46,69 @@
-         this(group, category, raw, result, true);
+@@ -103,6 +_,68 @@
+         );
      }
  
 +    // CraftBukkit start
-     @Override
-+    public org.bukkit.inventory.ShapedRecipe toBukkitRecipe(NamespacedKey id) {
-+        CraftItemStack result = CraftItemStack.asCraftMirror(this.result);
-+        CraftShapedRecipe recipe = new CraftShapedRecipe(id, result, this);
++    @Override
++    public org.bukkit.inventory.ShapedRecipe toBukkitRecipe(org.bukkit.NamespacedKey id) {
++        org.bukkit.craftbukkit.inventory.CraftItemStack result = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(this.result);
++        org.bukkit.craftbukkit.inventory.CraftShapedRecipe recipe = new org.bukkit.craftbukkit.inventory.CraftShapedRecipe(id, result, this);
 +        recipe.setGroup(this.group);
-+        recipe.setCategory(CraftRecipe.getCategory(this.category()));
++        recipe.setCategory(org.bukkit.craftbukkit.inventory.CraftRecipe.getCategory(this.category()));
 +
 +        switch (this.pattern.height()) {
 +        case 1:
@@ -69,8 +55,8 @@
 +        }
 +        char c = 'a';
 +        for (Optional<Ingredient> list : this.pattern.ingredients()) {
-+            RecipeChoice choice = CraftRecipe.toBukkit(list);
-+            if (choice != RecipeChoice.empty()) { // Paper
++            org.bukkit.inventory.RecipeChoice choice = org.bukkit.craftbukkit.inventory.CraftRecipe.toBukkit(list);
++            if (choice != org.bukkit.inventory.RecipeChoice.empty()) { // Paper
 +                recipe.setIngredient(c, choice);
 +            }
 +
@@ -80,7 +66,6 @@
 +    }
 +    // CraftBukkit end
 +
-+    @Override
-     public RecipeSerializer<? extends ShapedRecipe> getSerializer() {
-         return RecipeSerializer.SHAPED_RECIPE;
-     }
+     public static class Serializer implements RecipeSerializer<ShapedRecipe> {
+         public static final MapCodec<ShapedRecipe> CODEC = RecordCodecBuilder.mapCodec(
+             instance -> instance.group(
diff --git a/paper-server/patches/sources/net/minecraft/world/item/crafting/ShapelessRecipe.java.patch b/paper-server/patches/sources/net/minecraft/world/item/crafting/ShapelessRecipe.java.patch
new file mode 100644
index 0000000000..e13cbaac8e
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/crafting/ShapelessRecipe.java.patch
@@ -0,0 +1,24 @@
+--- a/net/minecraft/world/item/crafting/ShapelessRecipe.java
++++ b/net/minecraft/world/item/crafting/ShapelessRecipe.java
+@@ -31,6 +_,21 @@
+         this.ingredients = ingredients;
+     }
+ 
++    // CraftBukkit start
++    @Override
++    public org.bukkit.inventory.ShapelessRecipe toBukkitRecipe(org.bukkit.NamespacedKey id) {
++        org.bukkit.craftbukkit.inventory.CraftItemStack result = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(this.result);
++        org.bukkit.craftbukkit.inventory.CraftShapelessRecipe recipe = new org.bukkit.craftbukkit.inventory.CraftShapelessRecipe(id, result, this);
++        recipe.setGroup(this.group);
++        recipe.setCategory(org.bukkit.craftbukkit.inventory.CraftRecipe.getCategory(this.category()));
++
++        for (Ingredient list : this.ingredients) {
++            recipe.addIngredient(org.bukkit.craftbukkit.inventory.CraftRecipe.toBukkit(list));
++        }
++        return recipe;
++    }
++    // CraftBukkit end
++
+     @Override
+     public RecipeSerializer<ShapelessRecipe> getSerializer() {
+         return RecipeSerializer.SHAPELESS_RECIPE;
diff --git a/paper-server/patches/sources/net/minecraft/world/item/crafting/SmeltingRecipe.java.patch b/paper-server/patches/sources/net/minecraft/world/item/crafting/SmeltingRecipe.java.patch
new file mode 100644
index 0000000000..c174cfdcf8
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/crafting/SmeltingRecipe.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/item/crafting/SmeltingRecipe.java
++++ b/net/minecraft/world/item/crafting/SmeltingRecipe.java
+@@ -32,4 +_,17 @@
+             case MISC -> RecipeBookCategories.FURNACE_MISC;
+         };
+     }
++
++    // CraftBukkit start
++    @Override
++    public org.bukkit.inventory.Recipe toBukkitRecipe(org.bukkit.NamespacedKey id) {
++        org.bukkit.craftbukkit.inventory.CraftItemStack result = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(this.result());
++
++        org.bukkit.craftbukkit.inventory.CraftFurnaceRecipe recipe = new org.bukkit.craftbukkit.inventory.CraftFurnaceRecipe(id, result, org.bukkit.craftbukkit.inventory.CraftRecipe.toBukkit(this.input()), this.experience(), this.cookingTime());
++        recipe.setGroup(this.group());
++        recipe.setCategory(org.bukkit.craftbukkit.inventory.CraftRecipe.getCategory(this.category()));
++
++        return recipe;
++    }
++    // CraftBukkit end
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/item/crafting/SmithingTransformRecipe.java.patch b/paper-server/patches/sources/net/minecraft/world/item/crafting/SmithingTransformRecipe.java.patch
new file mode 100644
index 0000000000..5e4907953d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/crafting/SmithingTransformRecipe.java.patch
@@ -0,0 +1,46 @@
+--- a/net/minecraft/world/item/crafting/SmithingTransformRecipe.java
++++ b/net/minecraft/world/item/crafting/SmithingTransformRecipe.java
+@@ -21,8 +_,15 @@
+     final ItemStack result;
+     @Nullable
+     private PlacementInfo placementInfo;
++    final boolean copyDataComponents; // Paper - Option to prevent data components copy
+ 
+     public SmithingTransformRecipe(Optional<Ingredient> template, Optional<Ingredient> base, Optional<Ingredient> addition, ItemStack result) {
++        // Paper start - Option to prevent data components copy
++        this(template, base, addition, result, true);
++    }
++    public SmithingTransformRecipe(Optional<Ingredient> template, Optional<Ingredient> base, Optional<Ingredient> addition, ItemStack result, boolean copyDataComponents) {
++        this.copyDataComponents = copyDataComponents;
++        // Paper end - Option to prevent data components copy
+         this.template = template;
+         this.base = base;
+         this.addition = addition;
+@@ -32,7 +_,9 @@
+     @Override
+     public ItemStack assemble(SmithingRecipeInput input, HolderLookup.Provider registries) {
+         ItemStack itemStack = input.base().transmuteCopy(this.result.getItem(), this.result.getCount());
++        if (this.copyDataComponents) { // Paper - Option to prevent data components copy
+         itemStack.applyComponents(this.result.getComponentsPatch());
++        } // Paper - Option to prevent data components copy
+         return itemStack;
+     }
+ 
+@@ -77,6 +_,17 @@
+             )
+         );
+     }
++
++    // CraftBukkit start
++    @Override
++    public org.bukkit.inventory.Recipe toBukkitRecipe(org.bukkit.NamespacedKey id) {
++        org.bukkit.craftbukkit.inventory.CraftItemStack result = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(this.result);
++
++        org.bukkit.craftbukkit.inventory.CraftSmithingTransformRecipe recipe = new org.bukkit.craftbukkit.inventory.CraftSmithingTransformRecipe(id, result, org.bukkit.craftbukkit.inventory.CraftRecipe.toBukkit(this.template), org.bukkit.craftbukkit.inventory.CraftRecipe.toBukkit(this.base), org.bukkit.craftbukkit.inventory.CraftRecipe.toBukkit(this.addition), this.copyDataComponents); // Paper - Option to prevent data components copy
++
++        return recipe;
++    }
++    // CraftBukkit end
+ 
+     public static class Serializer implements RecipeSerializer<SmithingTransformRecipe> {
+         private static final MapCodec<SmithingTransformRecipe> CODEC = RecordCodecBuilder.mapCodec(
diff --git a/paper-server/patches/sources/net/minecraft/world/item/crafting/SmithingTrimRecipe.java.patch b/paper-server/patches/sources/net/minecraft/world/item/crafting/SmithingTrimRecipe.java.patch
new file mode 100644
index 0000000000..09cb0b71fe
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/crafting/SmithingTrimRecipe.java.patch
@@ -0,0 +1,58 @@
+--- a/net/minecraft/world/item/crafting/SmithingTrimRecipe.java
++++ b/net/minecraft/world/item/crafting/SmithingTrimRecipe.java
+@@ -27,8 +_,15 @@
+     final Optional<Ingredient> addition;
+     @Nullable
+     private PlacementInfo placementInfo;
++    final boolean copyDataComponents; // Paper - Option to prevent data components copy
+ 
+     public SmithingTrimRecipe(Optional<Ingredient> template, Optional<Ingredient> base, Optional<Ingredient> addition) {
++        // Paper start - Option to prevent data components copy
++        this(template, base, addition, true);
++    }
++    public SmithingTrimRecipe(Optional<Ingredient> template, Optional<Ingredient> base, Optional<Ingredient> addition, boolean copyDataComponents) {
++        this.copyDataComponents = copyDataComponents;
++        // Paper end - Option to prevent data components copy
+         this.template = template;
+         this.base = base;
+         this.addition = addition;
+@@ -36,10 +_,15 @@
+ 
+     @Override
+     public ItemStack assemble(SmithingRecipeInput input, HolderLookup.Provider registries) {
+-        return applyTrim(registries, input.base(), input.addition(), input.template());
++        return applyTrim(registries, input.base(), input.addition(), input.template(), this.copyDataComponents); // Paper - Option to prevent data components copy
+     }
+ 
+     public static ItemStack applyTrim(HolderLookup.Provider registries, ItemStack base, ItemStack addition, ItemStack template) {
++        // Paper start - Option to prevent data components copy
++        return applyTrim(registries, base, addition, template, true);
++    }
++    public static ItemStack applyTrim(HolderLookup.Provider registries, ItemStack base, ItemStack addition, ItemStack template, boolean copyDataComponents) {
++        // Paper end - Option to prevent data components copy
+         Optional<Holder.Reference<TrimMaterial>> fromIngredient = TrimMaterials.getFromIngredient(registries, addition);
+         Optional<Holder.Reference<TrimPattern>> fromTemplate = TrimPatterns.getFromTemplate(registries, template);
+         if (fromIngredient.isPresent() && fromTemplate.isPresent()) {
+@@ -47,7 +_,7 @@
+             if (armorTrim != null && armorTrim.hasPatternAndMaterial(fromTemplate.get(), fromIngredient.get())) {
+                 return ItemStack.EMPTY;
+             } else {
+-                ItemStack itemStack = base.copyWithCount(1);
++                ItemStack itemStack = copyDataComponents ? base.copyWithCount(1) : new ItemStack(base.getItem(), 1); // Paper - Option to prevent data components copy
+                 itemStack.set(DataComponents.TRIM, new ArmorTrim(fromIngredient.get(), fromTemplate.get()));
+                 return itemStack;
+             }
+@@ -100,6 +_,13 @@
+             )
+         );
+     }
++
++    // CraftBukkit start
++    @Override
++    public org.bukkit.inventory.Recipe toBukkitRecipe(org.bukkit.NamespacedKey id) {
++        return new org.bukkit.craftbukkit.inventory.CraftSmithingTrimRecipe(id, org.bukkit.craftbukkit.inventory.CraftRecipe.toBukkit(this.template), org.bukkit.craftbukkit.inventory.CraftRecipe.toBukkit(this.base), org.bukkit.craftbukkit.inventory.CraftRecipe.toBukkit(this.addition), this.copyDataComponents); // Paper - Option to prevent data components copy
++    }
++    // CraftBukkit end
+ 
+     public static class Serializer implements RecipeSerializer<SmithingTrimRecipe> {
+         private static final MapCodec<SmithingTrimRecipe> CODEC = RecordCodecBuilder.mapCodec(
diff --git a/paper-server/patches/sources/net/minecraft/world/item/crafting/SmokingRecipe.java.patch b/paper-server/patches/sources/net/minecraft/world/item/crafting/SmokingRecipe.java.patch
new file mode 100644
index 0000000000..e952bdf1b6
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/crafting/SmokingRecipe.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/item/crafting/SmokingRecipe.java
++++ b/net/minecraft/world/item/crafting/SmokingRecipe.java
+@@ -28,4 +_,17 @@
+     public RecipeBookCategory recipeBookCategory() {
+         return RecipeBookCategories.SMOKER_FOOD;
+     }
++
++    // CraftBukkit start
++    @Override
++    public org.bukkit.inventory.Recipe toBukkitRecipe(org.bukkit.NamespacedKey id) {
++        org.bukkit.craftbukkit.inventory.CraftItemStack result = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(this.result());
++
++        org.bukkit.craftbukkit.inventory.CraftSmokingRecipe recipe = new org.bukkit.craftbukkit.inventory.CraftSmokingRecipe(id, result, org.bukkit.craftbukkit.inventory.CraftRecipe.toBukkit(this.input()), this.experience(), this.cookingTime());
++        recipe.setGroup(this.group());
++        recipe.setCategory(org.bukkit.craftbukkit.inventory.CraftRecipe.getCategory(this.category()));
++
++        return recipe;
++    }
++    // CraftBukkit end
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/item/crafting/StonecutterRecipe.java.patch b/paper-server/patches/sources/net/minecraft/world/item/crafting/StonecutterRecipe.java.patch
new file mode 100644
index 0000000000..daa51ba292
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/crafting/StonecutterRecipe.java.patch
@@ -0,0 +1,19 @@
+--- a/net/minecraft/world/item/crafting/StonecutterRecipe.java
++++ b/net/minecraft/world/item/crafting/StonecutterRecipe.java
+@@ -35,4 +_,16 @@
+     public RecipeBookCategory recipeBookCategory() {
+         return RecipeBookCategories.STONECUTTER;
+     }
++
++    // CraftBukkit start
++    @Override
++    public org.bukkit.inventory.Recipe toBukkitRecipe(org.bukkit.NamespacedKey id) {
++        org.bukkit.craftbukkit.inventory.CraftItemStack result = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(this.result());
++
++        org.bukkit.craftbukkit.inventory.CraftStonecuttingRecipe recipe = new org.bukkit.craftbukkit.inventory.CraftStonecuttingRecipe(id, result, org.bukkit.craftbukkit.inventory.CraftRecipe.toBukkit(this.input()));
++        recipe.setGroup(this.group());
++
++        return recipe;
++    }
++    // CraftBukkit end
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/item/crafting/TransmuteRecipe.java.patch b/paper-server/patches/sources/net/minecraft/world/item/crafting/TransmuteRecipe.java.patch
new file mode 100644
index 0000000000..bdfba17d6a
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/crafting/TransmuteRecipe.java.patch
@@ -0,0 +1,16 @@
+--- a/net/minecraft/world/item/crafting/TransmuteRecipe.java
++++ b/net/minecraft/world/item/crafting/TransmuteRecipe.java
+@@ -88,6 +_,13 @@
+         );
+     }
+ 
++    // CraftBukkit start
++    @Override
++    public org.bukkit.inventory.Recipe toBukkitRecipe(org.bukkit.NamespacedKey id) {
++        return new org.bukkit.craftbukkit.inventory.CraftTransmuteRecipe(id, org.bukkit.craftbukkit.inventory.CraftItemType.minecraftToBukkit(this.result.value()), org.bukkit.craftbukkit.inventory.CraftRecipe.toBukkit(this.input), org.bukkit.craftbukkit.inventory.CraftRecipe.toBukkit(this.material));
++    }
++    // CraftBukkit end
++
+     @Override
+     public RecipeSerializer<TransmuteRecipe> getSerializer() {
+         return RecipeSerializer.TRANSMUTE;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/enchantment/ItemEnchantments.java.patch b/paper-server/patches/sources/net/minecraft/world/item/enchantment/ItemEnchantments.java.patch
similarity index 59%
rename from paper-server/patches/unapplied/net/minecraft/world/item/enchantment/ItemEnchantments.java.patch
rename to paper-server/patches/sources/net/minecraft/world/item/enchantment/ItemEnchantments.java.patch
index 519f69ad9b..d49d59d33a 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/item/enchantment/ItemEnchantments.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/item/enchantment/ItemEnchantments.java.patch
@@ -1,60 +1,54 @@
 --- a/net/minecraft/world/item/enchantment/ItemEnchantments.java
 +++ b/net/minecraft/world/item/enchantment/ItemEnchantments.java
-@@ -26,12 +26,25 @@
- import net.minecraft.world.item.Item;
- import net.minecraft.world.item.TooltipFlag;
+@@ -28,10 +_,19 @@
  import net.minecraft.world.item.component.TooltipProvider;
-+// Paper start
-+import it.unimi.dsi.fastutil.objects.Object2IntAVLTreeMap;
-+// Paper end
  
  public class ItemEnchantments implements TooltipProvider {
 -    public static final ItemEnchantments EMPTY = new ItemEnchantments(new Object2IntOpenHashMap<>(), true);
 +    // Paper start
 +    private static final java.util.Comparator<Holder<Enchantment>> ENCHANTMENT_ORDER = java.util.Comparator.comparing(Holder::getRegisteredName);
-+    public static final ItemEnchantments EMPTY = new ItemEnchantments(new Object2IntAVLTreeMap<>(ENCHANTMENT_ORDER), true);
++    public static final ItemEnchantments EMPTY = new ItemEnchantments(new it.unimi.dsi.fastutil.objects.Object2IntAVLTreeMap<>(ENCHANTMENT_ORDER), true);
 +    // Paper end
      private static final Codec<Integer> LEVEL_CODEC = Codec.intRange(1, 255);
 -    private static final Codec<Object2IntOpenHashMap<Holder<Enchantment>>> LEVELS_CODEC = Codec.unboundedMap(Enchantment.CODEC, LEVEL_CODEC)
 -        .xmap(Object2IntOpenHashMap::new, Function.identity());
-+    private static final Codec<Object2IntAVLTreeMap<Holder<Enchantment>>> LEVELS_CODEC = Codec.unboundedMap(
-+            Enchantment.CODEC, LEVEL_CODEC
-+        )// Paper start - sort enchantments
++    // Paper start - sort enchantments
++    private static final Codec<it.unimi.dsi.fastutil.objects.Object2IntAVLTreeMap<Holder<Enchantment>>> LEVELS_CODEC = Codec.unboundedMap(Enchantment.CODEC, LEVEL_CODEC)
 +        .xmap(m -> {
-+            final Object2IntAVLTreeMap<Holder<Enchantment>> map = new Object2IntAVLTreeMap<>(ENCHANTMENT_ORDER);
++            final it.unimi.dsi.fastutil.objects.Object2IntAVLTreeMap<Holder<Enchantment>> map = new it.unimi.dsi.fastutil.objects.Object2IntAVLTreeMap<>(ENCHANTMENT_ORDER);
 +            map.putAll(m);
 +            return map;
 +        }, Function.identity());
 +    // Paper end - sort enchantments
      private static final Codec<ItemEnchantments> FULL_CODEC = RecordCodecBuilder.create(
          instance -> instance.group(
-                     LEVELS_CODEC.fieldOf("levels").forGetter(component -> component.enchantments),
-@@ -41,16 +54,16 @@
+                 LEVELS_CODEC.fieldOf("levels").forGetter(itemEnchantments -> itemEnchantments.enchantments),
+@@ -41,16 +_,16 @@
      );
      public static final Codec<ItemEnchantments> CODEC = Codec.withAlternative(FULL_CODEC, LEVELS_CODEC, map -> new ItemEnchantments(map, true));
      public static final StreamCodec<RegistryFriendlyByteBuf, ItemEnchantments> STREAM_CODEC = StreamCodec.composite(
 -        ByteBufCodecs.map(Object2IntOpenHashMap::new, Enchantment.STREAM_CODEC, ByteBufCodecs.VAR_INT),
-+        ByteBufCodecs.map((v) -> new Object2IntAVLTreeMap<>(ENCHANTMENT_ORDER), Enchantment.STREAM_CODEC, ByteBufCodecs.VAR_INT),
-         component -> component.enchantments,
++        ByteBufCodecs.map((v) -> new it.unimi.dsi.fastutil.objects.Object2IntAVLTreeMap<>(ENCHANTMENT_ORDER), Enchantment.STREAM_CODEC, ByteBufCodecs.VAR_INT), // Paper
+         itemEnchantments -> itemEnchantments.enchantments,
          ByteBufCodecs.BOOL,
-         component -> component.showInTooltip,
+         itemEnchantments -> itemEnchantments.showInTooltip,
          ItemEnchantments::new
      );
 -    final Object2IntOpenHashMap<Holder<Enchantment>> enchantments;
-+    final Object2IntAVLTreeMap<Holder<Enchantment>> enchantments; // Paper
++    final it.unimi.dsi.fastutil.objects.Object2IntAVLTreeMap<Holder<Enchantment>> enchantments; // Paper
      public final boolean showInTooltip;
  
 -    ItemEnchantments(Object2IntOpenHashMap<Holder<Enchantment>> enchantments, boolean showInTooltip) {
-+    ItemEnchantments(Object2IntAVLTreeMap<Holder<Enchantment>> enchantments, boolean showInTooltip) { // Paper
++    ItemEnchantments(it.unimi.dsi.fastutil.objects.Object2IntAVLTreeMap<Holder<Enchantment>> enchantments, boolean showInTooltip) { // Paper
          this.enchantments = enchantments;
          this.showInTooltip = showInTooltip;
  
-@@ -139,7 +152,7 @@
+@@ -139,7 +_,7 @@
      }
  
      public static class Mutable {
 -        private final Object2IntOpenHashMap<Holder<Enchantment>> enchantments = new Object2IntOpenHashMap<>();
-+        private final Object2IntAVLTreeMap<Holder<Enchantment>> enchantments = new Object2IntAVLTreeMap<>(ENCHANTMENT_ORDER); // Paper
++        private final it.unimi.dsi.fastutil.objects.Object2IntAVLTreeMap<Holder<Enchantment>> enchantments = new it.unimi.dsi.fastutil.objects.Object2IntAVLTreeMap<>(ENCHANTMENT_ORDER); // Paper
          public boolean showInTooltip;
  
-         public Mutable(ItemEnchantments enchantmentsComponent) {
+         public Mutable(ItemEnchantments enchantments) {
diff --git a/paper-server/patches/sources/net/minecraft/world/item/enchantment/effects/ApplyMobEffect.java.patch b/paper-server/patches/sources/net/minecraft/world/item/enchantment/effects/ApplyMobEffect.java.patch
new file mode 100644
index 0000000000..47f39569b2
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/enchantment/effects/ApplyMobEffect.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/item/enchantment/effects/ApplyMobEffect.java
++++ b/net/minecraft/world/item/enchantment/effects/ApplyMobEffect.java
+@@ -44,7 +_,7 @@
+                 int max = Math.max(
+                     0, Math.round(Mth.randomBetween(random, this.minAmplifier.calculate(enchantmentLevel), this.maxAmplifier.calculate(enchantmentLevel)))
+                 );
+-                livingEntity.addEffect(new MobEffectInstance(randomElement.get(), rounded, max));
++                livingEntity.addEffect(new MobEffectInstance(randomElement.get(), rounded, max), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ATTACK); // CraftBukkit
+             }
+         }
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/item/enchantment/effects/ChangeItemDamage.java.patch b/paper-server/patches/sources/net/minecraft/world/item/enchantment/effects/ChangeItemDamage.java.patch
new file mode 100644
index 0000000000..0de21ed24c
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/enchantment/effects/ChangeItemDamage.java.patch
@@ -0,0 +1,14 @@
+--- a/net/minecraft/world/item/enchantment/effects/ChangeItemDamage.java
++++ b/net/minecraft/world/item/enchantment/effects/ChangeItemDamage.java
+@@ -21,9 +_,9 @@
+     public void apply(ServerLevel level, int enchantmentLevel, EnchantedItemInUse item, Entity entity, Vec3 origin) {
+         ItemStack itemStack = item.itemStack();
+         if (itemStack.has(DataComponents.MAX_DAMAGE) && itemStack.has(DataComponents.DAMAGE)) {
+-            ServerPlayer serverPlayer1 = item.owner() instanceof ServerPlayer serverPlayer ? serverPlayer : null;
++            //ServerPlayer serverPlayer1 = item.owner() instanceof ServerPlayer serverPlayer ? serverPlayer : null; // Paper - EntityDamageItemEvent - always pass in entity
+             int i = (int)this.amount.calculate(enchantmentLevel);
+-            itemStack.hurtAndBreak(i, level, serverPlayer1, item.onBreak());
++            itemStack.hurtAndBreak(i, level, item.owner(), item.onBreak()); // Paper - EntityDamageItemEvent - always pass in entity
+         }
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/item/enchantment/effects/Ignite.java.patch b/paper-server/patches/sources/net/minecraft/world/item/enchantment/effects/Ignite.java.patch
new file mode 100644
index 0000000000..e1daa636a5
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/enchantment/effects/Ignite.java.patch
@@ -0,0 +1,25 @@
+--- a/net/minecraft/world/item/enchantment/effects/Ignite.java
++++ b/net/minecraft/world/item/enchantment/effects/Ignite.java
+@@ -15,7 +_,21 @@
+ 
+     @Override
+     public void apply(ServerLevel level, int enchantmentLevel, EnchantedItemInUse item, Entity entity, Vec3 origin) {
+-        entity.igniteForSeconds(this.duration.calculate(enchantmentLevel));
++        // CraftBukkit start - Call a combust event when somebody hits with a fire enchanted item
++        org.bukkit.event.entity.EntityCombustEvent entityCombustEvent;
++        if (item.owner() != null) {
++            entityCombustEvent = new org.bukkit.event.entity.EntityCombustByEntityEvent(item.owner().getBukkitEntity(), entity.getBukkitEntity(), this.duration.calculate(enchantmentLevel));
++        } else {
++            entityCombustEvent = new org.bukkit.event.entity.EntityCombustEvent(entity.getBukkitEntity(), this.duration.calculate(enchantmentLevel));
++        }
++
++        org.bukkit.Bukkit.getPluginManager().callEvent(entityCombustEvent);
++        if (entityCombustEvent.isCancelled()) {
++            return;
++        }
++
++        entity.igniteForSeconds(entityCombustEvent.getDuration(), false);
++        // CraftBukkit end
+     }
+ 
+     @Override
diff --git a/paper-server/patches/sources/net/minecraft/world/item/enchantment/effects/ReplaceBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/item/enchantment/effects/ReplaceBlock.java.patch
new file mode 100644
index 0000000000..4e789c8e9d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/enchantment/effects/ReplaceBlock.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/item/enchantment/effects/ReplaceBlock.java
++++ b/net/minecraft/world/item/enchantment/effects/ReplaceBlock.java
+@@ -30,7 +_,7 @@
+     public void apply(ServerLevel level, int enchantmentLevel, EnchantedItemInUse item, Entity entity, Vec3 origin) {
+         BlockPos blockPos = BlockPos.containing(origin).offset(this.offset);
+         if (this.predicate.map(blockPredicate -> blockPredicate.test(level, blockPos)).orElse(true)
+-            && level.setBlockAndUpdate(blockPos, this.blockState.getState(entity.getRandom(), blockPos))) {
++            && org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(level, blockPos, this.blockState.getState(entity.getRandom(), blockPos), entity)) { // CraftBukkit - Call EntityBlockFormEvent
+             this.triggerGameEvent.ifPresent(holder -> level.gameEvent(entity, (Holder<GameEvent>)holder, blockPos));
+         }
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/item/enchantment/effects/ReplaceDisk.java.patch b/paper-server/patches/sources/net/minecraft/world/item/enchantment/effects/ReplaceDisk.java.patch
new file mode 100644
index 0000000000..08b184622e
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/enchantment/effects/ReplaceDisk.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/item/enchantment/effects/ReplaceDisk.java
++++ b/net/minecraft/world/item/enchantment/effects/ReplaceDisk.java
+@@ -47,7 +_,7 @@
+         for (BlockPos blockPos1 : BlockPos.betweenClosed(blockPos.offset(-i, 0, -i), blockPos.offset(i, Math.min(i1 - 1, 0), i))) {
+             if (blockPos1.distToCenterSqr(origin.x(), blockPos1.getY() + 0.5, origin.z()) < Mth.square(i)
+                 && this.predicate.map(predicate -> predicate.test(level, blockPos1)).orElse(true)
+-                && level.setBlockAndUpdate(blockPos1, this.blockState.getState(random, blockPos1))) {
++                && org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(level, blockPos1, this.blockState.getState(random, blockPos1), entity)) { // CraftBukkit - Call EntityBlockFormEvent for Frost Walker
+                 this.triggerGameEvent.ifPresent(event -> level.gameEvent(entity, (Holder<GameEvent>)event, blockPos1));
+             }
+         }
diff --git a/paper-server/patches/sources/net/minecraft/world/item/enchantment/effects/SummonEntityEffect.java.patch b/paper-server/patches/sources/net/minecraft/world/item/enchantment/effects/SummonEntityEffect.java.patch
new file mode 100644
index 0000000000..cf03e4582d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/enchantment/effects/SummonEntityEffect.java.patch
@@ -0,0 +1,22 @@
+--- a/net/minecraft/world/item/enchantment/effects/SummonEntityEffect.java
++++ b/net/minecraft/world/item/enchantment/effects/SummonEntityEffect.java
+@@ -34,11 +_,18 @@
+         if (Level.isInSpawnableBounds(blockPos)) {
+             Optional<Holder<EntityType<?>>> randomElement = this.entityTypes().getRandomElement(level.getRandom());
+             if (!randomElement.isEmpty()) {
+-                Entity entity1 = randomElement.get().value().spawn(level, blockPos, EntitySpawnReason.TRIGGERED);
++                Entity entity1 = randomElement.get().value().create(level, null, blockPos, EntitySpawnReason.TRIGGERED, false, false); // CraftBukkit
+                 if (entity1 != null) {
+                     if (entity1 instanceof LightningBolt lightningBolt && item.owner() instanceof ServerPlayer serverPlayer) {
+                         lightningBolt.setCause(serverPlayer);
+                     }
++                    // CraftBukkit start
++                    if (entity1 instanceof LightningBolt) {
++                        level.strikeLightning(entity1, (item.itemStack().is(net.minecraft.world.item.Items.TRIDENT)) ? org.bukkit.event.weather.LightningStrikeEvent.Cause.TRIDENT : org.bukkit.event.weather.LightningStrikeEvent.Cause.ENCHANTMENT);
++                    } else {
++                        level.addFreshEntityWithPassengers(entity1, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.ENCHANTMENT);
++                    }
++                    // CraftBukkit end
+ 
+                     if (this.joinTeam && entity.getTeam() != null) {
+                         level.getScoreboard().addPlayerToTeam(entity1.getScoreboardName(), entity.getTeam());
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/trading/Merchant.java.patch b/paper-server/patches/sources/net/minecraft/world/item/trading/Merchant.java.patch
similarity index 94%
rename from paper-server/patches/unapplied/net/minecraft/world/item/trading/Merchant.java.patch
rename to paper-server/patches/sources/net/minecraft/world/item/trading/Merchant.java.patch
index a8626b121e..36c879841f 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/item/trading/Merchant.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/item/trading/Merchant.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/item/trading/Merchant.java
 +++ b/net/minecraft/world/item/trading/Merchant.java
-@@ -20,6 +20,7 @@
+@@ -19,6 +_,7 @@
  
      void overrideOffers(MerchantOffers offers);
  
@@ -8,7 +8,7 @@
      void notifyTrade(MerchantOffer offer);
  
      void notifyTradeUpdated(ItemStack stack);
-@@ -54,4 +55,6 @@
+@@ -50,4 +_,6 @@
      boolean isClientSide();
  
      boolean stillValid(Player player);
diff --git a/paper-server/patches/sources/net/minecraft/world/item/trading/MerchantOffer.java.patch b/paper-server/patches/sources/net/minecraft/world/item/trading/MerchantOffer.java.patch
new file mode 100644
index 0000000000..05eb5597d3
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/trading/MerchantOffer.java.patch
@@ -0,0 +1,87 @@
+--- a/net/minecraft/world/item/trading/MerchantOffer.java
++++ b/net/minecraft/world/item/trading/MerchantOffer.java
+@@ -21,6 +_,7 @@
+                 Codec.INT.lenientOptionalFieldOf("demand", Integer.valueOf(0)).forGetter(merchantOffer -> merchantOffer.demand),
+                 Codec.FLOAT.lenientOptionalFieldOf("priceMultiplier", Float.valueOf(0.0F)).forGetter(merchantOffer -> merchantOffer.priceMultiplier),
+                 Codec.INT.lenientOptionalFieldOf("xp", Integer.valueOf(1)).forGetter(merchantOffer -> merchantOffer.xp)
++                , Codec.BOOL.lenientOptionalFieldOf("Paper.IgnoreDiscounts", false).forGetter(merchantOffer -> merchantOffer.ignoreDiscounts) // Paper
+             )
+             .apply(instance, MerchantOffer::new)
+     );
+@@ -37,6 +_,21 @@
+     public int demand;
+     public float priceMultiplier;
+     public int xp;
++    public boolean ignoreDiscounts; // Paper - Add ignore discounts API
++
++    // CraftBukkit start
++    private org.bukkit.craftbukkit.inventory.@org.jspecify.annotations.Nullable CraftMerchantRecipe bukkitHandle;
++
++    public org.bukkit.craftbukkit.inventory.CraftMerchantRecipe asBukkit() {
++        return (this.bukkitHandle == null) ? this.bukkitHandle = new org.bukkit.craftbukkit.inventory.CraftMerchantRecipe(this) : this.bukkitHandle;
++    }
++
++    public MerchantOffer(ItemCost baseCostA, Optional<ItemCost> costB, ItemStack result, int uses, int maxUses, int experience, float priceMultiplier, int demand, final boolean ignoreDiscounts, org.bukkit.craftbukkit.inventory.CraftMerchantRecipe bukkit) { // Paper
++        this(baseCostA, costB, result, uses, maxUses, experience, priceMultiplier, demand);
++        this.ignoreDiscounts = ignoreDiscounts; // Paper
++        this.bukkitHandle = bukkit;
++    }
++    // CraftBukkit end
+ 
+     private MerchantOffer(
+         ItemCost baseCostA,
+@@ -49,6 +_,7 @@
+         int demand,
+         float priceMultiplier,
+         int xp
++        , final boolean ignoreDiscounts // Paper
+     ) {
+         this.baseCostA = baseCostA;
+         this.costB = costB;
+@@ -60,6 +_,7 @@
+         this.demand = demand;
+         this.priceMultiplier = priceMultiplier;
+         this.xp = xp;
++        this.ignoreDiscounts = ignoreDiscounts; // Paper
+     }
+ 
+     public MerchantOffer(ItemCost baseCostA, ItemStack result, int maxUses, int xp, float priceMultiplier) {
+@@ -75,7 +_,7 @@
+     }
+ 
+     public MerchantOffer(ItemCost baseCostA, Optional<ItemCost> costB, ItemStack result, int _uses, int maxUses, int xp, float priceMultiplier, int demand) {
+-        this(baseCostA, costB, result, _uses, maxUses, true, 0, demand, priceMultiplier, xp);
++        this(baseCostA, costB, result, _uses, maxUses, true, 0, demand, priceMultiplier, xp, false); // Paper
+     }
+ 
+     private MerchantOffer(MerchantOffer other) {
+@@ -90,6 +_,7 @@
+             other.demand,
+             other.priceMultiplier,
+             other.xp
++            , other.ignoreDiscounts // Paper
+         );
+     }
+ 
+@@ -124,7 +_,7 @@
+     }
+ 
+     public void updateDemand() {
+-        this.demand = this.demand + this.uses - (this.maxUses - this.uses);
++        this.demand = Math.max(0, this.demand + this.uses - (this.maxUses - this.uses)); // Paper - Fix MC-163962
+     }
+ 
+     public ItemStack assemble() {
+@@ -205,7 +_,11 @@
+         if (!this.satisfiedBy(playerOfferA, playerOfferB)) {
+             return false;
+         } else {
+-            playerOfferA.shrink(this.getCostA().getCount());
++            // CraftBukkit start
++            if (!this.getCostA().isEmpty()) {
++                playerOfferA.shrink(this.getCostA().getCount());
++            }
++            // CraftBukkit end
+             if (!this.getCostB().isEmpty()) {
+                 playerOfferB.shrink(this.getCostB().getCount());
+             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/BaseCommandBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/BaseCommandBlock.java.patch
similarity index 50%
rename from paper-server/patches/unapplied/net/minecraft/world/level/BaseCommandBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/BaseCommandBlock.java.patch
index 3c091f17a7..a00c43b741 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/BaseCommandBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/BaseCommandBlock.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/level/BaseCommandBlock.java
 +++ b/net/minecraft/world/level/BaseCommandBlock.java
-@@ -33,6 +33,10 @@
+@@ -32,6 +_,11 @@
      private String command = "";
      @Nullable
      private Component customName;
@@ -8,27 +8,28 @@
 +    @Override
 +    public abstract org.bukkit.command.CommandSender getBukkitSender(CommandSourceStack wrapper);
 +    // CraftBukkit end
++
  
-     public BaseCommandBlock() {}
- 
-@@ -132,7 +136,7 @@
- 
-                         });
- 
--                        minecraftserver.getCommands().performPrefixedCommand(commandlistenerwrapper, this.command);
-+                        minecraftserver.getCommands().dispatchServerCommand(commandlistenerwrapper, this.command); // CraftBukkit
-                     } catch (Throwable throwable) {
-                         CrashReport crashreport = CrashReport.forThrowable(throwable, "Executing command block");
-                         CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Command to be executed");
-@@ -174,6 +178,7 @@
+     public int getSuccessCount() {
+         return this.successCount;
+@@ -126,7 +_,7 @@
+                             this.successCount++;
+                         }
+                     });
+-                    server.getCommands().performPrefixedCommand(commandSourceStack, this.command);
++                    server.getCommands().dispatchServerCommand(commandSourceStack, this.command); // CraftBukkit
+                 } catch (Throwable var6) {
+                     CrashReport crashReport = CrashReport.forThrowable(var6, "Executing command block");
+                     CrashReportCategory crashReportCategory = crashReport.addCategory("Command to be executed");
+@@ -162,6 +_,7 @@
      @Override
-     public void sendSystemMessage(Component message) {
+     public void sendSystemMessage(Component component) {
          if (this.trackOutput) {
 +            org.spigotmc.AsyncCatcher.catchOp("sendSystemMessage to a command block"); // Paper - Don't broadcast messages to command blocks
-             SimpleDateFormat simpledateformat = BaseCommandBlock.TIME_FORMAT;
-             Date date = new Date();
- 
-@@ -200,7 +205,7 @@
+             this.lastOutput = Component.literal("[" + TIME_FORMAT.format(new Date()) + "] ").append(component);
+             this.onUpdated();
+         }
+@@ -184,7 +_,7 @@
      }
  
      public InteractionResult usedBy(Player player) {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/BaseSpawner.java.patch b/paper-server/patches/sources/net/minecraft/world/level/BaseSpawner.java.patch
new file mode 100644
index 0000000000..a27f9ad41f
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/BaseSpawner.java.patch
@@ -0,0 +1,160 @@
+--- a/net/minecraft/world/level/BaseSpawner.java
++++ b/net/minecraft/world/level/BaseSpawner.java
+@@ -44,13 +_,15 @@
+     public int maxNearbyEntities = 6;
+     public int requiredPlayerRange = 16;
+     public int spawnRange = 4;
++    private int tickDelay = 0; // Paper - Configurable mob spawner tick rate
+ 
+     public void setEntityId(EntityType<?> type, @Nullable Level level, RandomSource random, BlockPos pos) {
+         this.getOrCreateNextSpawnData(level, random, pos).getEntityToSpawn().putString("id", BuiltInRegistries.ENTITY_TYPE.getKey(type).toString());
++        this.spawnPotentials = SimpleWeightedRandomList.empty(); // CraftBukkit - SPIGOT-3496, MC-92282
+     }
+ 
+     public boolean isNearPlayer(Level level, BlockPos pos) {
+-        return level.hasNearbyAlivePlayer(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, this.requiredPlayerRange);
++        return level.hasNearbyAlivePlayerThatAffectsSpawning(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, this.requiredPlayerRange); // Paper - Affects Spawning API
+     }
+ 
+     public void clientTick(Level level, BlockPos pos) {
+@@ -73,13 +_,19 @@
+     }
+ 
+     public void serverTick(ServerLevel serverLevel, BlockPos pos) {
++        if (spawnCount <= 0 || maxNearbyEntities <= 0) return; // Paper - Ignore impossible spawn tick
++        // Paper start - Configurable mob spawner tick rate
++        if (spawnDelay > 0 && --tickDelay > 0) return;
++        tickDelay = serverLevel.paperConfig().tickRates.mobSpawner;
++        if (tickDelay == -1) { return; } // If disabled
++        // Paper end - Configurable mob spawner tick rate
+         if (this.isNearPlayer(serverLevel, pos)) {
+-            if (this.spawnDelay == -1) {
++            if (this.spawnDelay < -tickDelay) { // Paper - Configurable mob spawner tick rate
+                 this.delay(serverLevel, pos);
+             }
+ 
+             if (this.spawnDelay > 0) {
+-                this.spawnDelay--;
++                this.spawnDelay -= tickDelay; // Paper - Configurable mob spawner tick rate
+             } else {
+                 boolean flag = false;
+                 RandomSource random = serverLevel.getRandom();
+@@ -113,6 +_,21 @@
+                             continue;
+                         }
+ 
++                        // Paper start - PreCreatureSpawnEvent
++                        com.destroystokyo.paper.event.entity.PreSpawnerSpawnEvent event = new com.destroystokyo.paper.event.entity.PreSpawnerSpawnEvent(
++                            io.papermc.paper.util.MCUtil.toLocation(serverLevel, d, d1, d2),
++                            org.bukkit.craftbukkit.entity.CraftEntityType.minecraftToBukkit(optional.get()),
++                            io.papermc.paper.util.MCUtil.toLocation(serverLevel, pos)
++                        );
++                        if (!event.callEvent()) {
++                            flag = true;
++                            if (event.shouldAbortSpawn()) {
++                                break;
++                            }
++                            continue;
++                        }
++                        // Paper end - PreCreatureSpawnEvent
++
+                         Entity entity = EntityType.loadEntityRecursive(entityToSpawn, serverLevel, EntitySpawnReason.SPAWNER, entity1 -> {
+                             entity1.moveTo(d, d1, d2, entity1.getYRot(), entity1.getXRot());
+                             return entity1;
+@@ -133,6 +_,7 @@
+                             return;
+                         }
+ 
++                        entity.preserveMotion = true; // Paper - Fix Entity Teleportation and cancel velocity if teleported; preserve entity motion from tag
+                         entity.moveTo(entity.getX(), entity.getY(), entity.getZ(), random.nextFloat() * 360.0F, 0.0F);
+                         if (entity instanceof Mob mob) {
+                             if (nextSpawnData.getCustomSpawnRules().isEmpty() && !mob.checkSpawnRules(serverLevel, EntitySpawnReason.SPAWNER)
+@@ -147,9 +_,22 @@
+                             }
+ 
+                             nextSpawnData.getEquipment().ifPresent(mob::equip);
++                            // Spigot start
++                            if (mob.level().spigotConfig.nerfSpawnerMobs) {
++                                mob.aware = false;
++                            }
++                            // Spigot end
+                         }
+ 
+-                        if (!serverLevel.tryAddFreshEntityWithPassengers(entity)) {
++                        entity.spawnedViaMobSpawner = true; // Paper
++                        entity.spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.SPAWNER; // Paper - Entity#getEntitySpawnReason
++                        flag = true; // Paper
++                        // CraftBukkit start
++                        if (org.bukkit.craftbukkit.event.CraftEventFactory.callSpawnerSpawnEvent(entity, pos).isCancelled()) {
++                            continue;
++                        }
++                        if (!serverLevel.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.SPAWNER)) {
++                            // CraftBukkit end
+                             this.delay(serverLevel, pos);
+                             return;
+                         }
+@@ -160,7 +_,7 @@
+                             ((Mob)entity).spawnAnim();
+                         }
+ 
+-                        flag = true;
++                        //flag = true; // Paper - moved up above cancellable event
+                     }
+                 }
+ 
+@@ -184,7 +_,13 @@
+     }
+ 
+     public void load(@Nullable Level level, BlockPos pos, CompoundTag tag) {
++        // Paper start - use larger int if set
++        if (tag.contains("Paper.Delay")) {
++            this.spawnDelay = tag.getInt("Paper.Delay");
++        } else {
+         this.spawnDelay = tag.getShort("Delay");
++        }
++        // Paper end
+         boolean flag = tag.contains("SpawnData", 10);
+         if (flag) {
+             SpawnData spawnData = SpawnData.CODEC
+@@ -205,9 +_,15 @@
+             this.spawnPotentials = SimpleWeightedRandomList.single(this.nextSpawnData != null ? this.nextSpawnData : new SpawnData());
+         }
+ 
++        // Paper start - use ints if set
++        if (tag.contains("Paper.MinSpawnDelay", net.minecraft.nbt.Tag.TAG_ANY_NUMERIC)) {
++            this.minSpawnDelay = tag.getInt("Paper.MinSpawnDelay");
++            this.maxSpawnDelay = tag.getInt("Paper.MaxSpawnDelay");
++            this.spawnCount = tag.getShort("SpawnCount");
++        } else // Paper end
+         if (tag.contains("MinSpawnDelay", 99)) {
+-            this.minSpawnDelay = tag.getShort("MinSpawnDelay");
+-            this.maxSpawnDelay = tag.getShort("MaxSpawnDelay");
++            this.minSpawnDelay = tag.getInt("MinSpawnDelay"); // Paper - short -> int
++            this.maxSpawnDelay = tag.getInt("MaxSpawnDelay"); // Paper - short -> int
+             this.spawnCount = tag.getShort("SpawnCount");
+         }
+ 
+@@ -224,9 +_,20 @@
+     }
+ 
+     public CompoundTag save(CompoundTag tag) {
+-        tag.putShort("Delay", (short)this.spawnDelay);
+-        tag.putShort("MinSpawnDelay", (short)this.minSpawnDelay);
+-        tag.putShort("MaxSpawnDelay", (short)this.maxSpawnDelay);
++        // Paper start
++        if (spawnDelay > Short.MAX_VALUE) {
++            tag.putInt("Paper.Delay", this.spawnDelay);
++        }
++        tag.putShort("Delay", (short) Math.min(Short.MAX_VALUE, this.spawnDelay));
++
++        if (minSpawnDelay > Short.MAX_VALUE || maxSpawnDelay > Short.MAX_VALUE) {
++            tag.putInt("Paper.MinSpawnDelay", this.minSpawnDelay);
++            tag.putInt("Paper.MaxSpawnDelay", this.maxSpawnDelay);
++        }
++
++        tag.putShort("MinSpawnDelay", (short) Math.min(Short.MAX_VALUE, this.minSpawnDelay));
++        tag.putShort("MaxSpawnDelay", (short) Math.min(Short.MAX_VALUE, this.maxSpawnDelay));
++        // Paper end
+         tag.putShort("SpawnCount", (short)this.spawnCount);
+         tag.putShort("MaxNearbyEntities", (short)this.maxNearbyEntities);
+         tag.putShort("RequiredPlayerRange", (short)this.requiredPlayerRange);
diff --git a/paper-server/patches/sources/net/minecraft/world/level/BlockGetter.java.patch b/paper-server/patches/sources/net/minecraft/world/level/BlockGetter.java.patch
new file mode 100644
index 0000000000..f329019055
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/BlockGetter.java.patch
@@ -0,0 +1,76 @@
+--- a/net/minecraft/world/level/BlockGetter.java
++++ b/net/minecraft/world/level/BlockGetter.java
+@@ -11,6 +_,7 @@
+ import net.minecraft.core.BlockPos;
+ import net.minecraft.core.Direction;
+ import net.minecraft.util.Mth;
++import net.minecraft.world.level.block.Block;
+ import net.minecraft.world.level.block.entity.BlockEntity;
+ import net.minecraft.world.level.block.entity.BlockEntityType;
+ import net.minecraft.world.level.block.state.BlockState;
+@@ -33,6 +_,16 @@
+ 
+     BlockState getBlockState(BlockPos pos);
+ 
++    // Paper start - if loaded util
++    @Nullable BlockState getBlockStateIfLoaded(BlockPos blockposition);
++
++    default @Nullable Block getBlockIfLoaded(BlockPos blockposition) {
++        BlockState type = this.getBlockStateIfLoaded(blockposition);
++        return type == null ? null : type.getBlock();
++    }
++    @Nullable FluidState getFluidIfLoaded(BlockPos blockposition);
++    // Paper end
++
+     FluidState getFluidState(BlockPos pos);
+ 
+     default int getLightEmission(BlockPos pos) {
+@@ -66,10 +_,25 @@
+         );
+     }
+ 
+-    default BlockHitResult clip(ClipContext context) {
+-        return traverseBlocks(context.getFrom(), context.getTo(), context, (traverseContext, traversePos) -> {
+-            BlockState blockState = this.getBlockState(traversePos);
+-            FluidState fluidState = this.getFluidState(traversePos);
++    // CraftBukkit start - moved block handling into separate method for use by Block#rayTrace
++    default BlockHitResult clip(ClipContext raytrace1, BlockPos blockposition) {
++        // Paper start - Add predicate for blocks when raytracing
++        return clip(raytrace1, blockposition, null);
++    }
++
++    default BlockHitResult clip(ClipContext traverseContext, BlockPos traversePos, java.util.function.Predicate<? super org.bukkit.block.Block> canCollide) {
++        // Paper end - Add predicate for blocks when raytracing
++        // Paper start - Prevent raytrace from loading chunks
++        BlockState blockState = this.getBlockStateIfLoaded(traversePos);
++        if (blockState == null) {
++            // copied the last function parameter (listed below)
++            Vec3 vec3d = traverseContext.getFrom().subtract(traverseContext.getTo());
++
++            return BlockHitResult.miss(traverseContext.getTo(), Direction.getApproximateNearest(vec3d.x, vec3d.y, vec3d.z), BlockPos.containing(traverseContext.getTo()));
++        }
++        // Paper end - Prevent raytrace from loading chunks
++        if (blockState.isAir() || (canCollide != null && this instanceof LevelAccessor levelAccessor && !canCollide.test(org.bukkit.craftbukkit.block.CraftBlock.at(levelAccessor, traversePos)))) return null; // Paper - Perf: optimise air cases & check canCollide predicate
++            FluidState fluidState = blockState.getFluidState(); // Paper - Perf: don't need to go to world state again
+             Vec3 from = traverseContext.getFrom();
+             Vec3 to = traverseContext.getTo();
+             VoxelShape blockShape = traverseContext.getBlockShape(blockState, this, traversePos);
+@@ -79,6 +_,18 @@
+             double d = blockHitResult == null ? Double.MAX_VALUE : traverseContext.getFrom().distanceToSqr(blockHitResult.getLocation());
+             double d1 = blockHitResult1 == null ? Double.MAX_VALUE : traverseContext.getFrom().distanceToSqr(blockHitResult1.getLocation());
+             return d <= d1 ? blockHitResult : blockHitResult1;
++    }
++    // CraftBukkit end
++
++    default BlockHitResult clip(ClipContext context) {
++        // Paper start - Add predicate for blocks when raytracing
++        return clip(context, (java.util.function.Predicate<org.bukkit.block.Block>) null);
++    }
++
++    default BlockHitResult clip(ClipContext context, java.util.function.Predicate<? super org.bukkit.block.Block> canCollide) {
++        // Paper end - Add predicate for blocks when raytracing
++        return (BlockHitResult) BlockGetter.traverseBlocks(context.getFrom(), context.getTo(), context, (raytrace1, blockposition) -> {
++            return this.clip(raytrace1, blockposition, canCollide); // CraftBukkit - moved into separate method // Paper - Add predicate for blocks when raytracing
+         }, failContext -> {
+             Vec3 vec3 = failContext.getFrom().subtract(failContext.getTo());
+             return BlockHitResult.miss(failContext.getTo(), Direction.getApproximateNearest(vec3.x, vec3.y, vec3.z), BlockPos.containing(failContext.getTo()));
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/ChunkPos.java.patch b/paper-server/patches/sources/net/minecraft/world/level/ChunkPos.java.patch
similarity index 69%
rename from paper-server/patches/unapplied/net/minecraft/world/level/ChunkPos.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/ChunkPos.java.patch
index be7a951951..891340e337 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/ChunkPos.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/ChunkPos.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/level/ChunkPos.java
 +++ b/net/minecraft/world/level/ChunkPos.java
-@@ -46,6 +46,7 @@
+@@ -46,6 +_,7 @@
      public static final int REGION_MAX_INDEX = 31;
      public final int x;
      public final int z;
@@ -8,10 +8,10 @@
      private static final int HASH_A = 1664525;
      private static final int HASH_C = 1013904223;
      private static final int HASH_Z_XOR = -559038737;
-@@ -53,16 +54,19 @@
-     public ChunkPos(int x, int z) {
+@@ -53,16 +_,19 @@
+     public ChunkPos(int x, int y) {
          this.x = x;
-         this.z = z;
+         this.z = y;
 +        this.longKey = asLong(this.x, this.z); // Paper
      }
  
@@ -21,19 +21,19 @@
 +        this.longKey = asLong(this.x, this.z); // Paper
      }
  
-     public ChunkPos(long pos) {
-         this.x = (int)pos;
-         this.z = (int)(pos >> 32);
+     public ChunkPos(long packedPos) {
+         this.x = (int)packedPos;
+         this.z = (int)(packedPos >> 32);
 +        this.longKey = asLong(this.x, this.z); // Paper
      }
  
-     public static ChunkPos minFromRegion(int x, int z) {
-@@ -74,7 +78,7 @@
+     public static ChunkPos minFromRegion(int chunkX, int chunkZ) {
+@@ -74,7 +_,7 @@
      }
  
      public long toLong() {
 -        return asLong(this.x, this.z);
-+        return longKey; // Paper
++        return this.longKey; // Paper
      }
  
-     public static long asLong(int chunkX, int chunkZ) {
+     public static long asLong(int x, int z) {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/ClipContext.java.patch b/paper-server/patches/sources/net/minecraft/world/level/ClipContext.java.patch
new file mode 100644
index 0000000000..85d8652279
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/ClipContext.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/level/ClipContext.java
++++ b/net/minecraft/world/level/ClipContext.java
+@@ -21,7 +_,7 @@
+     private final CollisionContext collisionContext;
+ 
+     public ClipContext(Vec3 from, Vec3 to, ClipContext.Block block, ClipContext.Fluid fluid, Entity entity) {
+-        this(from, to, block, fluid, CollisionContext.of(entity));
++        this(from, to, block, fluid, (entity == null) ? CollisionContext.empty() : CollisionContext.of(entity)); // CraftBukkit
+     }
+ 
+     public ClipContext(Vec3 from, Vec3 to, ClipContext.Block block, ClipContext.Fluid fluid, CollisionContext collisionContext) {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/EmptyBlockAndTintGetter.java.patch b/paper-server/patches/sources/net/minecraft/world/level/EmptyBlockAndTintGetter.java.patch
new file mode 100644
index 0000000000..6ca1c8d13e
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/EmptyBlockAndTintGetter.java.patch
@@ -0,0 +1,21 @@
+--- a/net/minecraft/world/level/EmptyBlockAndTintGetter.java
++++ b/net/minecraft/world/level/EmptyBlockAndTintGetter.java
+@@ -39,6 +_,18 @@
+         return Blocks.AIR.defaultBlockState();
+     }
+ 
++    // Paper start
++    @Override
++    public @org.jetbrains.annotations.Nullable BlockState getBlockStateIfLoaded(final BlockPos blockposition) {
++        return null;
++    }
++
++    @Override
++    public @org.jetbrains.annotations.Nullable FluidState getFluidIfLoaded(final BlockPos blockposition) {
++        return null;
++    }
++    // Paper end
++
+     @Override
+     public FluidState getFluidState(BlockPos pos) {
+         return Fluids.EMPTY.defaultFluidState();
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/EmptyBlockGetter.java.patch b/paper-server/patches/sources/net/minecraft/world/level/EmptyBlockGetter.java.patch
similarity index 95%
rename from paper-server/patches/unapplied/net/minecraft/world/level/EmptyBlockGetter.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/EmptyBlockGetter.java.patch
index b4743ab862..dfb2f644ab 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/EmptyBlockGetter.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/EmptyBlockGetter.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/world/level/EmptyBlockGetter.java
 +++ b/net/minecraft/world/level/EmptyBlockGetter.java
-@@ -17,7 +17,19 @@
+@@ -17,6 +_,18 @@
          return null;
      }
  
 +    // Paper start - If loaded util
-     @Override
++    @Override
 +    public final FluidState getFluidIfLoaded(BlockPos blockposition) {
 +        return Fluids.EMPTY.defaultFluidState();
 +    }
@@ -16,7 +16,6 @@
 +    }
 +    // Paper end
 +
-+    @Override
+     @Override
      public BlockState getBlockState(BlockPos pos) {
          return Blocks.AIR.defaultBlockState();
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/EntityGetter.java.patch b/paper-server/patches/sources/net/minecraft/world/level/EntityGetter.java.patch
similarity index 88%
rename from paper-server/patches/unapplied/net/minecraft/world/level/EntityGetter.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/EntityGetter.java.patch
index 09a6133595..edf6c35510 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/EntityGetter.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/EntityGetter.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/level/EntityGetter.java
 +++ b/net/minecraft/world/level/EntityGetter.java
-@@ -71,6 +71,11 @@
+@@ -71,6 +_,12 @@
          }
      }
  
@@ -9,10 +9,11 @@
 +        return this.getNearestPlayer(entity.getX(), entity.getY(), entity.getZ(), maxDistance, predicate);
 +    }
 +    // Paper end - Affects Spawning API
++
      @Nullable
-     default Player getNearestPlayer(double x, double y, double z, double maxDistance, @Nullable Predicate<Entity> targetPredicate) {
+     default Player getNearestPlayer(double x, double y, double z, double distance, @Nullable Predicate<Entity> predicate) {
          double d = -1.0;
-@@ -89,6 +94,28 @@
+@@ -89,6 +_,28 @@
          return player;
      }
  
@@ -39,10 +40,10 @@
 +    // Paper end
 +
      @Nullable
-     default Player getNearestPlayer(Entity entity, double maxDistance) {
-         return this.getNearestPlayer(entity.getX(), entity.getY(), entity.getZ(), maxDistance, false);
-@@ -100,6 +127,20 @@
-         return this.getNearestPlayer(x, y, z, maxDistance, predicate);
+     default Player getNearestPlayer(Entity entity, double distance) {
+         return this.getNearestPlayer(entity.getX(), entity.getY(), entity.getZ(), distance, false);
+@@ -100,6 +_,20 @@
+         return this.getNearestPlayer(x, y, z, distance, predicate);
      }
  
 +    // Paper start - Affects Spawning API
@@ -59,10 +60,10 @@
 +    }
 +    // Paper end - Affects Spawning API
 +
-     default boolean hasNearbyAlivePlayer(double x, double y, double z, double range) {
+     default boolean hasNearbyAlivePlayer(double x, double y, double z, double distance) {
          for (Player player : this.players()) {
              if (EntitySelector.NO_SPECTATORS.test(player) && EntitySelector.LIVING_ENTITY_STILL_ALIVE.test(player)) {
-@@ -124,4 +165,11 @@
+@@ -124,4 +_,11 @@
  
          return null;
      }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/GameRules.java.patch b/paper-server/patches/sources/net/minecraft/world/level/GameRules.java.patch
new file mode 100644
index 0000000000..0e251ca8c9
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/GameRules.java.patch
@@ -0,0 +1,298 @@
+--- a/net/minecraft/world/level/GameRules.java
++++ b/net/minecraft/world/level/GameRules.java
+@@ -32,6 +_,14 @@
+ import org.slf4j.Logger;
+ 
+ public class GameRules {
++    // Paper start - allow disabling gamerule limits
++    private static final boolean DISABLE_LIMITS = Boolean.getBoolean("paper.disableGameRuleLimits");
++
++    private static int limit(final int limit, final int unlimited) {
++        return DISABLE_LIMITS ? unlimited : limit;
++    }
++    // Paper end - allow disabling gamerule limits
++
+     public static final int DEFAULT_RANDOM_TICK_SPEED = 3;
+     static final Logger LOGGER = LogUtils.getLogger();
+     private static final Map<GameRules.Key<?>, GameRules.Type<?>> GAME_RULE_TYPES = Maps.newTreeMap(Comparator.comparing(entry -> entry.id));
+@@ -81,10 +_,10 @@
+         "sendCommandFeedback", GameRules.Category.CHAT, GameRules.BooleanValue.create(true)
+     );
+     public static final GameRules.Key<GameRules.BooleanValue> RULE_REDUCEDDEBUGINFO = register(
+-        "reducedDebugInfo", GameRules.Category.MISC, GameRules.BooleanValue.create(false, (server, value) -> {
++        "reducedDebugInfo", GameRules.Category.MISC, GameRules.BooleanValue.create(false, (level, value) -> { // Paper - rename param to match changes
+             byte b = (byte)(value.get() ? 22 : 23);
+ 
+-            for (ServerPlayer serverPlayer : server.getPlayerList().getPlayers()) {
++            for (ServerPlayer serverPlayer : level.players()) {
+                 serverPlayer.connection.send(new ClientboundEntityEventPacket(serverPlayer, b));
+             }
+         })
+@@ -108,8 +_,8 @@
+         "doWeatherCycle", GameRules.Category.UPDATES, GameRules.BooleanValue.create(true)
+     );
+     public static final GameRules.Key<GameRules.BooleanValue> RULE_LIMITED_CRAFTING = register(
+-        "doLimitedCrafting", GameRules.Category.PLAYER, GameRules.BooleanValue.create(false, (server, value) -> {
+-            for (ServerPlayer serverPlayer : server.getPlayerList().getPlayers()) {
++        "doLimitedCrafting", GameRules.Category.PLAYER, GameRules.BooleanValue.create(false, (level, value) -> { // Paper - rename param to match changes
++            for (ServerPlayer serverPlayer : level.players()) {
+                 serverPlayer.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.LIMITED_CRAFTING, value.get() ? 1.0F : 0.0F));
+             }
+         })
+@@ -133,8 +_,8 @@
+         "doInsomnia", GameRules.Category.SPAWNING, GameRules.BooleanValue.create(true)
+     );
+     public static final GameRules.Key<GameRules.BooleanValue> RULE_DO_IMMEDIATE_RESPAWN = register(
+-        "doImmediateRespawn", GameRules.Category.PLAYER, GameRules.BooleanValue.create(false, (server, value) -> {
+-            for (ServerPlayer serverPlayer : server.getPlayerList().getPlayers()) {
++        "doImmediateRespawn", GameRules.Category.PLAYER, GameRules.BooleanValue.create(false, (level, value) -> { // Paper - rename param to match changes
++            for (ServerPlayer serverPlayer : level.players()) {
+                 serverPlayer.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.IMMEDIATE_RESPAWN, value.get() ? 1.0F : 0.0F));
+             }
+         })
+@@ -205,16 +_,17 @@
+     public static final GameRules.Key<GameRules.IntegerValue> RULE_MINECART_MAX_SPEED = register(
+         "minecartMaxSpeed",
+         GameRules.Category.MISC,
+-        GameRules.IntegerValue.create(8, 1, 1000, FeatureFlagSet.of(FeatureFlags.MINECART_IMPROVEMENTS), (server, value) -> {})
++        GameRules.IntegerValue.create(8, 1, limit(1000, Integer.MAX_VALUE), FeatureFlagSet.of(FeatureFlags.MINECART_IMPROVEMENTS), (server, value) -> {}) // Paper - allow disabling gamerule limits
+     );
+     public static final GameRules.Key<GameRules.IntegerValue> RULE_SPAWN_CHUNK_RADIUS = register(
+-        "spawnChunkRadius", GameRules.Category.MISC, GameRules.IntegerValue.create(2, 0, 32, FeatureFlagSet.of(), (server, value) -> {
+-            ServerLevel serverLevel = server.overworld();
++        "spawnChunkRadius", GameRules.Category.MISC, GameRules.IntegerValue.create(2, 0, limit(32, Integer.MAX_VALUE), FeatureFlagSet.of(), (level, value) -> { // Paper - allow disabling gamerule limits - also, rename param
++            ServerLevel serverLevel = level; // CraftBukkit - per-world
+             serverLevel.setDefaultSpawnPos(serverLevel.getSharedSpawnPos(), serverLevel.getSharedSpawnAngle());
+         })
+     );
+     private final Map<GameRules.Key<?>, GameRules.Value<?>> rules;
+     private final FeatureFlagSet enabledFeatures;
++    private final GameRules.Value<?>[] gameruleArray; // Paper - Perf: Use array for gamerule storage
+ 
+     private static <T extends GameRules.Value<T>> GameRules.Key<T> register(String name, GameRules.Category category, GameRules.Type<T> type) {
+         GameRules.Key<T> key = new GameRules.Key<>(name, category);
+@@ -242,10 +_,21 @@
+     private GameRules(Map<GameRules.Key<?>, GameRules.Value<?>> rules, FeatureFlagSet enabledFeatures) {
+         this.rules = rules;
+         this.enabledFeatures = enabledFeatures;
++
++        // Paper start - Perf: Use array for gamerule storage
++        int arraySize = GameRules.Key.lastGameRuleIndex + 1;
++        GameRules.Value<?>[] values = new GameRules.Value[arraySize];
++
++        for (Entry<GameRules.Key<?>, GameRules.Value<?>> entry : rules.entrySet()) {
++            values[entry.getKey().gameRuleIndex] = entry.getValue();
++        }
++
++        this.gameruleArray = values;
++        // Paper end - Perf: Use array for gamerule storage
+     }
+ 
+     public <T extends GameRules.Value<T>> T getRule(GameRules.Key<T> key) {
+-        T value = (T)this.rules.get(key);
++        T value = key == null ? null : (T) this.gameruleArray[key.gameRuleIndex]; // Paper - Perf: Use array for gamerule storage
+         if (value == null) {
+             throw new IllegalArgumentException("Tried to access invalid game rule");
+         } else {
+@@ -286,13 +_,13 @@
+         }
+     }
+ 
+-    public void assignFrom(GameRules rules, @Nullable MinecraftServer server) {
+-        rules.rules.keySet().forEach(key -> this.assignCap((GameRules.Key<?>)key, rules, server));
++    public void assignFrom(GameRules rules, @Nullable ServerLevel level) { // CraftBukkit - per-world
++        rules.rules.keySet().forEach(key -> this.assignCap((GameRules.Key<?>)key, rules, level)); // CraftBukkit - per-world
+     }
+ 
+-    private <T extends GameRules.Value<T>> void assignCap(GameRules.Key<T> key, GameRules rules, @Nullable MinecraftServer server) {
++    private <T extends GameRules.Value<T>> void assignCap(GameRules.Key<T> key, GameRules rules, @Nullable ServerLevel level) { // CraftBukkit - per-world
+         T rule = rules.getRule(key);
+-        this.<T>getRule(key).setFrom(rule, server);
++        this.<T>getRule(key).setFrom(rule, level); // CraftBukkit - per-world
+     }
+ 
+     public boolean getBoolean(GameRules.Key<GameRules.BooleanValue> key) {
+@@ -306,7 +_,7 @@
+     public static class BooleanValue extends GameRules.Value<GameRules.BooleanValue> {
+         private boolean value;
+ 
+-        static GameRules.Type<GameRules.BooleanValue> create(boolean defaultValue, BiConsumer<MinecraftServer, GameRules.BooleanValue> changeListener) {
++        static GameRules.Type<GameRules.BooleanValue> create(boolean defaultValue, BiConsumer<ServerLevel, GameRules.BooleanValue> changeListener) { // CraftBukkit - per-world
+             return new GameRules.Type<>(
+                 BoolArgumentType::bool,
+                 type -> new GameRules.BooleanValue(type, defaultValue),
+@@ -326,17 +_,21 @@
+         }
+ 
+         @Override
+-        protected void updateFromArgument(CommandContext<CommandSourceStack> context, String paramName) {
+-            this.value = BoolArgumentType.getBool(context, paramName);
++        // Paper start - Add WorldGameRuleChangeEvent
++        protected void updateFromArgument(CommandContext<CommandSourceStack> context, String paramName, GameRules.Key<BooleanValue> gameRuleKey) {
++                io.papermc.paper.event.world.WorldGameRuleChangeEvent event = new io.papermc.paper.event.world.WorldGameRuleChangeEvent(context.getSource().getBukkitWorld(), context.getSource().getBukkitSender(), (org.bukkit.GameRule<Boolean>) org.bukkit.GameRule.getByName(gameRuleKey.toString()), String.valueOf(BoolArgumentType.getBool(context, paramName)));
++                if (!event.callEvent()) return;
++                this.value = Boolean.parseBoolean(event.getValue());
++        // Paper end - Add WorldGameRuleChangeEvent
+         }
+ 
+         public boolean get() {
+             return this.value;
+         }
+ 
+-        public void set(boolean value, @Nullable MinecraftServer server) {
++        public void set(boolean value, @Nullable ServerLevel level) { // CraftBukkit - per-world
+             this.value = value;
+-            this.onChanged(server);
++            this.onChanged(level); // CraftBukkit - per-world
+         }
+ 
+         @Override
+@@ -345,7 +_,7 @@
+         }
+ 
+         @Override
+-        protected void deserialize(String value) {
++        public void deserialize(String value) { // PAIL - protected->public
+             this.value = Boolean.parseBoolean(value);
+         }
+ 
+@@ -365,9 +_,9 @@
+         }
+ 
+         @Override
+-        public void setFrom(GameRules.BooleanValue value, @Nullable MinecraftServer server) {
++        public void setFrom(GameRules.BooleanValue value, @Nullable ServerLevel level) { // CraftBukkit - per-world
+             this.value = value.value;
+-            this.onChanged(server);
++            this.onChanged(level); // CraftBukkit - per-world
+         }
+     }
+ 
+@@ -405,7 +_,7 @@
+     public static class IntegerValue extends GameRules.Value<GameRules.IntegerValue> {
+         private int value;
+ 
+-        private static GameRules.Type<GameRules.IntegerValue> create(int defaultValue, BiConsumer<MinecraftServer, GameRules.IntegerValue> changeListener) {
++        private static GameRules.Type<GameRules.IntegerValue> create(int defaultValue, BiConsumer<ServerLevel, GameRules.IntegerValue> changeListener) { // CraftBukkit - per-world
+             return new GameRules.Type<>(
+                 IntegerArgumentType::integer,
+                 type -> new GameRules.IntegerValue(type, defaultValue),
+@@ -416,7 +_,7 @@
+         }
+ 
+         static GameRules.Type<GameRules.IntegerValue> create(
+-            int defaultValue, int min, int max, FeatureFlagSet requiredFeatures, BiConsumer<MinecraftServer, GameRules.IntegerValue> changeListener
++            int defaultValue, int min, int max, FeatureFlagSet requiredFeatures, BiConsumer<ServerLevel, GameRules.IntegerValue> changeListener // CraftBukkit - per-world
+         ) {
+             return new GameRules.Type<>(
+                 () -> IntegerArgumentType.integer(min, max),
+@@ -437,17 +_,21 @@
+         }
+ 
+         @Override
+-        protected void updateFromArgument(CommandContext<CommandSourceStack> context, String paramName) {
+-            this.value = IntegerArgumentType.getInteger(context, paramName);
++        // Paper start - Add WorldGameRuleChangeEvent
++        protected void updateFromArgument(CommandContext<CommandSourceStack> context, String paramName, GameRules.Key<IntegerValue> gameRuleKey) {
++            io.papermc.paper.event.world.WorldGameRuleChangeEvent event = new io.papermc.paper.event.world.WorldGameRuleChangeEvent(context.getSource().getBukkitWorld(), context.getSource().getBukkitSender(), (org.bukkit.GameRule<Integer>) org.bukkit.GameRule.getByName(gameRuleKey.toString()), String.valueOf(IntegerArgumentType.getInteger(context, paramName)));
++            if (!event.callEvent()) return;
++            this.value = Integer.parseInt(event.getValue());
++            // Paper end - Add WorldGameRuleChangeEvent
+         }
+ 
+         public int get() {
+             return this.value;
+         }
+ 
+-        public void set(int value, @Nullable MinecraftServer server) {
++        public void set(int value, @Nullable ServerLevel level) { // CraftBukkit - per-world
+             this.value = value;
+-            this.onChanged(server);
++            this.onChanged(level); // CraftBukkit - per-world
+         }
+ 
+         @Override
+@@ -456,7 +_,7 @@
+         }
+ 
+         @Override
+-        protected void deserialize(String value) {
++        public void deserialize(String value) { // PAIL - protected->public
+             this.value = safeParse(value);
+         }
+ 
+@@ -498,13 +_,17 @@
+         }
+ 
+         @Override
+-        public void setFrom(GameRules.IntegerValue value, @Nullable MinecraftServer server) {
++        public void setFrom(GameRules.IntegerValue value, @Nullable ServerLevel level) { // CraftBukkit - per-world
+             this.value = value.value;
+-            this.onChanged(server);
++            this.onChanged(level); // CraftBukkit - per-world
+         }
+     }
+ 
+     public static final class Key<T extends GameRules.Value<T>> {
++        // Paper start - Perf: Use array for gamerule storage
++        public static int lastGameRuleIndex = 0;
++        public final int gameRuleIndex = lastGameRuleIndex++;
++        // Paper end - Perf: Use array for gamerule storage
+         final String id;
+         private final GameRules.Category category;
+ 
+@@ -544,14 +_,14 @@
+     public static class Type<T extends GameRules.Value<T>> {
+         final Supplier<ArgumentType<?>> argument;
+         private final Function<GameRules.Type<T>, T> constructor;
+-        final BiConsumer<MinecraftServer, T> callback;
++        final BiConsumer<ServerLevel, T> callback; // CraftBukkit - per-world
+         private final GameRules.VisitorCaller<T> visitorCaller;
+         final FeatureFlagSet requiredFeatures;
+ 
+         Type(
+             Supplier<ArgumentType<?>> argument,
+             Function<GameRules.Type<T>, T> constructor,
+-            BiConsumer<MinecraftServer, T> callback,
++            BiConsumer<ServerLevel, T> callback, // CraftBukkit - per-world
+             GameRules.VisitorCaller<T> visitorCaller,
+             FeatureFlagSet requiredFeature
+         ) {
+@@ -586,20 +_,20 @@
+             this.type = type;
+         }
+ 
+-        protected abstract void updateFromArgument(CommandContext<CommandSourceStack> context, String paramName);
++        protected abstract void updateFromArgument(CommandContext<CommandSourceStack> context, String paramName, GameRules.Key<T> gameRuleKey); // Paper - Add WorldGameRuleChangeEvent
+ 
+-        public void setFromArgument(CommandContext<CommandSourceStack> context, String paramName) {
+-            this.updateFromArgument(context, paramName);
+-            this.onChanged(context.getSource().getServer());
++        public void setFromArgument(CommandContext<CommandSourceStack> context, String paramName, GameRules.Key<T> gameRuleKey) { // Paper - Add WorldGameRuleChangeEvent
++            this.updateFromArgument(context, paramName, gameRuleKey); // Paper - Add WorldGameRuleChangeEvent
++            this.onChanged(context.getSource().getLevel()); // CraftBukkit - per-world
+         }
+ 
+-        public void onChanged(@Nullable MinecraftServer server) {
+-            if (server != null) {
+-                this.type.callback.accept(server, this.getSelf());
++        public void onChanged(@Nullable ServerLevel level) { // CraftBukkit - per-world
++            if (level != null) { // CraftBukkit - per-world
++                this.type.callback.accept(level, this.getSelf()); // CraftBukkit - per-world
+             }
+         }
+ 
+-        protected abstract void deserialize(String value);
++        public abstract void deserialize(String value); // PAIL - private->public
+ 
+         public abstract String serialize();
+ 
+@@ -614,7 +_,7 @@
+ 
+         protected abstract T copy();
+ 
+-        public abstract void setFrom(T value, @Nullable MinecraftServer server);
++        public abstract void setFrom(T value, @Nullable ServerLevel level); // CraftBukkit - per-world
+     }
+ 
+     interface VisitorCaller<T extends GameRules.Value<T>> {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/Level.java.patch b/paper-server/patches/sources/net/minecraft/world/level/Level.java.patch
similarity index 68%
rename from paper-server/patches/unapplied/net/minecraft/world/level/Level.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/Level.java.patch
index 6a3ac30d97..9958bd72b6 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/Level.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/Level.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/level/Level.java
 +++ b/net/minecraft/world/level/Level.java
-@@ -25,8 +25,10 @@
+@@ -24,8 +_,10 @@
  import net.minecraft.network.protocol.Packet;
  import net.minecraft.resources.ResourceKey;
  import net.minecraft.resources.ResourceLocation;
@@ -11,30 +11,7 @@
  import net.minecraft.sounds.SoundEvent;
  import net.minecraft.sounds.SoundEvents;
  import net.minecraft.sounds.SoundSource;
-@@ -43,6 +45,7 @@
- import net.minecraft.world.entity.Entity;
- import net.minecraft.world.entity.boss.EnderDragonPart;
- import net.minecraft.world.entity.boss.enderdragon.EnderDragon;
-+import net.minecraft.world.entity.item.ItemEntity;
- import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.item.alchemy.PotionBrewing;
-@@ -57,12 +60,14 @@
- import net.minecraft.world.level.block.entity.FuelValues;
- import net.minecraft.world.level.block.entity.TickingBlockEntity;
- import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.world.level.border.BorderChangeListener;
- import net.minecraft.world.level.border.WorldBorder;
- import net.minecraft.world.level.chunk.ChunkAccess;
- import net.minecraft.world.level.chunk.ChunkSource;
- import net.minecraft.world.level.chunk.LevelChunk;
- import net.minecraft.world.level.chunk.status.ChunkStatus;
- import net.minecraft.world.level.dimension.DimensionType;
-+import net.minecraft.world.level.dimension.LevelStem;
- import net.minecraft.world.level.entity.EntityTypeTest;
- import net.minecraft.world.level.entity.LevelEntityGetter;
- import net.minecraft.world.level.gameevent.GameEvent;
-@@ -81,6 +86,25 @@
+@@ -79,6 +_,27 @@
  import net.minecraft.world.phys.Vec3;
  import net.minecraft.world.scores.Scoreboard;
  
@@ -46,6 +23,8 @@
 +import net.minecraft.network.protocol.game.ClientboundSetBorderSizePacket;
 +import net.minecraft.network.protocol.game.ClientboundSetBorderWarningDelayPacket;
 +import net.minecraft.network.protocol.game.ClientboundSetBorderWarningDistancePacket;
++import net.minecraft.world.level.border.BorderChangeListener;
++import net.minecraft.world.level.dimension.LevelStem;
 +import org.bukkit.Bukkit;
 +import org.bukkit.craftbukkit.CraftServer;
 +import org.bukkit.craftbukkit.CraftWorld;
@@ -58,9 +37,9 @@
 +// CraftBukkit end
 +
  public abstract class Level implements LevelAccessor, AutoCloseable {
- 
      public static final Codec<ResourceKey<Level>> RESOURCE_KEY_CODEC = ResourceKey.codec(Registries.DIMENSION);
-@@ -94,7 +118,7 @@
+     public static final ResourceKey<Level> OVERWORLD = ResourceKey.create(Registries.DIMENSION, ResourceLocation.withDefaultNamespace("overworld"));
+@@ -91,7 +_,7 @@
      public static final int TICKS_PER_DAY = 24000;
      public static final int MAX_ENTITY_SPAWN_Y = 20000000;
      public static final int MIN_ENTITY_SPAWN_Y = -20000000;
@@ -69,28 +48,22 @@
      protected final NeighborUpdater neighborUpdater;
      private final List<TickingBlockEntity> pendingBlockEntityTickers = Lists.newArrayList();
      private boolean tickingBlockEntities;
-@@ -121,23 +145,91 @@
+@@ -117,6 +_,61 @@
      private final DamageSources damageSources;
      private long subTickCount;
  
--    protected Level(WritableLevelData properties, ResourceKey<Level> registryRef, RegistryAccess registryManager, Holder<DimensionType> dimensionEntry, boolean isClient, boolean debugWorld, long seed, int maxChainedNeighborUpdates) {
--        this.levelData = properties;
--        this.dimensionTypeRegistration = dimensionEntry;
--        final DimensionType dimensionmanager = (DimensionType) dimensionEntry.value();
 +    // CraftBukkit start Added the following
 +    private final CraftWorld world;
 +    public boolean pvpMode;
 +    public org.bukkit.generator.ChunkGenerator generator;
- 
--        this.dimension = registryRef;
--        this.isClientSide = isClient;
++
 +    public boolean preventPoiUpdated = false; // CraftBukkit - SPIGOT-5710
 +    public boolean captureBlockStates = false;
 +    public boolean captureTreeGeneration = false;
 +    public boolean isBlockPlaceCancelled = false; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent
 +    public Map<BlockPos, org.bukkit.craftbukkit.block.CraftBlockState> capturedBlockStates = new java.util.LinkedHashMap<>(); // Paper
 +    public Map<BlockPos, BlockEntity> capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper - Retain block place order when capturing blockstates
-+    public List<ItemEntity> captureDrops;
++    public List<net.minecraft.world.entity.item.ItemEntity> captureDrops;
 +    public final it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<SpawnCategory> ticksPerSpawnCategory = new it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<>();
 +    public boolean populating;
 +    public final org.spigotmc.SpigotWorldConfig spigotConfig; // Spigot
@@ -134,8 +107,22 @@
 +
 +    public abstract ResourceKey<LevelStem> getTypeKey();
 +
-+    protected Level(WritableLevelData worlddatamutable, ResourceKey<Level> resourcekey, RegistryAccess iregistrycustom, Holder<DimensionType> holder, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function<org.spigotmc.SpigotWorldConfig, io.papermc.paper.configuration.WorldConfiguration> paperWorldConfigCreator) { // Paper - create paper world config
-+        this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot
+     protected Level(
+         WritableLevelData levelData,
+         ResourceKey<Level> dimension,
+@@ -125,8 +_,26 @@
+         boolean isClientSide,
+         boolean isDebug,
+         long biomeZoomSeed,
+-        int maxChainedNeighborUpdates
++        int maxChainedNeighborUpdates,
++        org.bukkit.generator.ChunkGenerator gen, // CraftBukkit
++        org.bukkit.generator.BiomeProvider biomeProvider, // CraftBukkit
++        org.bukkit.World.Environment env, // CraftBukkit
++        java.util.function.Function<org.spigotmc.SpigotWorldConfig, // Spigot - create per world config
++        io.papermc.paper.configuration.WorldConfiguration> paperWorldConfigCreator // Paper - create paper world config
+     ) {
++        this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) levelData).getLevelName()); // Spigot
 +        this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper - create paper world config
 +        this.generator = gen;
 +        this.world = new CraftWorld((ServerLevel) this, gen, biomeProvider, env);
@@ -148,42 +135,30 @@
 +        }
 +
 +        // CraftBukkit end
-+        this.levelData = worlddatamutable;
-+        this.dimensionTypeRegistration = holder;
-+        final DimensionType dimensionmanager = (DimensionType) holder.value();
-+
-+        this.dimension = resourcekey;
-+        this.isClientSide = flag;
-         if (dimensionmanager.coordinateScale() != 1.0D) {
--            this.worldBorder = new WorldBorder(this) {
-+            this.worldBorder = new WorldBorder() { // CraftBukkit - decompile error
+         this.levelData = levelData;
+         this.dimensionTypeRegistration = dimensionTypeRegistration;
+         final DimensionType dimensionType = dimensionTypeRegistration.value();
+@@ -136,12 +_,12 @@
+             this.worldBorder = new WorldBorder() {
                  @Override
                  public double getCenterX() {
--                    return super.getCenterX() / dimensionmanager.coordinateScale();
+-                    return super.getCenterX() / dimensionType.coordinateScale();
 +                    return super.getCenterX(); // CraftBukkit
                  }
  
                  @Override
                  public double getCenterZ() {
--                    return super.getCenterZ() / dimensionmanager.coordinateScale();
+-                    return super.getCenterZ() / dimensionType.coordinateScale();
 +                    return super.getCenterZ(); // CraftBukkit
                  }
              };
          } else {
-@@ -145,13 +237,90 @@
-         }
- 
-         this.thread = Thread.currentThread();
--        this.biomeManager = new BiomeManager(this, seed);
--        this.isDebug = debugWorld;
--        this.neighborUpdater = new CollectingNeighborUpdater(this, maxChainedNeighborUpdates);
--        this.registryAccess = registryManager;
--        this.damageSources = new DamageSources(registryManager);
-+        this.biomeManager = new BiomeManager(this, i);
-+        this.isDebug = flag1;
-+        this.neighborUpdater = new CollectingNeighborUpdater(this, j);
-+        this.registryAccess = iregistrycustom;
-+        this.damageSources = new DamageSources(iregistrycustom);
+@@ -154,7 +_,86 @@
+         this.neighborUpdater = new CollectingNeighborUpdater(this, maxChainedNeighborUpdates);
+         this.registryAccess = registryAccess;
+         this.damageSources = new DamageSources(registryAccess);
+-    }
++
 +        // CraftBukkit start
 +        this.getWorldBorder().world = (ServerLevel) this;
 +        // From PlayerList.setPlayerFileData
@@ -222,8 +197,8 @@
 +        // CraftBukkit end
 +        this.entityLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.entityMaxTickTime);
 +        this.tileLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.tileMaxTickTime);
-     }
- 
++    }
++
 +    // Paper start - Cancel hit for vanished players
 +    // ret true if no collision
 +    public final boolean checkEntityCollision(BlockState data, Entity source, net.minecraft.world.phys.shapes.CollisionContext voxelshapedcollision,
@@ -263,10 +238,10 @@
 +        return true;
 +    }
 +    // Paper end - Cancel hit for vanished players
+ 
      @Override
      public boolean isClientSide() {
-         return this.isClientSide;
-@@ -163,6 +332,13 @@
+@@ -167,8 +_,15 @@
          return null;
      }
  
@@ -278,14 +253,17 @@
 +    // Paper end
 +
      public boolean isInWorldBounds(BlockPos pos) {
-         return !this.isOutsideBuildHeight(pos) && Level.isInWorldBoundsHorizontal(pos);
+-        return !this.isOutsideBuildHeight(pos) && isInWorldBoundsHorizontal(pos);
++        return pos.isInsideBuildHeightAndWorldBoundsHorizontal(this); // Paper - Perf: Optimize isInWorldBounds
      }
-@@ -172,25 +348,87 @@
+ 
+     public static boolean isInSpawnableBounds(BlockPos pos) {
+@@ -176,21 +_,84 @@
      }
  
      private static boolean isInWorldBoundsHorizontal(BlockPos pos) {
 -        return pos.getX() >= -30000000 && pos.getZ() >= -30000000 && pos.getX() < 30000000 && pos.getZ() < 30000000;
-+        return pos.getX() >= -30000000 && pos.getZ() >= -30000000 && pos.getX() < 30000000 && pos.getZ() < 30000000; // Diff on change warnUnsafeChunk()
++        return pos.getX() >= -30000000 && pos.getZ() >= -30000000 && pos.getX() < 30000000 && pos.getZ() < 30000000; // Diff on change warnUnsafeChunk() and isInsideBuildHeightAndWorldBoundsHorizontal
      }
  
      private static boolean isOutsideSpawnableHeight(int y) {
@@ -299,7 +277,8 @@
  
      @Override
 -    public LevelChunk getChunk(int chunkX, int chunkZ) {
--        return (LevelChunk) this.getChunk(chunkX, chunkZ, ChunkStatus.FULL);
+-        return (LevelChunk)this.getChunk(chunkX, chunkZ, ChunkStatus.FULL);
+-    }
 +    public final LevelChunk getChunk(int chunkX, int chunkZ) { // Paper - final to help inline
 +        // Paper start - Perf: make sure loaded chunks get the inlined variant of this function
 +        net.minecraft.server.level.ServerChunkCache cps = ((ServerLevel)this).getChunkSource();
@@ -309,11 +288,11 @@
 +        }
 +        return (LevelChunk) cps.getChunk(chunkX, chunkZ, ChunkStatus.FULL, true); // Paper - avoid a method jump
 +        // Paper end - Perf: make sure loaded chunks get the inlined variant of this function
-     }
- 
++    }
++
 +    // Paper start - if loaded
-     @Nullable
-     @Override
++    @Nullable
++    @Override
 +    public final ChunkAccess getChunkIfLoadedImmediately(int x, int z) {
 +        return ((ServerLevel)this).chunkSource.getChunkAtIfLoadedImmediately(x, z);
 +    }
@@ -365,17 +344,14 @@
 +    public final @Nullable BlockState getBlockStateIfLoadedAndInBounds(BlockPos blockposition) {
 +        return getWorldBorder().isWithinBounds(blockposition) ? getBlockStateIfLoaded(blockposition) : null;
 +    }
-+
-+    @Override
-     public ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) {
-+        // Paper end
-         ChunkAccess ichunkaccess = this.getChunkSource().getChunk(chunkX, chunkZ, leastStatus, create);
++    // Paper end
  
-         if (ichunkaccess == null && create) {
-@@ -207,6 +445,22 @@
+     @Nullable
+     @Override
+@@ -210,6 +_,22 @@
  
      @Override
-     public boolean setBlock(BlockPos pos, BlockState state, int flags, int maxUpdateDepth) {
+     public boolean setBlock(BlockPos pos, BlockState state, int flags, int recursionLeft) {
 +        // CraftBukkit start - tree generation
 +        if (this.captureTreeGeneration) {
 +            // Paper start - Protect Bedrock and End Portal/Frames from being destroyed
@@ -395,12 +371,11 @@
          if (this.isOutsideBuildHeight(pos)) {
              return false;
          } else if (!this.isClientSide && this.isDebug()) {
-@@ -214,44 +468,125 @@
+@@ -217,11 +_,28 @@
          } else {
-             LevelChunk chunk = this.getChunkAt(pos);
+             LevelChunk chunkAt = this.getChunkAt(pos);
              Block block = state.getBlock();
--            BlockState iblockdata1 = chunk.setBlockState(pos, state, (flags & 64) != 0);
- 
+-            BlockState blockState = chunkAt.setBlockState(pos, state, (flags & 64) != 0);
 +            // CraftBukkit start - capture blockstates
 +            boolean captured = false;
 +            if (this.captureBlockStates && !this.capturedBlockStates.containsKey(pos)) {
@@ -411,9 +386,9 @@
 +            }
 +            // CraftBukkit end
 +
-+            BlockState iblockdata1 = chunk.setBlockState(pos, state, (flags & 64) != 0, (flags & 1024) == 0); // CraftBukkit custom NO_PLACE flag
++            BlockState blockState = chunkAt.setBlockState(pos, state, (flags & 64) != 0, (flags & 1024) == 0); // CraftBukkit custom NO_PLACE flag
 +
-             if (iblockdata1 == null) {
+             if (blockState == null) {
 +                // CraftBukkit start - remove blockstate if failed (or the same)
 +                if (this.captureBlockStates && captured) {
 +                    this.capturedBlockStates.remove(pos);
@@ -421,69 +396,35 @@
 +                // CraftBukkit end
                  return false;
              } else {
-                 BlockState iblockdata2 = this.getBlockState(pos);
- 
--                if (iblockdata2 == state) {
+                 BlockState blockState1 = this.getBlockState(pos);
 +                /*
-+                if (iblockdata2 == iblockdata) {
-                     if (iblockdata1 != iblockdata2) {
--                        this.setBlocksDirty(pos, iblockdata1, iblockdata2);
-+                        this.setBlocksDirty(blockposition, iblockdata1, iblockdata2);
-                     }
+                 if (blockState1 == state) {
+                     if (blockState != blockState1) {
+                         this.setBlocksDirty(pos, blockState, blockState1);
+@@ -249,12 +_,76 @@
  
--                    if ((flags & 2) != 0 && (!this.isClientSide || (flags & 4) == 0) && (this.isClientSide || chunk.getFullStatus() != null && chunk.getFullStatus().isOrAfter(FullChunkStatus.BLOCK_TICKING))) {
--                        this.sendBlockUpdated(pos, iblockdata1, state, flags);
-+                    if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk.getFullStatus() != null && chunk.getFullStatus().isOrAfter(FullChunkStatus.BLOCK_TICKING))) {
-+                        this.sendBlockUpdated(blockposition, iblockdata1, iblockdata, i);
-                     }
- 
--                    if ((flags & 1) != 0) {
--                        this.blockUpdated(pos, iblockdata1.getBlock());
--                        if (!this.isClientSide && state.hasAnalogOutputSignal()) {
--                            this.updateNeighbourForOutputSignal(pos, block);
-+                    if ((i & 1) != 0) {
-+                        this.blockUpdated(blockposition, iblockdata1.getBlock());
-+                        if (!this.isClientSide && iblockdata.hasAnalogOutputSignal()) {
-+                            this.updateNeighbourForOutputSignal(blockposition, block);
-                         }
-                     }
- 
--                    if ((flags & 16) == 0 && maxUpdateDepth > 0) {
--                        int k = flags & -34;
-+                    if ((i & 16) == 0 && j > 0) {
-+                        int k = i & -34;
- 
--                        iblockdata1.updateIndirectNeighbourShapes(this, pos, k, maxUpdateDepth - 1);
--                        state.updateNeighbourShapes(this, pos, k, maxUpdateDepth - 1);
--                        state.updateIndirectNeighbourShapes(this, pos, k, maxUpdateDepth - 1);
-+                        iblockdata1.updateIndirectNeighbourShapes(this, blockposition, k, j - 1);
-+                        iblockdata.updateNeighbourShapes(this, blockposition, k, j - 1);
-+                        iblockdata.updateIndirectNeighbourShapes(this, blockposition, k, j - 1);
-                     }
- 
--                    this.onBlockStateChange(pos, iblockdata1, iblockdata2);
-+                    this.onBlockStateChange(blockposition, iblockdata1, iblockdata2);
+                     this.onBlockStateChange(pos, blockState, blockState1);
                  }
 +                */
- 
++
 +                // CraftBukkit start
 +                if (!this.captureBlockStates) { // Don't notify clients or update physics while capturing blockstates
 +                    // Modularize client and physic updates
 +                    // Spigot start
 +                    try {
-+                        this.notifyAndUpdatePhysics(pos, chunk, iblockdata1, state, iblockdata2, flags, maxUpdateDepth);
++                        this.notifyAndUpdatePhysics(pos, chunkAt, blockState, state, blockState1, flags, recursionLeft);
 +                    } catch (StackOverflowError ex) {
 +                        Level.lastPhysicsProblem = new BlockPos(pos);
 +                    }
 +                    // Spigot end
 +                }
 +                // CraftBukkit end
-+
+ 
                  return true;
-+            }
-+        }
-+    }
-+
+             }
+         }
+     }
+ 
 +    // CraftBukkit start - Split off from above in order to directly send client and physic updates
 +    public void notifyAndUpdatePhysics(BlockPos blockposition, LevelChunk chunk, BlockState oldBlock, BlockState newBlock, BlockState actualBlock, int i, int j) {
 +        BlockState iblockdata = newBlock;
@@ -520,60 +461,59 @@
 +                }
 +                // CraftBukkit end
 +                if (!cancelledUpdates) { // Paper - Fix block place logic
-+                iblockdata.updateNeighbourShapes(this, blockposition, k, j - 1);
-+                iblockdata.updateIndirectNeighbourShapes(this, blockposition, k, j - 1);
++                    iblockdata.updateNeighbourShapes(this, blockposition, k, j - 1);
++                    iblockdata.updateIndirectNeighbourShapes(this, blockposition, k, j - 1);
 +                } // Paper - Fix block place logic
-             }
++            }
 +
 +            // CraftBukkit start - SPIGOT-5710
 +            if (!this.preventPoiUpdated) {
 +                this.onBlockStateChange(blockposition, iblockdata1, iblockdata2);
 +            }
 +            // CraftBukkit end
-         }
-     }
++        }
++    }
 +    // CraftBukkit end
++
+     public void onBlockStateChange(BlockPos pos, BlockState blockState, BlockState newState) {
+     }
  
-     public void onBlockStateChange(BlockPos pos, BlockState oldBlock, BlockState newBlock) {}
- 
-@@ -270,15 +605,33 @@
+@@ -271,13 +_,31 @@
              return false;
          } else {
-             FluidState fluid = this.getFluidState(pos);
+             FluidState fluidState = this.getFluidState(pos);
+-            if (!(blockState.getBlock() instanceof BaseFireBlock)) {
+-                this.levelEvent(2001, pos, Block.getId(blockState));
 +            // Paper start - BlockDestroyEvent; while the above setAir method is named same and looks very similar
 +            // they are NOT used with same intent and the above should not fire this event. The above method is more of a BlockSetToAirEvent,
 +            // it doesn't imply destruction of a block that plays a sound effect / drops an item.
 +            boolean playEffect = true;
-+            BlockState effectType = iblockdata;
-+            int xp = iblockdata.getBlock().getExpDrop(iblockdata, (ServerLevel) this, pos, ItemStack.EMPTY, true);
++            BlockState effectType = blockState;
++            int xp = blockState.getBlock().getExpDrop(blockState, (ServerLevel) this, pos, ItemStack.EMPTY, true);
 +            if (com.destroystokyo.paper.event.block.BlockDestroyEvent.getHandlerList().getRegisteredListeners().length > 0) {
-+                com.destroystokyo.paper.event.block.BlockDestroyEvent event = new com.destroystokyo.paper.event.block.BlockDestroyEvent(org.bukkit.craftbukkit.block.CraftBlock.at(this, pos), fluid.createLegacyBlock().createCraftBlockData(), effectType.createCraftBlockData(), xp, drop);
++                com.destroystokyo.paper.event.block.BlockDestroyEvent event = new com.destroystokyo.paper.event.block.BlockDestroyEvent(org.bukkit.craftbukkit.block.CraftBlock.at(this, pos), fluidState.createLegacyBlock().createCraftBlockData(), effectType.createCraftBlockData(), xp, dropBlock);
 +                if (!event.callEvent()) {
 +                    return false;
 +                }
 +                effectType = ((CraftBlockData) event.getEffectBlock()).getState();
 +                playEffect = event.playEffect();
-+                drop = event.willDrop();
++                dropBlock = event.willDrop();
 +                xp = event.getExpToDrop();
 +            }
 +            // Paper end - BlockDestroyEvent
- 
--            if (!(iblockdata.getBlock() instanceof BaseFireBlock)) {
--                this.levelEvent(2001, pos, Block.getId(iblockdata));
-+            if (playEffect && !(effectType.getBlock() instanceof BaseFireBlock)) { // Paper - BlockDestroyEvent
++            if (playEffect && !(blockState.getBlock() instanceof BaseFireBlock)) { // Paper - BlockDestroyEvent
 +                this.levelEvent(2001, pos, Block.getId(effectType)); // Paper - BlockDestroyEvent
              }
  
-             if (drop) {
-                 BlockEntity tileentity = iblockdata.hasBlockEntity() ? this.getBlockEntity(pos) : null;
- 
--                Block.dropResources(iblockdata, this, pos, tileentity, breakingEntity, ItemStack.EMPTY);
-+                Block.dropResources(iblockdata, this, pos, tileentity, breakingEntity, ItemStack.EMPTY, false); // Paper - Properly handle xp dropping
-+                iblockdata.getBlock().popExperience((ServerLevel) this, pos, xp, breakingEntity); // Paper - Properly handle xp dropping; custom amount
+             if (dropBlock) {
+                 BlockEntity blockEntity = blockState.hasBlockEntity() ? this.getBlockEntity(pos) : null;
+-                Block.dropResources(blockState, this, pos, blockEntity, entity, ItemStack.EMPTY);
++                Block.dropResources(blockState, this, pos, blockEntity, entity, ItemStack.EMPTY, false); // Paper - Properly handle xp dropping
++                blockState.getBlock().popExperience((ServerLevel) this, pos, xp, entity); // Paper - Properly handle xp dropping; custom amount
              }
  
-             boolean flag1 = this.setBlock(pos, fluid.createLegacyBlock(), 3, maxUpdateDepth);
-@@ -340,10 +693,18 @@
+             boolean flag = this.setBlock(pos, fluidState.createLegacyBlock(), 3, recursionLeft);
+@@ -344,10 +_,18 @@
  
      @Override
      public BlockState getBlockState(BlockPos pos) {
@@ -590,121 +530,107 @@
          } else {
 -            LevelChunk chunk = this.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ()));
 +            ChunkAccess chunk = this.getChunk(pos.getX() >> 4, pos.getZ() >> 4, ChunkStatus.FULL, true); // Paper - manually inline to reduce hops and avoid unnecessary null check to reduce total byte code size, this should never return null and if it does we will see it the next line but the real stack trace will matter in the chunk engine
- 
              return chunk.getBlockState(pos);
          }
-@@ -446,34 +807,53 @@
+     }
+@@ -454,32 +_,49 @@
              this.pendingBlockEntityTickers.clear();
          }
  
 -        Iterator<TickingBlockEntity> iterator = this.blockEntityTickers.iterator();
 +        // Spigot start
-+        // Iterator<TickingBlockEntity> iterator = this.blockEntityTickers.iterator();
-         boolean flag = this.tickRateManager().runsNormally();
+         boolean runsNormally = this.tickRateManager().runsNormally();
  
 -        while (iterator.hasNext()) {
--            TickingBlockEntity tickingblockentity = (TickingBlockEntity) iterator.next();
-+        int tilesThisCycle = 0;
+-            TickingBlockEntity tickingBlockEntity = iterator.next();
 +        var toRemove = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<TickingBlockEntity>(); // Paper - Fix MC-117075; use removeAll
 +        toRemove.add(null); // Paper - Fix MC-117075
 +        for (tileTickPosition = 0; tileTickPosition < this.blockEntityTickers.size(); tileTickPosition++) { // Paper - Disable tick limiters
 +            this.tileTickPosition = (this.tileTickPosition < this.blockEntityTickers.size()) ? this.tileTickPosition : 0;
-+            TickingBlockEntity tickingblockentity = (TickingBlockEntity) this.blockEntityTickers.get(this.tileTickPosition);
++            TickingBlockEntity tickingBlockEntity = this.blockEntityTickers.get(this.tileTickPosition);
 +            // Spigot end
- 
-             if (tickingblockentity.isRemoved()) {
+             if (tickingBlockEntity.isRemoved()) {
 -                iterator.remove();
-+                // Spigot start
-+                tilesThisCycle--;
-+                toRemove.add(tickingblockentity); // Paper - Fix MC-117075; use removeAll
-+                // Spigot end
-             } else if (flag && this.shouldTickBlocksAt(tickingblockentity.getPos())) {
-                 tickingblockentity.tick();
++                toRemove.add(tickingBlockEntity); // Paper - Fix MC-117075; use removeAll
+             } else if (runsNormally && this.shouldTickBlocksAt(tickingBlockEntity.getPos())) {
+                 tickingBlockEntity.tick();
              }
          }
 +        this.blockEntityTickers.removeAll(toRemove); // Paper - Fix MC-117075
  
          this.tickingBlockEntities = false;
-         gameprofilerfiller.pop();
+         profilerFiller.pop();
 +        this.spigotConfig.currentPrimedTnt = 0; // Spigot
      }
  
-     public <T extends Entity> void guardEntityTick(Consumer<T> tickConsumer, T entity) {
+     public <T extends Entity> void guardEntityTick(Consumer<T> consumerEntity, T entity) {
          try {
-             tickConsumer.accept(entity);
-         } catch (Throwable throwable) {
--            CrashReport crashreport = CrashReport.forThrowable(throwable, "Ticking entity");
--            CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Entity being ticked");
--
--            entity.fillCrashReportCategory(crashreportsystemdetails);
--            throw new ReportedException(crashreport);
+             consumerEntity.accept(entity);
+         } catch (Throwable var6) {
+-            CrashReport crashReport = CrashReport.forThrowable(var6, "Ticking entity");
+-            CrashReportCategory crashReportCategory = crashReport.addCategory("Entity being ticked");
+-            entity.fillCrashReportCategory(crashReportCategory);
+-            throw new ReportedException(crashReport);
 +            // Paper start - Prevent block entity and entity crashes
 +            final String msg = String.format("Entity threw exception at %s:%s,%s,%s", entity.level().getWorld().getName(), entity.getX(), entity.getY(), entity.getZ());
-+            MinecraftServer.LOGGER.error(msg, throwable);
-+            getCraftServer().getPluginManager().callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerInternalException(msg, throwable))); // Paper - ServerExceptionEvent
++            MinecraftServer.LOGGER.error(msg, var6);
++            getCraftServer().getPluginManager().callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerInternalException(msg, var6))); // Paper - ServerExceptionEvent
 +            entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD);
 +            // Paper end - Prevent block entity and entity crashes
          }
-+    }
+     }
++
 +    // Paper start - Option to prevent armor stands from doing entity lookups
 +    @Override
 +    public boolean noCollision(@Nullable Entity entity, AABB box) {
-+        if (entity instanceof net.minecraft.world.entity.decoration.ArmorStand && !entity.level().paperConfig().entities.armorStands.doCollisionEntityLookups) return false;
++        if (entity instanceof net.minecraft.world.entity.decoration.ArmorStand && !entity.level().paperConfig().entities.armorStands.doCollisionEntityLookups)
++            return false;
 +        return LevelAccessor.super.noCollision(entity, box);
-     }
++    }
 +    // Paper end - Option to prevent armor stands from doing entity lookups
  
      public boolean shouldTickDeath(Entity entity) {
          return true;
-@@ -510,13 +890,32 @@
+@@ -599,6 +_,19 @@
      @Nullable
      @Override
      public BlockEntity getBlockEntity(BlockPos pos) {
--        return this.isOutsideBuildHeight(pos) ? null : (!this.isClientSide && Thread.currentThread() != this.thread ? null : this.getChunkAt(pos).getBlockEntity(pos, LevelChunk.EntityCreationType.IMMEDIATE));
 +        // CraftBukkit start
 +        return this.getBlockEntity(pos, true);
-     }
- 
++    }
++
 +    @Nullable
-+    public BlockEntity getBlockEntity(BlockPos blockposition, boolean validate) {
++    public BlockEntity getBlockEntity(BlockPos pos, boolean validate) {
 +        // Paper start - Perf: Optimize capturedTileEntities lookup
 +        net.minecraft.world.level.block.entity.BlockEntity blockEntity;
-+        if (!this.capturedTileEntities.isEmpty() && (blockEntity = this.capturedTileEntities.get(blockposition)) != null) {
++        if (!this.capturedTileEntities.isEmpty() && (blockEntity = this.capturedTileEntities.get(pos)) != null) {
 +            return blockEntity;
 +        }
 +        // Paper end - Perf: Optimize capturedTileEntities lookup
 +        // CraftBukkit end
-+        return this.isOutsideBuildHeight(blockposition) ? null : (!this.isClientSide && Thread.currentThread() != this.thread ? null : this.getChunkAt(blockposition).getBlockEntity(blockposition, LevelChunk.EntityCreationType.IMMEDIATE));
-+    }
-+
+         if (this.isOutsideBuildHeight(pos)) {
+             return null;
+         } else {
+@@ -611,6 +_,12 @@
      public void setBlockEntity(BlockEntity blockEntity) {
-         BlockPos blockposition = blockEntity.getBlockPos();
- 
-         if (!this.isOutsideBuildHeight(blockposition)) {
+         BlockPos blockPos = blockEntity.getBlockPos();
+         if (!this.isOutsideBuildHeight(blockPos)) {
 +            // CraftBukkit start
 +            if (this.captureBlockStates) {
-+                this.capturedTileEntities.put(blockposition.immutable(), blockEntity);
++                this.capturedTileEntities.put(blockPos.immutable(), blockEntity);
 +                return;
 +            }
 +            // CraftBukkit end
-             this.getChunkAt(blockposition).addAndRegisterBlockEntity(blockEntity);
+             this.getChunkAt(blockPos).addAndRegisterBlockEntity(blockEntity);
          }
      }
-@@ -643,7 +1042,7 @@
- 
-                 for (int k = 0; k < j; ++k) {
-                     EnderDragonPart entitycomplexpart = aentitycomplexpart[k];
--                    T t0 = (Entity) filter.tryCast(entitycomplexpart);
-+                    T t0 = filter.tryCast(entitycomplexpart); // CraftBukkit - decompile error
- 
-                     if (t0 != null && predicate.test(t0)) {
-                         result.add(t0);
-@@ -912,7 +1311,7 @@
- 
-     public static enum ExplosionInteraction implements StringRepresentable {
- 
--        NONE("none"), BLOCK("block"), MOB("mob"), TNT("tnt"), TRIGGER("trigger");
-+        NONE("none"), BLOCK("block"), MOB("mob"), TNT("tnt"), TRIGGER("trigger"), STANDARD("standard"); // CraftBukkit - Add STANDARD which will always use Explosion.Effect.DESTROY
+@@ -987,7 +_,8 @@
+         BLOCK("block"),
+         MOB("mob"),
+         TNT("tnt"),
+-        TRIGGER("trigger");
++        TRIGGER("trigger"),
++        STANDARD("standard"); // CraftBukkit - Add STANDARD which will always use Explosion.Effect.DESTROY
  
          public static final Codec<Level.ExplosionInteraction> CODEC = StringRepresentable.fromEnum(Level.ExplosionInteraction::values);
          private final String id;
diff --git a/paper-server/patches/sources/net/minecraft/world/level/LevelAccessor.java.patch b/paper-server/patches/sources/net/minecraft/world/level/LevelAccessor.java.patch
new file mode 100644
index 0000000000..d80e4a75be
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/LevelAccessor.java.patch
@@ -0,0 +1,9 @@
+--- a/net/minecraft/world/level/LevelAccessor.java
++++ b/net/minecraft/world/level/LevelAccessor.java
+@@ -101,4 +_,6 @@
+     default void gameEvent(ResourceKey<GameEvent> gameEvent, BlockPos pos, GameEvent.Context context) {
+         this.gameEvent(this.registryAccess().lookupOrThrow(Registries.GAME_EVENT).getOrThrow(gameEvent), pos, context);
+     }
++
++    net.minecraft.server.level.ServerLevel getMinecraftWorld(); // CraftBukkit
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/LevelReader.java.patch b/paper-server/patches/sources/net/minecraft/world/level/LevelReader.java.patch
similarity index 81%
rename from paper-server/patches/unapplied/net/minecraft/world/level/LevelReader.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/LevelReader.java.patch
index 4d6ea6f76d..c740a23a7f 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/LevelReader.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/LevelReader.java.patch
@@ -1,8 +1,8 @@
 --- a/net/minecraft/world/level/LevelReader.java
 +++ b/net/minecraft/world/level/LevelReader.java
-@@ -26,6 +26,9 @@
+@@ -26,6 +_,9 @@
      @Nullable
-     ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create);
+     ChunkAccess getChunk(int x, int z, ChunkStatus chunkStatus, boolean requireChunk);
  
 +    @Nullable ChunkAccess getChunkIfLoadedImmediately(int x, int z); // Paper - ifLoaded api (we need this since current impl blocks if the chunk is loading)
 +    @Nullable default ChunkAccess getChunkIfLoadedImmediately(BlockPos pos) { return this.getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4);}
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/LevelWriter.java.patch b/paper-server/patches/sources/net/minecraft/world/level/LevelWriter.java.patch
similarity index 95%
rename from paper-server/patches/unapplied/net/minecraft/world/level/LevelWriter.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/LevelWriter.java.patch
index 3008822ea1..a092d0dad8 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/LevelWriter.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/LevelWriter.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/level/LevelWriter.java
 +++ b/net/minecraft/world/level/LevelWriter.java
-@@ -28,4 +28,10 @@
+@@ -27,4 +_,10 @@
      default boolean addFreshEntity(Entity entity) {
          return false;
      }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/NaturalSpawner.java.patch b/paper-server/patches/sources/net/minecraft/world/level/NaturalSpawner.java.patch
new file mode 100644
index 0000000000..89849d7d70
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/NaturalSpawner.java.patch
@@ -0,0 +1,225 @@
+--- a/net/minecraft/world/level/NaturalSpawner.java
++++ b/net/minecraft/world/level/NaturalSpawner.java
+@@ -49,6 +_,13 @@
+ import net.minecraft.world.phys.Vec3;
+ import org.slf4j.Logger;
+ 
++// CraftBukkit start
++import net.minecraft.world.level.storage.LevelData;
++import org.bukkit.craftbukkit.util.CraftSpawnCategory;
++import org.bukkit.entity.SpawnCategory;
++import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
++// CraftBukkit end
++
+ public final class NaturalSpawner {
+     private static final Logger LOGGER = LogUtils.getLogger();
+     private static final int MIN_SPAWN_DISTANCE = 24;
+@@ -72,6 +_,13 @@
+             if (!(entity instanceof Mob mob && (mob.isPersistenceRequired() || mob.requiresCustomPersistence()))) {
+                 MobCategory category = entity.getType().getCategory();
+                 if (category != MobCategory.MISC) {
++                    // Paper start - Only count natural spawns
++                    if (!entity.level().paperConfig().entities.spawning.countAllMobsForSpawning &&
++                        !(entity.spawnReason == org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.NATURAL ||
++                            entity.spawnReason == org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.CHUNK_GEN)) {
++                        continue;
++                    }
++                    // Paper end - Only count natural spawns
+                     BlockPos blockPos = entity.blockPosition();
+                     chunkGetter.query(ChunkPos.asLong(blockPos), chunk -> {
+                         MobSpawnSettings.MobSpawnCost mobSpawnCost = getRoughBiome(blockPos, chunk).getMobSettings().getMobSpawnCost(entity.getType());
+@@ -96,17 +_,34 @@
+         return chunk.getNoiseBiome(QuartPos.fromBlock(pos.getX()), QuartPos.fromBlock(pos.getY()), QuartPos.fromBlock(pos.getZ())).value();
+     }
+ 
++    // CraftBukkit start - add server
+     public static List<MobCategory> getFilteredSpawningCategories(
+-        NaturalSpawner.SpawnState spawnState, boolean spawnFriendlies, boolean spawnEnemies, boolean spawnPassives
++        NaturalSpawner.SpawnState spawnState, boolean spawnFriendlies, boolean spawnEnemies, boolean spawnPassives, ServerLevel level
+     ) {
++        LevelData worlddata = level.getLevelData(); // CraftBukkit - Other mob type spawn tick rate
++        // CraftBukkit end
+         List<MobCategory> list = new ArrayList<>(SPAWNING_CATEGORIES.length);
+ 
+         for (MobCategory mobCategory : SPAWNING_CATEGORIES) {
++            // CraftBukkit start - Use per-world spawn limits
++            boolean spawnThisTick = true;
++            int limit = mobCategory.getMaxInstancesPerChunk();
++            SpawnCategory spawnCategory = CraftSpawnCategory.toBukkit(mobCategory);
++            if (CraftSpawnCategory.isValidForLimits(spawnCategory)) {
++                spawnThisTick = level.ticksPerSpawnCategory.getLong(spawnCategory) != 0 && worlddata.getGameTime() % level.ticksPerSpawnCategory.getLong(spawnCategory) == 0;
++                limit = level.getWorld().getSpawnLimit(spawnCategory);
++            }
++
++            if (!spawnThisTick || limit == 0) {
++                continue;
++            }
++
+             if ((spawnFriendlies || !mobCategory.isFriendly())
+                 && (spawnEnemies || mobCategory.isFriendly())
+                 && (spawnPassives || !mobCategory.isPersistent())
+-                && spawnState.canSpawnForCategoryGlobal(mobCategory)) {
++                && spawnState.canSpawnForCategoryGlobal(mobCategory, limit)) { // Paper - Optional per player mob spawns; remove global check, check later during the local one
+                 list.add(mobCategory);
++                // CraftBukkit end
+             }
+         }
+ 
+@@ -126,6 +_,16 @@
+         profilerFiller.pop();
+     }
+ 
++    // Paper start - Add mobcaps commands
++    public static int globalLimitForCategory(final ServerLevel level, final MobCategory category, final int spawnableChunkCount) {
++        final int categoryLimit = level.getWorld().getSpawnLimitUnsafe(CraftSpawnCategory.toBukkit(category));
++        if (categoryLimit < 1) {
++            return categoryLimit;
++        }
++        return categoryLimit * spawnableChunkCount / NaturalSpawner.MAGIC_NUMBER;
++    }
++    // Paper end - Add mobcaps commands
++
+     public static void spawnCategoryForChunk(
+         MobCategory category, ServerLevel level, LevelChunk chunk, NaturalSpawner.SpawnPredicate filter, NaturalSpawner.AfterSpawnCallback callback
+     ) {
+@@ -151,8 +_,8 @@
+         StructureManager structureManager = level.structureManager();
+         ChunkGenerator generator = level.getChunkSource().getGenerator();
+         int y = pos.getY();
+-        BlockState blockState = chunk.getBlockState(pos);
+-        if (!blockState.isRedstoneConductor(chunk, pos)) {
++        BlockState blockState = level.getBlockStateIfLoadedAndInBounds(pos); // Paper - don't load chunks for mob spawn
++        if (blockState != null && !blockState.isRedstoneConductor(chunk, pos)) { // Paper - don't load chunks for mob spawn
+             BlockPos.MutableBlockPos mutableBlockPos = new BlockPos.MutableBlockPos();
+             int i = 0;
+ 
+@@ -174,7 +_,7 @@
+                     Player nearestPlayer = level.getNearestPlayer(d, y, d1, -1.0, false);
+                     if (nearestPlayer != null) {
+                         double d2 = nearestPlayer.distanceToSqr(d, y, d1);
+-                        if (isRightDistanceToPlayerAndSpawnPoint(level, chunk, mutableBlockPos, d2)) {
++                        if (level.isLoadedAndInBounds(mutableBlockPos) && isRightDistanceToPlayerAndSpawnPoint(level, chunk, mutableBlockPos, d2)) { // Paper - don't load chunks for mob spawn
+                             if (spawnerData == null) {
+                                 Optional<MobSpawnSettings.SpawnerData> randomSpawnMobAt = getRandomSpawnMobAt(
+                                     level, structureManager, generator, category, level.random, mutableBlockPos
+@@ -187,8 +_,13 @@
+                                 ceil = spawnerData.minCount + level.random.nextInt(1 + spawnerData.maxCount - spawnerData.minCount);
+                             }
+ 
+-                            if (isValidSpawnPostitionForType(level, category, structureManager, generator, spawnerData, mutableBlockPos, d2)
+-                                && filter.test(spawnerData.type, mutableBlockPos, chunk)) {
++                            // Paper start - PreCreatureSpawnEvent
++                            PreSpawnStatus doSpawning = isValidSpawnPostitionForType(level, category, structureManager, generator, spawnerData, mutableBlockPos, d2);
++                            if (doSpawning == PreSpawnStatus.ABORT) {
++                                return;
++                            }
++                            if (doSpawning == PreSpawnStatus.SUCCESS && filter.test(spawnerData.type, mutableBlockPos, chunk)) {
++                                // Paper end - PreCreatureSpawnEvent
+                                 Mob mobForSpawn = getMobForSpawn(level, spawnerData.type);
+                                 if (mobForSpawn == null) {
+                                     return;
+@@ -199,10 +_,15 @@
+                                     spawnGroupData = mobForSpawn.finalizeSpawn(
+                                         level, level.getCurrentDifficultyAt(mobForSpawn.blockPosition()), EntitySpawnReason.NATURAL, spawnGroupData
+                                     );
+-                                    i++;
+-                                    i3++;
+-                                    level.addFreshEntityWithPassengers(mobForSpawn);
+-                                    callback.run(mobForSpawn, chunk);
++                                    // CraftBukkit start
++                                    // SPIGOT-7045: Give ocelot babies back their special spawn reason. Note: This is the only modification required as ocelots count as monsters which means they only spawn during normal chunk ticking and do not spawn during chunk generation as starter mobs.
++                                    level.addFreshEntityWithPassengers(mobForSpawn, (mobForSpawn instanceof net.minecraft.world.entity.animal.Ocelot && !((org.bukkit.entity.Ageable) mobForSpawn.getBukkitEntity()).isAdult()) ? SpawnReason.OCELOT_BABY : SpawnReason.NATURAL);
++                                    if (!mobForSpawn.isRemoved()) {
++                                        ++i;
++                                        ++i3;
++                                        callback.run(mobForSpawn, chunk);
++                                    }
++                                    // CraftBukkit end
+                                     if (i >= mobForSpawn.getMaxSpawnClusterSize()) {
+                                         return;
+                                     }
+@@ -225,7 +_,15 @@
+             && (Objects.equals(new ChunkPos(pos), chunk.getPos()) || level.isNaturalSpawningAllowed(pos));
+     }
+ 
+-    private static boolean isValidSpawnPostitionForType(
++    // Paper start - PreCreatureSpawnEvent
++    private enum PreSpawnStatus {
++        FAIL,
++        SUCCESS,
++        CANCELLED,
++        ABORT
++    }
++    private static PreSpawnStatus isValidSpawnPostitionForType(
++    // Paper end - PreCreatureSpawnEvent
+         ServerLevel level,
+         MobCategory category,
+         StructureManager structureManager,
+@@ -235,7 +_,20 @@
+         double distance
+     ) {
+         EntityType<?> entityType = data.type;
+-        return entityType.getCategory() != MobCategory.MISC
++
++        // Paper start - PreCreatureSpawnEvent
++        com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent event = new com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent(
++            io.papermc.paper.util.MCUtil.toLocation(level, pos),
++            org.bukkit.craftbukkit.entity.CraftEntityType.minecraftToBukkit(entityType), SpawnReason.NATURAL
++        );
++        if (!event.callEvent()) {
++            if (event.shouldAbortSpawn()) {
++                return PreSpawnStatus.ABORT;
++            }
++            return PreSpawnStatus.CANCELLED;
++        }
++        final boolean success = entityType.getCategory() != MobCategory.MISC
++            // Paper end - PreCreatureSpawnEvent
+             && (
+                 entityType.canSpawnFarFromPlayer()
+                     || !(distance > entityType.getCategory().getDespawnDistance() * entityType.getCategory().getDespawnDistance())
+@@ -245,6 +_,7 @@
+             && SpawnPlacements.isSpawnPositionOk(entityType, level, pos)
+             && SpawnPlacements.checkSpawnRules(entityType, level, EntitySpawnReason.NATURAL, pos, level.random)
+             && level.noCollision(entityType.getSpawnAABB(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5));
++        return success ? PreSpawnStatus.SUCCESS : PreSpawnStatus.FAIL; // Paper - PreCreatureSpawnEvent
+     }
+ 
+     @Nullable
+@@ -258,6 +_,7 @@
+             LOGGER.warn("Can't spawn entity of type: {}", BuiltInRegistries.ENTITY_TYPE.getKey(entityType));
+         } catch (Exception var4) {
+             LOGGER.warn("Failed to create mob", (Throwable)var4);
++            com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(var4); // Paper - ServerExceptionEvent
+         }
+ 
+         return null;
+@@ -364,6 +_,7 @@
+                                     entity = spawnerData.type.create(levelAccessor.getLevel(), EntitySpawnReason.NATURAL);
+                                 } catch (Exception var27) {
+                                     LOGGER.warn("Failed to create mob", (Throwable)var27);
++                                    com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(var27); // Paper - ServerExceptionEvent
+                                     continue;
+                                 }
+ 
+@@ -381,7 +_,7 @@
+                                         EntitySpawnReason.CHUNK_GENERATION,
+                                         spawnGroupData
+                                     );
+-                                    levelAccessor.addFreshEntityWithPassengers(mob);
++                                    levelAccessor.addFreshEntityWithPassengers(mob, SpawnReason.CHUNK_GEN); // CraftBukkit
+                                     flag = true;
+                                 }
+                             }
+@@ -501,8 +_,10 @@
+             return this.unmodifiableMobCategoryCounts;
+         }
+ 
+-        boolean canSpawnForCategoryGlobal(MobCategory category) {
+-            int i = category.getMaxInstancesPerChunk() * this.spawnableChunkCount / NaturalSpawner.MAGIC_NUMBER;
++        // CraftBukkit start
++        boolean canSpawnForCategoryGlobal(MobCategory category, int limit) {
++            int i = limit * this.spawnableChunkCount / NaturalSpawner.MAGIC_NUMBER;
++            // CraftBukkit end
+             return this.mobCategoryCounts.getInt(category) < i;
+         }
+ 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/PathNavigationRegion.java.patch b/paper-server/patches/sources/net/minecraft/world/level/PathNavigationRegion.java.patch
similarity index 71%
rename from paper-server/patches/unapplied/net/minecraft/world/level/PathNavigationRegion.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/PathNavigationRegion.java.patch
index 085fb1983b..2305a1f778 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/PathNavigationRegion.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/PathNavigationRegion.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/level/PathNavigationRegion.java
 +++ b/net/minecraft/world/level/PathNavigationRegion.java
-@@ -8,6 +8,7 @@
+@@ -8,6 +_,7 @@
  import net.minecraft.core.Holder;
  import net.minecraft.core.SectionPos;
  import net.minecraft.core.registries.Registries;
@@ -8,19 +8,19 @@
  import net.minecraft.world.entity.Entity;
  import net.minecraft.world.level.biome.Biome;
  import net.minecraft.world.level.biome.Biomes;
-@@ -66,7 +67,7 @@
-     private ChunkAccess getChunk(int chunkX, int chunkZ) {
-         int i = chunkX - this.centerX;
-         int j = chunkZ - this.centerZ;
--        if (i >= 0 && i < this.chunks.length && j >= 0 && j < this.chunks[i].length) {
-+        if (i >= 0 && i < this.chunks.length && j >= 0 && j < this.chunks[i].length) { // Paper - if this changes, update getChunkIfLoaded below
-             ChunkAccess chunkAccess = this.chunks[i][j];
-             return (ChunkAccess)(chunkAccess != null ? chunkAccess : new EmptyLevelChunk(this.level, new ChunkPos(chunkX, chunkZ), this.plains.get()));
+@@ -66,13 +_,37 @@
+     private ChunkAccess getChunk(int x, int z) {
+         int i = x - this.centerX;
+         int i1 = z - this.centerZ;
+-        if (i >= 0 && i < this.chunks.length && i1 >= 0 && i1 < this.chunks[i].length) {
++        if (i >= 0 && i < this.chunks.length && i1 >= 0 && i1 < this.chunks[i].length) { // Paper - if this changes, update getChunkIfLoaded below
+             ChunkAccess chunkAccess = this.chunks[i][i1];
+             return (ChunkAccess)(chunkAccess != null ? chunkAccess : new EmptyLevelChunk(this.level, new ChunkPos(x, z), this.plains.get()));
          } else {
-@@ -74,7 +75,31 @@
+             return new EmptyLevelChunk(this.level, new ChunkPos(x, z), this.plains.get());
          }
      }
- 
++
 +    // Paper start - if loaded util
 +    private @Nullable ChunkAccess getChunkIfLoaded(int x, int z) {
 +        // Based on getChunk(int, int)
@@ -32,7 +32,7 @@
 +        }
 +        return null;
 +    }
-     @Override
++    @Override
 +    public final FluidState getFluidIfLoaded(BlockPos blockposition) {
 +        ChunkAccess chunk = getChunkIfLoaded(blockposition.getX() >> 4, blockposition.getZ() >> 4);
 +        return chunk == null ? null : chunk.getFluidState(blockposition);
@@ -44,8 +44,6 @@
 +        return chunk == null ? null : chunk.getBlockState(blockposition);
 +    }
 +    // Paper end
-+
-+    @Override
+ 
+     @Override
      public WorldBorder getWorldBorder() {
-         return this.level.getWorldBorder();
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/ServerExplosion.java.patch b/paper-server/patches/sources/net/minecraft/world/level/ServerExplosion.java.patch
similarity index 56%
rename from paper-server/patches/unapplied/net/minecraft/world/level/ServerExplosion.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/ServerExplosion.java.patch
index e0f12942e5..263bafc98c 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/ServerExplosion.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/ServerExplosion.java.patch
@@ -1,110 +1,103 @@
 --- a/net/minecraft/world/level/ServerExplosion.java
 +++ b/net/minecraft/world/level/ServerExplosion.java
-@@ -22,18 +22,27 @@
- import net.minecraft.world.entity.EntityType;
- import net.minecraft.world.entity.LivingEntity;
- import net.minecraft.world.entity.ai.attributes.Attributes;
-+import net.minecraft.world.entity.boss.EnderDragonPart;
-+import net.minecraft.world.entity.boss.enderdragon.EnderDragon;
- import net.minecraft.world.entity.item.ItemEntity;
- import net.minecraft.world.entity.item.PrimedTnt;
- import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.level.block.BaseFireBlock;
- import net.minecraft.world.level.block.Block;
--import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.level.material.FluidState;
- import net.minecraft.world.phys.AABB;
+@@ -33,6 +_,17 @@
  import net.minecraft.world.phys.HitResult;
  import net.minecraft.world.phys.Vec3;
+ 
++// CraftBukkit start
++import net.minecraft.world.entity.boss.EnderDragonPart;
++import net.minecraft.world.entity.boss.enderdragon.EnderDragon;
 +import net.minecraft.world.level.block.Blocks;
-+import net.minecraft.world.level.block.state.BlockState;
 +import org.bukkit.craftbukkit.event.CraftEventFactory;
 +import org.bukkit.craftbukkit.util.CraftLocation;
 +import org.bukkit.event.entity.EntityExplodeEvent;
 +import org.bukkit.Location;
 +import org.bukkit.event.block.BlockExplodeEvent;
 +// CraftBukkit end
- 
++
  public class ServerExplosion implements Explosion {
- 
-@@ -50,16 +59,22 @@
+     private static final ExplosionDamageCalculator EXPLOSION_DAMAGE_CALCULATOR = new ExplosionDamageCalculator();
+     private static final int MAX_DROPS_PER_COMBINED_STACK = 16;
+@@ -47,6 +_,11 @@
      private final DamageSource damageSource;
      private final ExplosionDamageCalculator damageCalculator;
-     private final Map<Player, Vec3> hitPlayers = new HashMap();
+     private final Map<Player, Vec3> hitPlayers = new HashMap<>();
 +    // CraftBukkit - add field
 +    public boolean wasCanceled = false;
 +    public float yield;
 +    // CraftBukkit end
 +    public boolean excludeSourceFromDamage = true; // Paper - Allow explosions to damage source
  
-     public ServerExplosion(ServerLevel world, @Nullable Entity entity, @Nullable DamageSource damageSource, @Nullable ExplosionDamageCalculator behavior, Vec3 pos, float power, boolean createFire, Explosion.BlockInteraction destructionType) {
-         this.level = world;
-         this.source = entity;
--        this.radius = power;
-+        this.radius = (float) Math.max(power, 0.0); // CraftBukkit - clamp bad values
-         this.center = pos;
-         this.fire = createFire;
-         this.blockInteraction = destructionType;
-         this.damageSource = damageSource == null ? world.damageSources().explosion(this) : damageSource;
-         this.damageCalculator = behavior == null ? this.makeDamageCalculator(entity) : behavior;
+     public ServerExplosion(
+         ServerLevel level,
+@@ -60,12 +_,13 @@
+     ) {
+         this.level = level;
+         this.source = source;
+-        this.radius = radius;
++        this.radius = (float) Math.max(radius, 0.0); // CraftBukkit - clamp bad values
+         this.center = center;
+         this.fire = fire;
+         this.blockInteraction = blockInteraction;
+         this.damageSource = damageSource == null ? level.damageSources().explosion(this) : damageSource;
+         this.damageCalculator = damageCalculator == null ? this.makeDamageCalculator(source) : damageCalculator;
 +        this.yield = this.blockInteraction == Explosion.BlockInteraction.DESTROY_WITH_DECAY ? 1.0F / this.radius : 1.0F; // CraftBukkit
      }
  
      private ExplosionDamageCalculator makeDamageCalculator(@Nullable Entity entity) {
-@@ -135,7 +150,8 @@
+@@ -139,7 +_,8 @@
                          for (float f1 = 0.3F; f > 0.0F; f -= 0.22500001F) {
-                             BlockPos blockposition = BlockPos.containing(d4, d5, d6);
-                             BlockState iblockdata = this.level.getBlockState(blockposition);
--                            FluidState fluid = this.level.getFluidState(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
- 
-                             if (!this.level.isInWorldBounds(blockposition)) {
+                             BlockPos blockPos = BlockPos.containing(d3, d4, d5);
+                             BlockState blockState = this.level.getBlockState(blockPos);
+-                            FluidState fluidState = this.level.getFluidState(blockPos);
++                            if (!blockState.isDestroyable()) continue; // Paper - Protect Bedrock and End Portal/Frames from being destroyed
++                            FluidState fluidState = blockState.getFluidState(); // Paper - Perf: Optimize call to getFluid for explosions
+                             if (!this.level.isInWorldBounds(blockPos)) {
                                  break;
-@@ -149,6 +165,15 @@
+                             }
+@@ -152,6 +_,15 @@
  
-                             if (f > 0.0F && this.damageCalculator.shouldBlockExplode(this, this.level, blockposition, iblockdata, f)) {
-                                 set.add(blockposition);
+                             if (f > 0.0F && this.damageCalculator.shouldBlockExplode(this, this.level, blockPos, blockState, f)) {
+                                 set.add(blockPos);
 +                                // Paper start - prevent headless pistons from forming
-+                                if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowHeadlessPistons && iblockdata.getBlock() == Blocks.MOVING_PISTON) {
-+                                    net.minecraft.world.level.block.entity.BlockEntity extension = this.level.getBlockEntity(blockposition);
++                                if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowHeadlessPistons && blockState.is(Blocks.MOVING_PISTON)) {
++                                    net.minecraft.world.level.block.entity.BlockEntity extension = this.level.getBlockEntity(blockPos);
 +                                    if (extension instanceof net.minecraft.world.level.block.piston.PistonMovingBlockEntity blockEntity && blockEntity.isSourcePiston()) {
-+                                        net.minecraft.core.Direction direction = iblockdata.getValue(net.minecraft.world.level.block.piston.PistonHeadBlock.FACING);
-+                                        set.add(blockposition.relative(direction.getOpposite()));
++                                        net.minecraft.core.Direction direction = blockState.getValue(net.minecraft.world.level.block.piston.PistonHeadBlock.FACING);
++                                        set.add(blockPos.relative(direction.getOpposite()));
 +                                    }
 +                                }
 +                                // Paper end - prevent headless pistons from forming
                              }
  
-                             d4 += d0 * 0.30000001192092896D;
-@@ -171,7 +196,7 @@
-         int l = Mth.floor(this.center.y + (double) f + 1.0D);
-         int i1 = Mth.floor(this.center.z - (double) f - 1.0D);
-         int j1 = Mth.floor(this.center.z + (double) f + 1.0D);
--        List<Entity> list = this.level.getEntities(this.source, new AABB((double) i, (double) k, (double) i1, (double) j, (double) l, (double) j1));
-+        List<Entity> list = this.level.getEntities(excludeSourceFromDamage ? this.source : null, new AABB((double) i, (double) k, (double) i1, (double) j, (double) l, (double) j1), (com.google.common.base.Predicate<Entity>) entity -> entity.isAlive() && !entity.isSpectator()); // Paper - Fix lag from explosions processing dead entities, Allow explosions to damage source
-         Iterator iterator = list.iterator();
- 
-         while (iterator.hasNext()) {
-@@ -192,10 +217,38 @@
-                         d3 /= d4;
-                         boolean flag = this.damageCalculator.shouldDamageEntity(this, entity);
-                         float f1 = this.damageCalculator.getKnockbackMultiplier(entity);
--                        float f2 = !flag && f1 == 0.0F ? 0.0F : ServerExplosion.getSeenPercent(this.center, entity);
-+                        float f2 = !flag && f1 == 0.0F ? 0.0F : this.getBlockDensity(this.center, entity); // Paper - Optimize explosions
- 
-                         if (flag) {
--                            entity.hurtServer(this.level, this.damageSource, this.damageCalculator.getEntityDamageAmount(this, entity, f2));
+                             d3 += d * 0.3F;
+@@ -174,8 +_,8 @@
+         int floor3 = Mth.floor(this.center.y + f + 1.0);
+         int floor4 = Mth.floor(this.center.z - f - 1.0);
+         int floor5 = Mth.floor(this.center.z + f + 1.0);
+-
+-        for (Entity entity : this.level.getEntities(this.source, new AABB(floor, floor2, floor4, floor1, floor3, floor5))) {
++        List <Entity> list = this.level.getEntities(excludeSourceFromDamage ? this.source : null, new AABB(floor, floor2, floor4, floor1, floor3, floor5), entity -> entity.isAlive() && !entity.isSpectator()); // Paper - Fix lag from explosions processing dead entities, Allow explosions to damage source
++        for (Entity entity : list) { // Paper - used in loop
+             if (!entity.ignoreExplosion(this)) {
+                 double d = Math.sqrt(entity.distanceToSqr(this.center)) / f;
+                 if (d <= 1.0) {
+@@ -189,15 +_,43 @@
+                         d3 /= squareRoot;
+                         boolean shouldDamageEntity = this.damageCalculator.shouldDamageEntity(this, entity);
+                         float knockbackMultiplier = this.damageCalculator.getKnockbackMultiplier(entity);
+-                        float f1 = !shouldDamageEntity && knockbackMultiplier == 0.0F ? 0.0F : getSeenPercent(this.center, entity);
++                        float f1 = !shouldDamageEntity && knockbackMultiplier == 0.0F ? 0.0F : this.getBlockDensity(this.center, entity); // Paper - Optimize explosions
+                         if (shouldDamageEntity) {
+-                            entity.hurtServer(this.level, this.damageSource, this.damageCalculator.getEntityDamageAmount(this, entity, f1));
 +                            // CraftBukkit start
 +
 +                            // Special case ender dragon only give knockback if no damage is cancelled
 +                            // Thinks to note:
-+                            // - Setting a velocity to a ComplexEntityPart is ignored (and therefore not needed)
-+                            // - Damaging ComplexEntityPart while forward the damage to EntityEnderDragon
++                            // - Setting a velocity to a EnderDragonPart is ignored (and therefore not needed)
++                            // - Damaging EnderDragonPart while forward the damage to EnderDragon
 +                            // - Damaging EntityEnderDragon does nothing
-+                            // - EntityEnderDragon hitbock always covers the other parts and is therefore always present
++                            // - EnderDragon hitbock always covers the other parts and is therefore always present
 +                            if (entity instanceof EnderDragonPart) {
 +                                continue;
 +                            }
@@ -112,14 +105,14 @@
 +                            entity.lastDamageCancelled = false;
 +
 +                            if (entity instanceof EnderDragon) {
-+                                for (EnderDragonPart entityComplexPart : ((EnderDragon) entity).subEntities) {
++                                for (EnderDragonPart dragonPart : ((EnderDragon) entity).getSubEntities()) {
 +                                    // Calculate damage separately for each EntityComplexPart
-+                                    if (list.contains(entityComplexPart)) {
-+                                        entityComplexPart.hurtServer(this.level, this.damageSource, this.damageCalculator.getEntityDamageAmount(this, entity, f2));
++                                    if (list.contains(dragonPart)) {
++                                        dragonPart.hurtServer(this.level, this.damageSource, this.damageCalculator.getEntityDamageAmount(this, entity, f1));
 +                                    }
 +                                }
 +                            } else {
-+                                entity.hurtServer(this.level, this.damageSource, this.damageCalculator.getEntityDamageAmount(this, entity, f2));
++                                entity.hurtServer(this.level, this.damageSource, this.damageCalculator.getEntityDamageAmount(this, entity, f1));
 +                            }
 +
 +                            if (entity.lastDamageCancelled) { // SPIGOT-5339, SPIGOT-6252, SPIGOT-6777: Skip entity if damage event was cancelled
@@ -128,48 +121,45 @@
 +                            // CraftBukkit end
                          }
  
-                         double d5 = (1.0D - d0) * (double) f2 * (double) f1;
-@@ -204,7 +257,7 @@
-                         if (entity instanceof LivingEntity) {
-                             LivingEntity entityliving = (LivingEntity) entity;
- 
--                            d6 = d5 * (1.0D - entityliving.getAttributeValue(Attributes.EXPLOSION_KNOCKBACK_RESISTANCE));
-+                            d6 = entity instanceof Player && this.level.paperConfig().environment.disableExplosionKnockback ? 0 : d5 * (1.0D - entityliving.getAttributeValue(Attributes.EXPLOSION_KNOCKBACK_RESISTANCE)); // Paper
+                         double d4 = (1.0 - d) * f1 * knockbackMultiplier;
+                         double d5;
+                         if (entity instanceof LivingEntity livingEntity) {
+-                            d5 = d4 * (1.0 - livingEntity.getAttributeValue(Attributes.EXPLOSION_KNOCKBACK_RESISTANCE));
++                            d5 = entity instanceof Player && this.level.paperConfig().environment.disableExplosionKnockback ? 0 : d4 * (1.0 - livingEntity.getAttributeValue(Attributes.EXPLOSION_KNOCKBACK_RESISTANCE)); // Paper
                          } else {
-                             d6 = d5;
+                             d5 = d4;
                          }
-@@ -214,11 +267,19 @@
-                         d3 *= d6;
-                         Vec3 vec3d = new Vec3(d1, d2, d3);
- 
+@@ -206,10 +_,18 @@
+                         d2 *= d5;
+                         d3 *= d5;
+                         Vec3 vec3 = new Vec3(d1, d2, d3);
 +                        // CraftBukkit start - Call EntityKnockbackEvent
 +                        if (entity instanceof LivingEntity) {
-+                           // Paper start - knockback events
-+                           io.papermc.paper.event.entity.EntityKnockbackEvent event = CraftEventFactory.callEntityKnockbackEvent((org.bukkit.craftbukkit.entity.CraftLivingEntity) entity.getBukkitEntity(), this.source, this.damageSource.getEntity() != null ? this.damageSource.getEntity() : this.source, io.papermc.paper.event.entity.EntityKnockbackEvent.Cause.EXPLOSION, d6, vec3d);
-+                            vec3d = event.isCancelled() ? Vec3.ZERO : org.bukkit.craftbukkit.util.CraftVector.toNMS(event.getKnockback());
-+                           // Paper end - knockback events
++                            // Paper start - knockback events
++                            io.papermc.paper.event.entity.EntityKnockbackEvent event = CraftEventFactory.callEntityKnockbackEvent((org.bukkit.craftbukkit.entity.CraftLivingEntity) entity.getBukkitEntity(), this.source, this.damageSource.getEntity() != null ? this.damageSource.getEntity() : this.source, io.papermc.paper.event.entity.EntityKnockbackEvent.Cause.EXPLOSION, d5, vec3);
++                            vec3 = event.isCancelled() ? Vec3.ZERO : org.bukkit.craftbukkit.util.CraftVector.toNMS(event.getKnockback());
++                            // Paper end - knockback events
 +                        }
 +                        // CraftBukkit end
-                         entity.push(vec3d);
+                         entity.push(vec3);
                          if (entity instanceof Player) {
-                             Player entityhuman = (Player) entity;
- 
--                            if (!entityhuman.isSpectator() && (!entityhuman.isCreative() || !entityhuman.getAbilities().flying)) {
-+                            if (!entityhuman.isSpectator() && (!entityhuman.isCreative() || !entityhuman.getAbilities().flying) && !level.paperConfig().environment.disableExplosionKnockback) { // Paper - Option to disable explosion knockback
-                                 this.hitPlayers.put(entityhuman, vec3d);
+                             Player player = (Player)entity;
+-                            if (!player.isSpectator() && (!player.isCreative() || !player.getAbilities().flying)) {
++                            if (!player.isSpectator() && (!player.isCreative() || !player.getAbilities().flying) && !level.paperConfig().environment.disableExplosionKnockback) { // Paper - Option to disable explosion knockback
+                                 this.hitPlayers.put(player, vec3);
                              }
                          }
-@@ -235,10 +296,62 @@
-         List<ServerExplosion.StackCollector> list1 = new ArrayList();
+@@ -225,7 +_,61 @@
+         List<ServerExplosion.StackCollector> list = new ArrayList<>();
+         Util.shuffle(blocks, this.level.random);
  
-         Util.shuffle(positions, this.level.random);
 +        // CraftBukkit start
 +        org.bukkit.World bworld = this.level.getWorld();
 +        Location location = CraftLocation.toBukkit(this.center, bworld);
 +
 +        List<org.bukkit.block.Block> blockList = new ObjectArrayList<>();
-+        for (int i1 = positions.size() - 1; i1 >= 0; i1--) {
-+            BlockPos cpos = positions.get(i1);
++        for (int i1 = blocks.size() - 1; i1 >= 0; i1--) {
++            BlockPos cpos = blocks.get(i1);
 +            org.bukkit.block.Block bblock = bworld.getBlockAt(cpos.getX(), cpos.getY(), cpos.getZ());
 +            if (!bblock.getType().isAir()) {
 +                blockList.add(bblock);
@@ -192,49 +182,47 @@
 +            this.yield = event.getYield();
 +        }
 +
-+        positions.clear();
++        blocks.clear();
 +
 +        for (org.bukkit.block.Block bblock : bukkitBlocks) {
 +            BlockPos coords = new BlockPos(bblock.getX(), bblock.getY(), bblock.getZ());
-+            positions.add(coords);
++            blocks.add(coords);
 +        }
 +
 +        if (this.wasCanceled) {
 +            return;
 +        }
 +        // CraftBukkit end
-         Iterator iterator = positions.iterator();
- 
-         while (iterator.hasNext()) {
-             BlockPos blockposition = (BlockPos) iterator.next();
++
+         for (BlockPos blockPos : blocks) {
 +            // CraftBukkit start - TNTPrimeEvent
-+            BlockState iblockdata = this.level.getBlockState(blockposition);
++            BlockState iblockdata = this.level.getBlockState(blockPos);
 +            Block block = iblockdata.getBlock();
 +            if (block instanceof net.minecraft.world.level.block.TntBlock) {
 +                Entity sourceEntity = this.source == null ? null : this.source;
 +                BlockPos sourceBlock = sourceEntity == null ? BlockPos.containing(this.center) : null;
-+                if (!CraftEventFactory.callTNTPrimeEvent(this.level, blockposition, org.bukkit.event.block.TNTPrimeEvent.PrimeCause.EXPLOSION, sourceEntity, sourceBlock)) {
-+                    this.level.sendBlockUpdated(blockposition, Blocks.AIR.defaultBlockState(), iblockdata, 3); // Update the block on the client
++                if (!CraftEventFactory.callTNTPrimeEvent(this.level, blockPos, org.bukkit.event.block.TNTPrimeEvent.PrimeCause.EXPLOSION, sourceEntity, sourceBlock)) {
++                    this.level.sendBlockUpdated(blockPos, Blocks.AIR.defaultBlockState(), iblockdata, 3); // Update the block on the client
 +                    continue;
 +                }
 +            }
 +            // CraftBukkit end
- 
-             this.level.getBlockState(blockposition).onExplosionHit(this.level, blockposition, this, (itemstack, blockposition1) -> {
-                 ServerExplosion.addOrAppendStack(list1, itemstack, blockposition1);
-@@ -262,13 +375,22 @@
-             BlockPos blockposition = (BlockPos) iterator.next();
- 
-             if (this.level.random.nextInt(3) == 0 && this.level.getBlockState(blockposition).isAir() && this.level.getBlockState(blockposition.below()).isSolidRender()) {
--                this.level.setBlockAndUpdate(blockposition, BaseFireBlock.getState(this.level, blockposition));
++
+             this.level
+                 .getBlockState(blockPos)
+                 .onExplosionHit(this.level, blockPos, this, (itemStack, blockPos1) -> addOrAppendStack(list, itemStack, blockPos1));
+@@ -239,12 +_,21 @@
+     private void createFire(List<BlockPos> blocks) {
+         for (BlockPos blockPos : blocks) {
+             if (this.level.random.nextInt(3) == 0 && this.level.getBlockState(blockPos).isAir() && this.level.getBlockState(blockPos.below()).isSolidRender()) {
+-                this.level.setBlockAndUpdate(blockPos, BaseFireBlock.getState(this.level, blockPos));
 +                // CraftBukkit start - Ignition by explosion
-+                if (!org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(this.level, blockposition, this).isCancelled()) {
-+                    this.level.setBlockAndUpdate(blockposition, BaseFireBlock.getState(this.level, blockposition));
++                if (!org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(this.level, blockPos, this).isCancelled()) {
++                    this.level.setBlockAndUpdate(blockPos, BaseFireBlock.getState(this.level, blockPos));
 +                }
 +                // CraftBukkit end
              }
          }
- 
      }
  
      public void explode() {
@@ -243,22 +231,23 @@
 +            return;
 +        }
 +        // CraftBukkit end
-         this.level.gameEvent(this.source, (Holder) GameEvent.EXPLODE, this.center);
+         this.level.gameEvent(this.source, GameEvent.EXPLODE, this.center);
          List<BlockPos> list = this.calculateExplodedPositions();
- 
-@@ -288,6 +410,7 @@
+         this.hurtEntities();
+@@ -261,6 +_,7 @@
      }
  
-     private static void addOrAppendStack(List<ServerExplosion.StackCollector> droppedItemsOut, ItemStack item, BlockPos pos) {
-+        if (item.isEmpty()) return; // CraftBukkit - SPIGOT-5425
-         Iterator iterator = droppedItemsOut.iterator();
- 
-         do {
-@@ -372,4 +495,85 @@
- 
+     private static void addOrAppendStack(List<ServerExplosion.StackCollector> stackCollectors, ItemStack stack, BlockPos pos) {
++        if (stack.isEmpty()) return; // CraftBukkit - SPIGOT-5425
+         for (ServerExplosion.StackCollector stackCollector : stackCollectors) {
+             stackCollector.tryMerge(stack);
+             if (stack.isEmpty()) {
+@@ -342,4 +_,86 @@
+             }
          }
      }
 +
++
 +    // Paper start - Optimize explosions
 +    private float getBlockDensity(Vec3 vec3d, Entity entity) {
 +        if (!this.level.paperConfig().environment.optimizeExplosions) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/ServerLevelAccessor.java.patch b/paper-server/patches/sources/net/minecraft/world/level/ServerLevelAccessor.java.patch
similarity index 96%
rename from paper-server/patches/unapplied/net/minecraft/world/level/ServerLevelAccessor.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/ServerLevelAccessor.java.patch
index 2c482ebee0..0d2900e6a1 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/ServerLevelAccessor.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/ServerLevelAccessor.java.patch
@@ -1,13 +1,14 @@
 --- a/net/minecraft/world/level/ServerLevelAccessor.java
 +++ b/net/minecraft/world/level/ServerLevelAccessor.java
-@@ -8,6 +8,17 @@
+@@ -7,6 +_,17 @@
      ServerLevel getLevel();
  
      default void addFreshEntityWithPassengers(Entity entity) {
 -        entity.getSelfAndPassengers().forEach(this::addFreshEntity);
+-    }
 +        // CraftBukkit start
 +        this.addFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT);
-     }
++    }
 +
 +    default void addFreshEntityWithPassengers(Entity entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) {
 +        entity.getSelfAndPassengers().forEach((e) -> this.addFreshEntity(e, reason));
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/StructureManager.java.patch b/paper-server/patches/sources/net/minecraft/world/level/StructureManager.java.patch
similarity index 67%
rename from paper-server/patches/unapplied/net/minecraft/world/level/StructureManager.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/StructureManager.java.patch
index 8a15d2f4f1..2286bb70ae 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/StructureManager.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/StructureManager.java.patch
@@ -1,20 +1,21 @@
 --- a/net/minecraft/world/level/StructureManager.java
 +++ b/net/minecraft/world/level/StructureManager.java
-@@ -48,7 +48,12 @@
+@@ -48,7 +_,13 @@
      }
  
-     public List<StructureStart> startsForStructure(ChunkPos pos, Predicate<Structure> predicate) {
--        Map<Structure, LongSet> map = this.level.getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_REFERENCES).getAllReferences();
+     public List<StructureStart> startsForStructure(ChunkPos chunkPos, Predicate<Structure> structurePredicate) {
+-        Map<Structure, LongSet> allReferences = this.level.getChunk(chunkPos.x, chunkPos.z, ChunkStatus.STRUCTURE_REFERENCES).getAllReferences();
 +        // Paper start - Fix swamp hut cat generation deadlock
-+        return this.startsForStructure(pos, predicate, null);
++        return this.startsForStructure(chunkPos, structurePredicate, null);
 +    }
-+    public List<StructureStart> startsForStructure(ChunkPos pos, Predicate<Structure> predicate, @Nullable ServerLevelAccessor levelAccessor) {
-+        Map<Structure, LongSet> map = (levelAccessor == null ? this.level : levelAccessor).getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_REFERENCES).getAllReferences();
++
++    public List<StructureStart> startsForStructure(ChunkPos chunkPos, Predicate<Structure> structurePredicate, @Nullable ServerLevelAccessor levelAccessor) {
++        Map<Structure, LongSet> allReferences = (levelAccessor == null ? this.level : levelAccessor).getChunk(chunkPos.x, chunkPos.z, ChunkStatus.STRUCTURE_REFERENCES).getAllReferences();
 +        // Paper end - Fix swamp hut cat generation deadlock
          Builder<StructureStart> builder = ImmutableList.builder();
  
-         for (Entry<Structure, LongSet> entry : map.entrySet()) {
-@@ -116,10 +121,20 @@
+         for (Entry<Structure, LongSet> entry : allReferences.entrySet()) {
+@@ -118,10 +_,20 @@
      }
  
      public StructureStart getStructureWithPieceAt(BlockPos pos, Predicate<Holder<Structure>> predicate) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/biome/MobSpawnSettings.java.patch b/paper-server/patches/sources/net/minecraft/world/level/biome/MobSpawnSettings.java.patch
similarity index 92%
rename from paper-server/patches/unapplied/net/minecraft/world/level/biome/MobSpawnSettings.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/biome/MobSpawnSettings.java.patch
index d1fb7a4640..1033724319 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/biome/MobSpawnSettings.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/biome/MobSpawnSettings.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/level/biome/MobSpawnSettings.java
 +++ b/net/minecraft/world/level/biome/MobSpawnSettings.java
-@@ -75,8 +75,40 @@
+@@ -75,8 +_,40 @@
      }
  
      public static class Builder {
@@ -36,7 +36,7 @@
 +        }
 +        // use toImmutableEnumMap collector
          private final Map<MobCategory, List<MobSpawnSettings.SpawnerData>> spawners = Stream.of(MobCategory.values())
--            .collect(ImmutableMap.toImmutableMap(mobCategory -> (MobCategory)mobCategory, mobCategory -> Lists.newArrayList()));
+-            .collect(ImmutableMap.toImmutableMap(key -> (MobCategory)key, value -> Lists.newArrayList()));
 +            .collect(Maps.toImmutableEnumMap(mobCategory -> (MobCategory)mobCategory, mobCategory -> new MobList())); // Use MobList instead of ArrayList
 +        // Paper end - Perf: keep track of data in a pair set to give O(1) contains calls
          private final Map<EntityType<?>, MobSpawnSettings.MobSpawnCost> mobSpawnCosts = Maps.newLinkedHashMap();
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/AbstractCandleBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/AbstractCandleBlock.java.patch
similarity index 57%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/AbstractCandleBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/AbstractCandleBlock.java.patch
index 2c42f4cb51..c641aa16f1 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/AbstractCandleBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/AbstractCandleBlock.java.patch
@@ -1,14 +1,14 @@
 --- a/net/minecraft/world/level/block/AbstractCandleBlock.java
 +++ b/net/minecraft/world/level/block/AbstractCandleBlock.java
-@@ -47,6 +47,11 @@
+@@ -44,6 +_,11 @@
      @Override
-     protected void onProjectileHit(Level world, BlockState state, BlockHitResult hit, Projectile projectile) {
-         if (!world.isClientSide && projectile.isOnFire() && this.canBeLit(state)) {
+     protected void onProjectileHit(Level level, BlockState state, BlockHitResult hit, Projectile projectile) {
+         if (!level.isClientSide && projectile.isOnFire() && this.canBeLit(state)) {
 +            // CraftBukkit start
-+            if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(world, hit.getBlockPos(), projectile).isCancelled()) {
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(level, hit.getBlockPos(), projectile).isCancelled()) {
 +                return;
 +            }
 +            // CraftBukkit end
-             AbstractCandleBlock.setLit(world, state, hit.getBlockPos(), true);
+             setLit(level, state, hit.getBlockPos(), true);
          }
- 
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/AbstractCauldronBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/AbstractCauldronBlock.java.patch
new file mode 100644
index 0000000000..506f6551ef
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/AbstractCauldronBlock.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/level/block/AbstractCauldronBlock.java
++++ b/net/minecraft/world/level/block/AbstractCauldronBlock.java
+@@ -58,7 +_,7 @@
+         ItemStack stack, BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hitResult
+     ) {
+         CauldronInteraction cauldronInteraction = this.interactions.map().get(stack.getItem());
+-        return cauldronInteraction.interact(state, level, pos, player, hand, stack);
++        return cauldronInteraction.interact(state, level, pos, player, hand, stack, hitResult.getDirection()); // Paper - pass hit direction
+     }
+ 
+     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/AnvilBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/AnvilBlock.java.patch
similarity index 62%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/AnvilBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/AnvilBlock.java.patch
index 81462ca698..8701c8ba8c 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/AnvilBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/AnvilBlock.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/world/level/block/AnvilBlock.java
 +++ b/net/minecraft/world/level/block/AnvilBlock.java
-@@ -62,8 +62,9 @@
+@@ -62,8 +_,9 @@
      @Override
-     protected InteractionResult useWithoutItem(BlockState state, Level world, BlockPos pos, Player player, BlockHitResult hit) {
-         if (!world.isClientSide) {
--            player.openMenu(state.getMenuProvider(world, pos));
-+            if (player.openMenu(state.getMenuProvider(world, pos)).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
+     protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) {
+         if (!level.isClientSide) {
+-            player.openMenu(state.getMenuProvider(level, pos));
++            if (player.openMenu(state.getMenuProvider(level, pos)).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
              player.awardStat(Stats.INTERACT_WITH_ANVIL);
 +            } // Paper - Fix InventoryOpenEvent cancellation
          }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/BambooSaplingBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/BambooSaplingBlock.java.patch
new file mode 100644
index 0000000000..df4b06991a
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/BambooSaplingBlock.java.patch
@@ -0,0 +1,19 @@
+--- a/net/minecraft/world/level/block/BambooSaplingBlock.java
++++ b/net/minecraft/world/level/block/BambooSaplingBlock.java
+@@ -43,7 +_,7 @@
+ 
+     @Override
+     protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+-        if (random.nextInt(3) == 0 && level.isEmptyBlock(pos.above()) && level.getRawBrightness(pos.above(), 0) >= 9) {
++        if (random.nextFloat() < (level.spigotConfig.bambooModifier / (100.0f * 3)) && level.isEmptyBlock(pos.above()) && level.getRawBrightness(pos.above(), 0) >= 9) { // Spigot - SPIGOT-7159: Better modifier resolution
+             this.growBamboo(level, pos);
+         }
+     }
+@@ -99,6 +_,6 @@
+     }
+ 
+     protected void growBamboo(Level level, BlockPos state) {
+-        level.setBlock(state.above(), Blocks.BAMBOO.defaultBlockState().setValue(BambooStalkBlock.LEAVES, BambooLeaves.SMALL), 3);
++        org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(level, state, state.above(), Blocks.BAMBOO.defaultBlockState().setValue(BambooStalkBlock.LEAVES, BambooLeaves.SMALL), 3); // CraftBukkit - BlockSpreadEvent
+     }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/BambooStalkBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/BambooStalkBlock.java.patch
new file mode 100644
index 0000000000..c5c9ef2c74
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/BambooStalkBlock.java.patch
@@ -0,0 +1,87 @@
+--- a/net/minecraft/world/level/block/BambooStalkBlock.java
++++ b/net/minecraft/world/level/block/BambooStalkBlock.java
+@@ -130,9 +_,9 @@
+     @Override
+     protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+         if (state.getValue(STAGE) == 0) {
+-            if (random.nextInt(3) == 0 && level.isEmptyBlock(pos.above()) && level.getRawBrightness(pos.above(), 0) >= 9) {
++            if (random.nextFloat() < (level.spigotConfig.bambooModifier / (100.0f * 3)) && level.isEmptyBlock(pos.above()) && level.getRawBrightness(pos.above(), 0) >= 9) { // Spigot - SPIGOT-7159: Better modifier resolution
+                 int i = this.getHeightBelowUpToMax(level, pos) + 1;
+-                if (i < 16) {
++                if (i < level.paperConfig().maxGrowthHeight.bamboo.max) { // Paper - Configurable cactus/bamboo/reed growth height
+                     this.growBamboo(state, level, pos, random, i);
+                 }
+             }
+@@ -168,7 +_,7 @@
+     public boolean isValidBonemealTarget(LevelReader level, BlockPos pos, BlockState state) {
+         int heightAboveUpToMax = this.getHeightAboveUpToMax(level, pos);
+         int heightBelowUpToMax = this.getHeightBelowUpToMax(level, pos);
+-        return heightAboveUpToMax + heightBelowUpToMax + 1 < 16 && level.getBlockState(pos.above(heightAboveUpToMax)).getValue(STAGE) != 1;
++        return heightAboveUpToMax + heightBelowUpToMax + 1 < ((Level) level).paperConfig().maxGrowthHeight.bamboo.max && level.getBlockState(pos.above(heightAboveUpToMax)).getValue(STAGE) != 1; // Paper - Configurable cactus/bamboo/reed growth height
+     }
+ 
+     @Override
+@@ -186,7 +_,7 @@
+         for (int i2 = 0; i2 < i1; i2++) {
+             BlockPos blockPos = pos.above(heightAboveUpToMax);
+             BlockState blockState = level.getBlockState(blockPos);
+-            if (i >= 16 || blockState.getValue(STAGE) == 1 || !level.isEmptyBlock(blockPos.above())) {
++            if (i >= level.paperConfig().maxGrowthHeight.bamboo.max || !blockState.is(Blocks.BAMBOO) || blockState.getValue(BambooStalkBlock.STAGE) == 1 || !level.isEmptyBlock(blockPos.above())) { // CraftBukkit - If the BlockSpreadEvent was cancelled, we have no bamboo here // Paper - Configurable cactus/bamboo/reed growth height
+                 return;
+             }
+ 
+@@ -206,29 +_,38 @@
+         BlockPos blockPos = pos.below(2);
+         BlockState blockState1 = level.getBlockState(blockPos);
+         BambooLeaves bambooLeaves = BambooLeaves.NONE;
++        boolean shouldUpdateOthers = false; // CraftBukkit
+         if (age >= 1) {
+             if (!blockState.is(Blocks.BAMBOO) || blockState.getValue(LEAVES) == BambooLeaves.NONE) {
+                 bambooLeaves = BambooLeaves.SMALL;
+             } else if (blockState.is(Blocks.BAMBOO) && blockState.getValue(LEAVES) != BambooLeaves.NONE) {
+                 bambooLeaves = BambooLeaves.LARGE;
+                 if (blockState1.is(Blocks.BAMBOO)) {
+-                    level.setBlock(pos.below(), blockState.setValue(LEAVES, BambooLeaves.SMALL), 3);
+-                    level.setBlock(blockPos, blockState1.setValue(LEAVES, BambooLeaves.NONE), 3);
++                    // CraftBukkit start - moved down
++                    // level.setBlock(pos.below(), blockState.setValue(LEAVES, BambooLeaves.SMALL), 3);
++                    // level.setBlock(blockPos, blockState1.setValue(LEAVES, BambooLeaves.NONE), 3);
++                    shouldUpdateOthers = true;
++                    // CraftBukkit end
+                 }
+             }
+         }
+ 
+         int i = state.getValue(AGE) != 1 && !blockState1.is(Blocks.BAMBOO) ? 0 : 1;
+-        int i1 = (age < 11 || !(random.nextFloat() < 0.25F)) && age != 15 ? 0 : 1;
+-        level.setBlock(
+-            pos.above(), this.defaultBlockState().setValue(AGE, Integer.valueOf(i)).setValue(LEAVES, bambooLeaves).setValue(STAGE, Integer.valueOf(i1)), 3
+-        );
++        int i1 = (age < level.paperConfig().maxGrowthHeight.bamboo.min || random.nextFloat() >= 0.25F) && age != (level.paperConfig().maxGrowthHeight.bamboo.max - 1) ? 0 : 1; // Paper - Configurable cactus/bamboo/reed growth height
++        // CraftBukkit start
++        if (org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(level, pos, pos.above(), this.defaultBlockState().setValue(net.minecraft.world.level.block.BambooStalkBlock.AGE, i).setValue(BambooStalkBlock.LEAVES, bambooLeaves).setValue(net.minecraft.world.level.block.BambooStalkBlock.STAGE, i1), 3)) {
++            if (shouldUpdateOthers) {
++                level.setBlock(pos.below(), blockState.setValue(BambooStalkBlock.LEAVES, BambooLeaves.SMALL), 3);
++                level.setBlock(blockPos, blockState1.setValue(BambooStalkBlock.LEAVES, BambooLeaves.NONE), 3);
++            }
++        }
++        // CraftBukkit end
+     }
+ 
+     protected int getHeightAboveUpToMax(BlockGetter level, BlockPos pos) {
+         int i = 0;
+ 
+-        while (i < 16 && level.getBlockState(pos.above(i + 1)).is(Blocks.BAMBOO)) {
++        while (i < ((Level) level).paperConfig().maxGrowthHeight.bamboo.max && level.getBlockState(pos.above(i + 1)).is(Blocks.BAMBOO)) { // Paper - Configurable cactus/bamboo/reed growth height
+             i++;
+         }
+ 
+@@ -238,7 +_,7 @@
+     protected int getHeightBelowUpToMax(BlockGetter level, BlockPos pos) {
+         int i = 0;
+ 
+-        while (i < 16 && level.getBlockState(pos.below(i + 1)).is(Blocks.BAMBOO)) {
++        while (i < ((Level) level).paperConfig().maxGrowthHeight.bamboo.max && level.getBlockState(pos.below(i + 1)).is(Blocks.BAMBOO)) { // Paper - Configurable cactus/bamboo/reed growth height
+             i++;
+         }
+ 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/BarrelBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/BarrelBlock.java.patch
similarity index 73%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/BarrelBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/BarrelBlock.java.patch
index 349f801388..23b0b9718c 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/BarrelBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/BarrelBlock.java.patch
@@ -1,12 +1,12 @@
 --- a/net/minecraft/world/level/block/BarrelBlock.java
 +++ b/net/minecraft/world/level/block/BarrelBlock.java
-@@ -41,8 +41,7 @@
+@@ -41,8 +_,7 @@
  
      @Override
-     protected InteractionResult useWithoutItem(BlockState state, Level world, BlockPos pos, Player player, BlockHitResult hit) {
--        if (world instanceof ServerLevel serverLevel && world.getBlockEntity(pos) instanceof BarrelBlockEntity barrelBlockEntity) {
+     protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) {
+-        if (level instanceof ServerLevel serverLevel && level.getBlockEntity(pos) instanceof BarrelBlockEntity barrelBlockEntity) {
 -            player.openMenu(barrelBlockEntity);
-+        if (world instanceof ServerLevel serverLevel && world.getBlockEntity(pos) instanceof BarrelBlockEntity barrelBlockEntity && player.openMenu(barrelBlockEntity).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
++        if (level instanceof ServerLevel serverLevel && level.getBlockEntity(pos) instanceof BarrelBlockEntity barrelBlockEntity && player.openMenu(barrelBlockEntity).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
              player.awardStat(Stats.OPEN_BARREL);
              PiglinAi.angerNearbyPiglins(serverLevel, player, true);
          }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/BaseFireBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/BaseFireBlock.java.patch
similarity index 56%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/BaseFireBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/BaseFireBlock.java.patch
index aeae088ca7..a9273622c5 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/BaseFireBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/BaseFireBlock.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/level/block/BaseFireBlock.java
 +++ b/net/minecraft/world/level/block/BaseFireBlock.java
-@@ -12,6 +12,7 @@
+@@ -12,6 +_,7 @@
  import net.minecraft.world.entity.Entity;
  import net.minecraft.world.entity.player.Player;
  import net.minecraft.world.item.context.BlockPlaceContext;
@@ -8,22 +8,22 @@
  import net.minecraft.world.level.BlockGetter;
  import net.minecraft.world.level.Level;
  import net.minecraft.world.level.block.state.BlockBehaviour;
-@@ -127,6 +128,7 @@
+@@ -128,6 +_,7 @@
  
      @Override
-     protected void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
-+        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
+     protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
++        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
          if (!entity.fireImmune()) {
              if (entity.getRemainingFireTicks() < 0) {
                  entity.setRemainingFireTicks(entity.getRemainingFireTicks() + 1);
-@@ -137,7 +139,18 @@
+@@ -137,7 +_,18 @@
              }
  
              if (entity.getRemainingFireTicks() >= 0) {
 -                entity.igniteForSeconds(8.0F);
 +                // CraftBukkit start
-+                org.bukkit.event.entity.EntityCombustEvent event = new org.bukkit.event.entity.EntityCombustByBlockEvent(org.bukkit.craftbukkit.block.CraftBlock.at(world, pos), entity.getBukkitEntity(), 8.0F);
-+                world.getCraftServer().getPluginManager().callEvent(event);
++                org.bukkit.event.entity.EntityCombustEvent event = new org.bukkit.event.entity.EntityCombustByBlockEvent(org.bukkit.craftbukkit.block.CraftBlock.at(level, pos), entity.getBukkitEntity(), 8.0F);
++                level.getCraftServer().getPluginManager().callEvent(event);
 +
 +                if (!event.isCancelled()) {
 +                    entity.igniteForSeconds(event.getDuration(), false);
@@ -36,41 +36,36 @@
              }
          }
  
-@@ -146,26 +159,26 @@
+@@ -146,24 +_,24 @@
      }
  
      @Override
--    protected void onPlace(BlockState state, Level world, BlockPos pos, BlockState oldState, boolean notify) {
--        if (!oldState.is(state.getBlock())) {
-+    protected void onPlace(BlockState iblockdata, Level world, BlockPos blockposition, BlockState iblockdata1, boolean flag, UseOnContext context) { // CraftBukkit - context
-+        if (!iblockdata1.is(iblockdata.getBlock())) {
-             if (BaseFireBlock.inPortalDimension(world)) {
--                Optional<PortalShape> optional = PortalShape.findEmptyPortalShape(world, pos, Direction.Axis.X);
-+                Optional<PortalShape> optional = PortalShape.findEmptyPortalShape(world, blockposition, Direction.Axis.X);
- 
+-    protected void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean isMoving) {
++    protected void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean isMoving, UseOnContext context) { // CraftBukkit - context
+         if (!oldState.is(state.getBlock())) {
+             if (inPortalDimension(level)) {
+                 Optional<PortalShape> optional = PortalShape.findEmptyPortalShape(level, pos, Direction.Axis.X);
                  if (optional.isPresent()) {
--                    ((PortalShape) optional.get()).createPortalBlocks(world);
-+                    ((PortalShape) optional.get()).createPortalBlocks(world, (context == null) ? null : context.getPlayer()); // CraftBukkit - player
+-                    optional.get().createPortalBlocks(level);
++                    optional.get().createPortalBlocks(level, (context == null) ? null : context.getPlayer()); // CraftBukkit - player
                      return;
                  }
              }
  
--            if (!state.canSurvive(world, pos)) {
--                world.removeBlock(pos, false);
-+            if (!iblockdata.canSurvive(world, blockposition)) {
-+                this.fireExtinguished(world, blockposition); // CraftBukkit - fuel block broke
+             if (!state.canSurvive(level, pos)) {
+-                level.removeBlock(pos, false);
++                this.fireExtinguished(level, pos); // CraftBukkit - fuel block broke
              }
- 
          }
      }
  
-     private static boolean inPortalDimension(Level world) {
--        return world.dimension() == Level.OVERWORLD || world.dimension() == Level.NETHER;
-+        return world.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.OVERWORLD || world.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER; // CraftBukkit - getTypeKey()
+     private static boolean inPortalDimension(Level level) {
+-        return level.dimension() == Level.OVERWORLD || level.dimension() == Level.NETHER;
++        return level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.OVERWORLD || level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER; // CraftBukkit - getTypeKey()
      }
  
      @Override
-@@ -213,4 +226,12 @@
+@@ -208,4 +_,12 @@
              }
          }
      }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/BasePressurePlateBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/BasePressurePlateBlock.java.patch
new file mode 100644
index 0000000000..2cada56fbe
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/BasePressurePlateBlock.java.patch
@@ -0,0 +1,45 @@
+--- a/net/minecraft/world/level/block/BasePressurePlateBlock.java
++++ b/net/minecraft/world/level/block/BasePressurePlateBlock.java
+@@ -81,6 +_,7 @@
+ 
+     @Override
+     protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
++        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
+         if (!level.isClientSide) {
+             int signalForState = this.getSignalForState(state);
+             if (signalForState == 0) {
+@@ -93,6 +_,19 @@
+         int signalStrength = this.getSignalStrength(level, pos);
+         boolean flag = currentSignal > 0;
+         boolean flag1 = signalStrength > 0;
++        
++        // CraftBukkit start - Interact Pressure Plate
++        org.bukkit.World bworld = level.getWorld();
++        org.bukkit.plugin.PluginManager manager = level.getCraftServer().getPluginManager();
++
++        if (flag != flag1) {
++            org.bukkit.event.block.BlockRedstoneEvent eventRedstone = new org.bukkit.event.block.BlockRedstoneEvent(bworld.getBlockAt(pos.getX(), pos.getY(), pos.getZ()), currentSignal, signalStrength);
++            manager.callEvent(eventRedstone);
++
++            flag1 = eventRedstone.getNewCurrent() > 0;
++            signalStrength = eventRedstone.getNewCurrent();
++        }
++        // CraftBukkit end
+         if (currentSignal != signalStrength) {
+             BlockState blockState = this.setSignalForState(state, signalStrength);
+             level.setBlock(pos, blockState, 2);
+@@ -145,7 +_,13 @@
+     }
+ 
+     protected static int getEntityCount(Level level, AABB box, Class<? extends Entity> entityClass) {
+-        return level.getEntitiesOfClass(entityClass, box, EntitySelector.NO_SPECTATORS.and(entity -> !entity.isIgnoringBlockTriggers())).size();
++        // CraftBukkit start
++        return BasePressurePlateBlock.getEntities(level, box, entityClass).size();
++    }
++
++    protected static <T extends Entity> java.util.List<T> getEntities(Level level, AABB box, Class<T> entityClass) {
++        return level.getEntitiesOfClass(entityClass, box, EntitySelector.NO_SPECTATORS.and(entity -> !entity.isIgnoringBlockTriggers()));
++        // CraftBukkit end
+     }
+ 
+     protected abstract int getSignalStrength(Level level, BlockPos pos);
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/BaseRailBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/BaseRailBlock.java.patch
new file mode 100644
index 0000000000..d8e5440b15
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/BaseRailBlock.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/world/level/block/BaseRailBlock.java
++++ b/net/minecraft/world/level/block/BaseRailBlock.java
+@@ -71,6 +_,7 @@
+         state = this.updateDir(level, pos, state, true);
+         if (this.isStraight) {
+             level.neighborChanged(state, pos, this, null, movedByPiston);
++            state = level.getBlockState(pos); // Paper - Fix some rails connecting improperly
+         }
+ 
+         return state;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/BeaconBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/BeaconBlock.java.patch
similarity index 69%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/BeaconBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/BeaconBlock.java.patch
index b23b55da23..3a2da5d64c 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/BeaconBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/BeaconBlock.java.patch
@@ -1,12 +1,12 @@
 --- a/net/minecraft/world/level/block/BeaconBlock.java
 +++ b/net/minecraft/world/level/block/BeaconBlock.java
-@@ -46,8 +46,7 @@
+@@ -46,8 +_,7 @@
  
      @Override
-     protected InteractionResult useWithoutItem(BlockState state, Level world, BlockPos pos, Player player, BlockHitResult hit) {
--        if (!world.isClientSide && world.getBlockEntity(pos) instanceof BeaconBlockEntity beaconBlockEntity) {
+     protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) {
+-        if (!level.isClientSide && level.getBlockEntity(pos) instanceof BeaconBlockEntity beaconBlockEntity) {
 -            player.openMenu(beaconBlockEntity);
-+        if (!world.isClientSide && world.getBlockEntity(pos) instanceof BeaconBlockEntity beaconBlockEntity && player.openMenu(beaconBlockEntity).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
++        if (!level.isClientSide && level.getBlockEntity(pos) instanceof BeaconBlockEntity beaconBlockEntity && player.openMenu(beaconBlockEntity).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
              player.awardStat(Stats.INTERACT_WITH_BEACON);
          }
  
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/BedBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/BedBlock.java.patch
similarity index 54%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/BedBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/BedBlock.java.patch
index 09946b2d9f..ad2f90861a 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/BedBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/BedBlock.java.patch
@@ -1,21 +1,20 @@
 --- a/net/minecraft/world/level/block/BedBlock.java
 +++ b/net/minecraft/world/level/block/BedBlock.java
-@@ -95,7 +95,8 @@
+@@ -92,7 +_,7 @@
                  }
              }
  
--            if (!BedBlock.canSetSpawn(world)) {
-+            // CraftBukkit - moved world and biome check into EntityHuman
-+            if (false && !BedBlock.canSetSpawn(world)) {
-                 world.removeBlock(pos, false);
-                 BlockPos blockposition1 = pos.relative(((Direction) state.getValue(BedBlock.FACING)).getOpposite());
- 
-@@ -108,25 +109,65 @@
-                 world.explode((Entity) null, world.damageSources().badRespawnPointExplosion(vec3d), (ExplosionDamageCalculator) null, vec3d, 5.0F, true, Level.ExplosionInteraction.BLOCK);
+-            if (!canSetSpawn(level)) {
++            if (false && !canSetSpawn(level)) { // CraftBukkit - moved world and biome check into EntityHuman
+                 level.removeBlock(pos, false);
+                 BlockPos blockPos = pos.relative(state.getValue(FACING).getOpposite());
+                 if (level.getBlockState(blockPos).is(this)) {
+@@ -103,22 +_,62 @@
+                 level.explode(null, level.damageSources().badRespawnPointExplosion(center), null, center, 5.0F, true, Level.ExplosionInteraction.BLOCK);
                  return InteractionResult.SUCCESS_SERVER;
-             } else if ((Boolean) state.getValue(BedBlock.OCCUPIED)) {
-+                if (!BedBlock.canSetSpawn(world)) return this.explodeBed(state, world, pos); // Paper - check explode first
-                 if (!this.kickVillagerOutOfBed(world, pos)) {
+             } else if (state.getValue(OCCUPIED)) {
++                if (!BedBlock.canSetSpawn(level)) return this.explodeBed(state, level, pos); // Paper - check explode first
+                 if (!this.kickVillagerOutOfBed(level, pos)) {
                      player.displayClientMessage(Component.translatable("block.minecraft.bed.occupied"), true);
                  }
  
@@ -25,32 +24,31 @@
 +                BlockState finaliblockdata = state;
 +                BlockPos finalblockposition = pos;
 +                // CraftBukkit end
-                 player.startSleepInBed(pos).ifLeft((entityhuman_enumbedresult) -> {
+                 player.startSleepInBed(pos).ifLeft(bedSleepingProblem -> {
 +                    // Paper start - PlayerBedFailEnterEvent
-+                    if (entityhuman_enumbedresult != null) {
-+                        io.papermc.paper.event.player.PlayerBedFailEnterEvent event = new io.papermc.paper.event.player.PlayerBedFailEnterEvent((org.bukkit.entity.Player) player.getBukkitEntity(), io.papermc.paper.event.player.PlayerBedFailEnterEvent.FailReason.values()[entityhuman_enumbedresult.ordinal()], org.bukkit.craftbukkit.block.CraftBlock.at(world, finalblockposition), !world.dimensionType().bedWorks(), io.papermc.paper.adventure.PaperAdventure.asAdventure(entityhuman_enumbedresult.getMessage()));
++                    if (bedSleepingProblem != null) {
++                        io.papermc.paper.event.player.PlayerBedFailEnterEvent event = new io.papermc.paper.event.player.PlayerBedFailEnterEvent((org.bukkit.entity.Player) player.getBukkitEntity(), io.papermc.paper.event.player.PlayerBedFailEnterEvent.FailReason.values()[bedSleepingProblem.ordinal()], org.bukkit.craftbukkit.block.CraftBlock.at(level, finalblockposition), !level.dimensionType().bedWorks(), io.papermc.paper.adventure.PaperAdventure.asAdventure(bedSleepingProblem.getMessage()));
 +                        if (!event.callEvent()) {
 +                            return;
 +                        }
 +                        // Paper end - PlayerBedFailEnterEvent
 +                    // CraftBukkit start - handling bed explosion from below here
 +                    if (event.getWillExplode()) { // Paper - PlayerBedFailEnterEvent
-+                        this.explodeBed(finaliblockdata, world, finalblockposition);
++                        this.explodeBed(finaliblockdata, level, finalblockposition);
 +                    } else
 +                    // CraftBukkit end
-                     if (entityhuman_enumbedresult.getMessage() != null) {
--                        player.displayClientMessage(entityhuman_enumbedresult.getMessage(), true);
+                     if (bedSleepingProblem.getMessage() != null) {
+-                        player.displayClientMessage(bedSleepingProblem.getMessage(), true);
 +                        final net.kyori.adventure.text.Component message = event.getMessage(); // Paper - PlayerBedFailEnterEvent
 +                        if (message != null) player.displayClientMessage(io.papermc.paper.adventure.PaperAdventure.asVanilla(message), true); // Paper - PlayerBedFailEnterEvent
                      }
 +                    } // Paper - PlayerBedFailEnterEvent
- 
                  });
                  return InteractionResult.SUCCESS_SERVER;
-+            }
-+        }
-+    }
-+
+             }
+         }
+     }
+ 
 +    // CraftBukkit start
 +    private InteractionResult explodeBed(BlockState iblockdata, Level world, BlockPos blockposition) {
 +        {
@@ -65,28 +63,25 @@
 +
 +                Vec3 vec3d = blockposition.getCenter();
 +
-+                world.explode((Entity) null, world.damageSources().badRespawnPointExplosion(vec3d, blockState), (ExplosionDamageCalculator) null, vec3d, 5.0F, true, Level.ExplosionInteraction.BLOCK); // CraftBukkit - add state
++                world.explode(null, world.damageSources().badRespawnPointExplosion(vec3d, blockState), null, vec3d, 5.0F, true, Level.ExplosionInteraction.BLOCK); // CraftBukkit - add state
 +                return InteractionResult.SUCCESS;
-             }
-         }
-     }
++             }
++         }
++     }
 +    // CraftBukkit end
- 
-     public static boolean canSetSpawn(Level world) {
--        return world.dimensionType().bedWorks();
-+        return world.dimensionType().bedWorks(); // Paper - actually check if the bed works
++
+     public static boolean canSetSpawn(Level level) {
+         return level.dimensionType().bedWorks();
      }
- 
-     private boolean kickVillagerOutOfBed(Level world, BlockPos pos) {
-@@ -320,6 +361,11 @@
-             BlockPos blockposition1 = pos.relative((Direction) state.getValue(BedBlock.FACING));
- 
-             world.setBlock(blockposition1, (BlockState) state.setValue(BedBlock.PART, BedPart.HEAD), 3);
+@@ -311,6 +_,11 @@
+         if (!level.isClientSide) {
+             BlockPos blockPos = pos.relative(state.getValue(FACING));
+             level.setBlock(blockPos, state.setValue(PART, BedPart.HEAD), 3);
 +            // CraftBukkit start - SPIGOT-7315: Don't updated if we capture block states
-+            if (world.captureBlockStates) {
++            if (level.captureBlockStates) {
 +                return;
 +            }
 +            // CraftBukkit end
-             world.blockUpdated(pos, Blocks.AIR);
-             state.updateNeighbourShapes(world, pos, 3);
+             level.blockUpdated(pos, Blocks.AIR);
+             state.updateNeighbourShapes(level, pos, 3);
          }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/BeehiveBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/BeehiveBlock.java.patch
new file mode 100644
index 0000000000..221e36c665
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/BeehiveBlock.java.patch
@@ -0,0 +1,60 @@
+--- a/net/minecraft/world/level/block/BeehiveBlock.java
++++ b/net/minecraft/world/level/block/BeehiveBlock.java
+@@ -91,8 +_,8 @@
+     }
+ 
+     @Override
+-    public void playerDestroy(Level level, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity te, ItemStack stack) {
+-        super.playerDestroy(level, player, pos, state, te, stack);
++    public void playerDestroy(Level level, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity te, ItemStack stack, boolean includeDrops, boolean dropExp) { // Paper - fix drops not preventing stats/food exhaustion
++        super.playerDestroy(level, player, pos, state, te, stack, includeDrops, dropExp); // Paper - fix drops not preventing stats/food exhaustion
+         if (!level.isClientSide && te instanceof BeehiveBlockEntity beehiveBlockEntity) {
+             if (!EnchantmentHelper.hasTag(stack, EnchantmentTags.PREVENTS_BEE_SPAWNS_WHEN_MINING)) {
+                 beehiveBlockEntity.emptyAllLivingFromHive(player, state, BeehiveBlockEntity.BeeReleaseStatus.EMERGENCY);
+@@ -100,7 +_,7 @@
+                 this.angerNearbyBees(level, pos);
+             }
+ 
+-            CriteriaTriggers.BEE_NEST_DESTROYED.trigger((ServerPlayer)player, state, stack, beehiveBlockEntity.getOccupantCount());
++            // CriteriaTriggers.BEE_NEST_DESTROYED.trigger((ServerPlayer)player, state, stack, beehiveBlockEntity.getOccupantCount()); // Paper - Trigger bee_nest_destroyed trigger in the correct place; moved until after items are dropped
+         }
+     }
+ 
+@@ -122,14 +_,14 @@
+             for (Bee bee : entitiesOfClass) {
+                 if (bee.getTarget() == null) {
+                     Player player = Util.getRandom(entitiesOfClass1, level.random);
+-                    bee.setTarget(player);
++                    bee.setTarget(player, org.bukkit.event.entity.EntityTargetEvent.TargetReason.CLOSEST_PLAYER, true); // CraftBukkit
+                 }
+             }
+         }
+     }
+ 
+     public static void dropHoneycomb(Level level, BlockPos pos) {
+-        popResource(level, pos, new ItemStack(Items.HONEYCOMB, 3));
++        popResource(level, pos, new ItemStack(Items.HONEYCOMB, 3)); // Paper - Add PlayerShearBlockEvent; conflict on change, item needs to be set below
+     }
+ 
+     @Override
+@@ -141,8 +_,19 @@
+         if (honeyLevelValue >= 5) {
+             Item item = stack.getItem();
+             if (stack.is(Items.SHEARS)) {
++                // Paper start - Add PlayerShearBlockEvent
++                io.papermc.paper.event.block.PlayerShearBlockEvent event = new io.papermc.paper.event.block.PlayerShearBlockEvent((org.bukkit.entity.Player) player.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(stack), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand), new java.util.ArrayList<>());
++                event.getDrops().add(org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(new ItemStack(Items.HONEYCOMB, 3)));
++                if (!event.callEvent()) {
++                    return InteractionResult.PASS;
++                }
++                // Paper end
+                 level.playSound(player, player.getX(), player.getY(), player.getZ(), SoundEvents.BEEHIVE_SHEAR, SoundSource.BLOCKS, 1.0F, 1.0F);
+-                dropHoneycomb(level, pos);
++                // Paper start - Add PlayerShearBlockEvent
++                for (org.bukkit.inventory.ItemStack itemDrop : event.getDrops()) {
++                    popResource(level, pos, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(itemDrop));
++                }
++                // Paper end - Add PlayerShearBlockEvent
+                 stack.hurtAndBreak(1, player, LivingEntity.getSlotForHand(hand));
+                 flag = true;
+                 level.gameEvent(player, GameEvent.SHEAR, pos);
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/BellBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/BellBlock.java.patch
new file mode 100644
index 0000000000..67deb9015b
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/BellBlock.java.patch
@@ -0,0 +1,14 @@
+--- a/net/minecraft/world/level/block/BellBlock.java
++++ b/net/minecraft/world/level/block/BellBlock.java
+@@ -142,6 +_,11 @@
+                 direction = level.getBlockState(pos).getValue(FACING);
+             }
+ 
++            // CraftBukkit start
++            if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBellRingEvent(level, pos, direction, entity)) {
++                return false;
++            }
++            // CraftBukkit end
+             ((BellBlockEntity)blockEntity).onHit(direction);
+             level.playSound(null, pos, SoundEvents.BELL_BLOCK, SoundSource.BLOCKS, 2.0F, 1.0F);
+             level.gameEvent(entity, GameEvent.BLOCK_CHANGE, pos);
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/BigDripleafBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/BigDripleafBlock.java.patch
new file mode 100644
index 0000000000..11a7f55411
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/BigDripleafBlock.java.patch
@@ -0,0 +1,91 @@
+--- a/net/minecraft/world/level/block/BigDripleafBlock.java
++++ b/net/minecraft/world/level/block/BigDripleafBlock.java
+@@ -136,7 +_,7 @@
+ 
+     @Override
+     protected void onProjectileHit(Level level, BlockState state, BlockHitResult hit, Projectile projectile) {
+-        this.setTiltAndScheduleTick(state, level, hit.getBlockPos(), Tilt.FULL, SoundEvents.BIG_DRIPLEAF_TILT_DOWN);
++        this.setTiltAndScheduleTick(state, level, hit.getBlockPos(), Tilt.FULL, SoundEvents.BIG_DRIPLEAF_TILT_DOWN, projectile); // CraftBukkit
+     }
+ 
+     @Override
+@@ -199,9 +_,23 @@
+ 
+     @Override
+     protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
++        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
+         if (!level.isClientSide) {
+             if (state.getValue(TILT) == Tilt.NONE && canEntityTilt(pos, entity) && !level.hasNeighborSignal(pos)) {
+-                this.setTiltAndScheduleTick(state, level, pos, Tilt.UNSTABLE, null);
++                // CraftBukkit start - tilt dripleaf
++                org.bukkit.event.Cancellable cancellable;
++                if (entity instanceof net.minecraft.world.entity.player.Player) {
++                    cancellable = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerInteractEvent((net.minecraft.world.entity.player.Player) entity, org.bukkit.event.block.Action.PHYSICAL, pos, null, null, null);
++                } else {
++                    cancellable = new org.bukkit.event.entity.EntityInteractEvent(entity.getBukkitEntity(), level.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ()));
++                    level.getCraftServer().getPluginManager().callEvent((org.bukkit.event.entity.EntityInteractEvent) cancellable);
++                }
++
++                if (cancellable.isCancelled()) {
++                    return;
++                }
++                this.setTiltAndScheduleTick(state, level, pos, Tilt.UNSTABLE, null, entity);
++                // CraftBukkit end
+             }
+         }
+     }
+@@ -213,9 +_,9 @@
+         } else {
+             Tilt tilt = state.getValue(TILT);
+             if (tilt == Tilt.UNSTABLE) {
+-                this.setTiltAndScheduleTick(state, level, pos, Tilt.PARTIAL, SoundEvents.BIG_DRIPLEAF_TILT_DOWN);
++                this.setTiltAndScheduleTick(state, level, pos, Tilt.PARTIAL, SoundEvents.BIG_DRIPLEAF_TILT_DOWN, null); // CraftBukkit
+             } else if (tilt == Tilt.PARTIAL) {
+-                this.setTiltAndScheduleTick(state, level, pos, Tilt.FULL, SoundEvents.BIG_DRIPLEAF_TILT_DOWN);
++                this.setTiltAndScheduleTick(state, level, pos, Tilt.FULL, SoundEvents.BIG_DRIPLEAF_TILT_DOWN, null); // CraftBukkit
+             } else if (tilt == Tilt.FULL) {
+                 resetTilt(state, level, pos);
+             }
+@@ -238,8 +_,11 @@
+         return entity.onGround() && entity.position().y > pos.getY() + 0.6875F;
+     }
+ 
+-    private void setTiltAndScheduleTick(BlockState state, Level level, BlockPos pos, Tilt tilt, @Nullable SoundEvent sound) {
+-        setTilt(state, level, pos, tilt);
++    private void setTiltAndScheduleTick(BlockState state, Level level, BlockPos pos, Tilt tilt, @Nullable SoundEvent sound, @Nullable Entity entity) {
++        if (!setTilt(state, level, pos, tilt, entity)) {
++            return;
++        }
++        // CraftBukkit end
+         if (sound != null) {
+             playTiltSound(level, pos, sound);
+         }
+@@ -251,18 +_,26 @@
+     }
+ 
+     private static void resetTilt(BlockState state, Level level, BlockPos pos) {
+-        setTilt(state, level, pos, Tilt.NONE);
++        setTilt(state, level, pos, Tilt.NONE, null); // CraftBukkit
+         if (state.getValue(TILT) != Tilt.NONE) {
+             playTiltSound(level, pos, SoundEvents.BIG_DRIPLEAF_TILT_UP);
+         }
+     }
+ 
+-    private static void setTilt(BlockState state, Level level, BlockPos pos, Tilt tilt) {
++    // CraftBukkit start
++    private static boolean setTilt(BlockState state, Level level, BlockPos pos, Tilt tilt, @Nullable Entity entity) {
++        if (entity != null) {
++            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(entity, pos, state.setValue(BigDripleafBlock.TILT, tilt))) {
++                return false;
++            }
++        }
++        // CraftBukkit end
+         Tilt tilt1 = state.getValue(TILT);
+         level.setBlock(pos, state.setValue(TILT, tilt), 2);
+         if (tilt.causesVibration() && tilt != tilt1) {
+             level.gameEvent(null, GameEvent.BLOCK_CHANGE, pos);
+         }
++        return true; // CraftBukkit
+     }
+ 
+     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/BlastFurnaceBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/BlastFurnaceBlock.java.patch
similarity index 78%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/BlastFurnaceBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/BlastFurnaceBlock.java.patch
index 3317c55a37..11ef775f5e 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/BlastFurnaceBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/BlastFurnaceBlock.java.patch
@@ -1,9 +1,9 @@
 --- a/net/minecraft/world/level/block/BlastFurnaceBlock.java
 +++ b/net/minecraft/world/level/block/BlastFurnaceBlock.java
-@@ -45,8 +45,7 @@
+@@ -45,8 +_,7 @@
      @Override
-     protected void openContainer(Level world, BlockPos pos, Player player) {
-         BlockEntity blockEntity = world.getBlockEntity(pos);
+     protected void openContainer(Level level, BlockPos pos, Player player) {
+         BlockEntity blockEntity = level.getBlockEntity(pos);
 -        if (blockEntity instanceof BlastFurnaceBlockEntity) {
 -            player.openMenu((MenuProvider)blockEntity);
 +        if (blockEntity instanceof BlastFurnaceBlockEntity && player.openMenu((MenuProvider)blockEntity).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/Block.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/Block.java.patch
similarity index 57%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/Block.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/Block.java.patch
index c1060afc42..e58b07c7a7 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/Block.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/Block.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/level/block/Block.java
 +++ b/net/minecraft/world/level/block/Block.java
-@@ -88,6 +88,21 @@
+@@ -90,6 +_,21 @@
      public static final int UPDATE_LIMIT = 512;
      protected final StateDefinition<Block, BlockState> stateDefinition;
      private BlockState defaultBlockState;
@@ -22,8 +22,8 @@
      @Nullable
      private Item item;
      private static final int CACHE_SIZE = 256;
-@@ -295,12 +310,38 @@
- 
+@@ -272,6 +_,27 @@
+         return state.getDrops(builder);
      }
  
 +    // Paper start - Add BlockBreakBlockEvent
@@ -47,91 +47,92 @@
 +    }
 +    // Paper end - Add BlockBreakBlockEvent
 +
-     public static void dropResources(BlockState state, Level world, BlockPos pos, @Nullable BlockEntity blockEntity, @Nullable Entity entity, ItemStack tool) {
+     public static void dropResources(BlockState state, Level level, BlockPos pos) {
+         if (level instanceof ServerLevel) {
+             getDrops(state, (ServerLevel)level, pos, null).forEach(itemStack -> popResource(level, pos, itemStack));
+@@ -287,9 +_,14 @@
+     }
+ 
+     public static void dropResources(BlockState state, Level level, BlockPos pos, @Nullable BlockEntity blockEntity, @Nullable Entity entity, ItemStack tool) {
 +    // Paper start - Properly handle xp dropping
-+        dropResources(state, world, pos, blockEntity, entity, tool, true);
++        dropResources(state, level, pos, blockEntity, entity, tool, true);
 +    }
-+    public static void dropResources(BlockState state, Level world, BlockPos pos, @Nullable BlockEntity blockEntity, @Nullable Entity entity, ItemStack tool, boolean dropExperience) {
++    public static void dropResources(BlockState state, Level level, BlockPos pos, @Nullable BlockEntity blockEntity, @Nullable Entity entity, ItemStack tool, boolean dropExperience) {
 +    // Paper end - Properly handle xp dropping
-         if (world instanceof ServerLevel) {
-             Block.getDrops(state, (ServerLevel) world, pos, blockEntity, entity, tool).forEach((itemstack1) -> {
-                 Block.popResource(world, pos, itemstack1);
-             });
--            state.spawnAfterBreak((ServerLevel) world, pos, tool, true);
-+            state.spawnAfterBreak((ServerLevel) world, pos, tool, dropExperience); // Paper - Properly handle xp dropping
+         if (level instanceof ServerLevel) {
+             getDrops(state, (ServerLevel)level, pos, blockEntity, entity, tool).forEach(itemStack -> popResource(level, pos, itemStack));
+-            state.spawnAfterBreak((ServerLevel)level, pos, tool, true);
++            state.spawnAfterBreak((ServerLevel) level, pos, tool, dropExperience); // Paper - Properly handle xp dropping
          }
- 
-     }
-@@ -340,7 +381,13 @@
-                 ItemEntity entityitem = (ItemEntity) itemEntitySupplier.get();
- 
-                 entityitem.setDefaultPickUpDelay();
--                world.addFreshEntity(entityitem);
-+                // CraftBukkit start
-+                if (world.captureDrops != null) {
-+                    world.captureDrops.add(entityitem);
-+                } else {
-+                    world.addFreshEntity(entityitem);
-+                }
-+                // CraftBukkit end
-                 return;
-             }
-         }
-@@ -348,8 +395,13 @@
      }
  
-     public void popExperience(ServerLevel world, BlockPos pos, int size) {
+@@ -320,13 +_,24 @@
+         if (level instanceof ServerLevel serverLevel && !stack.isEmpty() && serverLevel.getGameRules().getBoolean(GameRules.RULE_DOBLOCKDROPS)) {
+             ItemEntity itemEntity = itemEntitySupplier.get();
+             itemEntity.setDefaultPickUpDelay();
+-            level.addFreshEntity(itemEntity);
++            // CraftBukkit start
++            if (level.captureDrops != null) {
++                level.captureDrops.add(itemEntity);
++            } else {
++                level.addFreshEntity(itemEntity);
++            }
++            // CraftBukkit end
+         }
+     }
+ 
+     public void popExperience(ServerLevel level, BlockPos pos, int amount) {
 +        // Paper start - add entity parameter
-+        popExperience(world, pos, size, null);
++        popExperience(level, pos, amount, null);
 +    }
-+    public void popExperience(ServerLevel world, BlockPos pos, int size, net.minecraft.world.entity.Entity entity) {
++    public void popExperience(ServerLevel level, BlockPos pos, int amount, net.minecraft.world.entity.Entity entity) {
 +        // Paper end - add entity parameter
-         if (world.getGameRules().getBoolean(GameRules.RULE_DOBLOCKDROPS)) {
--            ExperienceOrb.award(world, Vec3.atCenterOf(pos), size);
-+            ExperienceOrb.award(world, Vec3.atCenterOf(pos), size, org.bukkit.entity.ExperienceOrb.SpawnReason.BLOCK_BREAK, entity); // Paper
+         if (level.getGameRules().getBoolean(GameRules.RULE_DOBLOCKDROPS)) {
+-            ExperienceOrb.award(level, Vec3.atCenterOf(pos), amount);
++            ExperienceOrb.award(level, Vec3.atCenterOf(pos), amount, org.bukkit.entity.ExperienceOrb.SpawnReason.BLOCK_BREAK, entity); // Paper
          }
- 
      }
-@@ -367,10 +419,18 @@
+ 
+@@ -345,10 +_,18 @@
          return this.defaultBlockState();
      }
  
 +    @io.papermc.paper.annotation.DoNotUse @Deprecated // Paper - fix drops not preventing stats/food exhaustion
-     public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool) {
-+        // Paper start - fix drops not preventing stats/food exhaustion
-+        this.playerDestroy(world, player, pos, state, blockEntity, tool, true, true);
+     public void playerDestroy(Level level, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool) {
++    // Paper start - fix drops not preventing stats/food exhaustion
++        this.playerDestroy(level, player, pos, state, blockEntity, tool, true, true);
 +    }
-+    public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool, boolean includeDrops, boolean dropExp) {
-+        // Paper end - fix drops not preventing stats/food exhaustion
++    public void playerDestroy(Level level, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool, boolean includeDrops, boolean dropExp) {
++    // Paper end - fix drops not preventing stats/food exhaustion
          player.awardStat(Stats.BLOCK_MINED.get(this));
 -        player.causeFoodExhaustion(0.005F);
--        Block.dropResources(state, world, pos, blockEntity, player, tool);
+-        dropResources(state, level, pos, blockEntity, player, tool);
 +        player.causeFoodExhaustion(0.005F, org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.BLOCK_MINED); // CraftBukkit - EntityExhaustionEvent
 +        if (includeDrops) { // Paper - fix drops not preventing stats/food exhaustion
-+        Block.dropResources(state, world, pos, blockEntity, player, tool, dropExp); // Paper - Properly handle xp dropping
++        Block.dropResources(state, level, pos, blockEntity, player, tool, dropExp); // Paper - Properly handle xp dropping
 +        } // Paper - fix drops not preventing stats/food exhaustion
      }
  
-     public void setPlacedBy(Level world, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack itemStack) {}
-@@ -490,15 +550,35 @@
+     public void setPlacedBy(Level level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) {
+@@ -469,12 +_,33 @@
          return this.builtInRegistryHolder;
      }
  
--    protected void tryDropExperience(ServerLevel world, BlockPos pos, ItemStack tool, IntProvider experience) {
--        int i = EnchantmentHelper.processBlockExperience(world, tool, experience.sample(world.getRandom()));
-+    // CraftBukkit start
-+    protected int tryDropExperience(ServerLevel worldserver, BlockPos blockposition, ItemStack itemstack, IntProvider intprovider) {
-+        int i = EnchantmentHelper.processBlockExperience(worldserver, itemstack, intprovider.sample(worldserver.getRandom()));
- 
+-    protected void tryDropExperience(ServerLevel level, BlockPos pos, ItemStack heldItem, IntProvider amount) {
++    protected int tryDropExperience(ServerLevel level, BlockPos pos, ItemStack heldItem, IntProvider amount) { // CraftBukkit
+         int i = EnchantmentHelper.processBlockExperience(level, heldItem, amount.sample(level.getRandom()));
          if (i > 0) {
--            this.popExperience(world, pos, i);
-+            // this.popExperience(worldserver, blockposition, i);
+-            this.popExperience(level, pos, i);
+-        }
+-    }
++            // CraftBukkit start
++            //this.popExperience(level, pos, i);
 +            return i;
-         }
- 
-+        return 0;
-     }
- 
++            // CraftBukkit end
++        }
++        return 0; // CraftBukkit
++    }
++    // CraftBukkit start
 +    public int getExpDrop(BlockState iblockdata, ServerLevel worldserver, BlockPos blockposition, ItemStack itemstack, boolean flag) {
 +        return 0;
 +    }
@@ -148,7 +149,6 @@
 +        return value;
 +    }
 +    // Spigot end
-+
-     private static record ShapePairKey(VoxelShape first, VoxelShape second) {
  
-         public boolean equals(Object object) {
+     record ShapePairKey(VoxelShape first, VoxelShape second) {
+         @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/BrewingStandBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/BrewingStandBlock.java.patch
similarity index 72%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/BrewingStandBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/BrewingStandBlock.java.patch
index ad19f46261..e2bbf75753 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/BrewingStandBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/BrewingStandBlock.java.patch
@@ -1,12 +1,12 @@
 --- a/net/minecraft/world/level/block/BrewingStandBlock.java
 +++ b/net/minecraft/world/level/block/BrewingStandBlock.java
-@@ -68,8 +68,7 @@
+@@ -68,8 +_,7 @@
  
      @Override
-     protected InteractionResult useWithoutItem(BlockState state, Level world, BlockPos pos, Player player, BlockHitResult hit) {
--        if (!world.isClientSide && world.getBlockEntity(pos) instanceof BrewingStandBlockEntity brewingStandBlockEntity) {
+     protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) {
+-        if (!level.isClientSide && level.getBlockEntity(pos) instanceof BrewingStandBlockEntity brewingStandBlockEntity) {
 -            player.openMenu(brewingStandBlockEntity);
-+        if (!world.isClientSide && world.getBlockEntity(pos) instanceof BrewingStandBlockEntity brewingStandBlockEntity && player.openMenu(brewingStandBlockEntity).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
++        if (!level.isClientSide && level.getBlockEntity(pos) instanceof BrewingStandBlockEntity brewingStandBlockEntity && player.openMenu(brewingStandBlockEntity).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
              player.awardStat(Stats.INTERACT_WITH_BREWINGSTAND);
          }
  
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/BubbleColumnBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/BubbleColumnBlock.java.patch
similarity index 66%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/BubbleColumnBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/BubbleColumnBlock.java.patch
index aa97c1238d..1ca54afa47 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/BubbleColumnBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/BubbleColumnBlock.java.patch
@@ -1,10 +1,10 @@
 --- a/net/minecraft/world/level/block/BubbleColumnBlock.java
 +++ b/net/minecraft/world/level/block/BubbleColumnBlock.java
-@@ -48,6 +48,7 @@
+@@ -48,6 +_,7 @@
  
      @Override
-     protected void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
-+        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
-         BlockState blockState = world.getBlockState(pos.above());
+     protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
++        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
+         BlockState blockState = level.getBlockState(pos.above());
          if (blockState.isAir()) {
              entity.onAboveBubbleCol(state.getValue(DRAG_DOWN));
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/BuddingAmethystBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/BuddingAmethystBlock.java.patch
new file mode 100644
index 0000000000..81ceb891c5
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/BuddingAmethystBlock.java.patch
@@ -0,0 +1,17 @@
+--- a/net/minecraft/world/level/block/BuddingAmethystBlock.java
++++ b/net/minecraft/world/level/block/BuddingAmethystBlock.java
+@@ -44,7 +_,13 @@
+                 BlockState blockState1 = block.defaultBlockState()
+                     .setValue(AmethystClusterBlock.FACING, direction)
+                     .setValue(AmethystClusterBlock.WATERLOGGED, Boolean.valueOf(blockState.getFluidState().getType() == Fluids.WATER));
+-                level.setBlockAndUpdate(blockPos, blockState1);
++                // Paper start - Have Amethyst throw both spread and grow events
++                if (block == Blocks.SMALL_AMETHYST_BUD) {
++                    org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(level, pos, blockPos, blockState1, 3); // CraftBukkit
++                } else {
++                    org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(level, blockPos, blockState1);
++                }
++                // Paper end - Have Amethyst throw both spread and grow events
+             }
+         }
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/BushBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/BushBlock.java.patch
new file mode 100644
index 0000000000..709e307ca3
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/BushBlock.java.patch
@@ -0,0 +1,29 @@
+--- a/net/minecraft/world/level/block/BushBlock.java
++++ b/net/minecraft/world/level/block/BushBlock.java
+@@ -6,6 +_,7 @@
+ import net.minecraft.tags.BlockTags;
+ import net.minecraft.util.RandomSource;
+ import net.minecraft.world.level.BlockGetter;
++import net.minecraft.world.level.Level;
+ import net.minecraft.world.level.LevelReader;
+ import net.minecraft.world.level.ScheduledTickAccess;
+ import net.minecraft.world.level.block.state.BlockBehaviour;
+@@ -35,9 +_,15 @@
+         BlockState neighborState,
+         RandomSource random
+     ) {
+-        return !state.canSurvive(level, pos)
+-            ? Blocks.AIR.defaultBlockState()
+-            : super.updateShape(state, level, scheduledTickAccess, pos, direction, neighborPos, neighborState, random);
++       // CraftBukkit start
++       if (!state.canSurvive(level, pos)) {
++           // Suppress during worldgen
++           if (!(level instanceof net.minecraft.server.level.ServerLevel world1 && world1.hasPhysicsEvent) || !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(world1, pos).isCancelled()) { // Paper
++               return Blocks.AIR.defaultBlockState();
++           }
++       }
++       return super.updateShape(state, level, scheduledTickAccess, pos, direction, neighborPos, neighborState, random);
++       // CraftBukkit end
+     }
+ 
+     @Override
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/ButtonBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/ButtonBlock.java.patch
new file mode 100644
index 0000000000..956c25b452
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/ButtonBlock.java.patch
@@ -0,0 +1,62 @@
+--- a/net/minecraft/world/level/block/ButtonBlock.java
++++ b/net/minecraft/world/level/block/ButtonBlock.java
+@@ -114,6 +_,19 @@
+         if (state.getValue(POWERED)) {
+             return InteractionResult.CONSUME;
+         } else {
++            // CraftBukkit start
++            boolean powered = state.getValue(ButtonBlock.POWERED);
++            org.bukkit.block.Block block = level.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ());
++            int old = (powered) ? 15 : 0;
++            int current = (!powered) ? 15 : 0;
++
++            org.bukkit.event.block.BlockRedstoneEvent eventRedstone = new org.bukkit.event.block.BlockRedstoneEvent(block, old, current);
++            level.getCraftServer().getPluginManager().callEvent(eventRedstone);
++
++            if ((eventRedstone.getNewCurrent() > 0) != (!powered)) {
++                return InteractionResult.SUCCESS;
++            }
++            // CraftBukkit end
+             this.press(state, level, pos, player);
+             return InteractionResult.SUCCESS;
+         }
+@@ -179,6 +_,7 @@
+ 
+     @Override
+     protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
++        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
+         if (!level.isClientSide && this.type.canButtonBeActivatedByArrows() && !state.getValue(POWERED)) {
+             this.checkPressed(state, level, pos);
+         }
+@@ -190,7 +_,31 @@
+             : null;
+         boolean flag = abstractArrow != null;
+         boolean poweredValue = state.getValue(POWERED);
++        // CraftBukkit start - Call interact event when arrows turn on wooden buttons
++        if (poweredValue != flag && flag) {
++            org.bukkit.block.Block block = level.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ());
++            org.bukkit.event.entity.EntityInteractEvent event = new org.bukkit.event.entity.EntityInteractEvent(abstractArrow.getBukkitEntity(), block);
++            level.getCraftServer().getPluginManager().callEvent(event);
++
++            if (event.isCancelled()) {
++                return;
++            }
++        }
++        // CraftBukkit end
+         if (flag != poweredValue) {
++            // CraftBukkit start
++            boolean powered = poweredValue;
++            org.bukkit.block.Block block = level.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ());
++            int old = (powered) ? 15 : 0;
++            int current = (!powered) ? 15 : 0;
++
++            org.bukkit.event.block.BlockRedstoneEvent eventRedstone = new org.bukkit.event.block.BlockRedstoneEvent(block, old, current);
++            level.getCraftServer().getPluginManager().callEvent(eventRedstone);
++
++            if ((flag && eventRedstone.getNewCurrent() <= 0) || (!flag && eventRedstone.getNewCurrent() > 0)) {
++                return;
++            }
++            // CraftBukkit end
+             level.setBlock(pos, state.setValue(POWERED, Boolean.valueOf(flag)), 3);
+             this.updateNeighbours(state, level, pos);
+             this.playSound(null, level, pos, flag);
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/CactusBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/CactusBlock.java.patch
new file mode 100644
index 0000000000..af19e96da9
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/CactusBlock.java.patch
@@ -0,0 +1,33 @@
+--- a/net/minecraft/world/level/block/CactusBlock.java
++++ b/net/minecraft/world/level/block/CactusBlock.java
+@@ -56,14 +_,16 @@
+                 i++;
+             }
+ 
+-            if (i < 3) {
++            if (i < level.paperConfig().maxGrowthHeight.cactus) { // Paper - Configurable cactus/bamboo/reed growth height
+                 int ageValue = state.getValue(AGE);
+-                if (ageValue == 15) {
+-                    level.setBlockAndUpdate(blockPos, this.defaultBlockState());
++
++                int modifier = level.spigotConfig.cactusModifier; // Spigot - SPIGOT-7159: Better modifier resolution
++                if (ageValue >= 15 || (modifier != 100 && random.nextFloat() < (modifier / (100.0f * 16)))) { // Spigot - SPIGOT-7159: Better modifier
++                    org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(level, blockPos, this.defaultBlockState()); // CraftBukkit
+                     BlockState blockState = state.setValue(AGE, Integer.valueOf(0));
+                     level.setBlock(pos, blockState, 4);
+                     level.neighborChanged(blockState, blockPos, this, null, false);
+-                } else {
++                } else if (modifier == 100 || random.nextFloat() < (modifier / (100.0f * 16))) { // Spigot - SPIGOT-7159: Better modifier resolution
+                     level.setBlock(pos, state.setValue(AGE, Integer.valueOf(ageValue + 1)), 4);
+                 }
+             }
+@@ -113,7 +_,8 @@
+ 
+     @Override
+     protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
+-        entity.hurt(level.damageSources().cactus(), 1.0F);
++        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
++        entity.hurt(level.damageSources().cactus().directBlock(level, pos), 1.0F); // CraftBukkit
+     }
+ 
+     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/CakeBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/CakeBlock.java.patch
similarity index 51%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/CakeBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/CakeBlock.java.patch
index 346de8857c..d564317a09 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/CakeBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/CakeBlock.java.patch
@@ -1,25 +1,25 @@
 --- a/net/minecraft/world/level/block/CakeBlock.java
 +++ b/net/minecraft/world/level/block/CakeBlock.java
-@@ -66,6 +66,12 @@
-             if (block instanceof CandleBlock) {
-                 CandleBlock candleblock = (CandleBlock) block;
- 
-+                // Paper start - call change block event
-+                if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(player, pos, CandleCakeBlock.byCandle(candleblock))) {
-+                    player.containerMenu.sendAllDataToRemote(); // update inv because candle could decrease
-+                    return InteractionResult.TRY_WITH_EMPTY_HAND;
-+                }
-+                // Paper end - call change block event
-                 stack.consume(1, player);
-                 world.playSound((Player) null, pos, SoundEvents.CAKE_ADD_CANDLE, SoundSource.BLOCKS, 1.0F, 1.0F);
-                 world.setBlockAndUpdate(pos, CandleCakeBlock.byCandle(candleblock));
-@@ -97,10 +103,29 @@
+@@ -67,6 +_,12 @@
+     ) {
+         Item item = stack.getItem();
+         if (stack.is(ItemTags.CANDLES) && state.getValue(BITES) == 0 && Block.byItem(item) instanceof CandleBlock candleBlock) {
++            // Paper start - call change block event
++            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(player, pos, CandleCakeBlock.byCandle(candleBlock))) {
++                player.containerMenu.sendAllDataToRemote(); // update inv because candle could decrease
++                return InteractionResult.TRY_WITH_EMPTY_HAND;
++            }
++            // Paper end - call change block event
+             stack.consume(1, player);
+             level.playSound(null, pos, SoundEvents.CAKE_ADD_CANDLE, SoundSource.BLOCKS, 1.0F, 1.0F);
+             level.setBlockAndUpdate(pos, CandleCakeBlock.byCandle(candleBlock));
+@@ -97,9 +_,28 @@
          if (!player.canEat(false)) {
              return InteractionResult.PASS;
          } else {
 +            // Paper start - call change block event
-+            int i = state.getValue(CakeBlock.BITES);
-+            final BlockState newState = i < MAX_BITES ? state.setValue(CakeBlock.BITES, i + 1) : world.getFluidState(pos).createLegacyBlock();
++            int bitesValue = state.getValue(CakeBlock.BITES);
++            final BlockState newState = bitesValue < MAX_BITES ? state.setValue(CakeBlock.BITES, bitesValue + 1) : level.getFluidState(pos).createLegacyBlock();
 +            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(player, pos, newState)) {
 +                ((net.minecraft.server.level.ServerPlayer) player).getBukkitEntity().sendHealthUpdate();
 +                return InteractionResult.PASS; // return a non-consume result to cake blocks don't drop their candles
@@ -27,11 +27,11 @@
 +            // Paper end - call change block event
              player.awardStat(Stats.EAT_CAKE_SLICE);
 -            player.getFoodData().eat(2, 0.1F);
--            int i = (Integer) state.getValue(CakeBlock.BITES);
+-            int bitesValue = state.getValue(BITES);
 +            // CraftBukkit start
 +            // entityhuman.getFoodData().eat(2, 0.1F);
 +            int oldFoodLevel = player.getFoodData().foodLevel;
- 
++
 +            org.bukkit.event.entity.FoodLevelChangeEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callFoodLevelChangeEvent(player, 2 + oldFoodLevel);
 +
 +            if (!event.isCancelled()) {
@@ -41,7 +41,6 @@
 +            ((net.minecraft.server.level.ServerPlayer) player).getBukkitEntity().sendHealthUpdate();
 +            // CraftBukkit end
 +            // Paper - move up
-+
-             world.gameEvent((Entity) player, (Holder) GameEvent.EAT, pos);
-             if (i < 6) {
-                 world.setBlock(pos, (BlockState) state.setValue(CakeBlock.BITES, i + 1), 3);
+             level.gameEvent(player, GameEvent.EAT, pos);
+             if (bitesValue < 6) {
+                 level.setBlock(pos, state.setValue(BITES, Integer.valueOf(bitesValue + 1)), 3);
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/CampfireBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/CampfireBlock.java.patch
new file mode 100644
index 0000000000..0663b2c497
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/CampfireBlock.java.patch
@@ -0,0 +1,25 @@
+--- a/net/minecraft/world/level/block/CampfireBlock.java
++++ b/net/minecraft/world/level/block/CampfireBlock.java
+@@ -112,8 +_,9 @@
+ 
+     @Override
+     protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
++        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
+         if (state.getValue(LIT) && entity instanceof LivingEntity) {
+-            entity.hurt(level.damageSources().campfire(), this.fireDamage);
++            entity.hurt(level.damageSources().campfire().directBlock(level, pos), (float) this.fireDamage); // CraftBukkit
+         }
+ 
+         super.entityInside(state, level, pos, entity);
+@@ -242,6 +_,11 @@
+             && projectile.mayInteract(serverLevel, blockPos)
+             && !state.getValue(LIT)
+             && !state.getValue(WATERLOGGED)) {
++            // CraftBukkit start
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(level, blockPos, projectile).isCancelled()) {
++                return;
++            }
++            // CraftBukkit end
+             level.setBlock(blockPos, state.setValue(BlockStateProperties.LIT, Boolean.valueOf(true)), 11);
+         }
+     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/CartographyTableBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/CartographyTableBlock.java.patch
similarity index 64%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/CartographyTableBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/CartographyTableBlock.java.patch
index 237e4da1be..2aa0c9dd39 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/CartographyTableBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/CartographyTableBlock.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/world/level/block/CartographyTableBlock.java
 +++ b/net/minecraft/world/level/block/CartographyTableBlock.java
-@@ -32,8 +32,9 @@
+@@ -32,8 +_,9 @@
      @Override
-     protected InteractionResult useWithoutItem(BlockState state, Level world, BlockPos pos, Player player, BlockHitResult hit) {
-         if (!world.isClientSide) {
--            player.openMenu(state.getMenuProvider(world, pos));
-+            if (player.openMenu(state.getMenuProvider(world, pos)).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
+     protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) {
+         if (!level.isClientSide) {
+-            player.openMenu(state.getMenuProvider(level, pos));
++            if (player.openMenu(state.getMenuProvider(level, pos)).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
              player.awardStat(Stats.INTERACT_WITH_CARTOGRAPHY_TABLE);
 +            } // Paper - Fix InventoryOpenEvent cancellation
          }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/CarvedPumpkinBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/CarvedPumpkinBlock.java.patch
new file mode 100644
index 0000000000..4343b42a31
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/CarvedPumpkinBlock.java.patch
@@ -0,0 +1,18 @@
+--- a/net/minecraft/world/level/block/CarvedPumpkinBlock.java
++++ b/net/minecraft/world/level/block/CarvedPumpkinBlock.java
+@@ -79,9 +_,13 @@
+     }
+ 
+     private static void spawnGolemInWorld(Level level, BlockPattern.BlockPatternMatch patternMatch, Entity golem, BlockPos pos) {
+-        clearPatternBlocks(level, patternMatch);
++        // clearPatternBlocks(level, patternMatch); // CraftBukkit - moved down
+         golem.moveTo(pos.getX() + 0.5, pos.getY() + 0.05, pos.getZ() + 0.5, 0.0F, 0.0F);
+-        level.addFreshEntity(golem);
++        if (!level.addFreshEntity(golem, (golem.getType() == EntityType.SNOW_GOLEM) ? org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.BUILD_SNOWMAN : org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.BUILD_IRONGOLEM)) {
++            return;
++        }
++        clearPatternBlocks(level, patternMatch); // CraftBukkit - from above
++        // CraftBukkit end
+ 
+         for (ServerPlayer serverPlayer : level.getEntitiesOfClass(ServerPlayer.class, golem.getBoundingBox().inflate(5.0))) {
+             CriteriaTriggers.SUMMONED_ENTITY.trigger(serverPlayer, golem);
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/CauldronBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/CauldronBlock.java.patch
new file mode 100644
index 0000000000..84db4f9fcc
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/CauldronBlock.java.patch
@@ -0,0 +1,46 @@
+--- a/net/minecraft/world/level/block/CauldronBlock.java
++++ b/net/minecraft/world/level/block/CauldronBlock.java
+@@ -40,9 +_,19 @@
+     public void handlePrecipitation(BlockState state, Level level, BlockPos pos, Biome.Precipitation precipitation) {
+         if (shouldHandlePrecipitation(level, precipitation)) {
+             if (precipitation == Biome.Precipitation.RAIN) {
++                // Paper start - Call CauldronLevelChangeEvent
++                if (!LayeredCauldronBlock.changeLevel(level, pos, Blocks.WATER_CAULDRON.defaultBlockState(), null, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason.NATURAL_FILL, false)) { // avoid duplicate game event
++                    return;
++                }
++                // Paper end - Call CauldronLevelChangeEvent
+                 level.setBlockAndUpdate(pos, Blocks.WATER_CAULDRON.defaultBlockState());
+                 level.gameEvent(null, GameEvent.BLOCK_CHANGE, pos);
+             } else if (precipitation == Biome.Precipitation.SNOW) {
++                // Paper start - Call CauldronLevelChangeEvent
++                if (!LayeredCauldronBlock.changeLevel(level, pos, Blocks.POWDER_SNOW_CAULDRON.defaultBlockState(), null, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason.NATURAL_FILL, false)) { // avoid duplicate game event
++                    return;
++                }
++                // Paper end - Call CauldronLevelChangeEvent
+                 level.setBlockAndUpdate(pos, Blocks.POWDER_SNOW_CAULDRON.defaultBlockState());
+                 level.gameEvent(null, GameEvent.BLOCK_CHANGE, pos);
+             }
+@@ -58,13 +_,19 @@
+     protected void receiveStalactiteDrip(BlockState state, Level level, BlockPos pos, Fluid fluid) {
+         if (fluid == Fluids.WATER) {
+             BlockState blockState = Blocks.WATER_CAULDRON.defaultBlockState();
+-            level.setBlockAndUpdate(pos, blockState);
+-            level.gameEvent(GameEvent.BLOCK_CHANGE, pos, GameEvent.Context.of(blockState));
++            // Paper start - Call CauldronLevelChangeEvent; don't send level event or game event if cancelled
++            if (!LayeredCauldronBlock.changeLevel(level, pos, blockState, null, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason.NATURAL_FILL)) { // CraftBukkit
++                return;
++            }
++            // Paper end - Call CauldronLevelChangeEvent
+             level.levelEvent(1047, pos, 0);
+         } else if (fluid == Fluids.LAVA) {
+             BlockState blockState = Blocks.LAVA_CAULDRON.defaultBlockState();
+-            level.setBlockAndUpdate(pos, blockState);
+-            level.gameEvent(GameEvent.BLOCK_CHANGE, pos, GameEvent.Context.of(blockState));
++            // Paper start - Call CauldronLevelChangeEvent; don't send level event or game event if cancelled
++            if (!LayeredCauldronBlock.changeLevel(level, pos, blockState, null, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason.NATURAL_FILL)) { // CraftBukkit
++                return;
++            }
++            // Paper end - Call CauldronLevelChangeEvent
+             level.levelEvent(1046, pos, 0);
+         }
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/CaveVines.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/CaveVines.java.patch
new file mode 100644
index 0000000000..6355a6f4b2
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/CaveVines.java.patch
@@ -0,0 +1,27 @@
+--- a/net/minecraft/world/level/block/CaveVines.java
++++ b/net/minecraft/world/level/block/CaveVines.java
+@@ -23,7 +_,23 @@
+ 
+     static InteractionResult use(@Nullable Entity entity, BlockState state, Level level, BlockPos pos) {
+         if (state.getValue(BERRIES)) {
+-            Block.popResource(level, pos, new ItemStack(Items.GLOW_BERRIES, 1));
++            // CraftBukkit start
++            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(entity, pos, (BlockState) state.setValue(CaveVines.BERRIES, false))) {
++                return InteractionResult.SUCCESS;
++            }
++
++            if (entity instanceof net.minecraft.world.entity.player.Player) {
++                org.bukkit.event.player.PlayerHarvestBlockEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerHarvestBlockEvent(level, pos, (net.minecraft.world.entity.player.Player) entity, net.minecraft.world.InteractionHand.MAIN_HAND, java.util.Collections.singletonList(new ItemStack(Items.GLOW_BERRIES, 1)));
++                if (event.isCancelled()) {
++                    return InteractionResult.SUCCESS; // We need to return a success either way, because making it PASS or FAIL will result in a bug where cancelling while harvesting w/ block in hand places block
++                }
++                for (org.bukkit.inventory.ItemStack itemStack : event.getItemsHarvested()) {
++                    Block.popResource(level, pos, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(itemStack));
++                }
++            } else {
++                Block.popResource(level, pos, new ItemStack(Items.GLOW_BERRIES, 1));
++            }
++            // CraftBukkit end
+             float f = Mth.randomBetween(level.random, 0.8F, 1.2F);
+             level.playSound(null, pos, SoundEvents.CAVE_VINES_PICK_BERRIES, SoundSource.BLOCKS, 1.0F, f);
+             BlockState blockState = state.setValue(BERRIES, Boolean.valueOf(false));
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/CaveVinesBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/CaveVinesBlock.java.patch
similarity index 75%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/CaveVinesBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/CaveVinesBlock.java.patch
index 511200af88..c0eb21e510 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/CaveVinesBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/CaveVinesBlock.java.patch
@@ -1,22 +1,20 @@
 --- a/net/minecraft/world/level/block/CaveVinesBlock.java
 +++ b/net/minecraft/world/level/block/CaveVinesBlock.java
-@@ -50,9 +50,18 @@
-         return to.setValue(BERRIES, from.getValue(BERRIES));
-     }
+@@ -52,8 +_,15 @@
  
-+    // Paper start - Fix Spigot growth modifiers
      @Override
-+    protected BlockState getGrowIntoState(BlockState state, RandomSource random, @javax.annotation.Nullable Level level) {
-+        final boolean value = random.nextFloat() < (level != null ? (0.11F * (level.spigotConfig.glowBerryModifier / 100.0F)) : 0.11F);
-+        return (BlockState) super.getGrowIntoState(state, random).setValue(CaveVinesBlock.BERRIES, value);
-+    }
-+    // Paper end - Fix Spigot growth modifiers
-+
-+    @Override
      protected BlockState getGrowIntoState(BlockState state, RandomSource random) {
 -        return super.getGrowIntoState(state, random).setValue(BERRIES, Boolean.valueOf(random.nextFloat() < 0.11F));
+-    }
 +        // Paper start - Fix Spigot growth modifiers
 +        return this.getGrowIntoState(state, random, null);
-     }
++    }
++    @Override
++    protected BlockState getGrowIntoState(BlockState state, RandomSource random, @javax.annotation.Nullable Level level) {
++        final boolean value = random.nextFloat() < (level != null ? (0.11F * (level.spigotConfig.glowBerryModifier / 100.0F)) : 0.11F);
++        return super.getGrowIntoState(state, random).setValue(CaveVinesBlock.BERRIES, value);
++    }
++    // Paper end - Fix Spigot growth modifiers
  
      @Override
+     protected ItemStack getCloneItemStack(LevelReader level, BlockPos pos, BlockState state, boolean includeData) {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/CeilingHangingSignBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/CeilingHangingSignBlock.java.patch
new file mode 100644
index 0000000000..f965aa92d2
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/CeilingHangingSignBlock.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/world/level/block/CeilingHangingSignBlock.java
++++ b/net/minecraft/world/level/block/CeilingHangingSignBlock.java
+@@ -184,6 +_,6 @@
+     @Nullable
+     @Override
+     public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> blockEntityType) {
+-        return createTickerHelper(blockEntityType, BlockEntityType.HANGING_SIGN, SignBlockEntity::tick);
++        return null; // CraftBukkit - remove unnecessary sign ticking
+     }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/ChangeOverTimeBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/ChangeOverTimeBlock.java.patch
new file mode 100644
index 0000000000..6601a243af
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/ChangeOverTimeBlock.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/level/block/ChangeOverTimeBlock.java
++++ b/net/minecraft/world/level/block/ChangeOverTimeBlock.java
+@@ -16,7 +_,7 @@
+     default void changeOverTime(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+         float f = 0.05688889F;
+         if (random.nextFloat() < 0.05688889F) {
+-            this.getNextState(state, level, pos, random).ifPresent(blockState -> level.setBlockAndUpdate(pos, blockState));
++            this.getNextState(state, level, pos, random).ifPresent(blockState -> org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(level, pos, blockState)); // CraftBukkit
+         }
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/ChestBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/ChestBlock.java.patch
new file mode 100644
index 0000000000..55671a0957
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/ChestBlock.java.patch
@@ -0,0 +1,79 @@
+--- a/net/minecraft/world/level/block/ChestBlock.java
++++ b/net/minecraft/world/level/block/ChestBlock.java
+@@ -120,6 +_,38 @@
+         }
+     };
+ 
++    // CraftBukkit start
++    public static class DoubleInventory implements MenuProvider {
++
++        private final ChestBlockEntity tileentitychest;
++        private final ChestBlockEntity tileentitychest1;
++        public final CompoundContainer inventorylargechest;
++
++        public DoubleInventory(ChestBlockEntity tileentitychest, ChestBlockEntity tileentitychest1, CompoundContainer inventorylargechest) {
++            this.tileentitychest = tileentitychest;
++            this.tileentitychest1 = tileentitychest1;
++            this.inventorylargechest = inventorylargechest;
++        }
++
++        @Nullable
++        @Override
++        public AbstractContainerMenu createMenu(int syncId, Inventory playerInventory, Player player) {
++            if (this.tileentitychest.canOpen(player) && this.tileentitychest1.canOpen(player)) {
++                this.tileentitychest.unpackLootTable(playerInventory.player);
++                this.tileentitychest1.unpackLootTable(playerInventory.player);
++                return ChestMenu.sixRows(syncId, playerInventory, this.inventorylargechest);
++            } else {
++                return null;
++            }
++        }
++
++        @Override
++        public Component getDisplayName() {
++            return (Component) (this.tileentitychest.hasCustomName() ? this.tileentitychest.getDisplayName() : (this.tileentitychest1.hasCustomName() ? this.tileentitychest1.getDisplayName() : Component.translatable("container.chestDouble")));
++        }
++    };
++    // CraftBukkit end
++
+     @Override
+     public MapCodec<? extends ChestBlock> codec() {
+         return CODEC;
+@@ -245,8 +_,7 @@
+     protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) {
+         if (level instanceof ServerLevel serverLevel) {
+             MenuProvider menuProvider = this.getMenuProvider(state, level, pos);
+-            if (menuProvider != null) {
+-                player.openMenu(menuProvider);
++            if (menuProvider != null && player.openMenu(menuProvider).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
+                 player.awardStat(this.getOpenChestStat());
+                 PiglinAi.angerNearbyPiglins(serverLevel, player, true);
+             }
+@@ -285,7 +_,14 @@
+     @Nullable
+     @Override
+     public MenuProvider getMenuProvider(BlockState state, Level level, BlockPos pos) {
+-        return this.combine(state, level, pos, false).apply(MENU_PROVIDER_COMBINER).orElse(null);
++    // CraftBukkit start
++        return this.getMenuProvider(state, level, pos, false);
++    }
++
++    @Nullable
++    public MenuProvider getMenuProvider(BlockState state, Level level, BlockPos pos, boolean ignoreObstructions) {
++        return this.combine(state, level, pos, ignoreObstructions).apply(MENU_PROVIDER_COMBINER).orElse(null);
++    // CraftBukkit end
+     }
+ 
+     public static DoubleBlockCombiner.Combiner<ChestBlockEntity, Float2FloatFunction> opennessCombiner(final LidBlockEntity lid) {
+@@ -328,6 +_,11 @@
+     }
+ 
+     private static boolean isCatSittingOnChest(LevelAccessor level, BlockPos pos) {
++        // Paper start - Option to disable chest cat detection
++        if (level.getMinecraftWorld().paperConfig().entities.behavior.disableChestCatDetection) {
++            return false;
++        }
++        // Paper end - Option to disable chest cat detection
+         List<Cat> entitiesOfClass = level.getEntitiesOfClass(
+             Cat.class, new AABB(pos.getX(), pos.getY() + 1, pos.getZ(), pos.getX() + 1, pos.getY() + 2, pos.getZ() + 1)
+         );
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/ChorusFlowerBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/ChorusFlowerBlock.java.patch
new file mode 100644
index 0000000000..907be03335
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/ChorusFlowerBlock.java.patch
@@ -0,0 +1,54 @@
+--- a/net/minecraft/world/level/block/ChorusFlowerBlock.java
++++ b/net/minecraft/world/level/block/ChorusFlowerBlock.java
+@@ -96,8 +_,10 @@
+                 }
+ 
+                 if (flag && allNeighborsEmpty(level, blockPos, null) && level.isEmptyBlock(pos.above(2))) {
++                    if (org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(level, pos, blockPos, this.defaultBlockState().setValue(ChorusFlowerBlock.AGE, ageValue), 2)) { // CraftBukkit - add event
+                     level.setBlock(pos, ChorusPlantBlock.getStateWithConnections(level, pos, this.plant.defaultBlockState()), 2);
+                     this.placeGrownFlower(level, blockPos, ageValue);
++                    } // CraftBukkit
+                 } else if (ageValue < 4) {
+                     int i = random.nextInt(4);
+                     if (flag1) {
+@@ -112,18 +_,28 @@
+                         if (level.isEmptyBlock(blockPos1)
+                             && level.isEmptyBlock(blockPos1.below())
+                             && allNeighborsEmpty(level, blockPos1, randomDirection.getOpposite())) {
++                            if (org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(level, pos, blockPos1, this.defaultBlockState().setValue(ChorusFlowerBlock.AGE, ageValue + 1), 2)) { // CraftBukkit - add event
+                             this.placeGrownFlower(level, blockPos1, ageValue + 1);
+                             flag2 = true;
++                            } // CraftBukkit
+                         }
+                     }
+ 
+                     if (flag2) {
+                         level.setBlock(pos, ChorusPlantBlock.getStateWithConnections(level, pos, this.plant.defaultBlockState()), 2);
+                     } else {
++                        // CraftBukkit start - add event
++                        if (org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(level, pos, this.defaultBlockState().setValue(ChorusFlowerBlock.AGE, Integer.valueOf(5)), 2)) {
+                         this.placeDeadFlower(level, pos);
++                        }
++                        // CraftBukkit end
+                     }
+                 } else {
++                    // CraftBukkit start - add event
++                    if (org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(level, pos, this.defaultBlockState().setValue(ChorusFlowerBlock.AGE, Integer.valueOf(5)), 2)) {
+                     this.placeDeadFlower(level, pos);
++                    }
++                    // CraftBukkit end
+                 }
+             }
+         }
+@@ -261,6 +_,11 @@
+     protected void onProjectileHit(Level level, BlockState state, BlockHitResult hit, Projectile projectile) {
+         BlockPos blockPos = hit.getBlockPos();
+         if (level instanceof ServerLevel serverLevel && projectile.mayInteract(serverLevel, blockPos) && projectile.mayBreak(serverLevel)) {
++            // CraftBukkit start
++            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(projectile, blockPos, state.getFluidState().createLegacyBlock())) { // Paper - fix wrong block state
++                return;
++            }
++            // CraftBukkit end
+             level.destroyBlock(blockPos, true, projectile);
+         }
+     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/ChorusPlantBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/ChorusPlantBlock.java.patch
similarity index 56%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/ChorusPlantBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/ChorusPlantBlock.java.patch
index 80c24699ea..f73493ba87 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/ChorusPlantBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/ChorusPlantBlock.java.patch
@@ -1,34 +1,34 @@
 --- a/net/minecraft/world/level/block/ChorusPlantBlock.java
 +++ b/net/minecraft/world/level/block/ChorusPlantBlock.java
-@@ -38,6 +38,7 @@
+@@ -38,6 +_,7 @@
  
      @Override
-     public BlockState getStateForPlacement(BlockPlaceContext ctx) {
+     public BlockState getStateForPlacement(BlockPlaceContext context) {
 +        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableChorusPlantUpdates) return this.defaultBlockState(); // Paper - add option to disable block updates
-         return getStateWithConnections(ctx.getLevel(), ctx.getClickedPos(), this.defaultBlockState());
+         return getStateWithConnections(context.getLevel(), context.getClickedPos(), this.defaultBlockState());
      }
  
-@@ -68,6 +69,7 @@
+@@ -68,6 +_,7 @@
          BlockState neighborState,
          RandomSource random
      ) {
 +        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableChorusPlantUpdates) return state; // Paper - add option to disable block updates
-         if (!state.canSurvive(world, pos)) {
-             tickView.scheduleTick(pos, this, 1);
-             return super.updateShape(state, world, tickView, pos, direction, neighborPos, neighborState, random);
-@@ -79,6 +81,7 @@
+         if (!state.canSurvive(level, pos)) {
+             scheduledTickAccess.scheduleTick(pos, this, 1);
+             return super.updateShape(state, level, scheduledTickAccess, pos, direction, neighborPos, neighborState, random);
+@@ -81,6 +_,7 @@
  
      @Override
-     protected void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
+     protected void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
 +        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableChorusPlantUpdates) return; // Paper - add option to disable block updates
-         if (!state.canSurvive(world, pos)) {
-             world.destroyBlock(pos, true);
+         if (!state.canSurvive(level, pos)) {
+             level.destroyBlock(pos, true);
          }
-@@ -86,6 +89,7 @@
+@@ -88,6 +_,7 @@
  
      @Override
-     protected boolean canSurvive(BlockState state, LevelReader world, BlockPos pos) {
+     protected boolean canSurvive(BlockState state, LevelReader level, BlockPos pos) {
 +        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableChorusPlantUpdates) return true; // Paper - add option to disable block updates
-         BlockState blockState = world.getBlockState(pos.below());
-         boolean bl = !world.getBlockState(pos.above()).isAir() && !blockState.isAir();
+         BlockState blockState = level.getBlockState(pos.below());
+         boolean flag = !level.getBlockState(pos.above()).isAir() && !blockState.isAir();
  
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/CocoaBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/CocoaBlock.java.patch
new file mode 100644
index 0000000000..1b46405a40
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/CocoaBlock.java.patch
@@ -0,0 +1,24 @@
+--- a/net/minecraft/world/level/block/CocoaBlock.java
++++ b/net/minecraft/world/level/block/CocoaBlock.java
+@@ -64,10 +_,10 @@
+ 
+     @Override
+     protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+-        if (level.random.nextInt(5) == 0) {
++        if (level.random.nextFloat() < (level.spigotConfig.cocoaModifier / (100.0f * 5))) { // Spigot - SPIGOT-7159: Better modifier resolution
+             int ageValue = state.getValue(AGE);
+             if (ageValue < 2) {
+-                level.setBlock(pos, state.setValue(AGE, Integer.valueOf(ageValue + 1)), 2);
++                org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(level, pos, state.setValue(CocoaBlock.AGE, ageValue + 1), 2); // CraftBukkkit
+             }
+         }
+     }
+@@ -141,7 +_,7 @@
+ 
+     @Override
+     public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) {
+-        level.setBlock(pos, state.setValue(AGE, Integer.valueOf(state.getValue(AGE) + 1)), 2);
++        org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(level, pos, state.setValue(CocoaBlock.AGE, state.getValue(CocoaBlock.AGE) + 1), 2); // CraftBukkit
+     }
+ 
+     @Override
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/CommandBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/CommandBlock.java.patch
new file mode 100644
index 0000000000..a1cc0188d9
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/CommandBlock.java.patch
@@ -0,0 +1,27 @@
+--- a/net/minecraft/world/level/block/CommandBlock.java
++++ b/net/minecraft/world/level/block/CommandBlock.java
+@@ -70,6 +_,15 @@
+ 
+     private void setPoweredAndUpdate(Level level, BlockPos pos, CommandBlockEntity blockEntity, boolean powered) {
+         boolean isPowered = blockEntity.isPowered();
++        // CraftBukkit start
++        org.bukkit.block.Block bukkitBlock = level.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ());
++        int old = isPowered ? 15 : 0;
++        int current = powered ? 15 : 0;
++
++        org.bukkit.event.block.BlockRedstoneEvent eventRedstone = new org.bukkit.event.block.BlockRedstoneEvent(bukkitBlock, old, current);
++        level.getCraftServer().getPluginManager().callEvent(eventRedstone);
++        powered = eventRedstone.getNewCurrent() > 0;
++        // CraftBukkit end
+         if (powered != isPowered) {
+             blockEntity.setPowered(powered);
+             if (powered) {
+@@ -126,7 +_,7 @@
+     @Override
+     protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) {
+         BlockEntity blockEntity = level.getBlockEntity(pos);
+-        if (blockEntity instanceof CommandBlockEntity && player.canUseGameMasterBlocks()) {
++        if (blockEntity instanceof CommandBlockEntity && (player.canUseGameMasterBlocks() || (player.isCreative() && player.getBukkitEntity().hasPermission("minecraft.commandblock")))) { // Paper - command block permission
+             player.openCommandBlock((CommandBlockEntity)blockEntity);
+             return InteractionResult.SUCCESS;
+         } else {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/ComparatorBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/ComparatorBlock.java.patch
new file mode 100644
index 0000000000..713361e2bd
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/ComparatorBlock.java.patch
@@ -0,0 +1,21 @@
+--- a/net/minecraft/world/level/block/ComparatorBlock.java
++++ b/net/minecraft/world/level/block/ComparatorBlock.java
+@@ -170,8 +_,18 @@
+             boolean shouldTurnOn = this.shouldTurnOn(level, pos, state);
+             boolean poweredValue = state.getValue(POWERED);
+             if (poweredValue && !shouldTurnOn) {
++                // CraftBukkit start
++                if (org.bukkit.craftbukkit.event.CraftEventFactory.callRedstoneChange(level, pos, 15, 0).getNewCurrent() != 0) {
++                    return;
++                }
++                // CraftBukkit end
+                 level.setBlock(pos, state.setValue(POWERED, Boolean.valueOf(false)), 2);
+             } else if (!poweredValue && shouldTurnOn) {
++                // CraftBukkit start
++                if (org.bukkit.craftbukkit.event.CraftEventFactory.callRedstoneChange(level, pos, 0, 15).getNewCurrent() != 15) {
++                    return;
++                }
++                // CraftBukkit end
+                 level.setBlock(pos, state.setValue(POWERED, Boolean.valueOf(true)), 2);
+             }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/ComposterBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/ComposterBlock.java.patch
new file mode 100644
index 0000000000..2f9e73d103
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/ComposterBlock.java.patch
@@ -0,0 +1,148 @@
+--- a/net/minecraft/world/level/block/ComposterBlock.java
++++ b/net/minecraft/world/level/block/ComposterBlock.java
+@@ -243,6 +_,11 @@
+         if (levelValue < 8 && COMPOSTABLES.containsKey(stack.getItem())) {
+             if (levelValue < 7 && !level.isClientSide) {
+                 BlockState blockState = addItem(player, state, level, pos, stack);
++                // Paper start - handle cancelled events
++                if (blockState == null) {
++                    return InteractionResult.PASS;
++                }
++                // Paper end
+                 level.levelEvent(1500, pos, state != blockState ? 1 : 0);
+                 player.awardStat(Stats.ITEM_USED.get(stack.getItem()));
+                 stack.consume(1, player);
+@@ -268,7 +_,19 @@
+     public static BlockState insertItem(Entity entity, BlockState state, ServerLevel level, ItemStack stack, BlockPos pos) {
+         int levelValue = state.getValue(LEVEL);
+         if (levelValue < 7 && COMPOSTABLES.containsKey(stack.getItem())) {
+-            BlockState blockState = addItem(entity, state, level, pos, stack);
++            // CraftBukkit start
++            double rand = level.getRandom().nextDouble();
++            BlockState blockState = null; // Paper
++            if (false && (state == blockState || !org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(entity, pos, blockState))) { // Paper - move event call into addItem
++                return state;
++            }
++            blockState = ComposterBlock.addItem(entity, state, level, pos, stack, rand);
++            // Paper start - handle cancelled events
++            if (blockState == null) {
++                return state;
++            }
++            // Paper end
++            // CraftBukkit end
+             stack.shrink(1);
+             return blockState;
+         } else {
+@@ -277,6 +_,14 @@
+     }
+ 
+     public static BlockState extractProduce(Entity entity, BlockState state, Level level, BlockPos pos) {
++        // CraftBukkit start
++        if (entity != null && !(entity instanceof Player)) {
++            BlockState iblockdata1 = ComposterBlock.empty(entity, state, org.bukkit.craftbukkit.util.DummyGeneratorAccess.INSTANCE, pos);
++            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(entity, pos, iblockdata1)) {
++                return state;
++            }
++        }
++        // CraftBukkit end
+         if (!level.isClientSide) {
+             Vec3 vec3 = Vec3.atLowerCornerWithOffset(pos, 0.5, 1.01, 0.5).offsetRandom(level.random, 0.7F);
+             ItemEntity itemEntity = new ItemEntity(level, vec3.x(), vec3.y(), vec3.z(), new ItemStack(Items.BONE_MEAL));
+@@ -296,14 +_,39 @@
+         return blockState;
+     }
+ 
++    @Nullable // Paper
+     static BlockState addItem(@Nullable Entity entity, BlockState state, LevelAccessor level, BlockPos pos, ItemStack stack) {
++        // CraftBukkit start
++        return ComposterBlock.addItem(entity, state, level, pos, stack, level.getRandom().nextDouble());
++    }
++    @Nullable // Paper - make it nullable
++    static BlockState addItem(@Nullable Entity entity, BlockState state, LevelAccessor level, BlockPos pos, ItemStack stack, double rand) {
+         int levelValue = state.getValue(LEVEL);
+         float _float = COMPOSTABLES.getFloat(stack.getItem());
+-        if ((levelValue != 0 || !(_float > 0.0F)) && !(level.getRandom().nextDouble() < _float)) {
++        // Paper start - Add CompostItemEvent and EntityCompostItemEvent
++        boolean willRaiseLevel = !((levelValue != 0 || _float <= 0.0F) && rand >= (double) _float);
++        final io.papermc.paper.event.block.CompostItemEvent event;
++        if (entity == null) {
++            event = new io.papermc.paper.event.block.CompostItemEvent(org.bukkit.craftbukkit.block.CraftBlock.at(level, pos), stack.getBukkitStack(), willRaiseLevel);
++        } else {
++            event = new io.papermc.paper.event.entity.EntityCompostItemEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos), stack.getBukkitStack(), willRaiseLevel);
++        }
++        if (!event.callEvent()) { // check for cancellation of entity event (non entity event can't be cancelled cause of hoppers)
++            return null;
++        }
++        willRaiseLevel = event.willRaiseLevel();
++
++        if (!willRaiseLevel) {
++            // Paper end - Add CompostItemEvent and EntityCompostItemEvent
+             return state;
+         } else {
+             int i = levelValue + 1;
+             BlockState blockState = state.setValue(LEVEL, Integer.valueOf(i));
++            // Paper start - move the EntityChangeBlockEvent here to avoid conflict later for the compost events
++            if (entity != null && !org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(entity, pos, blockState)) {
++                return null;
++            }
++            // Paper end
+             level.setBlock(pos, blockState, 3);
+             level.gameEvent(GameEvent.BLOCK_CHANGE, pos, GameEvent.Context.of(entity, blockState));
+             if (i == 7) {
+@@ -348,13 +_,14 @@
+         if (levelValue == 8) {
+             return new ComposterBlock.OutputContainer(state, level, pos, new ItemStack(Items.BONE_MEAL));
+         } else {
+-            return (WorldlyContainer)(levelValue < 7 ? new ComposterBlock.InputContainer(state, level, pos) : new ComposterBlock.EmptyContainer());
++            return (WorldlyContainer)(levelValue < 7 ? new ComposterBlock.InputContainer(state, level, pos) : new ComposterBlock.EmptyContainer(level, pos)); // CraftBukkit - empty generatoraccess, blockposition
+         }
+     }
+ 
+     public static class EmptyContainer extends SimpleContainer implements WorldlyContainer {
+-        public EmptyContainer() {
++        public EmptyContainer(LevelAccessor generatoraccess, BlockPos blockposition) { // CraftBukkit
+             super(0);
++            this.bukkitOwner = new org.bukkit.craftbukkit.inventory.CraftBlockInventoryHolder(generatoraccess, blockposition, this); // CraftBukkit
+         }
+ 
+         @Override
+@@ -381,9 +_,11 @@
+ 
+         public InputContainer(BlockState state, LevelAccessor level, BlockPos pos) {
+             super(1);
++            this.bukkitOwner = new org.bukkit.craftbukkit.inventory.CraftBlockInventoryHolder(level, pos, this); // CraftBukkit
+             this.state = state;
+             this.level = level;
+             this.pos = pos;
++            this.bukkitOwner = new org.bukkit.craftbukkit.inventory.CraftBlockInventoryHolder(level, pos, this); // CraftBukkit
+         }
+ 
+         @Override
+@@ -412,6 +_,11 @@
+             if (!item.isEmpty()) {
+                 this.changed = true;
+                 BlockState blockState = ComposterBlock.addItem(null, this.state, this.level, this.pos, item);
++                // Paper start - Add CompostItemEvent and EntityCompostItemEvent
++                if (blockState == null) {
++                    return;
++                }
++                // Paper end - Add CompostItemEvent and EntityCompostItemEvent
+                 this.level.levelEvent(1500, this.pos, blockState != this.state ? 1 : 0);
+                 this.removeItemNoUpdate(0);
+             }
+@@ -453,8 +_,15 @@
+ 
+         @Override
+         public void setChanged() {
++            // CraftBukkit start - allow putting items back (eg cancelled InventoryMoveItemEvent)
++            if (this.isEmpty()) {
+             ComposterBlock.empty(null, this.state, this.level, this.pos);
+             this.changed = true;
++            } else {
++                this.level.setBlock(this.pos, this.state, 3);
++                this.changed = false;
++            }
++            // CraftBukkit end
+         }
+     }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/ConcretePowderBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/ConcretePowderBlock.java.patch
new file mode 100644
index 0000000000..3c8335c0a5
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/ConcretePowderBlock.java.patch
@@ -0,0 +1,68 @@
+--- a/net/minecraft/world/level/block/ConcretePowderBlock.java
++++ b/net/minecraft/world/level/block/ConcretePowderBlock.java
+@@ -38,16 +_,33 @@
+     @Override
+     public void onLand(Level level, BlockPos pos, BlockState state, BlockState replaceableState, FallingBlockEntity fallingBlock) {
+         if (shouldSolidify(level, pos, replaceableState)) {
+-            level.setBlock(pos, this.concrete.defaultBlockState(), 3);
++            org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(level, pos, this.concrete.defaultBlockState(), 3); // CraftBukkit
+         }
+     }
+ 
+     @Override
+     public BlockState getStateForPlacement(BlockPlaceContext context) {
+-        BlockGetter level = context.getLevel();
++        Level level = context.getLevel(); // Paper
+         BlockPos clickedPos = context.getClickedPos();
+         BlockState blockState = level.getBlockState(clickedPos);
+-        return shouldSolidify(level, clickedPos, blockState) ? this.concrete.defaultBlockState() : super.getStateForPlacement(context);
++        // CraftBukkit start
++        if (!ConcretePowderBlock.shouldSolidify(level, clickedPos, blockState)) {
++            return super.getStateForPlacement(context);
++        }
++
++        // TODO: An event factory call for methods like this
++        org.bukkit.craftbukkit.block.CraftBlockState craftBlockState = org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState((net.minecraft.world.level.LevelAccessor) level, clickedPos);
++        craftBlockState.setData(this.concrete.defaultBlockState());
++
++        org.bukkit.event.block.BlockFormEvent event = new org.bukkit.event.block.BlockFormEvent(craftBlockState.getBlock(), craftBlockState);
++        level.getServer().server.getPluginManager().callEvent(event);
++
++        if (!event.isCancelled()) {
++            return craftBlockState.getHandle();
++        }
++
++        return super.getStateForPlacement(context);
++        // CraftBukkit end
+     }
+ 
+     private static boolean shouldSolidify(BlockGetter level, BlockPos pos, BlockState state) {
+@@ -88,9 +_,25 @@
+         BlockState neighborState,
+         RandomSource random
+     ) {
+-        return touchesLiquid(level, pos)
+-            ? this.concrete.defaultBlockState()
+-            : super.updateShape(state, level, scheduledTickAccess, pos, direction, neighborPos, neighborState, random);
++        // CraftBukkit start
++        if (ConcretePowderBlock.touchesLiquid(level, pos)) {
++            // Suppress during worldgen
++            if (!(level instanceof Level world1)) {
++                return this.concrete.defaultBlockState();
++            }
++            org.bukkit.craftbukkit.block.CraftBlockState blockState = org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(world1, pos);
++            blockState.setData(this.concrete.defaultBlockState());
++
++            org.bukkit.event.block.BlockFormEvent event = new org.bukkit.event.block.BlockFormEvent(blockState.getBlock(), blockState);
++            world1.getCraftServer().getPluginManager().callEvent(event);
++
++            if (!event.isCancelled()) {
++                return blockState.getHandle();
++            }
++        }
++
++        return super.updateShape(state, level, scheduledTickAccess, pos, direction, neighborPos, neighborState, random);
++        // CraftBukkit end
+     }
+ 
+     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/CoralBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/CoralBlock.java.patch
similarity index 58%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/CoralBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/CoralBlock.java.patch
index 389b3b96ec..87d67142fb 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/CoralBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/CoralBlock.java.patch
@@ -1,14 +1,14 @@
 --- a/net/minecraft/world/level/block/CoralBlock.java
 +++ b/net/minecraft/world/level/block/CoralBlock.java
-@@ -40,6 +40,11 @@
+@@ -37,6 +_,11 @@
      @Override
-     protected void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
-         if (!this.scanForWater(world, pos)) {
+     protected void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+         if (!this.scanForWater(level, pos)) {
 +            // CraftBukkit start
-+            if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(world, pos, this.deadBlock.defaultBlockState()).isCancelled()) {
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(level, pos, this.deadBlock.defaultBlockState()).isCancelled()) {
 +                return;
 +            }
 +            // CraftBukkit end
-             world.setBlock(pos, this.deadBlock.defaultBlockState(), 2);
+             level.setBlock(pos, this.deadBlock.defaultBlockState(), 2);
          }
- 
+     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/CoralFanBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/CoralFanBlock.java.patch
similarity index 55%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/CoralFanBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/CoralFanBlock.java.patch
index 295f4f0049..ff375bb06b 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/CoralFanBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/CoralFanBlock.java.patch
@@ -1,14 +1,14 @@
 --- a/net/minecraft/world/level/block/CoralFanBlock.java
 +++ b/net/minecraft/world/level/block/CoralFanBlock.java
-@@ -41,6 +41,11 @@
+@@ -38,6 +_,11 @@
      @Override
-     protected void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
-         if (!scanForWater(state, world, pos)) {
+     protected void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+         if (!scanForWater(state, level, pos)) {
 +            // CraftBukkit start
-+            if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(world, pos, this.deadBlock.defaultBlockState().setValue(CoralFanBlock.WATERLOGGED, false)).isCancelled()) {
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(level, pos, this.deadBlock.defaultBlockState().setValue(CoralFanBlock.WATERLOGGED, false)).isCancelled()) {
 +                return;
 +            }
 +            // CraftBukkit end
-             world.setBlock(pos, (BlockState) this.deadBlock.defaultBlockState().setValue(CoralFanBlock.WATERLOGGED, false), 2);
+             level.setBlock(pos, this.deadBlock.defaultBlockState().setValue(WATERLOGGED, Boolean.valueOf(false)), 2);
          }
- 
+     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/CoralPlantBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/CoralPlantBlock.java.patch
similarity index 55%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/CoralPlantBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/CoralPlantBlock.java.patch
index bccf3cd4a0..c768b4c634 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/CoralPlantBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/CoralPlantBlock.java.patch
@@ -1,14 +1,14 @@
 --- a/net/minecraft/world/level/block/CoralPlantBlock.java
 +++ b/net/minecraft/world/level/block/CoralPlantBlock.java
-@@ -46,6 +46,11 @@
+@@ -43,6 +_,11 @@
      @Override
-     protected void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
-         if (!scanForWater(state, world, pos)) {
+     protected void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+         if (!scanForWater(state, level, pos)) {
 +            // CraftBukkit start
-+            if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(world, pos, this.deadBlock.defaultBlockState().setValue(CoralPlantBlock.WATERLOGGED, false)).isCancelled()) {
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(level, pos, this.deadBlock.defaultBlockState().setValue(CoralPlantBlock.WATERLOGGED, false)).isCancelled()) {
 +                return;
 +            }
 +            // CraftBukkit end
-             world.setBlock(pos, (BlockState) this.deadBlock.defaultBlockState().setValue(CoralPlantBlock.WATERLOGGED, false), 2);
+             level.setBlock(pos, this.deadBlock.defaultBlockState().setValue(WATERLOGGED, Boolean.valueOf(false)), 2);
          }
- 
+     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/CoralWallFanBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/CoralWallFanBlock.java.patch
similarity index 53%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/CoralWallFanBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/CoralWallFanBlock.java.patch
index 172e22d8b2..7ac0a5182f 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/CoralWallFanBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/CoralWallFanBlock.java.patch
@@ -1,14 +1,14 @@
 --- a/net/minecraft/world/level/block/CoralWallFanBlock.java
 +++ b/net/minecraft/world/level/block/CoralWallFanBlock.java
-@@ -41,6 +41,11 @@
+@@ -38,6 +_,11 @@
      @Override
-     protected void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
-         if (!scanForWater(state, world, pos)) {
+     protected void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+         if (!scanForWater(state, level, pos)) {
 +            // CraftBukkit start
-+            if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(world, pos, this.deadBlock.defaultBlockState().setValue(CoralWallFanBlock.WATERLOGGED, false).setValue(CoralWallFanBlock.FACING, state.getValue(CoralWallFanBlock.FACING))).isCancelled()) {
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(level, pos, this.deadBlock.defaultBlockState().setValue(CoralWallFanBlock.WATERLOGGED, false).setValue(CoralWallFanBlock.FACING, state.getValue(CoralWallFanBlock.FACING))).isCancelled()) {
 +                return;
 +            }
 +            // CraftBukkit end
-             world.setBlock(pos, (BlockState) ((BlockState) this.deadBlock.defaultBlockState().setValue(CoralWallFanBlock.WATERLOGGED, false)).setValue(CoralWallFanBlock.FACING, (Direction) state.getValue(CoralWallFanBlock.FACING)), 2);
+             level.setBlock(pos, this.deadBlock.defaultBlockState().setValue(WATERLOGGED, Boolean.valueOf(false)).setValue(FACING, state.getValue(FACING)), 2);
          }
- 
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/CrafterBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/CrafterBlock.java.patch
new file mode 100644
index 0000000000..b592d3f859
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/CrafterBlock.java.patch
@@ -0,0 +1,76 @@
+--- a/net/minecraft/world/level/block/CrafterBlock.java
++++ b/net/minecraft/world/level/block/CrafterBlock.java
+@@ -11,6 +_,7 @@
+ import net.minecraft.server.level.ServerLevel;
+ import net.minecraft.server.level.ServerPlayer;
+ import net.minecraft.util.RandomSource;
++import net.minecraft.world.CompoundContainer;
+ import net.minecraft.world.Container;
+ import net.minecraft.world.Containers;
+ import net.minecraft.world.InteractionResult;
+@@ -159,6 +_,13 @@
+             } else {
+                 RecipeHolder<CraftingRecipe> recipeHolder = potentialResults.get();
+                 ItemStack itemStack = recipeHolder.value().assemble(var11, level.registryAccess());
++                // CraftBukkit start
++                org.bukkit.event.block.CrafterCraftEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callCrafterCraftEvent(pos, level, crafterBlockEntity, itemStack, recipeHolder);
++                if (event.isCancelled()) {
++                    return;
++                }
++                itemStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getResult());
++                // CraftBukkit end
+                 if (itemStack.isEmpty()) {
+                     level.levelEvent(1050, pos, 0);
+                 } else {
+@@ -193,7 +_,25 @@
+         Container containerAt = HopperBlockEntity.getContainerAt(level, pos.relative(direction));
+         ItemStack itemStack = stack.copy();
+         if (containerAt != null && (containerAt instanceof CrafterBlockEntity || stack.getCount() > containerAt.getMaxStackSize(stack))) {
++            // CraftBukkit start - InventoryMoveItemEvent
++            org.bukkit.craftbukkit.inventory.CraftItemStack oitemstack = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack);
++
++            org.bukkit.inventory.Inventory destinationInventory;
++            // Have to special case large chests as they work oddly
++            if (containerAt instanceof CompoundContainer) {
++                destinationInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest((CompoundContainer) containerAt);
++            } else {
++                destinationInventory = containerAt.getOwner().getInventory();
++            }
++
++            org.bukkit.event.inventory.InventoryMoveItemEvent event = new org.bukkit.event.inventory.InventoryMoveItemEvent(crafter.getOwner().getInventory(), oitemstack, destinationInventory, true);
++            level.getCraftServer().getPluginManager().callEvent(event);
++            itemStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem());
+             while (!itemStack.isEmpty()) {
++                if (event.isCancelled()) {
++                    break;
++                }
++                // CraftBukkit end
+                 ItemStack itemStack1 = itemStack.copyWithCount(1);
+                 ItemStack itemStack2 = HopperBlockEntity.addItem(crafter, containerAt, itemStack1, direction.getOpposite());
+                 if (!itemStack2.isEmpty()) {
+@@ -203,7 +_,25 @@
+                 itemStack.shrink(1);
+             }
+         } else if (containerAt != null) {
++            // CraftBukkit start - InventoryMoveItemEvent
++            org.bukkit.craftbukkit.inventory.CraftItemStack oitemstack = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack);
++
++            org.bukkit.inventory.Inventory destinationInventory;
++            // Have to special case large chests as they work oddly
++            if (containerAt instanceof CompoundContainer) {
++                destinationInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest((CompoundContainer) containerAt);
++            } else {
++                destinationInventory = containerAt.getOwner().getInventory();
++            }
++
++            org.bukkit.event.inventory.InventoryMoveItemEvent event = new org.bukkit.event.inventory.InventoryMoveItemEvent(crafter.getOwner().getInventory(), oitemstack, destinationInventory, true);
++            level.getCraftServer().getPluginManager().callEvent(event);
++            itemStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem());
+             while (!itemStack.isEmpty()) {
++                if (event.isCancelled()) {
++                    break;
++                }
++                // CraftBukkit end
+                 int count = itemStack.getCount();
+                 itemStack = HopperBlockEntity.addItem(crafter, containerAt, itemStack, direction.getOpposite());
+                 if (count == itemStack.getCount()) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/CraftingTableBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/CraftingTableBlock.java.patch
similarity index 64%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/CraftingTableBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/CraftingTableBlock.java.patch
index e611ea83b5..336a3679c6 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/CraftingTableBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/CraftingTableBlock.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/world/level/block/CraftingTableBlock.java
 +++ b/net/minecraft/world/level/block/CraftingTableBlock.java
-@@ -31,8 +31,9 @@
+@@ -31,8 +_,9 @@
      @Override
-     protected InteractionResult useWithoutItem(BlockState state, Level world, BlockPos pos, Player player, BlockHitResult hit) {
-         if (!world.isClientSide) {
--            player.openMenu(state.getMenuProvider(world, pos));
-+            if (player.openMenu(state.getMenuProvider(world, pos)).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
+     protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) {
+         if (!level.isClientSide) {
+-            player.openMenu(state.getMenuProvider(level, pos));
++            if (player.openMenu(state.getMenuProvider(level, pos)).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
              player.awardStat(Stats.INTERACT_WITH_CRAFTING_TABLE);
 +            } // Paper - Fix InventoryOpenEvent cancellation
          }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/CropBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/CropBlock.java.patch
new file mode 100644
index 0000000000..0897bcc22d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/CropBlock.java.patch
@@ -0,0 +1,49 @@
+--- a/net/minecraft/world/level/block/CropBlock.java
++++ b/net/minecraft/world/level/block/CropBlock.java
+@@ -88,8 +_,25 @@
+             int age = this.getAge(state);
+             if (age < this.getMaxAge()) {
+                 float growthSpeed = getGrowthSpeed(this, level, pos);
+-                if (random.nextInt((int)(25.0F / growthSpeed) + 1) == 0) {
+-                    level.setBlock(pos, this.getStateForAge(age + 1), 2);
++                // Spigot start
++                int modifier = 100;
++                if (this == Blocks.BEETROOTS) {
++                    modifier = level.spigotConfig.beetrootModifier;
++                } else if (this == Blocks.CARROTS) {
++                    modifier = level.spigotConfig.carrotModifier;
++                } else if (this == Blocks.POTATOES) {
++                    modifier = level.spigotConfig.potatoModifier;
++                // Paper start - Fix Spigot growth modifiers
++                } else if (this == Blocks.TORCHFLOWER_CROP) {
++                    modifier = level.spigotConfig.torchFlowerModifier;
++                // Paper end - Fix Spigot growth modifiers
++                } else if (this == Blocks.WHEAT) {
++                    modifier = level.spigotConfig.wheatModifier;
++                }
++
++                if (random.nextFloat() < (modifier / (100.0f * (Math.floor((25.0F / growthSpeed) + 1))))) { // Spigot - SPIGOT-7159: Better modifier resolution
++                    // Spigot end
++                    org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(level, pos, this.getStateForAge(age + 1), 2); // CraftBukkit
+                 }
+             }
+         }
+@@ -102,7 +_,7 @@
+             i = maxAge;
+         }
+ 
+-        level.setBlock(pos, this.getStateForAge(i), 2);
++        org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(level, pos, this.getStateForAge(i), 2); // CraftBukkit
+     }
+ 
+     protected int getBonemealAgeIncrease(Level level) {
+@@ -164,7 +_,8 @@
+ 
+     @Override
+     protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
+-        if (level instanceof ServerLevel serverLevel && entity instanceof Ravager && serverLevel.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) {
++        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
++        if (level instanceof ServerLevel serverLevel && entity instanceof Ravager && org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(entity, pos, Blocks.AIR.defaultBlockState(), !serverLevel.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING))) { // CraftBukkit
+             serverLevel.destroyBlock(pos, true, entity);
+         }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/DaylightDetectorBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/DaylightDetectorBlock.java.patch
new file mode 100644
index 0000000000..eee537faaa
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/DaylightDetectorBlock.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/world/level/block/DaylightDetectorBlock.java
++++ b/net/minecraft/world/level/block/DaylightDetectorBlock.java
+@@ -70,6 +_,7 @@
+ 
+         i = Mth.clamp(i, 0, 15);
+         if (state.getValue(POWER) != i) {
++            i = org.bukkit.craftbukkit.event.CraftEventFactory.callRedstoneChange(level, pos, state.getValue(DaylightDetectorBlock.POWER), i).getNewCurrent(); // CraftBukkit - Call BlockRedstoneEvent
+             level.setBlock(pos, state.setValue(POWER, Integer.valueOf(i)), 3);
+         }
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/DecoratedPotBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/DecoratedPotBlock.java.patch
new file mode 100644
index 0000000000..492d3d7e78
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/DecoratedPotBlock.java.patch
@@ -0,0 +1,14 @@
+--- a/net/minecraft/world/level/block/DecoratedPotBlock.java
++++ b/net/minecraft/world/level/block/DecoratedPotBlock.java
+@@ -237,6 +_,11 @@
+     protected void onProjectileHit(Level level, BlockState state, BlockHitResult hit, Projectile projectile) {
+         BlockPos blockPos = hit.getBlockPos();
+         if (level instanceof ServerLevel serverLevel && projectile.mayInteract(serverLevel, blockPos) && projectile.mayBreak(serverLevel)) {
++            // CraftBukkit start - call EntityChangeBlockEvent
++            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(projectile, blockPos, this.getFluidState(state).createLegacyBlock())) {
++                return;
++            }
++            // CraftBukkit end
+             level.setBlock(blockPos, state.setValue(CRACKED, Boolean.valueOf(true)), 4);
+             level.destroyBlock(blockPos, true, projectile);
+         }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/DetectorRailBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/DetectorRailBlock.java.patch
new file mode 100644
index 0000000000..d65731b15f
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/DetectorRailBlock.java.patch
@@ -0,0 +1,35 @@
+--- a/net/minecraft/world/level/block/DetectorRailBlock.java
++++ b/net/minecraft/world/level/block/DetectorRailBlock.java
+@@ -54,6 +_,7 @@
+ 
+     @Override
+     protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
++        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
+         if (!level.isClientSide) {
+             if (!state.getValue(POWERED)) {
+                 this.checkPressed(level, pos, state);
+@@ -84,6 +_,7 @@
+ 
+     private void checkPressed(Level level, BlockPos pos, BlockState state) {
+         if (this.canSurvive(state, level, pos)) {
++            if (!state.is(this)) { return; } // Paper - Fix some rails connecting improperly
+             boolean poweredValue = state.getValue(POWERED);
+             boolean flag = false;
+             List<AbstractMinecart> interactingMinecartOfType = this.getInteractingMinecartOfType(level, pos, AbstractMinecart.class, entity -> true);
+@@ -91,6 +_,16 @@
+                 flag = true;
+             }
+ 
++            // CraftBukkit start
++            if (poweredValue != flag) {
++                org.bukkit.block.Block block = level.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ());
++
++                org.bukkit.event.block.BlockRedstoneEvent eventRedstone = new org.bukkit.event.block.BlockRedstoneEvent(block, flag ? 15 : 0, flag ? 15 : 0);
++                level.getCraftServer().getPluginManager().callEvent(eventRedstone);
++
++                flag = eventRedstone.getNewCurrent() > 0;
++            }
++            // CraftBukkit end
+             if (flag && !poweredValue) {
+                 BlockState blockState = state.setValue(POWERED, Boolean.valueOf(true));
+                 level.setBlock(pos, blockState, 3);
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/DiodeBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/DiodeBlock.java.patch
new file mode 100644
index 0000000000..bde27499c8
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/DiodeBlock.java.patch
@@ -0,0 +1,21 @@
+--- a/net/minecraft/world/level/block/DiodeBlock.java
++++ b/net/minecraft/world/level/block/DiodeBlock.java
+@@ -56,8 +_,18 @@
+             boolean poweredValue = state.getValue(POWERED);
+             boolean shouldTurnOn = this.shouldTurnOn(level, pos, state);
+             if (poweredValue && !shouldTurnOn) {
++                // CraftBukkit start
++                if (org.bukkit.craftbukkit.event.CraftEventFactory.callRedstoneChange(level, pos, 15, 0).getNewCurrent() != 0) {
++                    return;
++                }
++                // CraftBukkit end
+                 level.setBlock(pos, state.setValue(POWERED, Boolean.valueOf(false)), 2);
+             } else if (!poweredValue) {
++                // CraftBukkit start
++                if (org.bukkit.craftbukkit.event.CraftEventFactory.callRedstoneChange(level, pos, 0, 15).getNewCurrent() != 15) {
++                    return;
++                }
++                // CraftBukkit end
+                 level.setBlock(pos, state.setValue(POWERED, Boolean.valueOf(true)), 2);
+                 if (!shouldTurnOn) {
+                     level.scheduleTick(pos, this, this.getDelay(state), TickPriority.VERY_HIGH);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/DirtPathBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/DirtPathBlock.java.patch
similarity index 61%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/DirtPathBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/DirtPathBlock.java.patch
index b394c4adfa..bc73d1c956 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/DirtPathBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/DirtPathBlock.java.patch
@@ -1,14 +1,14 @@
 --- a/net/minecraft/world/level/block/DirtPathBlock.java
 +++ b/net/minecraft/world/level/block/DirtPathBlock.java
-@@ -51,6 +51,11 @@
+@@ -60,6 +_,11 @@
  
      @Override
-     protected void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
+     protected void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
 +        // CraftBukkit start - do not fade if the block is valid here
-+        if (state.canSurvive(world, pos)) {
++        if (state.canSurvive(level, pos)) {
 +            return;
 +        }
 +        // CraftBukkit end
-         FarmBlock.turnToDirt((Entity) null, state, world, pos);
+         FarmBlock.turnToDirt(null, state, level, pos);
      }
  
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/DispenserBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/DispenserBlock.java.patch
new file mode 100644
index 0000000000..2bce500d42
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/DispenserBlock.java.patch
@@ -0,0 +1,48 @@
+--- a/net/minecraft/world/level/block/DispenserBlock.java
++++ b/net/minecraft/world/level/block/DispenserBlock.java
+@@ -50,6 +_,7 @@
+     private static final DefaultDispenseItemBehavior DEFAULT_BEHAVIOR = new DefaultDispenseItemBehavior();
+     public static final Map<Item, DispenseItemBehavior> DISPENSER_REGISTRY = new IdentityHashMap<>();
+     private static final int TRIGGER_DURATION = 4;
++    public static boolean eventFired = false; // CraftBukkit
+ 
+     @Override
+     public MapCodec<? extends DispenserBlock> codec() {
+@@ -71,8 +_,7 @@
+ 
+     @Override
+     protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) {
+-        if (!level.isClientSide && level.getBlockEntity(pos) instanceof DispenserBlockEntity dispenserBlockEntity) {
+-            player.openMenu(dispenserBlockEntity);
++        if (!level.isClientSide && level.getBlockEntity(pos) instanceof DispenserBlockEntity dispenserBlockEntity && player.openMenu(dispenserBlockEntity).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
+             player.awardStat(dispenserBlockEntity instanceof DropperBlockEntity ? Stats.INSPECT_DROPPER : Stats.INSPECT_DISPENSER);
+         }
+ 
+@@ -87,18 +_,27 @@
+             BlockSource blockSource = new BlockSource(level, pos, state, dispenserBlockEntity);
+             int randomSlot = dispenserBlockEntity.getRandomSlot(level.random);
+             if (randomSlot < 0) {
++                if (org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFailedDispenseEvent(level, pos)) { // Paper - Add BlockFailedDispenseEvent
+                 level.levelEvent(1001, pos, 0);
+                 level.gameEvent(GameEvent.BLOCK_ACTIVATE, pos, GameEvent.Context.of(dispenserBlockEntity.getBlockState()));
++                } // Paper - Add BlockFailedDispenseEvent
+             } else {
+                 ItemStack item = dispenserBlockEntity.getItem(randomSlot);
+                 DispenseItemBehavior dispenseMethod = this.getDispenseMethod(level, item);
+                 if (dispenseMethod != DispenseItemBehavior.NOOP) {
++                    if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockPreDispenseEvent(level, pos, item, randomSlot)) return; // Paper - Add BlockPreDispenseEvent
++                    DispenserBlock.eventFired = false; // CraftBukkit - reset event status
+                     dispenserBlockEntity.setItem(randomSlot, dispenseMethod.dispense(blockSource, item));
+                 }
+             }
+         }
+     }
+ 
++    // Paper start - Fix NPE with equippable and items without behavior
++    public static DispenseItemBehavior getDispenseBehavior(BlockSource pointer, ItemStack stack) {
++        return ((DispenserBlock) pointer.state().getBlock()).getDispenseMethod(pointer.level(), stack);
++    }
++    // Paper end - Fix NPE with equippable and items without behavior
+     protected DispenseItemBehavior getDispenseMethod(Level level, ItemStack item) {
+         if (!item.isItemEnabled(level.enabledFeatures())) {
+             return DEFAULT_BEHAVIOR;
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/DoorBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/DoorBlock.java.patch
new file mode 100644
index 0000000000..03420b6376
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/DoorBlock.java.patch
@@ -0,0 +1,28 @@
+--- a/net/minecraft/world/level/block/DoorBlock.java
++++ b/net/minecraft/world/level/block/DoorBlock.java
+@@ -229,9 +_,22 @@
+ 
+     @Override
+     protected void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighborBlock, @Nullable Orientation orientation, boolean movedByPiston) {
+-        boolean flag = level.hasNeighborSignal(pos)
+-            || level.hasNeighborSignal(pos.relative(state.getValue(HALF) == DoubleBlockHalf.LOWER ? Direction.UP : Direction.DOWN));
+-        if (!this.defaultBlockState().is(neighborBlock) && flag != state.getValue(POWERED)) {
++        // CraftBukkit start
++        BlockPos otherHalf = pos.relative(state.getValue(DoorBlock.HALF) == DoubleBlockHalf.LOWER ? Direction.UP : Direction.DOWN);
++        org.bukkit.block.Block bukkitBlock = org.bukkit.craftbukkit.block.CraftBlock.at(level, pos);
++        org.bukkit.block.Block blockTop = org.bukkit.craftbukkit.block.CraftBlock.at(level, otherHalf);
++
++        int power = bukkitBlock.getBlockPower();
++        int powerTop = blockTop.getBlockPower();
++        if (powerTop > power) power = powerTop;
++        int oldPower = state.getValue(DoorBlock.POWERED) ? net.minecraft.world.level.redstone.Redstone.SIGNAL_MAX : net.minecraft.world.level.redstone.Redstone.SIGNAL_MIN;
++
++        if (oldPower == 0 ^ power == 0) {
++            org.bukkit.event.block.BlockRedstoneEvent event = new org.bukkit.event.block.BlockRedstoneEvent(bukkitBlock, oldPower, power);
++            event.callEvent();
++
++            boolean flag = event.getNewCurrent() > 0;
++            // CraftBukkit end
+             if (flag != state.getValue(OPEN)) {
+                 this.playSound(null, level, pos, flag);
+                 level.gameEvent(null, flag ? GameEvent.BLOCK_OPEN : GameEvent.BLOCK_CLOSE, pos);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/DoubleBlockCombiner.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/DoubleBlockCombiner.java.patch
similarity index 63%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/DoubleBlockCombiner.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/DoubleBlockCombiner.java.patch
index 657fe3fb50..3416e7d85d 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/DoubleBlockCombiner.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/DoubleBlockCombiner.java.patch
@@ -1,16 +1,16 @@
 --- a/net/minecraft/world/level/block/DoubleBlockCombiner.java
 +++ b/net/minecraft/world/level/block/DoubleBlockCombiner.java
-@@ -34,7 +34,12 @@
+@@ -34,7 +_,12 @@
                  return new DoubleBlockCombiner.NeighborCombineResult.Single<>(blockEntity);
              } else {
-                 BlockPos blockPos = pos.relative(directionMapper.apply(state));
--                BlockState blockState = world.getBlockState(blockPos);
+                 BlockPos blockPos = pos.relative(directionGetter.apply(state));
+-                BlockState blockState = level.getBlockState(blockPos);
 +                // Paper start - Don't load Chunks from Hoppers and other things
-+                BlockState blockState = world.getBlockStateIfLoaded(blockPos);
++                BlockState blockState = level.getBlockStateIfLoaded(blockPos);
 +                if (blockState == null) {
 +                    return new DoubleBlockCombiner.NeighborCombineResult.Single<>(blockEntity);
 +                }
 +                // Paper end - Don't load Chunks from Hoppers and other things
                  if (blockState.is(state.getBlock())) {
-                     DoubleBlockCombiner.BlockType blockType2 = typeMapper.apply(blockState);
-                     if (blockType2 != DoubleBlockCombiner.BlockType.SINGLE
+                     DoubleBlockCombiner.BlockType blockType1 = doubleBlockTypeGetter.apply(blockState);
+                     if (blockType1 != DoubleBlockCombiner.BlockType.SINGLE
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/DoublePlantBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/DoublePlantBlock.java.patch
new file mode 100644
index 0000000000..f57b086e6a
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/DoublePlantBlock.java.patch
@@ -0,0 +1,21 @@
+--- a/net/minecraft/world/level/block/DoublePlantBlock.java
++++ b/net/minecraft/world/level/block/DoublePlantBlock.java
+@@ -112,11 +_,16 @@
+     }
+ 
+     @Override
+-    public void playerDestroy(Level level, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity te, ItemStack stack) {
+-        super.playerDestroy(level, player, pos, Blocks.AIR.defaultBlockState(), te, stack);
++    public void playerDestroy(Level level, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity te, ItemStack stack, boolean includeDrops, boolean dropExp) { // Paper - fix drops not preventing stats/food exhaustion
++        super.playerDestroy(level, player, pos, Blocks.AIR.defaultBlockState(), te, stack, includeDrops, dropExp); // Paper - fix drops not preventing stats/food exhaustion;
+     }
+ 
+     protected static void preventDropFromBottomPart(Level level, BlockPos pos, BlockState state, Player player) {
++        // CraftBukkit start
++        if (((net.minecraft.server.level.ServerLevel)level).hasPhysicsEvent && org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(level, pos).isCancelled()) { // Paper
++            return;
++        }
++        // CraftBukkit end
+         DoubleBlockHalf doubleBlockHalf = state.getValue(HALF);
+         if (doubleBlockHalf == DoubleBlockHalf.UPPER) {
+             BlockPos blockPos = pos.below();
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/DragonEggBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/DragonEggBlock.java.patch
new file mode 100644
index 0000000000..235b049f77
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/DragonEggBlock.java.patch
@@ -0,0 +1,19 @@
+--- a/net/minecraft/world/level/block/DragonEggBlock.java
++++ b/net/minecraft/world/level/block/DragonEggBlock.java
+@@ -55,6 +_,16 @@
+                 level.random.nextInt(16) - level.random.nextInt(16)
+             );
+             if (level.getBlockState(blockPos).isAir() && worldBorder.isWithinBounds(blockPos)) {
++                // CraftBukkit start
++                org.bukkit.block.Block from = org.bukkit.craftbukkit.block.CraftBlock.at(level, pos);
++                org.bukkit.block.Block to = org.bukkit.craftbukkit.block.CraftBlock.at(level, blockPos);
++                org.bukkit.event.block.BlockFromToEvent event = new org.bukkit.event.block.BlockFromToEvent(from, to);
++                if (!event.callEvent()) {
++                    return;
++                }
++
++                blockPos = new BlockPos(event.getToBlock().getX(), event.getToBlock().getY(), event.getToBlock().getZ());
++                // CraftBukkit end
+                 if (level.isClientSide) {
+                     for (int i1 = 0; i1 < 128; i1++) {
+                         double randomDouble = level.random.nextDouble();
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/DropExperienceBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/DropExperienceBlock.java.patch
similarity index 71%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/DropExperienceBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/DropExperienceBlock.java.patch
index 4d0a4875d4..1487606ce5 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/DropExperienceBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/DropExperienceBlock.java.patch
@@ -1,11 +1,12 @@
 --- a/net/minecraft/world/level/block/DropExperienceBlock.java
 +++ b/net/minecraft/world/level/block/DropExperienceBlock.java
-@@ -31,9 +31,16 @@
+@@ -31,8 +_,16 @@
      @Override
-     protected void spawnAfterBreak(BlockState state, ServerLevel world, BlockPos pos, ItemStack tool, boolean dropExperience) {
-         super.spawnAfterBreak(state, world, pos, tool, dropExperience);
+     protected void spawnAfterBreak(BlockState state, ServerLevel level, BlockPos pos, ItemStack stack, boolean dropExperience) {
+         super.spawnAfterBreak(state, level, pos, stack, dropExperience);
 -        if (dropExperience) {
--            this.tryDropExperience(world, pos, tool, this.xpRange);
+-            this.tryDropExperience(level, pos, stack, this.xpRange);
+-        }
 +        // CraftBukkit start - Delegate to getExpDrop
 +    }
 +
@@ -13,8 +14,8 @@
 +    public int getExpDrop(BlockState iblockdata, ServerLevel worldserver, BlockPos blockposition, ItemStack itemstack, boolean flag) {
 +        if (flag) {
 +            return this.tryDropExperience(worldserver, blockposition, itemstack, this.xpRange);
-         }
- 
++         }
++
 +        return 0;
 +        // CraftBukkit end
      }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/DropperBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/DropperBlock.java.patch
new file mode 100644
index 0000000000..047ee9babb
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/DropperBlock.java.patch
@@ -0,0 +1,59 @@
+--- a/net/minecraft/world/level/block/DropperBlock.java
++++ b/net/minecraft/world/level/block/DropperBlock.java
+@@ -8,6 +_,7 @@
+ import net.minecraft.core.dispenser.DefaultDispenseItemBehavior;
+ import net.minecraft.core.dispenser.DispenseItemBehavior;
+ import net.minecraft.server.level.ServerLevel;
++import net.minecraft.world.CompoundContainer;
+ import net.minecraft.world.Container;
+ import net.minecraft.world.item.ItemStack;
+ import net.minecraft.world.level.Level;
+@@ -23,7 +_,7 @@
+ public class DropperBlock extends DispenserBlock {
+     private static final Logger LOGGER = LogUtils.getLogger();
+     public static final MapCodec<DropperBlock> CODEC = simpleCodec(DropperBlock::new);
+-    private static final DispenseItemBehavior DISPENSE_BEHAVIOUR = new DefaultDispenseItemBehavior();
++    private static final DispenseItemBehavior DISPENSE_BEHAVIOUR = new DefaultDispenseItemBehavior(true); // CraftBukkit
+ 
+     @Override
+     public MapCodec<DropperBlock> codec() {
+@@ -53,6 +_,7 @@
+             BlockSource blockSource = new BlockSource(level, pos, state, dispenserBlockEntity);
+             int randomSlot = dispenserBlockEntity.getRandomSlot(level.random);
+             if (randomSlot < 0) {
++                if (org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFailedDispenseEvent(level, pos)) // Paper - Add BlockFailedDispenseEvent
+                 level.levelEvent(1001, pos, 0);
+             } else {
+                 ItemStack item = dispenserBlockEntity.getItem(randomSlot);
+@@ -61,10 +_,29 @@
+                     Container containerAt = HopperBlockEntity.getContainerAt(level, pos.relative(direction));
+                     ItemStack itemStack;
+                     if (containerAt == null) {
++                        if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockPreDispenseEvent(level, pos, item, randomSlot)) return; // Paper - Add BlockPreDispenseEvent
+                         itemStack = DISPENSE_BEHAVIOUR.dispense(blockSource, item);
+                     } else {
+-                        itemStack = HopperBlockEntity.addItem(dispenserBlockEntity, containerAt, item.copyWithCount(1), direction.getOpposite());
+-                        if (itemStack.isEmpty()) {
++                        // CraftBukkit start - Fire event when pushing items into other inventories
++                        org.bukkit.craftbukkit.inventory.CraftItemStack oitemstack = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1));
++
++                        org.bukkit.inventory.Inventory destinationInventory;
++                        // Have to special case large chests as they work oddly
++                        if (containerAt instanceof CompoundContainer) {
++                            destinationInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest((CompoundContainer) containerAt);
++                        } else {
++                            destinationInventory = containerAt.getOwner().getInventory();
++                        }
++
++                        org.bukkit.event.inventory.InventoryMoveItemEvent event = new org.bukkit.event.inventory.InventoryMoveItemEvent(containerAt.getOwner().getInventory(), oitemstack, destinationInventory, true);
++                        level.getCraftServer().getPluginManager().callEvent(event);
++                        if (event.isCancelled()) {
++                            return;
++                        }
++                        itemStack = HopperBlockEntity.addItem(dispenserBlockEntity, containerAt, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem()), direction.getOpposite());
++                        if (event.getItem().equals(oitemstack) && itemStack.isEmpty()) {
++                            // CraftBukkit end
++
+                             itemStack = item.copy();
+                             itemStack.shrink(1);
+                         } else {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/EndGatewayBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/EndGatewayBlock.java.patch
new file mode 100644
index 0000000000..2b564a8c31
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/EndGatewayBlock.java.patch
@@ -0,0 +1,30 @@
+--- a/net/minecraft/world/level/block/EndGatewayBlock.java
++++ b/net/minecraft/world/level/block/EndGatewayBlock.java
+@@ -89,10 +_,15 @@
+ 
+     @Override
+     protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
++        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
+         if (entity.canUsePortal(false)
+             && !level.isClientSide
+             && level.getBlockEntity(pos) instanceof TheEndGatewayBlockEntity theEndGatewayBlockEntity
+             && !theEndGatewayBlockEntity.isCoolingDown()) {
++            // Paper start - call EntityPortalEnterEvent
++            org.bukkit.event.entity.EntityPortalEnterEvent event = new org.bukkit.event.entity.EntityPortalEnterEvent(entity.getBukkitEntity(), new org.bukkit.Location(level.getWorld(), pos.getX(), pos.getY(), pos.getZ()), org.bukkit.PortalType.END_GATEWAY); // Paper - add portal type
++            if (!event.callEvent()) return;
++            // Paper end - call EntityPortalEnterEvent
+             entity.setAsInsidePortal(this, pos);
+             TheEndGatewayBlockEntity.triggerCooldown(level, pos, state, theEndGatewayBlockEntity);
+         }
+@@ -107,9 +_,9 @@
+                 return null;
+             } else {
+                 return entity instanceof ThrownEnderpearl
+-                    ? new TeleportTransition(level, portalPosition, Vec3.ZERO, 0.0F, 0.0F, Set.of(), TeleportTransition.PLACE_PORTAL_TICKET)
++                    ? new TeleportTransition(level, portalPosition, Vec3.ZERO, 0.0F, 0.0F, Set.of(), TeleportTransition.PLACE_PORTAL_TICKET, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_GATEWAY) // CraftBukkit
+                     : new TeleportTransition(
+-                        level, portalPosition, Vec3.ZERO, 0.0F, 0.0F, Relative.union(Relative.DELTA, Relative.ROTATION), TeleportTransition.PLACE_PORTAL_TICKET
++                        level, portalPosition, Vec3.ZERO, 0.0F, 0.0F, Relative.union(Relative.DELTA, Relative.ROTATION), TeleportTransition.PLACE_PORTAL_TICKET, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_GATEWAY // CraftBukkit
+                     );
+             }
+         } else {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/EndPortalBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/EndPortalBlock.java.patch
new file mode 100644
index 0000000000..436949f5f8
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/EndPortalBlock.java.patch
@@ -0,0 +1,62 @@
+--- a/net/minecraft/world/level/block/EndPortalBlock.java
++++ b/net/minecraft/world/level/block/EndPortalBlock.java
+@@ -56,8 +_,15 @@
+ 
+     @Override
+     protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
++        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
+         if (entity.canUsePortal(false)) {
++            // CraftBukkit start - Entity in portal
++            org.bukkit.event.entity.EntityPortalEnterEvent event = new org.bukkit.event.entity.EntityPortalEnterEvent(entity.getBukkitEntity(), new org.bukkit.Location(level.getWorld(), pos.getX(), pos.getY(), pos.getZ()), org.bukkit.PortalType.ENDER); // Paper - add portal type
++            level.getCraftServer().getPluginManager().callEvent(event);
++            if (event.isCancelled()) return; // Paper - make cancellable
++            // CraftBukkit end
+             if (!level.isClientSide && level.dimension() == Level.END && entity instanceof ServerPlayer serverPlayer && !serverPlayer.seenCredits) {
++                if (level.paperConfig().misc.disableEndCredits) {serverPlayer.seenCredits = true; return;} // Paper - Option to disable end credits
+                 serverPlayer.showEndCredits();
+             } else {
+                 entity.setAsInsidePortal(this, pos);
+@@ -67,7 +_,7 @@
+ 
+     @Override
+     public TeleportTransition getPortalDestination(ServerLevel level, Entity entity, BlockPos pos) {
+-        ResourceKey<Level> resourceKey = level.dimension() == Level.END ? Level.OVERWORLD : Level.END;
++        ResourceKey<Level> resourceKey = level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.END ? Level.OVERWORLD : Level.END; // CraftBukkit - SPIGOT-6152: send back to main overworld in custom ends
+         ServerLevel level1 = level.getServer().getLevel(resourceKey);
+         if (level1 == null) {
+             return null;
+@@ -78,7 +_,7 @@
+             float f;
+             Set<Relative> set;
+             if (flag) {
+-                EndPlatformFeature.createEndPlatform(level1, BlockPos.containing(bottomCenter).below(), true);
++                EndPlatformFeature.createEndPlatform(level1, BlockPos.containing(bottomCenter).below(), true, entity); // CraftBukkit
+                 f = Direction.WEST.toYRot();
+                 set = Relative.union(Relative.DELTA, Set.of(Relative.X_ROT));
+                 if (entity instanceof ServerPlayer) {
+@@ -88,15 +_,21 @@
+                 f = 0.0F;
+                 set = Relative.union(Relative.DELTA, Relative.ROTATION);
+                 if (entity instanceof ServerPlayer serverPlayer) {
+-                    return serverPlayer.findRespawnPositionAndUseSpawnBlock(false, TeleportTransition.DO_NOTHING);
++                    return serverPlayer.findRespawnPositionAndUseSpawnBlock(false, TeleportTransition.DO_NOTHING, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason.END_PORTAL); // CraftBukkit
+                 }
+ 
+                 bottomCenter = entity.adjustSpawnLocation(level1, blockPos).getBottomCenter();
+             }
+ 
+-            return new TeleportTransition(
+-                level1, bottomCenter, Vec3.ZERO, f, 0.0F, set, TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET)
+-            );
++            // CraftBukkit start
++            org.bukkit.craftbukkit.event.CraftPortalEvent event = entity.callPortalEvent(entity, org.bukkit.craftbukkit.util.CraftLocation.toBukkit(bottomCenter, level1.getWorld(), f, entity.getXRot()), org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_PORTAL, 0, 0);
++            if (event == null) {
++                return null;
++            }
++            org.bukkit.Location to = event.getTo();
++
++            return new TeleportTransition(((org.bukkit.craftbukkit.CraftWorld) to.getWorld()).getHandle(), org.bukkit.craftbukkit.util.CraftLocation.toVec3D(to), entity.getDeltaMovement(), to.getYaw(), to.getPitch(), set, TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET), org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_PORTAL);
++            // CraftBukkit end
+         }
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/EnderChestBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/EnderChestBlock.java.patch
new file mode 100644
index 0000000000..82a4a56c86
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/EnderChestBlock.java.patch
@@ -0,0 +1,26 @@
+--- a/net/minecraft/world/level/block/EnderChestBlock.java
++++ b/net/minecraft/world/level/block/EnderChestBlock.java
+@@ -78,16 +_,15 @@
+         PlayerEnderChestContainer enderChestInventory = player.getEnderChestInventory();
+         if (enderChestInventory != null && level.getBlockEntity(pos) instanceof EnderChestBlockEntity enderChestBlockEntity) {
+             BlockPos blockPos = pos.above();
+-            if (level.getBlockState(blockPos).isRedstoneConductor(level, blockPos)) {
++            if (level.getBlockState(blockPos).isRedstoneConductor(level, blockPos)) { // Paper - diff on change; make sure that EnderChest#isBlocked uses the same logic
+                 return InteractionResult.SUCCESS;
+             } else {
+-                if (level instanceof ServerLevel serverLevel) {
+-                    enderChestInventory.setActiveChest(enderChestBlockEntity);
+-                    player.openMenu(
+-                        new SimpleMenuProvider(
+-                            (containerId, playerInventory, player1) -> ChestMenu.threeRows(containerId, playerInventory, enderChestInventory), CONTAINER_TITLE
+-                        )
+-                    );
++                // Paper start - Fix InventoryOpenEvent cancellation - moved up;
++                enderChestInventory.setActiveChest(enderChestBlockEntity); // Needs to happen before ChestMenu.threeRows as it is required for opening animations
++                if (level instanceof ServerLevel serverLevel && player.openMenu(
++                    new SimpleMenuProvider((i, inventory, playerx) -> ChestMenu.threeRows(i, inventory, enderChestInventory), CONTAINER_TITLE)
++                ).isPresent()) {
++                // Paper end - Fix InventoryOpenEvent cancellation - moved up;
+                     player.awardStat(Stats.OPEN_ENDERCHEST);
+                     PiglinAi.angerNearbyPiglins(serverLevel, player, true);
+                 }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/EyeblossomBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/EyeblossomBlock.java.patch
similarity index 59%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/EyeblossomBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/EyeblossomBlock.java.patch
index 0f677874ee..a7765e31cc 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/EyeblossomBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/EyeblossomBlock.java.patch
@@ -1,10 +1,10 @@
 --- a/net/minecraft/world/level/block/EyeblossomBlock.java
 +++ b/net/minecraft/world/level/block/EyeblossomBlock.java
-@@ -100,6 +100,7 @@
+@@ -99,6 +_,7 @@
  
      @Override
-     protected void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
-+        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
-         if (!world.isClientSide()
-             && world.getDifficulty() != Difficulty.PEACEFUL
+     protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
++        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
+         if (!level.isClientSide()
+             && level.getDifficulty() != Difficulty.PEACEFUL
              && entity instanceof Bee bee
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/FarmBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/FarmBlock.java.patch
new file mode 100644
index 0000000000..fb670445d2
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/FarmBlock.java.patch
@@ -0,0 +1,93 @@
+--- a/net/minecraft/world/level/block/FarmBlock.java
++++ b/net/minecraft/world/level/block/FarmBlock.java
+@@ -95,31 +_,56 @@
+     @Override
+     protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+         int moistureValue = state.getValue(MOISTURE);
++        if (moistureValue > 0 && level.paperConfig().tickRates.wetFarmland != 1 && (level.paperConfig().tickRates.wetFarmland < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % level.paperConfig().tickRates.wetFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks
++        if (moistureValue == 0 && level.paperConfig().tickRates.dryFarmland != 1 && (level.paperConfig().tickRates.dryFarmland < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % level.paperConfig().tickRates.dryFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks
+         if (!isNearWater(level, pos) && !level.isRainingAt(pos.above())) {
+             if (moistureValue > 0) {
+-                level.setBlock(pos, state.setValue(MOISTURE, Integer.valueOf(moistureValue - 1)), 2);
++                org.bukkit.craftbukkit.event.CraftEventFactory.handleMoistureChangeEvent(level, pos, state.setValue(FarmBlock.MOISTURE, moistureValue - 1), 2); // CraftBukkit
+             } else if (!shouldMaintainFarmland(level, pos)) {
+                 turnToDirt(null, state, level, pos);
+             }
+         } else if (moistureValue < 7) {
+-            level.setBlock(pos, state.setValue(MOISTURE, Integer.valueOf(7)), 2);
++            org.bukkit.craftbukkit.event.CraftEventFactory.handleMoistureChangeEvent(level, pos, state.setValue(FarmBlock.MOISTURE, 7), 2); // CraftBukkit
+         }
+     }
+ 
+     @Override
+     public void fallOn(Level level, BlockState state, BlockPos pos, Entity entity, float fallDistance) {
++        super.fallOn(level, state, pos, entity, fallDistance); // CraftBukkit - moved here as game rules / events shouldn't affect fall damage.
+         if (level instanceof ServerLevel serverLevel
+             && level.random.nextFloat() < fallDistance - 0.5F
+             && entity instanceof LivingEntity
+             && (entity instanceof Player || serverLevel.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING))
+             && entity.getBbWidth() * entity.getBbWidth() * entity.getBbHeight() > 0.512F) {
++                // CraftBukkit start - Interact soil
++                org.bukkit.event.Cancellable cancellable;
++                if (entity instanceof Player) {
++                    cancellable = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerInteractEvent((Player) entity, org.bukkit.event.block.Action.PHYSICAL, pos, null, null, null);
++                } else {
++                    cancellable = new org.bukkit.event.entity.EntityInteractEvent(entity.getBukkitEntity(), level.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ()));
++                    level.getCraftServer().getPluginManager().callEvent((org.bukkit.event.entity.EntityInteractEvent) cancellable);
++                }
++
++                if (cancellable.isCancelled()) {
++                    return;
++                }
++
++                if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(entity, pos, Blocks.DIRT.defaultBlockState())) {
++                    return;
++                }
++                // CraftBukkit end
+             turnToDirt(entity, state, level, pos);
+         }
+ 
+-        super.fallOn(level, state, pos, entity, fallDistance);
++        // super.fallOn(level, state, pos, entity, fallDistance); // CraftBukkit - moved up
+     }
+ 
+     public static void turnToDirt(@Nullable Entity entity, BlockState state, Level level, BlockPos pos) {
++        // CraftBukkit start
++        if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(level, pos, Blocks.DIRT.defaultBlockState()).isCancelled()) {
++            return;
++        }
++        // CraftBukkit end
+         BlockState blockState = pushEntitiesUp(state, Blocks.DIRT.defaultBlockState(), level, pos);
+         level.setBlockAndUpdate(pos, blockState);
+         level.gameEvent(GameEvent.BLOCK_CHANGE, pos, GameEvent.Context.of(entity, blockState));
+@@ -130,13 +_,27 @@
+     }
+ 
+     private static boolean isNearWater(LevelReader level, BlockPos pos) {
+-        for (BlockPos blockPos : BlockPos.betweenClosed(pos.offset(-4, 0, -4), pos.offset(4, 1, 4))) {
+-            if (level.getFluidState(blockPos).is(FluidTags.WATER)) {
+-                return true;
++        // Paper start - Perf: remove abstract block iteration
++        int xOff = pos.getX();
++        int yOff = pos.getY();
++        int zOff = pos.getZ();
++        for (int dz = -4; dz <= 4; ++dz) {
++            int z = dz + zOff;
++            for (int dx = -4; dx <= 4; ++dx) {
++                int x = xOff + dx;
++                for (int dy = 0; dy <= 1; ++dy) {
++                    int y = dy + yOff;
++                    net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)level.getChunk(x >> 4, z >> 4);
++                    net.minecraft.world.level.material.FluidState fluid = chunk.getBlockStateFinal(x, y, z).getFluidState();
++                    if (fluid.is(FluidTags.WATER)) {
++                        return true;
++                    }
++                }
+             }
+         }
+ 
+         return false;
++        // Paper end - Perf: remove abstract block iteration
+     }
+ 
+     @Override
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/FenceGateBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/FenceGateBlock.java.patch
new file mode 100644
index 0000000000..718a99aac2
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/FenceGateBlock.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/level/block/FenceGateBlock.java
++++ b/net/minecraft/world/level/block/FenceGateBlock.java
+@@ -213,6 +_,17 @@
+     protected void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighborBlock, @Nullable Orientation orientation, boolean movedByPiston) {
+         if (!level.isClientSide) {
+             boolean hasNeighborSignal = level.hasNeighborSignal(pos);
++            // CraftBukkit start
++            boolean oldPowered = state.getValue(FenceGateBlock.POWERED);
++            if (oldPowered != hasNeighborSignal) {
++                int newPower = hasNeighborSignal ? 15 : 0;
++                int oldPower = oldPowered ? 15 : 0;
++                org.bukkit.block.Block bukkitBlock = org.bukkit.craftbukkit.block.CraftBlock.at(level, pos);
++                org.bukkit.event.block.BlockRedstoneEvent eventRedstone = new org.bukkit.event.block.BlockRedstoneEvent(bukkitBlock, oldPower, newPower);
++                level.getCraftServer().getPluginManager().callEvent(eventRedstone);
++                hasNeighborSignal = eventRedstone.getNewCurrent() > 0;
++            }
++            // CraftBukkit end
+             if (state.getValue(POWERED) != hasNeighborSignal) {
+                 level.setBlock(pos, state.setValue(POWERED, Boolean.valueOf(hasNeighborSignal)).setValue(OPEN, Boolean.valueOf(hasNeighborSignal)), 2);
+                 if (state.getValue(OPEN) != hasNeighborSignal) {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/FireBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/FireBlock.java.patch
new file mode 100644
index 0000000000..ebd1c8425d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/FireBlock.java.patch
@@ -0,0 +1,169 @@
+--- a/net/minecraft/world/level/block/FireBlock.java
++++ b/net/minecraft/world/level/block/FireBlock.java
+@@ -14,6 +_,7 @@
+ import net.minecraft.tags.BiomeTags;
+ import net.minecraft.util.RandomSource;
+ import net.minecraft.world.item.context.BlockPlaceContext;
++import net.minecraft.world.item.context.UseOnContext;
+ import net.minecraft.world.level.BlockGetter;
+ import net.minecraft.world.level.GameRules;
+ import net.minecraft.world.level.Level;
+@@ -122,7 +_,25 @@
+         BlockState neighborState,
+         RandomSource random
+     ) {
+-        return this.canSurvive(state, level, pos) ? this.getStateWithAge(level, pos, state.getValue(AGE)) : Blocks.AIR.defaultBlockState();
++        // CraftBukkit start
++        if (!(level instanceof ServerLevel)) return this.canSurvive(state, level, pos) ? (BlockState) this.getStateWithAge(level, pos, (Integer) state.getValue(FireBlock.AGE)) : Blocks.AIR.defaultBlockState(); // Paper - don't fire events in world generation
++        if (!this.canSurvive(state, level, pos)) {
++            // Suppress during worldgen
++            if (!(level instanceof Level world1)) {
++                return Blocks.AIR.defaultBlockState();
++            }
++            org.bukkit.craftbukkit.block.CraftBlockState blockState = org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(world1, pos);
++            blockState.setData(Blocks.AIR.defaultBlockState());
++
++            org.bukkit.event.block.BlockFadeEvent event = new org.bukkit.event.block.BlockFadeEvent(blockState.getBlock(), blockState);
++            world1.getCraftServer().getPluginManager().callEvent(event);
++
++            if (!event.isCancelled()) {
++                return blockState.getHandle();
++            }
++        }
++        return this.getStateWithAge(level, pos, (Integer) state.getValue(FireBlock.AGE)); // Paper - don't fire events in world generation; diff on change, see "don't fire events in world generation"
++        // CraftBukkit end
+     }
+ 
+     @Override
+@@ -162,10 +_,10 @@
+ 
+     @Override
+     protected void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+-        level.scheduleTick(pos, this, getFireTickDelay(level.random));
++        level.scheduleTick(pos, (Block) this, FireBlock.getFireTickDelay(level)); // Paper - Add fire-tick-delay option
+         if (level.getGameRules().getBoolean(GameRules.RULE_DOFIRETICK)) {
+             if (!state.canSurvive(level, pos)) {
+-                level.removeBlock(pos, false);
++                this.fireExtinguished(level, pos); // CraftBukkit - invalid place location
+             }
+ 
+             BlockState blockState = level.getBlockState(pos.below());
+@@ -184,26 +_,28 @@
+                     if (!this.isValidFireLocation(level, pos)) {
+                         BlockPos blockPos = pos.below();
+                         if (!level.getBlockState(blockPos).isFaceSturdy(level, blockPos, Direction.UP) || ageValue > 3) {
+-                            level.removeBlock(pos, false);
++                            this.fireExtinguished(level, pos); // CraftBukkit - extinguished by rain
+                         }
+ 
+                         return;
+                     }
+ 
+                     if (ageValue == 15 && random.nextInt(4) == 0 && !this.canBurn(level.getBlockState(pos.below()))) {
+-                        level.removeBlock(pos, false);
++                        this.fireExtinguished(level, pos); // CraftBukkit
+                         return;
+                     }
+                 }
+ 
+                 boolean isIncreasedFireBurnout = level.getBiome(pos).is(BiomeTags.INCREASED_FIRE_BURNOUT);
+                 int i = isIncreasedFireBurnout ? -50 : 0;
+-                this.checkBurnOut(level, pos.east(), 300 + i, random, ageValue);
+-                this.checkBurnOut(level, pos.west(), 300 + i, random, ageValue);
+-                this.checkBurnOut(level, pos.below(), 250 + i, random, ageValue);
+-                this.checkBurnOut(level, pos.above(), 250 + i, random, ageValue);
+-                this.checkBurnOut(level, pos.north(), 300 + i, random, ageValue);
+-                this.checkBurnOut(level, pos.south(), 300 + i, random, ageValue);
++                // CraftBukkit start - add source blockposition to burn calls
++                this.checkBurnOut(level, pos.east(), 300 + i, random, ageValue, pos);
++                this.checkBurnOut(level, pos.west(), 300 + i, random, ageValue, pos);
++                this.checkBurnOut(level, pos.below(), 250 + i, random, ageValue, pos);
++                this.checkBurnOut(level, pos.above(), 250 + i, random, ageValue, pos);
++                this.checkBurnOut(level, pos.north(), 300 + i, random, ageValue, pos);
++                this.checkBurnOut(level, pos.south(), 300 + i, random, ageValue, pos);
++                // CraftBukkit end
+                 BlockPos.MutableBlockPos mutableBlockPos = new BlockPos.MutableBlockPos();
+ 
+                 for (int i1 = -1; i1 <= 1; i1++) {
+@@ -225,7 +_,15 @@
+ 
+                                     if (i5 > 0 && random.nextInt(i4) <= i5 && (!level.isRaining() || !this.isNearRain(level, mutableBlockPos))) {
+                                         int min1 = Math.min(15, ageValue + random.nextInt(5) / 4);
+-                                        level.setBlock(mutableBlockPos, this.getStateWithAge(level, mutableBlockPos, min1), 3);
++                                        // CraftBukkit start - Call to stop spread of fire
++                                        if (level.getBlockState(mutableBlockPos).getBlock() != Blocks.FIRE) {
++                                            if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(level, mutableBlockPos, pos).isCancelled()) {
++                                                continue;
++                                            }
++
++                                            org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(level, pos, mutableBlockPos, this.getStateWithAge(level, mutableBlockPos, min1), 3); // CraftBukkit
++                                        }
++                                        // CraftBukkit end
+                                     }
+                                 }
+                             }
+@@ -256,19 +_,42 @@
+             : this.igniteOdds.getInt(state.getBlock());
+     }
+ 
+-    private void checkBurnOut(Level level, BlockPos pos, int chance, RandomSource random, int age) {
++    private void checkBurnOut(Level level, BlockPos pos, int chance, RandomSource random, int age, BlockPos sourceposition) { // CraftBukkit add sourceposition
+         int burnOdds = this.getBurnOdds(level.getBlockState(pos));
+         if (random.nextInt(chance) < burnOdds) {
+             BlockState blockState = level.getBlockState(pos);
++
++            // CraftBukkit start
++            org.bukkit.block.Block theBlock = level.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ());
++            org.bukkit.block.Block sourceBlock = level.getWorld().getBlockAt(sourceposition.getX(), sourceposition.getY(), sourceposition.getZ());
++
++            org.bukkit.event.block.BlockBurnEvent event = new org.bukkit.event.block.BlockBurnEvent(theBlock, sourceBlock);
++            level.getCraftServer().getPluginManager().callEvent(event);
++
++            if (event.isCancelled()) {
++                return;
++            }
++
++            if (blockState.getBlock() instanceof TntBlock && !org.bukkit.craftbukkit.event.CraftEventFactory.callTNTPrimeEvent(level, pos, org.bukkit.event.block.TNTPrimeEvent.PrimeCause.FIRE, null, sourceposition)) {
++                return;
++            }
++            // CraftBukkit end
+             if (random.nextInt(age + 10) < 5 && !level.isRainingAt(pos)) {
+                 int min = Math.min(age + random.nextInt(5) / 4, 15);
+                 level.setBlock(pos, this.getStateWithAge(level, pos, min), 3);
+             } else {
+-                level.removeBlock(pos, false);
++                if (!blockState.is(Blocks.TNT)) level.removeBlock(pos, false); // Paper - TNTPrimeEvent; We might be cancelling it below, move the setAir down
+             }
+ 
+             Block block = blockState.getBlock();
+             if (block instanceof TntBlock) {
++                // Paper start - TNTPrimeEvent
++                org.bukkit.block.Block tntBlock = org.bukkit.craftbukkit.block.CraftBlock.at(level, pos);
++                if (!new com.destroystokyo.paper.event.block.TNTPrimeEvent(tntBlock, com.destroystokyo.paper.event.block.TNTPrimeEvent.PrimeReason.FIRE, null).callEvent()) {
++                    return;
++                }
++                level.removeBlock(pos, false);
++                // Paper end - TNTPrimeEvent
+                 TntBlock.explode(level, pos);
+             }
+         }
+@@ -310,13 +_,14 @@
+     }
+ 
+     @Override
+-    protected void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean isMoving) {
+-        super.onPlace(state, level, pos, oldState, isMoving);
+-        level.scheduleTick(pos, this, getFireTickDelay(level.random));
++    protected void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean isMoving, UseOnContext context) {
++        super.onPlace(state, level, pos, oldState, isMoving, context);
++        // CraftBukkit end
++        level.scheduleTick(pos, this, FireBlock.getFireTickDelay(level)); // Paper - Add fire-tick-delay option
+     }
+ 
+-    private static int getFireTickDelay(RandomSource random) {
+-        return 30 + random.nextInt(10);
++    private static int getFireTickDelay(Level world) { // Paper - Add fire-tick-delay option
++        return world.paperConfig().environment.fireTickDelay + world.random.nextInt(10); // Paper - Add fire-tick-delay option
+     }
+ 
+     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/FlowerPotBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/FlowerPotBlock.java.patch
similarity index 90%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/FlowerPotBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/FlowerPotBlock.java.patch
index a2ee2e43ae..26f504f933 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/FlowerPotBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/FlowerPotBlock.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/world/level/block/FlowerPotBlock.java
 +++ b/net/minecraft/world/level/block/FlowerPotBlock.java
-@@ -63,6 +63,18 @@
+@@ -67,6 +_,18 @@
          } else if (!this.isEmpty()) {
              return InteractionResult.CONSUME;
          } else {
 +            // Paper start - Add PlayerFlowerPotManipulateEvent
-+            org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(world, pos);
++            org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(level, pos);
 +            org.bukkit.inventory.ItemStack placedStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitCopy(stack);
 +
 +            io.papermc.paper.event.player.PlayerFlowerPotManipulateEvent event = new io.papermc.paper.event.player.PlayerFlowerPotManipulateEvent((org.bukkit.entity.Player) player.getBukkitEntity(), block, placedStack, true);
@@ -16,15 +16,15 @@
 +                return InteractionResult.CONSUME;
 +            }
 +            // Paper end - Add PlayerFlowerPotManipulateEvent
-             world.setBlock(pos, blockState, 3);
-             world.gameEvent(player, GameEvent.BLOCK_CHANGE, pos);
+             level.setBlock(pos, blockState, 3);
+             level.gameEvent(player, GameEvent.BLOCK_CHANGE, pos);
              player.awardStat(Stats.POT_FLOWER);
-@@ -77,6 +89,18 @@
+@@ -81,6 +_,18 @@
              return InteractionResult.CONSUME;
          } else {
              ItemStack itemStack = new ItemStack(this.potted);
 +            // Paper start - Add PlayerFlowerPotManipulateEvent
-+            org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(world, pos);
++            org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(level, pos);
 +            org.bukkit.inventory.ItemStack pottedStack = new org.bukkit.inventory.ItemStack(org.bukkit.craftbukkit.block.CraftBlockType.minecraftToBukkit(this.potted));
 +
 +            io.papermc.paper.event.player.PlayerFlowerPotManipulateEvent event = new io.papermc.paper.event.player.PlayerFlowerPotManipulateEvent((org.bukkit.entity.Player) player.getBukkitEntity(), block, pottedStack, false);
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/FrogspawnBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/FrogspawnBlock.java.patch
new file mode 100644
index 0000000000..961a62ea3e
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/FrogspawnBlock.java.patch
@@ -0,0 +1,31 @@
+--- a/net/minecraft/world/level/block/FrogspawnBlock.java
++++ b/net/minecraft/world/level/block/FrogspawnBlock.java
+@@ -89,6 +_,7 @@
+ 
+     @Override
+     protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
++        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
+         if (entity.getType().equals(EntityType.FALLING_BLOCK)) {
+             this.destroyBlock(level, pos);
+         }
+@@ -101,6 +_,11 @@
+     }
+ 
+     private void hatchFrogspawn(ServerLevel level, BlockPos pos, RandomSource random) {
++        // Paper start - Call BlockFadeEvent
++        if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(level, pos, Blocks.AIR.defaultBlockState()).isCancelled()) {
++            return;
++        }
++        // Paper end - Call BlockFadeEven
+         this.destroyBlock(level, pos);
+         level.playSound(null, pos, SoundEvents.FROGSPAWN_HATCH, SoundSource.BLOCKS, 1.0F, 1.0F);
+         this.spawnTadpoles(level, pos, random);
+@@ -121,7 +_,7 @@
+                 int randomInt1 = random.nextInt(1, 361);
+                 tadpole.moveTo(d, pos.getY() - 0.5, d1, randomInt1, 0.0F);
+                 tadpole.setPersistenceRequired();
+-                level.addFreshEntity(tadpole);
++                level.addFreshEntity(tadpole, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.EGG); // Paper - use correct spawn reason
+             }
+         }
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/FrostedIceBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/FrostedIceBlock.java.patch
new file mode 100644
index 0000000000..0f1075049a
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/FrostedIceBlock.java.patch
@@ -0,0 +1,24 @@
+--- a/net/minecraft/world/level/block/FrostedIceBlock.java
++++ b/net/minecraft/world/level/block/FrostedIceBlock.java
+@@ -42,6 +_,7 @@
+ 
+     @Override
+     protected void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
++        if (!level.paperConfig().environment.frostedIce.enabled) return; // Paper - Frosted ice options
+         if ((random.nextInt(3) == 0 || this.fewerNeigboursThan(level, pos, 4))
+             && level.getMaxLocalRawBrightness(pos) > 11 - state.getValue(AGE) - state.getLightBlock()
+             && this.slightlyMelt(state, level, pos)) {
+@@ -51,11 +_,11 @@
+                 mutableBlockPos.setWithOffset(pos, direction);
+                 BlockState blockState = level.getBlockState(mutableBlockPos);
+                 if (blockState.is(this) && !this.slightlyMelt(blockState, level, mutableBlockPos)) {
+-                    level.scheduleTick(mutableBlockPos, this, Mth.nextInt(random, 20, 40));
++                    level.scheduleTick(mutableBlockPos, this, Mth.nextInt(random, level.paperConfig().environment.frostedIce.delay.min, level.paperConfig().environment.frostedIce.delay.max)); // Paper - Frosted ice options
+                 }
+             }
+         } else {
+-            level.scheduleTick(pos, this, Mth.nextInt(random, 20, 40));
++            level.scheduleTick(pos, this, Mth.nextInt(random, level.paperConfig().environment.frostedIce.delay.min, level.paperConfig().environment.frostedIce.delay.max)); // Paper - Frosted ice options
+         }
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/FungusBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/FungusBlock.java.patch
new file mode 100644
index 0000000000..1e6860b8a1
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/FungusBlock.java.patch
@@ -0,0 +1,21 @@
+--- a/net/minecraft/world/level/block/FungusBlock.java
++++ b/net/minecraft/world/level/block/FungusBlock.java
+@@ -72,6 +_,17 @@
+ 
+     @Override
+     public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) {
+-        this.getFeature(level).ifPresent(holder -> holder.value().place(level, level.getChunkSource().getGenerator(), random, pos));
++        this.getFeature(level)
++            // CraftBukkit start
++            .map((value) -> {
++                if (this == Blocks.WARPED_FUNGUS) {
++                    SaplingBlock.treeType = org.bukkit.TreeType.WARPED_FUNGUS;
++                } else if (this == Blocks.CRIMSON_FUNGUS) {
++                    SaplingBlock.treeType = org.bukkit.TreeType.CRIMSON_FUNGUS;
++                }
++                return value;
++            })
++            .ifPresent(holder -> holder.value().place(level, level.getChunkSource().getGenerator(), random, pos));
++        // CraftBukkit end
+     }
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/FurnaceBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/FurnaceBlock.java.patch
similarity index 77%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/FurnaceBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/FurnaceBlock.java.patch
index ffebc4893d..c383a4b8f2 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/FurnaceBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/FurnaceBlock.java.patch
@@ -1,9 +1,9 @@
 --- a/net/minecraft/world/level/block/FurnaceBlock.java
 +++ b/net/minecraft/world/level/block/FurnaceBlock.java
-@@ -45,8 +45,7 @@
+@@ -45,8 +_,7 @@
      @Override
-     protected void openContainer(Level world, BlockPos pos, Player player) {
-         BlockEntity blockEntity = world.getBlockEntity(pos);
+     protected void openContainer(Level level, BlockPos pos, Player player) {
+         BlockEntity blockEntity = level.getBlockEntity(pos);
 -        if (blockEntity instanceof FurnaceBlockEntity) {
 -            player.openMenu((MenuProvider)blockEntity);
 +        if (blockEntity instanceof FurnaceBlockEntity && player.openMenu((MenuProvider)blockEntity).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/GrindstoneBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/GrindstoneBlock.java.patch
new file mode 100644
index 0000000000..f242c126d6
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/GrindstoneBlock.java.patch
@@ -0,0 +1,12 @@
+--- a/net/minecraft/world/level/block/GrindstoneBlock.java
++++ b/net/minecraft/world/level/block/GrindstoneBlock.java
+@@ -151,8 +_,7 @@
+ 
+     @Override
+     protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) {
+-        if (!level.isClientSide) {
+-            player.openMenu(state.getMenuProvider(level, pos));
++        if (!level.isClientSide && player.openMenu(state.getMenuProvider(level, pos)).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
+             player.awardStat(Stats.INTERACT_WITH_GRINDSTONE);
+         }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/GrowingPlantHeadBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/GrowingPlantHeadBlock.java.patch
new file mode 100644
index 0000000000..997e408b2f
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/GrowingPlantHeadBlock.java.patch
@@ -0,0 +1,36 @@
+--- a/net/minecraft/world/level/block/GrowingPlantHeadBlock.java
++++ b/net/minecraft/world/level/block/GrowingPlantHeadBlock.java
+@@ -44,13 +_,31 @@
+ 
+     @Override
+     protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+-        if (state.getValue(AGE) < 25 && random.nextDouble() < this.growPerTickProbability) {
++        // Spigot start
++        int modifier = 100;
++        if (this == Blocks.KELP) {
++            modifier = level.spigotConfig.kelpModifier;
++        } else if (this == Blocks.TWISTING_VINES) {
++            modifier = level.spigotConfig.twistingVinesModifier;
++        } else if (this == Blocks.WEEPING_VINES) {
++            modifier = level.spigotConfig.weepingVinesModifier;
++        } else if (this == Blocks.CAVE_VINES) {
++            modifier = level.spigotConfig.caveVinesModifier;
++        }
++        if (state.getValue(AGE) < 25 && random.nextDouble() < ((modifier / 100.0D) * this.growPerTickProbability)) { // Spigot - SPIGOT-7159: Better modifier resolution
++            // Spigot end
+             BlockPos blockPos = pos.relative(this.growthDirection);
+             if (this.canGrowInto(level.getBlockState(blockPos))) {
+-                level.setBlockAndUpdate(blockPos, this.getGrowIntoState(state, level.random));
++                org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(level, pos, blockPos, this.getGrowIntoState(state, level.random, level)); // CraftBukkit // Paper - Fix Spigot growth modifiers
+             }
+         }
+     }
++
++    // Paper start - Fix Spigot growth modifiers
++    protected BlockState getGrowIntoState(BlockState state, RandomSource random, @javax.annotation.Nullable Level level) {
++        return this.getGrowIntoState(state, random);
++    }
++    // Paper end - Fix Spigot growth modifiers
+ 
+     protected BlockState getGrowIntoState(BlockState state, RandomSource random) {
+         return state.cycle(AGE);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/HoneyBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/HoneyBlock.java.patch
similarity index 75%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/HoneyBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/HoneyBlock.java.patch
index 4609ae3eee..36a0794c0b 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/HoneyBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/HoneyBlock.java.patch
@@ -1,10 +1,10 @@
 --- a/net/minecraft/world/level/block/HoneyBlock.java
 +++ b/net/minecraft/world/level/block/HoneyBlock.java
-@@ -60,6 +60,7 @@
+@@ -60,6 +_,7 @@
  
      @Override
-     protected void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
-+        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
+     protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
++        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
          if (this.isSlidingDown(pos, entity)) {
              this.maybeDoSlideAchievement(entity, pos);
              this.doSlideMovement(entity);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/HopperBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/HopperBlock.java.patch
similarity index 61%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/HopperBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/HopperBlock.java.patch
index 3e829e33d0..b33d6bde71 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/HopperBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/HopperBlock.java.patch
@@ -1,20 +1,20 @@
 --- a/net/minecraft/world/level/block/HopperBlock.java
 +++ b/net/minecraft/world/level/block/HopperBlock.java
-@@ -125,8 +125,7 @@
+@@ -125,8 +_,7 @@
  
      @Override
-     protected InteractionResult useWithoutItem(BlockState state, Level world, BlockPos pos, Player player, BlockHitResult hit) {
--        if (!world.isClientSide && world.getBlockEntity(pos) instanceof HopperBlockEntity hopperBlockEntity) {
+     protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) {
+-        if (!level.isClientSide && level.getBlockEntity(pos) instanceof HopperBlockEntity hopperBlockEntity) {
 -            player.openMenu(hopperBlockEntity);
-+        if (!world.isClientSide && world.getBlockEntity(pos) instanceof HopperBlockEntity hopperBlockEntity && player.openMenu(hopperBlockEntity).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
++        if (!level.isClientSide && level.getBlockEntity(pos) instanceof HopperBlockEntity hopperBlockEntity && player.openMenu(hopperBlockEntity).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
              player.awardStat(Stats.INSPECT_HOPPER);
          }
  
-@@ -178,6 +177,7 @@
+@@ -178,6 +_,7 @@
  
      @Override
-     protected void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
-+        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
-         BlockEntity blockEntity = world.getBlockEntity(pos);
+     protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
++        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
+         BlockEntity blockEntity = level.getBlockEntity(pos);
          if (blockEntity instanceof HopperBlockEntity) {
-             HopperBlockEntity.entityInside(world, pos, state, entity, (HopperBlockEntity)blockEntity);
+             HopperBlockEntity.entityInside(level, pos, state, entity, (HopperBlockEntity)blockEntity);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/HugeMushroomBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/HugeMushroomBlock.java.patch
similarity index 66%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/HugeMushroomBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/HugeMushroomBlock.java.patch
index fb60ae21d7..9a4c3e64c9 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/HugeMushroomBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/HugeMushroomBlock.java.patch
@@ -1,30 +1,30 @@
 --- a/net/minecraft/world/level/block/HugeMushroomBlock.java
 +++ b/net/minecraft/world/level/block/HugeMushroomBlock.java
-@@ -45,6 +45,7 @@
+@@ -45,6 +_,7 @@
  
      @Override
-     public BlockState getStateForPlacement(BlockPlaceContext ctx) {
+     public BlockState getStateForPlacement(BlockPlaceContext context) {
 +        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableMushroomBlockUpdates) return this.defaultBlockState(); // Paper - add option to disable block updates
-         BlockGetter blockGetter = ctx.getLevel();
-         BlockPos blockPos = ctx.getClickedPos();
+         BlockGetter level = context.getLevel();
+         BlockPos clickedPos = context.getClickedPos();
          return this.defaultBlockState()
-@@ -67,6 +68,7 @@
+@@ -67,6 +_,7 @@
          BlockState neighborState,
          RandomSource random
      ) {
 +        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableMushroomBlockUpdates) return state; // Paper - add option to disable block updates
          return neighborState.is(this)
              ? state.setValue(PROPERTY_BY_DIRECTION.get(direction), Boolean.valueOf(false))
-             : super.updateShape(state, world, tickView, pos, direction, neighborPos, neighborState, random);
-@@ -74,6 +76,7 @@
+             : super.updateShape(state, level, scheduledTickAccess, pos, direction, neighborPos, neighborState, random);
+@@ -74,6 +_,7 @@
  
      @Override
-     protected BlockState rotate(BlockState state, Rotation rotation) {
+     protected BlockState rotate(BlockState state, Rotation rot) {
 +        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableMushroomBlockUpdates) return state; // Paper - add option to disable block updates
-         return state.setValue(PROPERTY_BY_DIRECTION.get(rotation.rotate(Direction.NORTH)), state.getValue(NORTH))
-             .setValue(PROPERTY_BY_DIRECTION.get(rotation.rotate(Direction.SOUTH)), state.getValue(SOUTH))
-             .setValue(PROPERTY_BY_DIRECTION.get(rotation.rotate(Direction.EAST)), state.getValue(EAST))
-@@ -84,6 +87,7 @@
+         return state.setValue(PROPERTY_BY_DIRECTION.get(rot.rotate(Direction.NORTH)), state.getValue(NORTH))
+             .setValue(PROPERTY_BY_DIRECTION.get(rot.rotate(Direction.SOUTH)), state.getValue(SOUTH))
+             .setValue(PROPERTY_BY_DIRECTION.get(rot.rotate(Direction.EAST)), state.getValue(EAST))
+@@ -84,6 +_,7 @@
  
      @Override
      protected BlockState mirror(BlockState state, Mirror mirror) {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/IceBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/IceBlock.java.patch
new file mode 100644
index 0000000000..b89beb8bf6
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/IceBlock.java.patch
@@ -0,0 +1,30 @@
+--- a/net/minecraft/world/level/block/IceBlock.java
++++ b/net/minecraft/world/level/block/IceBlock.java
+@@ -32,8 +_,13 @@
+     }
+ 
+     @Override
+-    public void playerDestroy(Level level, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity te, ItemStack stack) {
+-        super.playerDestroy(level, player, pos, state, te, stack);
++    public void playerDestroy(Level level, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity te, ItemStack stack, boolean includeDrops, boolean dropExp) { // Paper - fix drops not preventing stats/food exhaustion
++        super.playerDestroy(level, player, pos, state, te, stack, includeDrops, dropExp); // Paper - fix drops not preventing stats/food exhaustion
++        // Paper start - Improve Block#breakNaturally API
++        this.afterDestroy(level, pos, stack);
++    }
++    public void afterDestroy(Level level, BlockPos pos, ItemStack stack) {
++        // Paper end - Improve Block#breakNaturally API
+         if (!EnchantmentHelper.hasTag(stack, EnchantmentTags.PREVENTS_ICE_MELTING)) {
+             if (level.dimensionType().ultraWarm()) {
+                 level.removeBlock(pos, false);
+@@ -55,6 +_,11 @@
+     }
+ 
+     protected void melt(BlockState state, Level level, BlockPos pos) {
++        // CraftBukkit start
++        if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(level, pos, level.dimensionType().ultraWarm() ? Blocks.AIR.defaultBlockState() : Blocks.WATER.defaultBlockState()).isCancelled()) {
++            return;
++        }
++        // CraftBukkit end
+         if (level.dimensionType().ultraWarm()) {
+             level.removeBlock(pos, false);
+         } else {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/InfestedBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/InfestedBlock.java.patch
new file mode 100644
index 0000000000..23815e6bfd
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/InfestedBlock.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/level/block/InfestedBlock.java
++++ b/net/minecraft/world/level/block/InfestedBlock.java
+@@ -52,7 +_,7 @@
+         Silverfish silverfish = EntityType.SILVERFISH.create(level, EntitySpawnReason.TRIGGERED);
+         if (silverfish != null) {
+             silverfish.moveTo(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5, 0.0F, 0.0F);
+-            level.addFreshEntity(silverfish);
++            level.addFreshEntity(silverfish, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.SILVERFISH_BLOCK); // CraftBukkit - add SpawnReason
+             silverfish.spawnAnim();
+         }
+     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/LavaCauldronBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/LavaCauldronBlock.java.patch
similarity index 74%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/LavaCauldronBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/LavaCauldronBlock.java.patch
index b8db542f5e..4c96395fdf 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/LavaCauldronBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/LavaCauldronBlock.java.patch
@@ -1,10 +1,10 @@
 --- a/net/minecraft/world/level/block/LavaCauldronBlock.java
 +++ b/net/minecraft/world/level/block/LavaCauldronBlock.java
-@@ -32,6 +32,7 @@
+@@ -32,6 +_,7 @@
  
      @Override
-     protected void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
-+        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
+     protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
++        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
          if (this.isEntityInsideContent(state, pos, entity)) {
              entity.lavaHurt();
          }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/LayeredCauldronBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/LayeredCauldronBlock.java.patch
new file mode 100644
index 0000000000..f4af9face8
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/LayeredCauldronBlock.java.patch
@@ -0,0 +1,98 @@
+--- a/net/minecraft/world/level/block/LayeredCauldronBlock.java
++++ b/net/minecraft/world/level/block/LayeredCauldronBlock.java
+@@ -61,35 +_,68 @@
+ 
+     @Override
+     protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
++        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
+         if (level instanceof ServerLevel serverLevel && entity.isOnFire() && this.isEntityInsideContent(state, pos, entity)) {
+-            entity.clearFire();
+-            if (entity.mayInteract(serverLevel, pos)) {
+-                this.handleEntityOnFireInside(state, level, pos);
++            // CraftBukkit start - moved down
++            // entity.clearFire();
++            if ((entity instanceof net.minecraft.world.entity.player.Player || serverLevel.getGameRules().getBoolean(net.minecraft.world.level.GameRules.RULE_MOBGRIEFING)) && entity.mayInteract(serverLevel, pos)) { // Paper - Fixes MC-248588
++                if (this.handleEntityOnFireInside(state, level, pos, entity)) { // Paper - fix powdered snow cauldron extinguishing entities
++                    entity.clearFire();
++                }
++            // CraftBukkit end
+             }
+         }
+     }
+ 
+-    private void handleEntityOnFireInside(BlockState state, Level level, BlockPos pos) {
++    // CraftBukkit start
++    private boolean handleEntityOnFireInside(BlockState state, Level level, BlockPos pos, Entity entity) {
+         if (this.precipitationType == Biome.Precipitation.SNOW) {
+-            lowerFillLevel(Blocks.WATER_CAULDRON.defaultBlockState().setValue(LEVEL, state.getValue(LEVEL)), level, pos);
++            return LayeredCauldronBlock.lowerFillLevel((BlockState) Blocks.WATER_CAULDRON.defaultBlockState().setValue(LayeredCauldronBlock.LEVEL, (Integer) state.getValue(LayeredCauldronBlock.LEVEL)), level, pos, entity, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason.EXTINGUISH); // CraftBukkit
+         } else {
+-            lowerFillLevel(state, level, pos);
++            return LayeredCauldronBlock.lowerFillLevel(state, level, pos, entity, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason.EXTINGUISH); // CraftBukkit
+         }
+     }
+ 
+     public static void lowerFillLevel(BlockState state, Level level, BlockPos pos) {
+-        int i = state.getValue(LEVEL) - 1;
+-        BlockState blockState = i == 0 ? Blocks.CAULDRON.defaultBlockState() : state.setValue(LEVEL, Integer.valueOf(i));
+-        level.setBlockAndUpdate(pos, blockState);
+-        level.gameEvent(GameEvent.BLOCK_CHANGE, pos, GameEvent.Context.of(blockState));
+-    }
++        // CraftBukkit start
++        LayeredCauldronBlock.lowerFillLevel(state, level, pos, null, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason.UNKNOWN);
++    }
++    public static boolean lowerFillLevel(BlockState state, Level level, BlockPos BlockPos, Entity entity, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason reason) {
++        int i = (Integer) state.getValue(LayeredCauldronBlock.LEVEL) - 1;
++        BlockState iblockdata1 = i == 0 ? Blocks.CAULDRON.defaultBlockState() : (BlockState) state.setValue(LayeredCauldronBlock.LEVEL, i);
++
++        return LayeredCauldronBlock.changeLevel(level, BlockPos, iblockdata1, entity, reason);
++     }
++
++    // CraftBukkit start
++    // Paper start - Call CauldronLevelChangeEvent
++    public static boolean changeLevel(Level world, BlockPos pos, BlockState newBlock, @javax.annotation.Nullable Entity entity, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason reason) { // Paper - entity is nullable
++        return changeLevel(world, pos, newBlock, entity, reason, true);
++    }
++
++    public static boolean changeLevel(Level world, BlockPos pos, BlockState newBlock, @javax.annotation.Nullable Entity entity, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason reason, boolean sendGameEvent) { // Paper - entity is nullable
++        // Paper end - Call CauldronLevelChangeEvent
++        org.bukkit.craftbukkit.block.CraftBlockState newState = org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(world, pos);
++        newState.setData(newBlock);
++
++        org.bukkit.event.block.CauldronLevelChangeEvent event = new org.bukkit.event.block.CauldronLevelChangeEvent(
++                org.bukkit.craftbukkit.block.CraftBlock.at(world, pos),
++                (entity == null) ? null : entity.getBukkitEntity(), reason, newState
++        );
++        if (!event.callEvent()) {
++            return false;
++        }
++        newState.update(true);
++        if (sendGameEvent) world.gameEvent(GameEvent.BLOCK_CHANGE, pos, GameEvent.Context.of(newBlock)); // Paper - Call CauldronLevelChangeEvent
++        return true;
++    }
++    // CraftBukkit end
+ 
+     @Override
+     public void handlePrecipitation(BlockState state, Level level, BlockPos pos, Biome.Precipitation precipitation) {
+         if (CauldronBlock.shouldHandlePrecipitation(level, precipitation) && state.getValue(LEVEL) != 3 && precipitation == this.precipitationType) {
+             BlockState blockState = state.cycle(LEVEL);
+-            level.setBlockAndUpdate(pos, blockState);
+-            level.gameEvent(GameEvent.BLOCK_CHANGE, pos, GameEvent.Context.of(blockState));
++            LayeredCauldronBlock.changeLevel(level, pos, blockState, null, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason.NATURAL_FILL); // CraftBukkit
+         }
+     }
+ 
+@@ -107,8 +_,11 @@
+     protected void receiveStalactiteDrip(BlockState state, Level level, BlockPos pos, Fluid fluid) {
+         if (!this.isFull(state)) {
+             BlockState blockState = state.setValue(LEVEL, Integer.valueOf(state.getValue(LEVEL) + 1));
+-            level.setBlockAndUpdate(pos, blockState);
+-            level.gameEvent(GameEvent.BLOCK_CHANGE, pos, GameEvent.Context.of(blockState));
++            // CraftBukkit start
++            if (!LayeredCauldronBlock.changeLevel(level, pos, blockState, null, org.bukkit.event.block.CauldronLevelChangeEvent.ChangeReason.NATURAL_FILL)) {
++                return;
++            }
++            // CraftBukkit end
+             level.levelEvent(1047, pos, 0);
+         }
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/LeavesBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/LeavesBlock.java.patch
new file mode 100644
index 0000000000..b186e88130
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/LeavesBlock.java.patch
@@ -0,0 +1,17 @@
+--- a/net/minecraft/world/level/block/LeavesBlock.java
++++ b/net/minecraft/world/level/block/LeavesBlock.java
+@@ -63,6 +_,14 @@
+     @Override
+     protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+         if (this.decaying(state)) {
++            // CraftBukkit start
++            org.bukkit.event.block.LeavesDecayEvent event = new org.bukkit.event.block.LeavesDecayEvent(level.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ()));
++            level.getCraftServer().getPluginManager().callEvent(event);
++
++            if (event.isCancelled() || level.getBlockState(pos).getBlock() != this) {
++                return;
++            }
++            // CraftBukkit end
+             dropResources(state, level, pos);
+             level.removeBlock(pos, false);
+         }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/LecternBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/LecternBlock.java.patch
new file mode 100644
index 0000000000..fffc21a6fe
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/LecternBlock.java.patch
@@ -0,0 +1,67 @@
+--- a/net/minecraft/world/level/block/LecternBlock.java
++++ b/net/minecraft/world/level/block/LecternBlock.java
+@@ -169,7 +_,24 @@
+ 
+     private static void placeBook(@Nullable LivingEntity entity, Level level, BlockPos pos, BlockState state, ItemStack stack) {
+         if (level.getBlockEntity(pos) instanceof LecternBlockEntity lecternBlockEntity) {
+-            lecternBlockEntity.setBook(stack.consumeAndReturn(1, entity));
++            // Paper start - Add PlayerInsertLecternBookEvent
++            ItemStack eventSourcedBookStack = null;
++            if (entity instanceof final net.minecraft.server.level.ServerPlayer serverPlayer) {
++                final io.papermc.paper.event.player.PlayerInsertLecternBookEvent event = new io.papermc.paper.event.player.PlayerInsertLecternBookEvent(
++                    serverPlayer.getBukkitEntity(),
++                    org.bukkit.craftbukkit.block.CraftBlock.at(level, pos),
++                    org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(stack.copyWithCount(1))
++                );
++                if (!event.callEvent()) return;
++                eventSourcedBookStack = org.bukkit.craftbukkit.inventory.CraftItemStack.unwrap(event.getBook());
++            }
++            if (eventSourcedBookStack == null) {
++                eventSourcedBookStack = stack.consumeAndReturn(1, entity);
++            } else {
++                stack.consume(1, entity);
++            }
++            lecternBlockEntity.setBook(eventSourcedBookStack);
++            // Paper end - Add PlayerInsertLecternBookEvent
+             resetBookState(entity, level, pos, state, true);
+             level.playSound(null, pos, SoundEvents.BOOK_PUT, SoundSource.BLOCKS, 1.0F, 1.0F);
+         }
+@@ -189,6 +_,16 @@
+     }
+ 
+     private static void changePowered(Level level, BlockPos pos, BlockState state, boolean powered) {
++        // Paper start - Call BlockRedstoneEvent properly
++        final int currentRedstoneLevel = state.getValue(LecternBlock.POWERED) ? 15 : 0, targetRedstoneLevel = powered ? 15 : 0;
++        if (currentRedstoneLevel != targetRedstoneLevel) {
++            final org.bukkit.event.block.BlockRedstoneEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callRedstoneChange(level, pos, currentRedstoneLevel, targetRedstoneLevel);
++
++            if (event.getNewCurrent() != targetRedstoneLevel) {
++                return;
++            }
++        }
++        // Paper end - Call BlockRedstoneEvent properly
+         level.setBlock(pos, state.setValue(POWERED, Boolean.valueOf(powered)), 3);
+         updateBelow(level, pos, state);
+     }
+@@ -218,9 +_,10 @@
+     }
+ 
+     private void popBook(BlockState state, Level level, BlockPos pos) {
+-        if (level.getBlockEntity(pos) instanceof LecternBlockEntity lecternBlockEntity) {
++        if (level.getBlockEntity(pos, false) instanceof LecternBlockEntity lecternBlockEntity) { // CraftBukkit - don't validate, type may be changed already
+             Direction direction = state.getValue(FACING);
+             ItemStack itemStack = lecternBlockEntity.getBook().copy();
++            if (itemStack.isEmpty()) return; // CraftBukkit - SPIGOT-5500
+             float f = 0.25F * direction.getStepX();
+             float f1 = 0.25F * direction.getStepZ();
+             ItemEntity itemEntity = new ItemEntity(level, pos.getX() + 0.5 + f, pos.getY() + 1, pos.getZ() + 0.5 + f1, itemStack);
+@@ -296,8 +_,7 @@
+ 
+     private void openScreen(Level level, BlockPos pos, Player player) {
+         BlockEntity blockEntity = level.getBlockEntity(pos);
+-        if (blockEntity instanceof LecternBlockEntity) {
+-            player.openMenu((LecternBlockEntity)blockEntity);
++        if (blockEntity instanceof LecternBlockEntity lecternBlockEntity && player.openMenu(lecternBlockEntity).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
+             player.awardStat(Stats.INTERACT_WITH_LECTERN);
+         }
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/LeverBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/LeverBlock.java.patch
new file mode 100644
index 0000000000..98f131c9db
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/LeverBlock.java.patch
@@ -0,0 +1,22 @@
+--- a/net/minecraft/world/level/block/LeverBlock.java
++++ b/net/minecraft/world/level/block/LeverBlock.java
+@@ -100,6 +_,19 @@
+                 makeParticle(blockState, level, pos, 1.0F);
+             }
+         } else {
++            // CraftBukkit start - Interact Lever
++            boolean powered = state.getValue(LeverBlock.POWERED); // Old powered state
++            org.bukkit.block.Block block = level.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ());
++            int old = (powered) ? 15 : 0;
++            int current = (!powered) ? 15 : 0;
++
++            org.bukkit.event.block.BlockRedstoneEvent eventRedstone = new org.bukkit.event.block.BlockRedstoneEvent(block, old, current);
++            level.getCraftServer().getPluginManager().callEvent(eventRedstone);
++
++            if ((eventRedstone.getNewCurrent() > 0) != (!powered)) {
++                return InteractionResult.SUCCESS;
++            }
++            // CraftBukkit end
+             this.pull(state, level, pos, null);
+         }
+ 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/LightBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/LightBlock.java.patch
similarity index 82%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/LightBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/LightBlock.java.patch
index 4e34991e01..c9fda343b0 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/LightBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/LightBlock.java.patch
@@ -1,18 +1,16 @@
 --- a/net/minecraft/world/level/block/LightBlock.java
 +++ b/net/minecraft/world/level/block/LightBlock.java
-@@ -50,7 +50,15 @@
+@@ -49,6 +_,13 @@
+     protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
          builder.add(LEVEL, WATERLOGGED);
      }
- 
 +    // Paper start - prevent unintended light block manipulation
-     @Override
++    @Override
 +    protected InteractionResult useItemOn(ItemStack stack, BlockState state, Level world, BlockPos pos, Player player, net.minecraft.world.InteractionHand hand, BlockHitResult hit) {
 +        if (player.getItemInHand(hand).getItem() != Items.LIGHT || (world instanceof final net.minecraft.server.level.ServerLevel serverLevel && !player.mayInteract(serverLevel, pos)) || !player.mayUseItemAt(pos, hit.getDirection(), player.getItemInHand(hand))) { return net.minecraft.world.InteractionResult.PASS; } // Paper - Prevent unintended light block manipulation
 +        return super.useItemOn(stack, state, world, pos, player, hand, hit);
 +    }
 +    // Paper end - prevent unintended light block manipulation
-+
-+    @Override
-     protected InteractionResult useWithoutItem(BlockState state, Level world, BlockPos pos, Player player, BlockHitResult hit) {
-         if (!world.isClientSide && player.canUseGameMasterBlocks()) {
-             world.setBlock(pos, state.cycle(LEVEL), 2);
+ 
+     @Override
+     protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/LightningRodBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/LightningRodBlock.java.patch
new file mode 100644
index 0000000000..7480d6343d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/LightningRodBlock.java.patch
@@ -0,0 +1,21 @@
+--- a/net/minecraft/world/level/block/LightningRodBlock.java
++++ b/net/minecraft/world/level/block/LightningRodBlock.java
+@@ -84,6 +_,18 @@
+     }
+ 
+     public void onLightningStrike(BlockState state, Level level, BlockPos pos) {
++        // CraftBukkit start
++        boolean powered = state.getValue(LightningRodBlock.POWERED);
++        int old = (powered) ? 15 : 0;
++        int current = (!powered) ? 15 : 0;
++
++        org.bukkit.event.block.BlockRedstoneEvent eventRedstone = new org.bukkit.event.block.BlockRedstoneEvent(org.bukkit.craftbukkit.block.CraftBlock.at(level, pos), old, current);
++        level.getCraftServer().getPluginManager().callEvent(eventRedstone);
++
++        if (eventRedstone.getNewCurrent() <= 0) {
++            return;
++        }
++        // CraftBukkit end
+         level.setBlock(pos, state.setValue(POWERED, Boolean.valueOf(true)), 3);
+         this.updateNeighbours(state, level, pos);
+         level.scheduleTick(pos, this, 8);
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/LiquidBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/LiquidBlock.java.patch
new file mode 100644
index 0000000000..35dc60692f
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/LiquidBlock.java.patch
@@ -0,0 +1,68 @@
+--- a/net/minecraft/world/level/block/LiquidBlock.java
++++ b/net/minecraft/world/level/block/LiquidBlock.java
+@@ -135,9 +_,28 @@
+     @Override
+     protected void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean isMoving) {
+         if (this.shouldSpreadLiquid(level, pos, state)) {
+-            level.scheduleTick(pos, state.getFluidState().getType(), this.fluid.getTickDelay(level));
+-        }
+-    }
++            level.scheduleTick(pos, state.getFluidState().getType(), this.getFlowSpeed(level, pos)); // Paper - Configurable speed for water flowing over lava
++        }
++    }
++    // Paper start - Configurable speed for water flowing over lava
++    public int getFlowSpeed(Level world, BlockPos blockposition) {
++        if (net.minecraft.core.registries.BuiltInRegistries.FLUID.wrapAsHolder(this.fluid).is(FluidTags.WATER)) {
++            if (
++                isLava(world, blockposition.north(1)) ||
++                isLava(world, blockposition.south(1)) ||
++                isLava(world, blockposition.west(1)) ||
++                isLava(world, blockposition.east(1))
++            ) {
++                return world.paperConfig().environment.waterOverLavaFlowSpeed;
++            }
++        }
++        return this.fluid.getTickDelay(world);
++    }
++    private static boolean isLava(Level world, BlockPos blockPos) {
++        final FluidState fluidState = world.getFluidIfLoaded(blockPos);
++        return fluidState != null && fluidState.is(FluidTags.LAVA);
++    }
++    // Paper end - Configurable speed for water flowing over lava
+ 
+     @Override
+     protected BlockState updateShape(
+@@ -160,7 +_,7 @@
+     @Override
+     protected void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighborBlock, @Nullable Orientation orientation, boolean movedByPiston) {
+         if (this.shouldSpreadLiquid(level, pos, state)) {
+-            level.scheduleTick(pos, state.getFluidState().getType(), this.fluid.getTickDelay(level));
++            level.scheduleTick(pos, state.getFluidState().getType(), this.getFlowSpeed(level, pos)); // Paper - Configurable speed for water flowing over lava
+         }
+     }
+ 
+@@ -172,14 +_,20 @@
+                 BlockPos blockPos = pos.relative(direction.getOpposite());
+                 if (level.getFluidState(blockPos).is(FluidTags.WATER)) {
+                     Block block = level.getFluidState(pos).isSource() ? Blocks.OBSIDIAN : Blocks.COBBLESTONE;
+-                    level.setBlockAndUpdate(pos, block.defaultBlockState());
+-                    this.fizz(level, pos);
++                    // CraftBukkit start
++                    if (org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(level, pos, block.defaultBlockState())) {
++                        this.fizz(level, pos);
++                    }
++                    // CraftBukkit end
+                     return false;
+                 }
+ 
+                 if (isSoulSoil && level.getBlockState(blockPos).is(Blocks.BLUE_ICE)) {
+-                    level.setBlockAndUpdate(pos, Blocks.BASALT.defaultBlockState());
+-                    this.fizz(level, pos);
++                    // CraftBukkit start
++                    if (org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(level, pos, Blocks.BASALT.defaultBlockState())) {
++                        this.fizz(level, pos);
++                    }
++                    // CraftBukkit end
+                     return false;
+                 }
+             }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/LoomBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/LoomBlock.java.patch
new file mode 100644
index 0000000000..04d34f0a8c
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/LoomBlock.java.patch
@@ -0,0 +1,12 @@
+--- a/net/minecraft/world/level/block/LoomBlock.java
++++ b/net/minecraft/world/level/block/LoomBlock.java
+@@ -32,8 +_,7 @@
+ 
+     @Override
+     protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) {
+-        if (!level.isClientSide) {
+-            player.openMenu(state.getMenuProvider(level, pos));
++        if (!level.isClientSide && player.openMenu(state.getMenuProvider(level, pos)).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
+             player.awardStat(Stats.INTERACT_WITH_LOOM);
+         }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/MagmaBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/MagmaBlock.java.patch
new file mode 100644
index 0000000000..ae7d0a7fd6
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/MagmaBlock.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/level/block/MagmaBlock.java
++++ b/net/minecraft/world/level/block/MagmaBlock.java
+@@ -29,7 +_,7 @@
+     @Override
+     public void stepOn(Level level, BlockPos pos, BlockState state, Entity entity) {
+         if (!entity.isSteppingCarefully() && entity instanceof LivingEntity) {
+-            entity.hurt(level.damageSources().hotFloor(), 1.0F);
++            entity.hurt(level.damageSources().hotFloor().directBlock(level, pos), 1.0F); // CraftBukkit
+         }
+ 
+         super.stepOn(level, pos, state, entity);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/MangrovePropaguleBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/MangrovePropaguleBlock.java.patch
similarity index 56%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/MangrovePropaguleBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/MangrovePropaguleBlock.java.patch
index 8a2e3114dc..0ebbe0b613 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/MangrovePropaguleBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/MangrovePropaguleBlock.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/world/level/block/MangrovePropaguleBlock.java
 +++ b/net/minecraft/world/level/block/MangrovePropaguleBlock.java
-@@ -123,7 +123,7 @@
+@@ -123,7 +_,7 @@
      @Override
-     protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
+     protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
          if (!isHanging(state)) {
 -            if (random.nextInt(7) == 0) {
-+            if (random.nextFloat() < (world.spigotConfig.saplingModifier / (100.0F * 7))) { // Paper - Fix Spigot growth modifiers
-                 this.advanceTree(world, pos, state, random);
++            if (random.nextFloat() < (level.spigotConfig.saplingModifier / (100.0F * 7))) { // Paper - Fix Spigot growth modifiers
+                 this.advanceTree(level, pos, state, random);
              }
          } else {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/MultifaceSpreader.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/MultifaceSpreader.java.patch
new file mode 100644
index 0000000000..20085ae07e
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/MultifaceSpreader.java.patch
@@ -0,0 +1,42 @@
+--- a/net/minecraft/world/level/block/MultifaceSpreader.java
++++ b/net/minecraft/world/level/block/MultifaceSpreader.java
+@@ -154,14 +_,14 @@
+                     level.getChunk(pos.pos()).markPosForPostprocessing(pos.pos());
+                 }
+ 
+-                return level.setBlock(pos.pos(), stateForPlacement, 2);
++                return org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(level, pos.source(), pos.pos(), stateForPlacement, 2); // CraftBukkit
+             } else {
+                 return false;
+             }
+         }
+     }
+ 
+-    public record SpreadPos(BlockPos pos, Direction face) {
++    public record SpreadPos(BlockPos pos, Direction face, BlockPos source) { // CraftBukkit
+     }
+ 
+     @FunctionalInterface
+@@ -173,19 +_,19 @@
+         SAME_POSITION {
+             @Override
+             public MultifaceSpreader.SpreadPos getSpreadPos(BlockPos pos, Direction face, Direction spreadDirection) {
+-                return new MultifaceSpreader.SpreadPos(pos, face);
++                return new MultifaceSpreader.SpreadPos(pos, face, pos); // CraftBukkit
+             }
+         },
+         SAME_PLANE {
+             @Override
+             public MultifaceSpreader.SpreadPos getSpreadPos(BlockPos pos, Direction face, Direction spreadDirection) {
+-                return new MultifaceSpreader.SpreadPos(pos.relative(face), spreadDirection);
++                return new MultifaceSpreader.SpreadPos(pos.relative(face), spreadDirection, pos); // CraftBukkit
+             }
+         },
+         WRAP_AROUND {
+             @Override
+             public MultifaceSpreader.SpreadPos getSpreadPos(BlockPos pos, Direction face, Direction spreadDirection) {
+-                return new MultifaceSpreader.SpreadPos(pos.relative(face).relative(spreadDirection), face.getOpposite());
++                return new MultifaceSpreader.SpreadPos(pos.relative(face).relative(spreadDirection), face.getOpposite(), pos); // CraftBukkit
+             }
+         };
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/MushroomBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/MushroomBlock.java.patch
new file mode 100644
index 0000000000..188da77aec
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/MushroomBlock.java.patch
@@ -0,0 +1,36 @@
+--- a/net/minecraft/world/level/block/MushroomBlock.java
++++ b/net/minecraft/world/level/block/MushroomBlock.java
+@@ -47,7 +_,7 @@
+ 
+     @Override
+     protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+-        if (random.nextInt(25) == 0) {
++        if (random.nextFloat() < (level.spigotConfig.mushroomModifier / (100.0f * 25))) { // Spigot - SPIGOT-7159: Better modifier resolution
+             int i = 5;
+             int i1 = 4;
+ 
+@@ -60,6 +_,7 @@
+             }
+ 
+             BlockPos blockPos1 = pos.offset(random.nextInt(3) - 1, random.nextInt(2) - random.nextInt(2), random.nextInt(3) - 1);
++            final BlockPos sourcePos = pos; // Paper - Use correct source for mushroom block spread event
+ 
+             for (int i2 = 0; i2 < 4; i2++) {
+                 if (level.isEmptyBlock(blockPos1) && state.canSurvive(level, blockPos1)) {
+@@ -70,7 +_,7 @@
+             }
+ 
+             if (level.isEmptyBlock(blockPos1) && state.canSurvive(level, blockPos1)) {
+-                level.setBlock(blockPos1, state, 2);
++                org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(level, sourcePos, blockPos1, state, 2); // CraftBukkit // Paper - Use correct source for mushroom block spread event
+             }
+         }
+     }
+@@ -93,6 +_,7 @@
+             return false;
+         } else {
+             level.removeBlock(pos, false);
++            SaplingBlock.treeType = (this == Blocks.BROWN_MUSHROOM) ? org.bukkit.TreeType.BROWN_MUSHROOM : org.bukkit.TreeType.RED_MUSHROOM; // CraftBukkit
+             if (optional.get().value().place(level, level.getChunkSource().getGenerator(), random, pos)) {
+                 return true;
+             } else {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/NetherPortalBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/NetherPortalBlock.java.patch
new file mode 100644
index 0000000000..fa161299ce
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/NetherPortalBlock.java.patch
@@ -0,0 +1,127 @@
+--- a/net/minecraft/world/level/block/NetherPortalBlock.java
++++ b/net/minecraft/world/level/block/NetherPortalBlock.java
+@@ -70,7 +_,7 @@
+ 
+     @Override
+     protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+-        if (level.dimensionType().natural()
++        if (level.spigotConfig.enableZombiePigmenPortalSpawns && level.dimensionType().natural() // Spigot
+             && level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)
+             && random.nextInt(2000) < level.getDifficulty().getId()) {
+             while (level.getBlockState(pos).is(this)) {
+@@ -78,9 +_,13 @@
+             }
+ 
+             if (level.getBlockState(pos).isValidSpawn(level, pos, EntityType.ZOMBIFIED_PIGLIN)) {
+-                Entity entity = EntityType.ZOMBIFIED_PIGLIN.spawn(level, pos.above(), EntitySpawnReason.STRUCTURE);
++                Entity entity = EntityType.ZOMBIFIED_PIGLIN.spawn(level, pos.above(), EntitySpawnReason.STRUCTURE, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.NETHER_PORTAL); // CraftBukkit - set spawn reason to NETHER_PORTAL
+                 if (entity != null) {
+                     entity.setPortalCooldown();
++                    // Paper start - Add option to nerf pigmen from nether portals
++                    entity.fromNetherPortal = true;
++                    if (level.paperConfig().entities.behavior.nerfPigmenFromNetherPortals) ((net.minecraft.world.entity.Mob) entity).aware = false;
++                    // Paper end - Add option to nerf pigmen from nether portals
+                     Entity vehicle = entity.getVehicle();
+                     if (vehicle != null) {
+                         vehicle.setPortalCooldown();
+@@ -111,7 +_,13 @@
+ 
+     @Override
+     protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
++        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
+         if (entity.canUsePortal(false)) {
++            // CraftBukkit start - Entity in portal
++            org.bukkit.event.entity.EntityPortalEnterEvent event = new org.bukkit.event.entity.EntityPortalEnterEvent(entity.getBukkitEntity(), new org.bukkit.Location(level.getWorld(), pos.getX(), pos.getY(), pos.getZ()), org.bukkit.PortalType.NETHER); // Paper - add portal type
++            level.getCraftServer().getPluginManager().callEvent(event);
++            if (event.isCancelled()) return; // Paper - make cancellable
++            // CraftBukkit end
+             entity.setAsInsidePortal(this, pos);
+         }
+     }
+@@ -134,22 +_,46 @@
+     @Nullable
+     @Override
+     public TeleportTransition getPortalDestination(ServerLevel level, Entity entity, BlockPos pos) {
+-        ResourceKey<Level> resourceKey = level.dimension() == Level.NETHER ? Level.OVERWORLD : Level.NETHER;
++        // CraftBukkit start
++        ResourceKey<Level> resourceKey = level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER ? Level.OVERWORLD : Level.NETHER;
+         ServerLevel level1 = level.getServer().getLevel(resourceKey);
++        // Paper start - Add EntityPortalReadyEvent
++        io.papermc.paper.event.entity.EntityPortalReadyEvent portalReadyEvent = new io.papermc.paper.event.entity.EntityPortalReadyEvent(entity.getBukkitEntity(), level1 == null ? null : level1.getWorld(), org.bukkit.PortalType.NETHER);
++        if (!portalReadyEvent.callEvent()) {
++            entity.portalProcess = null;
++            return null;
++        }
++        level1 = portalReadyEvent.getTargetWorld() == null ? null : ((org.bukkit.craftbukkit.CraftWorld) portalReadyEvent.getTargetWorld()).getHandle();
++        // Paper end - Add EntityPortalReadyEvent
+         if (level1 == null) {
+             return null;
+         } else {
+-            boolean flag = level1.dimension() == Level.NETHER;
++            boolean flag = level1.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER; // CraftBukkit
+             WorldBorder worldBorder = level1.getWorldBorder();
+             double teleportationScale = DimensionType.getTeleportationScale(level.dimensionType(), level1.dimensionType());
+             BlockPos blockPos = worldBorder.clampToBounds(entity.getX() * teleportationScale, entity.getY(), entity.getZ() * teleportationScale);
+-            return this.getExitPortal(level1, entity, pos, blockPos, flag, worldBorder);
++            // Paper start - Configurable portal search radius
++            int portalSearchRadius = level1.paperConfig().environment.portalSearchRadius;
++            if (entity.level().paperConfig().environment.portalSearchVanillaDimensionScaling && flag) { // flag = is going to nether
++                portalSearchRadius = (int) (portalSearchRadius / level1.dimensionType().coordinateScale());
++            }
++            // Paper end - Configurable portal search radius
++            // CraftBukkit start
++            org.bukkit.craftbukkit.event.CraftPortalEvent event = entity.callPortalEvent(entity, org.bukkit.craftbukkit.util.CraftLocation.toBukkit(blockPos, level1.getWorld()), org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.NETHER_PORTAL, portalSearchRadius, level1.paperConfig().environment.portalCreateRadius); // Paper - use custom portal search radius
++            if (event == null) {
++                return null;
++            }
++            level1 = ((org.bukkit.craftbukkit.CraftWorld) event.getTo().getWorld()).getHandle();
++            worldBorder = level1.getWorldBorder();
++            blockPos = worldBorder.clampToBounds(event.getTo().getX(), event.getTo().getY(), event.getTo().getZ());
++            return this.getExitPortal(level1, entity, pos, blockPos, flag, worldBorder, event.getSearchRadius(), event.getCanCreatePortal(), event.getCreationRadius());
++            // CraftBukkit end
+         }
+     }
+ 
+     @Nullable
+-    private TeleportTransition getExitPortal(ServerLevel level, Entity entity, BlockPos pos, BlockPos exitPos, boolean isNether, WorldBorder worldBorder) {
+-        Optional<BlockPos> optional = level.getPortalForcer().findClosestPortalPosition(exitPos, isNether, worldBorder);
++    private TeleportTransition getExitPortal(ServerLevel level, Entity entity, BlockPos pos, BlockPos exitPos, boolean isNether, WorldBorder worldBorder, int searchRadius, boolean canCreatePortal, int createRadius) { // CraftBukkit
++        Optional<BlockPos> optional = level.getPortalForcer().findClosestPortalPosition(exitPos, worldBorder, searchRadius); // CraftBukkit
+         BlockUtil.FoundRectangle largestRectangleAround;
+         TeleportTransition.PostTeleportTransition postTeleportTransition;
+         if (optional.isPresent()) {
+@@ -164,17 +_,22 @@
+                 blockPos1 -> level.getBlockState(blockPos1) == blockState
+             );
+             postTeleportTransition = TeleportTransition.PLAY_PORTAL_SOUND.then(entity1 -> entity1.placePortalTicket(blockPos));
+-        } else {
++        } else if (canCreatePortal) { // CraftBukkit
+             Direction.Axis axis = entity.level().getBlockState(pos).getOptionalValue(AXIS).orElse(Direction.Axis.X);
+-            Optional<BlockUtil.FoundRectangle> optional1 = level.getPortalForcer().createPortal(exitPos, axis);
++            Optional<BlockUtil.FoundRectangle> optional1 = level.getPortalForcer().createPortal(exitPos, axis, entity, createRadius); // CraftBukkit
+             if (optional1.isEmpty()) {
+-                LOGGER.error("Unable to create a portal, likely target out of worldborder");
++                // LOGGER.error("Unable to create a portal, likely target out of worldborder"); // CraftBukkit
+                 return null;
+             }
+ 
+             largestRectangleAround = optional1.get();
+             postTeleportTransition = TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET);
+         }
++        // CraftBukkit start
++        else {
++            return null;
++        }
++        // CraftBukkit end
+ 
+         return getDimensionTransitionFromExit(entity, pos, largestRectangleAround, level, postTeleportTransition);
+     }
+@@ -220,7 +_,7 @@
+         boolean flag = axis1 == Direction.Axis.X;
+         Vec3 vec3 = new Vec3(blockPos.getX() + (flag ? d2 : d4), blockPos.getY() + d3, blockPos.getZ() + (flag ? d4 : d2));
+         Vec3 vec31 = PortalShape.findCollisionFreePosition(vec3, level, entity, dimensions);
+-        return new TeleportTransition(level, vec31, Vec3.ZERO, i, 0.0F, Relative.union(Relative.DELTA, Relative.ROTATION), postTeleportTransition);
++        return new TeleportTransition(level, vec31, Vec3.ZERO, i, 0.0F, Relative.union(Relative.DELTA, Relative.ROTATION), postTeleportTransition, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.NETHER_PORTAL); // CraftBukkit
+     }
+ 
+     @Override
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/NetherWartBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/NetherWartBlock.java.patch
new file mode 100644
index 0000000000..d55e5647cf
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/NetherWartBlock.java.patch
@@ -0,0 +1,14 @@
+--- a/net/minecraft/world/level/block/NetherWartBlock.java
++++ b/net/minecraft/world/level/block/NetherWartBlock.java
+@@ -55,9 +_,9 @@
+     @Override
+     protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+         int ageValue = state.getValue(AGE);
+-        if (ageValue < 3 && random.nextInt(10) == 0) {
++        if (ageValue < 3 && random.nextFloat() < (level.spigotConfig.wartModifier / (100.0f * 10))) { // Spigot - SPIGOT-7159: Better modifier resolution
+             state = state.setValue(AGE, Integer.valueOf(ageValue + 1));
+-            level.setBlock(pos, state, 2);
++            org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(level, pos, state, 2); // CraftBukkit
+         }
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/NoteBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/NoteBlock.java.patch
new file mode 100644
index 0000000000..0ff72778c3
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/NoteBlock.java.patch
@@ -0,0 +1,78 @@
+--- a/net/minecraft/world/level/block/NoteBlock.java
++++ b/net/minecraft/world/level/block/NoteBlock.java
+@@ -70,6 +_,7 @@
+ 
+     @Override
+     public BlockState getStateForPlacement(BlockPlaceContext context) {
++        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableNoteblockUpdates) return this.defaultBlockState(); // Paper - place without considering instrument
+         return this.setInstrument(context.getLevel(), context.getClickedPos(), this.defaultBlockState());
+     }
+ 
+@@ -84,6 +_,7 @@
+         BlockState neighborState,
+         RandomSource random
+     ) {
++        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableNoteblockUpdates) return state; // Paper - prevent noteblock instrument from updating
+         boolean flag = direction.getAxis() == Direction.Axis.Y;
+         return flag
+             ? this.setInstrument(level, pos, state)
+@@ -92,10 +_,12 @@
+ 
+     @Override
+     protected void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighborBlock, @Nullable Orientation orientation, boolean movedByPiston) {
++        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableNoteblockUpdates) return; // Paper - prevent noteblock powered-state from updating
+         boolean hasNeighborSignal = level.hasNeighborSignal(pos);
+         if (hasNeighborSignal != state.getValue(POWERED)) {
+             if (hasNeighborSignal) {
+                 this.playNote(null, state, level, pos);
++                state = level.getBlockState(pos); // CraftBukkit - SPIGOT-5617: update in case changed in event
+             }
+ 
+             level.setBlock(pos, state.setValue(POWERED, Boolean.valueOf(hasNeighborSignal)), 3);
+@@ -104,6 +_,13 @@
+ 
+     private void playNote(@Nullable Entity entity, BlockState state, Level level, BlockPos pos) {
+         if (state.getValue(INSTRUMENT).worksAboveNoteBlock() || level.getBlockState(pos.above()).isAir()) {
++            // CraftBukkit start
++            // org.bukkit.event.block.NotePlayEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callNotePlayEvent(world, pos, state.getValue(NoteBlock.INSTRUMENT), state.getValue(NoteBlock.NOTE));
++            // if (event.isCancelled()) {
++            //     return;
++            // }
++            // CraftBukkit end
++            // Paper - move NotePlayEvent call to fix instrument/note changes; TODO any way to cancel the game event?
+             level.blockEvent(pos, this, 0, 0);
+             level.gameEvent(entity, GameEvent.NOTE_BLOCK_PLAY, pos);
+         }
+@@ -121,7 +_,7 @@
+     @Override
+     protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) {
+         if (!level.isClientSide) {
+-            state = state.cycle(NOTE);
++            if (!io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableNoteblockUpdates) state = state.cycle(NoteBlock.NOTE); // Paper - prevent noteblock note from updating
+             level.setBlock(pos, state, 3);
+             this.playNote(player, state, level, pos);
+             player.awardStat(Stats.TUNE_NOTEBLOCK);
+@@ -145,9 +_,13 @@
+     @Override
+     protected boolean triggerEvent(BlockState state, Level level, BlockPos pos, int id, int param) {
+         NoteBlockInstrument noteBlockInstrument = state.getValue(INSTRUMENT);
++        // Paper start - move NotePlayEvent call to fix instrument/note changes
++        org.bukkit.event.block.NotePlayEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callNotePlayEvent(level, pos, noteBlockInstrument, state.getValue(NOTE));
++        if (event.isCancelled()) return false;
++        // Paper end - move NotePlayEvent call to fix instrument/note changes
+         float pitchFromNote;
+         if (noteBlockInstrument.isTunable()) {
+-            int noteValue = state.getValue(NOTE);
++            int noteValue = event.getNote().getId(); // Paper - move NotePlayEvent call to fix instrument/note changes
+             pitchFromNote = getPitchFromNote(noteValue);
+             level.addParticle(ParticleTypes.NOTE, pos.getX() + 0.5, pos.getY() + 1.2, pos.getZ() + 0.5, noteValue / 24.0, 0.0, 0.0);
+         } else {
+@@ -163,7 +_,7 @@
+ 
+             holder = Holder.direct(SoundEvent.createVariableRangeEvent(customSoundId));
+         } else {
+-            holder = noteBlockInstrument.getSoundEvent();
++            holder = org.bukkit.craftbukkit.block.data.CraftBlockData.toNMS(event.getInstrument(), NoteBlockInstrument.class).getSoundEvent(); // Paper - move NotePlayEvent call to fix instrument/note changes
+         }
+ 
+         level.playSeededSound(
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/NyliumBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/NyliumBlock.java.patch
similarity index 52%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/NyliumBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/NyliumBlock.java.patch
index cc4aae4d48..c447936cca 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/NyliumBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/NyliumBlock.java.patch
@@ -1,14 +1,14 @@
 --- a/net/minecraft/world/level/block/NyliumBlock.java
 +++ b/net/minecraft/world/level/block/NyliumBlock.java
-@@ -41,6 +41,11 @@
+@@ -39,6 +_,11 @@
      @Override
-     protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
-         if (!NyliumBlock.canBeNylium(state, world, pos)) {
+     protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+         if (!canBeNylium(state, level, pos)) {
 +            // CraftBukkit start
-+            if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(world, pos, Blocks.NETHERRACK.defaultBlockState()).isCancelled()) {
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(level, pos, Blocks.NETHERRACK.defaultBlockState()).isCancelled()) {
 +                return;
 +            }
 +            // CraftBukkit end
-             world.setBlockAndUpdate(pos, Blocks.NETHERRACK.defaultBlockState());
+             level.setBlockAndUpdate(pos, Blocks.NETHERRACK.defaultBlockState());
          }
- 
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/ObserverBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/ObserverBlock.java.patch
new file mode 100644
index 0000000000..12eb553307
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/ObserverBlock.java.patch
@@ -0,0 +1,21 @@
+--- a/net/minecraft/world/level/block/ObserverBlock.java
++++ b/net/minecraft/world/level/block/ObserverBlock.java
+@@ -50,8 +_,18 @@
+     @Override
+     protected void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+         if (state.getValue(POWERED)) {
++            // CraftBukkit start
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.callRedstoneChange(level, pos, 15, 0).getNewCurrent() != 0) {
++                return;
++            }
++            // CraftBukkit end
+             level.setBlock(pos, state.setValue(POWERED, Boolean.valueOf(false)), 2);
+         } else {
++            // CraftBukkit start
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.callRedstoneChange(level, pos, 0, 15).getNewCurrent() != 15) {
++                return;
++            }
++            // CraftBukkit end
+             level.setBlock(pos, state.setValue(POWERED, Boolean.valueOf(true)), 2);
+             level.scheduleTick(pos, this, 2);
+         }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/PitcherCropBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/PitcherCropBlock.java.patch
new file mode 100644
index 0000000000..7015f932e1
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/PitcherCropBlock.java.patch
@@ -0,0 +1,28 @@
+--- a/net/minecraft/world/level/block/PitcherCropBlock.java
++++ b/net/minecraft/world/level/block/PitcherCropBlock.java
+@@ -107,6 +_,7 @@
+ 
+     @Override
+     public void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
++        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
+         if (level instanceof ServerLevel serverLevel && entity instanceof Ravager && serverLevel.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) {
+             serverLevel.destroyBlock(pos, true, entity);
+         }
+@@ -131,7 +_,7 @@
+     @Override
+     public void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+         float growthSpeed = CropBlock.getGrowthSpeed(this, level, pos);
+-        boolean flag = random.nextInt((int)(25.0F / growthSpeed) + 1) == 0;
++        boolean flag = random.nextFloat() < (level.spigotConfig.pitcherPlantModifier / (100.0F * (Math.floor(25.0F / growthSpeed) + 1))); // Paper - Fix Spigot growth modifiers
+         if (flag) {
+             this.grow(level, state, pos, 1);
+         }
+@@ -141,7 +_,7 @@
+         int min = Math.min(state.getValue(AGE) + ageIncrement, 4);
+         if (this.canGrow(level, pos, state, min)) {
+             BlockState blockState = state.setValue(AGE, Integer.valueOf(min));
+-            level.setBlock(pos, blockState, 2);
++            if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(level, pos, blockState, 2)) return; // Paper
+             if (isDouble(min)) {
+                 level.setBlock(pos.above(), blockState.setValue(HALF, DoubleBlockHalf.UPPER), 3);
+             }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/PointedDripstoneBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/PointedDripstoneBlock.java.patch
new file mode 100644
index 0000000000..98e5a6861e
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/PointedDripstoneBlock.java.patch
@@ -0,0 +1,68 @@
+--- a/net/minecraft/world/level/block/PointedDripstoneBlock.java
++++ b/net/minecraft/world/level/block/PointedDripstoneBlock.java
+@@ -147,6 +_,11 @@
+                 && projectile.mayBreak(serverLevel)
+                 && projectile instanceof ThrownTrident
+                 && projectile.getDeltaMovement().length() > 0.6) {
++                // CraftBukkit start
++                if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(projectile, blockPos, state.getFluidState().createLegacyBlock())) { // Paper - fix wrong block state
++                    return;
++                }
++                // CraftBukkit end
+                 level.destroyBlock(blockPos, true);
+             }
+         }
+@@ -155,7 +_,7 @@
+     @Override
+     public void fallOn(Level level, BlockState state, BlockPos pos, Entity entity, float fallDistance) {
+         if (state.getValue(TIP_DIRECTION) == Direction.UP && state.getValue(THICKNESS) == DripstoneThickness.TIP) {
+-            entity.causeFallDamage(fallDistance + 2.0F, 2.0F, level.damageSources().stalagmite());
++            entity.causeFallDamage(fallDistance + 2.0F, 2.0F, level.damageSources().stalagmite().directBlock(level, pos)); // CraftBukkit
+         } else {
+             super.fallOn(level, state, pos, entity, fallDistance);
+         }
+@@ -213,10 +_,11 @@
+                         if (blockPos != null) {
+                             if (fluidAboveStalactite.get().sourceState.is(Blocks.MUD) && fluid == Fluids.WATER) {
+                                 BlockState blockState = Blocks.CLAY.defaultBlockState();
+-                                level.setBlockAndUpdate(fluidAboveStalactite.get().pos, blockState);
++                                if (org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(level, fluidAboveStalactite.get().pos, blockState)) { // Paper - Call BlockFormEvent
+                                 Block.pushEntitiesUp(fluidAboveStalactite.get().sourceState, blockState, level, fluidAboveStalactite.get().pos);
+                                 level.gameEvent(GameEvent.BLOCK_CHANGE, fluidAboveStalactite.get().pos, GameEvent.Context.of(blockState));
+                                 level.levelEvent(1504, blockPos, 0);
++                                } // Paper - Call BlockFormEvent
+                             } else {
+                                 BlockPos blockPos1 = findFillableCauldronBelowStalactiteTip(level, blockPos, fluid);
+                                 if (blockPos1 != null) {
+@@ -380,17 +_,17 @@
+         if (isUnmergedTipWithDirection(blockState, direction.getOpposite())) {
+             createMergedTips(blockState, server, blockPos);
+         } else if (blockState.isAir() || blockState.is(Blocks.WATER)) {
+-            createDripstone(server, blockPos, direction, DripstoneThickness.TIP);
++            createDripstone(server, blockPos, direction, DripstoneThickness.TIP, pos); // CraftBukkit
+         }
+     }
+ 
+-    private static void createDripstone(LevelAccessor level, BlockPos pos, Direction direction, DripstoneThickness thickness) {
++    private static void createDripstone(LevelAccessor level, BlockPos pos, Direction direction, DripstoneThickness thickness, BlockPos source) { // CraftBukkit
+         BlockState blockState = Blocks.POINTED_DRIPSTONE
+             .defaultBlockState()
+             .setValue(TIP_DIRECTION, direction)
+             .setValue(THICKNESS, thickness)
+             .setValue(WATERLOGGED, Boolean.valueOf(level.getFluidState(pos).getType() == Fluids.WATER));
+-        level.setBlock(pos, blockState, 3);
++        org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(level, source, pos, blockState, 3); // CraftBukkit
+     }
+ 
+     private static void createMergedTips(BlockState state, LevelAccessor level, BlockPos pos) {
+@@ -404,8 +_,8 @@
+             blockPos = pos.below();
+         }
+ 
+-        createDripstone(level, blockPos1, Direction.DOWN, DripstoneThickness.TIP_MERGE);
+-        createDripstone(level, blockPos, Direction.UP, DripstoneThickness.TIP_MERGE);
++        createDripstone(level, blockPos1, Direction.DOWN, DripstoneThickness.TIP_MERGE, pos); // CraftBukkit
++        createDripstone(level, blockPos, Direction.UP, DripstoneThickness.TIP_MERGE, pos); // CraftBukkit
+     }
+ 
+     public static void spawnDripParticle(Level level, BlockPos pos, BlockState state) {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/PowderSnowBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/PowderSnowBlock.java.patch
new file mode 100644
index 0000000000..1254939fc6
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/PowderSnowBlock.java.patch
@@ -0,0 +1,25 @@
+--- a/net/minecraft/world/level/block/PowderSnowBlock.java
++++ b/net/minecraft/world/level/block/PowderSnowBlock.java
+@@ -58,6 +_,7 @@
+ 
+     @Override
+     protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
++        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
+         if (!(entity instanceof LivingEntity) || entity.getInBlockState().is(this)) {
+             entity.makeStuckInBlock(state, new Vec3(0.9F, 1.5, 0.9F));
+             if (level.isClientSide) {
+@@ -80,8 +_,13 @@
+         entity.setIsInPowderSnow(true);
+         if (level instanceof ServerLevel serverLevel) {
+             if (entity.isOnFire()
+-                && (serverLevel.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) || entity instanceof Player)
++                // CraftBukkit - move down
+                 && entity.mayInteract(serverLevel, pos)) {
++                // CraftBukkit start
++                if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(entity, pos, Blocks.AIR.defaultBlockState(), !(serverLevel.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) || entity instanceof Player))) {
++                    return;
++                }
++                // CraftBukkit end
+                 level.destroyBlock(pos, false);
+             }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/PoweredRailBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/PoweredRailBlock.java.patch
new file mode 100644
index 0000000000..04777f56ac
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/PoweredRailBlock.java.patch
@@ -0,0 +1,16 @@
+--- a/net/minecraft/world/level/block/PoweredRailBlock.java
++++ b/net/minecraft/world/level/block/PoweredRailBlock.java
+@@ -133,6 +_,13 @@
+             || this.findPoweredRailSignal(level, pos, state, true, 0)
+             || this.findPoweredRailSignal(level, pos, state, false, 0);
+         if (flag != poweredValue) {
++            // CraftBukkit start
++            int power = flag ? 15 : 0;
++            int newPower = org.bukkit.craftbukkit.event.CraftEventFactory.callRedstoneChange(level, pos, power, 15 - power).getNewCurrent();
++            if (newPower == power) {
++                return;
++            }
++            // CraftBukkit end
+             level.setBlock(pos, state.setValue(POWERED, Boolean.valueOf(flag)), 3);
+             level.updateNeighborsAt(pos.below(), this);
+             if (state.getValue(SHAPE).isSlope()) {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/PressurePlateBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/PressurePlateBlock.java.patch
new file mode 100644
index 0000000000..235e99adff
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/PressurePlateBlock.java.patch
@@ -0,0 +1,43 @@
+--- a/net/minecraft/world/level/block/PressurePlateBlock.java
++++ b/net/minecraft/world/level/block/PressurePlateBlock.java
+@@ -5,6 +_,7 @@
+ import net.minecraft.core.BlockPos;
+ import net.minecraft.world.entity.Entity;
+ import net.minecraft.world.entity.LivingEntity;
++import net.minecraft.world.entity.player.Player;
+ import net.minecraft.world.level.Level;
+ import net.minecraft.world.level.block.state.BlockBehaviour;
+ import net.minecraft.world.level.block.state.BlockState;
+@@ -46,7 +_,31 @@
+             case EVERYTHING -> Entity.class;
+             case MOBS -> LivingEntity.class;
+         };
+-        return getEntityCount(level, TOUCH_AABB.move(pos), clazz) > 0 ? 15 : 0;
++        // CraftBukkit start - Call interact event when turning on a pressure plate
++        for (Entity entity : getEntities(level, PressurePlateBlock.TOUCH_AABB.move(pos), clazz)) {
++            if (this.getSignalForState(level.getBlockState(pos)) == 0) {
++                org.bukkit.World bworld = level.getWorld();
++                org.bukkit.plugin.PluginManager manager = level.getCraftServer().getPluginManager();
++                org.bukkit.event.Cancellable cancellable;
++
++                if (entity instanceof Player) {
++                    cancellable = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerInteractEvent((Player) entity, org.bukkit.event.block.Action.PHYSICAL, pos, null, null, null);
++                } else {
++                    cancellable = new org.bukkit.event.entity.EntityInteractEvent(entity.getBukkitEntity(), bworld.getBlockAt(pos.getX(), pos.getY(), pos.getZ()));
++                    manager.callEvent((org.bukkit.event.entity.EntityInteractEvent) cancellable);
++                }
++
++                // We only want to block turning the plate on if all events are cancelled
++                if (cancellable.isCancelled()) {
++                    continue;
++                }
++            }
++
++            return 15;
++        }
++
++        return 0;
++        // CraftBukkit end
+     }
+ 
+     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/PumpkinBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/PumpkinBlock.java.patch
similarity index 61%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/PumpkinBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/PumpkinBlock.java.patch
index d3d384ea46..281a3997a5 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/PumpkinBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/PumpkinBlock.java.patch
@@ -1,36 +1,34 @@
 --- a/net/minecraft/world/level/block/PumpkinBlock.java
 +++ b/net/minecraft/world/level/block/PumpkinBlock.java
-@@ -38,16 +38,24 @@
-         } else if (world.isClientSide) {
+@@ -40,21 +_,30 @@
+         } else if (level.isClientSide) {
              return InteractionResult.SUCCESS;
          } else {
 +            // Paper start - Add PlayerShearBlockEvent
-+            io.papermc.paper.event.block.PlayerShearBlockEvent event = new io.papermc.paper.event.block.PlayerShearBlockEvent((org.bukkit.entity.Player) player.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(stack), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand), new java.util.ArrayList<>());
++            io.papermc.paper.event.block.PlayerShearBlockEvent event = new io.papermc.paper.event.block.PlayerShearBlockEvent((org.bukkit.entity.Player) player.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(stack), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand), new java.util.ArrayList<>());
 +            event.getDrops().add(org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(new ItemStack(Items.PUMPKIN_SEEDS, 4)));
 +            if (!event.callEvent()) {
 +                return InteractionResult.PASS;
 +            }
 +            // Paper end - Add PlayerShearBlockEvent
-             Direction direction = hit.getDirection();
-             Direction direction2 = direction.getAxis() == Direction.Axis.Y ? player.getDirection().getOpposite() : direction;
-             world.playSound(null, pos, SoundEvents.PUMPKIN_CARVE, SoundSource.BLOCKS, 1.0F, 1.0F);
-             world.setBlock(pos, Blocks.CARVED_PUMPKIN.defaultBlockState().setValue(CarvedPumpkinBlock.FACING, direction2), 11);
+             Direction direction = hitResult.getDirection();
+             Direction direction1 = direction.getAxis() == Direction.Axis.Y ? player.getDirection().getOpposite() : direction;
+             level.playSound(null, pos, SoundEvents.PUMPKIN_CARVE, SoundSource.BLOCKS, 1.0F, 1.0F);
+             level.setBlock(pos, Blocks.CARVED_PUMPKIN.defaultBlockState().setValue(CarvedPumpkinBlock.FACING, direction1), 11);
 +            for (org.bukkit.inventory.ItemStack item : event.getDrops()) { // Paper - Add PlayerShearBlockEvent
              ItemEntity itemEntity = new ItemEntity(
-                 world,
-                 (double)pos.getX() + 0.5 + (double)direction2.getStepX() * 0.65,
-                 (double)pos.getY() + 0.1,
-                 (double)pos.getZ() + 0.5 + (double)direction2.getStepZ() * 0.65,
+                 level,
+                 pos.getX() + 0.5 + direction1.getStepX() * 0.65,
+                 pos.getY() + 0.1,
+                 pos.getZ() + 0.5 + direction1.getStepZ() * 0.65,
 -                new ItemStack(Items.PUMPKIN_SEEDS, 4)
 +                org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(item) // Paper - Add PlayerShearBlockEvent
              );
              itemEntity.setDeltaMovement(
-                 0.05 * (double)direction2.getStepX() + world.random.nextDouble() * 0.02,
-@@ -55,6 +63,7 @@
-                 0.05 * (double)direction2.getStepZ() + world.random.nextDouble() * 0.02
+                 0.05 * direction1.getStepX() + level.random.nextDouble() * 0.02, 0.05, 0.05 * direction1.getStepZ() + level.random.nextDouble() * 0.02
              );
-             world.addFreshEntity(itemEntity);
+             level.addFreshEntity(itemEntity);
 +            } // Paper - Add PlayerShearBlockEvent
              stack.hurtAndBreak(1, player, LivingEntity.getSlotForHand(hand));
-             world.gameEvent(player, GameEvent.SHEAR, pos);
+             level.gameEvent(player, GameEvent.SHEAR, pos);
              player.awardStat(Stats.ITEM_USED.get(Items.SHEARS));
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/RailState.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/RailState.java.patch
similarity index 59%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/RailState.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/RailState.java.patch
index b79bfb5efd..1a9dbf420d 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/RailState.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/RailState.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/level/block/RailState.java
 +++ b/net/minecraft/world/level/block/RailState.java
-@@ -17,6 +17,12 @@
+@@ -17,6 +_,12 @@
      private final boolean isStraight;
      private final List<BlockPos> connections = Lists.newArrayList();
  
@@ -10,24 +10,24 @@
 +    }
 +    // Paper end - Fix some rails connecting improperly
 +
-     public RailState(Level world, BlockPos pos, BlockState state) {
-         this.level = world;
+     public RailState(Level level, BlockPos pos, BlockState state) {
+         this.level = level;
          this.pos = pos;
-@@ -141,6 +147,11 @@
+@@ -141,6 +_,11 @@
      }
  
-     private void connectTo(RailState placementHelper) {
+     private void connectTo(RailState state) {
 +        // Paper start - Fix some rails connecting improperly
-+        if (!this.isValid() || !placementHelper.isValid()) {
++        if (!this.isValid() || !state.isValid()) {
 +            return;
 +        }
 +        // Paper end - Fix some rails connecting improperly
-         this.connections.add(placementHelper.pos);
+         this.connections.add(state.pos);
          BlockPos blockPos = this.pos.north();
-         BlockPos blockPos2 = this.pos.south();
-@@ -331,10 +342,15 @@
-         this.state = this.state.setValue(this.block.getShapeProperty(), railShape2);
-         if (forceUpdate || this.level.getBlockState(this.pos) != this.state) {
+         BlockPos blockPos1 = this.pos.south();
+@@ -331,10 +_,15 @@
+         this.state = this.state.setValue(this.block.getShapeProperty(), railShape);
+         if (alwaysPlace || this.level.getBlockState(this.pos) != this.state) {
              this.level.setBlock(this.pos, this.state, 3);
 +            // Paper start - Fix some rails connecting improperly
 +            if (!this.isValid()) {
@@ -36,13 +36,13 @@
 +            // Paper end - Fix some rails connecting improperly
  
              for (int i = 0; i < this.connections.size(); i++) {
-                 RailState railState = this.getRail(this.connections.get(i));
--                if (railState != null) {
-+                if (railState != null && railState.isValid()) { // Paper - Fix some rails connecting improperly
-                     railState.removeSoftConnections();
-                     if (railState.canConnectTo(this)) {
-                         railState.connectTo(this);
-@@ -347,6 +363,6 @@
+                 RailState rail = this.getRail(this.connections.get(i));
+-                if (rail != null) {
++                if (rail != null && rail.isValid()) { // Paper - Fix some rails connecting improperly
+                     rail.removeSoftConnections();
+                     if (rail.canConnectTo(this)) {
+                         rail.connectTo(this);
+@@ -347,6 +_,6 @@
      }
  
      public BlockState getState() {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/RedStoneOreBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/RedStoneOreBlock.java.patch
new file mode 100644
index 0000000000..6f93bcddd4
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/RedStoneOreBlock.java.patch
@@ -0,0 +1,89 @@
+--- a/net/minecraft/world/level/block/RedStoneOreBlock.java
++++ b/net/minecraft/world/level/block/RedStoneOreBlock.java
+@@ -37,14 +_,27 @@
+ 
+     @Override
+     protected void attack(BlockState state, Level level, BlockPos pos, Player player) {
+-        interact(state, level, pos);
++        interact(state, level, pos, player); // CraftBukkit - add entityhuman
+         super.attack(state, level, pos, player);
+     }
+ 
+     @Override
+     public void stepOn(Level level, BlockPos pos, BlockState state, Entity entity) {
+         if (!entity.isSteppingCarefully()) {
+-            interact(state, level, pos);
++            // CraftBukkit start
++            if (entity instanceof Player) {
++                org.bukkit.event.player.PlayerInteractEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerInteractEvent((Player) entity, org.bukkit.event.block.Action.PHYSICAL, pos, null, null, null);
++                if (!event.isCancelled()) {
++                    RedStoneOreBlock.interact(level.getBlockState(pos), level, pos, entity); // add entity
++                }
++            } else {
++                org.bukkit.event.entity.EntityInteractEvent event = new org.bukkit.event.entity.EntityInteractEvent(entity.getBukkitEntity(), level.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ()));
++                level.getCraftServer().getPluginManager().callEvent(event);
++                if (!event.isCancelled()) {
++                    RedStoneOreBlock.interact(level.getBlockState(pos), level, pos, entity); // add entity
++                }
++            }
++            // CraftBukkit end
+         }
+ 
+         super.stepOn(level, pos, state, entity);
+@@ -57,7 +_,7 @@
+         if (level.isClientSide) {
+             spawnParticles(level, pos);
+         } else {
+-            interact(state, level, pos);
++            interact(state, level, pos, player); // CraftBukkit - add entityhuman
+         }
+ 
+         return (InteractionResult)(stack.getItem() instanceof BlockItem && new BlockPlaceContext(player, hand, stack, hitResult).canPlace()
+@@ -65,9 +_,14 @@
+             : InteractionResult.SUCCESS);
+     }
+ 
+-    private static void interact(BlockState state, Level level, BlockPos pos) {
++    private static void interact(BlockState state, Level level, BlockPos pos, Entity entity) { // CraftBukkit - add Entity
+         spawnParticles(level, pos);
+         if (!state.getValue(LIT)) {
++            // CraftBukkit start
++            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(entity, pos, state.setValue(RedStoneOreBlock.LIT, true))) {
++                return;
++            }
++            // CraftBukkit end
+             level.setBlock(pos, state.setValue(LIT, Boolean.valueOf(true)), 3);
+         }
+     }
+@@ -80,6 +_,11 @@
+     @Override
+     protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+         if (state.getValue(LIT)) {
++            // CraftBukkit start
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(level, pos, state.setValue(RedStoneOreBlock.LIT, false)).isCancelled()) {
++                return;
++            }
++            // CraftBukkit end
+             level.setBlock(pos, state.setValue(LIT, Boolean.valueOf(false)), 3);
+         }
+     }
+@@ -87,9 +_,17 @@
+     @Override
+     protected void spawnAfterBreak(BlockState state, ServerLevel level, BlockPos pos, ItemStack stack, boolean dropExperience) {
+         super.spawnAfterBreak(state, level, pos, stack, dropExperience);
+-        if (dropExperience) {
+-            this.tryDropExperience(level, pos, stack, UniformInt.of(1, 5));
++        // CraftBukkit start - Delegated to getExpDrop
++    }
++
++    @Override
++    public int getExpDrop(BlockState iblockdata, ServerLevel worldserver, BlockPos blockposition, ItemStack itemstack, boolean flag) {
++        if (flag) {
++            return this.tryDropExperience(worldserver, blockposition, itemstack, UniformInt.of(1, 5));
+         }
++
++        return 0;
++        // CraftBukkit end
+     }
+ 
+     @Override
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/RedstoneLampBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/RedstoneLampBlock.java.patch
new file mode 100644
index 0000000000..32b4488b6d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/RedstoneLampBlock.java.patch
@@ -0,0 +1,26 @@
+--- a/net/minecraft/world/level/block/RedstoneLampBlock.java
++++ b/net/minecraft/world/level/block/RedstoneLampBlock.java
+@@ -41,6 +_,11 @@
+                 if (litValue) {
+                     level.scheduleTick(pos, this, 4);
+                 } else {
++                    // CraftBukkit start
++                    if (org.bukkit.craftbukkit.event.CraftEventFactory.callRedstoneChange(level, pos, 0, 15).getNewCurrent() != 15) {
++                        return;
++                    }
++                    // CraftBukkit end
+                     level.setBlock(pos, state.cycle(LIT), 2);
+                 }
+             }
+@@ -50,6 +_,11 @@
+     @Override
+     protected void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+         if (state.getValue(LIT) && !level.hasNeighborSignal(pos)) {
++            // CraftBukkit start
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.callRedstoneChange(level, pos, 15, 0).getNewCurrent() != 0) {
++                return;
++            }
++            // CraftBukkit end
+             level.setBlock(pos, state.cycle(LIT), 2);
+         }
+     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/RedstoneTorchBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/RedstoneTorchBlock.java.patch
similarity index 55%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/RedstoneTorchBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/RedstoneTorchBlock.java.patch
index 69d1dcb6e7..d8efa215bb 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/RedstoneTorchBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/RedstoneTorchBlock.java.patch
@@ -1,47 +1,42 @@
 --- a/net/minecraft/world/level/block/RedstoneTorchBlock.java
 +++ b/net/minecraft/world/level/block/RedstoneTorchBlock.java
-@@ -22,11 +22,13 @@
- import net.minecraft.world.level.redstone.ExperimentalRedstoneUtils;
- import net.minecraft.world.level.redstone.Orientation;
- 
-+import org.bukkit.event.block.BlockRedstoneEvent; // CraftBukkit
-+
+@@ -24,7 +_,7 @@
  public class RedstoneTorchBlock extends BaseTorchBlock {
- 
      public static final MapCodec<RedstoneTorchBlock> CODEC = simpleCodec(RedstoneTorchBlock::new);
      public static final BooleanProperty LIT = BlockStateProperties.LIT;
--    private static final Map<BlockGetter, List<RedstoneTorchBlock.Toggle>> RECENT_TOGGLES = new WeakHashMap();
+-    private static final Map<BlockGetter, List<RedstoneTorchBlock.Toggle>> RECENT_TOGGLES = new WeakHashMap<>();
 +    // Paper - Faster redstone torch rapid clock removal; Move the mapped list to World
      public static final int RECENT_TOGGLE_TIMER = 60;
      public static final int MAX_RECENT_TOGGLES = 8;
      public static final int RESTART_DELAY = 160;
-@@ -79,14 +81,34 @@
+@@ -72,14 +_,34 @@
      @Override
-     protected void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
-         boolean flag = this.hasNeighborSignal(world, pos, state);
--        List<RedstoneTorchBlock.Toggle> list = (List) RedstoneTorchBlock.RECENT_TOGGLES.get(world);
+     protected void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+         boolean hasNeighborSignal = this.hasNeighborSignal(level, pos, state);
+-        List<RedstoneTorchBlock.Toggle> list = RECENT_TOGGLES.get(level);
 -
--        while (list != null && !list.isEmpty() && world.getGameTime() - ((RedstoneTorchBlock.Toggle) list.get(0)).when > 60L) {
+-        while (list != null && !list.isEmpty() && level.getGameTime() - list.get(0).when > 60L) {
 -            list.remove(0);
 +        // Paper start - Faster redstone torch rapid clock removal
-+        java.util.ArrayDeque<RedstoneTorchBlock.Toggle> redstoneUpdateInfos = world.redstoneUpdateInfos;
++        java.util.ArrayDeque<RedstoneTorchBlock.Toggle> redstoneUpdateInfos = level.redstoneUpdateInfos;
 +        if (redstoneUpdateInfos != null) {
 +            RedstoneTorchBlock.Toggle curr;
-+            while ((curr = redstoneUpdateInfos.peek()) != null && world.getGameTime() - curr.when > 60L) {
++            while ((curr = redstoneUpdateInfos.peek()) != null && level.getGameTime() - curr.when > 60L) {
 +                redstoneUpdateInfos.poll();
 +            }
          }
+-
 +        // Paper end - Faster redstone torch rapid clock removal
- 
++
 +        // CraftBukkit start
-+        org.bukkit.plugin.PluginManager manager = world.getCraftServer().getPluginManager();
-+        org.bukkit.block.Block block = world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ());
++        org.bukkit.plugin.PluginManager manager = level.getCraftServer().getPluginManager();
++        org.bukkit.block.Block block = level.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ());
 +        int oldCurrent = ((Boolean) state.getValue(RedstoneTorchBlock.LIT)).booleanValue() ? 15 : 0;
 +
-+        BlockRedstoneEvent event = new BlockRedstoneEvent(block, oldCurrent, oldCurrent);
++        org.bukkit.event.block.BlockRedstoneEvent event = new org.bukkit.event.block.BlockRedstoneEvent(block, oldCurrent, oldCurrent);
 +        // CraftBukkit end
-         if ((Boolean) state.getValue(RedstoneTorchBlock.LIT)) {
-             if (flag) {
+         if (state.getValue(LIT)) {
+             if (hasNeighborSignal) {
 +                // CraftBukkit start
 +                if (oldCurrent != 0) {
 +                    event.setNewCurrent(0);
@@ -51,13 +46,13 @@
 +                    }
 +                }
 +                // CraftBukkit end
-                 world.setBlock(pos, (BlockState) state.setValue(RedstoneTorchBlock.LIT, false), 3);
-                 if (RedstoneTorchBlock.isToggledTooFrequently(world, pos, true)) {
-                     world.levelEvent(1502, pos, 0);
-@@ -94,6 +116,15 @@
+                 level.setBlock(pos, state.setValue(LIT, Boolean.valueOf(false)), 3);
+                 if (isToggledTooFrequently(level, pos, true)) {
+                     level.levelEvent(1502, pos, 0);
+@@ -87,6 +_,15 @@
                  }
              }
-         } else if (!flag && !RedstoneTorchBlock.isToggledTooFrequently(world, pos, false)) {
+         } else if (!hasNeighborSignal && !isToggledTooFrequently(level, pos, false)) {
 +            // CraftBukkit start
 +            if (oldCurrent != 15) {
 +                event.setNewCurrent(15);
@@ -67,22 +62,20 @@
 +                }
 +            }
 +            // CraftBukkit end
-             world.setBlock(pos, (BlockState) state.setValue(RedstoneTorchBlock.LIT, true), 3);
+             level.setBlock(pos, state.setValue(LIT, Boolean.valueOf(true)), 3);
          }
- 
-@@ -134,9 +165,12 @@
+     }
+@@ -124,7 +_,12 @@
      }
  
-     private static boolean isToggledTooFrequently(Level world, BlockPos pos, boolean addNew) {
--        List<RedstoneTorchBlock.Toggle> list = (List) RedstoneTorchBlock.RECENT_TOGGLES.computeIfAbsent(world, (iblockaccess) -> {
--            return Lists.newArrayList();
--        });
+     private static boolean isToggledTooFrequently(Level level, BlockPos pos, boolean logToggle) {
+-        List<RedstoneTorchBlock.Toggle> list = RECENT_TOGGLES.computeIfAbsent(level, toggles -> Lists.newArrayList());
 +        // Paper start - Faster redstone torch rapid clock removal
-+        java.util.ArrayDeque<RedstoneTorchBlock.Toggle> list = world.redstoneUpdateInfos;
++        java.util.ArrayDeque<RedstoneTorchBlock.Toggle> list = level.redstoneUpdateInfos;
 +        if (list == null) {
-+            list = world.redstoneUpdateInfos = new java.util.ArrayDeque<>();
++            list = level.redstoneUpdateInfos = new java.util.ArrayDeque<>();
 +        }
 +        // Paper end - Faster redstone torch rapid clock removal
- 
-         if (addNew) {
-             list.add(new RedstoneTorchBlock.Toggle(pos.immutable(), world.getGameTime()));
+         if (logToggle) {
+             list.add(new RedstoneTorchBlock.Toggle(pos.immutable(), level.getGameTime()));
+         }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/RespawnAnchorBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/RespawnAnchorBlock.java.patch
new file mode 100644
index 0000000000..17ec642eda
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/RespawnAnchorBlock.java.patch
@@ -0,0 +1,37 @@
+--- a/net/minecraft/world/level/block/RespawnAnchorBlock.java
++++ b/net/minecraft/world/level/block/RespawnAnchorBlock.java
+@@ -102,11 +_,16 @@
+             if (!level.isClientSide) {
+                 ServerPlayer serverPlayer = (ServerPlayer)player;
+                 if (serverPlayer.getRespawnDimension() != level.dimension() || !pos.equals(serverPlayer.getRespawnPosition())) {
+-                    serverPlayer.setRespawnPosition(level.dimension(), pos, 0.0F, false, true);
++                    if (serverPlayer.setRespawnPosition(level.dimension(), pos, 0.0F, false, true, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.RESPAWN_ANCHOR)) { // Paper - Add PlayerSetSpawnEvent
+                     level.playSound(
+                         null, pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, SoundEvents.RESPAWN_ANCHOR_SET_SPAWN, SoundSource.BLOCKS, 1.0F, 1.0F
+                     );
+                     return InteractionResult.SUCCESS_SERVER;
++                    // Paper start - Add PlayerSetSpawnEvent
++                    } else {
++                        return InteractionResult.FAIL;
++                    }
++                    // Paper end - Add PlayerSetSpawnEvent
+                 }
+             }
+ 
+@@ -140,6 +_,7 @@
+     }
+ 
+     private void explode(BlockState state, Level level, final BlockPos pos2) {
++        org.bukkit.block.BlockState blockState = org.bukkit.craftbukkit.block.CraftBlock.at(level, pos2).getState(); // CraftBukkit - capture BlockState before remove block
+         level.removeBlock(pos2, false);
+         boolean flag = Direction.Plane.HORIZONTAL.stream().map(pos2::relative).anyMatch(pos -> isWaterThatWouldFlow(pos, level));
+         final boolean flag1 = flag || level.getFluidState(pos2.above()).is(FluidTags.WATER);
+@@ -153,7 +_,7 @@
+         };
+         Vec3 center = pos2.getCenter();
+         level.explode(
+-            null, level.damageSources().badRespawnPointExplosion(center), explosionDamageCalculator, center, 5.0F, true, Level.ExplosionInteraction.BLOCK
++            null, level.damageSources().badRespawnPointExplosion(center, blockState), explosionDamageCalculator, center, 5.0F, true, Level.ExplosionInteraction.BLOCK // CraftBukkit - add state
+         );
+     }
+ 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/RootedDirtBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/RootedDirtBlock.java.patch
similarity index 60%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/RootedDirtBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/RootedDirtBlock.java.patch
index de8d7301e4..6a3b2066a8 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/RootedDirtBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/RootedDirtBlock.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/world/level/block/RootedDirtBlock.java
 +++ b/net/minecraft/world/level/block/RootedDirtBlock.java
-@@ -34,7 +34,7 @@
+@@ -33,7 +_,7 @@
  
      @Override
-     public void performBonemeal(ServerLevel world, RandomSource random, BlockPos pos, BlockState state) {
--        world.setBlockAndUpdate(pos.below(), Blocks.HANGING_ROOTS.defaultBlockState());
-+        org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(world, pos, pos.below(), Blocks.HANGING_ROOTS.defaultBlockState()); // CraftBukkit
+     public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) {
+-        level.setBlockAndUpdate(pos.below(), Blocks.HANGING_ROOTS.defaultBlockState());
++        org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(level, pos, pos.below(), Blocks.HANGING_ROOTS.defaultBlockState()); // CraftBukkit
      }
  
      @Override
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/SaplingBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/SaplingBlock.java.patch
new file mode 100644
index 0000000000..e165ace2fb
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/SaplingBlock.java.patch
@@ -0,0 +1,54 @@
+--- a/net/minecraft/world/level/block/SaplingBlock.java
++++ b/net/minecraft/world/level/block/SaplingBlock.java
+@@ -26,6 +_,7 @@
+     protected static final float AABB_OFFSET = 6.0F;
+     protected static final VoxelShape SHAPE = Block.box(2.0, 0.0, 2.0, 14.0, 12.0, 14.0);
+     protected final TreeGrower treeGrower;
++    public static org.bukkit.TreeType treeType; // CraftBukkit
+ 
+     @Override
+     public MapCodec<? extends SaplingBlock> codec() {
+@@ -45,7 +_,7 @@
+ 
+     @Override
+     protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+-        if (level.getMaxLocalRawBrightness(pos.above()) >= 9 && random.nextInt(7) == 0) {
++        if (level.getMaxLocalRawBrightness(pos.above()) >= 9 && random.nextFloat() < (level.spigotConfig.saplingModifier / (100.0f * 7))) { // Spigot - SPIGOT-7159: Better modifier resolution
+             this.advanceTree(level, pos, state, random);
+         }
+     }
+@@ -54,7 +_,33 @@
+         if (state.getValue(STAGE) == 0) {
+             level.setBlock(pos, state.cycle(STAGE), 4);
+         } else {
+-            this.treeGrower.growTree(level, level.getChunkSource().getGenerator(), pos, state, random);
++            // CraftBukkit start
++            if (level.captureTreeGeneration) {
++                this.treeGrower.growTree(level, level.getChunkSource().getGenerator(), pos, state, random);
++            } else {
++                level.captureTreeGeneration = true;
++                this.treeGrower.growTree(level, level.getChunkSource().getGenerator(), pos, state, random);
++                level.captureTreeGeneration = false;
++                if (!level.capturedBlockStates.isEmpty()) {
++                    org.bukkit.TreeType treeType = SaplingBlock.treeType;
++                    SaplingBlock.treeType = null;
++                    org.bukkit.Location location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(pos, level.getWorld());
++                    java.util.List<org.bukkit.block.BlockState> blocks = new java.util.ArrayList<>(level.capturedBlockStates.values());
++                    level.capturedBlockStates.clear();
++                    org.bukkit.event.world.StructureGrowEvent event = null;
++                    if (treeType != null) {
++                        event = new org.bukkit.event.world.StructureGrowEvent(location, treeType, false, null, blocks);
++                        org.bukkit.Bukkit.getPluginManager().callEvent(event);
++                    }
++                    if (event == null || !event.isCancelled()) {
++                        for (org.bukkit.block.BlockState blockstate : blocks) {
++                            org.bukkit.craftbukkit.block.CapturedBlockState.setBlockState(blockstate);
++                            level.checkCapturedTreeStateForObserverNotify(pos, (org.bukkit.craftbukkit.block.CraftBlockState) blockstate); // Paper - notify observers even if grow failed
++                        }
++                    }
++                }
++            }
++            // CraftBukkit end
+         }
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/ScaffoldingBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/ScaffoldingBlock.java.patch
new file mode 100644
index 0000000000..fceeff2d27
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/ScaffoldingBlock.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/level/block/ScaffoldingBlock.java
++++ b/net/minecraft/world/level/block/ScaffoldingBlock.java
+@@ -119,7 +_,7 @@
+     protected void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+         int distance = getDistance(level, pos);
+         BlockState blockState = state.setValue(DISTANCE, Integer.valueOf(distance)).setValue(BOTTOM, Boolean.valueOf(this.isBottom(level, pos, distance)));
+-        if (blockState.getValue(DISTANCE) == 7) {
++        if (blockState.getValue(DISTANCE) == 7 && !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(level, pos, blockState.getFluidState().createLegacyBlock()).isCancelled()) { // CraftBukkit - BlockFadeEvent // Paper - fix wrong block state
+             if (state.getValue(DISTANCE) == 7) {
+                 FallingBlockEntity.fall(level, pos, blockState);
+             } else {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/SculkBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/SculkBlock.java.patch
new file mode 100644
index 0000000000..f17c4e7aa6
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/SculkBlock.java.patch
@@ -0,0 +1,13 @@
+--- a/net/minecraft/world/level/block/SculkBlock.java
++++ b/net/minecraft/world/level/block/SculkBlock.java
+@@ -37,8 +_,9 @@
+                 if (random.nextInt(growthSpawnCost) < charge) {
+                     BlockPos blockPos = pos1.above();
+                     BlockState randomGrowthState = this.getRandomGrowthState(level, blockPos, random, spreader.isWorldGeneration());
+-                    level.setBlock(blockPos, randomGrowthState, 3);
++                    if (org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(level, pos, blockPos, randomGrowthState, 3)) { // CraftBukkit - Call BlockSpreadEvent
+                     level.playSound(null, pos1, randomGrowthState.getSoundType().getPlaceSound(), SoundSource.BLOCKS, 1.0F, 1.0F);
++                    } // CraftBukkit - Call BlockSpreadEvent
+                 }
+ 
+                 return Math.max(0, charge - growthSpawnCost);
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/SculkCatalystBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/SculkCatalystBlock.java.patch
new file mode 100644
index 0000000000..5ba297feff
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/SculkCatalystBlock.java.patch
@@ -0,0 +1,21 @@
+--- a/net/minecraft/world/level/block/SculkCatalystBlock.java
++++ b/net/minecraft/world/level/block/SculkCatalystBlock.java
+@@ -61,8 +_,16 @@
+     @Override
+     protected void spawnAfterBreak(BlockState state, ServerLevel level, BlockPos pos, ItemStack stack, boolean dropExperience) {
+         super.spawnAfterBreak(state, level, pos, stack, dropExperience);
++        // CraftBukkit start - Delegate to getExpDrop
++    }
++
++    @Override
++    public int getExpDrop(BlockState state, ServerLevel level, BlockPos pos, ItemStack stack, boolean dropExperience) {
+         if (dropExperience) {
+-            this.tryDropExperience(level, pos, stack, this.xpRange);
++            return this.tryDropExperience(level, pos, stack, this.xpRange);
+         }
+-    }
++
++        return 0;
++        // CraftBukkit end
++     }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/SculkSensorBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/SculkSensorBlock.java.patch
new file mode 100644
index 0000000000..900eb7905e
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/SculkSensorBlock.java.patch
@@ -0,0 +1,77 @@
+--- a/net/minecraft/world/level/block/SculkSensorBlock.java
++++ b/net/minecraft/world/level/block/SculkSensorBlock.java
+@@ -108,6 +_,18 @@
+             && level.getBlockEntity(pos) instanceof SculkSensorBlockEntity sculkSensorBlockEntity
+             && level instanceof ServerLevel serverLevel
+             && sculkSensorBlockEntity.getVibrationUser().canReceiveVibration(serverLevel, pos, GameEvent.STEP, GameEvent.Context.of(state))) {
++            // CraftBukkit start
++            org.bukkit.event.Cancellable cancellable;
++            if (entity instanceof net.minecraft.world.entity.player.Player player) {
++                cancellable = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerInteractEvent(player, org.bukkit.event.block.Action.PHYSICAL, pos, null, null, null);
++            } else {
++                cancellable = new org.bukkit.event.entity.EntityInteractEvent(entity.getBukkitEntity(), level.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ()));
++                level.getCraftServer().getPluginManager().callEvent((org.bukkit.event.entity.EntityInteractEvent) cancellable);
++            }
++            if (cancellable.isCancelled()) {
++                return;
++            }
++            // CraftBukkit end
+             sculkSensorBlockEntity.getListener().forceScheduleVibration(serverLevel, GameEvent.STEP, GameEvent.Context.of(entity), entity.position());
+         }
+ 
+@@ -200,10 +_,19 @@
+     }
+ 
+     public static boolean canActivate(BlockState state) {
+-        return getPhase(state) == SculkSensorPhase.INACTIVE;
++        return state.getBlock() instanceof SculkSensorBlock &&  getPhase(state) == SculkSensorPhase.INACTIVE; // Paper - Check for a valid type
+     }
+ 
+     public static void deactivate(Level level, BlockPos pos, BlockState state) {
++        // CraftBukkit start
++        org.bukkit.event.block.BlockRedstoneEvent eventRedstone = new org.bukkit.event.block.BlockRedstoneEvent(org.bukkit.craftbukkit.block.CraftBlock.at(level, pos), state.getValue(SculkSensorBlock.POWER), 0);
++        level.getCraftServer().getPluginManager().callEvent(eventRedstone);
++
++        if (eventRedstone.getNewCurrent() > 0) {
++            level.setBlock(pos, state.setValue(SculkSensorBlock.POWER, eventRedstone.getNewCurrent()), 3);
++            return;
++        }
++        // CraftBukkit end
+         level.setBlock(pos, state.setValue(PHASE, SculkSensorPhase.COOLDOWN).setValue(POWER, Integer.valueOf(0)), 3);
+         level.scheduleTick(pos, state.getBlock(), 10);
+         updateNeighbours(level, pos, state);
+@@ -215,6 +_,15 @@
+     }
+ 
+     public void activate(@Nullable Entity entity, Level level, BlockPos pos, BlockState state, int power, int frequency) {
++        // CraftBukkit start
++        org.bukkit.event.block.BlockRedstoneEvent eventRedstone = new org.bukkit.event.block.BlockRedstoneEvent(org.bukkit.craftbukkit.block.CraftBlock.at(level, pos), state.getValue(SculkSensorBlock.POWER), power);
++        level.getCraftServer().getPluginManager().callEvent(eventRedstone);
++
++        if (eventRedstone.getNewCurrent() <= 0) {
++            return;
++        }
++        power = eventRedstone.getNewCurrent();
++        // CraftBukkit end
+         level.setBlock(pos, state.setValue(PHASE, SculkSensorPhase.ACTIVE).setValue(POWER, Integer.valueOf(power)), 3);
+         level.scheduleTick(pos, state.getBlock(), this.getActiveTicks());
+         updateNeighbours(level, pos, state);
+@@ -292,8 +_,16 @@
+     @Override
+     protected void spawnAfterBreak(BlockState state, ServerLevel level, BlockPos pos, ItemStack stack, boolean dropExperience) {
+         super.spawnAfterBreak(state, level, pos, stack, dropExperience);
++        // CraftBukkit start - Delegate to getExpDrop
++    }
++
++    @Override
++    public int getExpDrop(BlockState state, ServerLevel level, BlockPos pos, ItemStack stack, boolean dropExperience) {
+         if (dropExperience) {
+-            this.tryDropExperience(level, pos, stack, ConstantInt.of(5));
++            return this.tryDropExperience(level, pos, stack, ConstantInt.of(5));
+         }
+-    }
++
++        return 0;
++        // CraftBukkit end
++     }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/SculkShriekerBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/SculkShriekerBlock.java.patch
new file mode 100644
index 0000000000..ff23c12bec
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/SculkShriekerBlock.java.patch
@@ -0,0 +1,28 @@
+--- a/net/minecraft/world/level/block/SculkShriekerBlock.java
++++ b/net/minecraft/world/level/block/SculkShriekerBlock.java
+@@ -66,6 +_,7 @@
+         if (level instanceof ServerLevel serverLevel) {
+             ServerPlayer serverPlayer = SculkShriekerBlockEntity.tryGetPlayer(entity);
+             if (serverPlayer != null) {
++                if (org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerInteractEvent(serverPlayer, org.bukkit.event.block.Action.PHYSICAL, pos, null, null, null).isCancelled()) return; // CraftBukkit
+                 serverLevel.getBlockEntity(pos, BlockEntityType.SCULK_SHRIEKER).ifPresent(sculkShrieker -> sculkShrieker.tryShriek(serverLevel, serverPlayer));
+             }
+         }
+@@ -144,9 +_,16 @@
+     @Override
+     protected void spawnAfterBreak(BlockState state, ServerLevel level, BlockPos pos, ItemStack stack, boolean dropExperience) {
+         super.spawnAfterBreak(state, level, pos, stack, dropExperience);
++        // CraftBukkit start - Delegate to getExpDrop
++    }
++
++    @Override
++    public int getExpDrop(BlockState state, ServerLevel level, BlockPos pos, ItemStack stack, boolean dropExperience) {
+         if (dropExperience) {
+-            this.tryDropExperience(level, pos, stack, ConstantInt.of(5));
++            return this.tryDropExperience(level, pos, stack, ConstantInt.of(5));
+         }
++        return 0;
++        // CraftBukkit end
+     }
+ 
+     @Nullable
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/SculkSpreader.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/SculkSpreader.java.patch
new file mode 100644
index 0000000000..1d958f6ac9
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/SculkSpreader.java.patch
@@ -0,0 +1,46 @@
+--- a/net/minecraft/world/level/block/SculkSpreader.java
++++ b/net/minecraft/world/level/block/SculkSpreader.java
+@@ -50,6 +_,7 @@
+     private final int additionalDecayRate;
+     private List<SculkSpreader.ChargeCursor> cursors = new ArrayList<>();
+     private static final Logger LOGGER = LogUtils.getLogger();
++    public net.minecraft.world.level.Level level; // CraftBukkit
+ 
+     public SculkSpreader(
+         boolean isWorldGeneration, TagKey<Block> replaceableBlocks, int growthSpawnCoat, int noGrowthRadius, int chargeDecayRate, int additionalDecayRate
+@@ -114,7 +_,7 @@
+             int min = Math.min(list.size(), 32);
+ 
+             for (int i = 0; i < min; i++) {
+-                this.addCursor(list.get(i));
++                this.addCursor(list.get(i), false); // Paper - don't fire event for block entity loading
+             }
+         }
+     }
+@@ -130,13 +_,24 @@
+     public void addCursors(BlockPos pos, int charge) {
+         while (charge > 0) {
+             int min = Math.min(charge, 1000);
+-            this.addCursor(new SculkSpreader.ChargeCursor(pos, min));
++            this.addCursor(new SculkSpreader.ChargeCursor(pos, min), true); // Paper - allow firing event for other causes
+             charge -= min;
+         }
+     }
+ 
+-    private void addCursor(SculkSpreader.ChargeCursor cursor) {
++    private void addCursor(SculkSpreader.ChargeCursor cursor, boolean fireEvent) { // Paper - add boolean to conditionally fire SculkBloomEvent
+         if (this.cursors.size() < 32) {
++            // CraftBukkit start
++            if (!this.isWorldGeneration() && fireEvent) { // CraftBukkit - SPIGOT-7475: Don't call event during world generation // Paper - add boolean to conditionally fire SculkBloomEvent
++                org.bukkit.craftbukkit.block.CraftBlock bukkitBlock = org.bukkit.craftbukkit.block.CraftBlock.at(this.level, cursor.pos);
++                org.bukkit.event.block.SculkBloomEvent event = new org.bukkit.event.block.SculkBloomEvent(bukkitBlock, cursor.getCharge());
++                if (!event.callEvent()) {
++                    return;
++                }
++
++                cursor.charge = event.getCharge();
++            }
++            // CraftBukkit end
+             this.cursors.add(cursor);
+         }
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/SculkVeinBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/SculkVeinBlock.java.patch
new file mode 100644
index 0000000000..bc1a1f3a48
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/SculkVeinBlock.java.patch
@@ -0,0 +1,32 @@
+--- a/net/minecraft/world/level/block/SculkVeinBlock.java
++++ b/net/minecraft/world/level/block/SculkVeinBlock.java
+@@ -90,14 +_,14 @@
+     public int attemptUseCharge(
+         SculkSpreader.ChargeCursor cursor, LevelAccessor level, BlockPos pos, RandomSource random, SculkSpreader spreader, boolean shouldConvertBlocks
+     ) {
+-        if (shouldConvertBlocks && this.attemptPlaceSculk(spreader, level, cursor.getPos(), random)) {
++        if (shouldConvertBlocks && this.attemptPlaceSculk(spreader, level, cursor.getPos(), random, pos)) { // CraftBukkit - add source block
+             return cursor.getCharge() - 1;
+         } else {
+             return random.nextInt(spreader.chargeDecayRate()) == 0 ? Mth.floor(cursor.getCharge() * 0.5F) : cursor.getCharge();
+         }
+     }
+ 
+-    private boolean attemptPlaceSculk(SculkSpreader spreader, LevelAccessor level, BlockPos pos, RandomSource random) {
++    private boolean attemptPlaceSculk(SculkSpreader spreader, LevelAccessor level, BlockPos pos, RandomSource random, BlockPos sourceBlock) { // CraftBukkit
+         BlockState blockState = level.getBlockState(pos);
+         TagKey<Block> tagKey = spreader.replaceableBlocks();
+ 
+@@ -107,7 +_,11 @@
+                 BlockState blockState1 = level.getBlockState(blockPos);
+                 if (blockState1.is(tagKey)) {
+                     BlockState blockState2 = Blocks.SCULK.defaultBlockState();
+-                    level.setBlock(blockPos, blockState2, 3);
++                    // CraftBukkit start - Call BlockSpreadEvent
++                    if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(level, sourceBlock, blockPos, blockState2, 3)) {
++                        return false;
++                    }
++                    // CraftBukkit end
+                     Block.pushEntitiesUp(blockState1, blockState2, level, blockPos);
+                     level.playSound(null, blockPos, SoundEvents.SCULK_BLOCK_SPREAD, SoundSource.BLOCKS, 1.0F, 1.0F);
+                     this.veinSpreader.spreadAll(blockState2, level, blockPos, spreader.isWorldGeneration());
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/ShulkerBoxBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/ShulkerBoxBlock.java.patch
similarity index 67%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/ShulkerBoxBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/ShulkerBoxBlock.java.patch
index ef705416f0..148aaa4fdc 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/ShulkerBoxBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/ShulkerBoxBlock.java.patch
@@ -1,48 +1,48 @@
 --- a/net/minecraft/world/level/block/ShulkerBoxBlock.java
 +++ b/net/minecraft/world/level/block/ShulkerBoxBlock.java
-@@ -98,8 +98,8 @@
-     protected InteractionResult useWithoutItem(BlockState state, Level world, BlockPos pos, Player player, BlockHitResult hit) {
-         if (world instanceof ServerLevel serverLevel
-             && world.getBlockEntity(pos) instanceof ShulkerBoxBlockEntity shulkerBoxBlockEntity
--            && canOpen(state, world, pos, shulkerBoxBlockEntity)) {
+@@ -100,8 +_,8 @@
+     protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) {
+         if (level instanceof ServerLevel serverLevel
+             && level.getBlockEntity(pos) instanceof ShulkerBoxBlockEntity shulkerBoxBlockEntity
+-            && canOpen(state, level, pos, shulkerBoxBlockEntity)) {
 -            player.openMenu(shulkerBoxBlockEntity);
-+            && canOpen(state, world, pos, shulkerBoxBlockEntity) // Paper - Fix InventoryOpenEvent cancellation - expand if for belows check
-+            && player.openMenu(shulkerBoxBlockEntity).isPresent()) {  // Paper - Fix InventoryOpenEvent cancellation
++            && canOpen(state, level, pos, shulkerBoxBlockEntity) // Paper - Fix InventoryOpenEvent cancellation - expand if for belows check
++            && player.openMenu(shulkerBoxBlockEntity).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
              player.awardStat(Stats.OPEN_SHULKER_BOX);
              PiglinAi.angerNearbyPiglins(serverLevel, player, true);
          }
-@@ -137,7 +137,7 @@
+@@ -139,7 +_,7 @@
                  itemEntity.setDefaultPickUpDelay();
-                 world.addFreshEntity(itemEntity);
+                 level.addFreshEntity(itemEntity);
              } else {
 -                shulkerBoxBlockEntity.unpackLootTable(player);
 +                shulkerBoxBlockEntity.unpackLootTable(player, true); // Paper - force clear loot table so replenish data isn't persisted in the stack
              }
          }
  
-@@ -147,7 +147,15 @@
+@@ -149,7 +_,15 @@
      @Override
-     protected List<ItemStack> getDrops(BlockState state, LootParams.Builder builder) {
-         BlockEntity blockEntity = builder.getOptionalParameter(LootContextParams.BLOCK_ENTITY);
+     protected List<ItemStack> getDrops(BlockState state, LootParams.Builder params) {
+         BlockEntity blockEntity = params.getOptionalParameter(LootContextParams.BLOCK_ENTITY);
 +        Runnable reAdd = null; // Paper
          if (blockEntity instanceof ShulkerBoxBlockEntity shulkerBoxBlockEntity) {
 +            // Paper start - clear loot table if it was already used
-+            if (shulkerBoxBlockEntity.lootableData().getLastFill() != -1 || !builder.getLevel().paperConfig().lootables.retainUnlootedShulkerBoxLootTableOnNonPlayerBreak) {
++            if (shulkerBoxBlockEntity.lootableData().getLastFill() != -1 || !params.getLevel().paperConfig().lootables.retainUnlootedShulkerBoxLootTableOnNonPlayerBreak) {
 +                net.minecraft.resources.ResourceKey<net.minecraft.world.level.storage.loot.LootTable> lootTableResourceKey = shulkerBoxBlockEntity.getLootTable();
 +                reAdd = () -> shulkerBoxBlockEntity.setLootTable(lootTableResourceKey);
 +                shulkerBoxBlockEntity.setLootTable(null);
 +            }
 +            // Paper end
-             builder = builder.withDynamicDrop(CONTENTS, lootConsumer -> {
+             params = params.withDynamicDrop(CONTENTS, output -> {
                  for (int i = 0; i < shulkerBoxBlockEntity.getContainerSize(); i++) {
-                     lootConsumer.accept(shulkerBoxBlockEntity.getItem(i));
-@@ -155,7 +163,13 @@
+                     output.accept(shulkerBoxBlockEntity.getItem(i));
+@@ -157,7 +_,13 @@
              });
          }
  
 +        // Paper start - re-set loot table if it was cleared
 +        try {
-         return super.getDrops(state, builder);
+         return super.getDrops(state, params);
 +        } finally {
 +            if (reAdd != null) reAdd.run();
 +        }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/SignBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/SignBlock.java.patch
new file mode 100644
index 0000000000..8029d51d21
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/SignBlock.java.patch
@@ -0,0 +1,53 @@
+--- a/net/minecraft/world/level/block/SignBlock.java
++++ b/net/minecraft/world/level/block/SignBlock.java
+@@ -134,7 +_,7 @@
+             } else if (!this.otherPlayerIsEditingSign(player, signBlockEntity)
+                 && player.mayBuild()
+                 && this.hasEditableText(player, signBlockEntity, isFacingFrontText)) {
+-                this.openTextEdit(player, signBlockEntity, isFacingFrontText);
++                this.openTextEdit(player, signBlockEntity, isFacingFrontText, io.papermc.paper.event.player.PlayerOpenSignEvent.Cause.INTERACT); // Paper - Add PlayerOpenSignEvent
+                 return InteractionResult.SUCCESS_SERVER;
+             } else {
+                 return InteractionResult.PASS;
+@@ -176,7 +_,33 @@
+         return woodType;
+     }
+ 
++    @io.papermc.paper.annotation.DoNotUse @Deprecated // Paper - Add PlayerOpenSignEvent
+     public void openTextEdit(Player player, SignBlockEntity signEntity, boolean isFrontText) {
++        // Paper start - Add PlayerOpenSignEvent
++        this.openTextEdit(player, signEntity, isFrontText, io.papermc.paper.event.player.PlayerOpenSignEvent.Cause.UNKNOWN);
++    }
++    public void openTextEdit(Player player, SignBlockEntity signEntity, boolean isFrontText, io.papermc.paper.event.player.PlayerOpenSignEvent.Cause cause) {
++        org.bukkit.entity.Player bukkitPlayer = (org.bukkit.entity.Player) player.getBukkitEntity();
++        org.bukkit.block.Block bukkitBlock = org.bukkit.craftbukkit.block.CraftBlock.at(signEntity.getLevel(), signEntity.getBlockPos());
++        org.bukkit.craftbukkit.block.CraftSign<?> bukkitSign = (org.bukkit.craftbukkit.block.CraftSign<?>) org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(bukkitBlock);
++        io.papermc.paper.event.player.PlayerOpenSignEvent event = new io.papermc.paper.event.player.PlayerOpenSignEvent(
++            bukkitPlayer,
++            bukkitSign,
++            isFrontText ? org.bukkit.block.sign.Side.FRONT : org.bukkit.block.sign.Side.BACK,
++            cause);
++        if (!event.callEvent()) return;
++        if (org.bukkit.event.player.PlayerSignOpenEvent.getHandlerList().getRegisteredListeners().length > 0) {
++            final org.bukkit.event.player.PlayerSignOpenEvent.Cause legacyCause = switch (cause) {
++                case PLACE -> org.bukkit.event.player.PlayerSignOpenEvent.Cause.PLACE;
++                case PLUGIN -> org.bukkit.event.player.PlayerSignOpenEvent.Cause.PLUGIN;
++                case INTERACT -> org.bukkit.event.player.PlayerSignOpenEvent.Cause.INTERACT;
++                case UNKNOWN -> org.bukkit.event.player.PlayerSignOpenEvent.Cause.UNKNOWN;
++            };
++        if (!org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerSignOpenEvent(player, signEntity, isFrontText, legacyCause)) {
++        // Paper end - Add PlayerOpenSignEvent
++            return;
++        }
++        } // Paper - Add PlayerOpenSignEvent
+         signEntity.setAllowedPlayerEditor(player.getUUID());
+         player.openTextEdit(signEntity, isFrontText);
+     }
+@@ -189,6 +_,6 @@
+     @Nullable
+     @Override
+     public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> blockEntityType) {
+-        return createTickerHelper(blockEntityType, BlockEntityType.SIGN, SignBlockEntity::tick);
++        return null; // CraftBukkit - remove unnecessary sign ticking
+     }
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/SmithingTableBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/SmithingTableBlock.java.patch
similarity index 64%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/SmithingTableBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/SmithingTableBlock.java.patch
index e7724b2941..a2b4048bcb 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/SmithingTableBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/SmithingTableBlock.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/world/level/block/SmithingTableBlock.java
 +++ b/net/minecraft/world/level/block/SmithingTableBlock.java
-@@ -38,8 +38,9 @@
+@@ -38,8 +_,9 @@
      @Override
-     protected InteractionResult useWithoutItem(BlockState state, Level world, BlockPos pos, Player player, BlockHitResult hit) {
-         if (!world.isClientSide) {
--            player.openMenu(state.getMenuProvider(world, pos));
-+            if (player.openMenu(state.getMenuProvider(world, pos)).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
+     protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) {
+         if (!level.isClientSide) {
+-            player.openMenu(state.getMenuProvider(level, pos));
++            if (player.openMenu(state.getMenuProvider(level, pos)).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
              player.awardStat(Stats.INTERACT_WITH_SMITHING_TABLE);
 +            } // Paper - Fix InventoryOpenEvent cancellation
          }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/SmokerBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/SmokerBlock.java.patch
similarity index 51%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/SmokerBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/SmokerBlock.java.patch
index 1d94e2a7cc..27b9bafcfd 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/SmokerBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/SmokerBlock.java.patch
@@ -1,12 +1,12 @@
 --- a/net/minecraft/world/level/block/SmokerBlock.java
 +++ b/net/minecraft/world/level/block/SmokerBlock.java
-@@ -44,8 +44,7 @@
+@@ -44,8 +_,7 @@
      @Override
-     protected void openContainer(Level world, BlockPos pos, Player player) {
-         BlockEntity blockEntity = world.getBlockEntity(pos);
+     protected void openContainer(Level level, BlockPos pos, Player player) {
+         BlockEntity blockEntity = level.getBlockEntity(pos);
 -        if (blockEntity instanceof SmokerBlockEntity) {
 -            player.openMenu((MenuProvider)blockEntity);
-+        if (blockEntity instanceof SmokerBlockEntity && player.openMenu((MenuProvider)blockEntity).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
++        if (blockEntity instanceof SmokerBlockEntity smoker && player.openMenu(smoker).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
              player.awardStat(Stats.INTERACT_WITH_SMOKER);
          }
      }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/SnifferEggBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/SnifferEggBlock.java.patch
similarity index 55%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/SnifferEggBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/SnifferEggBlock.java.patch
index ff63f628fe..aa5c5e2817 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/SnifferEggBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/SnifferEggBlock.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/level/block/SnifferEggBlock.java
 +++ b/net/minecraft/world/level/block/SnifferEggBlock.java
-@@ -61,12 +61,31 @@
+@@ -61,12 +_,31 @@
          return this.getHatchLevel(state) == 2;
      }
  
@@ -13,41 +13,41 @@
 +    // Paper end - Call BlockFadeEvent
 +
      @Override
-     public void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
+     public void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
          if (!this.isReadyToHatch(state)) {
 +            // Paper start
-+            if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(world, pos, state.setValue(HATCH, Integer.valueOf(this.getHatchLevel(state) + 1)), 2)) {
-+                this.rescheduleTick(world, pos);
++            if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(level, pos, state.setValue(HATCH, Integer.valueOf(this.getHatchLevel(state) + 1)), 2)) {
++                this.rescheduleTick(level, pos);
 +                return;
 +            }
 +            // Paper end
-             world.playSound(null, pos, SoundEvents.SNIFFER_EGG_CRACK, SoundSource.BLOCKS, 0.7F, 0.9F + random.nextFloat() * 0.2F);
--            world.setBlock(pos, state.setValue(HATCH, Integer.valueOf(this.getHatchLevel(state) + 1)), 2);
+             level.playSound(null, pos, SoundEvents.SNIFFER_EGG_CRACK, SoundSource.BLOCKS, 0.7F, 0.9F + random.nextFloat() * 0.2F);
+-            level.setBlock(pos, state.setValue(HATCH, Integer.valueOf(this.getHatchLevel(state) + 1)), 2);
          } else {
 +            // Paper start - Call BlockFadeEvent
-+            if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(world, pos, state.getFluidState().createLegacyBlock()).isCancelled()) {
-+                this.rescheduleTick(world, pos);
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(level, pos, state.getFluidState().createLegacyBlock()).isCancelled()) {
++                this.rescheduleTick(level, pos);
 +                return;
 +            }
 +            // Paper end - Call BlockFadeEvent
-             world.playSound(null, pos, SoundEvents.SNIFFER_EGG_HATCH, SoundSource.BLOCKS, 0.7F, 0.9F + random.nextFloat() * 0.2F);
-             world.destroyBlock(pos, false);
-             Sniffer sniffer = EntityType.SNIFFER.create(world, EntitySpawnReason.BREEDING);
-@@ -74,7 +93,7 @@
-                 Vec3 vec3 = pos.getCenter();
+             level.playSound(null, pos, SoundEvents.SNIFFER_EGG_HATCH, SoundSource.BLOCKS, 0.7F, 0.9F + random.nextFloat() * 0.2F);
+             level.destroyBlock(pos, false);
+             Sniffer sniffer = EntityType.SNIFFER.create(level, EntitySpawnReason.BREEDING);
+@@ -74,7 +_,7 @@
+                 Vec3 center = pos.getCenter();
                  sniffer.setBaby(true);
-                 sniffer.moveTo(vec3.x(), vec3.y(), vec3.z(), Mth.wrapDegrees(world.random.nextFloat() * 360.0F), 0.0F);
--                world.addFreshEntity(sniffer);
-+                world.addFreshEntity(sniffer, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.EGG); // Paper - use correct spawn reason
+                 sniffer.moveTo(center.x(), center.y(), center.z(), Mth.wrapDegrees(level.random.nextFloat() * 360.0F), 0.0F);
+-                level.addFreshEntity(sniffer);
++                level.addFreshEntity(sniffer, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.EGG); // Paper - use correct spawn reason
              }
          }
      }
-@@ -86,7 +105,7 @@
-             world.levelEvent(3009, pos, 0);
+@@ -86,7 +_,7 @@
+             level.levelEvent(3009, pos, 0);
          }
  
--        int i = bl ? 12000 : 24000;
-+        int i = bl ? world.paperConfig().entities.sniffer.boostedHatchTime.or(BOOSTED_HATCH_TIME_TICKS) : world.paperConfig().entities.sniffer.hatchTime.or(REGULAR_HATCH_TIME_TICKS); // Paper
-         int j = i / 3;
-         world.gameEvent(GameEvent.BLOCK_PLACE, pos, GameEvent.Context.of(state));
-         world.scheduleTick(pos, this, j + world.random.nextInt(300));
+-        int i = flag ? 12000 : 24000;
++        int i = flag ? level.paperConfig().entities.sniffer.boostedHatchTime.or(BOOSTED_HATCH_TIME_TICKS) : level.paperConfig().entities.sniffer.hatchTime.or(REGULAR_HATCH_TIME_TICKS); // Paper
+         int i1 = i / 3;
+         level.gameEvent(GameEvent.BLOCK_PLACE, pos, GameEvent.Context.of(state));
+         level.scheduleTick(pos, this, i1 + level.random.nextInt(300));
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/SnowLayerBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/SnowLayerBlock.java.patch
new file mode 100644
index 0000000000..37be3cda95
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/SnowLayerBlock.java.patch
@@ -0,0 +1,14 @@
+--- a/net/minecraft/world/level/block/SnowLayerBlock.java
++++ b/net/minecraft/world/level/block/SnowLayerBlock.java
+@@ -123,6 +_,11 @@
+     @Override
+     protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+         if (level.getBrightness(LightLayer.BLOCK, pos) > 11) {
++            // CraftBukkit start
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(level, pos, Blocks.AIR.defaultBlockState()).isCancelled()) {
++                return;
++            }
++            // CraftBukkit end
+             dropResources(state, level, pos);
+             level.removeBlock(pos, false);
+         }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/SpawnerBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/SpawnerBlock.java.patch
new file mode 100644
index 0000000000..eebbd991a8
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/SpawnerBlock.java.patch
@@ -0,0 +1,24 @@
+--- a/net/minecraft/world/level/block/SpawnerBlock.java
++++ b/net/minecraft/world/level/block/SpawnerBlock.java
+@@ -46,11 +_,19 @@
+     @Override
+     protected void spawnAfterBreak(BlockState state, ServerLevel level, BlockPos pos, ItemStack stack, boolean dropExperience) {
+         super.spawnAfterBreak(state, level, pos, stack, dropExperience);
++        // CraftBukkit start - Delegate to getExpDrop
++    }
++
++    @Override
++    public int getExpDrop(BlockState state, ServerLevel level, BlockPos pos, ItemStack stack, boolean dropExperience) {
+         if (dropExperience) {
+             int i = 15 + level.random.nextInt(15) + level.random.nextInt(15);
+-            this.popExperience(level, pos, i);
++            // this.popExperience(level, pos, i);
++            return  i;
+         }
+-    }
++        return 0;
++        // CraftBukkit end
++     }
+ 
+     @Override
+     public void appendHoverText(ItemStack stack, Item.TooltipContext context, List<Component> tooltipComponents, TooltipFlag tooltipFlag) {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/SpongeBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/SpongeBlock.java.patch
new file mode 100644
index 0000000000..cba02b7710
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/SpongeBlock.java.patch
@@ -0,0 +1,96 @@
+--- a/net/minecraft/world/level/block/SpongeBlock.java
++++ b/net/minecraft/world/level/block/SpongeBlock.java
+@@ -50,7 +_,8 @@
+     }
+ 
+     private boolean removeWaterBreadthFirstSearch(Level level, BlockPos pos) {
+-        return BlockPos.breadthFirstTraversal(
++        org.bukkit.craftbukkit.util.BlockStateListPopulator blockList = new org.bukkit.craftbukkit.util.BlockStateListPopulator(level); // CraftBukkit - Use BlockStateListPopulator
++        BlockPos.breadthFirstTraversal(
+                 pos,
+                 6,
+                 65,
+@@ -63,16 +_,18 @@
+                     if (blockPos.equals(pos)) {
+                         return BlockPos.TraversalNodeStatus.ACCEPT;
+                     } else {
+-                        BlockState blockState = level.getBlockState(blockPos);
+-                        FluidState fluidState = level.getFluidState(blockPos);
++                        // CraftBukkit start
++                        BlockState blockState = blockList.getBlockState(blockPos);
++                        FluidState fluidState = blockList.getFluidState(blockPos);
++                        // CraftBukkit end
+                         if (!fluidState.is(FluidTags.WATER)) {
+                             return BlockPos.TraversalNodeStatus.SKIP;
+                         } else if (blockState.getBlock() instanceof BucketPickup bucketPickup
+-                            && !bucketPickup.pickupBlock(null, level, blockPos, blockState).isEmpty()) {
++                            && !bucketPickup.pickupBlock(null, blockList, blockPos, blockState).isEmpty()) { // CraftBukkit
+                             return BlockPos.TraversalNodeStatus.ACCEPT;
+                         } else {
+                             if (blockState.getBlock() instanceof LiquidBlock) {
+-                                level.setBlock(blockPos, Blocks.AIR.defaultBlockState(), 3);
++                                blockList.setBlock(blockPos, Blocks.AIR.defaultBlockState(), 3); // CraftBukkit
+                             } else {
+                                 if (!blockState.is(Blocks.KELP)
+                                     && !blockState.is(Blocks.KELP_PLANT)
+@@ -81,16 +_,55 @@
+                                     return BlockPos.TraversalNodeStatus.SKIP;
+                                 }
+ 
+-                                BlockEntity blockEntity = blockState.hasBlockEntity() ? level.getBlockEntity(blockPos) : null;
+-                                dropResources(blockState, level, blockPos, blockEntity);
+-                                level.setBlock(blockPos, Blocks.AIR.defaultBlockState(), 3);
++                                // CraftBukkit start
++                                // BlockEntity blockEntity = blockState.hasBlockEntity() ? level.getBlockEntity(blockPos) : null;
++                                // dropResources(blockState, level, blockPos, blockEntity);
++                                // level.setBlock(blockPos, Blocks.AIR.defaultBlockState(), 3);
++                                blockList.setBlock(blockPos, Blocks.AIR.defaultBlockState(), 3);
++                                // CraftBukkit end
+                             }
+ 
+                             return BlockPos.TraversalNodeStatus.ACCEPT;
+                         }
+                     }
+                 }
+-            )
+-            > 1;
++            );
++        // CraftBukkit start
++        java.util.List<org.bukkit.craftbukkit.block.CraftBlockState> blocks = blockList.getList(); // Is a clone
++        if (!blocks.isEmpty()) {
++            final org.bukkit.block.Block sponge = org.bukkit.craftbukkit.block.CraftBlock.at(level, pos);
++
++            org.bukkit.event.block.SpongeAbsorbEvent event = new org.bukkit.event.block.SpongeAbsorbEvent(sponge, (java.util.List<org.bukkit.block.BlockState>) (java.util.List) blocks);
++            if (!event.callEvent()) {
++                return false;
++            }
++
++            for (org.bukkit.craftbukkit.block.CraftBlockState block : blocks) {
++                BlockPos blockPos = block.getPosition();
++                BlockState state = level.getBlockState(blockPos);
++                FluidState fluid = level.getFluidState(blockPos);
++
++                if (fluid.is(FluidTags.WATER)) {
++                    if (state.getBlock() instanceof BucketPickup bucketPickup && !bucketPickup.pickupBlock(null, blockList, blockPos, state).isEmpty()) {
++                        // NOP
++                    } else if (state.getBlock() instanceof LiquidBlock) {
++                        // NOP
++                    } else if (state.is(Blocks.KELP) || state.is(Blocks.KELP_PLANT) || state.is(Blocks.SEAGRASS) || state.is(Blocks.TALL_SEAGRASS)) {
++                        BlockEntity blockEntity = state.hasBlockEntity() ? level.getBlockEntity(blockPos) : null;
++
++                        // Paper start - Fix SpongeAbsortEvent handling
++                        if (block.getHandle().isAir()) {
++                        dropResources(state, level, blockPos, blockEntity);
++                        }
++                        // Paper end - Fix SpongeAbsortEvent handling
++                    }
++                }
++                level.setBlock(blockPos, block.getHandle(), block.getFlag());
++            }
++
++            return true;
++        }
++        return false;
++        // CraftBukkit end
+     }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java.patch
new file mode 100644
index 0000000000..0e8433bda8
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java.patch
@@ -0,0 +1,25 @@
+--- a/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java
++++ b/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java
+@@ -39,7 +_,13 @@
+ 
+     @Override
+     protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
++        if (this instanceof GrassBlock && level.paperConfig().tickRates.grassSpread != 1 && (level.paperConfig().tickRates.grassSpread < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % level.paperConfig().tickRates.grassSpread != 0)) { return; } // Paper - Configurable random tick rates for blocks
+         if (!canBeGrass(state, level, pos)) {
++            // CraftBukkit start
++            if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(level, pos, Blocks.DIRT.defaultBlockState()).isCancelled()) {
++                return;
++            }
++            // CraftBukkit end
+             level.setBlockAndUpdate(pos, Blocks.DIRT.defaultBlockState());
+         } else {
+             if (level.getMaxLocalRawBrightness(pos.above()) >= 9) {
+@@ -48,7 +_,7 @@
+                 for (int i = 0; i < 4; i++) {
+                     BlockPos blockPos = pos.offset(random.nextInt(3) - 1, random.nextInt(5) - 3, random.nextInt(3) - 1);
+                     if (level.getBlockState(blockPos).is(Blocks.DIRT) && canPropagate(blockState, level, blockPos)) {
+-                        level.setBlockAndUpdate(blockPos, blockState.setValue(SNOWY, Boolean.valueOf(isSnowySetting(level.getBlockState(blockPos.above())))));
++                        org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(level, pos, blockPos, blockState.setValue(SNOWY, Boolean.valueOf(isSnowySetting(level.getBlockState(blockPos.above()))))); // CraftBukkit
+                     }
+                 }
+             }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/StemBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/StemBlock.java.patch
new file mode 100644
index 0000000000..3e1e7cca5a
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/StemBlock.java.patch
@@ -0,0 +1,38 @@
+--- a/net/minecraft/world/level/block/StemBlock.java
++++ b/net/minecraft/world/level/block/StemBlock.java
+@@ -80,11 +_,11 @@
+     protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+         if (level.getRawBrightness(pos, 0) >= 9) {
+             float growthSpeed = CropBlock.getGrowthSpeed(this, level, pos);
+-            if (random.nextInt((int)(25.0F / growthSpeed) + 1) == 0) {
++            if (random.nextFloat() < ((this == Blocks.PUMPKIN_STEM ? level.spigotConfig.pumpkinModifier : level.spigotConfig.melonModifier) / (100.0f * (Math.floor((25.0F / growthSpeed) + 1))))) { // Spigot - SPIGOT-7159: Better modifier resolution
+                 int ageValue = state.getValue(AGE);
+                 if (ageValue < 7) {
+                     state = state.setValue(AGE, Integer.valueOf(ageValue + 1));
+-                    level.setBlock(pos, state, 2);
++                    org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(level, pos, state, 2); // CraftBukkit
+                 } else {
+                     Direction randomDirection = Direction.Plane.HORIZONTAL.getRandomDirection(random);
+                     BlockPos blockPos = pos.relative(randomDirection);
+@@ -94,7 +_,11 @@
+                         Optional<Block> optional = registry.getOptional(this.fruit);
+                         Optional<Block> optional1 = registry.getOptional(this.attachedStem);
+                         if (optional.isPresent() && optional1.isPresent()) {
+-                            level.setBlockAndUpdate(blockPos, optional.get().defaultBlockState());
++                            // CraftBukkit start
++                            if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(level, blockPos, optional.get().defaultBlockState())) {
++                                return;
++                            }
++                            // CraftBukkit end
+                             level.setBlockAndUpdate(pos, optional1.get().defaultBlockState().setValue(HorizontalDirectionalBlock.FACING, randomDirection));
+                         }
+                     }
+@@ -122,7 +_,7 @@
+     public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) {
+         int min = Math.min(7, state.getValue(AGE) + Mth.nextInt(level.random, 2, 5));
+         BlockState blockState = state.setValue(AGE, Integer.valueOf(min));
+-        level.setBlock(pos, blockState, 2);
++        org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(level, pos, blockState, 2); // CraftBukkit
+         if (min == 7) {
+             blockState.randomTick(level, pos, level.random);
+         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/StonecutterBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/StonecutterBlock.java.patch
similarity index 63%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/StonecutterBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/StonecutterBlock.java.patch
index 33e97daa48..cf39a9ca01 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/StonecutterBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/StonecutterBlock.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/world/level/block/StonecutterBlock.java
 +++ b/net/minecraft/world/level/block/StonecutterBlock.java
-@@ -48,8 +48,9 @@
+@@ -48,8 +_,9 @@
      @Override
-     protected InteractionResult useWithoutItem(BlockState state, Level world, BlockPos pos, Player player, BlockHitResult hit) {
-         if (!world.isClientSide) {
--            player.openMenu(state.getMenuProvider(world, pos));
-+            if (player.openMenu(state.getMenuProvider(world, pos)).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
+     protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) {
+         if (!level.isClientSide) {
+-            player.openMenu(state.getMenuProvider(level, pos));
++            if (player.openMenu(state.getMenuProvider(level, pos)).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
              player.awardStat(Stats.INTERACT_WITH_STONECUTTER);
 +            } // Paper - Fix InventoryOpenEvent cancellation
          }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/SugarCaneBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/SugarCaneBlock.java.patch
new file mode 100644
index 0000000000..009e5542fb
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/SugarCaneBlock.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/level/block/SugarCaneBlock.java
++++ b/net/minecraft/world/level/block/SugarCaneBlock.java
+@@ -56,12 +_,13 @@
+                 i++;
+             }
+ 
+-            if (i < 3) {
++            if (i < level.paperConfig().maxGrowthHeight.reeds) { // Paper - Configurable cactus/bamboo/reed growth height
+                 int ageValue = state.getValue(AGE);
+-                if (ageValue == 15) {
+-                    level.setBlockAndUpdate(pos.above(), this.defaultBlockState());
++                int modifier = level.spigotConfig.caneModifier; // Spigot - SPIGOT-7159: Better modifier resolution
++                if (ageValue >= 15 || (modifier != 100 && random.nextFloat() < (modifier / (100.0f * 16)))) { // Spigot - SPIGOT-7159: Better modifier resolution
++                    org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(level, pos.above(), this.defaultBlockState()); // CraftBukkit
+                     level.setBlock(pos, state.setValue(AGE, Integer.valueOf(0)), 4);
+-                } else {
++                } else if (modifier == 100 || random.nextFloat() < (modifier / (100.0f * 16))) { // Spigot - SPIGOT-7159: Better modifier resolution
+                     level.setBlock(pos, state.setValue(AGE, Integer.valueOf(ageValue + 1)), 4);
+                 }
+             }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/SweetBerryBushBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/SweetBerryBushBlock.java.patch
new file mode 100644
index 0000000000..a228885132
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/SweetBerryBushBlock.java.patch
@@ -0,0 +1,47 @@
+--- a/net/minecraft/world/level/block/SweetBerryBushBlock.java
++++ b/net/minecraft/world/level/block/SweetBerryBushBlock.java
+@@ -68,15 +_,16 @@
+     @Override
+     protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+         int ageValue = state.getValue(AGE);
+-        if (ageValue < 3 && random.nextInt(5) == 0 && level.getRawBrightness(pos.above(), 0) >= 9) {
++        if (ageValue < 3 && random.nextFloat() < (level.spigotConfig.sweetBerryModifier / (100.0f * 5)) && level.getRawBrightness(pos.above(), 0) >= 9) { // Spigot - SPIGOT-7159: Better modifier resolution
+             BlockState blockState = state.setValue(AGE, Integer.valueOf(ageValue + 1));
+-            level.setBlock(pos, blockState, 2);
++            if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(level, pos, blockState, 2)) return; // CraftBukkit
+             level.gameEvent(GameEvent.BLOCK_CHANGE, pos, GameEvent.Context.of(blockState));
+         }
+     }
+ 
+     @Override
+     protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
++        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
+         if (entity instanceof LivingEntity && entity.getType() != EntityType.FOX && entity.getType() != EntityType.BEE) {
+             entity.makeStuckInBlock(state, new Vec3(0.8F, 0.75, 0.8F));
+             if (level instanceof ServerLevel serverLevel && state.getValue(AGE) != 0) {
+@@ -85,7 +_,7 @@
+                     double abs = Math.abs(vec3.x());
+                     double abs1 = Math.abs(vec3.z());
+                     if (abs >= 0.003F || abs1 >= 0.003F) {
+-                        entity.hurtServer(serverLevel, level.damageSources().sweetBerryBush(), 1.0F);
++                        entity.hurtServer(serverLevel, level.damageSources().sweetBerryBush().directBlock(level, pos), 1.0F); // CraftBukkit
+                     }
+                 }
+             }
+@@ -109,7 +_,15 @@
+         boolean flag = ageValue == 3;
+         if (ageValue > 1) {
+             int i = 1 + level.random.nextInt(2);
+-            popResource(level, pos, new ItemStack(Items.SWEET_BERRIES, i + (flag ? 1 : 0)));
++            // CraftBukkit start - useWithoutItem is always MAIN_HAND
++            org.bukkit.event.player.PlayerHarvestBlockEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerHarvestBlockEvent(level, pos, player, InteractionHand.MAIN_HAND, java.util.Collections.singletonList(new ItemStack(Items.SWEET_BERRIES, i + (flag ? 1 : 0))));
++            if (event.isCancelled()) {
++                return InteractionResult.SUCCESS; // We need to return a success either way, because making it PASS or FAIL will result in a bug where cancelling while harvesting w/ block in hand places block
++            }
++            for (org.bukkit.inventory.ItemStack itemStack : event.getItemsHarvested()) {
++                popResource(level, pos, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(itemStack));
++            }
++            // CraftBukkit end
+             level.playSound(null, pos, SoundEvents.SWEET_BERRY_BUSH_PICK_BERRIES, SoundSource.BLOCKS, 1.0F, 0.8F + level.random.nextFloat() * 0.4F);
+             BlockState blockState = state.setValue(AGE, Integer.valueOf(1));
+             level.setBlock(pos, blockState, 2);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/TargetBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/TargetBlock.java.patch
similarity index 60%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/TargetBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/TargetBlock.java.patch
index 5478fd0968..eeb2fc28bb 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/TargetBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/TargetBlock.java.patch
@@ -1,9 +1,9 @@
 --- a/net/minecraft/world/level/block/TargetBlock.java
 +++ b/net/minecraft/world/level/block/TargetBlock.java
-@@ -42,6 +42,10 @@
+@@ -42,6 +_,10 @@
      @Override
-     protected void onProjectileHit(Level world, BlockState state, BlockHitResult hit, Projectile projectile) {
-         int i = updateRedstoneOutput(world, state, hit, projectile);
+     protected void onProjectileHit(Level level, BlockState state, BlockHitResult hit, Projectile projectile) {
+         int i = updateRedstoneOutput(level, state, hit, projectile);
 +        // Paper start - Add TargetHitEvent
 +    }
 +    private static void awardTargetHitCriteria(Projectile projectile, BlockHitResult hit, int i) {
@@ -11,16 +11,15 @@
          if (projectile.getOwner() instanceof ServerPlayer serverPlayer) {
              serverPlayer.awardStat(Stats.TARGET_HIT);
              CriteriaTriggers.TARGET_BLOCK_HIT.trigger(serverPlayer, projectile, hit.getLocation(), i);
-@@ -51,10 +55,31 @@
-     private static int updateRedstoneOutput(LevelAccessor world, BlockState state, BlockHitResult hitResult, Entity entity) {
-         int i = getRedstoneStrength(hitResult, hitResult.getLocation());
-         int j = entity instanceof AbstractArrow ? 20 : 8;
+@@ -51,9 +_,29 @@
+     private static int updateRedstoneOutput(LevelAccessor level, BlockState state, BlockHitResult hit, Entity projectile) {
+         int redstoneStrength = getRedstoneStrength(hit, hit.getLocation());
+         int i = projectile instanceof AbstractArrow ? 20 : 8;
 +        // Paper start - Add TargetHitEvent
 +        boolean shouldAward = false;
-+        if (entity instanceof Projectile) {
-+            final Projectile projectile = (Projectile) entity;
-+            final org.bukkit.craftbukkit.block.CraftBlock craftBlock = org.bukkit.craftbukkit.block.CraftBlock.at(world, hitResult.getBlockPos());
-+            final org.bukkit.block.BlockFace blockFace = org.bukkit.craftbukkit.block.CraftBlock.notchToBlockFace(hitResult.getDirection());
++        if (projectile instanceof Projectile) {
++            final org.bukkit.craftbukkit.block.CraftBlock craftBlock = org.bukkit.craftbukkit.block.CraftBlock.at(level, hit.getBlockPos());
++            final org.bukkit.block.BlockFace blockFace = org.bukkit.craftbukkit.block.CraftBlock.notchToBlockFace(hit.getDirection());
 +            final io.papermc.paper.event.block.TargetHitEvent targetHitEvent = new io.papermc.paper.event.block.TargetHitEvent((org.bukkit.entity.Projectile) projectile.getBukkitEntity(), craftBlock, blockFace, i);
 +            if (targetHitEvent.callEvent()) {
 +                i = targetHitEvent.getSignalStrength();
@@ -30,16 +29,15 @@
 +            }
 +        }
 +        // Paper end - Add TargetHitEvent
-         if (!world.getBlockTicks().hasScheduledTick(hitResult.getBlockPos(), state.getBlock())) {
-             setOutputPower(world, state, i, hitResult.getBlockPos(), j);
+         if (!level.getBlockTicks().hasScheduledTick(hit.getBlockPos(), state.getBlock())) {
+             setOutputPower(level, state, redstoneStrength, hit.getBlockPos(), i);
          }
- 
++
 +        // Paper start - Award Hit Criteria after Block Update
 +        if (shouldAward) {
-+            awardTargetHitCriteria((Projectile) entity, hitResult, i);
++            awardTargetHitCriteria((Projectile) projectile, hit, i);
 +        }
 +        // Paper end - Award Hit Criteria after Block Update
-+
-         return i;
-     }
  
+         return redstoneStrength;
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/TntBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/TntBlock.java.patch
new file mode 100644
index 0000000000..b03b95ae3c
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/TntBlock.java.patch
@@ -0,0 +1,91 @@
+--- a/net/minecraft/world/level/block/TntBlock.java
++++ b/net/minecraft/world/level/block/TntBlock.java
+@@ -45,7 +_,13 @@
+     @Override
+     protected void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean isMoving) {
+         if (!oldState.is(state.getBlock())) {
+-            if (level.hasNeighborSignal(pos)) {
++            if (level.hasNeighborSignal(pos) && org.bukkit.craftbukkit.event.CraftEventFactory.callTNTPrimeEvent(level, pos, org.bukkit.event.block.TNTPrimeEvent.PrimeCause.REDSTONE, null, null)) { // CraftBukkit - TNTPrimeEvent
++                // Paper start - TNTPrimeEvent
++                org.bukkit.block.Block tntBlock = org.bukkit.craftbukkit.block.CraftBlock.at(level, pos);
++                if (!new com.destroystokyo.paper.event.block.TNTPrimeEvent(tntBlock, com.destroystokyo.paper.event.block.TNTPrimeEvent.PrimeReason.REDSTONE, null).callEvent()) {
++                    return;
++                }
++                // Paper end - TNTPrimeEvent
+                 explode(level, pos);
+                 level.removeBlock(pos, false);
+             }
+@@ -54,7 +_,13 @@
+ 
+     @Override
+     protected void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighborBlock, @Nullable Orientation orientation, boolean movedByPiston) {
+-        if (level.hasNeighborSignal(pos)) {
++        if (level.hasNeighborSignal(pos) && org.bukkit.craftbukkit.event.CraftEventFactory.callTNTPrimeEvent(level, pos, org.bukkit.event.block.TNTPrimeEvent.PrimeCause.REDSTONE, null, null)) { // CraftBukkit - TNTPrimeEvent
++            // Paper start - TNTPrimeEvent
++            org.bukkit.block.Block tntBlock = org.bukkit.craftbukkit.block.CraftBlock.at(level, pos);
++            if (!new com.destroystokyo.paper.event.block.TNTPrimeEvent(tntBlock, com.destroystokyo.paper.event.block.TNTPrimeEvent.PrimeReason.REDSTONE, null).callEvent()) {
++                return;
++            }
++            // Paper end - TNTPrimeEvent
+             explode(level, pos);
+             level.removeBlock(pos, false);
+         }
+@@ -62,7 +_,7 @@
+ 
+     @Override
+     public BlockState playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) {
+-        if (!level.isClientSide() && !player.isCreative() && state.getValue(UNSTABLE)) {
++        if (!level.isClientSide() && !player.isCreative() && state.getValue(UNSTABLE) && org.bukkit.craftbukkit.event.CraftEventFactory.callTNTPrimeEvent(level, pos, org.bukkit.event.block.TNTPrimeEvent.PrimeCause.BLOCK_BREAK, player, null)) { // CraftBukkit - TNTPrimeEvent
+             explode(level, pos);
+         }
+ 
+@@ -71,6 +_,13 @@
+ 
+     @Override
+     public void wasExploded(ServerLevel level, BlockPos pos, Explosion explosion) {
++        // Paper start - TNTPrimeEvent
++        org.bukkit.block.Block tntBlock = org.bukkit.craftbukkit.block.CraftBlock.at(level, pos);
++        org.bukkit.entity.Entity source = explosion.getDirectSourceEntity() != null ? explosion.getDirectSourceEntity().getBukkitEntity() : null;
++        if (!new com.destroystokyo.paper.event.block.TNTPrimeEvent(tntBlock, com.destroystokyo.paper.event.block.TNTPrimeEvent.PrimeReason.EXPLOSION, source).callEvent()) {
++            return;
++        }
++        // Paper end - TNTPrimeEvent
+         PrimedTnt primedTnt = new PrimedTnt(level, pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5, explosion.getIndirectSourceEntity());
+         int fuse = primedTnt.getFuse();
+         primedTnt.setFuse((short)(level.random.nextInt(fuse / 4) + fuse / 8));
+@@ -97,6 +_,17 @@
+         if (!stack.is(Items.FLINT_AND_STEEL) && !stack.is(Items.FIRE_CHARGE)) {
+             return super.useItemOn(stack, state, level, pos, player, hand, hitResult);
+         } else {
++            // CraftBukkit start - TNTPrimeEvent
++            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callTNTPrimeEvent(level, pos, org.bukkit.event.block.TNTPrimeEvent.PrimeCause.PLAYER, player, null)) {
++                return InteractionResult.CONSUME;
++            }
++            // CraftBukkit end
++            // Paper start - TNTPrimeEvent
++            org.bukkit.block.Block tntBlock = org.bukkit.craftbukkit.block.CraftBlock.at(level, pos);
++            if (!new com.destroystokyo.paper.event.block.TNTPrimeEvent(tntBlock, com.destroystokyo.paper.event.block.TNTPrimeEvent.PrimeReason.ITEM, player.getBukkitEntity()).callEvent()) {
++                return InteractionResult.FAIL;
++            }
++            // Paper end - TNTPrimeEvent
+             explode(level, pos, player);
+             level.setBlock(pos, Blocks.AIR.defaultBlockState(), 11);
+             Item item = stack.getItem();
+@@ -117,6 +_,17 @@
+             BlockPos blockPos = hit.getBlockPos();
+             Entity owner = projectile.getOwner();
+             if (projectile.isOnFire() && projectile.mayInteract(serverLevel, blockPos)) {
++                // CraftBukkit start
++                if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(projectile, blockPos, state.getFluidState().createLegacyBlock()) || !org.bukkit.craftbukkit.event.CraftEventFactory.callTNTPrimeEvent(level, blockPos, org.bukkit.event.block.TNTPrimeEvent.PrimeCause.PROJECTILE, projectile, null)) { // Paper - fix wrong block state
++                    return;
++                }
++                // CraftBukkit end
++                // Paper start - TNTPrimeEvent
++                org.bukkit.block.Block tntBlock = org.bukkit.craftbukkit.block.CraftBlock.at(level, blockPos);
++                if (!new com.destroystokyo.paper.event.block.TNTPrimeEvent(tntBlock, com.destroystokyo.paper.event.block.TNTPrimeEvent.PrimeReason.PROJECTILE, projectile.getBukkitEntity()).callEvent()) {
++                    return;
++                }
++                // Paper end - TNTPrimeEvent
+                 explode(level, blockPos, owner instanceof LivingEntity ? (LivingEntity)owner : null);
+                 level.removeBlock(blockPos, false);
+             }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/TrapDoorBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/TrapDoorBlock.java.patch
new file mode 100644
index 0000000000..dac679ac7e
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/TrapDoorBlock.java.patch
@@ -0,0 +1,44 @@
+--- a/net/minecraft/world/level/block/TrapDoorBlock.java
++++ b/net/minecraft/world/level/block/TrapDoorBlock.java
+@@ -146,7 +_,40 @@
+         if (!level.isClientSide) {
+             boolean hasNeighborSignal = level.hasNeighborSignal(pos);
+             if (hasNeighborSignal != state.getValue(POWERED)) {
+-                if (state.getValue(OPEN) != hasNeighborSignal) {
++                // if (state.getValue(OPEN) != hasNeighborSignal) {
++                // CraftBukkit start
++                org.bukkit.World bworld = level.getWorld();
++                org.bukkit.block.Block bblock = bworld.getBlockAt(pos.getX(), pos.getY(), pos.getZ());
++
++                int power = bblock.getBlockPower();
++                int oldPower = state.getValue(TrapDoorBlock.OPEN) ? 15 : 0;
++
++                if (oldPower == 0 ^ power == 0 || neighborBlock.defaultBlockState().isSignalSource()) {
++                    org.bukkit.event.block.BlockRedstoneEvent eventRedstone = new org.bukkit.event.block.BlockRedstoneEvent(bblock, oldPower, power);
++                    level.getCraftServer().getPluginManager().callEvent(eventRedstone);
++                    hasNeighborSignal = eventRedstone.getNewCurrent() > 0;
++                }
++                // CraftBukkit end
++                // Paper start - break redstone on trapdoors early
++                boolean open = state.getValue(TrapDoorBlock.OPEN) != hasNeighborSignal;
++                // note: this must run before any state for this block/its neighborus are written to the world
++                // we allow the redstone event to fire so that plugins can block
++                if (hasNeighborSignal && open) { // if we are now powered and it caused the trap door to open
++                    // in this case, first check for the redstone on top first
++                    BlockPos abovePos = pos.above();
++                    BlockState above = level.getBlockState(abovePos);
++                    if (above.getBlock() instanceof RedStoneWireBlock) {
++                        level.setBlock(abovePos, Blocks.AIR.defaultBlockState(), Block.UPDATE_CLIENTS | Block.UPDATE_NEIGHBORS);
++                        Block.popResource(level, abovePos, new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.REDSTONE));
++                        // now check that this didn't change our state
++                        if (level.getBlockState(pos) != state) {
++                            // our state was changed, so we cannot propagate this update
++                            return;
++                        }
++                    }
++                }
++                if (open) {
++                // Paper end - break redstone on trapdoors early
+                     state = state.setValue(OPEN, Boolean.valueOf(hasNeighborSignal));
+                     this.playSound(null, level, pos, hasNeighborSignal);
+                 }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/TripWireBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/TripWireBlock.java.patch
new file mode 100644
index 0000000000..4be069ee99
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/TripWireBlock.java.patch
@@ -0,0 +1,108 @@
+--- a/net/minecraft/world/level/block/TripWireBlock.java
++++ b/net/minecraft/world/level/block/TripWireBlock.java
+@@ -72,6 +_,7 @@
+ 
+     @Override
+     public BlockState getStateForPlacement(BlockPlaceContext context) {
++        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableTripwireUpdates) return this.defaultBlockState(); // Paper - place tripwire without updating
+         BlockGetter level = context.getLevel();
+         BlockPos clickedPos = context.getClickedPos();
+         return this.defaultBlockState()
+@@ -92,6 +_,7 @@
+         BlockState neighborState,
+         RandomSource random
+     ) {
++        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableTripwireUpdates) return state; // Paper - prevent tripwire from updating
+         return direction.getAxis().isHorizontal()
+             ? state.setValue(PROPERTY_BY_DIRECTION.get(direction), Boolean.valueOf(this.shouldConnectTo(neighborState, direction)))
+             : super.updateShape(state, level, scheduledTickAccess, pos, direction, neighborPos, neighborState, random);
+@@ -99,6 +_,7 @@
+ 
+     @Override
+     protected void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean isMoving) {
++        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableTripwireUpdates) return; // Paper - prevent adjacent tripwires from updating
+         if (!oldState.is(state.getBlock())) {
+             this.updateSource(level, pos, state);
+         }
+@@ -106,6 +_,7 @@
+ 
+     @Override
+     protected void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean isMoving) {
++        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableTripwireUpdates) return; // Paper - prevent adjacent tripwires from updating
+         if (!isMoving && !state.is(newState.getBlock())) {
+             this.updateSource(level, pos, state.setValue(POWERED, Boolean.valueOf(true)));
+         }
+@@ -113,6 +_,7 @@
+ 
+     @Override
+     public BlockState playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) {
++        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableTripwireUpdates) return state; // Paper - prevent disarming tripwires
+         if (!level.isClientSide && !player.getMainHandItem().isEmpty() && player.getMainHandItem().is(Items.SHEARS)) {
+             level.setBlock(pos, state.setValue(DISARMED, Boolean.valueOf(true)), 4);
+             level.gameEvent(player, GameEvent.SHEAR, pos);
+@@ -122,6 +_,7 @@
+     }
+ 
+     private void updateSource(Level level, BlockPos pos, BlockState state) {
++        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableTripwireUpdates) return; // Paper - prevent adjacent tripwires from updating
+         for (Direction direction : new Direction[]{Direction.SOUTH, Direction.WEST}) {
+             for (int i = 1; i < 42; i++) {
+                 BlockPos blockPos = pos.relative(direction, i);
+@@ -147,6 +_,8 @@
+ 
+     @Override
+     protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
++        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableTripwireUpdates) return; // Paper - prevent tripwires from detecting collision
++        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
+         if (!level.isClientSide) {
+             if (!state.getValue(POWERED)) {
+                 this.checkPressed(level, pos, List.of(entity));
+@@ -156,6 +_,7 @@
+ 
+     @Override
+     protected void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
++        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableTripwireUpdates) return; // Paper - prevent tripwire pressed check
+         if (level.getBlockState(pos).getValue(POWERED)) {
+             this.checkPressed(level, pos);
+         }
+@@ -179,6 +_,40 @@
+                 }
+             }
+         }
++
++        // CraftBukkit start - Call interact even when triggering connected tripwire
++        if (flag != poweredValue && poweredValue && blockState.getValue(TripWireBlock.ATTACHED)) {
++            org.bukkit.World bworld = level.getWorld();
++            org.bukkit.plugin.PluginManager manager = level.getCraftServer().getPluginManager();
++            org.bukkit.block.Block block = bworld.getBlockAt(pos.getX(), pos.getY(), pos.getZ());
++            boolean allowed = false;
++
++            // If all the events are cancelled block the tripwire trigger, else allow
++            for (Object object : entities) {
++                if (object != null) {
++                    org.bukkit.event.Cancellable cancellable;
++
++                    if (object instanceof Player) {
++                        cancellable = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerInteractEvent((Player) object, org.bukkit.event.block.Action.PHYSICAL, pos, null, null, null);
++                    } else if (object instanceof Entity) {
++                        cancellable = new org.bukkit.event.entity.EntityInteractEvent(((Entity) object).getBukkitEntity(), block);
++                        manager.callEvent((org.bukkit.event.entity.EntityInteractEvent) cancellable);
++                    } else {
++                        continue;
++                    }
++
++                    if (!cancellable.isCancelled()) {
++                        allowed = true;
++                        break;
++                    }
++                }
++            }
++
++            if (!allowed) {
++                return;
++            }
++        }
++        // CraftBukkit end
+ 
+         if (flag != poweredValue) {
+             blockState = blockState.setValue(POWERED, Boolean.valueOf(flag));
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/TripWireHookBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/TripWireHookBlock.java.patch
new file mode 100644
index 0000000000..661176a03d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/TripWireHookBlock.java.patch
@@ -0,0 +1,21 @@
+--- a/net/minecraft/world/level/block/TripWireHookBlock.java
++++ b/net/minecraft/world/level/block/TripWireHookBlock.java
+@@ -173,9 +_,18 @@
+                 notifyNeighbors(block, level, blockPosx, opposite);
+                 emitState(level, blockPosx, flag2, flag3, flag, flag1);
+             }
++            // CraftBukkit start
++            org.bukkit.event.block.BlockRedstoneEvent eventRedstone = new org.bukkit.event.block.BlockRedstoneEvent(org.bukkit.craftbukkit.block.CraftBlock.at(level, pos), 15, 0);
++            level.getCraftServer().getPluginManager().callEvent(eventRedstone);
++
++            if (eventRedstone.getNewCurrent() > 0) {
++                return;
++            }
++            // CraftBukkit end
+ 
+             emitState(level, pos, flag2, flag3, flag, flag1);
+             if (!attaching) {
++                if (level.getBlockState(pos).is(Blocks.TRIPWIRE_HOOK)) // Paper - Validate tripwire hook placement before update
+                 level.setBlock(pos, blockState1.setValue(FACING, direction), 3);
+                 if (shouldNotifyNeighbours) {
+                     notifyNeighbors(block, level, pos, direction);
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/TurtleEggBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/TurtleEggBlock.java.patch
new file mode 100644
index 0000000000..e7880ebb4f
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/TurtleEggBlock.java.patch
@@ -0,0 +1,64 @@
+--- a/net/minecraft/world/level/block/TurtleEggBlock.java
++++ b/net/minecraft/world/level/block/TurtleEggBlock.java
+@@ -74,6 +_,19 @@
+             && level instanceof ServerLevel serverLevel
+             && this.canDestroyEgg(serverLevel, entity)
+             && level.random.nextInt(chance) == 0) {
++            // CraftBukkit start - Step on eggs
++            org.bukkit.event.Cancellable cancellable;
++            if (entity instanceof Player) {
++                cancellable = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerInteractEvent((Player) entity, org.bukkit.event.block.Action.PHYSICAL, pos, null, null, null);
++            } else {
++                cancellable = new org.bukkit.event.entity.EntityInteractEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos));
++                level.getCraftServer().getPluginManager().callEvent((org.bukkit.event.entity.EntityInteractEvent) cancellable);
++            }
++
++            if (cancellable.isCancelled()) {
++                return;
++            }
++            // CraftBukkit end
+             this.decreaseEggs(serverLevel, pos, state);
+         }
+     }
+@@ -95,10 +_,20 @@
+         if (this.shouldUpdateHatchLevel(level) && onSand(level, pos)) {
+             int hatchValue = state.getValue(HATCH);
+             if (hatchValue < 2) {
++                // CraftBukkit start - Call BlockGrowEvent
++                if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(level, pos, state.setValue(TurtleEggBlock.HATCH, hatchValue + 1), 2)) {
++                    return;
++                }
++                // CraftBukkit end
+                 level.playSound(null, pos, SoundEvents.TURTLE_EGG_CRACK, SoundSource.BLOCKS, 0.7F, 0.9F + random.nextFloat() * 0.2F);
+-                level.setBlock(pos, state.setValue(HATCH, Integer.valueOf(hatchValue + 1)), 2);
++                // level.setBlock(pos, state.setValue(HATCH, Integer.valueOf(hatchValue + 1)), 2); // CraftBukkit - handled above
+                 level.gameEvent(GameEvent.BLOCK_CHANGE, pos, GameEvent.Context.of(state));
+             } else {
++                // CraftBukkit start - Call BlockFadeEvent
++                if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(level, pos, Blocks.AIR.defaultBlockState()).isCancelled()) {
++                    return;
++                }
++                // CraftBukkit end
+                 level.playSound(null, pos, SoundEvents.TURTLE_EGG_HATCH, SoundSource.BLOCKS, 0.7F, 0.9F + random.nextFloat() * 0.2F);
+                 level.removeBlock(pos, false);
+                 level.gameEvent(GameEvent.BLOCK_DESTROY, pos, GameEvent.Context.of(state));
+@@ -110,7 +_,7 @@
+                         turtle.setAge(-24000);
+                         turtle.setHomePos(pos);
+                         turtle.moveTo(pos.getX() + 0.3 + i * 0.2, pos.getY(), pos.getZ() + 0.3, 0.0F, 0.0F);
+-                        level.addFreshEntity(turtle);
++                        level.addFreshEntity(turtle, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.EGG); // CraftBukkit
+                     }
+                 }
+             }
+@@ -138,8 +_,8 @@
+     }
+ 
+     @Override
+-    public void playerDestroy(Level level, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity te, ItemStack stack) {
+-        super.playerDestroy(level, player, pos, state, te, stack);
++    public void playerDestroy(Level level, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity te, ItemStack stack, boolean includeDrops, boolean dropExp) { // Paper - fix drops not preventing stats/food exhaustion
++        super.playerDestroy(level, player, pos, state, te, stack, includeDrops, dropExp); // Paper - fix drops not preventing stats/food exhaustion
+         this.decreaseEggs(level, pos, state);
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/VineBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/VineBlock.java.patch
new file mode 100644
index 0000000000..f5fb874107
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/VineBlock.java.patch
@@ -0,0 +1,68 @@
+--- a/net/minecraft/world/level/block/VineBlock.java
++++ b/net/minecraft/world/level/block/VineBlock.java
+@@ -191,7 +_,7 @@
+     @Override
+     protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
+         if (level.getGameRules().getBoolean(GameRules.RULE_DO_VINES_SPREAD)) {
+-            if (random.nextInt(4) == 0) {
++            if (random.nextFloat() < (level.spigotConfig.vineModifier / (100.0f * 4))) { // Spigot - SPIGOT-7159: Better modifier resolution
+                 Direction random1 = Direction.getRandom(random);
+                 BlockPos blockPos = pos.above();
+                 if (random1.getAxis().isHorizontal() && !state.getValue(getPropertyForFace(random1))) {
+@@ -205,28 +_,31 @@
+                             boolean value1 = state.getValue(getPropertyForFace(counterClockWise));
+                             BlockPos blockPos2 = blockPos1.relative(clockWise);
+                             BlockPos blockPos3 = blockPos1.relative(counterClockWise);
++                            // CraftBukkit start - Call BlockSpreadEvent
++                            BlockPos source = pos;
+                             if (value && isAcceptableNeighbour(level, blockPos2, clockWise)) {
+-                                level.setBlock(blockPos1, this.defaultBlockState().setValue(getPropertyForFace(clockWise), Boolean.valueOf(true)), 2);
++                                org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(level, source, blockPos1, this.defaultBlockState().setValue(getPropertyForFace(clockWise), Boolean.valueOf(true)), 2);
+                             } else if (value1 && isAcceptableNeighbour(level, blockPos3, counterClockWise)) {
+-                                level.setBlock(blockPos1, this.defaultBlockState().setValue(getPropertyForFace(counterClockWise), Boolean.valueOf(true)), 2);
++                                org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(level, source, blockPos1, this.defaultBlockState().setValue(getPropertyForFace(counterClockWise), Boolean.valueOf(true)), 2);
+                             } else {
+                                 Direction opposite = random1.getOpposite();
+                                 if (value && level.isEmptyBlock(blockPos2) && isAcceptableNeighbour(level, pos.relative(clockWise), opposite)) {
+-                                    level.setBlock(blockPos2, this.defaultBlockState().setValue(getPropertyForFace(opposite), Boolean.valueOf(true)), 2);
++                                    org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(level, source, blockPos2, this.defaultBlockState().setValue(getPropertyForFace(opposite), Boolean.valueOf(true)), 2);
+                                 } else if (value1 && level.isEmptyBlock(blockPos3) && isAcceptableNeighbour(level, pos.relative(counterClockWise), opposite)) {
+-                                    level.setBlock(blockPos3, this.defaultBlockState().setValue(getPropertyForFace(opposite), Boolean.valueOf(true)), 2);
++                                    org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(level, source, blockPos3, this.defaultBlockState().setValue(getPropertyForFace(opposite), Boolean.valueOf(true)), 2);
+                                 } else if (random.nextFloat() < 0.05 && isAcceptableNeighbour(level, blockPos1.above(), Direction.UP)) {
+-                                    level.setBlock(blockPos1, this.defaultBlockState().setValue(UP, Boolean.valueOf(true)), 2);
++                                    org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(level, source, blockPos1, this.defaultBlockState().setValue(UP, Boolean.valueOf(true)), 2);
+                                 }
++                                // CraftBukkit end
+                             }
+                         } else if (isAcceptableNeighbour(level, blockPos1, random1)) {
+-                            level.setBlock(pos, state.setValue(getPropertyForFace(random1), Boolean.valueOf(true)), 2);
++                            org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(level, pos, (BlockState) state.setValue(VineBlock.getPropertyForFace(random1), true), 2); // CraftBukkit
+                         }
+                     }
+                 } else {
+                     if (random1 == Direction.UP && pos.getY() < level.getMaxY()) {
+                         if (this.canSupportAtFace(level, pos, random1)) {
+-                            level.setBlock(pos, state.setValue(UP, Boolean.valueOf(true)), 2);
++                            org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(level, pos, state.setValue(UP, Boolean.valueOf(true)), 2); // CraftBukkit
+                             return;
+                         }
+ 
+@@ -244,7 +_,7 @@
+                             }
+ 
+                             if (this.hasHorizontalConnection(blockState1)) {
+-                                level.setBlock(blockPos, blockState1, 2);
++                                org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(level, pos, blockPos, blockState1, 2); // CraftBukkit
+                             }
+ 
+                             return;
+@@ -258,7 +_,7 @@
+                             BlockState blockState2 = blockState.isAir() ? this.defaultBlockState() : blockState;
+                             BlockState blockState3 = this.copyRandomFaces(state, blockState2, random);
+                             if (blockState2 != blockState3 && this.hasHorizontalConnection(blockState3)) {
+-                                level.setBlock(blockPos1, blockState3, 2);
++                                org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(level, pos, blockPos1, blockState3, 2); // CraftBukkit
+                             }
+                         }
+                     }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/WallHangingSignBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/WallHangingSignBlock.java.patch
new file mode 100644
index 0000000000..ad86e1ce3a
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/WallHangingSignBlock.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/world/level/block/WallHangingSignBlock.java
++++ b/net/minecraft/world/level/block/WallHangingSignBlock.java
+@@ -187,6 +_,6 @@
+     @Nullable
+     @Override
+     public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> blockEntityType) {
+-        return createTickerHelper(blockEntityType, BlockEntityType.HANGING_SIGN, SignBlockEntity::tick);
++        return null; // CraftBukkit - remove unnecessary sign ticking
+     }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/WaterlilyBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/WaterlilyBlock.java.patch
new file mode 100644
index 0000000000..7176715848
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/WaterlilyBlock.java.patch
@@ -0,0 +1,17 @@
+--- a/net/minecraft/world/level/block/WaterlilyBlock.java
++++ b/net/minecraft/world/level/block/WaterlilyBlock.java
+@@ -29,8 +_,14 @@
+ 
+     @Override
+     protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
++        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
+         super.entityInside(state, level, pos, entity);
+         if (level instanceof ServerLevel && entity instanceof AbstractBoat) {
++            // CraftBukkit start
++            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(entity, pos, state.getFluidState().createLegacyBlock())) { // Paper - fix wrong block state
++                return;
++            }
++            // CraftBukkit end
+             level.destroyBlock(new BlockPos(pos), true, entity);
+         }
+     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/WebBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/WebBlock.java.patch
similarity index 77%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/WebBlock.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/WebBlock.java.patch
index 1204a1e128..e774bcdbb1 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/WebBlock.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/WebBlock.java.patch
@@ -1,10 +1,10 @@
 --- a/net/minecraft/world/level/block/WebBlock.java
 +++ b/net/minecraft/world/level/block/WebBlock.java
-@@ -24,6 +24,7 @@
+@@ -24,6 +_,7 @@
  
      @Override
-     protected void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
-+        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
+     protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
++        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
          Vec3 vec3 = new Vec3(0.25, 0.05F, 0.25);
          if (entity instanceof LivingEntity livingEntity && livingEntity.hasEffect(MobEffects.WEAVING)) {
              vec3 = new Vec3(0.5, 0.25, 0.5);
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/WeightedPressurePlateBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/WeightedPressurePlateBlock.java.patch
new file mode 100644
index 0000000000..26787da952
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/WeightedPressurePlateBlock.java.patch
@@ -0,0 +1,39 @@
+--- a/net/minecraft/world/level/block/WeightedPressurePlateBlock.java
++++ b/net/minecraft/world/level/block/WeightedPressurePlateBlock.java
+@@ -6,6 +_,7 @@
+ import net.minecraft.core.BlockPos;
+ import net.minecraft.util.Mth;
+ import net.minecraft.world.entity.Entity;
++import net.minecraft.world.entity.player.Player;
+ import net.minecraft.world.level.Level;
+ import net.minecraft.world.level.block.state.BlockBehaviour;
+ import net.minecraft.world.level.block.state.BlockState;
+@@ -39,7 +_,27 @@
+ 
+     @Override
+     protected int getSignalStrength(Level level, BlockPos pos) {
+-        int min = Math.min(getEntityCount(level, TOUCH_AABB.move(pos), Entity.class), this.maxWeight);
++        // CraftBukkit start
++        // int min = Math.min(getEntityCount(level, TOUCH_AABB.move(pos), Entity.class), this.maxWeight);
++        int min = 0;
++        for (Entity entity : getEntities(level, WeightedPressurePlateBlock.TOUCH_AABB.move(pos), Entity.class)) {
++            org.bukkit.event.Cancellable cancellable;
++
++            if (entity instanceof Player) {
++                cancellable = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerInteractEvent((Player) entity, org.bukkit.event.block.Action.PHYSICAL, pos, null, null, null);
++            } else {
++                cancellable = new org.bukkit.event.entity.EntityInteractEvent(entity.getBukkitEntity(), level.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ()));
++                level.getCraftServer().getPluginManager().callEvent((org.bukkit.event.entity.EntityInteractEvent) cancellable);
++            }
++
++            // We only want to block turning the plate on if all events are cancelled
++            if (!cancellable.isCancelled()) {
++                min++;
++            }
++        }
++
++        min = Math.min(min, this.maxWeight);
++        // CraftBukkit end
+         if (min > 0) {
+             float f = (float)Math.min(this.maxWeight, min) / this.maxWeight;
+             return Mth.ceil(f * 15.0F);
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/WitherRoseBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/WitherRoseBlock.java.patch
new file mode 100644
index 0000000000..6a3cdd4e5a
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/WitherRoseBlock.java.patch
@@ -0,0 +1,16 @@
+--- a/net/minecraft/world/level/block/WitherRoseBlock.java
++++ b/net/minecraft/world/level/block/WitherRoseBlock.java
+@@ -63,11 +_,12 @@
+ 
+     @Override
+     protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) {
++        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(level, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
+         if (level instanceof ServerLevel serverLevel
+             && level.getDifficulty() != Difficulty.PEACEFUL
+             && entity instanceof LivingEntity livingEntity
+             && !livingEntity.isInvulnerableTo(serverLevel, level.damageSources().wither())) {
+-            livingEntity.addEffect(this.getBeeInteractionEffect());
++            livingEntity.addEffect(this.getBeeInteractionEffect(), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.WITHER_ROSE); // CraftBukkit
+         }
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/WitherSkullBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/WitherSkullBlock.java.patch
new file mode 100644
index 0000000000..63d29d1d8d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/WitherSkullBlock.java.patch
@@ -0,0 +1,39 @@
+--- a/net/minecraft/world/level/block/WitherSkullBlock.java
++++ b/net/minecraft/world/level/block/WitherSkullBlock.java
+@@ -51,6 +_,7 @@
+     }
+ 
+     public static void checkSpawn(Level level, BlockPos pos, SkullBlockEntity blockEntity) {
++        if (level.captureBlockStates) return; // CraftBukkit
+         if (!level.isClientSide) {
+             BlockState blockState = blockEntity.getBlockState();
+             boolean flag = blockState.is(Blocks.WITHER_SKELETON_SKULL) || blockState.is(Blocks.WITHER_SKELETON_WALL_SKULL);
+@@ -59,7 +_,7 @@
+                 if (blockPatternMatch != null) {
+                     WitherBoss witherBoss = EntityType.WITHER.create(level, EntitySpawnReason.TRIGGERED);
+                     if (witherBoss != null) {
+-                        CarvedPumpkinBlock.clearPatternBlocks(level, blockPatternMatch);
++                        // CarvedPumpkinBlock.clearPatternBlocks(level, blockPatternMatch); // CraftBukkit - move down
+                         BlockPos pos1 = blockPatternMatch.getBlock(1, 2, 0).getPos();
+                         witherBoss.moveTo(
+                             pos1.getX() + 0.5,
+@@ -70,12 +_,18 @@
+                         );
+                         witherBoss.yBodyRot = blockPatternMatch.getForwards().getAxis() == Direction.Axis.X ? 0.0F : 90.0F;
+                         witherBoss.makeInvulnerable();
++                        // CraftBukkit start
++                        if (!level.addFreshEntity(witherBoss, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.BUILD_WITHER)) {
++                            return;
++                        }
++                        CarvedPumpkinBlock.clearPatternBlocks(level, blockPatternMatch); // CraftBukkit - from above
++                        // CraftBukkit end
+ 
+                         for (ServerPlayer serverPlayer : level.getEntitiesOfClass(ServerPlayer.class, witherBoss.getBoundingBox().inflate(50.0))) {
+                             CriteriaTriggers.SUMMONED_ENTITY.trigger(serverPlayer, witherBoss);
+                         }
+ 
+-                        level.addFreshEntity(witherBoss);
++                        // level.addFreshEntity(witherBoss); // CraftBukkit - moved up
+                         CarvedPumpkinBlock.updatePatternBlocks(level, blockPatternMatch);
+                     }
+                 }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java.patch
new file mode 100644
index 0000000000..5907877ed7
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java.patch
@@ -0,0 +1,291 @@
+--- a/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java
+@@ -99,11 +_,44 @@
+     };
+     public final Reference2IntOpenHashMap<ResourceKey<Recipe<?>>> recipesUsed = new Reference2IntOpenHashMap<>();
+     private final RecipeManager.CachedCheck<SingleRecipeInput, ? extends AbstractCookingRecipe> quickCheck;
++    public final RecipeType<? extends AbstractCookingRecipe> recipeType; // Paper - cook speed multiplier API
++    public double cookSpeedMultiplier = 1.0; // Paper - cook speed multiplier API
+ 
+     protected AbstractFurnaceBlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState blockState, RecipeType<? extends AbstractCookingRecipe> recipeType) {
+         super(type, pos, blockState);
+         this.quickCheck = RecipeManager.createCheck(recipeType);
+-    }
++        this.recipeType = recipeType; // Paper - cook speed multiplier API
++    }
++
++    // CraftBukkit start - add fields and methods
++    private int maxStack = MAX_STACK;
++    public List<org.bukkit.entity.HumanEntity> transaction = new java.util.ArrayList<>();
++
++    public List<ItemStack> getContents() {
++        return this.items;
++    }
++
++    public void onOpen(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.add(player);
++    }
++
++    public void onClose(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.remove(player);
++    }
++
++    public List<org.bukkit.entity.HumanEntity> getViewers() {
++        return this.transaction;
++    }
++
++    @Override
++    public int getMaxStackSize() {
++        return this.maxStack;
++    }
++
++    public void setMaxStackSize(int size) {
++        this.maxStack = size;
++    }
++    // CraftBukkit end
+ 
+     private boolean isLit() {
+         return this.litTimeRemaining > 0;
+@@ -121,8 +_,19 @@
+         CompoundTag compound = tag.getCompound("RecipesUsed");
+ 
+         for (String string : compound.getAllKeys()) {
+-            this.recipesUsed.put(ResourceKey.create(Registries.RECIPE, ResourceLocation.parse(string)), compound.getInt(string));
+-        }
++            // Paper start - Validate ResourceLocation
++            final ResourceLocation resourceLocation = ResourceLocation.tryParse(string);
++            if (resourceLocation != null) {
++                this.recipesUsed.put(ResourceKey.create(Registries.RECIPE, resourceLocation), tag.getInt(string));
++            }
++            // Paper end - Validate ResourceLocation
++        }
++
++        // Paper start - cook speed multiplier API
++        if (tag.contains("Paper.CookSpeedMultiplier")) {
++            this.cookSpeedMultiplier = tag.getDouble("Paper.CookSpeedMultiplier");
++        }
++        // Paper end - cook speed multiplier API
+     }
+ 
+     @Override
+@@ -132,6 +_,7 @@
+         tag.putShort("cooking_total_time", (short)this.cookingTotalTime);
+         tag.putShort("lit_time_remaining", (short)this.litTimeRemaining);
+         tag.putShort("lit_total_time", (short)this.litTotalTime);
++        tag.putDouble("Paper.CookSpeedMultiplier", this.cookSpeedMultiplier); // Paper - cook speed multiplier API
+         ContainerHelper.saveAllItems(tag, this.items, registries);
+         CompoundTag compoundTag = new CompoundTag();
+         this.recipesUsed.forEach((recipe, count) -> compoundTag.putInt(recipe.location().toString(), count));
+@@ -160,11 +_,22 @@
+ 
+             int maxStackSize = furnace.getMaxStackSize();
+             if (!furnace.isLit() && canBurn(level.registryAccess(), recipeHolder, singleRecipeInput, furnace.items, maxStackSize)) {
+-                furnace.litTimeRemaining = furnace.getBurnDuration(level.fuelValues(), itemStack);
++                // CraftBukkit start
++                org.bukkit.craftbukkit.inventory.CraftItemStack fuel = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack);
++
++                org.bukkit.event.inventory.FurnaceBurnEvent furnaceBurnEvent = new org.bukkit.event.inventory.FurnaceBurnEvent(
++                    org.bukkit.craftbukkit.block.CraftBlock.at(level, pos),
++                    fuel,
++                    furnace.getBurnDuration(level.fuelValues(), itemStack)
++                );
++                if (!furnaceBurnEvent.callEvent()) return;
++                // CraftBukkit end
++
++                furnace.litTimeRemaining = furnaceBurnEvent.getBurnTime(); // CraftBukkit - respect event output
+                 furnace.litTotalTime = furnace.litTimeRemaining;
+-                if (furnace.isLit()) {
++                if (furnace.isLit() && furnaceBurnEvent.isBurning()) { // CraftBukkit - respect event output
+                     flag = true;
+-                    if (flag2) {
++                    if (flag2 && furnaceBurnEvent.willConsumeFuel()) { // Paper - add consumeFuel to FurnaceBurnEvent
+                         Item item = itemStack.getItem();
+                         itemStack.shrink(1);
+                         if (itemStack.isEmpty()) {
+@@ -175,11 +_,28 @@
+             }
+ 
+             if (furnace.isLit() && canBurn(level.registryAccess(), recipeHolder, singleRecipeInput, furnace.items, maxStackSize)) {
++                // CraftBukkit start
++                if (recipeHolder != null && furnace.cookingTimer == 0) {
++                    org.bukkit.craftbukkit.inventory.CraftItemStack source = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(furnace.items.get(0));
++                    org.bukkit.inventory.CookingRecipe<?> recipe = (org.bukkit.inventory.CookingRecipe<?>) recipeHolder.toBukkitRecipe();
++
++                    org.bukkit.event.inventory.FurnaceStartSmeltEvent event = new org.bukkit.event.inventory.FurnaceStartSmeltEvent(
++                        org.bukkit.craftbukkit.block.CraftBlock.at(level, pos),
++                        source,
++                        recipe,
++                        getTotalCookTime(level, furnace, furnace.recipeType, furnace.cookSpeedMultiplier) // Paper - cook speed multiplier API
++                    );
++                    event.callEvent();
++
++                    furnace.cookingTotalTime = event.getTotalCookTime();
++                }
++                // CraftBukkit end
++
+                 furnace.cookingTimer++;
+-                if (furnace.cookingTimer == furnace.cookingTotalTime) {
++                if (furnace.cookingTimer >= furnace.cookingTotalTime) { // Paper - cook speed multiplier API
+                     furnace.cookingTimer = 0;
+-                    furnace.cookingTotalTime = getTotalCookTime(level, furnace);
+-                    if (burn(level.registryAccess(), recipeHolder, singleRecipeInput, furnace.items, maxStackSize)) {
++                    furnace.cookingTotalTime = getTotalCookTime(level, furnace, furnace.recipeType, furnace.cookSpeedMultiplier); // Paper - cook speed multiplier API
++                    if (burn(level.registryAccess(), recipeHolder, singleRecipeInput, furnace.items, maxStackSize, level, furnace.worldPosition)) { // CraftBukkit
+                         furnace.setRecipeUsed(recipeHolder);
+                     }
+ 
+@@ -233,17 +_,47 @@
+         @Nullable RecipeHolder<? extends AbstractCookingRecipe> recipe,
+         SingleRecipeInput recipeInput,
+         NonNullList<ItemStack> items,
+-        int maxStackSize
++        int maxStackSize,
++        net.minecraft.world.level.Level level, // CraftBukkit
++        BlockPos blockPos // CraftBukkit
+     ) {
+         if (recipe != null && canBurn(registryAccess, recipe, recipeInput, items, maxStackSize)) {
+-            ItemStack itemStack = items.get(0);
+-            ItemStack itemStack1 = recipe.value().assemble(recipeInput, registryAccess);
+-            ItemStack itemStack2 = items.get(2);
++            ItemStack itemStack = items.get(0); final ItemStack ingredient = itemStack; // Paper - OBFHELPER
++            ItemStack itemStack1 = recipe.value().assemble(recipeInput, registryAccess); ItemStack result = itemStack1; // Paper - OBFHELPER
++            ItemStack itemStack2 = items.get(2); final ItemStack existingResults = itemStack2; // Paper - OBFHELPER
++            // CraftBukkit start - fire FurnaceSmeltEvent
++            org.bukkit.craftbukkit.inventory.CraftItemStack apiIngredient = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(ingredient);
++            org.bukkit.inventory.ItemStack apiResult = org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitCopy(result);
++
++            org.bukkit.event.inventory.FurnaceSmeltEvent furnaceSmeltEvent = new org.bukkit.event.inventory.FurnaceSmeltEvent(
++                org.bukkit.craftbukkit.block.CraftBlock.at(level, blockPos),
++                apiIngredient,
++                apiResult,
++                (org.bukkit.inventory.CookingRecipe<?>) recipe.toBukkitRecipe() // Paper - Add recipe to cook events
++            );
++            if (!furnaceSmeltEvent.callEvent()) return false;
++
++            apiResult = furnaceSmeltEvent.getResult();
++            itemStack1 = result = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(apiResult);
++
++            if (!result.isEmpty()) {
++                if (existingResults.isEmpty()) {
++                    items.set(2, result.copy());
++                } else if (org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(existingResults).isSimilar(apiResult)) {
++                    existingResults.grow(result.getCount());
++                } else {
++                    return false;
++                }
++            }
++
++            /*
+             if (itemStack2.isEmpty()) {
+                 items.set(2, itemStack1.copy());
+             } else if (ItemStack.isSameItemSameComponents(itemStack2, itemStack1)) {
+                 itemStack2.grow(1);
+             }
++            */
++            // CraftBukkit end
+ 
+             if (itemStack.is(Blocks.WET_SPONGE.asItem()) && !items.get(1).isEmpty() && items.get(1).is(Items.BUCKET)) {
+                 items.set(1, new ItemStack(Items.WATER_BUCKET));
+@@ -260,9 +_,16 @@
+         return fuelValues.burnDuration(stack);
+     }
+ 
+-    public static int getTotalCookTime(ServerLevel level, AbstractFurnaceBlockEntity furnace) {
++    public static int getTotalCookTime(@Nullable ServerLevel level, AbstractFurnaceBlockEntity furnace, RecipeType<? extends AbstractCookingRecipe> recipeType, double cookSpeedMultiplier) { // Paper - cook speed multiplier API
+         SingleRecipeInput singleRecipeInput = new SingleRecipeInput(furnace.getItem(0));
+-        return furnace.quickCheck.getRecipeFor(singleRecipeInput, level).map(recipe -> recipe.value().cookingTime()).orElse(200);
++        // Paper start - cook speed multiplier API
++        /* Scale the recipe's cooking time to the current cookSpeedMultiplier */
++        int cookTime = level != null
++            ? furnace.quickCheck.getRecipeFor(singleRecipeInput, level).map(holder -> holder.value().cookingTime()).orElse(200)
++            /* passing a null level here is safe. world is only used for map extending recipes which won't happen here */
++            : (net.minecraft.server.MinecraftServer.getServer().getRecipeManager().getRecipeFor(recipeType, singleRecipeInput, level).map(holder -> holder.value().cookingTime()).orElse(200));
++        return (int) Math.ceil (cookTime / cookSpeedMultiplier);
++        // Paper end - cook speed multiplier API
+     }
+ 
+     @Override
+@@ -306,7 +_,7 @@
+         this.items.set(index, stack);
+         stack.limitSize(this.getMaxStackSize(stack));
+         if (index == 0 && !flag && this.level instanceof ServerLevel serverLevel) {
+-            this.cookingTotalTime = getTotalCookTime(serverLevel, this);
++            this.cookingTotalTime = getTotalCookTime(serverLevel, this, this.recipeType, this.cookSpeedMultiplier); // Paper - cook speed multiplier API
+             this.cookingTimer = 0;
+             this.setChanged();
+         }
+@@ -342,8 +_,8 @@
+     public void awardUsedRecipes(Player player, List<ItemStack> items) {
+     }
+ 
+-    public void awardUsedRecipesAndPopExperience(ServerPlayer player) {
+-        List<RecipeHolder<?>> recipesToAwardAndPopExperience = this.getRecipesToAwardAndPopExperience(player.serverLevel(), player.position());
++    public void awardUsedRecipesAndPopExperience(ServerPlayer player, ItemStack itemstack, int amount) { // CraftBukkit
++        List<RecipeHolder<?>> recipesToAwardAndPopExperience = this.getRecipesToAwardAndPopExperience(player.serverLevel(), player.position(), this.worldPosition, player, itemstack, amount); // CraftBukkit - overload for exp spawn events
+         player.awardRecipes(recipesToAwardAndPopExperience);
+ 
+         for (RecipeHolder<?> recipeHolder : recipesToAwardAndPopExperience) {
+@@ -356,30 +_,60 @@
+     }
+ 
+     public List<RecipeHolder<?>> getRecipesToAwardAndPopExperience(ServerLevel level, Vec3 popVec) {
++    // CraftBukkit start
++        return this.getRecipesToAwardAndPopExperience(level, popVec, this.worldPosition, null, null, 0);
++    }
++    public List<RecipeHolder<?>> getRecipesToAwardAndPopExperience(ServerLevel level, Vec3 popVec, BlockPos blockPos, ServerPlayer serverPlayer, ItemStack itemStack, int amount) {
++    // CraftBukkit end
+         List<RecipeHolder<?>> list = Lists.newArrayList();
+ 
+         for (Entry<ResourceKey<Recipe<?>>> entry : this.recipesUsed.reference2IntEntrySet()) {
+             level.recipeAccess().byKey(entry.getKey()).ifPresent(recipe -> {
++                if (!(recipe.value() instanceof AbstractCookingRecipe)) return; // Paper - don't process non-cooking recipes
+                 list.add((RecipeHolder<?>)recipe);
+-                createExperience(level, popVec, entry.getIntValue(), ((AbstractCookingRecipe)recipe.value()).experience());
++                createExperience(level, popVec, entry.getIntValue(), ((AbstractCookingRecipe)recipe.value()).experience(), blockPos, serverPlayer, itemStack, amount);
+             });
+         }
+ 
+         return list;
+     }
+ 
+-    private static void createExperience(ServerLevel level, Vec3 popVec, int recipeIndex, float experience) {
++    private static void createExperience(ServerLevel level, Vec3 popVec, int recipeIndex, float experience, BlockPos blockPos, ServerPlayer serverPlayer, ItemStack itemStack, int amount) { // CraftBukkit
+         int floor = Mth.floor(recipeIndex * experience);
+         float fraction = Mth.frac(recipeIndex * experience);
+         if (fraction != 0.0F && Math.random() < fraction) {
+             floor++;
+         }
+ 
+-        ExperienceOrb.award(level, popVec, floor);
++        // CraftBukkit start - fire FurnaceExtractEvent / BlockExpEvent
++        org.bukkit.event.block.BlockExpEvent event;
++        if (amount != 0) {
++            event = new org.bukkit.event.inventory.FurnaceExtractEvent(
++                serverPlayer.getBukkitEntity(),
++                org.bukkit.craftbukkit.block.CraftBlock.at(level, blockPos),
++                org.bukkit.craftbukkit.inventory.CraftItemType.minecraftToBukkit(itemStack.getItem()),
++                amount,
++                floor
++            );
++        } else {
++            event = new org.bukkit.event.block.BlockExpEvent(
++                org.bukkit.craftbukkit.block.CraftBlock.at(level, blockPos),
++                floor
++            );
++        }
++        event.callEvent();
++        floor = event.getExpToDrop();
++        // CraftBukkit end
++
++        ExperienceOrb.award(level, popVec, floor, org.bukkit.entity.ExperienceOrb.SpawnReason.FURNACE, serverPlayer); // Paper
+     }
+ 
+     @Override
+     public void fillStackedContents(StackedItemContents stackedContents) {
++        // Paper start - don't account fuel stack (fixes MC-243057)
++        stackedContents.accountStack(this.items.get(SLOT_INPUT));
++        stackedContents.accountStack(this.items.get(SLOT_RESULT));
++        // Paper end - don't account fuel stack (fixes MC-243057)
+         for (ItemStack itemStack : this.items) {
+             stackedContents.accountStack(itemStack);
+         }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/BannerBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/BannerBlockEntity.java.patch
new file mode 100644
index 0000000000..a398e2a015
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/BannerBlockEntity.java.patch
@@ -0,0 +1,71 @@
+--- a/net/minecraft/world/level/block/entity/BannerBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/BannerBlockEntity.java
+@@ -23,7 +_,7 @@
+     public static final int MAX_PATTERNS = 6;
+     private static final String TAG_PATTERNS = "patterns";
+     @Nullable
+-    private Component name;
++    public Component name; // Paper - AT public
+     public DyeColor baseColor;
+     private BannerPatternLayers patterns = BannerPatternLayers.EMPTY;
+ 
+@@ -50,7 +_,7 @@
+     @Override
+     protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
+         super.saveAdditional(tag, registries);
+-        if (!this.patterns.equals(BannerPatternLayers.EMPTY)) {
++        if (!this.patterns.equals(BannerPatternLayers.EMPTY) || serialisingForNetwork.get()) { // Paper - always send patterns to client
+             tag.put("patterns", BannerPatternLayers.CODEC.encodeStart(registries.createSerializationContext(NbtOps.INSTANCE), this.patterns).getOrThrow());
+         }
+ 
+@@ -70,7 +_,7 @@
+             BannerPatternLayers.CODEC
+                 .parse(registries.createSerializationContext(NbtOps.INSTANCE), tag.get("patterns"))
+                 .resultOrPartial(string -> LOGGER.error("Failed to parse banner patterns: '{}'", string))
+-                .ifPresent(bannerPatternLayers -> this.patterns = bannerPatternLayers);
++                .ifPresent(bannerPatternLayers -> this.setPatterns(bannerPatternLayers)); // CraftBukkit - apply limits
+         }
+     }
+ 
+@@ -79,9 +_,18 @@
+         return ClientboundBlockEntityDataPacket.create(this);
+     }
+ 
++    // Paper start - always send patterns to client
++    ThreadLocal<Boolean> serialisingForNetwork = ThreadLocal.withInitial(() -> Boolean.FALSE);
+     @Override
+     public CompoundTag getUpdateTag(HolderLookup.Provider registries) {
++        final Boolean wasSerialisingForNetwork = serialisingForNetwork.get();
++        try {
++            serialisingForNetwork.set(Boolean.TRUE);
+         return this.saveWithoutMetadata(registries);
++        } finally {
++            serialisingForNetwork.set(wasSerialisingForNetwork);
++        }
++        // Paper end - always send patterns to client
+     }
+ 
+     public BannerPatternLayers getPatterns() {
+@@ -101,7 +_,7 @@
+     @Override
+     protected void applyImplicitComponents(BlockEntity.DataComponentInput componentInput) {
+         super.applyImplicitComponents(componentInput);
+-        this.patterns = componentInput.getOrDefault(DataComponents.BANNER_PATTERNS, BannerPatternLayers.EMPTY);
++        this.setPatterns(componentInput.getOrDefault(DataComponents.BANNER_PATTERNS, BannerPatternLayers.EMPTY)); // CraftBukkit - apply limits
+         this.name = componentInput.get(DataComponents.CUSTOM_NAME);
+     }
+ 
+@@ -117,4 +_,13 @@
+         tag.remove("patterns");
+         tag.remove("CustomName");
+     }
++
++    // CraftBukkit start
++    public void setPatterns(BannerPatternLayers bannerPatternLayers) {
++        if (bannerPatternLayers.layers().size() > 20) {
++            bannerPatternLayers = new BannerPatternLayers(java.util.List.copyOf(bannerPatternLayers.layers().subList(0, 20)));
++        }
++        this.patterns = bannerPatternLayers;
++    }
++    // CraftBukkit end
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/BarrelBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/BarrelBlockEntity.java.patch
new file mode 100644
index 0000000000..a7e3cbcde9
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/BarrelBlockEntity.java.patch
@@ -0,0 +1,43 @@
+--- a/net/minecraft/world/level/block/entity/BarrelBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/BarrelBlockEntity.java
+@@ -21,6 +_,40 @@
+ import net.minecraft.world.level.block.state.BlockState;
+ 
+ public class BarrelBlockEntity extends RandomizableContainerBlockEntity {
++    // CraftBukkit start - add fields and methods
++    public java.util.List<org.bukkit.entity.HumanEntity> transaction = new java.util.ArrayList<>();
++    private int maxStack = MAX_STACK;
++
++    @Override
++    public java.util.List<ItemStack> getContents() {
++        return this.items;
++    }
++
++    @Override
++    public void onOpen(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.add(player);
++    }
++
++    @Override
++    public void onClose(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.remove(player);
++    }
++
++    @Override
++    public java.util.List<org.bukkit.entity.HumanEntity> getViewers() {
++        return this.transaction;
++    }
++
++    @Override
++    public int getMaxStackSize() {
++        return this.maxStack;
++    }
++
++    @Override
++    public void setMaxStackSize(int i) {
++        this.maxStack = i;
++    }
++    // CraftBukkit end
+     private NonNullList<ItemStack> items = NonNullList.withSize(27, ItemStack.EMPTY);
+     public final ContainerOpenersCounter openersCounter = new ContainerOpenersCounter() {
+         @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java.patch
similarity index 75%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java.patch
index 07ed6af225..09da60cff3 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java.patch
@@ -1,28 +1,28 @@
 --- a/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java
 +++ b/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java
-@@ -73,17 +73,44 @@
+@@ -68,17 +_,44 @@
      protected abstract Component getDefaultName();
  
      public boolean canOpen(Player player) {
--        return BaseContainerBlockEntity.canUnlock(player, this.lockKey, this.getDisplayName());
-+        return BaseContainerBlockEntity.canUnlock(player, this.lockKey, this.getDisplayName(), this); // Paper - Add BlockLockCheckEvent
+-        return canUnlock(player, this.lockKey, this.getDisplayName());
++        return canUnlock(player, this.lockKey, this.getDisplayName(), this); // Paper - Add BlockLockCheckEvent
      }
  
 +    @Deprecated @io.papermc.paper.annotation.DoNotUse // Paper - Add BlockLockCheckEvent
-     public static boolean canUnlock(Player player, LockCode lock, Component containerName) {
+     public static boolean canUnlock(Player player, LockCode code, Component displayName) {
 +        // Paper start - Add BlockLockCheckEvent
-+        return canUnlock(player, lock, containerName, null);
++        return canUnlock(player, code, displayName, null);
 +    }
-+    public static boolean canUnlock(Player player, LockCode lock, Component containerName, @Nullable BlockEntity blockEntity) {
++    public static boolean canUnlock(Player player, LockCode code, Component displayName, @Nullable BlockEntity blockEntity) {
 +        if (player instanceof net.minecraft.server.level.ServerPlayer serverPlayer && blockEntity != null && blockEntity.getLevel() != null && blockEntity.getLevel().getBlockEntity(blockEntity.getBlockPos()) == blockEntity) {
 +            final org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(blockEntity.getLevel(), blockEntity.getBlockPos());
-+            net.kyori.adventure.text.Component lockedMessage = net.kyori.adventure.text.Component.translatable("container.isLocked", io.papermc.paper.adventure.PaperAdventure.asAdventure(containerName));
++            net.kyori.adventure.text.Component lockedMessage = net.kyori.adventure.text.Component.translatable("container.isLocked", io.papermc.paper.adventure.PaperAdventure.asAdventure(displayName));
 +            net.kyori.adventure.sound.Sound lockedSound = net.kyori.adventure.sound.Sound.sound(org.bukkit.Sound.BLOCK_CHEST_LOCKED, net.kyori.adventure.sound.Sound.Source.BLOCK, 1.0F, 1.0F);
 +            final io.papermc.paper.event.block.BlockLockCheckEvent event = new io.papermc.paper.event.block.BlockLockCheckEvent(block, serverPlayer.getBukkitEntity(), lockedMessage, lockedSound);
 +            event.callEvent();
 +            if (event.getResult() == org.bukkit.event.Event.Result.ALLOW) {
 +                return true;
-+            } else if (event.getResult() == org.bukkit.event.Event.Result.DENY || (!player.isSpectator() && !lock.unlocksWith(event.isUsingCustomKeyItemStack() ? org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getKeyItem()) : player.getMainHandItem()))) {
++            } else if (event.getResult() == org.bukkit.event.Event.Result.DENY || (!player.isSpectator() && !code.unlocksWith(event.isUsingCustomKeyItemStack() ? org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getKeyItem()) : player.getMainHandItem()))) {
 +                if (event.getLockedMessage() != null) {
 +                    event.getPlayer().sendActionBar(event.getLockedMessage());
 +                }
@@ -35,9 +35,9 @@
 +            }
 +        } else { // logic below is replaced by logic above
 +        // Paper end - Add BlockLockCheckEvent
-         if (!player.isSpectator() && !lock.unlocksWith(player.getMainHandItem())) {
--            player.displayClientMessage(Component.translatable("container.isLocked", containerName), true);
-+            player.displayClientMessage(Component.translatable("container.isLocked", containerName), true); // Paper - diff on change
+         if (!player.isSpectator() && !code.unlocksWith(player.getMainHandItem())) {
+-            player.displayClientMessage(Component.translatable("container.isLocked", displayName), true);
++            player.displayClientMessage(Component.translatable("container.isLocked", displayName), true); // Paper - diff on change
              player.playNotifySound(SoundEvents.CHEST_LOCKED, SoundSource.BLOCKS, 1.0F, 1.0F);
              return false;
          } else {
@@ -47,9 +47,9 @@
      }
  
      protected abstract NonNullList<ItemStack> getItems();
-@@ -178,4 +205,12 @@
-         nbt.remove("lock");
-         nbt.remove("Items");
+@@ -166,4 +_,12 @@
+         tag.remove("lock");
+         tag.remove("Items");
      }
 +
 +    // CraftBukkit start
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/BeaconBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/BeaconBlockEntity.java.patch
new file mode 100644
index 0000000000..55e57b57f1
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/BeaconBlockEntity.java.patch
@@ -0,0 +1,243 @@
+--- a/net/minecraft/world/level/block/entity/BeaconBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/BeaconBlockEntity.java
+@@ -106,6 +_,53 @@
+             return 3;
+         }
+     };
++    // CraftBukkit start - add fields and methods
++    @Nullable
++    public org.bukkit.potion.PotionEffect getPrimaryEffect() {
++        return (this.primaryPower != null)
++            ? org.bukkit.craftbukkit.potion.CraftPotionUtil.toBukkit(new MobEffectInstance(
++                this.primaryPower,
++                BeaconBlockEntity.computeEffectDuration(this.levels),
++                BeaconBlockEntity.computeEffectAmplifier(this.levels, this.primaryPower, this.secondaryPower),
++                true,
++                true
++            ))
++            : null;
++    }
++
++    @Nullable
++    public org.bukkit.potion.PotionEffect getSecondaryEffect() {
++        return (BeaconBlockEntity.hasSecondaryEffect(this.levels, this.primaryPower, this.secondaryPower))
++            ? org.bukkit.craftbukkit.potion.CraftPotionUtil.toBukkit(new MobEffectInstance(
++                this.secondaryPower,
++                BeaconBlockEntity.computeEffectDuration(this.levels),
++                BeaconBlockEntity.computeEffectAmplifier(this.levels, this.primaryPower, this.secondaryPower),
++                true,
++                true
++            ))
++            : null;
++    }
++    // CraftBukkit end
++    // Paper start - Custom beacon ranges
++    private final String PAPER_RANGE_TAG = "Paper.Range";
++    private double effectRange = -1;
++
++    public double getEffectRange() {
++        if (this.effectRange < 0) {
++            return this.levels * 10 + 10;
++        } else {
++            return effectRange;
++        }
++    }
++
++    public void setEffectRange(double range) {
++        this.effectRange = range;
++    }
++
++    public void resetEffectRange() {
++        this.effectRange = -1;
++    }
++    // Paper end - Custom beacon ranges
+ 
+     @Nullable
+     static Holder<MobEffect> filterEffect(@Nullable Holder<MobEffect> effect) {
+@@ -163,17 +_,26 @@
+             blockEntity.lastCheckY++;
+         }
+ 
+-        int i = blockEntity.levels;
++        int i = blockEntity.levels; final int originalLevels = i; // Paper - OBFHELPER
+         if (level.getGameTime() % 80L == 0L) {
+             if (!blockEntity.beamSections.isEmpty()) {
+                 blockEntity.levels = updateBase(level, x, y, z);
+             }
+ 
+             if (blockEntity.levels > 0 && !blockEntity.beamSections.isEmpty()) {
+-                applyEffects(level, pos, blockEntity.levels, blockEntity.primaryPower, blockEntity.secondaryPower);
++                applyEffects(level, pos, blockEntity.levels, blockEntity.primaryPower, blockEntity.secondaryPower, blockEntity); // Paper - Custom beacon ranges
+                 playSound(level, pos, SoundEvents.BEACON_AMBIENT);
+             }
+         }
++        // Paper start - beacon activation/deactivation events
++        if (originalLevels <= 0 && blockEntity.levels > 0) {
++            org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(level, pos);
++            new io.papermc.paper.event.block.BeaconActivatedEvent(block).callEvent();
++        } else if (originalLevels > 0 && blockEntity.levels <= 0) {
++            org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(level, pos);
++            new io.papermc.paper.event.block.BeaconDeactivatedEvent(block).callEvent();
++        }
++        // Paper end - beacon activation/deactivation events
+ 
+         if (blockEntity.lastCheckY >= height) {
+             blockEntity.lastCheckY = level.getMinY() - 1;
+@@ -224,35 +_,99 @@
+ 
+     @Override
+     public void setRemoved() {
++        // Paper start - beacon activation/deactivation events
++        org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(level, worldPosition);
++        new io.papermc.paper.event.block.BeaconDeactivatedEvent(block).callEvent();
++        // Paper end - beacon activation/deactivation events
++        // Paper start - fix MC-153086
++        if (this.levels > 0 && !this.beamSections.isEmpty()) {
+         playSound(this.level, this.worldPosition, SoundEvents.BEACON_DEACTIVATE);
++        }
++        // Paper end
+         super.setRemoved();
+     }
+ 
++    @io.papermc.paper.annotation.DoNotUse // Paper - pass beacon block entity
+     private static void applyEffects(
+         Level level, BlockPos pos, int beaconLevel, @Nullable Holder<MobEffect> primaryEffect, @Nullable Holder<MobEffect> secondaryEffect
+     ) {
++    // Paper start - pass beacon block entity
++        applyEffects(level, pos, beaconLevel, primaryEffect, secondaryEffect, null);
++    }
++    private static void applyEffects(
++        Level level, BlockPos pos, int beaconLevel, @Nullable Holder<MobEffect> primaryEffect, @Nullable Holder<MobEffect> secondaryEffect, @Nullable BeaconBlockEntity blockEntity
++    ) {
++    // Paper emd - pass beacon block entity
+         if (!level.isClientSide && primaryEffect != null) {
+-            double d = beaconLevel * 10 + 10;
+-            int i = 0;
+-            if (beaconLevel >= 4 && Objects.equals(primaryEffect, secondaryEffect)) {
+-                i = 1;
+-            }
+-
+-            int i1 = (9 + beaconLevel * 2) * 20;
+-            AABB aabb = new AABB(pos).inflate(d).expandTowards(0.0, level.getHeight(), 0.0);
+-            List<Player> entitiesOfClass = level.getEntitiesOfClass(Player.class, aabb);
+-
+-            for (Player player : entitiesOfClass) {
+-                player.addEffect(new MobEffectInstance(primaryEffect, i1, i, true, true));
+-            }
+-
+-            if (beaconLevel >= 4 && !Objects.equals(primaryEffect, secondaryEffect) && secondaryEffect != null) {
+-                for (Player player : entitiesOfClass) {
+-                    player.addEffect(new MobEffectInstance(secondaryEffect, i1, 0, true, true));
++            double d = computeBeaconRange(beaconLevel); // Paper - diff out applyEffects logic components - see below
++            int i = computeEffectAmplifier(beaconLevel, primaryEffect, secondaryEffect); // Paper - diff out applyEffects logic components - see below
++
++            int i1 = computeEffectDuration(beaconLevel); // Paper - diff out applyEffects logic components - see below
++            List<Player> entitiesOfClass = getHumansInRange(level, pos, beaconLevel, blockEntity); // Paper - diff out applyEffects logic components - see below
++
++            applyEffectsAndCallEvent(level, pos, entitiesOfClass, new MobEffectInstance(primaryEffect, i1, i, true, true), true); // Paper - BeaconEffectEvent
++
++            if (hasSecondaryEffect(beaconLevel, primaryEffect, secondaryEffect)) { // Paper - diff out applyEffects logic components - see below
++                applyEffectsAndCallEvent(level, pos, entitiesOfClass, new MobEffectInstance(secondaryEffect, i1, 0, true, true), false); // Paper - BeaconEffectEvent
++            }
++        }
++    }
++
++    // Paper start - diff out applyEffects logic components
++    // Generally smarter than spigot trying to split the logic up, as that diff is giant.
++    private static int computeEffectDuration(final int beaconLevel) {
++        return (9 + beaconLevel * 2) * 20; // Diff from applyEffects
++    }
++
++    private static int computeEffectAmplifier(final int beaconLevel, @Nullable Holder<MobEffect> primaryEffect, @Nullable Holder<MobEffect> secondaryEffect) {
++        int i = 0;
++        if (beaconLevel >= 4 && Objects.equals(primaryEffect, secondaryEffect)) {
++            i = 1;
++        }
++        return i;
++    }
++
++    private static double computeBeaconRange(final int beaconLevel) {
++        return beaconLevel * 10 + 10; // Diff from applyEffects
++    }
++
++    public static List<Player> getHumansInRange(final Level level, final BlockPos pos, final int beaconLevel, final @Nullable BeaconBlockEntity blockEntity) {
++        final double d = blockEntity != null ? blockEntity.getEffectRange() : computeBeaconRange(beaconLevel);
++        AABB aabb = new AABB(pos).inflate(d).expandTowards(0.0, level.getHeight(), 0.0); // Diff from applyEffects
++        // Improve performance of human lookup by switching to a global player iteration when searching over 128 blocks
++        List<Player> list;
++        if (d <= 128.0) {
++            list = level.getEntitiesOfClass(Player.class, aabb); // Diff from applyEffect
++        } else {
++            list = new java.util.ArrayList<>();
++            for (final Player player : level.players()) {
++                if (!net.minecraft.world.entity.EntitySelector.NO_SPECTATORS.test(player)) continue;
++                if (player.getBoundingBox().intersects(aabb)) {
++                    list.add(player);
+                 }
+             }
+         }
+-    }
++        return list;
++    }
++
++    private static boolean hasSecondaryEffect(final int beaconLevel, final Holder<MobEffect> primaryEffect, final @Nullable Holder<MobEffect> secondaryEffect) {
++        return beaconLevel >= 4 && !Objects.equals(primaryEffect, secondaryEffect) && secondaryEffect != null;
++    }
++    // Paper end - diff out applyEffects logic components
++
++    // Paper start - BeaconEffectEvent
++    private static void applyEffectsAndCallEvent(final Level level, final BlockPos position, final List<Player> players, final MobEffectInstance mobEffectInstance, final boolean isPrimary) {
++        final org.bukkit.potion.PotionEffect apiEffect = org.bukkit.craftbukkit.potion.CraftPotionUtil.toBukkit(mobEffectInstance);
++        final org.bukkit.craftbukkit.block.CraftBlock apiBlock = org.bukkit.craftbukkit.block.CraftBlock.at(level, position);
++        for (final Player player : players) {
++            final com.destroystokyo.paper.event.block.BeaconEffectEvent event = new com.destroystokyo.paper.event.block.BeaconEffectEvent(
++                apiBlock, apiEffect, (org.bukkit.entity.Player) player.getBukkitEntity(), isPrimary
++            );
++            if (!event.callEvent()) continue;
++            player.addEffect(org.bukkit.craftbukkit.potion.CraftPotionUtil.fromBukkit(event.getEffect()), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.BEACON);
++        }
++    }
++    // Paper end - BeaconEffectEvent
+ 
+     public static void playSound(Level level, BlockPos pos, SoundEvent sound) {
+         level.playSound(null, pos, sound, SoundSource.BLOCKS, 1.0F, 1.0F);
+@@ -282,7 +_,7 @@
+     private static Holder<MobEffect> loadEffect(CompoundTag tag, String key) {
+         if (tag.contains(key, 8)) {
+             ResourceLocation resourceLocation = ResourceLocation.tryParse(tag.getString(key));
+-            return resourceLocation == null ? null : BuiltInRegistries.MOB_EFFECT.get(resourceLocation).map(BeaconBlockEntity::filterEffect).orElse(null);
++            return resourceLocation == null ? null : BuiltInRegistries.MOB_EFFECT.get(resourceLocation).orElse(null); // CraftBukkit - persist manually set non-default beacon effects (SPIGOT-3598)
+         } else {
+             return null;
+         }
+@@ -293,11 +_,13 @@
+         super.loadAdditional(tag, registries);
+         this.primaryPower = loadEffect(tag, "primary_effect");
+         this.secondaryPower = loadEffect(tag, "secondary_effect");
++        this.levels = tag.getInt("Levels"); // CraftBukkit - SPIGOT-5053, use where available
+         if (tag.contains("CustomName", 8)) {
+             this.name = parseCustomNameSafe(tag.getString("CustomName"), registries);
+         }
+ 
+         this.lockKey = LockCode.fromTag(tag, registries);
++        this.effectRange = tag.contains(PAPER_RANGE_TAG, 6) ? tag.getDouble(PAPER_RANGE_TAG) : -1; // Paper - Custom beacon ranges
+     }
+ 
+     @Override
+@@ -311,6 +_,7 @@
+         }
+ 
+         this.lockKey.addToTag(tag, registries);
++        tag.putDouble(PAPER_RANGE_TAG, this.effectRange); // Paper - Custom beacon ranges
+     }
+ 
+     public void setCustomName(@Nullable Component name) {
+@@ -326,7 +_,7 @@
+     @Nullable
+     @Override
+     public AbstractContainerMenu createMenu(int containerId, Inventory playerInventory, Player player) {
+-        return BaseContainerBlockEntity.canUnlock(player, this.lockKey, this.getDisplayName())
++        return BaseContainerBlockEntity.canUnlock(player, this.lockKey, this.getDisplayName(), this) // Paper - Add BlockLockCheckEvent
+             ? new BeaconMenu(containerId, playerInventory, this.dataAccess, ContainerLevelAccess.create(this.level, this.getBlockPos()))
+             : null;
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java.patch
new file mode 100644
index 0000000000..7950dedc62
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java.patch
@@ -0,0 +1,244 @@
+--- a/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java
+@@ -83,6 +_,7 @@
+     private List<BeehiveBlockEntity.BeeData> stored = Lists.newArrayList();
+     @Nullable
+     public BlockPos savedFlowerPos;
++    public int maxBees = MAX_OCCUPANTS; // CraftBukkit - allow setting max amount of bees a hive can hold
+ 
+     public BeehiveBlockEntity(BlockPos pos, BlockState blockState) {
+         super(BlockEntityType.BEEHIVE, pos, blockState);
+@@ -116,7 +_,7 @@
+     }
+ 
+     public boolean isFull() {
+-        return this.stored.size() == 3;
++        return this.stored.size() == this.maxBees; // CraftBukkit
+     }
+ 
+     public void emptyAllLivingFromHive(@Nullable Player player, BlockState state, BeehiveBlockEntity.BeeReleaseStatus releaseStatus) {
+@@ -127,7 +_,7 @@
+                     Bee bee = (Bee)entity;
+                     if (player.position().distanceToSqr(entity.position()) <= 16.0) {
+                         if (!this.isSedated()) {
+-                            bee.setTarget(player);
++                            bee.setTarget(player, org.bukkit.event.entity.EntityTargetEvent.TargetReason.CLOSEST_PLAYER, true); // CraftBukkit
+                         } else {
+                             bee.setStayOutOfHiveCountdown(400);
+                         }
+@@ -138,8 +_,14 @@
+     }
+ 
+     private List<Entity> releaseAllOccupants(BlockState state, BeehiveBlockEntity.BeeReleaseStatus releaseStatus) {
++        // CraftBukkit start - This allows us to bypass the night/rain/emergency check
++        return this.releaseBees(state, releaseStatus, false);
++    }
++
++    public List<Entity> releaseBees(BlockState state, BeehiveBlockEntity.BeeReleaseStatus releaseStatus, boolean force) {
++        // CraftBukkit end - This allows us to bypass t he night/rain/emergecny check
+         List<Entity> list = Lists.newArrayList();
+-        this.stored.removeIf(data -> releaseOccupant(this.level, this.worldPosition, state, data.toOccupant(), list, releaseStatus, this.savedFlowerPos));
++        this.stored.removeIf(data -> releaseOccupant(this.level, this.worldPosition, state, data.toOccupant(), list, releaseStatus, this.savedFlowerPos, force)); // CraftBukkit - This allows us to bypass t he night/rain/emergecny check
+         if (!list.isEmpty()) {
+             super.setChanged();
+         }
+@@ -152,6 +_,11 @@
+         return this.stored.size();
+     }
+ 
++    // Paper start - Add EntityBlockStorage clearEntities
++    public void clearBees() {
++        this.stored.clear();
++    }
++    // Paper end - Add EntityBlockStorage clearEntities
+     public static int getHoneyLevel(BlockState state) {
+         return state.getValue(BeehiveBlock.HONEY_LEVEL);
+     }
+@@ -162,7 +_,16 @@
+     }
+ 
+     public void addOccupant(Bee bee) {
+-        if (this.stored.size() < 3) {
++        if (this.stored.size() < this.maxBees) { // CraftBukkit
++            // CraftBukkit start
++            if (this.level != null) {
++                org.bukkit.event.entity.EntityEnterBlockEvent event = new org.bukkit.event.entity.EntityEnterBlockEvent(bee.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(this.level, this.getBlockPos()));
++                if (!event.callEvent()) {
++                    bee.setStayOutOfHiveCountdown(MIN_TICKS_BEFORE_REENTERING_HIVE);
++                    return;
++                }
++            }
++            // CraftBukkit end
+             bee.stopRiding();
+             bee.ejectPassengers();
+             bee.dropLeash();
+@@ -187,7 +_,7 @@
+                 this.level.gameEvent(GameEvent.BLOCK_CHANGE, blockPos, GameEvent.Context.of(bee, this.getBlockState()));
+             }
+ 
+-            bee.discard();
++            bee.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.ENTER_BLOCK); // CraftBukkit - add Bukkit remove cause
+             super.setChanged();
+         }
+     }
+@@ -205,7 +_,21 @@
+         BeehiveBlockEntity.BeeReleaseStatus releaseStatus,
+         @Nullable BlockPos storedFlowerPos
+     ) {
+-        if (Bee.isNightOrRaining(level) && releaseStatus != BeehiveBlockEntity.BeeReleaseStatus.EMERGENCY) {
++        // CraftBukkit start
++        return releaseOccupant(level, pos, state, occupant, storedInHives, releaseStatus, storedFlowerPos, false);
++    }
++    private static boolean releaseOccupant(
++        Level level,
++        BlockPos pos,
++        BlockState state,
++        BeehiveBlockEntity.Occupant occupant,
++        @Nullable List<Entity> storedInHives,
++        BeehiveBlockEntity.BeeReleaseStatus releaseStatus,
++        @Nullable BlockPos storedFlowerPos,
++        boolean force
++    ) {
++        if (!force && Bee.isNightOrRaining(level) && releaseStatus != BeehiveBlockEntity.BeeReleaseStatus.EMERGENCY) {
++        // CraftBukkit end
+             return false;
+         } else {
+             Direction direction = state.getValue(BeehiveBlock.FACING);
+@@ -216,6 +_,17 @@
+             } else {
+                 Entity entity = occupant.createEntity(level, pos);
+                 if (entity != null) {
++                    // CraftBukkit start
++                    if (entity instanceof Bee) {
++                        float bbWidth = entity.getBbWidth();
++                        double d = flag ? 0.0 : 0.55 + bbWidth / 2.0F;
++                        double d1 = pos.getX() + 0.5 + d * direction.getStepX();
++                        double d2 = pos.getY() + 0.5 - entity.getBbHeight() / 2.0F;
++                        double d3 = pos.getZ() + 0.5 + d * direction.getStepZ();
++                        entity.moveTo(d1, d2, d3, entity.getYRot(), entity.getXRot());
++                    }
++                    if (!level.addFreshEntity(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.BEEHIVE)) return false; // CraftBukkit - SpawnReason, moved from below
++                    // CraftBukkit end
+                     if (entity instanceof Bee bee) {
+                         if (storedFlowerPos != null && !bee.hasSavedFlowerPos() && level.random.nextFloat() < 0.9F) {
+                             bee.setSavedFlowerPos(storedFlowerPos);
+@@ -231,7 +_,13 @@
+                                         i--;
+                                     }
+ 
+-                                    level.setBlockAndUpdate(pos, state.setValue(BeehiveBlock.HONEY_LEVEL, Integer.valueOf(honeyLevel + i)));
++                                    // Paper start - Fire EntityChangeBlockEvent in more places
++                                    BlockState newBlockState = state.setValue(BeehiveBlock.HONEY_LEVEL, Integer.valueOf(honeyLevel + i));
++
++                                    if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(entity, pos, newBlockState)) {
++                                        level.setBlockAndUpdate(pos, newBlockState);
++                                    }
++                                    // Paper end - Fire EntityChangeBlockEvent in more places
+                                 }
+                             }
+                         }
+@@ -240,17 +_,19 @@
+                             storedInHives.add(bee);
+                         }
+ 
++                        /* CraftBukkit start - move up
+                         float bbWidth = entity.getBbWidth();
+                         double d = flag ? 0.0 : 0.55 + bbWidth / 2.0F;
+                         double d1 = pos.getX() + 0.5 + d * direction.getStepX();
+                         double d2 = pos.getY() + 0.5 - entity.getBbHeight() / 2.0F;
+                         double d3 = pos.getZ() + 0.5 + d * direction.getStepZ();
+                         entity.moveTo(d1, d2, d3, entity.getYRot(), entity.getXRot());
++                        */ // CraftBukkit end
+                     }
+ 
+                     level.playSound(null, pos, SoundEvents.BEEHIVE_EXIT, SoundSource.BLOCKS, 1.0F, 1.0F);
+                     level.gameEvent(GameEvent.BLOCK_CHANGE, pos, GameEvent.Context.of(entity, level.getBlockState(pos)));
+-                    return level.addFreshEntity(entity);
++                    return true; // CraftBukkit - moved up
+                 } else {
+                     return false;
+                 }
+@@ -276,6 +_,11 @@
+                     flag = true;
+                     iterator.remove();
+                 }
++                // Paper start - Fix bees aging inside; use exitTickCounter to keep actual bee life
++                else {
++                    beeData.exitTickCounter = beeData.occupant.minTicksInHive / 2;
++                }
++                // Paper end - Fix bees aging inside; use exitTickCounter to keep actual bee life
+             }
+         }
+ 
+@@ -299,7 +_,7 @@
+     @Override
+     protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
+         super.loadAdditional(tag, registries);
+-        this.stored.clear();
++        this.stored = Lists.newArrayList(); // CraftBukkit - SPIGOT-7790: create new copy (may be modified in physics event triggered by honey change)
+         if (tag.contains("bees")) {
+             BeehiveBlockEntity.Occupant.LIST_CODEC
+                 .parse(NbtOps.INSTANCE, tag.get("bees"))
+@@ -308,6 +_,11 @@
+         }
+ 
+         this.savedFlowerPos = NbtUtils.readBlockPos(tag, "flower_pos").orElse(null);
++        // CraftBukkit start
++        if (tag.contains("Bukkit.MaxEntities")) {
++            this.maxBees = tag.getInt("Bukkit.MaxEntities");
++        }
++        // CraftBukkit end
+     }
+ 
+     @Override
+@@ -317,12 +_,13 @@
+         if (this.hasSavedFlowerPos()) {
+             tag.put("flower_pos", NbtUtils.writeBlockPos(this.savedFlowerPos));
+         }
++        tag.putInt("Bukkit.MaxEntities", this.maxBees); // CraftBukkit
+     }
+ 
+     @Override
+     protected void applyImplicitComponents(BlockEntity.DataComponentInput componentInput) {
+         super.applyImplicitComponents(componentInput);
+-        this.stored.clear();
++        this.stored = Lists.newArrayList(); // CraftBukkit - SPIGOT-7790: create new copy (may be modified in physics event triggered by honey change)
+         List<BeehiveBlockEntity.Occupant> list = componentInput.getOrDefault(DataComponents.BEES, List.of());
+         list.forEach(this::storeBee);
+     }
+@@ -345,15 +_,18 @@
+ 
+     static class BeeData {
+         private final BeehiveBlockEntity.Occupant occupant;
++        private int exitTickCounter; // Paper - Fix bees aging inside hives; separate counter for checking if bee should exit to reduce exit attempts
+         private int ticksInHive;
+ 
+         BeeData(BeehiveBlockEntity.Occupant occupant) {
+             this.occupant = occupant;
+             this.ticksInHive = occupant.ticksInHive();
++            this.exitTickCounter = this.ticksInHive; // Paper - Fix bees aging inside hives
+         }
+ 
+         public boolean tick() {
+-            return this.ticksInHive++ > this.occupant.minTicksInHive;
++            this.ticksInHive++; // Paper - Fix bees aging inside hives
++            return this.exitTickCounter++ > this.occupant.minTicksInHive; // Paper - Fix bees aging inside hives
+         }
+ 
+         public BeehiveBlockEntity.Occupant toOccupant() {
+@@ -424,6 +_,7 @@
+         }
+ 
+         private static void setBeeReleaseData(int ticksInHive, Bee bee) {
++            if (!bee.ageLocked) { // Paper - Honor ageLock
+             int age = bee.getAge();
+             if (age < 0) {
+                 bee.setAge(Math.min(0, age + ticksInHive));
+@@ -432,6 +_,7 @@
+             }
+ 
+             bee.setInLoveTime(Math.max(0, bee.getInLoveTime() - ticksInHive));
++            } // Paper - Honor ageLock
+         }
+     }
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BellBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/BellBlockEntity.java.patch
similarity index 50%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BellBlockEntity.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/entity/BellBlockEntity.java.patch
index c8f5ced60a..810b2c47e5 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BellBlockEntity.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/BellBlockEntity.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/level/block/entity/BellBlockEntity.java
 +++ b/net/minecraft/world/level/block/entity/BellBlockEntity.java
-@@ -63,6 +63,11 @@
+@@ -60,6 +_,11 @@
  
          if (blockEntity.ticks >= 50) {
              blockEntity.shaking = false;
@@ -12,53 +12,48 @@
              blockEntity.ticks = 0;
          }
  
-@@ -76,6 +81,7 @@
-                 ++blockEntity.resonationTicks;
+@@ -73,6 +_,7 @@
+                 blockEntity.resonationTicks++;
              } else {
-                 bellEffect.run(world, pos, blockEntity.nearbyEntities);
+                 resonationEndAction.run(level, pos, blockEntity.nearbyEntities);
 +                blockEntity.nearbyEntities.clear(); // Paper - Fix bell block entity memory leak
                  blockEntity.resonating = false;
              }
          }
-@@ -120,11 +126,12 @@
-                 LivingEntity entityliving = (LivingEntity) iterator.next();
- 
-                 if (entityliving.isAlive() && !entityliving.isRemoved() && blockposition.closerToCenterThan(entityliving.position(), 32.0D)) {
--                    entityliving.getBrain().setMemory(MemoryModuleType.HEARD_BELL_TIME, (Object) this.level.getGameTime());
-+                    entityliving.getBrain().setMemory(MemoryModuleType.HEARD_BELL_TIME, this.level.getGameTime()); // CraftBukkit - decompile error
+@@ -113,6 +_,8 @@
                  }
              }
          }
- 
++
 +        this.nearbyEntities.removeIf(e -> !e.isAlive()); // Paper - Fix bell block entity memory leak
      }
  
-     private static boolean areRaidersNearby(BlockPos pos, List<LivingEntity> hearingEntities) {
-@@ -144,9 +151,13 @@
+     private static boolean areRaidersNearby(BlockPos pos, List<LivingEntity> raiders) {
+@@ -129,7 +_,10 @@
      }
  
-     private static void makeRaidersGlow(Level world, BlockPos pos, List<LivingEntity> hearingEntities) {
-+        List<org.bukkit.entity.LivingEntity> entities = // CraftBukkit
-         hearingEntities.stream().filter((entityliving) -> {
-             return BellBlockEntity.isRaiderWithinRange(pos, entityliving);
--        }).forEach(BellBlockEntity::glow);
-+        }).map((entity) -> (org.bukkit.entity.LivingEntity) entity.getBukkitEntity()).collect(java.util.stream.Collectors.toCollection(java.util.ArrayList::new)); // CraftBukkit
-+
-+        org.bukkit.craftbukkit.event.CraftEventFactory.handleBellResonateEvent(world, pos, entities).forEach(entity -> glow(entity, pos)); // Paper - Add BellRevealRaiderEvent
-+        // CraftBukkit end
+     private static void makeRaidersGlow(Level level, BlockPos pos, List<LivingEntity> raiders) {
+-        raiders.stream().filter(raider -> isRaiderWithinRange(pos, raider)).forEach(BellBlockEntity::glow);
++        // Paper start - call bell resonate event and bell reveal raider event
++        final List<org.bukkit.entity.LivingEntity> inRangeRaiders = raiders.stream().filter(raider -> isRaiderWithinRange(pos, raider)).map(e -> (org.bukkit.entity.LivingEntity) e.getBukkitEntity()).toList();
++        org.bukkit.craftbukkit.event.CraftEventFactory.handleBellResonateEvent(level, pos, inRangeRaiders).forEach(e -> glow(e, pos));
++        // Paper end - call bell resonate event and bell reveal raider event
      }
  
-     private static void showBellParticles(Level world, BlockPos pos, List<LivingEntity> hearingEntities) {
-@@ -178,6 +189,13 @@
+     private static void showBellParticles(Level level, BlockPos pos, List<LivingEntity> raiders) {
+@@ -159,7 +_,16 @@
+         return raider.isAlive() && !raider.isRemoved() && pos.closerToCenterThan(raider.position(), 48.0) && raider.getType().is(EntityTypeTags.RAIDERS);
      }
  
++    @Deprecated @io.papermc.paper.annotation.DoNotUse // Paper - Add BellRevealRaiderEvent
      private static void glow(LivingEntity entity) {
 +        // Paper start - Add BellRevealRaiderEvent
 +        glow(entity, null);
 +    }
 +
 +    private static void glow(LivingEntity entity, @javax.annotation.Nullable BlockPos pos) {
-+        if (pos != null && !new io.papermc.paper.event.block.BellRevealRaiderEvent(org.bukkit.craftbukkit.block.CraftBlock.at(entity.level(), pos), (org.bukkit.entity.Raider) entity.getBukkitEntity()).callEvent()) return;
++        if (pos != null && !new io.papermc.paper.event.block.BellRevealRaiderEvent(org.bukkit.craftbukkit.block.CraftBlock.at(entity.level(), pos), (org.bukkit.entity.Raider) entity.getBukkitEntity()).callEvent())
++            return;
 +        // Paper end - Add BellRevealRaiderEvent
          entity.addEffect(new MobEffectInstance(MobEffects.GLOWING, 60));
      }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/BlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/BlockEntity.java.patch
new file mode 100644
index 0000000000..c54e142ea3
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/BlockEntity.java.patch
@@ -0,0 +1,128 @@
+--- a/net/minecraft/world/level/block/entity/BlockEntity.java
++++ b/net/minecraft/world/level/block/entity/BlockEntity.java
+@@ -26,6 +_,10 @@
+ import org.slf4j.Logger;
+ 
+ public abstract class BlockEntity {
++    // CraftBukkit start - data containers
++    private static final org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry();
++    public org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer persistentDataContainer;
++    // CraftBukkit end
+     private static final Logger LOGGER = LogUtils.getLogger();
+     private final BlockEntityType<?> type;
+     @Nullable
+@@ -40,6 +_,7 @@
+         this.worldPosition = pos.immutable();
+         this.validateBlockState(blockState);
+         this.blockState = blockState;
++        this.persistentDataContainer = new org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer(DATA_TYPE_REGISTRY); // Paper - always init
+     }
+ 
+     private void validateBlockState(BlockState state) {
+@@ -70,6 +_,14 @@
+     }
+ 
+     protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
++        // Paper start - read persistent data container
++        this.persistentDataContainer.clear(); // Paper - clear instead of init
++
++        net.minecraft.nbt.Tag persistentDataTag = tag.get("PublicBukkitValues");
++        if (persistentDataTag instanceof CompoundTag) {
++            this.persistentDataContainer.putAll((CompoundTag) persistentDataTag);
++        }
++        // Paper end - read persistent data container
+     }
+ 
+     public final void loadWithComponents(CompoundTag tag, HolderLookup.Provider registries) {
+@@ -106,12 +_,22 @@
+             .encodeStart(registries.createSerializationContext(NbtOps.INSTANCE), this.components)
+             .resultOrPartial(string -> LOGGER.warn("Failed to save components: {}", string))
+             .ifPresent(tag -> compoundTag.merge((CompoundTag)tag));
++        // CraftBukkit start - store container
++        if (this.persistentDataContainer != null && !this.persistentDataContainer.isEmpty()) {
++            compoundTag.put("PublicBukkitValues", this.persistentDataContainer.toTagCompound());
++        }
++        // CraftBukkit end
+         return compoundTag;
+     }
+ 
+     public final CompoundTag saveCustomOnly(HolderLookup.Provider registries) {
+         CompoundTag compoundTag = new CompoundTag();
+         this.saveAdditional(compoundTag, registries);
++        // Paper start - store PDC here as well
++        if (this.persistentDataContainer != null && !this.persistentDataContainer.isEmpty()) {
++            compoundTag.put("PublicBukkitValues", this.persistentDataContainer.toTagCompound());
++        }
++        // Paper end
+         return compoundTag;
+     }
+ 
+@@ -220,7 +_,12 @@
+     public void fillCrashReportCategory(CrashReportCategory reportCategory) {
+         reportCategory.setDetail("Name", this::getNameForReporting);
+         if (this.level != null) {
+-            CrashReportCategory.populateBlockDetails(reportCategory, this.level, this.worldPosition, this.getBlockState());
++            // Paper start - Prevent block entity and entity crashes
++            BlockState block = this.getBlockState();
++            if (block != null) {
++                CrashReportCategory.populateBlockDetails(reportCategory, this.level, this.worldPosition, block);
++            }
++            // Paper end - Prevent block entity and entity crashes
+             CrashReportCategory.populateBlockDetails(reportCategory, this.level, this.worldPosition, this.level.getBlockState(this.worldPosition));
+         }
+     }
+@@ -247,6 +_,12 @@
+     }
+ 
+     public final void applyComponents(DataComponentMap components, DataComponentPatch patch) {
++        // CraftBukkit start
++        this.applyComponentsSet(components, patch);
++    }
++
++    public final Set<DataComponentType<?>> applyComponentsSet(DataComponentMap components, DataComponentPatch patch) {
++        // CraftBukkit end
+         final Set<DataComponentType<?>> set = new HashSet<>();
+         set.add(DataComponents.BLOCK_ENTITY_DATA);
+         set.add(DataComponents.BLOCK_STATE);
+@@ -267,6 +_,10 @@
+         });
+         DataComponentPatch dataComponentPatch = patch.forget(set::contains);
+         this.components = dataComponentPatch.split().added();
++        // CraftBukkit start
++        set.remove(DataComponents.BLOCK_ENTITY_DATA); // Remove as never actually added by applyImplicitComponents
++        return set;
++        // CraftBukkit end
+     }
+ 
+     protected void collectImplicitComponents(DataComponentMap.Builder components) {
+@@ -300,6 +_,30 @@
+             return null;
+         }
+     }
++
++    // CraftBukkit start - add method
++    public org.bukkit.inventory.InventoryHolder getOwner() {
++        // Paper start
++        return getOwner(true);
++    }
++    public org.bukkit.inventory.InventoryHolder getOwner(boolean useSnapshot) {
++        // Paper end
++        if (this.level == null) return null;
++        org.bukkit.block.Block block = this.level.getWorld().getBlockAt(this.worldPosition.getX(), this.worldPosition.getY(), this.worldPosition.getZ());
++        // if (block.getType() == org.bukkit.Material.AIR) return null; // Paper - actually get the tile entity if it still exists
++        org.bukkit.block.BlockState state = block.getState(useSnapshot); // Paper
++        return state instanceof final org.bukkit.inventory.InventoryHolder inventoryHolder ? inventoryHolder : null;
++    }
++    // CraftBukkit end
++
++    // Paper start - Sanitize sent data
++    public CompoundTag sanitizeSentNbt(CompoundTag tag) {
++        tag.remove("PublicBukkitValues");
++
++        return tag;
++    }
++    // Paper end - Sanitize sent data
++
+ 
+     static class ComponentHelper {
+         public static final Codec<DataComponentMap> COMPONENTS_CODEC = DataComponentMap.CODEC.optionalFieldOf("components", DataComponentMap.EMPTY).codec();
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java.patch
new file mode 100644
index 0000000000..15e9dfa1d3
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java.patch
@@ -0,0 +1,174 @@
+--- a/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java
+@@ -36,6 +_,7 @@
+     public static final int NUM_DATA_VALUES = 2;
+     private NonNullList<ItemStack> items = NonNullList.withSize(5, ItemStack.EMPTY);
+     public int brewTime;
++    public int recipeBrewTime = 400; // Paper - Add recipeBrewTime
+     private boolean[] lastPotionCount;
+     private Item ingredient;
+     public int fuel;
+@@ -45,6 +_,7 @@
+             return switch (index) {
+                 case 0 -> BrewingStandBlockEntity.this.brewTime;
+                 case 1 -> BrewingStandBlockEntity.this.fuel;
++                case 2 -> BrewingStandBlockEntity.this.recipeBrewTime; // Paper - Add recipeBrewTime
+                 default -> 0;
+             };
+         }
+@@ -57,14 +_,49 @@
+                     break;
+                 case 1:
+                     BrewingStandBlockEntity.this.fuel = value;
++                // Paper start - Add recipeBrewTime
++                    break;
++                case 2:
++                    BrewingStandBlockEntity.this.recipeBrewTime = value;
++                    break;
++                // Paper end - Add recipeBrewTime
+             }
+         }
+ 
+         @Override
+         public int getCount() {
+-            return 2;
++            return 3; // Paper - Add recipeBrewTime
+         }
+     };
++    // CraftBukkit start - add fields and methods
++    public java.util.List<org.bukkit.entity.HumanEntity> transaction = new java.util.ArrayList<>();
++    private int maxStack = MAX_STACK;
++
++    public void onOpen(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.add(player);
++    }
++
++    public void onClose(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.remove(player);
++    }
++
++    public java.util.List<org.bukkit.entity.HumanEntity> getViewers() {
++        return this.transaction;
++    }
++
++    public java.util.List<net.minecraft.world.item.ItemStack> getContents() {
++        return this.items;
++    }
++
++    @Override
++    public int getMaxStackSize() {
++        return this.maxStack;
++    }
++
++    public void setMaxStackSize(int size) {
++        this.maxStack = size;
++    }
++    // CraftBukkit end
+ 
+     public BrewingStandBlockEntity(BlockPos pos, BlockState state) {
+         super(BlockEntityType.BREWING_STAND, pos, state);
+@@ -93,8 +_,21 @@
+     public static void serverTick(Level level, BlockPos pos, BlockState state, BrewingStandBlockEntity blockEntity) {
+         ItemStack itemStack = blockEntity.items.get(4);
+         if (blockEntity.fuel <= 0 && itemStack.is(ItemTags.BREWING_FUEL)) {
+-            blockEntity.fuel = 20;
+-            itemStack.shrink(1);
++            // CraftBukkit start
++            org.bukkit.event.inventory.BrewingStandFuelEvent event = new org.bukkit.event.inventory.BrewingStandFuelEvent(
++                org.bukkit.craftbukkit.block.CraftBlock.at(level, pos),
++                org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack),
++                20
++            );
++            if (!event.callEvent()) {
++                return;
++            }
++
++            blockEntity.fuel = event.getFuelPower();
++            if (blockEntity.fuel > 0 && event.isConsuming()) {
++                itemStack.shrink(1);
++            }
++            // CraftBukkit end
+             setChanged(level, pos, state);
+         }
+ 
+@@ -105,7 +_,7 @@
+             blockEntity.brewTime--;
+             boolean flag1 = blockEntity.brewTime == 0;
+             if (flag1 && isBrewable) {
+-                doBrew(level, pos, blockEntity.items);
++                doBrew(level, pos, blockEntity.items, blockEntity); // CraftBukkit
+             } else if (!isBrewable || !itemStack1.is(blockEntity.ingredient)) {
+                 blockEntity.brewTime = 0;
+             }
+@@ -114,6 +_,14 @@
+         } else if (isBrewable && blockEntity.fuel > 0) {
+             blockEntity.fuel--;
+             blockEntity.brewTime = 400;
++            // CraftBukkit start
++            org.bukkit.event.block.BrewingStartEvent event = new org.bukkit.event.block.BrewingStartEvent(
++                org.bukkit.craftbukkit.block.CraftBlock.at(level, pos),
++                org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack1), 400);
++            event.callEvent();
++            blockEntity.recipeBrewTime = event.getRecipeBrewTime(); // Paper - use recipe brew time from event
++            blockEntity.brewTime = event.getBrewingTime(); // 400 -> event.getTotalBrewTime() // Paper - use brewing time from event
++            // CraftBukkit end
+             blockEntity.ingredient = itemStack1.getItem();
+             setChanged(level, pos, state);
+         }
+@@ -164,13 +_,37 @@
+         }
+     }
+ 
+-    private static void doBrew(Level level, BlockPos pos, NonNullList<ItemStack> items) {
++    private static void doBrew(Level level, BlockPos pos, NonNullList<ItemStack> items, BrewingStandBlockEntity brewingStandBlockEntity) { // CraftBukkit
+         ItemStack itemStack = items.get(3);
+         PotionBrewing potionBrewing = level.potionBrewing();
+ 
++        // CraftBukkit start
++        org.bukkit.inventory.InventoryHolder owner = brewingStandBlockEntity.getOwner();
++        java.util.List<org.bukkit.inventory.ItemStack> brewResults = new java.util.ArrayList<>(3);
+         for (int i = 0; i < 3; i++) {
+-            items.set(i, potionBrewing.mix(itemStack, items.get(i)));
+-        }
++            brewResults.add(i, org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(potionBrewing.mix(itemStack, items.get(i))));
++        }
++
++        if (owner != null) {
++            org.bukkit.event.inventory.BrewEvent event = new org.bukkit.event.inventory.BrewEvent(
++                org.bukkit.craftbukkit.block.CraftBlock.at(level, pos),
++                (org.bukkit.inventory.BrewerInventory) owner.getInventory(),
++                brewResults,
++                brewingStandBlockEntity.fuel
++            );
++            if (!event.callEvent()) {
++                return;
++            }
++
++            for (int i = 0; i < 3; i++) {
++                if (i < brewResults.size()) {
++                    items.set(i, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(brewResults.get(i)));
++                } else {
++                    items.set(i, ItemStack.EMPTY);
++                }
++            }
++        }
++        // CraftBukkit end
+ 
+         itemStack.shrink(1);
+         ItemStack craftingRemainder = itemStack.getItem().getCraftingRemainder();
+@@ -209,13 +_,13 @@
+ 
+     @Override
+     public boolean canPlaceItem(int index, ItemStack stack) {
++        PotionBrewing potionBrewing = this.level != null ? this.level.potionBrewing() : PotionBrewing.EMPTY; // Paper - move up
+         if (index == 3) {
+-            PotionBrewing potionBrewing = this.level != null ? this.level.potionBrewing() : PotionBrewing.EMPTY;
+             return potionBrewing.isIngredient(stack);
+         } else {
+             return index == 4
+                 ? stack.is(ItemTags.BREWING_FUEL)
+-                : (stack.is(Items.POTION) || stack.is(Items.SPLASH_POTION) || stack.is(Items.LINGERING_POTION) || stack.is(Items.GLASS_BOTTLE))
++                : (stack.is(Items.POTION) || stack.is(Items.SPLASH_POTION) || stack.is(Items.LINGERING_POTION) || stack.is(Items.GLASS_BOTTLE) || potionBrewing.isCustomInput(stack)) // Paper - Custom Potion Mixes
+                     && this.getItem(index).isEmpty();
+         }
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/BrushableBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/BrushableBlockEntity.java.patch
new file mode 100644
index 0000000000..b8231aa9ed
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/BrushableBlockEntity.java.patch
@@ -0,0 +1,23 @@
+--- a/net/minecraft/world/level/block/entity/BrushableBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/BrushableBlockEntity.java
+@@ -138,7 +_,10 @@
+             double d5 = blockPos.getZ() + 0.5 * d1 + d2;
+             ItemEntity itemEntity = new ItemEntity(level, d3, d4, d5, this.item.split(level.random.nextInt(21) + 10));
+             itemEntity.setDeltaMovement(Vec3.ZERO);
+-            level.addFreshEntity(itemEntity);
++            // CraftBukkit start
++            org.bukkit.block.Block bblock = org.bukkit.craftbukkit.block.CraftBlock.at(this.level, this.worldPosition);
++            org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockDropItemEvent(bblock, bblock.getState(), (ServerPlayer) player, java.util.List.of(itemEntity));
++            // CraftBukkit end
+             this.item = ItemStack.EMPTY;
+         }
+     }
+@@ -167,7 +_,7 @@
+ 
+     private boolean tryLoadLootTable(CompoundTag tag) {
+         if (tag.contains("LootTable", 8)) {
+-            this.lootTable = ResourceKey.create(Registries.LOOT_TABLE, ResourceLocation.parse(tag.getString("LootTable")));
++            this.lootTable = net.minecraft.Optionull.map(ResourceLocation.tryParse(tag.getString("LootTable")), rl -> ResourceKey.create(Registries.LOOT_TABLE, rl)); // Paper - Validate ResourceLocation
+             this.lootTableSeed = tag.getLong("LootTableSeed");
+             return true;
+         } else {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/CalibratedSculkSensorBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/CalibratedSculkSensorBlockEntity.java.patch
similarity index 92%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/entity/CalibratedSculkSensorBlockEntity.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/entity/CalibratedSculkSensorBlockEntity.java.patch
index 944379decd..196b1b49d7 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/CalibratedSculkSensorBlockEntity.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/CalibratedSculkSensorBlockEntity.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/level/block/entity/CalibratedSculkSensorBlockEntity.java
 +++ b/net/minecraft/world/level/block/entity/CalibratedSculkSensorBlockEntity.java
-@@ -20,6 +20,12 @@
+@@ -20,6 +_,12 @@
      public VibrationSystem.User createVibrationUser() {
          return new CalibratedSculkSensorBlockEntity.VibrationUser(this.getBlockPos());
      }
@@ -12,8 +12,8 @@
 +    // Paper end - Configurable sculk sensor listener range
  
      protected class VibrationUser extends SculkSensorBlockEntity.VibrationUser {
-         public VibrationUser(final BlockPos pos) {
-@@ -28,6 +34,7 @@
+         public VibrationUser(final BlockPos pos1) {
+@@ -28,6 +_,7 @@
  
          @Override
          public int getListenerRadius() {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/CampfireBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/CampfireBlockEntity.java.patch
new file mode 100644
index 0000000000..f9fd2a5136
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/CampfireBlockEntity.java.patch
@@ -0,0 +1,106 @@
+--- a/net/minecraft/world/level/block/entity/CampfireBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/CampfireBlockEntity.java
+@@ -36,6 +_,7 @@
+     private final NonNullList<ItemStack> items = NonNullList.withSize(4, ItemStack.EMPTY);
+     public final int[] cookingProgress = new int[4];
+     public final int[] cookingTime = new int[4];
++    public final boolean[] stopCooking = new boolean[4]; // Paper - Add more Campfire API
+ 
+     public CampfireBlockEntity(BlockPos pos, BlockState blockState) {
+         super(BlockEntityType.CAMPFIRE, pos, blockState);
+@@ -54,14 +_,44 @@
+             ItemStack itemStack = campfire.items.get(i);
+             if (!itemStack.isEmpty()) {
+                 flag = true;
++                if (!campfire.stopCooking[i]) { // Paper - Add more Campfire API
+                 campfire.cookingProgress[i]++;
++                } // Paper - Add more Campfire API
+                 if (campfire.cookingProgress[i] >= campfire.cookingTime[i]) {
+                     SingleRecipeInput singleRecipeInput = new SingleRecipeInput(itemStack);
+-                    ItemStack itemStack1 = check.getRecipeFor(singleRecipeInput, level)
++                    // Paper start - add recipe to cook events
++                    final var optionalCookingRecipe = check.getRecipeFor(singleRecipeInput, level);
++                    ItemStack itemStack1 = optionalCookingRecipe
+                         .map(recipe -> recipe.value().assemble(singleRecipeInput, level.registryAccess()))
+                         .orElse(itemStack);
++                    // Paper end - add recipe to cook events
+                     if (itemStack1.isItemEnabled(level.enabledFeatures())) {
+-                        Containers.dropItemStack(level, pos.getX(), pos.getY(), pos.getZ(), itemStack1);
++                        // CraftBukkit start - fire BlockCookEvent
++                        org.bukkit.craftbukkit.inventory.CraftItemStack source = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack);
++                        org.bukkit.inventory.ItemStack result = org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitCopy(itemStack1);
++
++                        org.bukkit.event.block.BlockCookEvent blockCookEvent = new org.bukkit.event.block.BlockCookEvent(
++                            org.bukkit.craftbukkit.block.CraftBlock.at(level, pos),
++                            source,
++                            result,
++                            (org.bukkit.inventory.CookingRecipe<?>) optionalCookingRecipe.map(RecipeHolder::toBukkitRecipe).orElse(null) // Paper -Add recipe to cook events
++                        );
++
++                        if (!blockCookEvent.callEvent()) {
++                            return;
++                        }
++
++                        result = blockCookEvent.getResult();
++                        itemStack1 = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(result);
++                        // CraftBukkit end
++                        // Paper start - Fix item locations dropped from campfires
++                        double deviation = 0.05F * RandomSource.GAUSSIAN_SPREAD_FACTOR;
++                        while (!itemStack1.isEmpty()) {
++                            net.minecraft.world.entity.item.ItemEntity droppedItem = new net.minecraft.world.entity.item.ItemEntity(level, pos.getX() + 0.5D, pos.getY() + 0.5D, pos.getZ() + 0.5D, itemStack1.split(level.random.nextInt(21) + 10));
++                            droppedItem.setDeltaMovement(level.random.triangle(0.0D, deviation), level.random.triangle(0.2D, deviation), level.random.triangle(0.0D, deviation));
++                            level.addFreshEntity(droppedItem);
++                        }
++                        // Paper end - Fix item locations dropped from campfires
+                         campfire.items.set(i, ItemStack.EMPTY);
+                         level.sendBlockUpdated(pos, state, state, 3);
+                         level.gameEvent(GameEvent.BLOCK_CHANGE, pos, GameEvent.Context.of(state));
+@@ -133,6 +_,17 @@
+             int[] intArray = tag.getIntArray("CookingTotalTimes");
+             System.arraycopy(intArray, 0, this.cookingTime, 0, Math.min(this.cookingTime.length, intArray.length));
+         }
++
++        // Paper start - Add more Campfire API
++        if (tag.contains("Paper.StopCooking", org.bukkit.craftbukkit.util.CraftMagicNumbers.NBT.TAG_BYTE_ARRAY)) {
++            byte[] abyte = tag.getByteArray("Paper.StopCooking");
++            boolean[] cookingState = new boolean[4];
++            for (int index = 0; index < abyte.length; index++) {
++                cookingState[index] = abyte[index] == 1;
++            }
++            System.arraycopy(cookingState, 0, this.stopCooking, 0, Math.min(this.stopCooking.length, abyte.length));
++        }
++        // Paper end - Add more Campfire API
+     }
+ 
+     @Override
+@@ -141,6 +_,13 @@
+         ContainerHelper.saveAllItems(tag, this.items, true, registries);
+         tag.putIntArray("CookingTimes", this.cookingProgress);
+         tag.putIntArray("CookingTotalTimes", this.cookingTime);
++        // Paper start - Add more Campfire API
++        byte[] cookingState = new byte[4];
++        for (int index = 0; index < cookingState.length; index++) {
++            cookingState[index] = (byte) (this.stopCooking[index] ? 1 : 0);
++        }
++        tag.putByteArray("Paper.StopCooking", cookingState);
++        // Paper end - Add more Campfire API
+     }
+ 
+     @Override
+@@ -165,7 +_,15 @@
+                     return false;
+                 }
+ 
+-                this.cookingTime[i] = recipeFor.get().value().cookingTime();
++                // CraftBukkit start
++                org.bukkit.event.block.CampfireStartEvent event = new org.bukkit.event.block.CampfireStartEvent(
++                    org.bukkit.craftbukkit.block.CraftBlock.at(this.level,this.worldPosition),
++                    org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(stack),
++                    (org.bukkit.inventory.CampfireRecipe) recipeFor.get().toBukkitRecipe()
++                );
++                this.level.getCraftServer().getPluginManager().callEvent(event);
++                this.cookingTime[i] = event.getTotalCookTime(); // i -> event.getTotalCookTime()
++                // CraftBukkit end
+                 this.cookingProgress[i] = 0;
+                 this.items.set(i, stack.consumeAndReturn(1, entity));
+                 level.gameEvent(GameEvent.BLOCK_CHANGE, this.getBlockPos(), GameEvent.Context.of(entity, this.getBlockState()));
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/ChestBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/ChestBlockEntity.java.patch
new file mode 100644
index 0000000000..32a967c2c3
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/ChestBlockEntity.java.patch
@@ -0,0 +1,39 @@
+--- a/net/minecraft/world/level/block/entity/ChestBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/ChestBlockEntity.java
+@@ -56,6 +_,36 @@
+     };
+     private final ChestLidController chestLidController = new ChestLidController();
+ 
++    // CraftBukkit start - add fields and methods
++    public java.util.List<org.bukkit.entity.HumanEntity> transaction = new java.util.ArrayList<>();
++    private int maxStack = MAX_STACK;
++
++    public java.util.List<net.minecraft.world.item.ItemStack> getContents() {
++        return this.items;
++    }
++
++    public void onOpen(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.add(player);
++    }
++
++    public void onClose(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.remove(player);
++    }
++
++    public java.util.List<org.bukkit.entity.HumanEntity> getViewers() {
++        return this.transaction;
++    }
++
++    @Override
++    public int getMaxStackSize() {
++        return this.maxStack;
++    }
++
++    public void setMaxStackSize(int size) {
++        this.maxStack = size;
++    }
++    // CraftBukkit end
++
+     protected ChestBlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState blockState) {
+         super(type, pos, blockState);
+     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ChiseledBookShelfBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/ChiseledBookShelfBlockEntity.java.patch
similarity index 53%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ChiseledBookShelfBlockEntity.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/entity/ChiseledBookShelfBlockEntity.java.patch
index af35923253..2c8981a4cb 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ChiseledBookShelfBlockEntity.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/ChiseledBookShelfBlockEntity.java.patch
@@ -1,43 +1,30 @@
 --- a/net/minecraft/world/level/block/entity/ChiseledBookShelfBlockEntity.java
 +++ b/net/minecraft/world/level/block/entity/ChiseledBookShelfBlockEntity.java
-@@ -23,13 +23,55 @@
- import net.minecraft.world.level.gameevent.GameEvent;
- import org.slf4j.Logger;
+@@ -27,6 +_,42 @@
+     private final NonNullList<ItemStack> items = NonNullList.withSize(6, ItemStack.EMPTY);
+     public int lastInteractedSlot = -1;
  
-+// CraftBukkit start
-+import java.util.List;
-+import org.bukkit.Location;
-+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
-+import org.bukkit.entity.HumanEntity;
-+// CraftBukkit end
-+
- public class ChiseledBookShelfBlockEntity extends BlockEntity implements Container {
- 
-     public static final int MAX_BOOKS_IN_STORAGE = 6;
-     private static final Logger LOGGER = LogUtils.getLogger();
-     private final NonNullList<ItemStack> items;
-     public int lastInteractedSlot;
 +    // CraftBukkit start - add fields and methods
-+    public List<HumanEntity> transaction = new java.util.ArrayList<>();
++    public java.util.List<org.bukkit.entity.HumanEntity> transaction = new java.util.ArrayList<>();
 +    private int maxStack = 1;
- 
++
 +    @Override
-+    public List<ItemStack> getContents() {
++    public java.util.List<net.minecraft.world.item.ItemStack> getContents() {
 +        return this.items;
 +    }
 +
 +    @Override
-+    public void onOpen(CraftHumanEntity who) {
-+        this.transaction.add(who);
++    public void onOpen(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.add(player);
 +    }
 +
 +    @Override
-+    public void onClose(CraftHumanEntity who) {
-+        this.transaction.remove(who);
++    public void onClose(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.remove(player);
 +    }
 +
 +    @Override
-+    public List<HumanEntity> getViewers() {
++    public java.util.List<org.bukkit.entity.HumanEntity> getViewers() {
 +        return this.transaction;
 +    }
 +
@@ -47,25 +34,25 @@
 +    }
 +
 +    @Override
-+    public Location getLocation() {
++    public org.bukkit.Location getLocation() {
 +        if (this.level == null) return null;
-+        return new org.bukkit.Location(this.level.getWorld(), this.worldPosition.getX(), this.worldPosition.getY(), this.worldPosition.getZ());
++        return io.papermc.paper.util.MCUtil.toLocation(this.level, this.worldPosition);
 +    }
 +    // CraftBukkit end
 +
      public ChiseledBookShelfBlockEntity(BlockPos pos, BlockState state) {
          super(BlockEntityType.CHISELED_BOOKSHELF, pos, state);
-         this.items = NonNullList.withSize(6, ItemStack.EMPTY);
-@@ -100,7 +142,7 @@
- 
+     }
+@@ -93,7 +_,7 @@
+         ItemStack itemStack = Objects.requireNonNullElse(this.items.get(slot), ItemStack.EMPTY);
          this.items.set(slot, ItemStack.EMPTY);
-         if (!itemstack.isEmpty()) {
+         if (!itemStack.isEmpty()) {
 -            this.updateState(slot);
 +            if (this.level != null) this.updateState(slot); // CraftBukkit - SPIGOT-7381: check for null world
          }
  
-         return itemstack;
-@@ -115,7 +157,7 @@
+         return itemStack;
+@@ -108,7 +_,7 @@
      public void setItem(int slot, ItemStack stack) {
          if (stack.is(ItemTags.BOOKSHELF_BOOKS)) {
              this.items.set(slot, stack);
@@ -74,7 +61,7 @@
          } else if (stack.isEmpty()) {
              this.removeItem(slot, 1);
          }
-@@ -131,7 +173,7 @@
+@@ -124,7 +_,7 @@
  
      @Override
      public int getMaxStackSize() {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/CommandBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/CommandBlockEntity.java.patch
new file mode 100644
index 0000000000..dca791d38d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/CommandBlockEntity.java.patch
@@ -0,0 +1,25 @@
+--- a/net/minecraft/world/level/block/entity/CommandBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/CommandBlockEntity.java
+@@ -21,6 +_,13 @@
+     private boolean auto;
+     private boolean conditionMet;
+     private final BaseCommandBlock commandBlock = new BaseCommandBlock() {
++        // CraftBukkit start
++        @Override
++        public org.bukkit.command.CommandSender getBukkitSender(CommandSourceStack wrapper) {
++            return new org.bukkit.craftbukkit.command.CraftBlockCommandSender(wrapper, CommandBlockEntity.this);
++        }
++        // CraftBukkit end
++
+         @Override
+         public void setCommand(String command) {
+             super.setCommand(command);
+@@ -51,7 +_,7 @@
+                 Vec3.atCenterOf(CommandBlockEntity.this.worldPosition),
+                 new Vec2(0.0F, direction.toYRot()),
+                 this.getLevel(),
+-                2,
++                this.getLevel().paperConfig().commandBlocks.permissionsLevel, // Paper - configurable command block perm level
+                 this.getName().getString(),
+                 this.getName(),
+                 this.getLevel().getServer(),
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/ConduitBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/ConduitBlockEntity.java.patch
new file mode 100644
index 0000000000..31b86a4f4f
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/ConduitBlockEntity.java.patch
@@ -0,0 +1,70 @@
+--- a/net/minecraft/world/level/block/entity/ConduitBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/ConduitBlockEntity.java
+@@ -9,6 +_,7 @@
+ import net.minecraft.core.particles.ParticleTypes;
+ import net.minecraft.nbt.CompoundTag;
+ import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
++import net.minecraft.server.level.ServerLevel;
+ import net.minecraft.sounds.SoundEvent;
+ import net.minecraft.sounds.SoundEvents;
+ import net.minecraft.sounds.SoundSource;
+@@ -168,8 +_,20 @@
+     }
+ 
+     private static void applyEffects(Level level, BlockPos pos, List<BlockPos> positions) {
++        // CraftBukkit start
++        ConduitBlockEntity.applyEffects(level, pos, ConduitBlockEntity.getRange(positions));
++    }
++
++    public static int getRange(List<BlockPos> positions) {
++        // CraftBukkit end
+         int size = positions.size();
+         int i = size / 7 * 16;
++        // CraftBukkit start
++        return i;
++    }
++
++    private static void applyEffects(Level level, BlockPos pos, int i) { // i = effect range in blocks
++        // CraftBukkit end
+         int x = pos.getX();
+         int y = pos.getY();
+         int z = pos.getZ();
+@@ -178,13 +_,19 @@
+         if (!entitiesOfClass.isEmpty()) {
+             for (Player player : entitiesOfClass) {
+                 if (pos.closerThan(player.blockPosition(), i) && player.isInWaterOrRain()) {
+-                    player.addEffect(new MobEffectInstance(MobEffects.CONDUIT_POWER, 260, 0, true, true));
++                    player.addEffect(new MobEffectInstance(MobEffects.CONDUIT_POWER, 260, 0, true, true), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.CONDUIT); // CraftBukkit
+                 }
+             }
+         }
+     }
+ 
+     private static void updateDestroyTarget(Level level, BlockPos pos, BlockState state, List<BlockPos> positions, ConduitBlockEntity blockEntity) {
++        // CraftBukkit start - add "damageTarget" boolean
++        ConduitBlockEntity.updateDestroyTarget(level, pos, state, positions, blockEntity, true);
++    }
++
++    public static void updateDestroyTarget(Level level, BlockPos pos, BlockState state, List<BlockPos> positions, ConduitBlockEntity blockEntity, boolean damageTarget) {
++        // CraftBukkit end
+         LivingEntity livingEntity = blockEntity.destroyTarget;
+         int size = positions.size();
+         if (size < 42) {
+@@ -203,7 +_,8 @@
+             blockEntity.destroyTarget = null;
+         }
+ 
+-        if (blockEntity.destroyTarget != null) {
++        if (damageTarget && blockEntity.destroyTarget != null) { // CraftBukkit
++            if (blockEntity.destroyTarget.hurtServer((net.minecraft.server.level.ServerLevel) level, level.damageSources().magic().directBlock(level, pos), 4.0F)) // CraftBukkit
+             level.playSound(
+                 null,
+                 blockEntity.destroyTarget.getX(),
+@@ -214,7 +_,6 @@
+                 1.0F,
+                 1.0F
+             );
+-            blockEntity.destroyTarget.hurt(level.damageSources().magic(), 4.0F);
+         }
+ 
+         if (livingEntity != blockEntity.destroyTarget) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ContainerOpenersCounter.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/ContainerOpenersCounter.java.patch
similarity index 51%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ContainerOpenersCounter.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/entity/ContainerOpenersCounter.java.patch
index b594786d46..6d0edb5d1d 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ContainerOpenersCounter.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/ContainerOpenersCounter.java.patch
@@ -1,76 +1,76 @@
 --- a/net/minecraft/world/level/block/entity/ContainerOpenersCounter.java
 +++ b/net/minecraft/world/level/block/entity/ContainerOpenersCounter.java
-@@ -17,6 +17,7 @@
+@@ -13,6 +_,7 @@
      private static final int CHECK_TICK_DELAY = 5;
      private int openCount;
      private double maxInteractionRange;
 +    public boolean opened; // CraftBukkit
  
-     public ContainerOpenersCounter() {}
+     protected abstract void onOpen(Level level, BlockPos pos, BlockState state);
  
-@@ -26,11 +27,36 @@
+@@ -20,10 +_,36 @@
  
-     protected abstract void openerCountChanged(Level world, BlockPos pos, BlockState state, int oldViewerCount, int newViewerCount);
+     protected abstract void openerCountChanged(Level level, BlockPos pos, BlockState state, int count, int openCount);
  
 +    // CraftBukkit start
-+    public void onAPIOpen(Level world, BlockPos blockposition, BlockState iblockdata) {
-+        this.onOpen(world, blockposition, iblockdata);
++    public void onAPIOpen(Level level, BlockPos blockPos, BlockState blockState) {
++        this.onOpen(level, blockPos, blockState);
 +    }
 +
-+    public void onAPIClose(Level world, BlockPos blockposition, BlockState iblockdata) {
-+        this.onClose(world, blockposition, iblockdata);
++    public void onAPIClose(Level level, BlockPos blockPos, BlockState blockState) {
++        this.onClose(level, blockPos, blockState);
 +    }
 +
-+    public void openerAPICountChanged(Level world, BlockPos blockposition, BlockState iblockdata, int i, int j) {
-+        this.openerCountChanged(world, blockposition, iblockdata, i, j);
++    public void openerAPICountChanged(Level level, BlockPos blockPos, BlockState blockState, int count, int openCount) {
++        this.openerCountChanged(level, blockPos, blockState, count, openCount);
 +    }
 +    // CraftBukkit end
 +
      protected abstract boolean isOwnContainer(Player player);
  
-     public void incrementOpeners(Player player, Level world, BlockPos pos, BlockState state) {
+     public void incrementOpeners(Player player, Level level, BlockPos pos, BlockState state) {
 +        int oldPower = Math.max(0, Math.min(15, this.openCount)); // CraftBukkit - Get power before new viewer is added
          int i = this.openCount++;
- 
++
 +        // CraftBukkit start - Call redstone event
-+        if (world.getBlockState(pos).is(net.minecraft.world.level.block.Blocks.TRAPPED_CHEST)) {
++        if (level.getBlockState(pos).is(net.minecraft.world.level.block.Blocks.TRAPPED_CHEST)) {
 +            int newPower = Math.max(0, Math.min(15, this.openCount));
 +
 +            if (oldPower != newPower) {
-+                org.bukkit.craftbukkit.event.CraftEventFactory.callRedstoneChange(world, pos, oldPower, newPower);
++                org.bukkit.craftbukkit.event.CraftEventFactory.callRedstoneChange(level, pos, oldPower, newPower);
 +            }
 +        }
 +        // CraftBukkit end
 +
          if (i == 0) {
-             this.onOpen(world, pos, state);
-             world.gameEvent((Entity) player, (Holder) GameEvent.CONTAINER_OPEN, pos);
-@@ -42,8 +68,20 @@
+             this.onOpen(level, pos, state);
+             level.gameEvent(player, GameEvent.CONTAINER_OPEN, pos);
+@@ -35,7 +_,20 @@
      }
  
-     public void decrementOpeners(Player player, Level world, BlockPos pos, BlockState state) {
+     public void decrementOpeners(Player player, Level level, BlockPos pos, BlockState state) {
 +        int oldPower = Math.max(0, Math.min(15, this.openCount)); // CraftBukkit - Get power before new viewer is added
 +        if (this.openCount == 0) return; // Paper - Prevent ContainerOpenersCounter openCount from going negative
          int i = this.openCount--;
- 
++
 +        // CraftBukkit start - Call redstone event
-+        if (world.getBlockState(pos).is(net.minecraft.world.level.block.Blocks.TRAPPED_CHEST)) {
++        if (level.getBlockState(pos).is(net.minecraft.world.level.block.Blocks.TRAPPED_CHEST)) {
 +            int newPower = Math.max(0, Math.min(15, this.openCount));
 +
 +            if (oldPower != newPower) {
-+                org.bukkit.craftbukkit.event.CraftEventFactory.callRedstoneChange(world, pos, oldPower, newPower);
++                org.bukkit.craftbukkit.event.CraftEventFactory.callRedstoneChange(level, pos, oldPower, newPower);
 +            }
 +        }
 +        // CraftBukkit end
 +
          if (this.openCount == 0) {
-             this.onClose(world, pos, state);
-             world.gameEvent((Entity) player, (Holder) GameEvent.CONTAINER_CLOSE, pos);
-@@ -72,6 +110,7 @@
+             this.onClose(level, pos, state);
+             level.gameEvent(player, GameEvent.CONTAINER_CLOSE, pos);
+@@ -60,6 +_,7 @@
          }
  
-         int i = list.size();
-+        if (this.opened) i++; // CraftBukkit - add dummy count from API
-         int j = this.openCount;
- 
-         if (j != i) {
+         int size = playersWithContainerOpen.size();
++        if (this.opened) size++; // CraftBukkit - add dummy count from API
+         int i = this.openCount;
+         if (i != size) {
+             boolean flag = size != 0;
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/CrafterBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/CrafterBlockEntity.java.patch
new file mode 100644
index 0000000000..edc04acff8
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/CrafterBlockEntity.java.patch
@@ -0,0 +1,50 @@
+--- a/net/minecraft/world/level/block/entity/CrafterBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/CrafterBlockEntity.java
+@@ -56,6 +_,47 @@
+         }
+     };
+ 
++    // CraftBukkit start - add fields and methods
++    public java.util.List<org.bukkit.entity.HumanEntity> transaction = new java.util.ArrayList<>();
++    private int maxStack = MAX_STACK;
++
++    @Override
++    public java.util.List<net.minecraft.world.item.ItemStack> getContents() {
++        return this.items;
++    }
++
++    @Override
++    public void onOpen(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.add(player);
++    }
++
++    @Override
++    public void onClose(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.remove(player);
++    }
++
++    @Override
++    public java.util.List<org.bukkit.entity.HumanEntity> getViewers() {
++        return this.transaction;
++    }
++
++    @Override
++    public int getMaxStackSize() {
++        return this.maxStack;
++    }
++
++    @Override
++    public void setMaxStackSize(int size) {
++        this.maxStack = size;
++    }
++
++    @Override
++    public org.bukkit.Location getLocation() {
++        if (this.level == null) return null;
++        return io.papermc.paper.util.MCUtil.toLocation(this.level, this.worldPosition);
++    }
++    // CraftBukkit end
++
+     public CrafterBlockEntity(BlockPos pos, BlockState state) {
+         super(BlockEntityType.CRAFTER, pos, state);
+     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/DecoratedPotBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/DecoratedPotBlockEntity.java.patch
similarity index 54%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/entity/DecoratedPotBlockEntity.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/entity/DecoratedPotBlockEntity.java.patch
index 6c93436b46..08a08744a5 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/DecoratedPotBlockEntity.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/DecoratedPotBlockEntity.java.patch
@@ -1,48 +1,37 @@
 --- a/net/minecraft/world/level/block/entity/DecoratedPotBlockEntity.java
 +++ b/net/minecraft/world/level/block/entity/DecoratedPotBlockEntity.java
-@@ -20,8 +20,59 @@
- import net.minecraft.world.level.storage.loot.LootTable;
+@@ -20,6 +_,48 @@
  import net.minecraft.world.ticks.ContainerSingleItem;
  
-+// CraftBukkit start
-+import java.util.ArrayList;
-+import java.util.Arrays;
-+import java.util.List;
-+import org.bukkit.Location;
-+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
-+import org.bukkit.craftbukkit.util.CraftLocation;
-+import org.bukkit.entity.HumanEntity;
-+// CraftBukkit end
-+
  public class DecoratedPotBlockEntity extends BlockEntity implements RandomizableContainer, ContainerSingleItem.BlockContainerSingleItem {
- 
++
 +    // CraftBukkit start - add fields and methods
-+    public List<HumanEntity> transaction = new ArrayList<>();
++    public List<org.bukkit.entity.HumanEntity> transaction = new java.util.ArrayList<>();
 +    private int maxStack = MAX_STACK;
 +
 +    @Override
 +    public List<ItemStack> getContents() {
-+        return Arrays.asList(this.item);
++        return java.util.List.of(this.item);
 +    }
 +
 +    @Override
-+    public void onOpen(CraftHumanEntity who) {
-+        this.transaction.add(who);
++    public void onOpen(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.add(player);
 +    }
 +
 +    @Override
-+    public void onClose(CraftHumanEntity who) {
-+        this.transaction.remove(who);
++    public void onClose(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.remove(player);
 +    }
 +
 +    @Override
-+    public List<HumanEntity> getViewers() {
++    public java.util.List<org.bukkit.entity.HumanEntity> getViewers() {
 +        return this.transaction;
 +    }
 +
 +    @Override
 +    public int getMaxStackSize() {
-+       return this.maxStack;
++        return this.maxStack;
 +    }
 +
 +    @Override
@@ -51,9 +40,9 @@
 +    }
 +
 +    @Override
-+    public Location getLocation() {
++    public org.bukkit.Location getLocation() {
 +        if (this.level == null) return null;
-+        return CraftLocation.toBukkit(this.worldPosition, this.level.getWorld());
++        return org.bukkit.craftbukkit.util.CraftLocation.toBukkit(this.worldPosition, this.level.getWorld());
 +    }
 +    // CraftBukkit end
 +
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/DispenserBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/DispenserBlockEntity.java.patch
new file mode 100644
index 0000000000..1448e2338d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/DispenserBlockEntity.java.patch
@@ -0,0 +1,39 @@
+--- a/net/minecraft/world/level/block/entity/DispenserBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/DispenserBlockEntity.java
+@@ -17,6 +_,36 @@
+     public static final int CONTAINER_SIZE = 9;
+     private NonNullList<ItemStack> items = NonNullList.withSize(9, ItemStack.EMPTY);
+ 
++    // CraftBukkit start - add fields and methods
++    public java.util.List<org.bukkit.entity.HumanEntity> transaction = new java.util.ArrayList<>();
++    private int maxStack = MAX_STACK;
++
++    public java.util.List<net.minecraft.world.item.ItemStack> getContents() {
++        return this.items;
++    }
++
++    public void onOpen(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.add(player);
++    }
++
++    public void onClose(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.remove(player);
++    }
++
++    public java.util.List<org.bukkit.entity.HumanEntity> getViewers() {
++        return this.transaction;
++    }
++
++    @Override
++    public int getMaxStackSize() {
++        return this.maxStack;
++    }
++
++    public void setMaxStackSize(int size) {
++        this.maxStack = size;
++    }
++    // CraftBukkit end
++
+     protected DispenserBlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState blockState) {
+         super(type, pos, blockState);
+     }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/HopperBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/HopperBlockEntity.java.patch
new file mode 100644
index 0000000000..f654073e2d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/HopperBlockEntity.java.patch
@@ -0,0 +1,304 @@
+--- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java
+@@ -37,6 +_,37 @@
+     private long tickedGameTime;
+     private Direction facing;
+ 
++    // CraftBukkit start - add fields and methods
++    public List<org.bukkit.entity.HumanEntity> transaction = new java.util.ArrayList<>();
++    private int maxStack = MAX_STACK;
++
++    public List<ItemStack> getContents() {
++        return this.items;
++    }
++
++    public void onOpen(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.add(player);
++    }
++
++    public void onClose(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.remove(player);
++    }
++
++    public java.util.List<org.bukkit.entity.HumanEntity> getViewers() {
++        return this.transaction;
++    }
++
++    @Override
++    public int getMaxStackSize() {
++        return this.maxStack;
++    }
++
++    public void setMaxStackSize(int size) {
++        this.maxStack = size;
++    }
++    // CraftBukkit end
++
++
+     public HopperBlockEntity(BlockPos pos, BlockState blockState) {
+         super(BlockEntityType.HOPPER, pos, blockState);
+         this.facing = blockState.getValue(HopperBlock.FACING);
+@@ -97,7 +_,14 @@
+         blockEntity.tickedGameTime = level.getGameTime();
+         if (!blockEntity.isOnCooldown()) {
+             blockEntity.setCooldown(0);
+-            tryMoveItems(level, pos, state, blockEntity, () -> suckInItems(level, blockEntity));
++            // Spigot start
++            boolean result = tryMoveItems(level, pos, state, blockEntity, () -> {
++                return suckInItems(level, blockEntity);
++            });
++            if (!result && blockEntity.level.spigotConfig.hopperCheck > 1) {
++                blockEntity.setCooldown(blockEntity.level.spigotConfig.hopperCheck);
++            }
++            // Spigot end
+         }
+     }
+ 
+@@ -116,7 +_,7 @@
+                 }
+ 
+                 if (flag) {
+-                    blockEntity.setCooldown(8);
++                    blockEntity.setCooldown(level.spigotConfig.hopperTransfer); // Spigot
+                     setChanged(level, pos, state);
+                     return true;
+                 }
+@@ -149,14 +_,47 @@
+                     ItemStack item = blockEntity.getItem(i);
+                     if (!item.isEmpty()) {
+                         int count = item.getCount();
+-                        ItemStack itemStack = addItem(blockEntity, attachedContainer, blockEntity.removeItem(i, 1), opposite);
++                        // CraftBukkit start - Call event when pushing items into other inventories
++                        ItemStack original = item.copy();
++                        org.bukkit.craftbukkit.inventory.CraftItemStack oitemstack = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(
++                            blockEntity.removeItem(i, level.spigotConfig.hopperAmount)
++                        ); // Spigot
++
++                        org.bukkit.inventory.Inventory destinationInventory;
++                        // Have to special case large chests as they work oddly
++                        if (attachedContainer instanceof final net.minecraft.world.CompoundContainer compoundContainer) {
++                            destinationInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest(compoundContainer);
++                        } else if (attachedContainer.getOwner() != null) {
++                            destinationInventory = attachedContainer.getOwner().getInventory();
++                        } else {
++                            destinationInventory = new org.bukkit.craftbukkit.inventory.CraftInventory(attachedContainer);
++                        }
++
++                        org.bukkit.event.inventory.InventoryMoveItemEvent event = new org.bukkit.event.inventory.InventoryMoveItemEvent(
++                            blockEntity.getOwner().getInventory(),
++                            oitemstack,
++                            destinationInventory,
++                            true
++                        );
++                        if (!event.callEvent()) {
++                            blockEntity.setItem(i, original);
++                            blockEntity.setCooldown(level.spigotConfig.hopperTransfer); // Delay hopper checks // Spigot
++                            return false;
++                        }
++                        int origCount = event.getItem().getAmount(); // Spigot
++                        ItemStack itemStack = HopperBlockEntity.addItem(blockEntity, attachedContainer, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem()), opposite);
++                        // CraftBukkit end
++
+                         if (itemStack.isEmpty()) {
+                             attachedContainer.setChanged();
+                             return true;
+                         }
+ 
+                         item.setCount(count);
+-                        if (count == 1) {
++                        // Spigot start
++                        item.shrink(origCount - itemStack.getCount());
++                        if (count <= level.spigotConfig.hopperAmount) {
++                            // Spigot end
+                             blockEntity.setItem(i, item);
+                         }
+                     }
+@@ -219,7 +_,7 @@
+             Direction direction = Direction.DOWN;
+ 
+             for (int i : getSlots(sourceContainer, direction)) {
+-                if (tryTakeInItemFromSlot(hopper, sourceContainer, i, direction)) {
++                if (tryTakeInItemFromSlot(hopper, sourceContainer, i, direction, level)) { // Spigot
+                     return true;
+                 }
+             }
+@@ -239,18 +_,56 @@
+         }
+     }
+ 
+-    private static boolean tryTakeInItemFromSlot(Hopper hopper, Container container, int slot, Direction direction) {
++    private static boolean tryTakeInItemFromSlot(Hopper hopper, Container container, int slot, Direction direction, Level level) { // Spigot
+         ItemStack item = container.getItem(slot);
+         if (!item.isEmpty() && canTakeItemFromContainer(hopper, container, item, slot, direction)) {
+             int count = item.getCount();
+-            ItemStack itemStack = addItem(container, hopper, container.removeItem(slot, 1), null);
++            // CraftBukkit start - Call event on collection of items from inventories into the hopper
++            ItemStack original = item.copy();
++            org.bukkit.craftbukkit.inventory.CraftItemStack oitemstack = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(
++                container.removeItem(slot, level.spigotConfig.hopperAmount) // Spigot
++            );
++
++            org.bukkit.inventory.Inventory sourceInventory;
++            // Have to special case large chests as they work oddly
++            if (container instanceof final net.minecraft.world.CompoundContainer compoundContainer) {
++                sourceInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest(compoundContainer);
++            } else if (container.getOwner() != null) {
++                sourceInventory = container.getOwner().getInventory();
++            } else {
++                sourceInventory = new org.bukkit.craftbukkit.inventory.CraftInventory(container);
++            }
++
++            org.bukkit.event.inventory.InventoryMoveItemEvent event = new org.bukkit.event.inventory.InventoryMoveItemEvent(
++                sourceInventory,
++                oitemstack,
++                hopper.getOwner().getInventory(),
++                false
++            );
++
++            if (!event.callEvent()) {
++                container.setItem(slot, original);
++
++                if (hopper instanceof final HopperBlockEntity hopperBlockEntity) {
++                    hopperBlockEntity.setCooldown(level.spigotConfig.hopperTransfer); // Spigot
++                }
++
++                return false;
++            }
++            int origCount = event.getItem().getAmount(); // Spigot
++            ItemStack itemStack = HopperBlockEntity.addItem(container, hopper, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem()), null);
++            // CraftBukkit end
++
+             if (itemStack.isEmpty()) {
+                 container.setChanged();
+                 return true;
+             }
+ 
+             item.setCount(count);
+-            if (count == 1) {
++            // Spigot start
++            item.shrink(origCount - itemStack.getCount());
++            if (count <= level.spigotConfig.hopperAmount) {
++                // Spigot end
+                 container.setItem(slot, item);
+             }
+         }
+@@ -260,12 +_,20 @@
+ 
+     public static boolean addItem(Container container, ItemEntity item) {
+         boolean flag = false;
++        // CraftBukkit start
++        org.bukkit.event.inventory.InventoryPickupItemEvent event = new org.bukkit.event.inventory.InventoryPickupItemEvent(
++            container.getOwner().getInventory(), (org.bukkit.entity.Item) item.getBukkitEntity()
++        );
++        if (!event.callEvent()) {
++            return false;
++        }
++        // CraftBukkit end
+         ItemStack itemStack = item.getItem().copy();
+         ItemStack itemStack1 = addItem(null, container, itemStack, null);
+         if (itemStack1.isEmpty()) {
+             flag = true;
+             item.setItem(ItemStack.EMPTY);
+-            item.discard();
++            item.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
+         } else {
+             item.setItem(itemStack1);
+         }
+@@ -307,11 +_,18 @@
+             boolean flag = false;
+             boolean isEmpty = destination.isEmpty();
+             if (item.isEmpty()) {
++                // Spigot start - SPIGOT-6693, SimpleContainer#setItem
++                ItemStack leftover = ItemStack.EMPTY; // Paper - Make hoppers respect inventory max stack size
++                if (!stack.isEmpty() && stack.getCount() > destination.getMaxStackSize()) {
++                    leftover = stack; // Paper - Make hoppers respect inventory max stack size
++                    stack = stack.split(destination.getMaxStackSize());
++                }
++                // Spigot end
+                 destination.setItem(slot, stack);
+-                stack = ItemStack.EMPTY;
++                stack = leftover; // Paper - Make hoppers respect inventory max stack size
+                 flag = true;
+             } else if (canMergeItems(item, stack)) {
+-                int i = stack.getMaxStackSize() - item.getCount();
++                int i = Math.min(stack.getMaxStackSize(), destination.getMaxStackSize()) - item.getCount(); // Paper - Make hoppers respect inventory max stack size
+                 int min = Math.min(stack.getCount(), i);
+                 stack.shrink(min);
+                 item.grow(min);
+@@ -325,7 +_,7 @@
+                         min = 1;
+                     }
+ 
+-                    hopperBlockEntity.setCooldown(8 - min);
++                    hopperBlockEntity.setCooldown(hopperBlockEntity.level.spigotConfig.hopperTransfer - min); // Spigot
+                 }
+ 
+                 destination.setChanged();
+@@ -335,14 +_,57 @@
+         return stack;
+     }
+ 
++    // CraftBukkit start
++    @Nullable
++    private static Container runHopperInventorySearchEvent(
++        Container container,
++        org.bukkit.craftbukkit.block.CraftBlock hopper,
++        org.bukkit.craftbukkit.block.CraftBlock searchLocation,
++        org.bukkit.event.inventory.HopperInventorySearchEvent.ContainerType containerType
++    ) {
++        org.bukkit.event.inventory.HopperInventorySearchEvent event = new org.bukkit.event.inventory.HopperInventorySearchEvent(
++            (container != null) ? new org.bukkit.craftbukkit.inventory.CraftInventory(container) : null,
++            containerType,
++            hopper,
++            searchLocation
++        );
++        event.callEvent();
++        return (event.getInventory() != null) ? ((org.bukkit.craftbukkit.inventory.CraftInventory) event.getInventory()).getInventory() : null;
++    }
++    // CraftBukkit end
++
+     @Nullable
+     private static Container getAttachedContainer(Level level, BlockPos pos, HopperBlockEntity blockEntity) {
+-        return getContainerAt(level, pos.relative(blockEntity.facing));
++        // CraftBukkit start
++        BlockPos searchPosition = pos.relative(blockEntity.facing);
++        Container inventory = getContainerAt(level, searchPosition);
++
++        org.bukkit.craftbukkit.block.CraftBlock hopper = org.bukkit.craftbukkit.block.CraftBlock.at(level, pos);
++        org.bukkit.craftbukkit.block.CraftBlock searchBlock = org.bukkit.craftbukkit.block.CraftBlock.at(level, searchPosition);
++        return HopperBlockEntity.runHopperInventorySearchEvent(
++            inventory,
++            hopper,
++            searchBlock,
++            org.bukkit.event.inventory.HopperInventorySearchEvent.ContainerType.DESTINATION
++        );
++        // CraftBukkit end
+     }
+ 
+     @Nullable
+     private static Container getSourceContainer(Level level, Hopper hopper, BlockPos pos, BlockState state) {
+-        return getContainerAt(level, pos, state, hopper.getLevelX(), hopper.getLevelY() + 1.0, hopper.getLevelZ());
++        // CraftBukkit start
++        final Container inventory = HopperBlockEntity.getContainerAt(level, pos, state, hopper.getLevelX(), hopper.getLevelY() + 1.0D, hopper.getLevelZ());
++
++        final BlockPos blockPosition = BlockPos.containing(hopper.getLevelX(), hopper.getLevelY(), hopper.getLevelZ());
++        org.bukkit.craftbukkit.block.CraftBlock hopperBlock = org.bukkit.craftbukkit.block.CraftBlock.at(level, blockPosition);
++        org.bukkit.craftbukkit.block.CraftBlock containerBlock = org.bukkit.craftbukkit.block.CraftBlock.at(level, blockPosition.above());
++        return HopperBlockEntity.runHopperInventorySearchEvent(
++            inventory,
++            hopperBlock,
++            containerBlock,
++            org.bukkit.event.inventory.HopperInventorySearchEvent.ContainerType.SOURCE
++        );
++        // CraftBukkit end
+     }
+ 
+     public static List<ItemEntity> getItemsAtAndAbove(Level level, Hopper hopper) {
+@@ -367,6 +_,7 @@
+ 
+     @Nullable
+     private static Container getBlockContainer(Level level, BlockPos pos, BlockState state) {
++        if (!level.spigotConfig.hopperCanLoadChunks && !level.hasChunkAt(pos)) return null; // Spigot
+         Block block = state.getBlock();
+         if (block instanceof WorldlyContainerHolder) {
+             return ((WorldlyContainerHolder)block).getContainer(state, level, pos);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/JigsawBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/JigsawBlockEntity.java.patch
similarity index 52%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/entity/JigsawBlockEntity.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/entity/JigsawBlockEntity.java.patch
index 11519f80c2..a023b4d2ec 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/JigsawBlockEntity.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/JigsawBlockEntity.java.patch
@@ -1,16 +1,16 @@
 --- a/net/minecraft/world/level/block/entity/JigsawBlockEntity.java
 +++ b/net/minecraft/world/level/block/entity/JigsawBlockEntity.java
-@@ -131,7 +131,12 @@
-     public void generate(ServerLevel world, int maxDepth, boolean keepJigsaws) {
+@@ -131,7 +_,12 @@
+     public void generate(ServerLevel level, int maxDepth, boolean keepJigsaws) {
          BlockPos blockPos = this.getBlockPos().relative(this.getBlockState().getValue(JigsawBlock.ORIENTATION).front());
-         Registry<StructureTemplatePool> registry = world.registryAccess().lookupOrThrow(Registries.TEMPLATE_POOL);
--        Holder<StructureTemplatePool> holder = registry.getOrThrow(this.pool);
+         Registry<StructureTemplatePool> registry = level.registryAccess().lookupOrThrow(Registries.TEMPLATE_POOL);
+-        Holder<StructureTemplatePool> orThrow = registry.getOrThrow(this.pool);
 +        // Paper start - Replace getHolderOrThrow with a null check
-+        Holder<StructureTemplatePool> holder = registry.get(this.pool).orElse(null);
-+        if (holder == null) {
++        Holder<StructureTemplatePool> orThrow = registry.get(this.pool).orElse(null);
++        if (orThrow == null) {
 +            return;
 +        }
 +        // Paper end - Replace getHolderOrThrow with a null check
-         JigsawPlacement.generateJigsaw(world, holder, this.target, maxDepth, blockPos, keepJigsaws);
+         JigsawPlacement.generateJigsaw(level, orThrow, this.target, maxDepth, blockPos, keepJigsaws);
      }
  
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/JukeboxBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/JukeboxBlockEntity.java.patch
similarity index 51%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/entity/JukeboxBlockEntity.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/entity/JukeboxBlockEntity.java.patch
index fa8fa48ae9..7ef85bb6b9 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/JukeboxBlockEntity.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/JukeboxBlockEntity.java.patch
@@ -1,45 +1,32 @@
 --- a/net/minecraft/world/level/block/entity/JukeboxBlockEntity.java
 +++ b/net/minecraft/world/level/block/entity/JukeboxBlockEntity.java
-@@ -19,13 +19,57 @@
- import net.minecraft.world.phys.Vec3;
+@@ -20,6 +_,44 @@
  import net.minecraft.world.ticks.ContainerSingleItem;
  
-+// CraftBukkit start
-+import java.util.Collections;
-+import java.util.List;
-+import org.bukkit.Location;
-+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
-+import org.bukkit.entity.HumanEntity;
-+// CraftBukkit end
-+
  public class JukeboxBlockEntity extends BlockEntity implements ContainerSingleItem.BlockContainerSingleItem {
- 
-     public static final String SONG_ITEM_TAG_ID = "RecordItem";
-     public static final String TICKS_SINCE_SONG_STARTED_TAG_ID = "ticks_since_song_started";
-     private ItemStack item;
-     private final JukeboxSongPlayer jukeboxSongPlayer;
++
 +    // CraftBukkit start - add fields and methods
-+    public List<HumanEntity> transaction = new java.util.ArrayList<HumanEntity>();
++    public java.util.List<org.bukkit.entity.HumanEntity> transaction = new java.util.ArrayList<>();
 +    private int maxStack = MAX_STACK;
 +    public boolean opened;
- 
++
 +    @Override
-+    public List<ItemStack> getContents() {
-+        return Collections.singletonList(this.item);
++    public java.util.List<net.minecraft.world.item.ItemStack> getContents() {
++        return java.util.Collections.singletonList(this.item);
 +    }
 +
 +    @Override
-+    public void onOpen(CraftHumanEntity who) {
-+        this.transaction.add(who);
++    public void onOpen(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.add(player);
 +    }
 +
 +    @Override
-+    public void onClose(CraftHumanEntity who) {
-+        this.transaction.remove(who);
++    public void onClose(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.remove(player);
 +    }
 +
 +    @Override
-+    public List<HumanEntity> getViewers() {
++    public java.util.List<org.bukkit.entity.HumanEntity> getViewers() {
 +        return this.transaction;
 +    }
 +
@@ -49,16 +36,16 @@
 +    }
 +
 +    @Override
-+    public Location getLocation() {
++    public org.bukkit.Location getLocation() {
 +        if (this.level == null) return null;
-+        return new org.bukkit.Location(this.level.getWorld(), this.worldPosition.getX(), this.worldPosition.getY(), this.worldPosition.getZ());
++        return io.papermc.paper.util.MCUtil.toLocation(this.level, this.worldPosition);
 +    }
 +    // CraftBukkit end
 +
-     public JukeboxBlockEntity(BlockPos pos, BlockState state) {
-         super(BlockEntityType.JUKEBOX, pos, state);
-         this.item = ItemStack.EMPTY;
-@@ -137,7 +181,7 @@
+     public static final String SONG_ITEM_TAG_ID = "RecordItem";
+     public static final String TICKS_SINCE_SONG_STARTED_TAG_ID = "ticks_since_song_started";
+     private ItemStack item = ItemStack.EMPTY;
+@@ -126,7 +_,7 @@
  
      @Override
      public int getMaxStackSize() {
@@ -67,21 +54,19 @@
      }
  
      @Override
-@@ -156,12 +200,17 @@
+@@ -145,11 +_,16 @@
      }
  
      @VisibleForTesting
 -    public void setSongItemWithoutPlaying(ItemStack stack) {
--        this.item = stack;
--        JukeboxSong.fromStack(this.level.registryAccess(), stack).ifPresent((holder) -> {
--            this.jukeboxSongPlayer.setSongWithoutPlaying(holder, 0L);
-+    public void setSongItemWithoutPlaying(ItemStack itemstack, long ticksSinceSongStarted) { // CraftBukkit - add argument
-+        this.item = itemstack;
-+        this.jukeboxSongPlayer.song = null; // CraftBukkit - reset
-+        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
-         });
++    public void setSongItemWithoutPlaying(ItemStack stack, final long ticksSinceSongStarted) { // CraftBukkit - passed ticks since song started
+         this.item = stack;
+-        JukeboxSong.fromStack(this.level.registryAccess(), stack)
+-            .ifPresent(holder -> this.jukeboxSongPlayer.setSongWithoutPlaying((Holder<JukeboxSong>)holder, 0L));
 -        this.level.updateNeighborsAt(this.getBlockPos(), this.getBlockState().getBlock());
++        this.jukeboxSongPlayer.song = null; // CraftBukkit - reset
++        JukeboxSong.fromStack(this.level != null ? this.level.registryAccess() : org.bukkit.craftbukkit.CraftRegistry.getMinecraftRegistry(), stack) // Paper - fallback to other RegistryAccess if no level
++            .ifPresent(holder -> this.jukeboxSongPlayer.setSongWithoutPlaying((Holder<JukeboxSong>)holder, ticksSinceSongStarted)); // CraftBukkit - passed ticks since song started
 +        // CraftBukkit start - add null check for level
 +        if (this.level != null) {
 +            this.level.updateNeighborsAt(this.getBlockPos(), this.getBlockState().getBlock());
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/LecternBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/LecternBlockEntity.java.patch
similarity index 51%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/entity/LecternBlockEntity.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/entity/LecternBlockEntity.java.patch
index afaf4c6781..d3be2ca424 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/LecternBlockEntity.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/LecternBlockEntity.java.patch
@@ -1,24 +1,6 @@
 --- a/net/minecraft/world/level/block/entity/LecternBlockEntity.java
 +++ b/net/minecraft/world/level/block/entity/LecternBlockEntity.java
-@@ -28,6 +28,17 @@
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.phys.Vec2;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import java.util.ArrayList;
-+import java.util.Arrays;
-+import java.util.List;
-+import org.bukkit.Location;
-+import org.bukkit.block.Lectern;
-+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
-+import org.bukkit.craftbukkit.util.CraftLocation;
-+import org.bukkit.entity.HumanEntity;
-+import org.bukkit.inventory.InventoryHolder;
-+// CraftBukkit end
- 
- public class LecternBlockEntity extends BlockEntity implements Clearable, MenuProvider {
- 
-@@ -35,8 +46,55 @@
+@@ -32,7 +_,53 @@
      public static final int NUM_DATA = 1;
      public static final int SLOT_BOOK = 0;
      public static final int NUM_SLOTS = 1;
@@ -26,27 +8,26 @@
 +    // CraftBukkit start - add fields and methods
 +    public final Container bookAccess = new LecternInventory();
 +    public class LecternInventory implements Container {
-+
-+        public List<HumanEntity> transaction = new ArrayList<>();
++        public java.util.List<org.bukkit.entity.HumanEntity> transaction = new java.util.ArrayList<>();
 +        private int maxStack = 1;
 +
-         @Override
-+        public List<ItemStack> getContents() {
-+            return Arrays.asList(LecternBlockEntity.this.book);
++        @Override
++        public java.util.List<net.minecraft.world.item.ItemStack> getContents() {
++            return java.util.List.of(LecternBlockEntity.this.book);
 +        }
 +
 +        @Override
-+        public void onOpen(CraftHumanEntity who) {
-+            this.transaction.add(who);
++        public void onOpen(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++            this.transaction.add(player);
 +        }
 +
 +        @Override
-+        public void onClose(CraftHumanEntity who) {
-+            this.transaction.remove(who);
++        public void onClose(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++            this.transaction.remove(player);
 +        }
 +
 +        @Override
-+        public List<HumanEntity> getViewers() {
++        public java.util.List<org.bukkit.entity.HumanEntity> getViewers() {
 +            return this.transaction;
 +        }
 +
@@ -56,14 +37,14 @@
 +        }
 +
 +        @Override
-+        public Location getLocation() {
++        public org.bukkit.Location getLocation() {
 +            if (LecternBlockEntity.this.level == null) return null;
-+            return CraftLocation.toBukkit(LecternBlockEntity.this.worldPosition, LecternBlockEntity.this.level.getWorld());
++            return io.papermc.paper.util.MCUtil.toLocation(LecternBlockEntity.this.level, LecternBlockEntity.this.worldPosition);
 +        }
 +
 +        @Override
-+        public InventoryHolder getOwner() {
-+            return (Lectern) LecternBlockEntity.this.getOwner();
++        public org.bukkit.inventory.InventoryHolder getOwner() {
++            return LecternBlockEntity.this.getOwner();
 +        }
 +
 +        public LecternBlockEntity getLectern() {
@@ -71,25 +52,22 @@
 +        }
 +        // CraftBukkit end
 +
-+        @Override
+         @Override
          public int getContainerSize() {
              return 1;
-         }
-@@ -80,11 +138,20 @@
-         }
+@@ -76,11 +_,19 @@
  
          @Override
--        public void setItem(int slot, ItemStack stack) {}
-+        // CraftBukkit start
-+        public void setItem(int slot, ItemStack stack) {
+         public void setItem(int slot, ItemStack stack) {
++            // CraftBukkit start
 +            if (slot == 0) {
 +                LecternBlockEntity.this.setBook(stack);
 +                if (LecternBlockEntity.this.getLevel() != null) {
 +                    LecternBlock.resetBookState(null, LecternBlockEntity.this.getLevel(), LecternBlockEntity.this.getBlockPos(), LecternBlockEntity.this.getBlockState(), LecternBlockEntity.this.hasBook());
 +                }
 +            }
-+        }
-+        // CraftBukkit end
++            // CraftBukkit end
+         }
  
          @Override
          public int getMaxStackSize() {
@@ -98,17 +76,17 @@
          }
  
          @Override
-@@ -164,7 +231,7 @@
-         if (j != this.page) {
-             this.page = j;
+@@ -158,7 +_,7 @@
+         if (i != this.page) {
+             this.page = i;
              this.setChanged();
 -            LecternBlock.signalPageChange(this.getLevel(), this.getBlockPos(), this.getBlockState());
 +            if (this.level != null) LecternBlock.signalPageChange(this.getLevel(), this.getBlockPos(), this.getBlockState()); // CraftBukkit
          }
- 
      }
-@@ -189,6 +256,35 @@
-         return book;
+ 
+@@ -179,6 +_,36 @@
+         return stack;
      }
  
 +    // CraftBukkit start
@@ -119,8 +97,10 @@
 +        }
 +
 +        @Override
-+        public org.bukkit.command.CommandSender getBukkitSender(CommandSourceStack wrapper) {
-+            return wrapper.getEntity() != null ? wrapper.getEntity().getBukkitEntity() : new org.bukkit.craftbukkit.command.CraftBlockCommandSender(wrapper, LecternBlockEntity.this);
++        public org.bukkit.command.CommandSender getBukkitSender(CommandSourceStack commandSourceStack) {
++            return commandSourceStack.getEntity() != null
++                ? commandSourceStack.getEntity().getBukkitEntity()
++                : new org.bukkit.craftbukkit.command.CraftBlockCommandSender(commandSourceStack, LecternBlockEntity.this);
 +        }
 +
 +        @Override
@@ -139,26 +119,24 @@
 +        }
 +    };
 +    // CraftBukkit end
-+
-     private CommandSourceStack createCommandSourceStack(@Nullable Player player, ServerLevel world) {
-         String s;
-         Object object;
-@@ -203,7 +299,8 @@
+     private CommandSourceStack createCommandSourceStack(@Nullable Player player, ServerLevel level) {
+         String string;
+         Component component;
+@@ -191,7 +_,7 @@
+         }
  
-         Vec3 vec3d = Vec3.atCenterOf(this.worldPosition);
- 
--        return new CommandSourceStack(CommandSource.NULL, vec3d, Vec2.ZERO, world, 2, s, (Component) object, world.getServer(), player);
-+        // CraftBukkit - commandSource
-+        return new CommandSourceStack(this.commandSource, vec3d, Vec2.ZERO, world, 2, s, (Component) object, world.getServer(), player);
+         Vec3 vec3 = Vec3.atCenterOf(this.worldPosition);
+-        return new CommandSourceStack(CommandSource.NULL, vec3, Vec2.ZERO, level, 2, string, component, level.getServer(), player);
++        return new CommandSourceStack(this.commandSource, vec3, Vec2.ZERO, level, 2, string, component, level.getServer(), player); // CraftBukkit - commandSource
      }
  
      @Override
-@@ -236,7 +333,7 @@
+@@ -223,7 +_,7 @@
  
      @Override
-     public AbstractContainerMenu createMenu(int syncId, Inventory playerInventory, Player player) {
--        return new LecternMenu(syncId, this.bookAccess, this.dataAccess);
-+        return new LecternMenu(syncId, this.bookAccess, this.dataAccess, playerInventory); // CraftBukkit
+     public AbstractContainerMenu createMenu(int containerId, Inventory playerInventory, Player player) {
+-        return new LecternMenu(containerId, this.bookAccess, this.dataAccess);
++        return new LecternMenu(containerId, this.bookAccess, this.dataAccess, playerInventory); // CraftBukkit
      }
  
      @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java.patch
similarity index 85%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java.patch
index 16ff81b3cd..cb89a7e6d1 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java.patch
@@ -1,8 +1,8 @@
 --- a/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java
 +++ b/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java
-@@ -115,4 +115,13 @@
-         nbt.remove("LootTable");
-         nbt.remove("LootTableSeed");
+@@ -115,4 +_,13 @@
+         tag.remove("LootTable");
+         tag.remove("LootTableSeed");
      }
 +
 +    // Paper start - LootTable API
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java.patch
new file mode 100644
index 0000000000..657dcc0967
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java.patch
@@ -0,0 +1,21 @@
+--- a/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java
+@@ -34,8 +_,18 @@
+         this.catalystListener = new SculkCatalystBlockEntity.CatalystListener(blockState, new BlockPositionSource(pos));
+     }
+ 
++    // Paper start - Fix NPE in SculkBloomEvent world access
++    @Override
++    public void setLevel(Level level) {
++        super.setLevel(level);
++        this.catalystListener.sculkSpreader.level = level;
++    }
++    // Paper end - Fix NPE in SculkBloomEvent world access
++
+     public static void serverTick(Level level, BlockPos pos, BlockState state, SculkCatalystBlockEntity sculkCatalyst) {
++        org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = sculkCatalyst.getBlockPos(); // CraftBukkit - SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep.
+         sculkCatalyst.catalystListener.getSculkSpreader().updateCursors(level, pos, level.getRandom(), true);
++        org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = null; // CraftBukkit
+     }
+ 
+     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SculkSensorBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/SculkSensorBlockEntity.java.patch
similarity index 72%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SculkSensorBlockEntity.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/entity/SculkSensorBlockEntity.java.patch
index 112cb33190..1f46bbe4dc 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SculkSensorBlockEntity.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/SculkSensorBlockEntity.java.patch
@@ -1,20 +1,20 @@
 --- a/net/minecraft/world/level/block/entity/SculkSensorBlockEntity.java
 +++ b/net/minecraft/world/level/block/entity/SculkSensorBlockEntity.java
-@@ -26,6 +26,7 @@
+@@ -26,6 +_,7 @@
      private final VibrationSystem.Listener vibrationListener;
      private final VibrationSystem.User vibrationUser = this.createVibrationUser();
      public int lastVibrationFrequency;
 +    @Nullable public Integer rangeOverride = null; // Paper - Configurable sculk sensor listener range
  
-     protected SculkSensorBlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) {
-         super(type, pos, state);
-@@ -52,8 +53,16 @@
+     protected SculkSensorBlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState blockState) {
+         super(type, pos, blockState);
+@@ -52,8 +_,16 @@
                  .resultOrPartial(string -> LOGGER.error("Failed to parse vibration listener for Sculk Sensor: '{}'", string))
-                 .ifPresent(listener -> this.vibrationData = listener);
+                 .ifPresent(data -> this.vibrationData = data);
          }
 +        // Paper start - Configurable sculk sensor listener range
-+        if (nbt.contains(PAPER_LISTENER_RANGE_NBT_KEY)) {
-+            this.rangeOverride = nbt.getInt(PAPER_LISTENER_RANGE_NBT_KEY);
++        if (tag.contains(PAPER_LISTENER_RANGE_NBT_KEY)) {
++            this.rangeOverride = tag.getInt(PAPER_LISTENER_RANGE_NBT_KEY);
 +        } else {
 +            this.rangeOverride = null;
 +        }
@@ -23,23 +23,24 @@
  
 +    protected static final String PAPER_LISTENER_RANGE_NBT_KEY = "Paper.ListenerRange"; // Paper - Configurable sculk sensor listener range
      @Override
-     protected void saveAdditional(CompoundTag nbt, HolderLookup.Provider registries) {
-         super.saveAdditional(nbt, registries);
-@@ -63,7 +72,13 @@
+     protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
+         super.saveAdditional(tag, registries);
+@@ -63,7 +_,13 @@
              .encodeStart(registryOps, this.vibrationData)
              .resultOrPartial(string -> LOGGER.error("Failed to encode vibration listener for Sculk Sensor: '{}'", string))
-             .ifPresent(listenerNbt -> nbt.put("listener", listenerNbt));
-+        this.saveRangeOverride(nbt); // Paper - Configurable sculk sensor listener range
-     }
+             .ifPresent(tag1 -> tag.put("listener", tag1));
+-    }
++        this.saveRangeOverride(tag); // Paper - Configurable sculk sensor listener range
++    }
 +    // Paper start - Configurable sculk sensor listener range
-+    protected void saveRangeOverride(CompoundTag nbt) {
-+        if (this.rangeOverride != null && this.rangeOverride != VibrationUser.LISTENER_RANGE) nbt.putInt(PAPER_LISTENER_RANGE_NBT_KEY, this.rangeOverride); // only save if it's different from the default
++    protected void saveRangeOverride(CompoundTag tag) {
++        if (this.rangeOverride != null && this.rangeOverride != VibrationUser.LISTENER_RANGE) tag.putInt(PAPER_LISTENER_RANGE_NBT_KEY, this.rangeOverride); // only save if it's different from the default
 +    }
 +    // Paper end - Configurable sculk sensor listener range
  
      @Override
      public VibrationSystem.Data getVibrationData() {
-@@ -100,6 +115,7 @@
+@@ -100,6 +_,7 @@
  
          @Override
          public int getListenerRadius() {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SculkShriekerBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/SculkShriekerBlockEntity.java.patch
similarity index 83%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SculkShriekerBlockEntity.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/entity/SculkShriekerBlockEntity.java.patch
index fb8ab267d1..f6de079656 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SculkShriekerBlockEntity.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/SculkShriekerBlockEntity.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/level/block/entity/SculkShriekerBlockEntity.java
 +++ b/net/minecraft/world/level/block/entity/SculkShriekerBlockEntity.java
-@@ -105,6 +105,13 @@
+@@ -105,6 +_,13 @@
  
      @Nullable
      public static ServerPlayer tryGetPlayer(@Nullable Entity entity) {
@@ -14,12 +14,12 @@
          if (entity instanceof ServerPlayer) {
              return (ServerPlayer)entity;
          } else {
-@@ -190,7 +197,7 @@
-     private boolean trySummonWarden(ServerLevel world) {
+@@ -190,7 +_,7 @@
+     private boolean trySummonWarden(ServerLevel level) {
          return this.warningLevel >= 4
              && SpawnUtil.trySpawnMob(
--                    EntityType.WARDEN, EntitySpawnReason.TRIGGERED, world, this.getBlockPos(), 20, 5, 6, SpawnUtil.Strategy.ON_TOP_OF_COLLIDER, false
-+                    EntityType.WARDEN, EntitySpawnReason.TRIGGERED, world, this.getBlockPos(), 20, 5, 6, SpawnUtil.Strategy.ON_TOP_OF_COLLIDER, false, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.NATURAL, null // Paper - Entity#getEntitySpawnReason
+-                    EntityType.WARDEN, EntitySpawnReason.TRIGGERED, level, this.getBlockPos(), 20, 5, 6, SpawnUtil.Strategy.ON_TOP_OF_COLLIDER, false
++                    EntityType.WARDEN, EntitySpawnReason.TRIGGERED, level, this.getBlockPos(), 20, 5, 6, SpawnUtil.Strategy.ON_TOP_OF_COLLIDER, false, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.NATURAL, null // Paper - Entity#getEntitySpawnReason
                  )
                  .isPresent();
      }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java.patch
similarity index 53%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java.patch
index 384ab32533..dba115b265 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java.patch
@@ -1,22 +1,11 @@
 --- a/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java
 +++ b/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java
-@@ -33,6 +33,10 @@
- import net.minecraft.world.level.material.PushReaction;
- import net.minecraft.world.phys.AABB;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
-+import org.bukkit.entity.HumanEntity;
-+// CraftBukkit end
- 
- public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity implements WorldlyContainer {
- 
-@@ -52,6 +56,37 @@
+@@ -49,6 +_,37 @@
      @Nullable
      private final DyeColor color;
  
 +    // CraftBukkit start - add fields and methods
-+    public List<HumanEntity> transaction = new java.util.ArrayList<HumanEntity>();
++    public List<org.bukkit.entity.HumanEntity> transaction = new java.util.ArrayList<>();
 +    private int maxStack = MAX_STACK;
 +    public boolean opened;
 +
@@ -24,15 +13,15 @@
 +        return this.itemStacks;
 +    }
 +
-+    public void onOpen(CraftHumanEntity who) {
-+        this.transaction.add(who);
++    public void onOpen(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.add(player);
 +    }
 +
-+    public void onClose(CraftHumanEntity who) {
-+        this.transaction.remove(who);
++    public void onClose(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
++        this.transaction.remove(player);
 +    }
 +
-+    public List<HumanEntity> getViewers() {
++    public List<org.bukkit.entity.HumanEntity> getViewers() {
 +        return this.transaction;
 +    }
 +
@@ -46,22 +35,22 @@
 +    }
 +    // CraftBukkit end
 +
-     public ShulkerBoxBlockEntity(@Nullable DyeColor color, BlockPos pos, BlockState state) {
-         super(BlockEntityType.SHULKER_BOX, pos, state);
-         this.itemStacks = NonNullList.withSize(27, ItemStack.EMPTY);
-@@ -184,6 +219,7 @@
+     public ShulkerBoxBlockEntity(@Nullable DyeColor color, BlockPos pos, BlockState blockState) {
+         super(BlockEntityType.SHULKER_BOX, pos, blockState);
+         this.color = color;
+@@ -167,6 +_,7 @@
              }
  
-             ++this.openCount;
-+            if (this.opened) return; // CraftBukkit - only animate if the ShulkerBox hasn't been forced open already by an API call.
+             this.openCount++;
++            if (this.opened) return; // CraftBukkit - only animate if the ShulkerBox hasn't been forced open already by an API call
              this.level.blockEvent(this.worldPosition, this.getBlockState().getBlock(), 1, this.openCount);
              if (this.openCount == 1) {
-                 this.level.gameEvent((Entity) player, (Holder) GameEvent.CONTAINER_OPEN, this.worldPosition);
-@@ -197,6 +233,7 @@
+                 this.level.gameEvent(player, GameEvent.CONTAINER_OPEN, this.worldPosition);
+@@ -180,6 +_,7 @@
      public void stopOpen(Player player) {
          if (!this.remove && !player.isSpectator()) {
-             --this.openCount;
+             this.openCount--;
 +            if (this.opened) return; // CraftBukkit - only animate if the ShulkerBox hasn't been forced open already by an API call.
              this.level.blockEvent(this.worldPosition, this.getBlockState().getBlock(), 1, this.openCount);
              if (this.openCount <= 0) {
-                 this.level.gameEvent((Entity) player, (Holder) GameEvent.CONTAINER_CLOSE, this.worldPosition);
+                 this.level.gameEvent(player, GameEvent.CONTAINER_CLOSE, this.worldPosition);
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/SignBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/SignBlockEntity.java.patch
new file mode 100644
index 0000000000..b84d89679a
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/SignBlockEntity.java.patch
@@ -0,0 +1,183 @@
+--- a/net/minecraft/world/level/block/entity/SignBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/SignBlockEntity.java
+@@ -54,11 +_,16 @@
+         return new SignText();
+     }
+ 
+-    public boolean isFacingFrontText(Player player) {
++    public boolean isFacingFrontText(net.minecraft.world.entity.player.Player player) {
++        // Paper start - More Sign Block API
++        return this.isFacingFrontText(player.getX(), player.getZ());
++    }
++    public boolean isFacingFrontText(double x, double z) {
++        // Paper end - More Sign Block API
+         if (this.getBlockState().getBlock() instanceof SignBlock signBlock) {
+             Vec3 signHitboxCenterPosition = signBlock.getSignHitboxCenterPosition(this.getBlockState());
+-            double d = player.getX() - (this.getBlockPos().getX() + signHitboxCenterPosition.x);
+-            double d1 = player.getZ() - (this.getBlockPos().getZ() + signHitboxCenterPosition.z);
++            double d = x - ((double) this.getBlockPos().getX() + signHitboxCenterPosition.x); // Paper - More Sign Block API
++            double d1 = z - ((double) this.getBlockPos().getZ() + signHitboxCenterPosition.z); // Paper - More Sign Block AP
+             float yRotationDegrees = signBlock.getYRotationDegrees(this.getBlockState());
+             float f = (float)(Mth.atan2(d1, d) * 180.0F / (float)Math.PI) - 90.0F;
+             return Mth.degreesDifferenceAbs(yRotationDegrees, f) <= 90.0F;
+@@ -143,11 +_,13 @@
+ 
+     public void updateSignText(Player player, boolean isFrontText, List<FilteredText> filteredText) {
+         if (!this.isWaxed() && player.getUUID().equals(this.getPlayerWhoMayEdit()) && this.level != null) {
+-            this.updateText(signText -> this.setMessages(player, filteredText, signText), isFrontText);
++            this.updateText(signText -> this.setMessages(player, filteredText, signText, isFrontText), isFrontText); // CraftBukkit
+             this.setAllowedPlayerEditor(null);
+             this.level.sendBlockUpdated(this.getBlockPos(), this.getBlockState(), this.getBlockState(), 3);
+         } else {
+             LOGGER.warn("Player {} just tried to change non-editable sign", player.getName().getString());
++            if (player.distanceToSqr(this.getBlockPos().getX(), this.getBlockPos().getY(), this.getBlockPos().getZ()) < Mth.square(32)) // Paper - Don't send far away sign update
++            ((net.minecraft.server.level.ServerPlayer) player).connection.send(this.getUpdatePacket()); // CraftBukkit
+         }
+     }
+ 
+@@ -156,18 +_,40 @@
+         return this.setText(updater.apply(text), isFrontText);
+     }
+ 
+-    private SignText setMessages(Player player, List<FilteredText> filteredText, SignText text) {
++    private SignText setMessages(Player player, List<FilteredText> filteredText, SignText text, boolean front) { // CraftBukkit
++        SignText originalText = text; // CraftBukkit
+         for (int i = 0; i < filteredText.size(); i++) {
+             FilteredText filteredText1 = filteredText.get(i);
+             Style style = text.getMessage(i, player.isTextFilteringEnabled()).getStyle();
+             if (player.isTextFilteringEnabled()) {
+-                text = text.setMessage(i, Component.literal(filteredText1.filteredOrEmpty()).setStyle(style));
++                text = text.setMessage(i, Component.literal(net.minecraft.util.StringUtil.filterText(filteredText1.filteredOrEmpty())).setStyle(style)); // Paper - filter sign text to chat only
+             } else {
+                 text = text.setMessage(
+-                    i, Component.literal(filteredText1.raw()).setStyle(style), Component.literal(filteredText1.filteredOrEmpty()).setStyle(style)
++                    i, Component.literal(filteredText1.raw()).setStyle(style), Component.literal(net.minecraft.util.StringUtil.filterText(filteredText1.filteredOrEmpty())).setStyle(style) // Paper - filter sign text to chat only
+                 );
+             }
+         }
++
++        // CraftBukkit start
++        org.bukkit.entity.Player apiPlayer = ((net.minecraft.server.level.ServerPlayer) player).getBukkitEntity();
++        List<net.kyori.adventure.text.Component> lines = new java.util.ArrayList<>(); // Paper - adventure
++
++        for (int i = 0; i < filteredText.size(); ++i) {
++            lines.add(io.papermc.paper.adventure.PaperAdventure.asAdventure(text.getMessage(i, player.isTextFilteringEnabled()))); // Paper - Adventure
++        }
++
++        org.bukkit.event.block.SignChangeEvent event = new org.bukkit.event.block.SignChangeEvent(org.bukkit.craftbukkit.block.CraftBlock.at(this.level, this.worldPosition), apiPlayer, new java.util.ArrayList<>(lines), (front) ? org.bukkit.block.sign.Side.FRONT : org.bukkit.block.sign.Side.BACK); // Paper - Adventure
++        if (!event.callEvent()) {
++            return originalText;
++        }
++
++        Component[] components = org.bukkit.craftbukkit.block.CraftSign.sanitizeLines(event.lines()); // Paper - Adventure
++        for (int i = 0; i < components.length; i++) {
++            if (!java.util.Objects.equals(lines.get(i), event.line(i))) { // Paper - Adventure
++                text = text.setMessage(i, components[i]);
++            }
++        }
++        // CraftBukkit end
+ 
+         return text;
+     }
+@@ -207,7 +_,23 @@
+             Style style = component.getStyle();
+             ClickEvent clickEvent = style.getClickEvent();
+             if (clickEvent != null && clickEvent.getAction() == ClickEvent.Action.RUN_COMMAND) {
+-                player.getServer().getCommands().performPrefixedCommand(createCommandSourceStack(player, level, pos), clickEvent.getValue());
++                // Paper start - Fix commands from signs not firing command events
++                String command = clickEvent.getValue().startsWith("/") ? clickEvent.getValue() : "/" + clickEvent.getValue();
++                if (org.spigotmc.SpigotConfig.logCommands)  {
++                    LOGGER.info("{} issued server command: {}", player.getScoreboardName(), command);
++                }
++                io.papermc.paper.event.player.PlayerSignCommandPreprocessEvent event = new io.papermc.paper.event.player.PlayerSignCommandPreprocessEvent(
++                    (org.bukkit.entity.Player) player.getBukkitEntity(),
++                    command,
++                    new org.bukkit.craftbukkit.util.LazyPlayerSet(player.getServer()),
++                    (org.bukkit.block.Sign) org.bukkit.craftbukkit.block.CraftBlock.at(this.level, this.worldPosition).getState(),
++                    frontText ? org.bukkit.block.sign.Side.FRONT : org.bukkit.block.sign.Side.BACK
++                );
++                if (!event.callEvent()) {
++                    return false;
++                }
++                player.getServer().getCommands().performPrefixedCommand(this.createCommandSourceStack(((org.bukkit.craftbukkit.entity.CraftPlayer) event.getPlayer()).getHandle(), level, pos), event.getMessage());
++                // Paper end - Fix commands from signs not firing command events
+                 flag = true;
+             }
+         }
+@@ -215,10 +_,55 @@
+         return flag;
+     }
+ 
+-    private static CommandSourceStack createCommandSourceStack(@Nullable Player player, Level level, BlockPos pos) {
++    // CraftBukkit start
++    private final CommandSource commandSource = new CommandSource() {
++
++        @Override
++        public void sendSystemMessage(Component message) {}
++
++        @Override
++        public org.bukkit.command.CommandSender getBukkitSender(CommandSourceStack commandSourceStack) {
++            return commandSourceStack.getEntity() != null ? commandSourceStack.getEntity().getBukkitEntity() : new org.bukkit.craftbukkit.command.CraftBlockCommandSender(commandSourceStack, SignBlockEntity.this);
++        }
++
++        @Override
++        public boolean acceptsSuccess() {
++            return false;
++        }
++
++        @Override
++        public boolean acceptsFailure() {
++            return false;
++        }
++
++        @Override
++        public boolean shouldInformAdmins() {
++            return false;
++        }
++    };
++
++    private CommandSourceStack createCommandSourceStack(@Nullable Player player, Level world, BlockPos pos) {
++        // CraftBukkit end
+         String string = player == null ? "Sign" : player.getName().getString();
+         Component component = (Component)(player == null ? Component.literal("Sign") : player.getDisplayName());
+-        return new CommandSourceStack(CommandSource.NULL, Vec3.atCenterOf(pos), Vec2.ZERO, (ServerLevel)level, 2, string, component, level.getServer(), player);
++
++        // Paper start - Fix commands from signs not firing command events
++        CommandSource commandSource = world.paperConfig().misc.showSignClickCommandFailureMsgsToPlayer ? new io.papermc.paper.commands.DelegatingCommandSource(this.commandSource) {
++            @Override
++            public void sendSystemMessage(Component message) {
++                if (player instanceof final net.minecraft.server.level.ServerPlayer serverPlayer) {
++                    serverPlayer.sendSystemMessage(message);
++                }
++            }
++
++            @Override
++            public boolean acceptsFailure() {
++                return true;
++            }
++        } : this.commandSource;
++        // Paper end - Fix commands from signs not firing command events
++        // CraftBukkit - this
++        return new CommandSourceStack(commandSource, Vec3.atCenterOf(pos), Vec2.ZERO, (ServerLevel) world, 2, string, component, world.getServer(), player); // Paper - Fix commands from signs not firing command events
+     }
+ 
+     @Override
+@@ -237,12 +_,17 @@
+ 
+     @Nullable
+     public UUID getPlayerWhoMayEdit() {
++        // CraftBukkit start - unnecessary sign ticking removed, so do this lazily
++        if (this.level != null && this.playerWhoMayEdit != null) {
++            this.clearInvalidPlayerWhoMayEdit(this, this.level, this.playerWhoMayEdit);
++        }
++        // CraftBukkit end
+         return this.playerWhoMayEdit;
+     }
+ 
+     private void markUpdated() {
+         this.setChanged();
+-        this.level.sendBlockUpdated(this.getBlockPos(), this.getBlockState(), this.getBlockState(), 3);
++        if (this.level != null) this.level.sendBlockUpdated(this.getBlockPos(), this.getBlockState(), this.getBlockState(), 3); // CraftBukkit - skip notify if world is null (SPIGOT-5122)
+     }
+ 
+     public boolean isWaxed() {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SkullBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/SkullBlockEntity.java.patch
similarity index 76%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SkullBlockEntity.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/entity/SkullBlockEntity.java.patch
index a683a1cb1b..c28bc38459 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SkullBlockEntity.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/SkullBlockEntity.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/level/block/entity/SkullBlockEntity.java
 +++ b/net/minecraft/world/level/block/entity/SkullBlockEntity.java
-@@ -41,7 +41,7 @@
+@@ -41,7 +_,7 @@
      @Nullable
      private static LoadingCache<String, CompletableFuture<Optional<GameProfile>>> profileCacheByName;
      @Nullable
@@ -9,45 +9,45 @@
      public static final Executor CHECKED_MAIN_THREAD_EXECUTOR = runnable -> {
          Executor executor = mainThreadExecutor;
          if (executor != null) {
-@@ -76,9 +76,9 @@
+@@ -76,9 +_,9 @@
          profileCacheById = CacheBuilder.newBuilder()
              .expireAfterAccess(Duration.ofMinutes(10L))
              .maximumSize(256L)
 -            .build(new CacheLoader<UUID, CompletableFuture<Optional<GameProfile>>>() {
 +            .build(new CacheLoader<>() { // Paper - player profile events
                  @Override
--                public CompletableFuture<Optional<GameProfile>> load(UUID uUID) {
-+                public CompletableFuture<Optional<GameProfile>> load(com.mojang.datafixers.util.Pair<java.util.UUID, @org.jetbrains.annotations.Nullable GameProfile> uUID) { // Paper - player profile events
-                     return SkullBlockEntity.fetchProfileById(uUID, apiServices, booleanSupplier);
+-                public CompletableFuture<Optional<GameProfile>> load(UUID id) {
++                public CompletableFuture<Optional<GameProfile>> load(com.mojang.datafixers.util.Pair<java.util.UUID, @org.jetbrains.annotations.Nullable GameProfile> id) { // Paper - player profile events
+                     return SkullBlockEntity.fetchProfileById(id, services, booleanSupplier);
                  }
              });
-@@ -89,23 +89,29 @@
+@@ -89,23 +_,29 @@
              .getAsync(name)
              .thenCompose(
                  optional -> {
 -                    LoadingCache<UUID, CompletableFuture<Optional<GameProfile>>> loadingCache = profileCacheById;
 +                    LoadingCache<com.mojang.datafixers.util.Pair<java.util.UUID, @org.jetbrains.annotations.Nullable GameProfile>, CompletableFuture<Optional<GameProfile>>> loadingCache = profileCacheById; // Paper - player profile events
                      return loadingCache != null && !optional.isEmpty()
--                        ? loadingCache.getUnchecked(optional.get().getId()).thenApply(optional2 -> optional2.or(() -> optional))
-+                        ? loadingCache.getUnchecked(new com.mojang.datafixers.util.Pair<>(optional.get().getId(), optional.get())).thenApply(optional2 -> optional2.or(() -> optional)) // Paper - player profile events
+-                        ? loadingCache.getUnchecked(optional.get().getId()).thenApply(optional1 -> optional1.or(() -> optional))
++                        ? loadingCache.getUnchecked(new com.mojang.datafixers.util.Pair<>(optional.get().getId(), optional.get())).thenApply(optional1 -> optional1.or(() -> optional)) // Paper - player profile events
                          : CompletableFuture.completedFuture(Optional.empty());
                  }
              );
      }
  
--    static CompletableFuture<Optional<GameProfile>> fetchProfileById(UUID uuid, Services apiServices, BooleanSupplier booleanSupplier) {
-+    static CompletableFuture<Optional<GameProfile>> fetchProfileById(com.mojang.datafixers.util.Pair<java.util.UUID, @org.jetbrains.annotations.Nullable GameProfile> pair, Services apiServices, BooleanSupplier booleanSupplier) { // Paper
+-    static CompletableFuture<Optional<GameProfile>> fetchProfileById(UUID id, Services services, BooleanSupplier cacheUninitialized) {
++    static CompletableFuture<Optional<GameProfile>> fetchProfileById(com.mojang.datafixers.util.Pair<java.util.UUID, @org.jetbrains.annotations.Nullable GameProfile> id, Services services, BooleanSupplier cacheUninitialized) { // Paper
          return CompletableFuture.supplyAsync(() -> {
-             if (booleanSupplier.getAsBoolean()) {
+             if (cacheUninitialized.getAsBoolean()) {
                  return Optional.empty();
              } else {
--                ProfileResult profileResult = apiServices.sessionService().fetchProfile(uuid, true);
+-                ProfileResult profileResult = services.sessionService().fetchProfile(id, true);
 +                // Paper start - fill player profile events
-+                if (apiServices.sessionService() instanceof com.destroystokyo.paper.profile.PaperMinecraftSessionService paperService) {
-+                    final GameProfile profile = pair.getSecond() != null ? pair.getSecond() : new com.mojang.authlib.GameProfile(pair.getFirst(), "");
++                if (services.sessionService() instanceof com.destroystokyo.paper.profile.PaperMinecraftSessionService paperService) {
++                    final GameProfile profile = id.getSecond() != null ? id.getSecond() : new com.mojang.authlib.GameProfile(id.getFirst(), "");
 +                    return Optional.ofNullable(paperService.fetchProfile(profile, true)).map(ProfileResult::profile);
 +                }
-+                ProfileResult profileResult = apiServices.sessionService().fetchProfile(pair.getFirst(), true);
++                ProfileResult profileResult = services.sessionService().fetchProfile(id.getFirst(), true);
 +                // Paper end - fill player profile events
                  return Optional.ofNullable(profileResult).map(ProfileResult::profile);
              }
@@ -56,13 +56,13 @@
      }
  
      public static void clear() {
-@@ -210,9 +216,11 @@
+@@ -210,9 +_,11 @@
              : CompletableFuture.completedFuture(Optional.empty());
      }
  
--    public static CompletableFuture<Optional<GameProfile>> fetchGameProfile(UUID uuid) {
+-    public static CompletableFuture<Optional<GameProfile>> fetchGameProfile(UUID profileUuid) {
 -        LoadingCache<UUID, CompletableFuture<Optional<GameProfile>>> loadingCache = profileCacheById;
--        return loadingCache != null ? loadingCache.getUnchecked(uuid) : CompletableFuture.completedFuture(Optional.empty());
+-        return loadingCache != null ? loadingCache.getUnchecked(profileUuid) : CompletableFuture.completedFuture(Optional.empty());
 +    // Paper start - player profile events
 +    public static CompletableFuture<Optional<GameProfile>> fetchGameProfile(UUID uuid, @Nullable String name) {
 +        LoadingCache<com.mojang.datafixers.util.Pair<java.util.UUID,  @org.jetbrains.annotations.Nullable GameProfile>, CompletableFuture<Optional<GameProfile>>> loadingCache = profileCacheById;
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java.patch
new file mode 100644
index 0000000000..09f5bd3726
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java
+@@ -131,7 +_,7 @@
+ 
+     @Nullable
+     public Vec3 getPortalPosition(ServerLevel level, BlockPos pos) {
+-        if (this.exitPortal == null && level.dimension() == Level.END) {
++        if (this.exitPortal == null && level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.END) { // CraftBukkit - work in alternate worlds
+             BlockPos blockPos = findOrCreateValidTeleportPos(level, pos);
+             blockPos = blockPos.above(10);
+             LOGGER.debug("Creating portal at {}", blockPos);
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/trialspawner/TrialSpawner.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/trialspawner/TrialSpawner.java.patch
new file mode 100644
index 0000000000..028fc739b8
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/trialspawner/TrialSpawner.java.patch
@@ -0,0 +1,47 @@
+--- a/net/minecraft/world/level/block/entity/trialspawner/TrialSpawner.java
++++ b/net/minecraft/world/level/block/entity/trialspawner/TrialSpawner.java
+@@ -237,7 +_,14 @@
+                                 nextSpawnData.getEquipment().ifPresent(mob::equip);
+                             }
+ 
+-                            if (!level.tryAddFreshEntityWithPassengers(entity)) {
++                            entity.spawnedViaMobSpawner = true; // Paper
++                            entity.spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.TRIAL_SPAWNER; // Paper - Entity#getEntitySpawnReason
++                            // CraftBukkit start
++                            if (org.bukkit.craftbukkit.event.CraftEventFactory.callTrialSpawnerSpawnEvent(entity, pos).isCancelled()) {
++                                return Optional.empty();
++                            }
++                            if (!level.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.TRIAL_SPAWNER)) {
++                                // CraftBukkit end
+                                 return Optional.empty();
+                             } else {
+                                 TrialSpawner.FlameParticle flameParticle = this.isOminous
+@@ -260,6 +_,19 @@
+         LootParams lootParams = new LootParams.Builder(level).create(LootContextParamSets.EMPTY);
+         ObjectArrayList<ItemStack> randomItems = lootTable1.getRandomItems(lootParams);
+         if (!randomItems.isEmpty()) {
++            // CraftBukkit start
++            org.bukkit.event.block.BlockDispenseLootEvent spawnerDispenseLootEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callBlockDispenseLootEvent(
++                level,
++                pos,
++                null,
++                randomItems
++            );
++            if (spawnerDispenseLootEvent.isCancelled()) {
++                return;
++            }
++
++            randomItems = new ObjectArrayList<>(spawnerDispenseLootEvent.getDispensedLoot().stream().map(org.bukkit.craftbukkit.inventory.CraftItemStack::asNMSCopy).toList());
++            // CraftBukkit end
+             for (ItemStack itemStack : randomItems) {
+                 DefaultDispenseItemBehavior.spawnItem(level, itemStack, 2, Direction.UP, Vec3.atBottomCenterOf(pos).relative(Direction.UP, 1.2));
+             }
+@@ -362,7 +_,7 @@
+     }
+ 
+     public void overrideEntityToSpawn(EntityType<?> entityType, Level level) {
+-        this.data.reset();
++        this.data.reset(this); // Paper - Fix TrialSpawner forgets assigned mob; MC-273635
+         this.normalConfig = Holder.direct(this.normalConfig.value().withSpawning(entityType));
+         this.ominousConfig = Holder.direct(this.ominousConfig.value().withSpawning(entityType));
+         this.setState(level, TrialSpawnerState.INACTIVE);
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerData.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerData.java.patch
new file mode 100644
index 0000000000..351f01f843
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerData.java.patch
@@ -0,0 +1,23 @@
+--- a/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerData.java
++++ b/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerData.java
+@@ -101,9 +_,9 @@
+         this.ejectingLootTable = ejectingLootTable;
+     }
+ 
+-    public void reset() {
++    public void reset(TrialSpawner spawner) { // Paper - Fix TrialSpawner forgets assigned mob; MC-273635
+         this.currentMobs.clear();
+-        this.nextSpawnData = Optional.empty();
++        if (!spawner.getConfig().spawnPotentialsDefinition().isEmpty()) this.nextSpawnData = Optional.empty(); // Paper - Fix TrialSpawner forgets assigned mob; MC-273635
+         this.resetStatistics();
+     }
+ 
+@@ -206,7 +_,7 @@
+                     mob.dropPreservedEquipment(level);
+                 }
+ 
+-                entity.remove(Entity.RemovalReason.DISCARDED);
++                entity.remove(Entity.RemovalReason.DISCARDED, org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - Add bukkit remove cause;
+             }
+         });
+         if (!spawner.getOminousConfig().spawnPotentialsDefinition().isEmpty()) {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerState.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerState.java.patch
new file mode 100644
index 0000000000..28b53c34d3
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerState.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerState.java
++++ b/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerState.java
+@@ -145,7 +_,7 @@
+                     yield ACTIVE;
+                 } else if (data.isCooldownFinished(level)) {
+                     spawner.removeOminous(level, pos);
+-                    data.reset();
++                    data.reset(spawner); // Paper - Fix TrialSpawner forgets assigned mob; MC-273635
+                     yield WAITING_FOR_PLAYERS;
+                 } else {
+                     yield this;
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/vault/VaultBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/vault/VaultBlockEntity.java.patch
new file mode 100644
index 0000000000..055a081be5
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/vault/VaultBlockEntity.java.patch
@@ -0,0 +1,26 @@
+--- a/net/minecraft/world/level/block/entity/vault/VaultBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/vault/VaultBlockEntity.java
+@@ -272,6 +_,11 @@
+                     if (!list.isEmpty()) {
+                         player.awardStat(Stats.ITEM_USED.get(stack.getItem()));
+                         stack.consume(config.keyItem().getCount(), player);
++                        // CraftBukkit start
++                        org.bukkit.event.block.BlockDispenseLootEvent vaultDispenseLootEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callBlockDispenseLootEvent(level, pos, player, list);
++                        if (vaultDispenseLootEvent.isCancelled()) return;
++                        list = vaultDispenseLootEvent.getDispensedLoot().stream().map(org.bukkit.craftbukkit.inventory.CraftItemStack::asNMSCopy).toList();
++                        // CraftBukkit end
+                         unlock(level, state, pos, config, serverData, sharedData, list);
+                         serverData.addToRewardedPlayers(player);
+                         sharedData.updateConnectedPlayersWithinRange(level, pos, serverData, config, config.deactivationRange());
+@@ -294,6 +_,11 @@
+                 ItemStack randomDisplayItemFromLootTable = getRandomDisplayItemFromLootTable(
+                     level, pos, config.overrideLootTableToDisplay().orElse(config.lootTable())
+                 );
++                // CraftBukkit start
++                org.bukkit.event.block.VaultDisplayItemEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callVaultDisplayItemEvent(level, pos, randomDisplayItemFromLootTable);
++                if (event.isCancelled()) return;
++                randomDisplayItemFromLootTable = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getDisplayItem());
++                // CraftBukkit end
+                 sharedData.setDisplayItem(randomDisplayItemFromLootTable);
+             }
+         }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/grower/TreeGrower.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/grower/TreeGrower.java.patch
new file mode 100644
index 0000000000..c4316b4d6e
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/grower/TreeGrower.java.patch
@@ -0,0 +1,78 @@
+--- a/net/minecraft/world/level/block/grower/TreeGrower.java
++++ b/net/minecraft/world/level/block/grower/TreeGrower.java
+@@ -132,6 +_,7 @@
+                 .get(configuredMegaFeature)
+                 .orElse(null);
+             if (holder != null) {
++                this.setTreeType(holder); // CraftBukkit
+                 for (int i = 0; i >= -1; i--) {
+                     for (int i1 = 0; i1 >= -1; i1--) {
+                         if (isTwoByTwoSapling(state, level, pos, i, i1)) {
+@@ -164,6 +_,7 @@
+             if (holder1 == null) {
+                 return false;
+             } else {
++                this.setTreeType(holder1); // CraftBukkit
+                 ConfiguredFeature<?, ?> configuredFeature2 = holder1.value();
+                 BlockState blockState1 = level.getFluidState(pos).createLegacyBlock();
+                 level.setBlock(pos, blockState1, 4);
+@@ -198,4 +_,59 @@
+ 
+         return false;
+     }
++
++    // CraftBukkit start
++    private void setTreeType(Holder<ConfiguredFeature<?, ?>> holder) {
++        ResourceKey<ConfiguredFeature<?, ?>> treeFeature = holder.unwrapKey().get();
++        if (treeFeature == TreeFeatures.OAK || treeFeature == TreeFeatures.OAK_BEES_005) {
++            net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TREE;
++        } else if (treeFeature == TreeFeatures.HUGE_RED_MUSHROOM) {
++            net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.RED_MUSHROOM;
++        } else if (treeFeature == TreeFeatures.HUGE_BROWN_MUSHROOM) {
++            net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.BROWN_MUSHROOM;
++        } else if (treeFeature == TreeFeatures.JUNGLE_TREE) {
++            net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.COCOA_TREE;
++        } else if (treeFeature == TreeFeatures.JUNGLE_TREE_NO_VINE) {
++            net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.SMALL_JUNGLE;
++        } else if (treeFeature == TreeFeatures.PINE) {
++            net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TALL_REDWOOD;
++        } else if (treeFeature == TreeFeatures.SPRUCE) {
++            net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.REDWOOD;
++        } else if (treeFeature == TreeFeatures.ACACIA) {
++            net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.ACACIA;
++        } else if (treeFeature == TreeFeatures.BIRCH || treeFeature == TreeFeatures.BIRCH_BEES_005) {
++            net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.BIRCH;
++        } else if (treeFeature == TreeFeatures.SUPER_BIRCH_BEES_0002) {
++            net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TALL_BIRCH;
++        } else if (treeFeature == TreeFeatures.SWAMP_OAK) {
++            net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.SWAMP;
++        } else if (treeFeature == TreeFeatures.FANCY_OAK || treeFeature == TreeFeatures.FANCY_OAK_BEES_005) {
++            net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.BIG_TREE;
++        } else if (treeFeature == TreeFeatures.JUNGLE_BUSH) {
++            net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.JUNGLE_BUSH;
++        } else if (treeFeature == TreeFeatures.DARK_OAK) {
++            net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.DARK_OAK;
++        } else if (treeFeature == TreeFeatures.MEGA_SPRUCE) {
++            net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MEGA_REDWOOD;
++        } else if (treeFeature == TreeFeatures.MEGA_PINE) {
++            net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MEGA_PINE;
++        } else if (treeFeature == TreeFeatures.MEGA_JUNGLE_TREE) {
++            net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.JUNGLE;
++        } else if (treeFeature == TreeFeatures.AZALEA_TREE) {
++            net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.AZALEA;
++        } else if (treeFeature == TreeFeatures.MANGROVE) {
++            net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MANGROVE;
++        } else if (treeFeature == TreeFeatures.TALL_MANGROVE) {
++            net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TALL_MANGROVE;
++        } else if (treeFeature == TreeFeatures.CHERRY || treeFeature == TreeFeatures.CHERRY_BEES_005) {
++            net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.CHERRY;
++        } else if (treeFeature == TreeFeatures.PALE_OAK || treeFeature == TreeFeatures.PALE_OAK_BONEMEAL) {
++            net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.PALE_OAK;
++        } else if (treeFeature == TreeFeatures.PALE_OAK_CREAKING) {
++            net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.PALE_OAK_CREAKING;
++        } else {
++            throw new IllegalArgumentException("Unknown tree generator " + treeFeature);
++        }
++    }
++    // CraftBukkit end
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/piston/PistonBaseBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/piston/PistonBaseBlock.java.patch
new file mode 100644
index 0000000000..9ee94df160
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/piston/PistonBaseBlock.java.patch
@@ -0,0 +1,168 @@
+--- a/net/minecraft/world/level/block/piston/PistonBaseBlock.java
++++ b/net/minecraft/world/level/block/piston/PistonBaseBlock.java
+@@ -145,6 +_,18 @@
+                 i = 2;
+             }
+ 
++            // CraftBukkit start
++            // if (!this.isSticky) { // Paper - Fix sticky pistons and BlockPistonRetractEvent; Move further down
++            //     org.bukkit.block.Block block = world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ());
++            //     BlockPistonRetractEvent event = new BlockPistonRetractEvent(block, ImmutableList.<org.bukkit.block.Block>of(), CraftBlock.notchToBlockFace(enumdirection));
++            //     world.getCraftServer().getPluginManager().callEvent(event);
++            //
++            //     if (event.isCancelled()) {
++            //         return;
++            //     }
++            // }
++            // PAIL: checkME - what happened to setTypeAndData?
++            // CraftBukkit end
+             level.blockEvent(pos, this, i, direction.get3DDataValue());
+         }
+     }
+@@ -174,6 +_,12 @@
+     @Override
+     protected boolean triggerEvent(BlockState state, Level level, BlockPos pos, int id, int param) {
+         Direction direction = state.getValue(FACING);
++        // Paper start - Protect Bedrock and End Portal/Frames from being destroyed; prevent retracting when we're facing the wrong way (we were replaced before retraction could occur)
++        Direction directionQueuedAs = Direction.from3DDataValue(param & 7); // Paper - copied from below
++        if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits && direction != directionQueuedAs) {
++            return false;
++        }
++        // Paper end - Protect Bedrock and End Portal/Frames from being destroyed
+         BlockState blockState = state.setValue(EXTENDED, Boolean.valueOf(true));
+         if (!level.isClientSide) {
+             boolean neighborSignal = this.getNeighborSignal(level, pos, direction);
+@@ -205,10 +_,17 @@
+                 .defaultBlockState()
+                 .setValue(MovingPistonBlock.FACING, direction)
+                 .setValue(MovingPistonBlock.TYPE, this.isSticky ? PistonType.STICKY : PistonType.DEFAULT);
++            // Paper start - Fix sticky pistons and BlockPistonRetractEvent; Move empty piston retract call to fix multiple event fires
++            if (!this.isSticky) {
++                if (!new org.bukkit.event.block.BlockPistonRetractEvent(org.bukkit.craftbukkit.block.CraftBlock.at(level, pos), java.util.Collections.emptyList(), org.bukkit.craftbukkit.block.CraftBlock.notchToBlockFace(direction)).callEvent()) {
++                    return false;
++                }
++            }
++            // Paper end - Fix sticky pistons and BlockPistonRetractEvent
+             level.setBlock(pos, blockState1, 20);
+             level.setBlockEntity(
+                 MovingPistonBlock.newMovingBlockEntity(
+-                    pos, blockState1, this.defaultBlockState().setValue(FACING, Direction.from3DDataValue(param & 7)), direction, false, true
++                    pos, blockState1, this.defaultBlockState().setValue(FACING, Direction.from3DDataValue(param & 7)), direction, false, true // Paper - Protect Bedrock and End Portal/Frames from being destroyed; diff on change
+                 )
+             );
+             level.blockUpdated(pos, blockState1.getBlock());
+@@ -232,13 +_,27 @@
+                         || blockState2.getPistonPushReaction() != PushReaction.NORMAL
+                             && !blockState2.is(Blocks.PISTON)
+                             && !blockState2.is(Blocks.STICKY_PISTON)) {
++                        // Paper start - Fix sticky pistons and BlockPistonRetractEvent; fire BlockPistonRetractEvent for sticky pistons retracting nothing (air)
++                        if (id == TRIGGER_CONTRACT && blockState1.isAir()) {
++                            if (!new org.bukkit.event.block.BlockPistonRetractEvent(org.bukkit.craftbukkit.block.CraftBlock.at(level, pos), java.util.Collections.emptyList(), org.bukkit.craftbukkit.block.CraftBlock.notchToBlockFace(direction)).callEvent()) {
++                                return false;
++                            }
++                        }
++                        // Paper end - Fix sticky pistons and BlockPistonRetractEvent
+                         level.removeBlock(pos.relative(direction), false);
+                     } else {
+                         this.moveBlocks(level, pos, direction, false);
+                     }
+                 }
+             } else {
+-                level.removeBlock(pos.relative(direction), false);
++                // Paper start - Protect Bedrock and End Portal/Frames from being destroyed; fix headless pistons breaking blocks
++                BlockPos headPos = pos.relative(direction);
++                if (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits || level.getBlockState(headPos) == Blocks.PISTON_HEAD.defaultBlockState().setValue(FACING, direction)) { // double check to make sure we're not a headless piston.
++                    level.removeBlock(headPos, false);
++                } else {
++                    ((ServerLevel) level).getChunkSource().blockChanged(headPos); // ... fix client desync
++                }
++                // Paper end - Protect Bedrock and End Portal/Frames from being destroyed
+             }
+ 
+             level.playSound(null, pos, SoundEvents.PISTON_CONTRACT, SoundSource.BLOCKS, 0.5F, level.random.nextFloat() * 0.15F + 0.6F);
+@@ -305,12 +_,54 @@
+             BlockState[] blockStates = new BlockState[toPush.size() + toDestroy.size()];
+             Direction direction = extending ? facing : facing.getOpposite();
+             int i = 0;
++            // CraftBukkit start
++            final org.bukkit.block.Block bblock = level.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ());
++
++            final List<BlockPos> moved = pistonStructureResolver.getToPush();
++            final List<BlockPos> broken = pistonStructureResolver.getToDestroy();
++
++            List<org.bukkit.block.Block> blocks = new java.util.AbstractList<>() {
++
++                @Override
++                public int size() {
++                    return moved.size() + broken.size();
++                }
++
++                @Override
++                public org.bukkit.block.Block get(int index) {
++                    if (index >= this.size() || index < 0) {
++                        throw new ArrayIndexOutOfBoundsException(index);
++                    }
++                    BlockPos pos = index < moved.size() ? moved.get(index) : broken.get(index - moved.size());
++                    return bblock.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ());
++                }
++            };
++            org.bukkit.event.block.BlockPistonEvent event;
++            if (extending) {
++                event = new org.bukkit.event.block.BlockPistonExtendEvent(bblock, blocks, org.bukkit.craftbukkit.block.CraftBlock.notchToBlockFace(direction));
++            } else {
++                event = new org.bukkit.event.block.BlockPistonRetractEvent(bblock, blocks, org.bukkit.craftbukkit.block.CraftBlock.notchToBlockFace(direction));
++            }
++            level.getCraftServer().getPluginManager().callEvent(event);
++
++            if (event.isCancelled()) {
++                for (BlockPos b : broken) {
++                    level.sendBlockUpdated(b, Blocks.AIR.defaultBlockState(), level.getBlockState(b), 3);
++                }
++                for (BlockPos b : moved) {
++                    level.sendBlockUpdated(b, Blocks.AIR.defaultBlockState(), level.getBlockState(b), 3);
++                    b = b.relative(direction);
++                    level.sendBlockUpdated(b, Blocks.AIR.defaultBlockState(), level.getBlockState(b), 3);
++                }
++                return false;
++            }
++            // CraftBukkit end
+ 
+             for (int i1 = toDestroy.size() - 1; i1 >= 0; i1--) {
+                 BlockPos blockPos2 = toDestroy.get(i1);
+                 BlockState blockState1 = level.getBlockState(blockPos2);
+                 BlockEntity blockEntity = blockState1.hasBlockEntity() ? level.getBlockEntity(blockPos2) : null;
+-                dropResources(blockState1, level, blockPos2, blockEntity);
++                dropResources(blockState1, level, blockPos2, blockEntity, pos); // Paper - Add BlockBreakBlockEvent
+                 level.setBlock(blockPos2, Blocks.AIR.defaultBlockState(), 18);
+                 level.gameEvent(GameEvent.BLOCK_DESTROY, blockPos2, GameEvent.Context.of(blockState1));
+                 if (!blockState1.is(BlockTags.FIRE)) {
+@@ -321,13 +_,26 @@
+             }
+ 
+             for (int i1 = toPush.size() - 1; i1 >= 0; i1--) {
+-                BlockPos blockPos2 = toPush.get(i1);
+-                BlockState blockState1 = level.getBlockState(blockPos2);
++                // Paper start - fix a variety of piston desync dupes
++                boolean allowDesync = io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPistonDuplication;
++                BlockPos blockPos2;
++                BlockPos oldPos = blockPos2 = toPush.get(i1);
++                BlockState blockState1 = allowDesync ? level.getBlockState(oldPos) : null;
++                // Paper end - fix a variety of piston desync dupes
+                 blockPos2 = blockPos2.relative(direction);
+                 map.remove(blockPos2);
+                 BlockState blockState2 = Blocks.MOVING_PISTON.defaultBlockState().setValue(FACING, facing);
+                 level.setBlock(blockPos2, blockState2, 68);
+-                level.setBlockEntity(MovingPistonBlock.newMovingBlockEntity(blockPos2, blockState2, list.get(i1), facing, extending, false));
++                // Paper start - fix a variety of piston desync dupes
++                if (!allowDesync) {
++                    blockState1 = level.getBlockState(oldPos);
++                    map.replace(oldPos, blockState1);
++                }
++                level.setBlockEntity(MovingPistonBlock.newMovingBlockEntity(blockPos2, blockState2, allowDesync ? list.get(i1) : blockState1, facing, extending, false));
++                if (!allowDesync) {
++                    level.setBlock(oldPos, Blocks.AIR.defaultBlockState(), Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_MOVE_BY_PISTON | 1024); // set air to prevent later physics updates from seeing this block
++                }
++                // Paper end - fix a variety of piston desync dupes
+                 blockStates[i++] = blockState1;
+             }
+ 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java.patch
similarity index 73%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java.patch
index 03d9e64403..2774d551a4 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java
 +++ b/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java
-@@ -306,7 +306,7 @@
-                 if (world.getBlockState(pos).is(Blocks.MOVING_PISTON)) {
-                     BlockState blockState = Block.updateFromNeighbourShapes(blockEntity.movedState, world, pos);
+@@ -299,7 +_,7 @@
+                 if (level.getBlockState(pos).is(Blocks.MOVING_PISTON)) {
+                     BlockState blockState = Block.updateFromNeighbourShapes(blockEntity.movedState, level, pos);
                      if (blockState.isAir()) {
--                        world.setBlock(pos, blockEntity.movedState, 84);
-+                        world.setBlock(pos, blockEntity.movedState, io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPistonDuplication ? 84 : (84 | Block.UPDATE_CLIENTS)); // Paper - fix a variety of piston desync dupes; force notify (flag 2), it's possible the set type by the piston block (which doesn't notify) set this block to air
-                         Block.updateOrDestroy(blockEntity.movedState, blockState, world, pos, 3);
+-                        level.setBlock(pos, blockEntity.movedState, 84);
++                        level.setBlock(pos, blockEntity.movedState, io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPistonDuplication ? 84 : (84 | Block.UPDATE_CLIENTS)); // Paper - fix a variety of piston desync dupes; force notify (flag 2), it's possible the set type by the piston block (which doesn't notify) set this block to air
+                         Block.updateOrDestroy(blockEntity.movedState, blockState, level, pos, 3);
                      } else {
                          if (blockState.hasProperty(BlockStateProperties.WATERLOGGED) && blockState.getValue(BlockStateProperties.WATERLOGGED)) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/state/BlockBehaviour.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/state/BlockBehaviour.java.patch
similarity index 50%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/state/BlockBehaviour.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/state/BlockBehaviour.java.patch
index 56a6b42a76..ee4aa83de5 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/state/BlockBehaviour.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/state/BlockBehaviour.java.patch
@@ -1,118 +1,99 @@
 --- a/net/minecraft/world/level/block/state/BlockBehaviour.java
 +++ b/net/minecraft/world/level/block/state/BlockBehaviour.java
-@@ -46,6 +46,7 @@
- import net.minecraft.world.item.Item;
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.item.context.BlockPlaceContext;
-+import net.minecraft.world.item.context.UseOnContext;
- import net.minecraft.world.level.BlockGetter;
- import net.minecraft.world.level.EmptyBlockGetter;
- import net.minecraft.world.level.Explosion;
-@@ -83,6 +84,8 @@
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.Shapes;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+import net.minecraft.world.level.ServerExplosion;
-+// CraftBukkit end
+@@ -167,16 +_,24 @@
+     }
  
- public abstract class BlockBehaviour implements FeatureElement {
- 
-@@ -156,9 +159,18 @@
- 
-     protected void neighborChanged(BlockState state, Level world, BlockPos pos, Block sourceBlock, @Nullable Orientation wireOrientation, boolean notify) {}
- 
--    protected void onPlace(BlockState state, Level world, BlockPos pos, BlockState oldState, boolean notify) {}
-+    protected void onPlace(BlockState state, Level world, BlockPos pos, BlockState oldState, boolean notify) {
+     protected void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean movedByPiston) {
+-    }
 +        org.spigotmc.AsyncCatcher.catchOp("block onPlace"); // Spigot
 +    }
- 
++
 +    // CraftBukkit start
-+    protected void onPlace(BlockState iblockdata, Level world, BlockPos blockposition, BlockState iblockdata1, boolean flag, @Nullable UseOnContext context) {
++    protected void onPlace(BlockState iblockdata, Level world, BlockPos blockposition, BlockState iblockdata1, boolean flag, @Nullable net.minecraft.world.item.context.UseOnContext context) {
 +        this.onPlace(iblockdata, world, blockposition, iblockdata1, flag);
 +    }
 +    // CraftBukkit end
-+
-     protected void onRemove(BlockState state, Level world, BlockPos pos, BlockState newState, boolean moved) {
+ 
+     protected void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean movedByPiston) {
 +        org.spigotmc.AsyncCatcher.catchOp("block remove"); // Spigot
          if (state.hasBlockEntity() && !state.is(newState.getBlock())) {
-             world.removeBlockEntity(pos);
+             level.removeBlockEntity(pos);
          }
-@@ -166,7 +178,7 @@
      }
  
-     protected void onExplosionHit(BlockState state, ServerLevel world, BlockPos pos, Explosion explosion, BiConsumer<ItemStack, BlockPos> stackMerger) {
+     protected void onExplosionHit(BlockState state, ServerLevel level, BlockPos pos, Explosion explosion, BiConsumer<ItemStack, BlockPos> dropConsumer) {
 -        if (!state.isAir() && explosion.getBlockInteraction() != Explosion.BlockInteraction.TRIGGER_BLOCK) {
 +        if (!state.isAir() && explosion.getBlockInteraction() != Explosion.BlockInteraction.TRIGGER_BLOCK && state.isDestroyable()) { // Paper - Protect Bedrock and End Portal/Frames from being destroyed
              Block block = state.getBlock();
              boolean flag = explosion.getIndirectSourceEntity() instanceof Player;
- 
-@@ -174,8 +186,10 @@
-                 BlockEntity tileentity = state.hasBlockEntity() ? world.getBlockEntity(pos) : null;
-                 LootParams.Builder lootparams_a = (new LootParams.Builder(world)).withParameter(LootContextParams.ORIGIN, Vec3.atCenterOf(pos)).withParameter(LootContextParams.TOOL, ItemStack.EMPTY).withOptionalParameter(LootContextParams.BLOCK_ENTITY, tileentity).withOptionalParameter(LootContextParams.THIS_ENTITY, explosion.getDirectSourceEntity());
- 
+             if (block.dropFromExplosion(explosion)) {
+@@ -186,8 +_,10 @@
+                     .withParameter(LootContextParams.TOOL, ItemStack.EMPTY)
+                     .withOptionalParameter(LootContextParams.BLOCK_ENTITY, blockEntity)
+                     .withOptionalParameter(LootContextParams.THIS_ENTITY, explosion.getDirectSourceEntity());
 -                if (explosion.getBlockInteraction() == Explosion.BlockInteraction.DESTROY_WITH_DECAY) {
--                    lootparams_a.withParameter(LootContextParams.EXPLOSION_RADIUS, explosion.radius());
+-                    builder.withParameter(LootContextParams.EXPLOSION_RADIUS, explosion.radius());
 +                // CraftBukkit start - add yield
-+                if (explosion instanceof ServerExplosion serverExplosion && serverExplosion.yield < 1.0F) {
-+                    lootparams_a.withParameter(LootContextParams.EXPLOSION_RADIUS, 1.0F / serverExplosion.yield);
++                if (explosion instanceof net.minecraft.world.level.ServerExplosion serverExplosion && serverExplosion.yield < 1.0F) {
++                    builder.withParameter(LootContextParams.EXPLOSION_RADIUS, 1.0F / serverExplosion.yield);
 +                    // CraftBukkit end
                  }
  
-                 state.spawnAfterBreak(world, pos, ItemStack.EMPTY, flag);
-@@ -243,7 +257,7 @@
+                 state.spawnAfterBreak(level, pos, ItemStack.EMPTY, flag);
+@@ -255,7 +_,7 @@
      }
  
-     protected boolean canBeReplaced(BlockState state, BlockPlaceContext context) {
--        return state.canBeReplaced() && (context.getItemInHand().isEmpty() || !context.getItemInHand().is(this.asItem()));
-+        return state.canBeReplaced() && (context.getItemInHand().isEmpty() || !context.getItemInHand().is(this.asItem())) && (state.isDestroyable() || (context.getPlayer() != null && context.getPlayer().getAbilities().instabuild)); // Paper - Protect Bedrock and End Portal/Frames from being destroyed
+     protected boolean canBeReplaced(BlockState state, BlockPlaceContext useContext) {
+-        return state.canBeReplaced() && (useContext.getItemInHand().isEmpty() || !useContext.getItemInHand().is(this.asItem()));
++        return state.canBeReplaced() && (useContext.getItemInHand().isEmpty() || !useContext.getItemInHand().is(this.asItem())) && (state.isDestroyable() || (useContext.getPlayer() != null && useContext.getPlayer().getAbilities().instabuild)); // Paper - Protect Bedrock and End Portal/Frames from being destroyed
      }
  
      protected boolean canBeReplaced(BlockState state, Fluid fluid) {
-@@ -851,7 +865,15 @@
-             this.spawnTerrainParticles = blockbase_info.spawnTerrainParticles;
-             this.instrument = blockbase_info.instrument;
-             this.replaceable = blockbase_info.replaceable;
-+        }
+@@ -468,6 +_,16 @@
+             this.instrument = properties.instrument;
+             this.replaceable = properties.replaceable;
+         }
 +        // Paper start - Perf: impl cached craft block data, lazy load to fix issue with loading at the wrong time
++        @Nullable
 +        private org.bukkit.craftbukkit.block.data.CraftBlockData cachedCraftBlockData;
 +
 +        public org.bukkit.craftbukkit.block.data.CraftBlockData createCraftBlockData() {
-+            if (cachedCraftBlockData == null) cachedCraftBlockData = org.bukkit.craftbukkit.block.data.CraftBlockData.createData(asState());
-+            return (org.bukkit.craftbukkit.block.data.CraftBlockData) cachedCraftBlockData.clone();
-         }
++            if (this.cachedCraftBlockData == null) this.cachedCraftBlockData = org.bukkit.craftbukkit.block.data.CraftBlockData.createData(this.asState());
++            return (org.bukkit.craftbukkit.block.data.CraftBlockData) this.cachedCraftBlockData.clone();
++        }
 +        // Paper end - Perf: impl cached craft block data, lazy load to fix issue with loading at the wrong time
++
  
          private boolean calculateSolid() {
-             if (((Block) this.owner).properties.forceSolidOn) {
-@@ -873,12 +895,14 @@
+             if (this.owner.properties.forceSolidOn) {
+@@ -487,12 +_,14 @@
              }
          }
  
 +        protected boolean shapeExceedsCube = true; // Paper - moved from actual method to here
          public void initCache() {
-             this.fluidState = ((Block) this.owner).getFluidState(this.asState());
-             this.isRandomlyTicking = ((Block) this.owner).isRandomlyTicking(this.asState());
+             this.fluidState = this.owner.getFluidState(this.asState());
+             this.isRandomlyTicking = this.owner.isRandomlyTicking(this.asState());
              if (!this.getBlock().hasDynamicShape()) {
                  this.cache = new BlockBehaviour.BlockStateBase.Cache(this.asState());
              }
 +            this.shapeExceedsCube = this.cache == null || this.cache.largeCollisionShape; // Paper - moved from actual method to here
  
              this.legacySolid = this.calculateSolid();
-             this.occlusionShape = this.canOcclude ? ((Block) this.owner).getOcclusionShape(this.asState()) : Shapes.empty();
-@@ -925,6 +949,12 @@
+             this.occlusionShape = this.canOcclude ? this.owner.getOcclusionShape(this.asState()) : Shapes.empty();
+@@ -531,6 +_,11 @@
+         public boolean isSolid() {
              return this.legacySolid;
          }
- 
 +        // Paper start - Protect Bedrock and End Portal/Frames from being destroyed
 +        public final boolean isDestroyable() {
 +            return getBlock().isDestroyable();
 +        }
 +        // Paper end - Protect Bedrock and End Portal/Frames from being destroyed
-+
-         public boolean isValidSpawn(BlockGetter world, BlockPos pos, EntityType<?> type) {
-             return this.getBlock().properties.isValidSpawn.test(this.asState(), world, pos, type);
-         }
-@@ -945,19 +975,19 @@
+ 
+         public boolean isValidSpawn(BlockGetter level, BlockPos pos, EntityType<?> entityType) {
+             return this.getBlock().properties.isValidSpawn.test(this.asState(), level, pos, entityType);
+@@ -552,19 +_,19 @@
              return this.occlusionShape;
          }
  
@@ -137,7 +118,7 @@
              return this.isAir;
          }
  
-@@ -1028,14 +1058,14 @@
+@@ -634,14 +_,14 @@
          }
  
          public PushReaction getPistonPushReaction() {
@@ -154,31 +135,31 @@
              return this.canOcclude;
          }
  
-@@ -1125,7 +1155,13 @@
+@@ -725,7 +_,13 @@
          }
  
-         public void onPlace(Level world, BlockPos pos, BlockState state, boolean notify) {
--            this.getBlock().onPlace(this.asState(), world, pos, state, notify);
+         public void onPlace(Level level, BlockPos pos, BlockState oldState, boolean movedByPiston) {
+-            this.getBlock().onPlace(this.asState(), level, pos, oldState, movedByPiston);
 +            // CraftBukkit start
-+            this.onPlace(world, pos, state, notify, null);
++            this.onPlace(level, pos, oldState, movedByPiston, null);
 +        }
 +
-+        public void onPlace(Level world, BlockPos blockposition, BlockState iblockdata, boolean flag, @Nullable UseOnContext context) {
-+            this.getBlock().onPlace(this.asState(), world, blockposition, iblockdata, flag, context);
++        public void onPlace(Level level, BlockPos pos, BlockState oldState, boolean movedByPiston, @Nullable net.minecraft.world.item.context.UseOnContext context) {
++            this.getBlock().onPlace(this.asState(), level, pos, oldState, movedByPiston, context);
 +            // CraftBukkit end
          }
  
-         public void onRemove(Level world, BlockPos pos, BlockState state, boolean moved) {
-@@ -1154,6 +1190,7 @@
+         public void onRemove(Level level, BlockPos pos, BlockState newState, boolean movedByPiston) {
+@@ -754,6 +_,7 @@
  
-         public void spawnAfterBreak(ServerLevel world, BlockPos pos, ItemStack tool, boolean dropExperience) {
-             this.getBlock().spawnAfterBreak(this.asState(), world, pos, tool, dropExperience);
-+            if (dropExperience) {getBlock().popExperience(world, pos, this.getBlock().getExpDrop(asState(), world, pos, tool, true));} // Paper - Properly handle xp dropping
+         public void spawnAfterBreak(ServerLevel level, BlockPos pos, ItemStack stack, boolean dropExperience) {
+             this.getBlock().spawnAfterBreak(this.asState(), level, pos, stack, dropExperience);
++            if (dropExperience) {this.getBlock().popExperience(level, pos, this.getBlock().getExpDrop(this.asState(), level, pos, stack, true));} // Paper - Properly handle xp dropping
          }
  
-         public List<ItemStack> getDrops(LootParams.Builder builder) {
-@@ -1250,11 +1287,11 @@
-             return this.getBlock().builtInRegistryHolder().is(key);
+         public List<ItemStack> getDrops(LootParams.Builder lootParams) {
+@@ -858,11 +_,11 @@
+             return this.getBlock().builtInRegistryHolder().is(block);
          }
  
 -        public FluidState getFluidState() {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/state/BlockState.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/state/BlockState.java.patch
similarity index 71%
rename from paper-server/patches/unapplied/net/minecraft/world/level/block/state/BlockState.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/block/state/BlockState.java.patch
index 77d0d6d1c5..01e51688b1 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/state/BlockState.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/state/BlockState.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/world/level/block/state/BlockState.java
 +++ b/net/minecraft/world/level/block/state/BlockState.java
-@@ -10,6 +10,16 @@
+@@ -10,6 +_,17 @@
  public class BlockState extends BlockBehaviour.BlockStateBase {
      public static final Codec<BlockState> CODEC = codec(BuiltInRegistries.BLOCK.byNameCodec(), Block::defaultBlockState).stable();
  
 +    // Paper start - optimise getType calls
-+    org.bukkit.Material cachedMaterial;
++    org.bukkit.@org.jspecify.annotations.Nullable Material cachedMaterial;
 +
 +    public final org.bukkit.Material getBukkitMaterial() {
 +        if (this.cachedMaterial == null) {
@@ -14,6 +14,7 @@
 +        return this.cachedMaterial;
 +    }
 +    // Paper end - optimise getType calls
-     public BlockState(Block block, Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap, MapCodec<BlockState> codec) {
-         super(block, propertyMap, codec);
++
+     public BlockState(Block owner, Reference2ObjectArrayMap<Property<?>, Comparable<?>> values, MapCodec<BlockState> propertiesCodec) {
+         super(owner, values, propertiesCodec);
      }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/state/properties/EnumProperty.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/state/properties/EnumProperty.java.patch
new file mode 100644
index 0000000000..94537a6a1d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/state/properties/EnumProperty.java.patch
@@ -0,0 +1,12 @@
+--- a/net/minecraft/world/level/block/state/properties/EnumProperty.java
++++ b/net/minecraft/world/level/block/state/properties/EnumProperty.java
+@@ -59,8 +_,7 @@
+         return this.ordinalToIndex[value.ordinal()];
+     }
+ 
+-    @Override
+-    public boolean equals(Object other) {
++    public boolean equals_unused(Object other) { // Paper - Perf: Optimize hashCode/equals
+         return this == other || other instanceof EnumProperty<?> enumProperty && super.equals(other) && this.values.equals(enumProperty.values);
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/state/properties/IntegerProperty.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/state/properties/IntegerProperty.java.patch
new file mode 100644
index 0000000000..a3b33fce20
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/state/properties/IntegerProperty.java.patch
@@ -0,0 +1,12 @@
+--- a/net/minecraft/world/level/block/state/properties/IntegerProperty.java
++++ b/net/minecraft/world/level/block/state/properties/IntegerProperty.java
+@@ -28,8 +_,7 @@
+         return this.values;
+     }
+ 
+-    @Override
+-    public boolean equals(Object other) {
++    public boolean equals_unused(Object other) { // Paper - Perf: Optimize hashCode/equals
+         return this == other || other instanceof IntegerProperty integerProperty && super.equals(other) && this.values.equals(integerProperty.values);
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/state/properties/Property.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/state/properties/Property.java.patch
new file mode 100644
index 0000000000..78b6de8b7e
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/state/properties/Property.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/level/block/state/properties/Property.java
++++ b/net/minecraft/world/level/block/state/properties/Property.java
+@@ -72,7 +_,7 @@
+ 
+     @Override
+     public boolean equals(Object other) {
+-        return this == other || other instanceof Property<?> property && this.clazz.equals(property.clazz) && this.name.equals(property.name);
++        return this == other; // Paper - Perf: Optimize hashCode/equals
+     }
+ 
+     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/border/WorldBorder.java.patch b/paper-server/patches/sources/net/minecraft/world/level/border/WorldBorder.java.patch
similarity index 77%
rename from paper-server/patches/unapplied/net/minecraft/world/level/border/WorldBorder.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/border/WorldBorder.java.patch
index c2615ad43a..a7a6057099 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/border/WorldBorder.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/border/WorldBorder.java.patch
@@ -1,23 +1,25 @@
 --- a/net/minecraft/world/level/border/WorldBorder.java
 +++ b/net/minecraft/world/level/border/WorldBorder.java
-@@ -30,6 +30,7 @@
+@@ -28,6 +_,7 @@
      int absoluteMaxSize = 29999984;
-     private WorldBorder.BorderExtent extent = new WorldBorder.StaticBorderExtent(5.9999968E7D);
-     public static final WorldBorder.Settings DEFAULT_SETTINGS = new WorldBorder.Settings(0.0D, 0.0D, 0.2D, 5.0D, 5, 15, 5.9999968E7D, 0L, 0.0D);
+     private WorldBorder.BorderExtent extent = new WorldBorder.StaticBorderExtent(5.999997E7F);
+     public static final WorldBorder.Settings DEFAULT_SETTINGS = new WorldBorder.Settings(0.0, 0.0, 0.2, 5.0, 5, 15, 5.999997E7F, 0L, 0.0);
 +    public net.minecraft.server.level.ServerLevel world; // CraftBukkit
  
-     public WorldBorder() {}
- 
-@@ -45,6 +46,18 @@
-         return this.isWithinBounds((double) chunkPos.getMinBlockX(), (double) chunkPos.getMinBlockZ()) && this.isWithinBounds((double) chunkPos.getMaxBlockX(), (double) chunkPos.getMaxBlockZ());
+     public boolean isWithinBounds(BlockPos pos) {
+         return this.isWithinBounds(pos.getX(), pos.getZ());
+@@ -41,6 +_,20 @@
+         return this.isWithinBounds(chunkPos.getMinBlockX(), chunkPos.getMinBlockZ()) && this.isWithinBounds(chunkPos.getMaxBlockX(), chunkPos.getMaxBlockZ());
      }
  
 +    // Paper start - Bound treasure maps to world border
 +    private final BlockPos.MutableBlockPos mutPos = new BlockPos.MutableBlockPos();
++
 +    public boolean isBlockInBounds(int chunkX, int chunkZ) {
 +        this.mutPos.set(chunkX, 64, chunkZ);
 +        return this.isWithinBounds(this.mutPos);
 +    }
++
 +    public boolean isChunkInBounds(int chunkX, int chunkZ) {
 +        this.mutPos.set(((chunkX << 4) + 15), 64, (chunkZ << 4) + 15);
 +        return this.isWithinBounds(this.mutPos);
@@ -25,9 +27,9 @@
 +    // Paper end - Bound treasure maps to world border
 +
      public boolean isWithinBounds(AABB box) {
-         return this.isWithinBounds(box.minX, box.minZ, box.maxX - 9.999999747378752E-6D, box.maxZ - 9.999999747378752E-6D);
+         return this.isWithinBounds(box.minX, box.minZ, box.maxX - 1.0E-5F, box.maxZ - 1.0E-5F);
      }
-@@ -135,6 +148,14 @@
+@@ -129,6 +_,14 @@
      }
  
      public void setCenter(double x, double z) {
@@ -42,7 +44,7 @@
          this.centerX = x;
          this.centerZ = z;
          this.extent.onCenterChange();
-@@ -161,6 +182,17 @@
+@@ -151,6 +_,17 @@
      }
  
      public void setSize(double size) {
@@ -58,30 +60,30 @@
 +        }
 +        // Paper end - Add worldborder events
          this.extent = new WorldBorder.StaticBorderExtent(size);
-         Iterator iterator = this.getListeners().iterator();
  
-@@ -173,6 +205,20 @@
+         for (BorderChangeListener borderChangeListener : this.getListeners()) {
+@@ -159,6 +_,20 @@
      }
  
-     public void lerpSizeBetween(double fromSize, double toSize, long time) {
+     public void lerpSizeBetween(double oldSize, double newSize, long time) {
 +        // Paper start - Add worldborder events
 +        if (this.world != null) {
 +            io.papermc.paper.event.world.border.WorldBorderBoundsChangeEvent.Type type;
-+            if (fromSize == toSize) { // new size = old size
++            if (oldSize == newSize) { // new size = old size
 +                type = io.papermc.paper.event.world.border.WorldBorderBoundsChangeEvent.Type.INSTANT_MOVE; // Use INSTANT_MOVE because below it creates a Static border if they are equal.
 +            } else {
 +                type = io.papermc.paper.event.world.border.WorldBorderBoundsChangeEvent.Type.STARTED_MOVE;
 +            }
-+            io.papermc.paper.event.world.border.WorldBorderBoundsChangeEvent event = new io.papermc.paper.event.world.border.WorldBorderBoundsChangeEvent(world.getWorld(), world.getWorld().getWorldBorder(), type, fromSize, toSize, time);
++            io.papermc.paper.event.world.border.WorldBorderBoundsChangeEvent event = new io.papermc.paper.event.world.border.WorldBorderBoundsChangeEvent(world.getWorld(), world.getWorld().getWorldBorder(), type, oldSize, newSize, time);
 +            if (!event.callEvent()) return;
-+            toSize = event.getNewSize();
++            newSize = event.getNewSize();
 +            time = event.getDuration();
 +        }
 +        // Paper end - Add worldborder events
-         this.extent = (WorldBorder.BorderExtent) (fromSize == toSize ? new WorldBorder.StaticBorderExtent(toSize) : new WorldBorder.MovingBorderExtent(fromSize, toSize, time));
-         Iterator iterator = this.getListeners().iterator();
- 
-@@ -189,6 +235,7 @@
+         this.extent = (WorldBorder.BorderExtent)(oldSize == newSize
+             ? new WorldBorder.StaticBorderExtent(newSize)
+             : new WorldBorder.MovingBorderExtent(oldSize, newSize, time));
+@@ -173,6 +_,7 @@
      }
  
      public void addListener(BorderChangeListener listener) {
@@ -89,11 +91,11 @@
          this.listeners.add(listener);
      }
  
-@@ -483,6 +530,7 @@
+@@ -369,6 +_,7 @@
  
          @Override
          public WorldBorder.BorderExtent update() {
 +            if (world != null && this.getLerpRemainingTime() <= 0L) new io.papermc.paper.event.world.border.WorldBorderBoundsChangeFinishEvent(world.getWorld(), world.getWorld().getWorldBorder(), this.from, this.to, this.lerpDuration).callEvent(); // Paper - Add worldborder events
-             return (WorldBorder.BorderExtent) (this.getLerpRemainingTime() <= 0L ? WorldBorder.this.new StaticBorderExtent(this.to) : this);
+             return (WorldBorder.BorderExtent)(this.getLerpRemainingTime() <= 0L ? WorldBorder.this.new StaticBorderExtent(this.to) : this);
          }
  
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/ChunkAccess.java.patch b/paper-server/patches/sources/net/minecraft/world/level/chunk/ChunkAccess.java.patch
similarity index 65%
rename from paper-server/patches/unapplied/net/minecraft/world/level/chunk/ChunkAccess.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/chunk/ChunkAccess.java.patch
index 54c4c6342e..d1b5769824 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/ChunkAccess.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/chunk/ChunkAccess.java.patch
@@ -1,43 +1,53 @@
 --- a/net/minecraft/world/level/chunk/ChunkAccess.java
 +++ b/net/minecraft/world/level/chunk/ChunkAccess.java
-@@ -65,7 +65,7 @@
+@@ -64,7 +_,7 @@
      protected final ShortList[] postProcessing;
      private volatile boolean unsaved;
      private volatile boolean isLightCorrect;
 -    protected final ChunkPos chunkPos;
 +    protected final ChunkPos chunkPos; public final long coordinateKey; public final int locX; public final int locZ; // Paper - cache coordinate key
      private long inhabitedTime;
-     /** @deprecated */
      @Nullable
-@@ -85,8 +85,14 @@
+     @Deprecated
+@@ -82,6 +_,11 @@
+     public final Map<BlockPos, BlockEntity> blockEntities = new Object2ObjectOpenHashMap<>();
      protected final LevelHeightAccessor levelHeightAccessor;
      protected final LevelChunkSection[] sections;
- 
 +    // CraftBukkit start - SPIGOT-6814: move to IChunkAccess to account for 1.17 to 1.18 chunk upgrading.
 +    private static final org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry();
 +    public org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer persistentDataContainer = new org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer(ChunkAccess.DATA_TYPE_REGISTRY);
 +    // CraftBukkit end
-+
-     public ChunkAccess(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor heightLimitView, Registry<Biome> biomeRegistry, long inhabitedTime, @Nullable LevelChunkSection[] sectionArray, @Nullable BlendingData blendingData) {
--        this.chunkPos = pos;
-+        this.locX = pos.x; this.locZ = pos.z; // Paper - reduce need for field lookups
-+        this.chunkPos = pos; this.coordinateKey = ChunkPos.asLong(locX, locZ); // Paper - cache long key
++    public final Registry<Biome> biomeRegistry; // CraftBukkit
+ 
+     public ChunkAccess(
+         ChunkPos chunkPos,
+@@ -92,7 +_,8 @@
+         @Nullable LevelChunkSection[] sections,
+         @Nullable BlendingData blendingData
+     ) {
+-        this.chunkPos = chunkPos;
++        this.locX = chunkPos.x; this.locZ = chunkPos.z; // Paper - reduce need for field lookups
++        this.chunkPos = chunkPos; this.coordinateKey = ChunkPos.asLong(locX, locZ); // Paper - cache long key
          this.upgradeData = upgradeData;
-         this.levelHeightAccessor = heightLimitView;
-         this.sections = new LevelChunkSection[heightLimitView.getSectionsCount()];
-@@ -103,7 +109,11 @@
+         this.levelHeightAccessor = levelHeightAccessor;
+         this.sections = new LevelChunkSection[levelHeightAccessor.getSectionsCount()];
+@@ -109,6 +_,7 @@
          }
  
-         ChunkAccess.replaceMissingSections(biomeRegistry, this.sections);
-+        // CraftBukkit start
-+        this.biomeRegistry = biomeRegistry;
+         replaceMissingSections(biomeRegistry, this.sections);
++        this.biomeRegistry = biomeRegistry; // CraftBukkit
      }
-+    public final Registry<Biome> biomeRegistry;
-+    // CraftBukkit end
  
-     private static void replaceMissingSections(Registry<Biome> biomeRegistry, LevelChunkSection[] sectionArray) {
-         for (int i = 0; i < sectionArray.length; ++i) {
-@@ -275,6 +285,7 @@
+     private static void replaceMissingSections(Registry<Biome> biomeRegistry, LevelChunkSection[] sections) {
+@@ -123,6 +_,7 @@
+         return GameEventListenerRegistry.NOOP;
+     }
+ 
++    public abstract BlockState getBlockState(final int x, final int y, final int z); // Paper
+     @Nullable
+     public abstract BlockState setBlockState(BlockPos pos, BlockState state, boolean isMoving);
+ 
+@@ -273,6 +_,7 @@
      public boolean tryMarkSaved() {
          if (this.unsaved) {
              this.unsaved = false;
@@ -45,7 +55,7 @@
              return true;
          } else {
              return false;
-@@ -282,7 +293,7 @@
+@@ -280,7 +_,7 @@
      }
  
      public boolean isUnsaved() {
@@ -54,15 +64,10 @@
      }
  
      public abstract ChunkStatus getPersistedStatus();
-@@ -458,10 +469,31 @@
- 
-             crashreportsystemdetails.setDetail("Location", () -> {
-                 return CrashReportCategory.formatLocation(this, biomeX, biomeY, biomeZ);
-+            });
-+            throw new ReportedException(crashreport);
-+        }
-+    }
-+
+@@ -446,6 +_,26 @@
+             throw new ReportedException(crashReport);
+         }
+     }
 +    // CraftBukkit start
 +    public void setBiome(int i, int j, int k, Holder<Biome> biome) {
 +        try {
@@ -78,11 +83,11 @@
 +
 +            crashreportsystemdetails.setDetail("Location", () -> {
 +                return CrashReportCategory.formatLocation(this, i, j, k);
-             });
-             throw new ReportedException(crashreport);
-         }
-     }
++            });
++            throw new ReportedException(crashreport);
++        }
++    }
 +    // CraftBukkit end
  
-     public void fillBiomesFromNoise(BiomeResolver biomeSupplier, Climate.Sampler sampler) {
-         ChunkPos chunkcoordintpair = this.getPos();
+     public void fillBiomesFromNoise(BiomeResolver resolver, Climate.Sampler sampler) {
+         ChunkPos pos = this.getPos();
diff --git a/paper-server/patches/sources/net/minecraft/world/level/chunk/ChunkGenerator.java.patch b/paper-server/patches/sources/net/minecraft/world/level/chunk/ChunkGenerator.java.patch
new file mode 100644
index 0000000000..5fa4572bc5
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/chunk/ChunkGenerator.java.patch
@@ -0,0 +1,148 @@
+--- a/net/minecraft/world/level/chunk/ChunkGenerator.java
++++ b/net/minecraft/world/level/chunk/ChunkGenerator.java
+@@ -104,8 +_,8 @@
+ 
+     protected abstract MapCodec<? extends ChunkGenerator> codec();
+ 
+-    public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> structureSetLookup, RandomState randomState, long seed) {
+-        return ChunkGeneratorStructureState.createForNormal(randomState, seed, this.biomeSource, structureSetLookup);
++    public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> structureSetLookup, RandomState randomState, long seed, org.spigotmc.SpigotWorldConfig conf) { // Spigot
++        return ChunkGeneratorStructureState.createForNormal(randomState, seed, this.biomeSource, structureSetLookup, conf); // Spigot
+     }
+ 
+     public Optional<ResourceKey<MapCodec<? extends ChunkGenerator>>> getTypeNameForDataFixer() {
+@@ -127,6 +_,24 @@
+     public Pair<BlockPos, Holder<Structure>> findNearestMapStructure(
+         ServerLevel level, HolderSet<Structure> structure, BlockPos pos, int searchRadius, boolean skipKnownStructures
+     ) {
++        // Paper start - StructuresLocateEvent
++        final org.bukkit.World bukkitWorld = level.getWorld();
++        final org.bukkit.Location origin = io.papermc.paper.util.MCUtil.toLocation(level, pos);
++        final List<org.bukkit.generator.structure.Structure> apiStructures = structure.stream().map(Holder::value).map(nms -> org.bukkit.craftbukkit.generator.structure.CraftStructure.minecraftToBukkit(nms)).toList();
++        if (!apiStructures.isEmpty()) {
++            final io.papermc.paper.event.world.StructuresLocateEvent event = new io.papermc.paper.event.world.StructuresLocateEvent(bukkitWorld, origin, apiStructures, searchRadius, skipKnownStructures);
++            if (!event.callEvent()) {
++                return null;
++            }
++            if (event.getResult() != null) {
++                return Pair.of(io.papermc.paper.util.MCUtil.toBlockPos(event.getResult().pos()), level.registryAccess().lookupOrThrow(Registries.STRUCTURE).wrapAsHolder(org.bukkit.craftbukkit.generator.structure.CraftStructure.bukkitToMinecraft(event.getResult().structure())));
++            }
++            pos = io.papermc.paper.util.MCUtil.toBlockPosition(event.getOrigin());
++            searchRadius = event.getRadius();
++            skipKnownStructures = event.shouldFindUnexplored();
++            structure = HolderSet.direct(api -> level.registryAccess().lookupOrThrow(Registries.STRUCTURE).wrapAsHolder(org.bukkit.craftbukkit.generator.structure.CraftStructure.bukkitToMinecraft(api)), event.getStructures());
++        }
++        // Paper end
+         ChunkGeneratorStructureState generatorState = level.getChunkSource().getGeneratorState();
+         Map<StructurePlacement, Set<Holder<Structure>>> map = new Object2ObjectArrayMap<>();
+ 
+@@ -222,6 +_,7 @@
+             BlockPos.MutableBlockPos mutableBlockPos = new BlockPos.MutableBlockPos();
+ 
+             for (ChunkPos chunkPos : ringPositionsFor) {
++                if (!level.paperConfig().environment.locateStructuresOutsideWorldBorder && !level.getWorldBorder().isChunkInBounds(chunkPos.x, chunkPos.z)) { continue; } // Paper - Bound treasure maps to world border
+                 mutableBlockPos.set(SectionPos.sectionToBlockCoord(chunkPos.x, 8), 32, SectionPos.sectionToBlockCoord(chunkPos.z, 8));
+                 double d1 = mutableBlockPos.distSqr(pos);
+                 boolean flag = pair == null || d1 < d;
+@@ -255,11 +_,15 @@
+         int spacing = spreadPlacement.spacing();
+ 
+         for (int i = -z; i <= z; i++) {
+-            boolean flag = i == -z || i == z;
++            // Paper start - Perf: iterate over border chunks instead of entire square chunk area
++            final int radius = z;
++            boolean flag = i == -z || i == z; final boolean onBorderAlongZAxis = flag; // Paper - OBFHELPER
+ 
+-            for (int i1 = -z; i1 <= z; i1++) {
+-                boolean flag1 = i1 == -z || i1 == z;
+-                if (flag || flag1) {
++            for (int i1 = -radius; i1 <= radius; i1 += onBorderAlongZAxis ? 1 : radius * 2) {
++                // boolean flag1 = i1 == -z || i1 == z;
++                // if (flag || flag1) {
++                if (true) {
++            // Paper end - Perf: iterate over border chunks instead of entire square chunk area
+                     int i2 = x + spacing * i;
+                     int i3 = y + spacing * i1;
+                     ChunkPos potentialStructureChunk = spreadPlacement.getPotentialStructureChunk(seed, i2, i3);
+@@ -312,7 +_,7 @@
+         }
+     }
+ 
+-    public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager) {
++    public void addVanillaDecorations(WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager) { // CraftBukkit - rename
+         ChunkPos pos = chunk.getPos();
+         if (!SharedConstants.debugVoidTerrain(pos)) {
+             SectionPos sectionPos = SectionPos.of(pos, level.getMinSectionY());
+@@ -385,7 +_,14 @@
+                             int i3 = ints[i2];
+                             PlacedFeature placedFeature = stepFeatureData1.features().get(i3);
+                             Supplier<String> supplier1 = () -> registry1.getResourceKey(placedFeature).map(Object::toString).orElseGet(placedFeature::toString);
+-                            worldgenRandom.setFeatureSeed(l, i3, i);
++                            // Paper start - Configurable feature seeds; change populationSeed used in random
++                            long featurePopulationSeed = l;
++                            final long configFeatureSeed = level.getMinecraftWorld().paperConfig().featureSeeds.features.getLong(placedFeature.feature());
++                            if (configFeatureSeed != -1) {
++                                featurePopulationSeed = worldgenRandom.setDecorationSeed(configFeatureSeed, blockPos.getX(), blockPos.getZ()); // See WorldgenRandom.setDecorationSeed from above
++                            }
++                            worldgenRandom.setFeatureSeed(featurePopulationSeed, i3, i);
++                            // Paper end - Configurable feature seeds
+ 
+                             try {
+                                 level.setCurrentlyGenerating(supplier1);
+@@ -407,6 +_,32 @@
+             }
+         }
+     }
++   // CraftBukkit start
++    public void applyBiomeDecoration(WorldGenLevel world, ChunkAccess chunk, StructureManager structureAccessor) {
++        this.applyBiomeDecoration(world, chunk, structureAccessor, true);
++    }
++
++    public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) {
++        if (vanilla) {
++            this.addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
++        }
++
++        org.bukkit.World world = generatoraccessseed.getMinecraftWorld().getWorld();
++        // only call when a populator is present (prevents unnecessary entity conversion)
++        if (!world.getPopulators().isEmpty()) {
++            org.bukkit.craftbukkit.generator.CraftLimitedRegion limitedRegion = new org.bukkit.craftbukkit.generator.CraftLimitedRegion(generatoraccessseed, ichunkaccess.getPos());
++            int x = ichunkaccess.getPos().x;
++            int z = ichunkaccess.getPos().z;
++            for (org.bukkit.generator.BlockPopulator populator : world.getPopulators()) {
++                WorldgenRandom seededrandom = new WorldgenRandom(new net.minecraft.world.level.levelgen.LegacyRandomSource(generatoraccessseed.getSeed()));
++                seededrandom.setDecorationSeed(generatoraccessseed.getSeed(), x, z);
++                populator.populate(world, new org.bukkit.craftbukkit.util.RandomSourceWrapper.RandomWrapper(seededrandom), x, z, limitedRegion);
++            }
++            limitedRegion.saveEntities();
++            limitedRegion.breakLink();
++        }
++    }
++    // CraftBukkit end
+ 
+     private static BoundingBox getWritableArea(ChunkAccess chunk) {
+         ChunkPos pos = chunk.getPos();
+@@ -483,7 +_,7 @@
+                         }
+                     }
+ 
+-                    if (structurePlacement.isStructureChunk(structureState, pos.x, pos.z)) {
++                    if (structurePlacement.isStructureChunk(structureState, pos.x, pos.z, structurePlacement instanceof net.minecraft.world.level.chunk.ChunkGeneratorStructureState.KeyedRandomSpreadStructurePlacement keyed ? keyed.key : null)) { // Paper - Add missing structure set seed configs
+                         if (list.size() == 1) {
+                             this.tryGenerateStructure(
+                                 list.get(0),
+@@ -577,6 +_,14 @@
+             predicate
+         );
+         if (structureStart.isValid()) {
++            // CraftBukkit start
++            BoundingBox box = structureStart.getBoundingBox();
++            org.bukkit.event.world.AsyncStructureSpawnEvent event = new org.bukkit.event.world.AsyncStructureSpawnEvent(structureManager.level.getMinecraftWorld().getWorld(), org.bukkit.craftbukkit.generator.structure.CraftStructure.minecraftToBukkit(structure), new org.bukkit.util.BoundingBox(box.minX(), box.minY(), box.minZ(), box.maxX(), box.maxY(), box.maxZ()), chunkPos.x, chunkPos.z);
++            org.bukkit.Bukkit.getPluginManager().callEvent(event);
++            if (event.isCancelled()) {
++                return true;
++            }
++            // CraftBukkit end
+             structureManager.setStartForStructure(sectionPos, structure, structureStart, chunk);
+             return true;
+         } else {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/ChunkGeneratorStructureState.java.patch b/paper-server/patches/sources/net/minecraft/world/level/chunk/ChunkGeneratorStructureState.java.patch
similarity index 54%
rename from paper-server/patches/unapplied/net/minecraft/world/level/chunk/ChunkGeneratorStructureState.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/chunk/ChunkGeneratorStructureState.java.patch
index f9ebf0e3b1..a10f3c36e7 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/ChunkGeneratorStructureState.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/chunk/ChunkGeneratorStructureState.java.patch
@@ -1,53 +1,33 @@
 --- a/net/minecraft/world/level/chunk/ChunkGeneratorStructureState.java
 +++ b/net/minecraft/world/level/chunk/ChunkGeneratorStructureState.java
-@@ -1,3 +1,4 @@
-+// mc-dev import
- package net.minecraft.world.level.chunk;
- 
- import com.google.common.base.Stopwatch;
-@@ -33,6 +34,11 @@
- import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
- import org.slf4j.Logger;
- 
-+// Spigot start
-+import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
-+import org.spigotmc.SpigotWorldConfig;
-+// Spigot end
-+
- public class ChunkGeneratorStructureState {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
-@@ -44,22 +50,109 @@
-     private final Map<ConcentricRingsStructurePlacement, CompletableFuture<List<ChunkPos>>> ringPositions = new Object2ObjectArrayMap();
+@@ -41,22 +_,109 @@
+     private final Map<ConcentricRingsStructurePlacement, CompletableFuture<List<ChunkPos>>> ringPositions = new Object2ObjectArrayMap<>();
      private boolean hasGeneratedPositions;
      private final List<Holder<StructureSet>> possibleStructureSets;
-+    public final SpigotWorldConfig conf; // Paper - Add missing structure set seed configs
++    public final org.spigotmc.SpigotWorldConfig conf; // Paper - Add missing structure set seed configs
  
--    public static ChunkGeneratorStructureState createForFlat(RandomState noiseConfig, long seed, BiomeSource biomeSource, Stream<Holder<StructureSet>> structureSets) {
--        List<Holder<StructureSet>> list = structureSets.filter((holder) -> {
--            return ChunkGeneratorStructureState.hasBiomesForStructureSet((StructureSet) holder.value(), biomeSource);
-+    public static ChunkGeneratorStructureState createForFlat(RandomState randomstate, long i, BiomeSource worldchunkmanager, Stream<Holder<StructureSet>> stream, SpigotWorldConfig conf) { // Spigot
-+        List<Holder<StructureSet>> list = stream.filter((holder) -> {
-+            return ChunkGeneratorStructureState.hasBiomesForStructureSet((StructureSet) holder.value(), worldchunkmanager);
-         }).toList();
- 
--        return new ChunkGeneratorStructureState(noiseConfig, biomeSource, seed, 0L, list);
-+        return new ChunkGeneratorStructureState(randomstate, worldchunkmanager, i, 0L, ChunkGeneratorStructureState.injectSpigot(list, conf), conf); // Spigot
+     public static ChunkGeneratorStructureState createForFlat(
+-        RandomState randomState, long levelSeed, BiomeSource biomeSource, Stream<Holder<StructureSet>> structureSets
++        RandomState randomState, long levelSeed, BiomeSource biomeSource, Stream<Holder<StructureSet>> structureSets, org.spigotmc.SpigotWorldConfig conf // Spigot
+     ) {
+         List<Holder<StructureSet>> list = structureSets.filter(structureSet -> hasBiomesForStructureSet(structureSet.value(), biomeSource)).toList();
+-        return new ChunkGeneratorStructureState(randomState, biomeSource, levelSeed, 0L, list);
++        return new ChunkGeneratorStructureState(randomState, biomeSource, levelSeed, 0L, ChunkGeneratorStructureState.injectSpigot(list, conf), conf); // Spigot
      }
  
--    public static ChunkGeneratorStructureState createForNormal(RandomState noiseConfig, long seed, BiomeSource biomeSource, HolderLookup<StructureSet> structureSetRegistry) {
--        List<Holder<StructureSet>> list = (List) structureSetRegistry.listElements().filter((holder_c) -> {
--            return ChunkGeneratorStructureState.hasBiomesForStructureSet((StructureSet) holder_c.value(), biomeSource);
-+    public static ChunkGeneratorStructureState createForNormal(RandomState randomstate, long i, BiomeSource worldchunkmanager, HolderLookup<StructureSet> holderlookup, SpigotWorldConfig conf) { // Spigot
-+        List<Holder<StructureSet>> list = (List) holderlookup.listElements().filter((holder_c) -> {
-+            return ChunkGeneratorStructureState.hasBiomesForStructureSet((StructureSet) holder_c.value(), worldchunkmanager);
-         }).collect(Collectors.toUnmodifiableList());
- 
--        return new ChunkGeneratorStructureState(noiseConfig, biomeSource, seed, seed, list);
-+        return new ChunkGeneratorStructureState(randomstate, worldchunkmanager, i, i, ChunkGeneratorStructureState.injectSpigot(list, conf), conf); // Spigot
+     public static ChunkGeneratorStructureState createForNormal(
+-        RandomState randomState, long seed, BiomeSource biomeSource, HolderLookup<StructureSet> structureSetLookup
++        RandomState randomState, long seed, BiomeSource biomeSource, HolderLookup<StructureSet> structureSetLookup, org.spigotmc.SpigotWorldConfig conf // Spigot
+     ) {
+         List<Holder<StructureSet>> list = structureSetLookup.listElements()
+             .filter(structureSet -> hasBiomesForStructureSet(structureSet.value(), biomeSource))
+             .collect(Collectors.toUnmodifiableList());
+-        return new ChunkGeneratorStructureState(randomState, biomeSource, seed, seed, list);
+-    }
++        return new ChunkGeneratorStructureState(randomState, biomeSource, seed, seed, ChunkGeneratorStructureState.injectSpigot(list, conf), conf); // Spigot
 +    }
 +    // Paper start - Add missing structure set seed configs; horrible hack because spigot creates a ton of direct Holders which lose track of the identifying key
-+    public static final class KeyedRandomSpreadStructurePlacement extends RandomSpreadStructurePlacement {
++    public static final class KeyedRandomSpreadStructurePlacement extends net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement {
 +        public final net.minecraft.resources.ResourceKey<StructureSet> key;
 +        public KeyedRandomSpreadStructurePlacement(net.minecraft.resources.ResourceKey<StructureSet> key, net.minecraft.core.Vec3i locateOffset, FrequencyReductionMethod frequencyReductionMethod, float frequency, int salt, java.util.Optional<StructurePlacement.ExclusionZone> exclusionZone, int spacing, int separation, net.minecraft.world.level.levelgen.structure.placement.RandomSpreadType spreadType) {
 +            super(locateOffset, frequencyReductionMethod, frequency, salt, exclusionZone, spacing, separation, spreadType);
@@ -57,11 +37,11 @@
 +    // Paper end - Add missing structure set seed configs
 +
 +    // Spigot start
-+    private static List<Holder<StructureSet>> injectSpigot(List<Holder<StructureSet>> list, SpigotWorldConfig conf) {
++    private static List<Holder<StructureSet>> injectSpigot(List<Holder<StructureSet>> list, org.spigotmc.SpigotWorldConfig conf) {
 +        return list.stream().map((holder) -> {
 +            StructureSet structureset = holder.value();
 +            final Holder<StructureSet> newHolder; // Paper - Add missing structure set seed configs
-+            if (structureset.placement() instanceof RandomSpreadStructurePlacement randomConfig && holder.unwrapKey().orElseThrow().location().getNamespace().equals(net.minecraft.resources.ResourceLocation.DEFAULT_NAMESPACE)) { // Paper - Add missing structure set seed configs; check namespace cause datapacks could add structure sets with the same path
++            if (structureset.placement() instanceof net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement randomConfig && holder.unwrapKey().orElseThrow().location().getNamespace().equals(net.minecraft.resources.ResourceLocation.DEFAULT_NAMESPACE)) { // Paper - Add missing structure set seed configs; check namespace cause datapacks could add structure sets with the same path
 +                String name = holder.unwrapKey().orElseThrow().location().getPath();
 +                int seed = randomConfig.salt;
 +
@@ -130,46 +110,47 @@
 +            return newHolder;
 +            // Paper end - Add missing structure set seed configs
 +        }).collect(Collectors.toUnmodifiableList());
-     }
++    }
 +    // Spigot end
  
      private static boolean hasBiomesForStructureSet(StructureSet structureSet, BiomeSource biomeSource) {
-         Stream<Holder<Biome>> stream = structureSet.structures().stream().flatMap((structureset_a) -> {
-@@ -73,12 +166,13 @@
-         return stream.anyMatch(set::contains);
+         Stream<Holder<Biome>> stream = structureSet.structures().stream().flatMap(structureEntry -> {
+@@ -67,13 +_,14 @@
      }
  
--    private ChunkGeneratorStructureState(RandomState noiseConfig, BiomeSource biomeSource, long structureSeed, long concentricRingSeed, List<Holder<StructureSet>> structureSets) {
-+    private ChunkGeneratorStructureState(RandomState noiseConfig, BiomeSource biomeSource, long structureSeed, long concentricRingSeed, List<Holder<StructureSet>> structureSets, SpigotWorldConfig conf) { // Paper - Add missing structure set seed configs
-         this.randomState = noiseConfig;
-         this.levelSeed = structureSeed;
+     private ChunkGeneratorStructureState(
+-        RandomState randomState, BiomeSource biomeSource, long levelSeed, long cocentricRingsSeed, List<Holder<StructureSet>> possibleStructureSets
++        RandomState randomState, BiomeSource biomeSource, long levelSeed, long cocentricRingsSeed, List<Holder<StructureSet>> possibleStructureSets, org.spigotmc.SpigotWorldConfig conf // Paper - Add missing structure set seed configs
+     ) {
+         this.randomState = randomState;
+         this.levelSeed = levelSeed;
          this.biomeSource = biomeSource;
-         this.concentricRingsSeed = concentricRingSeed;
-         this.possibleStructureSets = structureSets;
+         this.concentricRingsSeed = cocentricRingsSeed;
+         this.possibleStructureSets = possibleStructureSets;
 +        this.conf = conf; // Paper - Add missing structure set seed configs
      }
  
      public List<Holder<StructureSet>> possibleStructureSets() {
-@@ -132,7 +226,13 @@
-             HolderSet<Biome> holderset = placement.preferredBiomes();
-             RandomSource randomsource = RandomSource.create();
- 
+@@ -118,7 +_,13 @@
+             int spread = placement.spread();
+             HolderSet<Biome> holderSet = placement.preferredBiomes();
+             RandomSource randomSource = RandomSource.create();
 +            // Paper start - Add missing structure set seed configs
-+            if (this.conf.strongholdSeed != null && structureSetEntry.is(net.minecraft.world.level.levelgen.structure.BuiltinStructureSets.STRONGHOLDS)) {
-+                randomsource.setSeed(this.conf.strongholdSeed);
++            if (this.conf.strongholdSeed != null && structureSet.is(net.minecraft.world.level.levelgen.structure.BuiltinStructureSets.STRONGHOLDS)) {
++                randomSource.setSeed(this.conf.strongholdSeed);
 +            } else {
 +            // Paper end - Add missing structure set seed configs
-             randomsource.setSeed(this.concentricRingsSeed);
+             randomSource.setSeed(this.concentricRingsSeed);
 +            } // Paper - Add missing structure set seed configs
-             double d0 = randomsource.nextDouble() * Math.PI * 2.0D;
-             int l = 0;
+             double d = randomSource.nextDouble() * Math.PI * 2.0;
+             int i = 0;
              int i1 = 0;
-@@ -209,7 +309,7 @@
+@@ -197,7 +_,7 @@
  
-         for (int l = centerChunkX - chunkCount; l <= centerChunkX + chunkCount; ++l) {
-             for (int i1 = centerChunkZ - chunkCount; i1 <= centerChunkZ + chunkCount; ++i1) {
--                if (structureplacement.isStructureChunk(this, l, i1)) {
-+                if (structureplacement.isStructureChunk(this, l, i1, structureplacement instanceof KeyedRandomSpreadStructurePlacement keyed ? keyed.key : null)) { // Paper - Add missing structure set seed configs
+         for (int i = x - range; i <= x + range; i++) {
+             for (int i1 = z - range; i1 <= z + range; i1++) {
+-                if (structurePlacement.isStructureChunk(this, i, i1)) {
++                if (structurePlacement.isStructureChunk(this, i, i1, structurePlacement instanceof KeyedRandomSpreadStructurePlacement keyed ? keyed.key : null)) { // Paper - Add missing structure set seed configs
                      return true;
                  }
              }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/EmptyLevelChunk.java.patch b/paper-server/patches/sources/net/minecraft/world/level/chunk/EmptyLevelChunk.java.patch
similarity index 95%
rename from paper-server/patches/unapplied/net/minecraft/world/level/chunk/EmptyLevelChunk.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/chunk/EmptyLevelChunk.java.patch
index 1ccf6729a6..a67013f5ff 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/EmptyLevelChunk.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/chunk/EmptyLevelChunk.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/level/chunk/EmptyLevelChunk.java
 +++ b/net/minecraft/world/level/chunk/EmptyLevelChunk.java
-@@ -25,6 +25,12 @@
+@@ -25,6 +_,12 @@
      public BlockState getBlockState(BlockPos pos) {
          return Blocks.VOID_AIR.defaultBlockState();
      }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/chunk/HashMapPalette.java.patch b/paper-server/patches/sources/net/minecraft/world/level/chunk/HashMapPalette.java.patch
new file mode 100644
index 0000000000..df3071147b
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/chunk/HashMapPalette.java.patch
@@ -0,0 +1,30 @@
+--- a/net/minecraft/world/level/chunk/HashMapPalette.java
++++ b/net/minecraft/world/level/chunk/HashMapPalette.java
+@@ -20,7 +_,7 @@
+     }
+ 
+     public HashMapPalette(IdMap<T> registry, int bits, PaletteResize<T> resizeHandler) {
+-        this(registry, bits, resizeHandler, CrudeIncrementalIntIdentityHashBiMap.create(1 << bits));
++        this(registry, bits, resizeHandler, CrudeIncrementalIntIdentityHashBiMap.create((1 << bits) + 1)); // Paper - Perf: Avoid unnecessary resize operation in CrudeIncrementalIntIdentityHashBiMap
+     }
+ 
+     private HashMapPalette(IdMap<T> registry, int bits, PaletteResize<T> resizeHandler, CrudeIncrementalIntIdentityHashBiMap<T> values) {
+@@ -38,10 +_,16 @@
+     public int idFor(T state) {
+         int id = this.values.getId(state);
+         if (id == -1) {
+-            id = this.values.add(state);
+-            if (id >= 1 << this.bits) {
++            // Paper start - Perf: Avoid unnecessary resize operation in CrudeIncrementalIntIdentityHashBiMap and optimize
++            // We use size() instead of the result from add(K)
++            // This avoids adding another object unnecessarily
++            // Without this change, + 2 would be required in the constructor
++            if (this.values.size() >= 1 << this.bits) {
+                 id = this.resizeHandler.onResize(this.bits + 1, state);
++            } else {
++                id = this.values.add(state);
+             }
++            // Paper end - Perf: Avoid unnecessary resize operation in CrudeIncrementalIntIdentityHashBiMap and optimize
+         }
+ 
+         return id;
diff --git a/paper-server/patches/sources/net/minecraft/world/level/chunk/ImposterProtoChunk.java.patch b/paper-server/patches/sources/net/minecraft/world/level/chunk/ImposterProtoChunk.java.patch
new file mode 100644
index 0000000000..da521db813
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/chunk/ImposterProtoChunk.java.patch
@@ -0,0 +1,15 @@
+--- a/net/minecraft/world/level/chunk/ImposterProtoChunk.java
++++ b/net/minecraft/world/level/chunk/ImposterProtoChunk.java
+@@ -56,6 +_,12 @@
+     public BlockState getBlockState(BlockPos pos) {
+         return this.wrapped.getBlockState(pos);
+     }
++    // Paper start
++    @Override
++    public final BlockState getBlockState(final int x, final int y, final int z) {
++        return this.wrapped.getBlockStateFinal(x, y, z);
++    }
++    // Paper end
+ 
+     @Override
+     public FluidState getFluidState(BlockPos pos) {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/chunk/LevelChunk.java.patch b/paper-server/patches/sources/net/minecraft/world/level/chunk/LevelChunk.java.patch
new file mode 100644
index 0000000000..dcd413b184
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/chunk/LevelChunk.java.patch
@@ -0,0 +1,305 @@
+--- a/net/minecraft/world/level/chunk/LevelChunk.java
++++ b/net/minecraft/world/level/chunk/LevelChunk.java
+@@ -76,7 +_,7 @@
+     };
+     private final Map<BlockPos, LevelChunk.RebindableTickingBlockEntityWrapper> tickersInLevel = Maps.newHashMap();
+     public boolean loaded;
+-    public final Level level;
++    public final ServerLevel level; // CraftBukkit - type
+     @Nullable
+     private Supplier<FullChunkStatus> fullStatus;
+     @Nullable
+@@ -85,6 +_,14 @@
+     private final LevelChunkTicks<Block> blockTicks;
+     private final LevelChunkTicks<Fluid> fluidTicks;
+     private LevelChunk.UnsavedListener unsavedListener = chunkPos -> {};
++    // CraftBukkit start
++    public boolean mustNotSave;
++    public boolean needsDecoration;
++    // CraftBukkit end
++
++    // Paper start
++    boolean loadedTicketLevel;
++    // Paper end
+ 
+     public LevelChunk(Level level, ChunkPos pos) {
+         this(level, pos, UpgradeData.EMPTY, new LevelChunkTicks<>(), new LevelChunkTicks<>(), 0L, null, null, null);
+@@ -102,7 +_,7 @@
+         @Nullable BlendingData blendingData
+     ) {
+         super(pos, data, level, level.registryAccess().lookupOrThrow(Registries.BIOME), inhabitedTime, sections, blendingData);
+-        this.level = level;
++        this.level = (net.minecraft.server.level.ServerLevel) level; // CraftBukkit - type
+         this.gameEventListenerRegistrySections = new Int2ObjectOpenHashMap<>();
+ 
+         for (Heightmap.Types types : Heightmap.Types.values()) {
+@@ -154,6 +_,10 @@
+         this.skyLightSources = chunk.skyLightSources;
+         this.setLightCorrect(chunk.isLightCorrect());
+         this.markUnsaved();
++        this.needsDecoration = true; // CraftBukkit
++        // CraftBukkit start
++        this.persistentDataContainer = chunk.persistentDataContainer; // SPIGOT-6814: copy PDC to account for 1.17 to 1.18 chunk upgrading.
++        // CraftBukkit end
+     }
+ 
+     public void setUnsavedListener(LevelChunk.UnsavedListener unsavedListener) {
+@@ -162,6 +_,12 @@
+             unsavedListener.setUnsaved(this.chunkPos);
+         }
+     }
++    // Paper start
++    @Override
++    public long getInhabitedTime() {
++        return this.level.paperConfig().chunks.fixedChunkInhabitedTime < 0 ? super.getInhabitedTime() : this.level.paperConfig().chunks.fixedChunkInhabitedTime;
++    }
++    // Paper end
+ 
+     @Override
+     public void markUnsaved() {
+@@ -195,8 +_,25 @@
+             : super.getListenerRegistry(sectionY);
+     }
+ 
++    // Paper start - Perf: Reduce instructions and provide final method
++    public BlockState getBlockState(final int x, final int y, final int z) {
++        return this.getBlockStateFinal(x, y, z);
++    }
++    public BlockState getBlockStateFinal(final int x, final int y, final int z) {
++        // Copied and modified from below
++        final int sectionIndex = this.getSectionIndex(y);
++        if (sectionIndex < 0 || sectionIndex >= this.sections.length
++            || this.sections[sectionIndex].nonEmptyBlockCount == 0) {
++            return Blocks.AIR.defaultBlockState();
++        }
++        return this.sections[sectionIndex].states.get((y & 15) << 8 | (z & 15) << 4 | x & 15);
++    }
+     @Override
+     public BlockState getBlockState(BlockPos pos) {
++        if (true) {
++            return this.getBlockStateFinal(pos.getX(), pos.getY(), pos.getZ());
++        }
++        // Paper end - Perf: Reduce instructions and provide final method
+         int x = pos.getX();
+         int y = pos.getY();
+         int z = pos.getZ();
+@@ -231,33 +_,54 @@
+         }
+     }
+ 
++    // Paper start - If loaded util
++    @Override
++    public final FluidState getFluidIfLoaded(BlockPos blockposition) {
++        return this.getFluidState(blockposition);
++    }
++
++    @Override
++    public final BlockState getBlockStateIfLoaded(BlockPos blockposition) {
++        return this.getBlockState(blockposition);
++    }
++    // Paper end
++
+     @Override
+     public FluidState getFluidState(BlockPos pos) {
+         return this.getFluidState(pos.getX(), pos.getY(), pos.getZ());
+     }
+ 
+     public FluidState getFluidState(int x, int y, int z) {
+-        try {
++        // try { // Paper start - Perf: Optimise Chunk#getFluid
+             int sectionIndex = this.getSectionIndex(y);
+             if (sectionIndex >= 0 && sectionIndex < this.sections.length) {
+                 LevelChunkSection levelChunkSection = this.sections[sectionIndex];
+                 if (!levelChunkSection.hasOnlyAir()) {
+-                    return levelChunkSection.getFluidState(x & 15, y & 15, z & 15);
++                    return levelChunkSection.states.get((y & 15) << 8 | (z & 15) << 4 | x & 15).getFluidState(); // Paper - Perf: Optimise Chunk#getFluid
+                 }
+             }
+ 
+             return Fluids.EMPTY.defaultFluidState();
++        /* // Paper - Perf: Optimise Chunk#getFluid
+         } catch (Throwable var7) {
+             CrashReport crashReport = CrashReport.forThrowable(var7, "Getting fluid state");
+             CrashReportCategory crashReportCategory = crashReport.addCategory("Block being got");
+             crashReportCategory.setDetail("Location", () -> CrashReportCategory.formatLocation(this, x, y, z));
+             throw new ReportedException(crashReport);
+         }
++        */ // Paper - Perf: Optimise Chunk#getFluid
+     }
+ 
+     @Nullable
+     @Override
+     public BlockState setBlockState(BlockPos pos, BlockState state, boolean isMoving) {
++// CraftBukkit start
++        return this.setBlockState(pos, state, isMoving, true);
++    }
++
++    @Nullable
++    public BlockState setBlockState(BlockPos pos, BlockState state, boolean isMoving, boolean doPlace) {
++        // CraftBukkit end
+         int y = pos.getY();
+         LevelChunkSection section = this.getSection(this.getSectionIndex(y));
+         boolean hasOnlyAir = section.hasOnlyAir();
+@@ -292,7 +_,7 @@
+                 }
+ 
+                 boolean hasBlockEntity = blockState.hasBlockEntity();
+-                if (!this.level.isClientSide) {
++                if (!this.level.isClientSide && !this.level.isBlockPlaceCancelled) { // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent
+                     blockState.onRemove(this.level, pos, state, isMoving);
+                 } else if (!blockState.is(block) && hasBlockEntity) {
+                     this.removeBlockEntity(pos);
+@@ -301,7 +_,7 @@
+                 if (!section.getBlockState(i, i1, i2).is(block)) {
+                     return null;
+                 } else {
+-                    if (!this.level.isClientSide) {
++                    if (!this.level.isClientSide && doPlace && (!this.level.captureBlockStates || block instanceof net.minecraft.world.level.block.BaseEntityBlock)) { // CraftBukkit - Don't place while processing the BlockPlaceEvent, unless it's a BlockContainer. Prevents blocks such as TNT from activating when cancelled.
+                         state.onPlace(this.level, pos, blockState, isMoving);
+                     }
+ 
+@@ -355,7 +_,12 @@
+ 
+     @Nullable
+     public BlockEntity getBlockEntity(BlockPos pos, LevelChunk.EntityCreationType creationType) {
+-        BlockEntity blockEntity = this.blockEntities.get(pos);
++        // CraftBukkit start
++        BlockEntity blockEntity = this.level.capturedTileEntities.get(pos);
++        if (blockEntity == null) {
++            blockEntity = this.blockEntities.get(pos);
++        }
++        // CraftBukkit end
+         if (blockEntity == null) {
+             CompoundTag compoundTag = this.pendingBlockEntities.remove(pos);
+             if (compoundTag != null) {
+@@ -409,7 +_,13 @@
+         BlockPos blockPos = blockEntity.getBlockPos();
+         BlockState blockState = this.getBlockState(blockPos);
+         if (!blockState.hasBlockEntity()) {
+-            LOGGER.warn("Trying to set block entity {} at position {}, but state {} does not allow it", blockEntity, blockPos, blockState);
++            // Paper start - ServerExceptionEvent
++            com.destroystokyo.paper.exception.ServerInternalException e = new com.destroystokyo.paper.exception.ServerInternalException(
++                "Trying to set block entity %s at position %s, but state %s does not allow it".formatted(blockEntity, blockPos, blockState)
++            );
++            e.printStackTrace();
++            com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(e);
++            // Paper end - ServerExceptionEvent
+         } else {
+             BlockState blockState1 = blockEntity.getBlockState();
+             if (blockState != blockState1) {
+@@ -457,6 +_,11 @@
+     public void removeBlockEntity(BlockPos pos) {
+         if (this.isInLevel()) {
+             BlockEntity blockEntity = this.blockEntities.remove(pos);
++            // CraftBukkit start - SPIGOT-5561: Also remove from pending map
++            if (!this.pendingBlockEntities.isEmpty()) {
++                this.pendingBlockEntities.remove(pos);
++            }
++            // CraftBukkit end
+             if (blockEntity != null) {
+                 if (this.level instanceof ServerLevel serverLevel) {
+                     this.removeGameEventListener(blockEntity, serverLevel);
+@@ -499,6 +_,65 @@
+         }
+     }
+ 
++    // CraftBukkit start
++    public void loadCallback() {
++        // Paper start
++        this.loadedTicketLevel = true;
++        // Paper end
++        org.bukkit.Server server = this.level.getCraftServer();
++        this.level.getChunkSource().addLoadedChunk(this); // Paper
++        if (server != null) {
++            /*
++             * If it's a new world, the first few chunks are generated inside
++             * the World constructor. We can't reliably alter that, so we have
++             * no way of creating a CraftWorld/CraftServer at that point.
++             */
++            org.bukkit.Chunk bukkitChunk = new org.bukkit.craftbukkit.CraftChunk(this);
++            server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkLoadEvent(bukkitChunk, this.needsDecoration));
++
++            if (this.needsDecoration) {
++                this.needsDecoration = false;
++                java.util.Random random = new java.util.Random();
++                random.setSeed(this.level.getSeed());
++                long xRand = random.nextLong() / 2L * 2L + 1L;
++                long zRand = random.nextLong() / 2L * 2L + 1L;
++                random.setSeed((long) this.chunkPos.x * xRand + (long) this.chunkPos.z * zRand ^ this.level.getSeed());
++
++                org.bukkit.World world = this.level.getWorld();
++                if (world != null) {
++                    this.level.populating = true;
++                    try {
++                        for (org.bukkit.generator.BlockPopulator populator : world.getPopulators()) {
++                            populator.populate(world, random, bukkitChunk);
++                        }
++                    } finally {
++                        this.level.populating = false;
++                    }
++                }
++                server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkPopulateEvent(bukkitChunk));
++            }
++        }
++    }
++
++    public void unloadCallback() {
++        org.bukkit.Server server = this.level.getCraftServer();
++        org.bukkit.Chunk bukkitChunk = new org.bukkit.craftbukkit.CraftChunk(this);
++        org.bukkit.event.world.ChunkUnloadEvent unloadEvent = new org.bukkit.event.world.ChunkUnloadEvent(bukkitChunk, this.isUnsaved());
++        server.getPluginManager().callEvent(unloadEvent);
++        // note: saving can be prevented, but not forced if no saving is actually required
++        this.mustNotSave = !unloadEvent.isSaveChunk();
++        this.level.getChunkSource().removeLoadedChunk(this); // Paper
++        // Paper start
++        this.loadedTicketLevel = false;
++        // Paper end
++    }
++
++    @Override
++    public boolean isUnsaved() {
++        return super.isUnsaved() && !this.mustNotSave;
++    }
++    // CraftBukkit end
++
+     public boolean isEmpty() {
+         return false;
+     }
+@@ -711,23 +_,24 @@
+                         if (this.blockEntity.getType().isValid(blockState)) {
+                             this.ticker.tick(LevelChunk.this.level, this.blockEntity.getBlockPos(), blockState, this.blockEntity);
+                             this.loggedInvalidBlockState = false;
+-                        } else if (!this.loggedInvalidBlockState) {
+-                            this.loggedInvalidBlockState = true;
+-                            LevelChunk.LOGGER
+-                                .warn(
+-                                    "Block entity {} @ {} state {} invalid for ticking:",
+-                                    LogUtils.defer(this::getType),
+-                                    LogUtils.defer(this::getPos),
+-                                    blockState
+-                                );
++                        // Paper start - Remove the Block Entity if it's invalid
++                        } else {
++                            LevelChunk.this.removeBlockEntity(this.getPos());
++                            if (!this.loggedInvalidBlockState) {
++                                this.loggedInvalidBlockState = true;
++                                LevelChunk.LOGGER.warn("Block entity {} @ {} state {} invalid for ticking:", LogUtils.defer(this::getType), LogUtils.defer(this::getPos), blockState);
++                            }
++                            // Paper end - Remove the Block Entity if it's invalid
+                         }
+ 
+                         profilerFiller.pop();
+                     } catch (Throwable var5) {
+-                        CrashReport crashReport = CrashReport.forThrowable(var5, "Ticking block entity");
+-                        CrashReportCategory crashReportCategory = crashReport.addCategory("Block entity being ticked");
+-                        this.blockEntity.fillCrashReportCategory(crashReportCategory);
+-                        throw new ReportedException(crashReport);
++                        // Paper start - Prevent block entity and entity crashes
++                        final String msg = String.format("BlockEntity threw exception at %s:%s,%s,%s", LevelChunk.this.getLevel().getWorld().getName(), this.getPos().getX(), this.getPos().getY(), this.getPos().getZ());
++                        net.minecraft.server.MinecraftServer.LOGGER.error(msg, var5);
++                        net.minecraft.world.level.chunk.LevelChunk.this.level.getCraftServer().getPluginManager().callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerInternalException(msg, var5))); // Paper - ServerExceptionEvent
++                        LevelChunk.this.removeBlockEntity(this.getPos());
++                        // Paper end - Prevent block entity and entity crashes
+                     }
+                 }
+             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/LevelChunkSection.java.patch b/paper-server/patches/sources/net/minecraft/world/level/chunk/LevelChunkSection.java.patch
similarity index 60%
rename from paper-server/patches/unapplied/net/minecraft/world/level/chunk/LevelChunkSection.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/chunk/LevelChunkSection.java.patch
index f6007d4aea..f4a6ffa6fb 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/LevelChunkSection.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/chunk/LevelChunkSection.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/level/chunk/LevelChunkSection.java
 +++ b/net/minecraft/world/level/chunk/LevelChunkSection.java
-@@ -19,11 +19,11 @@
+@@ -18,11 +_,11 @@
      public static final int SECTION_HEIGHT = 16;
      public static final int SECTION_SIZE = 4096;
      public static final int BIOME_CONTAINER_BITS = 2;
@@ -14,38 +14,33 @@
  
      private LevelChunkSection(LevelChunkSection section) {
          this.nonEmptyBlockCount = section.nonEmptyBlockCount;
-@@ -33,9 +33,9 @@
+@@ -32,7 +_,7 @@
          this.biomes = section.biomes.copy();
      }
  
--    public LevelChunkSection(PalettedContainer<BlockState> blockStateContainer, PalettedContainerRO<Holder<Biome>> biomeContainer) {
--        this.states = blockStateContainer;
--        this.biomes = biomeContainer;
-+    public LevelChunkSection(PalettedContainer<BlockState> datapaletteblock, PalettedContainer<Holder<Biome>> palettedcontainerro) { // CraftBukkit - read/write
-+        this.states = datapaletteblock;
-+        this.biomes = palettedcontainerro;
+-    public LevelChunkSection(PalettedContainer<BlockState> states, PalettedContainerRO<Holder<Biome>> biomes) {
++    public LevelChunkSection(PalettedContainer<BlockState> states, PalettedContainer<Holder<Biome>> biomes) { // CraftBukkit - read/write
+         this.states = states;
+         this.biomes = biomes;
          this.recalcBlockCounts();
-     }
- 
-@@ -49,7 +49,7 @@
+@@ -48,7 +_,7 @@
      }
  
      public FluidState getFluidState(int x, int y, int z) {
--        return ((BlockState) this.states.get(x, y, z)).getFluidState();
+-        return this.states.get(x, y, z).getFluidState();
 +        return this.states.get(x, y, z).getFluidState(); // Paper - Perf: Optimise Chunk#getFluid; diff on change - we expect this to be effectively just getType(x, y, z).getFluid(). If this changes we need to check other patches that use IBlockData#getFluid.
      }
  
      public void acquire() {
-@@ -196,6 +196,12 @@
-         return (Holder) this.biomes.get(x, y, z);
+@@ -185,6 +_,11 @@
+     public Holder<Biome> getNoiseBiome(int x, int y, int z) {
+         return this.biomes.get(x, y, z);
      }
- 
 +    // CraftBukkit start
 +    public void setBiome(int i, int j, int k, Holder<Biome> biome) {
 +        this.biomes.set(i, j, k, biome);
 +    }
 +    // CraftBukkit end
-+
-     public void fillBiomesFromNoise(BiomeResolver biomeSupplier, Climate.Sampler sampler, int x, int y, int z) {
-         PalettedContainer<Holder<Biome>> datapaletteblock = this.biomes.recreate();
-         boolean flag = true;
+ 
+     public void fillBiomesFromNoise(BiomeResolver biomeResolver, Climate.Sampler climateSampler, int x, int y, int z) {
+         PalettedContainer<Holder<Biome>> palettedContainer = this.biomes.recreate();
diff --git a/paper-server/patches/sources/net/minecraft/world/level/chunk/PalettedContainer.java.patch b/paper-server/patches/sources/net/minecraft/world/level/chunk/PalettedContainer.java.patch
new file mode 100644
index 0000000000..360faa2856
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/chunk/PalettedContainer.java.patch
@@ -0,0 +1,74 @@
+--- a/net/minecraft/world/level/chunk/PalettedContainer.java
++++ b/net/minecraft/world/level/chunk/PalettedContainer.java
+@@ -30,14 +_,14 @@
+     public final IdMap<T> registry;
+     private volatile PalettedContainer.Data<T> data;
+     private final PalettedContainer.Strategy strategy;
+-    private final ThreadingDetector threadingDetector = new ThreadingDetector("PalettedContainer");
++    //private final ThreadingDetector threadingDetector = new ThreadingDetector("PalettedContainer"); // Paper - unused
+ 
+     public void acquire() {
+-        this.threadingDetector.checkAndLock();
++        // this.threadingDetector.checkAndLock(); // Paper - disable this - use proper synchronization
+     }
+ 
+     public void release() {
+-        this.threadingDetector.checkAndUnlock();
++        // this.threadingDetector.checkAndUnlock(); // Paper - disable this - use proper synchronization
+     }
+ 
+     public static <T> Codec<PalettedContainer<T>> codecRW(IdMap<T> registry, Codec<T> codec, PalettedContainer.Strategy strategy, T value) {
+@@ -99,7 +_,7 @@
+     }
+ 
+     @Override
+-    public int onResize(int bits, T objectAdded) {
++    public synchronized int onResize(int bits, T objectAdded) { // Paper - synchronize
+         PalettedContainer.Data<T> data = this.data;
+         PalettedContainer.Data<T> data1 = this.createOrReuseData(data, bits);
+         data1.copyFrom(data.palette, data.storage);
+@@ -107,7 +_,7 @@
+         return data1.palette.idFor(objectAdded);
+     }
+ 
+-    public T getAndSet(int x, int y, int z, T state) {
++    public synchronized T getAndSet(int x, int y, int z, T state) { // Paper - synchronize
+         this.acquire();
+ 
+         Object var5;
+@@ -130,7 +_,7 @@
+         return this.data.palette.valueFor(andSet);
+     }
+ 
+-    public void set(int x, int y, int z, T state) {
++    public synchronized void set(int x, int y, int z, T state) { // Paper - synchronize
+         this.acquire();
+ 
+         try {
+@@ -163,7 +_,7 @@
+         set.forEach(id -> consumer.accept(palette.valueFor(id)));
+     }
+ 
+-    public void read(FriendlyByteBuf buffer) {
++    public synchronized void read(FriendlyByteBuf buffer) { // Paper - synchronize
+         this.acquire();
+ 
+         try {
+@@ -178,7 +_,7 @@
+     }
+ 
+     @Override
+-    public void write(FriendlyByteBuf buffer) {
++    public synchronized void write(FriendlyByteBuf buffer) { // Paper - synchronize
+         this.acquire();
+ 
+         try {
+@@ -226,7 +_,7 @@
+     }
+ 
+     @Override
+-    public PalettedContainerRO.PackedData<T> pack(IdMap<T> registry, PalettedContainer.Strategy strategy) {
++    public synchronized PalettedContainerRO.PackedData<T> pack(IdMap<T> registry, PalettedContainer.Strategy strategy) { // Paper - synchronize
+         this.acquire();
+ 
+         PalettedContainerRO.PackedData var12;
diff --git a/paper-server/patches/sources/net/minecraft/world/level/chunk/ProtoChunk.java.patch b/paper-server/patches/sources/net/minecraft/world/level/chunk/ProtoChunk.java.patch
new file mode 100644
index 0000000000..d0111479a1
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/chunk/ProtoChunk.java.patch
@@ -0,0 +1,38 @@
+--- a/net/minecraft/world/level/chunk/ProtoChunk.java
++++ b/net/minecraft/world/level/chunk/ProtoChunk.java
+@@ -85,14 +_,32 @@
+         return new ChunkAccess.PackedTicks(this.blockTicks.pack(gametime), this.fluidTicks.pack(gametime));
+     }
+ 
++    // Paper start - If loaded util
++    @Override
++    public final FluidState getFluidIfLoaded(BlockPos blockposition) {
++        return this.getFluidState(blockposition);
++    }
++
++    @Override
++    public final BlockState getBlockStateIfLoaded(BlockPos blockposition) {
++        return this.getBlockState(blockposition);
++    }
++    // Paper end
++
+     @Override
+     public BlockState getBlockState(BlockPos pos) {
+-        int y = pos.getY();
++        // Paper start
++        return getBlockState(pos.getX(), pos.getY(), pos.getZ());
++    }
++    public BlockState getBlockState(final int x, final int y, final int z) {
++        // Paper end
+         if (this.isOutsideBuildHeight(y)) {
+             return Blocks.VOID_AIR.defaultBlockState();
+         } else {
+-            LevelChunkSection section = this.getSection(this.getSectionIndex(y));
+-            return section.hasOnlyAir() ? Blocks.AIR.defaultBlockState() : section.getBlockState(pos.getX() & 15, y & 15, pos.getZ() & 15);
++            // Paper start
++            LevelChunkSection section = this.getSections()[this.getSectionIndex(y)];
++            return section.hasOnlyAir() ? Blocks.AIR.defaultBlockState() : section.getBlockState(x & 15, y & 15, z & 15);
++            // Paper end
+         }
+     }
+ 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/UpgradeData.java.patch b/paper-server/patches/sources/net/minecraft/world/level/chunk/UpgradeData.java.patch
similarity index 75%
rename from paper-server/patches/unapplied/net/minecraft/world/level/chunk/UpgradeData.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/chunk/UpgradeData.java.patch
index 902ea36be8..8628e64dd8 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/UpgradeData.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/chunk/UpgradeData.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/level/chunk/UpgradeData.java
 +++ b/net/minecraft/world/level/chunk/UpgradeData.java
-@@ -113,12 +113,36 @@
+@@ -113,6 +_,24 @@
          }
      }
  
@@ -22,26 +22,25 @@
 +        }
 +    }
 +    // Paper end - filter out relocated neighbour ticks
-+
      public void upgrade(LevelChunk chunk) {
          this.upgradeInside(chunk);
  
-         for (Direction8 direction8 : DIRECTIONS) {
+@@ -120,6 +_,10 @@
              upgradeSides(chunk, direction8);
          }
-+
+ 
 +        // Paper start - filter out relocated neighbour ticks
 +        filterTickList(chunk.locX, chunk.locZ, this.neighborBlockTicks);
 +        filterTickList(chunk.locX, chunk.locZ, this.neighborFluidTicks);
 +        // Paper end - filter out relocated neighbour ticks
- 
          Level level = chunk.getLevel();
-         this.neighborBlockTicks.forEach(tick -> {
-@@ -129,6 +153,7 @@
-             Fluid fluid = tick.type() == Fluids.EMPTY ? level.getFluidState(tick.pos()).getType() : tick.type();
-             level.scheduleTick(tick.pos(), fluid, tick.delay(), tick.priority());
+         this.neighborBlockTicks.forEach(blockTicker -> {
+             Block block = blockTicker.type() == Blocks.AIR ? level.getBlockState(blockTicker.pos()).getBlock() : blockTicker.type();
+@@ -129,6 +_,7 @@
+             Fluid fluid = fluidTicker.type() == Fluids.EMPTY ? level.getFluidState(fluidTicker.pos()).getType() : fluidTicker.type();
+             level.scheduleTick(fluidTicker.pos(), fluid, fluidTicker.delay(), fluidTicker.priority());
          });
 +        UpgradeData.BlockFixers.values(); // Paper - force the class init so that we don't access CHUNKY_FIXERS before all BlockFixers are initialised
-         CHUNKY_FIXERS.forEach(logic -> logic.processChunk(level));
+         CHUNKY_FIXERS.forEach(fixers -> fixers.processChunk(level));
      }
  
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java.patch b/paper-server/patches/sources/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java.patch
similarity index 58%
rename from paper-server/patches/unapplied/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java.patch
index 9f0303442e..30e279c283 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java.patch
@@ -1,46 +1,38 @@
 --- a/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
 +++ b/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
-@@ -36,7 +36,7 @@
-     static CompletableFuture<ChunkAccess> generateStructureStarts(WorldGenContext context, ChunkStep step, StaticCache2D<GenerationChunkHolder> chunks, ChunkAccess chunk) {
-         ServerLevel worldserver = context.level();
- 
--        if (worldserver.getServer().getWorldData().worldGenOptions().generateStructures()) {
-+        if (worldserver.serverLevelData.worldGenOptions().generateStructures()) { // CraftBukkit
-             context.generator().createStructures(worldserver.registryAccess(), worldserver.getChunkSource().getGeneratorState(), worldserver.structureManager(), chunk, context.structureManager(), worldserver.dimension());
-         }
- 
-@@ -151,7 +151,7 @@
-             if (protochunk instanceof ImposterProtoChunk protochunkextension) {
-                 chunk1 = protochunkextension.getWrapped();
-             } else {
--                chunk1 = new LevelChunk(worldserver, protochunk, (chunk1) -> {
-+                chunk1 = new LevelChunk(worldserver, protochunk, ($) -> { // Paper - decompile fix
-                     ChunkStatusTasks.postLoadProtoChunk(worldserver, protochunk.getEntities());
-                 });
-                 generationchunkholder.replaceProtoChunk(new ImposterProtoChunk(chunk1, false));
-@@ -168,10 +168,61 @@
-         }, context.mainThreadExecutor());
+@@ -35,7 +_,7 @@
+         WorldGenContext worldGenContext, ChunkStep step, StaticCache2D<GenerationChunkHolder> cache, ChunkAccess chunk
+     ) {
+         ServerLevel serverLevel = worldGenContext.level();
+-        if (serverLevel.getServer().getWorldData().worldGenOptions().generateStructures()) {
++        if (serverLevel.serverLevelData.worldGenOptions().generateStructures()) { // CraftBukkit
+             worldGenContext.generator()
+                 .createStructures(
+                     serverLevel.registryAccess(),
+@@ -196,9 +_,60 @@
+         }, worldGenContext.mainThreadExecutor());
      }
  
--    private static void postLoadProtoChunk(ServerLevel world, List<CompoundTag> entities) {
-+    public static void postLoadProtoChunk(ServerLevel world, List<CompoundTag> entities) { // Paper - public
-         if (!entities.isEmpty()) {
--            world.addWorldGenChunkEntities(EntityType.loadEntitiesRecursive(entities, world, EntitySpawnReason.LOAD));
+-    private static void postLoadProtoChunk(ServerLevel level, List<CompoundTag> entityTags) {
++    public static void postLoadProtoChunk(ServerLevel level, List<CompoundTag> entityTags) { // Paper - public
+         if (!entityTags.isEmpty()) {
+-            level.addWorldGenChunkEntities(EntityType.loadEntitiesRecursive(entityTags, level, EntitySpawnReason.LOAD));
+-        }
+-    }
 +            // CraftBukkit start - these are spawned serialized (DefinedStructure) and we don't call an add event below at the moment due to ordering complexities
-+            world.addWorldGenChunkEntities(EntityType.loadEntitiesRecursive(entities, world, EntitySpawnReason.LOAD).filter((entity) -> {
++            level.addWorldGenChunkEntities(EntityType.loadEntitiesRecursive(entityTags, level, EntitySpawnReason.LOAD).filter((entity) -> {
 +                boolean needsRemoval = false;
-+                net.minecraft.server.dedicated.DedicatedServer server = world.getCraftServer().getServer();
-+                if (!world.getChunkSource().spawnFriendlies && (entity instanceof net.minecraft.world.entity.animal.Animal || entity instanceof net.minecraft.world.entity.animal.WaterAnimal)) {
++                net.minecraft.server.dedicated.DedicatedServer server = level.getCraftServer().getServer();
++                if (!level.getChunkSource().spawnFriendlies && (entity instanceof net.minecraft.world.entity.animal.Animal || entity instanceof net.minecraft.world.entity.animal.WaterAnimal)) {
 +                    entity.discard(null); // CraftBukkit - add Bukkit remove cause
 +                    needsRemoval = true;
 +                }
-+                checkDupeUUID(world, entity); // Paper - duplicate uuid resolving
++                checkDupeUUID(level, entity); // Paper - duplicate uuid resolving
 +                return !needsRemoval;
 +            }));
 +            // CraftBukkit end
-         }
- 
-     }
++        }
++    }
 +
 +    // Paper start - duplicate uuid resolving
 +    // rets true if to prevent the entity from being added
@@ -58,7 +50,7 @@
 +        }
 +
 +        if (mode == io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.SAFE_REGEN && other != null && !other.isRemoved()
-+            && Objects.equals(other.getEncodeId(), entity.getEncodeId())
++            && java.util.Objects.equals(other.getEncodeId(), entity.getEncodeId())
 +            && entity.getBukkitEntity().getLocation().distance(other.getBukkitEntity().getLocation()) < level.paperConfig().entities.spawning.duplicateUuid.safeRegenDeleteRange
 +        ) {
 +            entity.discard(null);
diff --git a/paper-server/patches/sources/net/minecraft/world/level/chunk/storage/ChunkStorage.java.patch b/paper-server/patches/sources/net/minecraft/world/level/chunk/storage/ChunkStorage.java.patch
new file mode 100644
index 0000000000..9188002f34
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/chunk/storage/ChunkStorage.java.patch
@@ -0,0 +1,130 @@
+--- a/net/minecraft/world/level/chunk/storage/ChunkStorage.java
++++ b/net/minecraft/world/level/chunk/storage/ChunkStorage.java
+@@ -38,17 +_,63 @@
+         return this.worker.isOldChunkAround(pos, radius);
+     }
+ 
++    // CraftBukkit start
++    private boolean check(net.minecraft.server.level.ServerChunkCache cps, int x, int z) {
++        if (true) return true; // Paper - Perf: this isn't even needed anymore, light is purged updating to 1.14+, why are we holding up the conversion process reading chunk data off disk - return true, we need to set light populated to true so the converter recognizes the chunk as being "full"
++        ChunkPos pos = new ChunkPos(x, z);
++        if (cps != null) {
++            com.google.common.base.Preconditions.checkState(org.bukkit.Bukkit.isPrimaryThread(), "primary thread");
++            if (cps.hasChunk(x, z)) {
++                return true;
++            }
++        }
++
++        CompoundTag nbt;
++        try {
++            nbt = this.read(pos).get().orElse(null);
++        } catch (InterruptedException | java.util.concurrent.ExecutionException ex) {
++            throw new RuntimeException(ex);
++        }
++        if (nbt != null) {
++            CompoundTag level = nbt.getCompound("Level");
++            if (level.getBoolean("TerrainPopulated")) {
++                return true;
++            }
++
++            net.minecraft.world.level.chunk.status.ChunkStatus status = net.minecraft.world.level.chunk.status.ChunkStatus.byName(level.getString("Status"));
++            if (status != null && status.isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.FEATURES)) {
++                return true;
++            }
++        }
++
++        return false;
++    }
++
+     public CompoundTag upgradeChunkTag(
+-        ResourceKey<Level> levelKey,
++        ResourceKey<net.minecraft.world.level.dimension.LevelStem> levelKey,
+         Supplier<DimensionDataStorage> storage,
+         CompoundTag chunkData,
+-        Optional<ResourceKey<MapCodec<? extends ChunkGenerator>>> chunkGeneratorKey
++        Optional<ResourceKey<MapCodec<? extends ChunkGenerator>>> chunkGeneratorKey,
++        ChunkPos pos,
++        @Nullable net.minecraft.world.level.LevelAccessor generatoraccess
++        // CraftBukkit end
+     ) {
+         int version = getVersion(chunkData);
+         if (version == SharedConstants.getCurrentVersion().getDataVersion().getVersion()) {
+             return chunkData;
+         } else {
+             try {
++                // CraftBukkit start
++                if (version < 1466) {
++                    CompoundTag level = chunkData.getCompound("Level");
++                    if (level.getBoolean("TerrainPopulated") && !level.getBoolean("LightPopulated")) {
++                        net.minecraft.server.level.ServerChunkCache cps = (generatoraccess == null) ? null : ((net.minecraft.server.level.ServerLevel) generatoraccess).getChunkSource();
++                        if (this.check(cps, pos.x - 1, pos.z) && this.check(cps, pos.x - 1, pos.z - 1) && this.check(cps, pos.x, pos.z - 1)) {
++                            level.putBoolean("LightPopulated", true);
++                        }
++                    }
++                }
++                // CraftBukkit end
+                 if (version < 1493) {
+                     chunkData = DataFixTypes.CHUNK.update(this.fixerUpper, chunkData, version, 1493);
+                     if (chunkData.getCompound("Level").getBoolean("hasLegacyStructureData")) {
+@@ -57,8 +_,22 @@
+                     }
+                 }
+ 
++                // Spigot start - SPIGOT-6806: Quick and dirty way to prevent below zero generation in old chunks, by setting the status to heightmap instead of empty
++                boolean stopBelowZero = false;
++                boolean belowZeroGenerationInExistingChunks = (generatoraccess != null) ? ((net.minecraft.server.level.ServerLevel) generatoraccess).spigotConfig.belowZeroGenerationInExistingChunks : org.spigotmc.SpigotConfig.belowZeroGenerationInExistingChunks;
++
++                if (version <= 2730 && !belowZeroGenerationInExistingChunks) {
++                    stopBelowZero = "full".equals(chunkData.getCompound("Level").getString("Status"));
++                }
++                // Spigot end
++
+                 injectDatafixingContext(chunkData, levelKey, chunkGeneratorKey);
+                 chunkData = DataFixTypes.CHUNK.updateToCurrentVersion(this.fixerUpper, chunkData, Math.max(1493, version));
++                // Spigot start
++                if (stopBelowZero) {
++                    chunkData.putString("Status", net.minecraft.core.registries.BuiltInRegistries.CHUNK_STATUS.getKey(net.minecraft.world.level.chunk.status.ChunkStatus.SPAWN).toString());
++                }
++                // Spigot end
+                 removeDatafixingContext(chunkData);
+                 NbtUtils.addCurrentDataVersion(chunkData);
+                 return chunkData;
+@@ -71,7 +_,7 @@
+         }
+     }
+ 
+-    private LegacyStructureDataHandler getLegacyStructureHandler(ResourceKey<Level> level, Supplier<DimensionDataStorage> storage) {
++    private LegacyStructureDataHandler getLegacyStructureHandler(ResourceKey<net.minecraft.world.level.dimension.LevelStem> level, Supplier<DimensionDataStorage> storage) { // CraftBukkit
+         LegacyStructureDataHandler legacyStructureDataHandler = this.legacyStructureHandler;
+         if (legacyStructureDataHandler == null) {
+             synchronized (this) {
+@@ -86,7 +_,7 @@
+     }
+ 
+     public static void injectDatafixingContext(
+-        CompoundTag chunkData, ResourceKey<Level> levelKey, Optional<ResourceKey<MapCodec<? extends ChunkGenerator>>> chunkGeneratorKey
++        CompoundTag chunkData, ResourceKey<net.minecraft.world.level.dimension.LevelStem> levelKey, Optional<ResourceKey<MapCodec<? extends ChunkGenerator>>> chunkGeneratorKey // CraftBukkit
+     ) {
+         CompoundTag compoundTag = new CompoundTag();
+         compoundTag.putString("dimension", levelKey.location().toString());
+@@ -107,8 +_,19 @@
+     }
+ 
+     public CompletableFuture<Void> write(ChunkPos pos, Supplier<CompoundTag> tagSupplier) {
++        // Paper start - guard against possible chunk pos desync
++        final Supplier<CompoundTag> guardedPosCheck = () -> {
++            CompoundTag nbt = tagSupplier.get();
++            if (nbt != null && !pos.equals(SerializableChunkData.getChunkCoordinate(nbt))) {
++                final String world = (ChunkStorage.this instanceof net.minecraft.server.level.ChunkMap) ? ((net.minecraft.server.level.ChunkMap) ChunkStorage.this).level.getWorld().getName() : null;
++                throw new IllegalArgumentException("Chunk coordinate and serialized data do not have matching coordinates, trying to serialize coordinate " + pos
++                    + " but compound says coordinate is " + SerializableChunkData.getChunkCoordinate(nbt) + (world == null ? " for an unknown world" : (" for world: " + world)));
++            }
++            return nbt;
++        };
++        // Paper end - guard against possible chunk pos desync
+         this.handleLegacyStructureIndex(pos);
+-        return this.worker.store(pos, tagSupplier);
++        return this.worker.store(pos, guardedPosCheck); // Paper - guard against possible chunk pos desync
+     }
+ 
+     protected void handleLegacyStructureIndex(ChunkPos chunkPos) {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/chunk/storage/RegionFile.java.patch b/paper-server/patches/sources/net/minecraft/world/level/chunk/storage/RegionFile.java.patch
new file mode 100644
index 0000000000..93e36df68d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/chunk/storage/RegionFile.java.patch
@@ -0,0 +1,68 @@
+--- a/net/minecraft/world/level/chunk/storage/RegionFile.java
++++ b/net/minecraft/world/level/chunk/storage/RegionFile.java
+@@ -46,7 +_,7 @@
+     protected final RegionBitmap usedSectors = new RegionBitmap();
+ 
+     public RegionFile(RegionStorageInfo info, Path path, Path externalFileDir, boolean sync) throws IOException {
+-        this(info, path, externalFileDir, RegionFileVersion.getSelected(), sync);
++        this(info, path, externalFileDir, RegionFileVersion.getCompressionFormat(), sync); // Paper - Configurable region compression format
+     }
+ 
+     public RegionFile(RegionStorageInfo info, Path path, Path externalFileDir, RegionFileVersion version, boolean sync) throws IOException {
+@@ -82,6 +_,14 @@
+                     if (i2 != 0) {
+                         int sectorNumber = getSectorNumber(i2);
+                         int numSectors = getNumSectors(i2);
++                        // Spigot start
++                        if (numSectors == 255) {
++                            // We're maxed out, so we need to read the proper length from the section
++                            ByteBuffer realLen = ByteBuffer.allocate(4);
++                            this.file.read(realLen, sectorNumber * 4096);
++                            numSectors = (realLen.getInt(0) + 4) / 4096 + 1;
++                        }
++                        // Spigot end
+                         if (sectorNumber < 2) {
+                             LOGGER.warn("Region file {} has invalid sector at index: {}; sector {} overlaps with header", path, i1, sectorNumber);
+                             this.offsets.put(i1, 0);
+@@ -117,6 +_,13 @@
+         } else {
+             int sectorNumber = getSectorNumber(offset);
+             int numSectors = getNumSectors(offset);
++            // Spigot start
++            if (numSectors == 255) {
++                ByteBuffer realLen = ByteBuffer.allocate(4);
++                this.file.read(realLen, sectorNumber * 4096);
++                numSectors = (realLen.getInt(0) + 4) / 4096 + 1;
++            }
++            // Spigot end
+             int i = numSectors * 4096;
+             ByteBuffer byteBuffer = ByteBuffer.allocate(i);
+             this.file.read(byteBuffer, sectorNumber * 4096);
+@@ -260,6 +_,7 @@
+                     return true;
+                 }
+             } catch (IOException var9) {
++                com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(var9); // Paper - ServerExceptionEvent
+                 return false;
+             }
+         }
+@@ -331,13 +_,18 @@
+         try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
+             chunkData.position(5);
+             fileChannel.write(chunkData);
++            // Paper start - ServerExceptionEvent
++        } catch (Throwable throwable) {
++            com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(throwable);
++            throw throwable;
++            // Paper end - ServerExceptionEvent
+         }
+ 
+         return () -> Files.move(path, externalChunkFile, StandardCopyOption.REPLACE_EXISTING);
+     }
+ 
+     private void writeHeader() throws IOException {
+-        this.header.position(0);
++        ((java.nio.Buffer) this.header).position(0); // CraftBukkit - decompile error
+         this.file.write(this.header, 0L);
+     }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/level/chunk/storage/RegionFileStorage.java.patch b/paper-server/patches/sources/net/minecraft/world/level/chunk/storage/RegionFileStorage.java.patch
new file mode 100644
index 0000000000..fadb8b6cdb
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/chunk/storage/RegionFileStorage.java.patch
@@ -0,0 +1,61 @@
+--- a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
++++ b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+@@ -28,18 +_,19 @@
+         this.info = info;
+     }
+ 
+-    private RegionFile getRegionFile(ChunkPos chunkPos) throws IOException {
++    @org.jetbrains.annotations.Contract("_, false -> !null") @Nullable private RegionFile getRegionFile(ChunkPos chunkPos, boolean existingOnly) throws IOException { // CraftBukkit
+         long packedChunkPos = ChunkPos.asLong(chunkPos.getRegionX(), chunkPos.getRegionZ());
+         RegionFile regionFile = this.regionCache.getAndMoveToFirst(packedChunkPos);
+         if (regionFile != null) {
+             return regionFile;
+         } else {
+-            if (this.regionCache.size() >= 256) {
++            if (this.regionCache.size() >= io.papermc.paper.configuration.GlobalConfiguration.get().misc.regionFileCacheSize) { // Paper - Sanitise RegionFileCache and make configurable
+                 this.regionCache.removeLast().close();
+             }
+ 
+             FileUtil.createDirectoriesSafe(this.folder);
+             Path path = this.folder.resolve("r." + chunkPos.getRegionX() + "." + chunkPos.getRegionZ() + ".mca");
++            if (existingOnly && !java.nio.file.Files.exists(path)) return null; // CraftBukkit
+             RegionFile regionFile1 = new RegionFile(this.info, path, this.folder, this.sync);
+             this.regionCache.putAndMoveToFirst(packedChunkPos, regionFile1);
+             return regionFile1;
+@@ -48,7 +_,12 @@
+ 
+     @Nullable
+     public CompoundTag read(ChunkPos chunkPos) throws IOException {
+-        RegionFile regionFile = this.getRegionFile(chunkPos);
++        // CraftBukkit start - SPIGOT-5680: There's no good reason to preemptively create files on read, save that for writing
++        RegionFile regionFile = this.getRegionFile(chunkPos, true);
++        if (regionFile == null) {
++            return null;
++        }
++        // CraftBukkit end
+ 
+         CompoundTag var4;
+         try (DataInputStream chunkDataInputStream = regionFile.getChunkDataInputStream(chunkPos)) {
+@@ -63,7 +_,12 @@
+     }
+ 
+     public void scanChunk(ChunkPos chunkPos, StreamTagVisitor visitor) throws IOException {
+-        RegionFile regionFile = this.getRegionFile(chunkPos);
++        // CraftBukkit start - SPIGOT-5680: There's no good reason to preemptively create files on read, save that for writing
++        RegionFile regionFile = this.getRegionFile(chunkPos, true);
++        if (regionFile == null) {
++            return;
++        }
++        // CraftBukkit end
+ 
+         try (DataInputStream chunkDataInputStream = regionFile.getChunkDataInputStream(chunkPos)) {
+             if (chunkDataInputStream != null) {
+@@ -73,7 +_,7 @@
+     }
+ 
+     protected void write(ChunkPos chunkPos, @Nullable CompoundTag chunkData) throws IOException {
+-        RegionFile regionFile = this.getRegionFile(chunkPos);
++        RegionFile regionFile = this.getRegionFile(chunkPos, false); // CraftBukkit
+         if (chunkData == null) {
+             regionFile.clear(chunkPos);
+         } else {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/storage/RegionFileVersion.java.patch b/paper-server/patches/sources/net/minecraft/world/level/chunk/storage/RegionFileVersion.java.patch
similarity index 93%
rename from paper-server/patches/unapplied/net/minecraft/world/level/chunk/storage/RegionFileVersion.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/chunk/storage/RegionFileVersion.java.patch
index 65bf070d12..ce1088103a 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/storage/RegionFileVersion.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/chunk/storage/RegionFileVersion.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/level/chunk/storage/RegionFileVersion.java
 +++ b/net/minecraft/world/level/chunk/storage/RegionFileVersion.java
-@@ -58,6 +58,15 @@
+@@ -61,6 +_,15 @@
      private final RegionFileVersion.StreamWrapper<InputStream> inputWrapper;
      private final RegionFileVersion.StreamWrapper<OutputStream> outputWrapper;
  
@@ -15,4 +15,4 @@
 +    // Paper end - Configurable region compression format
      private RegionFileVersion(
          int id,
-         @Nullable String name,
+         @Nullable String optionName,
diff --git a/paper-server/patches/sources/net/minecraft/world/level/chunk/storage/SerializableChunkData.java.patch b/paper-server/patches/sources/net/minecraft/world/level/chunk/storage/SerializableChunkData.java.patch
new file mode 100644
index 0000000000..56133882ab
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/chunk/storage/SerializableChunkData.java.patch
@@ -0,0 +1,177 @@
+--- a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
++++ b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
+@@ -91,6 +_,7 @@
+     List<CompoundTag> entities,
+     List<CompoundTag> blockEntities,
+     CompoundTag structureData
++    , @Nullable net.minecraft.nbt.Tag persistentDataContainer // CraftBukkit - persistentDataContainer
+ ) {
+     public static final Codec<PalettedContainer<BlockState>> BLOCK_STATE_CODEC = PalettedContainer.codecRW(
+         Block.BLOCK_STATE_REGISTRY, BlockState.CODEC, PalettedContainer.Strategy.SECTION_STATES, Blocks.AIR.defaultBlockState()
+@@ -107,12 +_,39 @@
+     public static final String BLOCK_LIGHT_TAG = "BlockLight";
+     public static final String SKY_LIGHT_TAG = "SkyLight";
+ 
++    // Paper start - guard against serializing mismatching coordinates
++    // TODO Note: This needs to be re-checked each update
++    public static ChunkPos getChunkCoordinate(final CompoundTag chunkData) {
++        final int dataVersion = ChunkStorage.getVersion(chunkData);
++        if (dataVersion < 2842) { // Level tag is removed after this version
++            final CompoundTag levelData = chunkData.getCompound("Level");
++            return new ChunkPos(levelData.getInt("xPos"), levelData.getInt("zPos"));
++        } else {
++            return new ChunkPos(chunkData.getInt("xPos"), chunkData.getInt("zPos"));
++        }
++    }
++    // Paper end - guard against serializing mismatching coordinates
++
++    // Paper start - Do not let the server load chunks from newer versions
++    private static final int CURRENT_DATA_VERSION = net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion();
++    private static final boolean JUST_CORRUPT_IT = Boolean.getBoolean("Paper.ignoreWorldDataVersion");
++    // Paper end - Do not let the server load chunks from newer versions
++
+     @Nullable
+     public static SerializableChunkData parse(LevelHeightAccessor levelHeightAccessor, RegistryAccess registries, CompoundTag tag) {
+         if (!tag.contains("Status", 8)) {
+             return null;
+         } else {
+-            ChunkPos chunkPos = new ChunkPos(tag.getInt("xPos"), tag.getInt("zPos"));
++            // Paper start - Do not let the server load chunks from newer versions
++            if (tag.contains("DataVersion", net.minecraft.nbt.Tag.TAG_ANY_NUMERIC)) {
++                final int dataVersion = tag.getInt("DataVersion");
++                if (!JUST_CORRUPT_IT && dataVersion > CURRENT_DATA_VERSION) {
++                    new RuntimeException("Server attempted to load chunk saved with newer version of minecraft! " + dataVersion + " > " + CURRENT_DATA_VERSION).printStackTrace();
++                    System.exit(1);
++                }
++            }
++            // Paper end - Do not let the server load chunks from newer versions
++            ChunkPos chunkPos = new ChunkPos(tag.getInt("xPos"), tag.getInt("zPos")); // Paper - guard against serializing mismatching coordinates; diff on change, see ChunkSerializer#getChunkCoordinate
+             long _long = tag.getLong("LastUpdate");
+             long _long1 = tag.getLong("InhabitedTime");
+             ChunkStatus chunkStatus = ChunkStatus.byName(tag.getString("Status"));
+@@ -181,7 +_,7 @@
+             ListTag list7 = tag.getList("sections", 10);
+             List<SerializableChunkData.SectionData> list8 = new ArrayList<>(list7.size());
+             Registry<Biome> registry = registries.lookupOrThrow(Registries.BIOME);
+-            Codec<PalettedContainerRO<Holder<Biome>>> codec = makeBiomeCodec(registry);
++            Codec<PalettedContainer<Holder<Biome>>> codec = makeBiomeCodecRW(registry); // CraftBukkit - read/write
+ 
+             for (int i2 = 0; i2 < list7.size(); i2++) {
+                 CompoundTag compound2 = list7.getCompound(i2);
+@@ -199,7 +_,7 @@
+                         );
+                     }
+ 
+-                    PalettedContainerRO<Holder<Biome>> palettedContainerRo;
++                    PalettedContainer<Holder<Biome>> palettedContainerRo; // CraftBukkit - read/write
+                     if (compound2.contains("biomes", 10)) {
+                         palettedContainerRo = codec.parse(NbtOps.INSTANCE, compound2.getCompound("biomes"))
+                             .promotePartial(string -> logErrors(chunkPos, _byte, string))
+@@ -239,6 +_,7 @@
+                 list5,
+                 list6,
+                 compound1
++                , tag.get("ChunkBukkitValues") // CraftBukkit - ChunkBukkitValues
+             );
+         }
+     }
+@@ -316,6 +_,12 @@
+             }
+         }
+ 
++        // CraftBukkit start - load chunk persistent data from nbt - SPIGOT-6814: Already load PDC here to account for 1.17 to 1.18 chunk upgrading.
++        if (this.persistentDataContainer instanceof CompoundTag) {
++            chunkAccess.persistentDataContainer.putAll((CompoundTag) this.persistentDataContainer);
++        }
++        // CraftBukkit end
++
+         chunkAccess.setLightCorrect(this.lightCorrect);
+         EnumSet<Heightmap.Types> set = EnumSet.noneOf(Heightmap.Types.class);
+ 
+@@ -346,6 +_,13 @@
+             }
+ 
+             for (CompoundTag compoundTag : this.blockEntities) {
++                // Paper start - do not read tile entities positioned outside the chunk
++                final BlockPos blockposition = BlockEntity.getPosFromTag(compoundTag);
++                if ((blockposition.getX() >> 4) != this.chunkPos.x || (blockposition.getZ() >> 4) != this.chunkPos.z) {
++                    LOGGER.warn("Tile entity serialized in chunk {} in world '{}' positioned at {} is located outside of the chunk", this.chunkPos, level.getWorld().getName(), blockposition);
++                    continue;
++                }
++                // Paper end - do not read tile entities positioned outside the chunk
+                 protoChunk1.setBlockEntityNbt(compoundTag);
+             }
+ 
+@@ -370,6 +_,12 @@
+         );
+     }
+ 
++    // CraftBukkit start - read/write
++    private static Codec<PalettedContainer<Holder<Biome>>> makeBiomeCodecRW(Registry<Biome> iregistry) {
++        return PalettedContainer.codecRW(iregistry.asHolderIdMap(), iregistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, iregistry.getOrThrow(Biomes.PLAINS));
++    }
++    // CraftBukkit end
++
+     public static SerializableChunkData copyOf(ServerLevel level, ChunkAccess chunk) {
+         if (!chunk.canBeSerialized()) {
+             throw new IllegalArgumentException("Chunk can't be serialized: " + chunk);
+@@ -428,6 +_,12 @@
+             CompoundTag compoundTag = packStructureData(
+                 StructurePieceSerializationContext.fromLevel(level), pos, chunk.getAllStarts(), chunk.getAllReferences()
+             );
++            // CraftBukkit start - store chunk persistent data in nbt
++            CompoundTag persistentDataContainer = null;
++            if (!chunk.persistentDataContainer.isEmpty()) { // SPIGOT-6814: Always save PDC to account for 1.17 to 1.18 chunk upgrading.
++                persistentDataContainer = chunk.persistentDataContainer.toTagCompound();
++            }
++            // CraftBukkit end
+             return new SerializableChunkData(
+                 level.registryAccess().lookupOrThrow(Registries.BIOME),
+                 pos,
+@@ -447,6 +_,7 @@
+                 list2,
+                 list1,
+                 compoundTag
++                , persistentDataContainer // CraftBukkit - persistentDataContainer
+             );
+         }
+     }
+@@ -525,6 +_,11 @@
+         this.heightmaps.forEach((types, longs) -> compoundTag2.put(types.getSerializationKey(), new LongArrayTag(longs)));
+         compoundTag.put("Heightmaps", compoundTag2);
+         compoundTag.put("structures", this.structureData);
++        // CraftBukkit start - store chunk persistent data in nbt
++        if (this.persistentDataContainer != null) { // SPIGOT-6814: Always save PDC to account for 1.17 to 1.18 chunk upgrading.
++            compoundTag.put("ChunkBukkitValues", this.persistentDataContainer);
++        }
++        // CraftBukkit end
+         return compoundTag;
+     }
+ 
+@@ -562,6 +_,13 @@
+                     chunk.setBlockEntityNbt(compoundTag);
+                 } else {
+                     BlockPos posFromTag = BlockEntity.getPosFromTag(compoundTag);
++                    // Paper start - do not read tile entities positioned outside the chunk
++                    ChunkPos chunkPos = chunk.getPos();
++                    if ((posFromTag.getX() >> 4) != chunkPos.x || (posFromTag.getZ() >> 4) != chunkPos.z) {
++                        LOGGER.warn("Tile entity serialized in chunk " + chunkPos + " in world '" + level.getWorld().getName() + "' positioned at " + posFromTag + " is located outside of the chunk");
++                        continue;
++                    }
++                    // Paper end - do not read tile entities positioned outside the chunk
+                     BlockEntity blockEntity = BlockEntity.loadStatic(posFromTag, chunk.getBlockState(posFromTag), compoundTag, level.registryAccess());
+                     if (blockEntity != null) {
+                         chunk.setBlockEntity(blockEntity);
+@@ -610,6 +_,12 @@
+             } else {
+                 StructureStart structureStart = StructureStart.loadStaticStart(context, compound.getCompound(string), seed);
+                 if (structureStart != null) {
++                    // CraftBukkit start - load persistent data for structure start
++                    net.minecraft.nbt.Tag persistentBase = compound.getCompound(string).get("StructureBukkitValues");
++                    if (persistentBase instanceof CompoundTag) {
++                        structureStart.persistentDataContainer.putAll((CompoundTag) persistentBase);
++                    }
++                    // CraftBukkit end
+                     map.put(structure, structureStart);
+                 }
+             }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/dimension/end/DragonRespawnAnimation.java.patch b/paper-server/patches/sources/net/minecraft/world/level/dimension/end/DragonRespawnAnimation.java.patch
new file mode 100644
index 0000000000..495ef0300d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/dimension/end/DragonRespawnAnimation.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/level/dimension/end/DragonRespawnAnimation.java
++++ b/net/minecraft/world/level/dimension/end/DragonRespawnAnimation.java
+@@ -90,7 +_,7 @@
+                 for (EndCrystal endCrystal : crystals) {
+                     endCrystal.setBeamTarget(null);
+                     level.explode(endCrystal, endCrystal.getX(), endCrystal.getY(), endCrystal.getZ(), 6.0F, Level.ExplosionInteraction.NONE);
+-                    endCrystal.discard();
++                    endCrystal.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.EXPLODE); // CraftBukkit - add Bukkit remove cause
+                 }
+             } else if (ticks >= 80) {
+                 level.levelEvent(3001, new BlockPos(0, 128, 0), 0);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/dimension/end/EndDragonFight.java.patch b/paper-server/patches/sources/net/minecraft/world/level/dimension/end/EndDragonFight.java.patch
similarity index 56%
rename from paper-server/patches/unapplied/net/minecraft/world/level/dimension/end/EndDragonFight.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/dimension/end/EndDragonFight.java.patch
index 5514be96df..c1082899a2 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/dimension/end/EndDragonFight.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/dimension/end/EndDragonFight.java.patch
@@ -1,63 +1,52 @@
 --- a/net/minecraft/world/level/dimension/end/EndDragonFight.java
 +++ b/net/minecraft/world/level/dimension/end/EndDragonFight.java
-@@ -74,6 +74,7 @@
+@@ -70,8 +_,9 @@
      private static final int GATEWAY_DISTANCE = 96;
      public static final int DRAGON_SPAWN_Y = 128;
      private final Predicate<Entity> validPlayer;
-+    private static final Component DEFAULT_BOSS_EVENT_NAME = Component.translatable("entity.minecraft.ender_dragon"); // Paper - ensure reset EnderDragon boss event name
-     public final ServerBossEvent dragonEvent;
-     public final ServerLevel level;
-     private final BlockPos origin;
-@@ -102,7 +103,7 @@
-     }
- 
-     public EndDragonFight(ServerLevel world, long gatewaysSeed, EndDragonFight.Data data, BlockPos origin) {
--        this.dragonEvent = (ServerBossEvent) (new ServerBossEvent(Component.translatable("entity.minecraft.ender_dragon"), BossEvent.BossBarColor.PINK, BossEvent.BossBarOverlay.PROGRESS)).setPlayBossMusic(true).setCreateWorldFog(true);
-+        this.dragonEvent = (ServerBossEvent) (new ServerBossEvent(DEFAULT_BOSS_EVENT_NAME, BossEvent.BossBarColor.PINK, BossEvent.BossBarOverlay.PROGRESS)).setPlayBossMusic(true).setCreateWorldFog(true); // Paper - ensure reset EnderDragon boss event name
-         this.gateways = new ObjectArrayList();
-         this.ticksSinceLastPlayerScan = 21;
-         this.skipArenaLoadedCheck = false;
-@@ -111,14 +112,20 @@
-         this.origin = origin;
-         this.validPlayer = EntitySelector.ENTITY_STILL_ALIVE.and(EntitySelector.withinDistance((double) origin.getX(), (double) (128 + origin.getY()), (double) origin.getZ(), 192.0D));
-         this.needsStateScanning = data.needsStateScanning;
--        this.dragonUUID = (UUID) data.dragonUUID.orElse((Object) null);
-+        this.dragonUUID = (UUID) data.dragonUUID.orElse(null); // CraftBukkit - decompile error
-         this.dragonKilled = data.dragonKilled;
-         this.previouslyKilled = data.previouslyKilled;
++    private static final Component DEFAULT_BOSS_EVENT_NAME = Component.translatable("entity.minecraft.ender_dragon"); // Paper - reset EnderDragon boss event name
+     public final ServerBossEvent dragonEvent = (ServerBossEvent)new ServerBossEvent(
+-            Component.translatable("entity.minecraft.ender_dragon"), BossEvent.BossBarColor.PINK, BossEvent.BossBarOverlay.PROGRESS
++            DEFAULT_BOSS_EVENT_NAME, BossEvent.BossBarColor.PINK, BossEvent.BossBarOverlay.PROGRESS // Paper - reset EnderDragon boss event name
+         )
+         .setPlayBossMusic(true)
+         .setCreateWorldFog(true);
+@@ -112,7 +_,12 @@
          if (data.isRespawning) {
              this.respawnStage = DragonRespawnAnimation.START;
          }
+-
 +        // Paper start - Add config to disable ender dragon legacy check
-+        if (data == EndDragonFight.Data.DEFAULT && !world.paperConfig().entities.spawning.scanForLegacyEnderDragon) {
++        if (data == EndDragonFight.Data.DEFAULT && !level.paperConfig().entities.spawning.scanForLegacyEnderDragon) {
 +            this.needsStateScanning = false;
 +            this.dragonKilled = true;
 +        }
 +        // Paper end - Add config to disable ender dragon legacy check
- 
--        this.portalLocation = (BlockPos) data.exitPortalLocation.orElse((Object) null);
-+        this.portalLocation = (BlockPos) data.exitPortalLocation.orElse(null); // CraftBukkit - decompile error
-         this.gateways.addAll((Collection) data.gateways.orElseGet(() -> {
-             ObjectArrayList<Integer> objectarraylist = new ObjectArrayList(ContiguousSet.create(Range.closedOpen(0, 20), DiscreteDomain.integers()));
- 
-@@ -206,9 +213,9 @@
-             this.dragonUUID = entityenderdragon.getUUID();
-             EndDragonFight.LOGGER.info("Found that there's a dragon still alive ({})", entityenderdragon);
+         this.portalLocation = data.exitPortalLocation.orElse(null);
+         this.gateways.addAll(data.gateways.orElseGet(() -> {
+             ObjectArrayList<Integer> list = new ObjectArrayList<>(ContiguousSet.create(Range.closedOpen(0, 20), DiscreteDomain.integers()));
+@@ -209,9 +_,9 @@
+             this.dragonUUID = enderDragon.getUUID();
+             LOGGER.info("Found that there's a dragon still alive ({})", enderDragon);
              this.dragonKilled = false;
--            if (!flag) {
-+            if (!flag && this.level.paperConfig().entities.behavior.shouldRemoveDragon) { // Paper - Toggle for removing existing dragon
-                 EndDragonFight.LOGGER.info("But we didn't have a portal, let's remove it.");
--                entityenderdragon.discard();
-+                entityenderdragon.discard(null); // CraftBukkit - add Bukkit remove cause
+-            if (!hasActiveExitPortal) {
++            if (!hasActiveExitPortal && this.level.paperConfig().entities.behavior.shouldRemoveDragon) { // Paper - Toggle for removing existing dragon
+                 LOGGER.info("But we didn't have a portal, let's remove it.");
+-                enderDragon.discard();
++                enderDragon.discard(null); // CraftBukkit - add Bukkit remove cause
                  this.dragonUUID = null;
              }
          }
-@@ -404,9 +411,23 @@
+@@ -366,12 +_,22 @@
              this.dragonEvent.setVisible(false);
              this.spawnExitPortal(true);
              this.spawnNewGateway();
 -            if (!this.previouslyKilled) {
--                this.level.setBlockAndUpdate(this.level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, EndPodiumFeature.getLocation(this.origin)), Blocks.DRAGON_EGG.defaultBlockState());
+-                this.level
+-                    .setBlockAndUpdate(
+-                        this.level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, EndPodiumFeature.getLocation(this.origin)),
+-                        Blocks.DRAGON_EGG.defaultBlockState()
+-                    );
 +            // Paper start - Add DragonEggFormEvent
 +            BlockPos eggPosition = this.level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, EndPodiumFeature.getLocation(this.origin));
 +            org.bukkit.craftbukkit.block.CraftBlockState eggState = org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(this.level, eggPosition);
@@ -70,20 +59,17 @@
 +                // this.level.setBlockAndUpdate(this.level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, EndPodiumFeature.getLocation(this.origin)), Blocks.DRAGON_EGG.defaultBlockState());
 +            } else {
 +                eggEvent.setCancelled(true);
-             }
++            }
 +            if (eggEvent.callEvent()) {
 +                eggEvent.getNewState().update(true);
 +                // Paper end - Add DragonEggFormEvent
-+            }
+             }
  
              this.previouslyKilled = true;
-             this.dragonKilled = true;
-@@ -419,7 +440,25 @@
-     @VisibleForTesting
-     public void removeAllGateways() {
+@@ -385,6 +_,24 @@
          this.gateways.clear();
-+    }
-+
+     }
+ 
 +    // Paper start - More DragonBattle API
 +    public boolean spawnNewGatewayIfPossible() {
 +        if (!this.gateways.isEmpty()) {
@@ -99,12 +85,13 @@
 +            endCrystals.addAll(this.level.getEntitiesOfClass(EndCrystal.class, spike.getTopBoundingBox()));
 +        }
 +        return endCrystals;
-     }
++    }
 +    // Paper end - More DragonBattle API
- 
++
      private void spawnNewGateway() {
          if (!this.gateways.isEmpty()) {
-@@ -449,6 +488,11 @@
+             int i = this.gateways.remove(this.gateways.size() - 1);
+@@ -413,6 +_,11 @@
              }
          }
  
@@ -113,18 +100,18 @@
 +            this.portalLocation = this.portalLocation.atY(this.level.getMinY() + 1);
 +        }
 +        // Paper end - Prevent "softlocked" exit portal generation
-         if (worldgenendtrophy.place(FeatureConfiguration.NONE, this.level, this.level.getChunkSource().getGenerator(), RandomSource.create(), this.portalLocation)) {
-             int i = Mth.positiveCeilDiv(4, 16);
- 
-@@ -469,6 +513,7 @@
-             entityenderdragon.moveTo((double) this.origin.getX(), (double) (128 + this.origin.getY()), (double) this.origin.getZ(), this.level.random.nextFloat() * 360.0F, 0.0F);
-             this.level.addFreshEntity(entityenderdragon);
-             this.dragonUUID = entityenderdragon.getUUID();
+         if (endPodiumFeature.place(
+             FeatureConfiguration.NONE, this.level, this.level.getChunkSource().getGenerator(), RandomSource.create(), this.portalLocation
+         )) {
+@@ -432,6 +_,7 @@
+             enderDragon.moveTo(this.origin.getX(), 128 + this.origin.getY(), this.origin.getZ(), this.level.random.nextFloat() * 360.0F, 0.0F);
+             this.level.addFreshEntity(enderDragon);
+             this.dragonUUID = enderDragon.getUUID();
 +            this.resetSpikeCrystals(); // Paper - Reset ender crystals on dragon spawn
          }
  
-         return entityenderdragon;
-@@ -480,6 +525,10 @@
+         return enderDragon;
+@@ -443,6 +_,10 @@
              this.ticksSinceDragonSeen = 0;
              if (dragon.hasCustomName()) {
                  this.dragonEvent.setName(dragon.getDisplayName());
@@ -134,8 +121,8 @@
 +                // Paper end - ensure reset EnderDragon boss event name
              }
          }
- 
-@@ -513,7 +562,13 @@
+     }
+@@ -470,7 +_,13 @@
          return this.previouslyKilled;
      }
  
@@ -148,64 +135,60 @@
 +    public boolean tryRespawn(@Nullable BlockPos placedEndCrystalPos) { // placedEndCrystalPos is null if the tryRespawn() call was not caused by a placed end crystal
 +        // Paper end - Perf: Do crystal-portal proximity check before entity lookup
          if (this.dragonKilled && this.respawnStage == null) {
-             BlockPos blockposition = this.portalLocation;
+             BlockPos blockPos = this.portalLocation;
+             if (blockPos == null) {
+@@ -485,6 +_,22 @@
  
-@@ -531,6 +586,22 @@
-                 blockposition = this.portalLocation;
+                 blockPos = this.portalLocation;
              }
- 
 +            // Paper start - Perf: Do crystal-portal proximity check before entity lookup
 +            if (placedEndCrystalPos != null) {
 +                // The end crystal must be 0 or 1 higher than the portal origin
-+                int dy = placedEndCrystalPos.getY() - blockposition.getY();
++                int dy = placedEndCrystalPos.getY() - blockPos.getY();
 +                if (dy != 0 && dy != 1) {
 +                    return false;
 +                }
 +                // The end crystal must be within a distance of 1 in one planar direction, and 3 in the other
-+                int dx = placedEndCrystalPos.getX() - blockposition.getX();
-+                int dz = placedEndCrystalPos.getZ() - blockposition.getZ();
++                int dx = placedEndCrystalPos.getX() - blockPos.getX();
++                int dz = placedEndCrystalPos.getZ() - blockPos.getZ();
 +                if (!((dx >= -1 && dx <= 1 && dz >= -3 && dz <= 3) || (dx >= -3 && dx <= 3 && dz >= -1 && dz <= 1))) {
 +                    return false;
 +                }
 +            }
 +            // Paper end - Perf: Do crystal-portal proximity check before entity lookup
 +
-             List<EndCrystal> list = Lists.newArrayList();
-             BlockPos blockposition1 = blockposition.above(1);
-             Iterator iterator = Direction.Plane.HORIZONTAL.iterator();
-@@ -540,19 +611,19 @@
-                 List<EndCrystal> list1 = this.level.getEntitiesOfClass(EndCrystal.class, new AABB(blockposition1.relative(enumdirection, 2)));
  
-                 if (list1.isEmpty()) {
+             List<EndCrystal> list = Lists.newArrayList();
+             BlockPos blockPos1 = blockPos.above(1);
+@@ -492,18 +_,19 @@
+             for (Direction direction : Direction.Plane.HORIZONTAL) {
+                 List<EndCrystal> entitiesOfClass = this.level.getEntitiesOfClass(EndCrystal.class, new AABB(blockPos1.relative(direction, 2)));
+                 if (entitiesOfClass.isEmpty()) {
 -                    return;
 +                    return false; // CraftBukkit - return value
                  }
  
-                 list.addAll(list1);
+                 list.addAll(entitiesOfClass);
              }
  
-             EndDragonFight.LOGGER.debug("Found all crystals, respawning dragon.");
+             LOGGER.debug("Found all crystals, respawning dragon.");
 -            this.respawnDragon(list);
 +            return this.respawnDragon(list); // CraftBukkit - return value
          }
--
 +        return false; // CraftBukkit - return value
      }
  
 -    public void respawnDragon(List<EndCrystal> crystals) {
-+    public boolean respawnDragon(List<EndCrystal> list) { // CraftBukkit - return boolean
++    public boolean respawnDragon(List<EndCrystal> crystals) { // CraftBukkit - return boolean
          if (this.dragonKilled && this.respawnStage == null) {
-             for (BlockPattern.BlockPatternMatch shapedetector_shapedetectorcollection = this.findExitPortal(); shapedetector_shapedetectorcollection != null; shapedetector_shapedetectorcollection = this.findExitPortal()) {
-                 for (int i = 0; i < this.exitPortalPattern.getWidth(); ++i) {
-@@ -571,9 +642,10 @@
-             this.respawnStage = DragonRespawnAnimation.START;
+             for (BlockPattern.BlockPatternMatch blockPatternMatch = this.findExitPortal(); blockPatternMatch != null; blockPatternMatch = this.findExitPortal()) {
+                 for (int i = 0; i < this.exitPortalPattern.getWidth(); i++) {
+@@ -522,7 +_,9 @@
              this.respawnTime = 0;
              this.spawnExitPortal(false);
--            this.respawnCrystals = crystals;
-+            this.respawnCrystals = list;
+             this.respawnCrystals = crystals;
 +            return true; // CraftBukkit - return value
          }
--
 +        return false; // CraftBukkit - return value
      }
  
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/entity/EntityAccess.java.patch b/paper-server/patches/sources/net/minecraft/world/level/entity/EntityAccess.java.patch
similarity index 50%
rename from paper-server/patches/unapplied/net/minecraft/world/level/entity/EntityAccess.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/entity/EntityAccess.java.patch
index 517de585e8..6c5e3be1cc 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/entity/EntityAccess.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/entity/EntityAccess.java.patch
@@ -1,21 +1,11 @@
 --- a/net/minecraft/world/level/entity/EntityAccess.java
 +++ b/net/minecraft/world/level/entity/EntityAccess.java
-@@ -5,6 +5,9 @@
- import net.minecraft.core.BlockPos;
- import net.minecraft.world.entity.Entity;
- import net.minecraft.world.phys.AABB;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
+@@ -23,6 +_,12 @@
  
- public interface EntityAccess {
- 
-@@ -24,6 +27,12 @@
- 
-     void setRemoved(Entity.RemovalReason reason);
+     void setRemoved(Entity.RemovalReason removalReason);
  
 +    // CraftBukkit start - add Bukkit remove cause
-+    default void setRemoved(Entity.RemovalReason entity_removalreason, EntityRemoveEvent.Cause cause) {
++    default void setRemoved(Entity.RemovalReason entity_removalreason, org.bukkit.event.entity.EntityRemoveEvent.Cause cause) {
 +        this.setRemoved(entity_removalreason);
 +    }
 +    // CraftBukkit end
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/entity/EntityLookup.java.patch b/paper-server/patches/sources/net/minecraft/world/level/entity/EntityLookup.java.patch
similarity index 76%
rename from paper-server/patches/unapplied/net/minecraft/world/level/entity/EntityLookup.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/entity/EntityLookup.java.patch
index b3b9c9ccb4..27c389bc82 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/entity/EntityLookup.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/entity/EntityLookup.java.patch
@@ -1,9 +1,9 @@
 --- a/net/minecraft/world/level/entity/EntityLookup.java
 +++ b/net/minecraft/world/level/entity/EntityLookup.java
-@@ -33,6 +33,14 @@
-         UUID uUID = entity.getUUID();
-         if (this.byUuid.containsKey(uUID)) {
-             LOGGER.warn("Duplicate entity UUID {}: {}", uUID, entity);
+@@ -33,6 +_,14 @@
+         UUID uuid = entity.getUUID();
+         if (this.byUuid.containsKey(uuid)) {
+             LOGGER.warn("Duplicate entity UUID {}: {}", uuid, entity);
 +            // Paper start - extra debug info
 +            if (entity instanceof net.minecraft.world.entity.Entity) {
 +                final T old = this.byUuid.get(entity.getUUID());
@@ -13,5 +13,5 @@
 +            }
 +            // Paper end - extra debug info
          } else {
-             this.byUuid.put(uUID, entity);
+             this.byUuid.put(uuid, entity);
              this.byId.put(entity.getId(), entity);
diff --git a/paper-server/patches/sources/net/minecraft/world/level/entity/PersistentEntitySectionManager.java.patch b/paper-server/patches/sources/net/minecraft/world/level/entity/PersistentEntitySectionManager.java.patch
new file mode 100644
index 0000000000..384e9f3c58
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/entity/PersistentEntitySectionManager.java.patch
@@ -0,0 +1,208 @@
+--- a/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
++++ b/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
+@@ -52,6 +_,16 @@
+         this.entityGetter = new LevelEntityGetterAdapter<>(this.visibleEntityStorage, this.sectionStorage);
+     }
+ 
++    // CraftBukkit start - add method to get all entities in chunk
++    public List<Entity> getEntities(ChunkPos chunkCoordIntPair) {
++        return this.sectionStorage.getExistingSectionsInChunk(chunkCoordIntPair.toLong()).flatMap(EntitySection::getEntities).map(entity -> (Entity) entity).collect(Collectors.toList());
++    }
++
++    public boolean isPending(long pair) {
++        return this.chunkLoadStatuses.get(pair) == ChunkLoadStatus.PENDING;
++    }
++    // CraftBukkit end
++
+     void removeSectionIfEmpty(long sectionKey, EntitySection<T> section) {
+         if (section.isEmpty()) {
+             this.sectionStorage.remove(sectionKey);
+@@ -59,6 +_,7 @@
+     }
+ 
+     private boolean addEntityUuid(T entity) {
++        org.spigotmc.AsyncCatcher.catchOp("Entity add by UUID"); // Paper
+         if (!this.knownUuids.add(entity.getUUID())) {
+             LOGGER.warn("UUID of added entity already exists: {}", entity);
+             return false;
+@@ -72,6 +_,17 @@
+     }
+ 
+     private boolean addEntity(T entity, boolean worldGenSpawned) {
++        org.spigotmc.AsyncCatcher.catchOp("Entity add"); // Paper
++        // Paper start - chunk system hooks
++        // I don't want to know why this is a generic type.
++        Entity entityCasted = (Entity)entity;
++        boolean wasRemoved = entityCasted.isRemoved();
++        boolean screened = ca.spottedleaf.moonrise.common.PlatformHooks.get().screenEntity((net.minecraft.server.level.ServerLevel)entityCasted.level(), entityCasted, worldGenSpawned, true);
++        if ((!wasRemoved && entityCasted.isRemoved()) || !screened) {
++            // removed by callback
++            return false;
++        }
++        // Paper end - chunk system hooks
+         if (!this.addEntityUuid(entity)) {
+             return false;
+         } else {
+@@ -109,19 +_,23 @@
+     }
+ 
+     void startTicking(T entity) {
++        org.spigotmc.AsyncCatcher.catchOp("Entity start ticking"); // Paper
+         this.callbacks.onTickingStart(entity);
+     }
+ 
+     void stopTicking(T entity) {
++        org.spigotmc.AsyncCatcher.catchOp("Entity stop ticking"); // Paper
+         this.callbacks.onTickingEnd(entity);
+     }
+ 
+     void startTracking(T entity) {
++        org.spigotmc.AsyncCatcher.catchOp("Entity start tracking"); // Paper
+         this.visibleEntityStorage.add(entity);
+         this.callbacks.onTrackingStart(entity);
+     }
+ 
+     void stopTracking(T entity) {
++        org.spigotmc.AsyncCatcher.catchOp("Entity stop tracking"); // Paper
+         this.callbacks.onTrackingEnd(entity);
+         this.visibleEntityStorage.remove(entity);
+     }
+@@ -132,6 +_,7 @@
+     }
+ 
+     public void updateChunkStatus(ChunkPos pos, Visibility visibility) {
++        org.spigotmc.AsyncCatcher.catchOp("Update chunk status"); // Paper
+         long packedChunkPos = pos.toLong();
+         if (visibility == Visibility.HIDDEN) {
+             this.chunkVisibility.remove(packedChunkPos);
+@@ -165,6 +_,7 @@
+     }
+ 
+     public void ensureChunkQueuedForLoad(long chunkPosValue) {
++        org.spigotmc.AsyncCatcher.catchOp("Entity chunk save"); // Paper
+         PersistentEntitySectionManager.ChunkLoadStatus chunkLoadStatus = this.chunkLoadStatuses.get(chunkPosValue);
+         if (chunkLoadStatus == PersistentEntitySectionManager.ChunkLoadStatus.FRESH) {
+             this.requestChunkLoad(chunkPosValue);
+@@ -172,6 +_,11 @@
+     }
+ 
+     private boolean storeChunkSections(long chunkPosValue, Consumer<T> entityAction) {
++        // CraftBukkit start
++        return storeChunkSections(chunkPosValue, entityAction, false);
++    }
++    private boolean storeChunkSections(long chunkPosValue, Consumer<T> entityAction, boolean callEvent) {
++        // CraftBukkit end
+         PersistentEntitySectionManager.ChunkLoadStatus chunkLoadStatus = this.chunkLoadStatuses.get(chunkPosValue);
+         if (chunkLoadStatus == PersistentEntitySectionManager.ChunkLoadStatus.PENDING) {
+             return false;
+@@ -182,6 +_,7 @@
+                 .collect(Collectors.toList());
+             if (list.isEmpty()) {
+                 if (chunkLoadStatus == PersistentEntitySectionManager.ChunkLoadStatus.LOADED) {
++                    if (callEvent) org.bukkit.craftbukkit.event.CraftEventFactory.callEntitiesUnloadEvent(((net.minecraft.world.level.chunk.storage.EntityStorage) this.permanentStorage).level, new ChunkPos(chunkPosValue), ImmutableList.of()); // CraftBukkit
+                     this.permanentStorage.storeEntities(new ChunkEntities<>(new ChunkPos(chunkPosValue), ImmutableList.of()));
+                 }
+ 
+@@ -190,6 +_,7 @@
+                 this.requestChunkLoad(chunkPosValue);
+                 return false;
+             } else {
++                if (callEvent) org.bukkit.craftbukkit.event.CraftEventFactory.callEntitiesUnloadEvent(((net.minecraft.world.level.chunk.storage.EntityStorage) this.permanentStorage).level, new ChunkPos(chunkPosValue), list.stream().map(entity -> (Entity) entity).collect(Collectors.toList())); // CraftBukkit
+                 this.permanentStorage.storeEntities(new ChunkEntities<>(new ChunkPos(chunkPosValue), list));
+                 list.forEach(entityAction);
+                 return true;
+@@ -198,6 +_,7 @@
+     }
+ 
+     private void requestChunkLoad(long chunkPosValue) {
++        org.spigotmc.AsyncCatcher.catchOp("Entity chunk load request"); // Paper
+         this.chunkLoadStatuses.put(chunkPosValue, PersistentEntitySectionManager.ChunkLoadStatus.PENDING);
+         ChunkPos chunkPos = new ChunkPos(chunkPosValue);
+         this.permanentStorage.loadEntities(chunkPos).thenAccept(this.loadingInbox::add).exceptionally(throwable -> {
+@@ -207,7 +_,8 @@
+     }
+ 
+     private boolean processChunkUnload(long chunkPosValue) {
+-        boolean flag = this.storeChunkSections(chunkPosValue, entity -> entity.getPassengersAndSelf().forEach(this::unloadEntity));
++        org.spigotmc.AsyncCatcher.catchOp("Entity chunk unload process"); // Paper
++        boolean flag = this.storeChunkSections(chunkPosValue, entity -> entity.getPassengersAndSelf().forEach(this::unloadEntity), true); // CraftBukkit - add boolean for event call
+         if (!flag) {
+             return false;
+         } else {
+@@ -217,7 +_,7 @@
+     }
+ 
+     private void unloadEntity(EntityAccess entity) {
+-        entity.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK);
++        entity.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK, org.bukkit.event.entity.EntityRemoveEvent.Cause.UNLOAD); // CraftBukkit - add Bukkit remove cause
+         entity.setLevelCallback(EntityInLevelCallback.NULL);
+     }
+ 
+@@ -227,14 +_,20 @@
+     }
+ 
+     private void processPendingLoads() {
++        org.spigotmc.AsyncCatcher.catchOp("Entity chunk process pending loads"); // Paper
+         ChunkEntities<T> chunkEntities;
+         while ((chunkEntities = this.loadingInbox.poll()) != null) {
+             chunkEntities.getEntities().forEach(entity -> this.addEntity((T)entity, true));
+             this.chunkLoadStatuses.put(chunkEntities.getPos().toLong(), PersistentEntitySectionManager.ChunkLoadStatus.LOADED);
++            // CraftBukkit start - call entity load event
++            List<Entity> entities = this.getEntities(chunkEntities.getPos());
++            org.bukkit.craftbukkit.event.CraftEventFactory.callEntitiesLoadEvent(((net.minecraft.world.level.chunk.storage.EntityStorage) this.permanentStorage).level, chunkEntities.getPos(), entities);
++            // CraftBukkit end
+         }
+     }
+ 
+     public void tick() {
++        org.spigotmc.AsyncCatcher.catchOp("Entity manager tick"); // Paper
+         this.processPendingLoads();
+         this.processUnloads();
+     }
+@@ -252,6 +_,7 @@
+     }
+ 
+     public void autoSave() {
++        org.spigotmc.AsyncCatcher.catchOp("Entity manager autosave"); // Paper
+         this.getAllChunksToSave().forEach(packedChunkPos -> {
+             boolean flag = this.chunkVisibility.get(packedChunkPos) == Visibility.HIDDEN;
+             if (flag) {
+@@ -263,6 +_,7 @@
+     }
+ 
+     public void saveAll() {
++        org.spigotmc.AsyncCatcher.catchOp("Entity manager save"); // Paper
+         LongSet allChunksToSave = this.getAllChunksToSave();
+ 
+         while (!allChunksToSave.isEmpty()) {
+@@ -279,7 +_,13 @@
+ 
+     @Override
+     public void close() throws IOException {
+-        this.saveAll();
++        // CraftBukkit start
++        this.close(true);
++    }
++
++    public void close(boolean save) throws IOException {
++        if (save) this.saveAll();
++        // CraftBukkit end
+         this.permanentStorage.close();
+     }
+ 
+@@ -380,6 +_,7 @@
+             BlockPos blockPos = this.entity.blockPosition();
+             long packedSectionPos = SectionPos.asLong(blockPos);
+             if (packedSectionPos != this.currentSectionKey) {
++                org.spigotmc.AsyncCatcher.catchOp("Entity move"); // Paper
+                 Visibility status = this.currentSection.getStatus();
+                 if (!this.currentSection.remove(this.entity)) {
+                     PersistentEntitySectionManager.LOGGER
+@@ -427,6 +_,7 @@
+ 
+         @Override
+         public void onRemove(Entity.RemovalReason reason) {
++            org.spigotmc.AsyncCatcher.catchOp("Entity remove"); // Paper
+             if (!this.currentSection.remove(this.entity)) {
+                 PersistentEntitySectionManager.LOGGER
+                     .warn("Entity {} wasn't found in section {} (destroying due to {})", this.entity, SectionPos.of(this.currentSectionKey), reason);
diff --git a/paper-server/patches/sources/net/minecraft/world/level/gameevent/DynamicGameEventListener.java.patch b/paper-server/patches/sources/net/minecraft/world/level/gameevent/DynamicGameEventListener.java.patch
new file mode 100644
index 0000000000..c0246df6bb
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/gameevent/DynamicGameEventListener.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/level/gameevent/DynamicGameEventListener.java
++++ b/net/minecraft/world/level/gameevent/DynamicGameEventListener.java
+@@ -41,7 +_,7 @@
+ 
+     private static void ifChunkExists(LevelReader level, @Nullable SectionPos sectionPos, Consumer<GameEventListenerRegistry> dispatcherConsumer) {
+         if (sectionPos != null) {
+-            ChunkAccess chunk = level.getChunk(sectionPos.x(), sectionPos.z(), ChunkStatus.FULL, false);
++            ChunkAccess chunk = level.getChunkIfLoadedImmediately(sectionPos.getX(), sectionPos.getZ()); // Paper - Perf: can cause sync loads while completing a chunk, resulting in deadlock
+             if (chunk != null) {
+                 dispatcherConsumer.accept(chunk.getListenerRegistry(sectionPos.y()));
+             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/gameevent/GameEvent.java.patch b/paper-server/patches/sources/net/minecraft/world/level/gameevent/GameEvent.java.patch
similarity index 51%
rename from paper-server/patches/unapplied/net/minecraft/world/level/gameevent/GameEvent.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/gameevent/GameEvent.java.patch
index 508ac18f4c..be2290d7c9 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/gameevent/GameEvent.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/gameevent/GameEvent.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/world/level/gameevent/GameEvent.java
 +++ b/net/minecraft/world/level/gameevent/GameEvent.java
-@@ -85,7 +85,7 @@
+@@ -85,7 +_,7 @@
      }
  
-     private static Holder.Reference<GameEvent> 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
+     private static Holder.Reference<GameEvent> register(String name, int notificationRadius) {
+-        return Registry.registerForHolder(BuiltInRegistries.GAME_EVENT, ResourceLocation.withDefaultNamespace(name), new GameEvent(notificationRadius));
++        return io.papermc.paper.registry.PaperRegistryListenerManager.INSTANCE.registerForHolderWithListeners(BuiltInRegistries.GAME_EVENT, ResourceLocation.withDefaultNamespace(name), new GameEvent(notificationRadius)); // Paper - run with listeners
      }
  
-     public static record Context(@Nullable Entity sourceEntity, @Nullable BlockState affectedState) {
+     public record Context(@Nullable Entity sourceEntity, @Nullable BlockState affectedState) {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/gameevent/GameEventDispatcher.java.patch b/paper-server/patches/sources/net/minecraft/world/level/gameevent/GameEventDispatcher.java.patch
new file mode 100644
index 0000000000..bcefb8669e
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/gameevent/GameEventDispatcher.java.patch
@@ -0,0 +1,40 @@
+--- a/net/minecraft/world/level/gameevent/GameEventDispatcher.java
++++ b/net/minecraft/world/level/gameevent/GameEventDispatcher.java
+@@ -11,6 +_,13 @@
+ import net.minecraft.world.level.chunk.ChunkAccess;
+ import net.minecraft.world.phys.Vec3;
+ 
++// CraftBukkit start
++import org.bukkit.Bukkit;
++import org.bukkit.craftbukkit.CraftGameEvent;
++import org.bukkit.craftbukkit.util.CraftLocation;
++import org.bukkit.event.world.GenericGameEvent;
++// CraftBukkit end
++
+ public class GameEventDispatcher {
+     private final ServerLevel level;
+ 
+@@ -21,6 +_,14 @@
+     public void post(Holder<GameEvent> gameEvent, Vec3 pos, GameEvent.Context context) {
+         int notificationRadius = gameEvent.value().notificationRadius();
+         BlockPos blockPos = BlockPos.containing(pos);
++        // CraftBukkit start
++        GenericGameEvent apiEvent = new GenericGameEvent(CraftGameEvent.minecraftToBukkit(gameEvent.value()), CraftLocation.toBukkit(blockPos, this.level.getWorld()), (context.sourceEntity() == null) ? null : context.sourceEntity().getBukkitEntity(), notificationRadius, !Bukkit.isPrimaryThread());
++        this.level.getCraftServer().getPluginManager().callEvent(apiEvent);
++        if (apiEvent.isCancelled()) {
++            return;
++        }
++        notificationRadius = apiEvent.getRadius();
++        // CraftBukkit end
+         int sectionPosCoord = SectionPos.blockToSectionCoord(blockPos.getX() - notificationRadius);
+         int sectionPosCoord1 = SectionPos.blockToSectionCoord(blockPos.getY() - notificationRadius);
+         int sectionPosCoord2 = SectionPos.blockToSectionCoord(blockPos.getZ() - notificationRadius);
+@@ -39,7 +_,7 @@
+ 
+         for (int i = sectionPosCoord; i <= sectionPosCoord3; i++) {
+             for (int i1 = sectionPosCoord2; i1 <= sectionPosCoord5; i1++) {
+-                ChunkAccess chunkNow = this.level.getChunkSource().getChunkNow(i, i1);
++                ChunkAccess chunkNow = this.level.getChunkIfLoadedImmediately(i, i1); // Paper - Use getChunkIfLoadedImmediately
+                 if (chunkNow != null) {
+                     for (int i2 = sectionPosCoord1; i2 <= sectionPosCoord4; i2++) {
+                         flag |= chunkNow.getListenerRegistry(i2).visitInRangeListeners(gameEvent, pos, context, listenerVisitor);
diff --git a/paper-server/patches/sources/net/minecraft/world/level/gameevent/vibrations/VibrationSystem.java.patch b/paper-server/patches/sources/net/minecraft/world/level/gameevent/vibrations/VibrationSystem.java.patch
new file mode 100644
index 0000000000..36b61e1dd6
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/gameevent/vibrations/VibrationSystem.java.patch
@@ -0,0 +1,18 @@
+--- a/net/minecraft/world/level/gameevent/vibrations/VibrationSystem.java
++++ b/net/minecraft/world/level/gameevent/vibrations/VibrationSystem.java
+@@ -214,7 +_,14 @@
+                     return false;
+                 } else {
+                     Vec3 vec3 = position.get();
+-                    if (!vibrationUser.canReceiveVibration(level, BlockPos.containing(pos), gameEvent, context)) {
++                    // CraftBukkit start
++                    boolean defaultCancel = !vibrationUser.canReceiveVibration(level, BlockPos.containing(pos), gameEvent, context);
++                    Entity entity = context.sourceEntity();
++                    org.bukkit.event.block.BlockReceiveGameEvent event1 = new org.bukkit.event.block.BlockReceiveGameEvent(org.bukkit.craftbukkit.CraftGameEvent.minecraftToBukkit(gameEvent.value()), org.bukkit.craftbukkit.block.CraftBlock.at(level, BlockPos.containing(vec3)), (entity == null) ? null : entity.getBukkitEntity());
++                    event1.setCancelled(defaultCancel);
++                    level.getCraftServer().getPluginManager().callEvent(event1);
++                    if (event1.isCancelled()) {
++                        // CraftBukkit end
+                         return false;
+                     } else if (isOccluded(level, pos, vec3)) {
+                         return false;
diff --git a/paper-server/patches/sources/net/minecraft/world/level/levelgen/DensityFunctions.java.patch b/paper-server/patches/sources/net/minecraft/world/level/levelgen/DensityFunctions.java.patch
new file mode 100644
index 0000000000..087b4b7d6d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/levelgen/DensityFunctions.java.patch
@@ -0,0 +1,55 @@
+--- a/net/minecraft/world/level/levelgen/DensityFunctions.java
++++ b/net/minecraft/world/level/levelgen/DensityFunctions.java
+@@ -506,6 +_,16 @@
+         );
+         private static final float ISLAND_THRESHOLD = -0.9F;
+         private final SimplexNoise islandNoise;
++        // Paper start - Perf: Optimize end generation
++        private static final class NoiseCache {
++            public long[] keys = new long[8192];
++            public float[] values = new float[8192];
++            public NoiseCache() {
++                java.util.Arrays.fill(keys, Long.MIN_VALUE);
++            }
++        }
++        private static final ThreadLocal<java.util.Map<SimplexNoise, NoiseCache>> noiseCache = ThreadLocal.withInitial(java.util.WeakHashMap::new);
++        // Paper end - Perf: Optimize end generation
+ 
+         public EndIslandDensityFunction(long seed) {
+             RandomSource randomSource = new LegacyRandomSource(seed);
+@@ -518,15 +_,31 @@
+             int i1 = z / 2;
+             int i2 = x % 2;
+             int i3 = z % 2;
+-            float f = 100.0F - Mth.sqrt(x * x + z * z) * 8.0F;
++            float f = 100.0F - Mth.sqrt((long)x * (long)x + (long)z * (long)z) * 8.0F; // Paper - cast ints to long to avoid integer overflow
+             f = Mth.clamp(f, -100.0F, 80.0F);
+ 
++            NoiseCache cache = noiseCache.get().computeIfAbsent(noise, noiseKey -> new NoiseCache()); // Paper - Perf: Optimize end generation
+             for (int i4 = -12; i4 <= 12; i4++) {
+                 for (int i5 = -12; i5 <= 12; i5++) {
+-                    long l = i + i4;
+-                    long l1 = i1 + i5;
++                    long l = i + i4; final int chunkX = (int) l; // Paper - OBFHELPER
++                    long l1 = i1 + i5; final int chunkZ = (int) l1; // Paper - OBFHELPER
++                    // Paper start - Perf: Optimize end generation by using a noise cache
++                    final long chunkKey = net.minecraft.world.level.ChunkPos.asLong(chunkX, chunkZ);
++                    final int cacheIndex = (int) it.unimi.dsi.fastutil.HashCommon.mix(chunkKey) & 8191;
++                    float f1 = Float.MIN_VALUE; // noise value
++                    if (cache.keys[cacheIndex] == chunkKey) {
++                        // Use cache
++                        f1 = cache.values[cacheIndex];
++                    } else {
++                        // Vanilla function
+                     if (l * l + l1 * l1 > 4096L && noise.getValue(l, l1) < -0.9F) {
+-                        float f1 = (Mth.abs((float)l) * 3439.0F + Mth.abs((float)l1) * 147.0F) % 13.0F + 9.0F;
++                        f1 = (Mth.abs((float)l) * 3439.0F + Mth.abs((float)l1) * 147.0F) % 13.0F + 9.0F;
++                    }
++                        cache.keys[cacheIndex] = chunkKey;
++                        cache.values[cacheIndex] = f1;
++                    }
++                    if (f1 != Float.MIN_VALUE) {
++                        // Paper end - Perf: Optimize end generation
+                         float f2 = i2 - i4 * 2;
+                         float f3 = i3 - i5 * 2;
+                         float f4 = 100.0F - Mth.sqrt(f2 * f2 + f3 * f3) * f1;
diff --git a/paper-server/patches/sources/net/minecraft/world/level/levelgen/FlatLevelSource.java.patch b/paper-server/patches/sources/net/minecraft/world/level/levelgen/FlatLevelSource.java.patch
new file mode 100644
index 0000000000..f1cd4a895b
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/levelgen/FlatLevelSource.java.patch
@@ -0,0 +1,28 @@
+--- a/net/minecraft/world/level/levelgen/FlatLevelSource.java
++++ b/net/minecraft/world/level/levelgen/FlatLevelSource.java
+@@ -33,17 +_,22 @@
+     private final FlatLevelGeneratorSettings settings;
+ 
+     public FlatLevelSource(FlatLevelGeneratorSettings settings) {
+-        super(new FixedBiomeSource(settings.getBiome()), Util.memoize(settings::adjustGenerationSettings));
++        // CraftBukkit start
++        this(settings, new FixedBiomeSource(settings.getBiome()));
++    }
++    public FlatLevelSource(FlatLevelGeneratorSettings settings, net.minecraft.world.level.biome.BiomeSource biomeSource) {
++        super(biomeSource, Util.memoize(settings::adjustGenerationSettings));
++        // CraftBukkit end
+         this.settings = settings;
+     }
+ 
+     @Override
+-    public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> structureSetLookup, RandomState randomState, long seed) {
++    public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> structureSetLookup, RandomState randomState, long seed, org.spigotmc.SpigotWorldConfig conf) { // Spigot
+         Stream<Holder<StructureSet>> stream = this.settings
+             .structureOverrides()
+             .map(HolderSet::stream)
+             .orElseGet(() -> structureSetLookup.listElements().map(reference -> (Holder<StructureSet>)reference));
+-        return ChunkGeneratorStructureState.createForFlat(randomState, seed, this.biomeSource, stream);
++        return ChunkGeneratorStructureState.createForFlat(randomState, seed, this.biomeSource, stream, conf); // Spigot
+     }
+ 
+     @Override
diff --git a/paper-server/patches/sources/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java.patch b/paper-server/patches/sources/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java.patch
new file mode 100644
index 0000000000..c986ea09a9
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java.patch
@@ -0,0 +1,25 @@
+--- a/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
++++ b/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
+@@ -1,3 +_,4 @@
++// keep
+ package net.minecraft.world.level.levelgen;
+ 
+ import com.google.common.annotations.VisibleForTesting;
+@@ -218,7 +_,7 @@
+     @Override
+     public void buildSurface(WorldGenRegion level, StructureManager structureManager, RandomState random, ChunkAccess chunk) {
+         if (!SharedConstants.debugVoidTerrain(chunk.getPos())) {
+-            WorldGenerationContext worldGenerationContext = new WorldGenerationContext(this, level);
++            WorldGenerationContext worldGenerationContext = new WorldGenerationContext(this, level, level.getMinecraftWorld()); // Paper - Flat bedrock generator settings
+             this.buildSurface(
+                 chunk,
+                 worldGenerationContext,
+@@ -260,7 +_,7 @@
+         NoiseChunk noiseChunk = chunk.getOrCreateNoiseChunk(chunkAccess -> this.createNoiseChunk(chunkAccess, structureManager, Blender.of(level), random));
+         Aquifer aquifer = noiseChunk.aquifer();
+         CarvingContext carvingContext = new CarvingContext(
+-            this, level.registryAccess(), chunk.getHeightAccessorForGeneration(), noiseChunk, random, this.settings.value().surfaceRule()
++            this, level.registryAccess(), chunk.getHeightAccessorForGeneration(), noiseChunk, random, this.settings.value().surfaceRule(), level.getMinecraftWorld() // Paper - Flat bedrock generator settings
+         );
+         CarvingMask carvingMask = ((ProtoChunk)chunk).getOrCreateCarvingMask();
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/level/levelgen/PatrolSpawner.java.patch b/paper-server/patches/sources/net/minecraft/world/level/levelgen/PatrolSpawner.java.patch
new file mode 100644
index 0000000000..b25a255606
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/levelgen/PatrolSpawner.java.patch
@@ -0,0 +1,91 @@
+--- a/net/minecraft/world/level/levelgen/PatrolSpawner.java
++++ b/net/minecraft/world/level/levelgen/PatrolSpawner.java
+@@ -20,28 +_,66 @@
+ 
+     @Override
+     public int tick(ServerLevel level, boolean spawnEnemies, boolean spawnFriendlies) {
++        if (level.paperConfig().entities.behavior.pillagerPatrols.disable || level.paperConfig().entities.behavior.pillagerPatrols.spawnChance == 0) return 0; // Paper - Add option to disable pillager patrols & Pillager patrol spawn settings and per player options
+         if (!spawnEnemies) {
+             return 0;
+         } else if (!level.getGameRules().getBoolean(GameRules.RULE_DO_PATROL_SPAWNING)) {
+             return 0;
+         } else {
+             RandomSource randomSource = level.random;
+-            this.nextTick--;
+-            if (this.nextTick > 0) {
+-                return 0;
+-            } else {
+-                this.nextTick = this.nextTick + 12000 + randomSource.nextInt(1200);
+-                long l = level.getDayTime() / 24000L;
+-                if (l < 5L || !level.isDay()) {
+-                    return 0;
+-                } else if (randomSource.nextInt(5) != 0) {
+-                    return 0;
+-                } else {
+-                    int size = level.players().size();
++            // this.nextTick--;
++            // if (this.nextTick > 0) {
++            //     return 0;
++            // } else {
++            //     this.nextTick = this.nextTick + 12000 + randomSource.nextInt(1200);
++            //     long l = level.getDayTime() / 24000L;
++            //     if (l < 5L || !level.isDay()) {
++            //         return 0;
++            //     } else if (randomSource.nextInt(5) != 0) {
++            // Paper start - Pillager patrol spawn settings and per player options
++            // Random player selection moved up for per player spawning and configuration
++            int size = level.players().size();
++            if (size < 1) {
++                return 0;
++            }
++
++            net.minecraft.server.level.ServerPlayer player = level.players().get(randomSource.nextInt(size));
++            if (player.isSpectator()) {
++                return 0;
++            }
++
++            int patrolSpawnDelay;
++            if (level.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.perPlayer) {
++                --player.patrolSpawnDelay;
++                patrolSpawnDelay = player.patrolSpawnDelay;
++            } else {
++                this.nextTick--;
++                patrolSpawnDelay = this.nextTick;
++            }
++            if (patrolSpawnDelay > 0) {
++                return 0;
++            } else {
++                long days;
++                if (level.paperConfig().entities.behavior.pillagerPatrols.start.perPlayer) {
++                    days = player.getStats().getValue(net.minecraft.stats.Stats.CUSTOM.get(net.minecraft.stats.Stats.PLAY_TIME)) / 24000L; // PLAY_TIME is counting in ticks
++                } else {
++                    days = level.getDayTime() / 24000L;
++                }
++                if (level.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.perPlayer) {
++                    player.patrolSpawnDelay += level.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomSource.nextInt(1200);
++                } else {
++                    this.nextTick += level.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomSource.nextInt(1200);
++                }
++
++                if (days < level.paperConfig().entities.behavior.pillagerPatrols.start.day || !level.isDay()) {
++                    return 0;
++                } else if (randomSource.nextDouble() >= level.paperConfig().entities.behavior.pillagerPatrols.spawnChance) {
++                    // Paper end - Pillager patrol spawn settings and per player options
++                    return 0;
++                } else {
+                     if (size < 1) {
+                         return 0;
+                     } else {
+-                        Player player = level.players().get(randomSource.nextInt(size));
+                         if (player.isSpectator()) {
+                             return 0;
+                         } else if (level.isCloseToVillage(player.blockPosition(), 2)) {
+@@ -104,7 +_,7 @@
+ 
+                 patrollingMonster.setPos(pos.getX(), pos.getY(), pos.getZ());
+                 patrollingMonster.finalizeSpawn(level, level.getCurrentDifficultyAt(pos), EntitySpawnReason.PATROL, null);
+-                level.addFreshEntityWithPassengers(patrollingMonster);
++                level.addFreshEntityWithPassengers(patrollingMonster, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.PATROL); // CraftBukkit
+                 return true;
+             } else {
+                 return false;
diff --git a/paper-server/patches/sources/net/minecraft/world/level/levelgen/PhantomSpawner.java.patch b/paper-server/patches/sources/net/minecraft/world/level/levelgen/PhantomSpawner.java.patch
new file mode 100644
index 0000000000..19c728a9ed
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/levelgen/PhantomSpawner.java.patch
@@ -0,0 +1,67 @@
+--- a/net/minecraft/world/level/levelgen/PhantomSpawner.java
++++ b/net/minecraft/world/level/levelgen/PhantomSpawner.java
+@@ -28,19 +_,28 @@
+         } else if (!level.getGameRules().getBoolean(GameRules.RULE_DOINSOMNIA)) {
+             return 0;
+         } else {
++            // Paper start - Ability to control player's insomnia and phantoms
++            if (level.paperConfig().entities.behavior.phantomsSpawnAttemptMaxSeconds <= 0) {
++                return 0;
++            }
++            // Paper end - Ability to control player's insomnia and phantoms
+             RandomSource randomSource = level.random;
+             this.nextTick--;
+             if (this.nextTick > 0) {
+                 return 0;
+             } else {
+-                this.nextTick = this.nextTick + (60 + randomSource.nextInt(60)) * 20;
++                // Paper start - Ability to control player's insomnia and phantoms
++                int spawnAttemptMinSeconds = level.paperConfig().entities.behavior.phantomsSpawnAttemptMinSeconds;
++                int spawnAttemptMaxSeconds = level.paperConfig().entities.behavior.phantomsSpawnAttemptMaxSeconds;
++                this.nextTick += (spawnAttemptMinSeconds + randomSource.nextInt(spawnAttemptMaxSeconds - spawnAttemptMinSeconds + 1)) * 20;
++                // Paper end - Ability to control player's insomnia and phantoms
+                 if (level.getSkyDarken() < 5 && level.dimensionType().hasSkyLight()) {
+                     return 0;
+                 } else {
+                     int i = 0;
+ 
+                     for (ServerPlayer serverPlayer : level.players()) {
+-                        if (!serverPlayer.isSpectator()) {
++                        if (!serverPlayer.isSpectator() && (!level.paperConfig().entities.behavior.phantomsDoNotSpawnOnCreativePlayers || !serverPlayer.isCreative())) { // Paper - Add phantom creative and insomniac controls
+                             BlockPos blockPos = serverPlayer.blockPosition();
+                             if (!level.dimensionType().hasSkyLight() || blockPos.getY() >= level.getSeaLevel() && level.canSeeSky(blockPos)) {
+                                 DifficultyInstance currentDifficultyAt = level.getCurrentDifficultyAt(blockPos);
+@@ -48,7 +_,7 @@
+                                     ServerStatsCounter stats = serverPlayer.getStats();
+                                     int i1 = Mth.clamp(stats.getValue(Stats.CUSTOM.get(Stats.TIME_SINCE_REST)), 1, Integer.MAX_VALUE);
+                                     int i2 = 24000;
+-                                    if (randomSource.nextInt(i1) >= 72000) {
++                                    if (randomSource.nextInt(i1) >= level.paperConfig().entities.behavior.playerInsomniaStartTicks) { // Paper - Ability to control player's insomnia and phantoms
+                                         BlockPos blockPos1 = blockPos.above(20 + randomSource.nextInt(15))
+                                             .east(-10 + randomSource.nextInt(21))
+                                             .south(-10 + randomSource.nextInt(21));
+@@ -59,13 +_,23 @@
+                                             int i3 = 1 + randomSource.nextInt(currentDifficultyAt.getDifficulty().getId() + 1);
+ 
+                                             for (int i4 = 0; i4 < i3; i4++) {
++                                                // Paper start - PhantomPreSpawnEvent
++                                                com.destroystokyo.paper.event.entity.PhantomPreSpawnEvent event = new com.destroystokyo.paper.event.entity.PhantomPreSpawnEvent(io.papermc.paper.util.MCUtil.toLocation(level, blockPos1), serverPlayer.getBukkitEntity(), org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.NATURAL);
++                                                if (!event.callEvent()) {
++                                                    if (event.shouldAbortSpawn()) {
++                                                        break;
++                                                    }
++                                                    continue;
++                                                }
++                                                // Paper end - PhantomPreSpawnEvent
+                                                 Phantom phantom = EntityType.PHANTOM.create(level, EntitySpawnReason.NATURAL);
+                                                 if (phantom != null) {
++                                                    phantom.spawningEntity = serverPlayer.getUUID(); // Paper - PhantomPreSpawnEvent
+                                                     phantom.moveTo(blockPos1, 0.0F, 0.0F);
+                                                     spawnGroupData = phantom.finalizeSpawn(
+                                                         level, currentDifficultyAt, EntitySpawnReason.NATURAL, spawnGroupData
+                                                     );
+-                                                    level.addFreshEntityWithPassengers(phantom);
++                                                    level.addFreshEntityWithPassengers(phantom, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.NATURAL); // CraftBukkit
+                                                     i++;
+                                                 }
+                                             }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/levelgen/WorldGenerationContext.java.patch b/paper-server/patches/sources/net/minecraft/world/level/levelgen/WorldGenerationContext.java.patch
new file mode 100644
index 0000000000..a0993bf36a
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/levelgen/WorldGenerationContext.java.patch
@@ -0,0 +1,32 @@
+--- a/net/minecraft/world/level/levelgen/WorldGenerationContext.java
++++ b/net/minecraft/world/level/levelgen/WorldGenerationContext.java
+@@ -6,8 +_,15 @@
+ public class WorldGenerationContext {
+     private final int minY;
+     private final int height;
++    // Paper start - Flat bedrock generator settings
++    private final @javax.annotation.Nullable net.minecraft.world.level.Level serverLevel;
+ 
+     public WorldGenerationContext(ChunkGenerator generator, LevelHeightAccessor level) {
++        this(generator, level, null);
++    }
++    public WorldGenerationContext(ChunkGenerator generator, LevelHeightAccessor level, net.minecraft.world.level.Level serverLevel) {
++        this.serverLevel = serverLevel;
++        // Paper end - Flat bedrock generator settings
+         this.minY = Math.max(level.getMinY(), generator.getMinY());
+         this.height = Math.min(level.getHeight(), generator.getGenDepth());
+     }
+@@ -19,4 +_,13 @@
+     public int getGenDepth() {
+         return this.height;
+     }
++
++    // Paper start - Flat bedrock generator settings
++    public net.minecraft.world.level.Level level() {
++        if (this.serverLevel == null) {
++            throw new NullPointerException("WorldGenerationContext was initialized without a Level, but WorldGenerationContext#level was called");
++        }
++        return this.serverLevel;
++    }
++    // Paper end - Flat bedrock generator settings
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/levelgen/carver/CarvingContext.java.patch b/paper-server/patches/sources/net/minecraft/world/level/levelgen/carver/CarvingContext.java.patch
new file mode 100644
index 0000000000..63d7237796
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/levelgen/carver/CarvingContext.java.patch
@@ -0,0 +1,14 @@
+--- a/net/minecraft/world/level/levelgen/carver/CarvingContext.java
++++ b/net/minecraft/world/level/levelgen/carver/CarvingContext.java
+@@ -27,9 +_,9 @@
+         LevelHeightAccessor level,
+         NoiseChunk noiseChunk,
+         RandomState randomState,
+-        SurfaceRules.RuleSource surfaceRule
++        SurfaceRules.RuleSource surfaceRule, @javax.annotation.Nullable net.minecraft.world.level.Level serverLevel  // Paper - Flat bedrock generator settings
+     ) {
+-        super(generator, level);
++        super(generator, level, serverLevel); // Paper - Flat bedrock generator settings
+         this.registryAccess = registryAccess;
+         this.noiseChunk = noiseChunk;
+         this.randomState = randomState;
diff --git a/paper-server/patches/sources/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java.patch b/paper-server/patches/sources/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java.patch
new file mode 100644
index 0000000000..bfd494df87
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java.patch
@@ -0,0 +1,52 @@
+--- a/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java
++++ b/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java
+@@ -19,6 +_,12 @@
+     }
+ 
+     public static void createEndPlatform(ServerLevelAccessor level, BlockPos pos, boolean dropBlocks) {
++        // CraftBukkit start
++        createEndPlatform(level, pos, dropBlocks, null);
++    }
++    public static void createEndPlatform(ServerLevelAccessor level, BlockPos pos, boolean dropBlocks, net.minecraft.world.entity.Entity entity) {
++        org.bukkit.craftbukkit.util.BlockStateListPopulator blockList = new org.bukkit.craftbukkit.util.BlockStateListPopulator(level);
++        // CraftBukkit end
+         BlockPos.MutableBlockPos mutableBlockPos = pos.mutable();
+ 
+         for (int i = -2; i <= 2; i++) {
+@@ -26,15 +_,33 @@
+                 for (int i2 = -1; i2 < 3; i2++) {
+                     BlockPos blockPos = mutableBlockPos.set(pos).move(i1, i2, i);
+                     Block block = i2 == -1 ? Blocks.OBSIDIAN : Blocks.AIR;
+-                    if (!level.getBlockState(blockPos).is(block)) {
++                    // CraftBukkit start
++                    if (!blockList.getBlockState(blockPos).is(block)) {
+                         if (dropBlocks) {
+-                            level.destroyBlock(blockPos, true, null);
++                            blockList.destroyBlock(blockPos, true, null);
+                         }
+ 
+-                        level.setBlock(blockPos, block.defaultBlockState(), 3);
++                        blockList.setBlock(blockPos, block.defaultBlockState(), 3);
++                        // CraftBukkit end
+                     }
+                 }
+             }
+         }
++
++        // CraftBukkit start
++        // SPIGOT-7746: Entity will only be null during world generation, which is async, so just generate without event
++        if (entity != null) {
++            org.bukkit.World bworld = level.getLevel().getWorld();
++            org.bukkit.event.world.PortalCreateEvent portalEvent = new org.bukkit.event.world.PortalCreateEvent((java.util.List<org.bukkit.block.BlockState>) (java.util.List) blockList.getList(), bworld, entity.getBukkitEntity(), org.bukkit.event.world.PortalCreateEvent.CreateReason.END_PLATFORM);
++            level.getLevel().getCraftServer().getPluginManager().callEvent(portalEvent);
++            if (portalEvent.isCancelled()) return;
++        }
++
++        // SPIGOT-7856: End platform not dropping items after replacing blocks
++        if (dropBlocks) {
++            blockList.getList().forEach((state) -> level.destroyBlock(state.getPosition(), true, null));
++        }
++        blockList.updateList();
++        // CraftBukkit end
+     }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/levelgen/feature/SpikeFeature.java.patch b/paper-server/patches/sources/net/minecraft/world/level/levelgen/feature/SpikeFeature.java.patch
new file mode 100644
index 0000000000..9de3934ace
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/levelgen/feature/SpikeFeature.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/world/level/levelgen/feature/SpikeFeature.java
++++ b/net/minecraft/world/level/levelgen/feature/SpikeFeature.java
+@@ -113,6 +_,7 @@
+             endCrystal.setBeamTarget(config.getCrystalBeamTarget());
+             endCrystal.setInvulnerable(config.isCrystalInvulnerable());
+             endCrystal.moveTo(spike.getCenterX() + 0.5, spike.getHeight() + 1, spike.getCenterZ() + 0.5, random.nextFloat() * 360.0F, 0.0F);
++            endCrystal.generatedByDragonFight = true; // Paper - Fix invulnerable end crystals
+             level.addFreshEntity(endCrystal);
+             BlockPos blockPosx = endCrystal.blockPosition();
+             this.setBlock(level, blockPosx.below(), Blocks.BEDROCK.defaultBlockState());
diff --git a/paper-server/patches/sources/net/minecraft/world/level/levelgen/feature/treedecorators/CocoaDecorator.java.patch b/paper-server/patches/sources/net/minecraft/world/level/levelgen/feature/treedecorators/CocoaDecorator.java.patch
new file mode 100644
index 0000000000..f034ad2abb
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/levelgen/feature/treedecorators/CocoaDecorator.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/world/level/levelgen/feature/treedecorators/CocoaDecorator.java
++++ b/net/minecraft/world/level/levelgen/feature/treedecorators/CocoaDecorator.java
+@@ -26,6 +_,7 @@
+ 
+     @Override
+     public void place(TreeDecorator.Context context) {
++        if (context.logs().isEmpty()) return; // Paper - Fix crash when trying to generate without logs
+         RandomSource randomSource = context.random();
+         if (!(randomSource.nextFloat() >= this.probability)) {
+             List<BlockPos> list = context.logs();
diff --git a/paper-server/patches/sources/net/minecraft/world/level/levelgen/placement/PlacementContext.java.patch b/paper-server/patches/sources/net/minecraft/world/level/levelgen/placement/PlacementContext.java.patch
new file mode 100644
index 0000000000..1d38b1ee7c
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/levelgen/placement/PlacementContext.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/level/levelgen/placement/PlacementContext.java
++++ b/net/minecraft/world/level/levelgen/placement/PlacementContext.java
+@@ -17,7 +_,7 @@
+     private final Optional<PlacedFeature> topFeature;
+ 
+     public PlacementContext(WorldGenLevel level, ChunkGenerator generator, Optional<PlacedFeature> topFeature) {
+-        super(generator, level);
++        super(generator, level, level.getLevel()); // Paper - Flat bedrock generator settings
+         this.level = level;
+         this.generator = generator;
+         this.topFeature = topFeature;
diff --git a/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/LegacyStructureDataHandler.java.patch b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/LegacyStructureDataHandler.java.patch
new file mode 100644
index 0000000000..23f014d704
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/LegacyStructureDataHandler.java.patch
@@ -0,0 +1,24 @@
+--- a/net/minecraft/world/level/levelgen/structure/LegacyStructureDataHandler.java
++++ b/net/minecraft/world/level/levelgen/structure/LegacyStructureDataHandler.java
+@@ -217,17 +_,17 @@
+         }
+     }
+ 
+-    public static LegacyStructureDataHandler getLegacyStructureHandler(ResourceKey<Level> level, @Nullable DimensionDataStorage storage) {
+-        if (level == Level.OVERWORLD) {
++    public static LegacyStructureDataHandler getLegacyStructureHandler(ResourceKey<net.minecraft.world.level.dimension.LevelStem> level, @Nullable DimensionDataStorage storage) { // CraftBukkit
++        if (level == net.minecraft.world.level.dimension.LevelStem.OVERWORLD) { // CraftBukkit
+             return new LegacyStructureDataHandler(
+                 storage,
+                 ImmutableList.of("Monument", "Stronghold", "Village", "Mineshaft", "Temple", "Mansion"),
+                 ImmutableList.of("Village", "Mineshaft", "Mansion", "Igloo", "Desert_Pyramid", "Jungle_Pyramid", "Swamp_Hut", "Stronghold", "Monument")
+             );
+-        } else if (level == Level.NETHER) {
++        } else if (level == net.minecraft.world.level.dimension.LevelStem.NETHER) { // CraftBukkit
+             List<String> list = ImmutableList.of("Fortress");
+             return new LegacyStructureDataHandler(storage, list, list);
+-        } else if (level == Level.END) {
++        } else if (level == net.minecraft.world.level.dimension.LevelStem.END) { // CraftBukkit
+             List<String> list = ImmutableList.of("EndCity");
+             return new LegacyStructureDataHandler(storage, list, list);
+         } else {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/StructureCheck.java.patch b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/StructureCheck.java.patch
similarity index 67%
rename from paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/StructureCheck.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/StructureCheck.java.patch
index 9e6cb9ad46..d79e90f861 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/StructureCheck.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/StructureCheck.java.patch
@@ -1,6 +1,6 @@
 --- a/net/minecraft/world/level/levelgen/structure/StructureCheck.java
 +++ b/net/minecraft/world/level/levelgen/structure/StructureCheck.java
-@@ -40,7 +40,7 @@
+@@ -40,7 +_,7 @@
      private final ChunkScanAccess storageAccess;
      private final RegistryAccess registryAccess;
      private final StructureTemplateManager structureTemplateManager;
@@ -9,17 +9,17 @@
      private final ChunkGenerator chunkGenerator;
      private final RandomState randomState;
      private final LevelHeightAccessor heightAccessor;
-@@ -54,7 +54,7 @@
-         ChunkScanAccess chunkIoWorker,
-         RegistryAccess registryManager,
+@@ -54,7 +_,7 @@
+         ChunkScanAccess storageAccess,
+         RegistryAccess registryAccess,
          StructureTemplateManager structureTemplateManager,
--        ResourceKey<Level> worldKey,
-+        ResourceKey<net.minecraft.world.level.dimension.LevelStem> worldKey, // Paper - fix missing CB diff
+-        ResourceKey<Level> dimension,
++        ResourceKey<net.minecraft.world.level.dimension.LevelStem> dimension, // Paper - fix missing CB diff
          ChunkGenerator chunkGenerator,
-         RandomState noiseConfig,
-         LevelHeightAccessor world,
-@@ -74,6 +74,20 @@
-         this.fixerUpper = dataFixer;
+         RandomState randomState,
+         LevelHeightAccessor heightAccessor,
+@@ -74,6 +_,20 @@
+         this.fixerUpper = fixerUpper;
      }
  
 +    // Paper start - add missing structure salt configs
@@ -36,15 +36,15 @@
 +    }
 +    // Paper end - add missing structure seed configs
 +
-     public StructureCheckResult checkStart(ChunkPos pos, Structure type, StructurePlacement placement, boolean skipReferencedStructures) {
-         long l = pos.toLong();
-         Object2IntMap<Structure> object2IntMap = this.loadedChunks.get(l);
-@@ -83,7 +97,7 @@
-             StructureCheckResult structureCheckResult = this.tryLoadFromStorage(pos, type, skipReferencedStructures, l);
+     public StructureCheckResult checkStart(ChunkPos chunkPos, Structure structure, StructurePlacement placement, boolean skipKnownStructures) {
+         long packedChunkPos = chunkPos.toLong();
+         Object2IntMap<Structure> map = this.loadedChunks.get(packedChunkPos);
+@@ -83,7 +_,7 @@
+             StructureCheckResult structureCheckResult = this.tryLoadFromStorage(chunkPos, structure, skipKnownStructures, packedChunkPos);
              if (structureCheckResult != null) {
                  return structureCheckResult;
--            } else if (!placement.applyAdditionalChunkRestrictions(pos.x, pos.z, this.seed)) {
-+            } else if (!placement.applyAdditionalChunkRestrictions(pos.x, pos.z, this.seed, this.getSaltOverride(type))) { // Paper - add missing structure seed configs
+-            } else if (!placement.applyAdditionalChunkRestrictions(chunkPos.x, chunkPos.z, this.seed)) {
++            } else if (!placement.applyAdditionalChunkRestrictions(chunkPos.x, chunkPos.z, this.seed, this.getSaltOverride(structure))) { // Paper - add missing structure seed configs
                  return StructureCheckResult.START_NOT_PRESENT;
              } else {
-                 boolean bl = this.featureChecks
+                 boolean flag = this.featureChecks
diff --git a/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/StructurePiece.java.patch b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/StructurePiece.java.patch
new file mode 100644
index 0000000000..9baa2a5f6d
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/StructurePiece.java.patch
@@ -0,0 +1,119 @@
+--- a/net/minecraft/world/level/levelgen/structure/StructurePiece.java
++++ b/net/minecraft/world/level/levelgen/structure/StructurePiece.java
+@@ -47,7 +_,7 @@
+     private Rotation rotation;
+     protected int genDepth;
+     private final StructurePieceType type;
+-    private static final Set<Block> SHAPE_CHECK_BLOCKS = ImmutableSet.<Block>builder()
++    public static final Set<Block> SHAPE_CHECK_BLOCKS = ImmutableSet.<Block>builder() // PAIL private -> public
+         .add(Blocks.NETHER_BRICK_FENCE)
+         .add(Blocks.TORCH)
+         .add(Blocks.WALL_TORCH)
+@@ -189,6 +_,11 @@
+                 }
+ 
+                 level.setBlock(worldPos, blockstate, 2);
++                // CraftBukkit start - fluid handling is already done if we have a transformer generator access
++                if (level instanceof org.bukkit.craftbukkit.util.TransformerGeneratorAccess) {
++                    return;
++                }
++                // CraftBukkit end
+                 FluidState fluidState = level.getFluidState(worldPos);
+                 if (!fluidState.isEmpty()) {
+                     level.scheduleTick(worldPos, fluidState.getType(), 0);
+@@ -201,6 +_,38 @@
+         }
+     }
+ 
++    // CraftBukkit start
++    protected boolean placeCraftBlockEntity(ServerLevelAccessor serverLevelAccessor, BlockPos pos, org.bukkit.craftbukkit.block.CraftBlockEntityState<?> craftBlockEntityState, int flags) {
++        if (serverLevelAccessor instanceof org.bukkit.craftbukkit.util.TransformerGeneratorAccess transformerAccess) {
++            return transformerAccess.setCraftBlock(pos, craftBlockEntityState, flags);
++        }
++        boolean result = serverLevelAccessor.setBlock(pos, craftBlockEntityState.getHandle(), flags);
++        BlockEntity tileEntity = serverLevelAccessor.getBlockEntity(pos);
++        if (tileEntity != null) {
++            tileEntity.loadWithComponents(craftBlockEntityState.getSnapshotNBT(), serverLevelAccessor.registryAccess());
++        }
++        return result;
++    }
++
++    protected void placeCraftSpawner(ServerLevelAccessor worldAccess, BlockPos position, org.bukkit.entity.EntityType entityType, int i) {
++        // This method is used in structures that are generated by code and place spawners as they set the entity after the block was placed making it impossible for plugins to access that information
++        org.bukkit.craftbukkit.block.CraftCreatureSpawner spawner = (org.bukkit.craftbukkit.block.CraftCreatureSpawner) org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(worldAccess, position, Blocks.SPAWNER.defaultBlockState(), null);
++        spawner.setSpawnedType(entityType);
++        this.placeCraftBlockEntity(worldAccess, position, spawner, i);
++    }
++
++    protected void setCraftLootTable(ServerLevelAccessor worldAccess, BlockPos position, RandomSource randomSource, ResourceKey<LootTable> loottableKey) {
++        // This method is used in structures that use data markers to a loot table to loot containers as otherwise plugins won't have access to that information.
++        net.minecraft.world.level.block.entity.BlockEntity tileEntity = worldAccess.getBlockEntity(position);
++        if (tileEntity instanceof net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity tileEntityLootable) {
++            tileEntityLootable.setLootTable(loottableKey, randomSource.nextLong());
++            if (worldAccess instanceof org.bukkit.craftbukkit.util.TransformerGeneratorAccess transformerAccess) {
++                transformerAccess.setCraftBlock(position, (org.bukkit.craftbukkit.block.CraftBlockState) org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(worldAccess, position, tileEntity.getBlockState(), tileEntityLootable.saveWithFullMetadata(worldAccess.registryAccess())), 3);
++            }
++        }
++    }
++    // CraftBukkit end
++
+     protected boolean canBeReplaced(LevelReader level, int x, int y, int z, BoundingBox box) {
+         return true;
+     }
+@@ -429,11 +_,17 @@
+                 state = reorient(level, pos, Blocks.CHEST.defaultBlockState());
+             }
+ 
+-            level.setBlock(pos, state, 2);
+-            BlockEntity blockEntity = level.getBlockEntity(pos);
+-            if (blockEntity instanceof ChestBlockEntity) {
+-                ((ChestBlockEntity)blockEntity).setLootTable(lootTable, random.nextLong());
+-            }
++            // CraftBukkit start
++            // level.setBlock(pos, state, 2);
++            // BlockEntity blockEntity = level.getBlockEntity(pos);
++            // if (blockEntity instanceof ChestBlockEntity) {
++            //     ((ChestBlockEntity)blockEntity).setLootTable(lootTable, random.nextLong());
++            // }
++            org.bukkit.craftbukkit.block.CraftChest chestState = (org.bukkit.craftbukkit.block.CraftChest) org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(level, pos, state, null);
++            chestState.setLootTable(org.bukkit.craftbukkit.CraftLootTable.minecraftToBukkit(lootTable));
++            chestState.setSeed(random.nextLong());
++            this.placeCraftBlockEntity(level, pos, chestState, 2);
++            // CraftBukkit end
+ 
+             return true;
+         } else {
+@@ -446,11 +_,28 @@
+     ) {
+         BlockPos worldPos = this.getWorldPos(x, y, z);
+         if (box.isInside(worldPos) && !level.getBlockState(worldPos).is(Blocks.DISPENSER)) {
+-            this.placeBlock(level, Blocks.DISPENSER.defaultBlockState().setValue(DispenserBlock.FACING, facing), x, y, z, box);
+-            BlockEntity blockEntity = level.getBlockEntity(worldPos);
+-            if (blockEntity instanceof DispenserBlockEntity) {
+-                ((DispenserBlockEntity)blockEntity).setLootTable(lootTable, random.nextLong());
+-            }
++            // CraftBukkit start
++            // this.placeBlock(level, Blocks.DISPENSER.defaultBlockState().setValue(DispenserBlock.FACING, facing), x, y, z, box);
++            // BlockEntity blockEntity = level.getBlockEntity(worldPos);
++            // if (blockEntity instanceof DispenserBlockEntity) {
++            //     ((DispenserBlockEntity)blockEntity).setLootTable(lootTable, random.nextLong());
++            // }
++            if (!this.canBeReplaced(level, x, y, z, this.boundingBox)) {
++                return true;
++            }
++            BlockState dispenserBlockState = Blocks.DISPENSER.defaultBlockState().setValue(DispenserBlock.FACING, facing);
++            if (this.mirror != Mirror.NONE) {
++                dispenserBlockState = dispenserBlockState.mirror(this.mirror);
++            }
++            if (this.rotation != Rotation.NONE) {
++                dispenserBlockState = dispenserBlockState.rotate(this.rotation);
++            }
++
++            org.bukkit.craftbukkit.block.CraftDispenser dispenserState = (org.bukkit.craftbukkit.block.CraftDispenser) org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(level, worldPos, dispenserBlockState, null);
++            dispenserState.setLootTable(org.bukkit.craftbukkit.CraftLootTable.minecraftToBukkit(lootTable));
++            dispenserState.setSeed(random.nextLong());
++            this.placeCraftBlockEntity(level, worldPos, dispenserState, 2);
++            // CraftBukkit end
+ 
+             return true;
+         } else {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/StructureStart.java.patch b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/StructureStart.java.patch
new file mode 100644
index 0000000000..9efa80e8e3
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/StructureStart.java.patch
@@ -0,0 +1,54 @@
+--- a/net/minecraft/world/level/levelgen/structure/StructureStart.java
++++ b/net/minecraft/world/level/levelgen/structure/StructureStart.java
+@@ -30,6 +_,12 @@
+     @Nullable
+     private volatile BoundingBox cachedBoundingBox;
+ 
++    // CraftBukkit start
++    private static final org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry();
++    public org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer persistentDataContainer = new org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer(StructureStart.DATA_TYPE_REGISTRY);
++    public org.bukkit.event.world.AsyncStructureGenerateEvent.Cause generationEventCause = org.bukkit.event.world.AsyncStructureGenerateEvent.Cause.WORLD_GENERATION;
++    // CraftBukkit end
++
+     public StructureStart(Structure structure, ChunkPos chunkPos, int references, PiecesContainer pieceContainer) {
+         this.structure = structure;
+         this.chunkPos = chunkPos;
+@@ -87,11 +_,23 @@
+             BlockPos center = boundingBox.getCenter();
+             BlockPos blockPos = new BlockPos(center.getX(), boundingBox.minY(), center.getZ());
+ 
+-            for (StructurePiece structurePiece : list) {
+-                if (structurePiece.getBoundingBox().intersects(box)) {
+-                    structurePiece.postProcess(level, structureManager, generator, random, box, chunkPos, blockPos);
++            // CraftBukkit start
++            // for (StructurePiece structurePiece : list) {
++            //     if (structurePiece.getBoundingBox().intersects(box)) {
++            //         structurePiece.postProcess(level, structureManager, generator, random, box, chunkPos, blockPos);
++            //     }
++            // }
++            List<StructurePiece> pieces = list.stream().filter(piece -> piece.getBoundingBox().intersects(box)).toList();
++            if (!pieces.isEmpty()) {
++                org.bukkit.craftbukkit.util.TransformerGeneratorAccess transformerAccess = new org.bukkit.craftbukkit.util.TransformerGeneratorAccess();
++                transformerAccess.setHandle(level);
++                transformerAccess.setStructureTransformer(new org.bukkit.craftbukkit.util.CraftStructureTransformer(this.generationEventCause, level, structureManager, this.structure, box, chunkPos));
++                for (StructurePiece piece : pieces) {
++                    piece.postProcess(transformerAccess, structureManager, generator, random, box, chunkPos, blockPos);
+                 }
++                transformerAccess.getStructureTransformer().discard();
+             }
++            // CraftBukkit end
+ 
+             this.structure.afterPlace(level, structureManager, generator, random, box, chunkPos, this.pieceContainer);
+         }
+@@ -99,6 +_,11 @@
+ 
+     public CompoundTag createTag(StructurePieceSerializationContext context, ChunkPos chunkPos) {
+         CompoundTag compoundTag = new CompoundTag();
++        // CraftBukkit start - store persistent data in nbt
++        if (!this.persistentDataContainer.isEmpty()) {
++            compoundTag.put("StructureBukkitValues", this.persistentDataContainer.toTagCompound());
++        }
++        // CraftBukkit end
+         if (this.isValid()) {
+             compoundTag.putString("id", context.registryAccess().lookupOrThrow(Registries.STRUCTURE).getKey(this.structure).toString());
+             compoundTag.putInt("ChunkX", chunkPos.x);
diff --git a/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/placement/StructurePlacement.java.patch b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/placement/StructurePlacement.java.patch
new file mode 100644
index 0000000000..d79551ea59
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/placement/StructurePlacement.java.patch
@@ -0,0 +1,93 @@
+--- a/net/minecraft/world/level/levelgen/structure/placement/StructurePlacement.java
++++ b/net/minecraft/world/level/levelgen/structure/placement/StructurePlacement.java
+@@ -79,14 +_,30 @@
+         return this.exclusionZone;
+     }
+ 
++    @Deprecated @io.papermc.paper.annotation.DoNotUse // Paper - Add missing structure set seed configs
+     public boolean isStructureChunk(ChunkGeneratorStructureState structureState, int x, int z) {
++        // Paper start - Add missing structure set seed configs
++        return this.isStructureChunk(structureState, x, z, null);
++    }
++    public boolean isStructureChunk(ChunkGeneratorStructureState structureState, int x, int z, @org.jetbrains.annotations.Nullable net.minecraft.resources.ResourceKey<StructureSet> structureSetKey) {
++        Integer saltOverride = null;
++        if (structureSetKey != null) {
++            if (structureSetKey == net.minecraft.world.level.levelgen.structure.BuiltinStructureSets.MINESHAFTS) {
++                saltOverride = structureState.conf.mineshaftSeed;
++            } else if (structureSetKey == net.minecraft.world.level.levelgen.structure.BuiltinStructureSets.BURIED_TREASURES) {
++                saltOverride = structureState.conf.buriedTreasureSeed;
++            }
++        }
++        // Paper end - Add missing structure set seed configs
+         return this.isPlacementChunk(structureState, x, z)
+-            && this.applyAdditionalChunkRestrictions(x, z, structureState.getLevelSeed())
++            && this.applyAdditionalChunkRestrictions(x, z, structureState.getLevelSeed(), saltOverride) // Paper - Add missing structure set seed configs
+             && this.applyInteractionsWithOtherStructures(structureState, x, z);
+     }
+ 
+-    public boolean applyAdditionalChunkRestrictions(int regionX, int regionZ, long levelSeed) {
+-        return !(this.frequency < 1.0F) || this.frequencyReductionMethod.shouldGenerate(levelSeed, this.salt, regionX, regionZ, this.frequency);
++    // Paper start - Add missing structure set seed configs
++    public boolean applyAdditionalChunkRestrictions(int regionX, int regionZ, long levelSeed, @org.jetbrains.annotations.Nullable Integer saltOverride) {
++        return !(this.frequency < 1.0F) || this.frequencyReductionMethod.shouldGenerate(levelSeed, this.salt, regionX, regionZ, this.frequency, saltOverride);
++        // Paper end - Add missing structure set seed configs
+     }
+ 
+     public boolean applyInteractionsWithOtherStructures(ChunkGeneratorStructureState structureState, int x, int z) {
+@@ -101,25 +_,31 @@
+ 
+     public abstract StructurePlacementType<?> type();
+ 
+-    private static boolean probabilityReducer(long levelSeed, int regionX, int regionZ, int salt, float probability) {
++    private static boolean probabilityReducer(long levelSeed, int regionX, int regionZ, int salt, float probability, @org.jetbrains.annotations.Nullable Integer saltOverride) { // Paper - Add missing structure set seed configs; ignore here
+         WorldgenRandom worldgenRandom = new WorldgenRandom(new LegacyRandomSource(0L));
+         worldgenRandom.setLargeFeatureWithSalt(levelSeed, regionX, regionZ, salt);
+         return worldgenRandom.nextFloat() < probability;
+     }
+ 
+-    private static boolean legacyProbabilityReducerWithDouble(long baseSeed, int salt, int chunkX, int chunkZ, float probability) {
++    private static boolean legacyProbabilityReducerWithDouble(long baseSeed, int salt, int chunkX, int chunkZ, float probability, @org.jetbrains.annotations.Nullable Integer saltOverride) { // Paper - Add missing structure set seed configs
+         WorldgenRandom worldgenRandom = new WorldgenRandom(new LegacyRandomSource(0L));
++        if (saltOverride == null) { // Paper - Add missing structure set seed configs
+         worldgenRandom.setLargeFeatureSeed(baseSeed, chunkX, chunkZ);
++        // Paper start - Add missing structure set seed configs
++        } else {
++            worldgenRandom.setLargeFeatureWithSalt(baseSeed, chunkX, chunkZ, saltOverride);
++        }
++        // Paper end - Add missing structure set seed configs
+         return worldgenRandom.nextDouble() < probability;
+     }
+ 
+-    private static boolean legacyArbitrarySaltProbabilityReducer(long levelSeed, int salt, int regionX, int regionZ, float probability) {
++    private static boolean legacyArbitrarySaltProbabilityReducer(long levelSeed, int salt, int regionX, int regionZ, float probability, @org.jetbrains.annotations.Nullable Integer saltOverride) { // Paper - Add missing structure set seed configs
+         WorldgenRandom worldgenRandom = new WorldgenRandom(new LegacyRandomSource(0L));
+-        worldgenRandom.setLargeFeatureWithSalt(levelSeed, regionX, regionZ, 10387320);
++        worldgenRandom.setLargeFeatureWithSalt(levelSeed, regionX, regionZ, saltOverride != null ? saltOverride : HIGHLY_ARBITRARY_RANDOM_SALT); // Paper - Add missing structure set seed configs
+         return worldgenRandom.nextFloat() < probability;
+     }
+ 
+-    private static boolean legacyPillagerOutpostReducer(long levelSeed, int salt, int regionX, int regionZ, float probability) {
++    private static boolean legacyPillagerOutpostReducer(long levelSeed, int salt, int regionX, int regionZ, float probability, @org.jetbrains.annotations.Nullable Integer saltOverride) { // Paper - Add missing structure set seed configs; ignore here
+         int i = regionX >> 4;
+         int i1 = regionZ >> 4;
+         WorldgenRandom worldgenRandom = new WorldgenRandom(new LegacyRandomSource(0L));
+@@ -147,7 +_,7 @@
+ 
+     @FunctionalInterface
+     public interface FrequencyReducer {
+-        boolean shouldGenerate(long levelSeed, int i, int salt, int regionX, float regionZ);
++        boolean shouldGenerate(long levelSeed, int i, int salt, int regionX, float regionZ, @org.jetbrains.annotations.Nullable Integer saltOverride); // Paper - Add missing structure set seed configs
+     }
+ 
+     public static enum FrequencyReductionMethod implements StringRepresentable {
+@@ -167,8 +_,8 @@
+             this.reducer = reducer;
+         }
+ 
+-        public boolean shouldGenerate(long levelSeed, int salt, int regionX, int regionZ, float probability) {
+-            return this.reducer.shouldGenerate(levelSeed, salt, regionX, regionZ, probability);
++        public boolean shouldGenerate(long levelSeed, int salt, int regionX, int regionZ, float probability, @org.jetbrains.annotations.Nullable Integer saltOverride) { // Paper - Add missing structure set seed configs
++            return this.reducer.shouldGenerate(levelSeed, salt, regionX, regionZ, probability, saltOverride); // Paper - Add missing structure set seed configs
+         }
+ 
+         @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/DesertPyramidStructure.java.patch b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/structures/DesertPyramidStructure.java.patch
similarity index 52%
rename from paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/DesertPyramidStructure.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/structures/DesertPyramidStructure.java.patch
index 84696b809f..73853cb81e 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/DesertPyramidStructure.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/structures/DesertPyramidStructure.java.patch
@@ -1,18 +1,18 @@
 --- a/net/minecraft/world/level/levelgen/structure/structures/DesertPyramidStructure.java
 +++ b/net/minecraft/world/level/levelgen/structure/structures/DesertPyramidStructure.java
-@@ -68,6 +68,15 @@
+@@ -65,6 +_,15 @@
  
-     private static void placeSuspiciousSand(BoundingBox box, WorldGenLevel world, BlockPos pos) {
-         if (box.isInside(pos)) {
+     private static void placeSuspiciousSand(BoundingBox boundingBox, WorldGenLevel worldGenLevel, BlockPos pos) {
+         if (boundingBox.isInside(pos)) {
 +            // CraftBukkit start
-+            if (world instanceof org.bukkit.craftbukkit.util.TransformerGeneratorAccess transformerAccess) {
-+                org.bukkit.craftbukkit.block.CraftBrushableBlock brushableState = (org.bukkit.craftbukkit.block.CraftBrushableBlock) org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(world, pos, Blocks.SUSPICIOUS_SAND.defaultBlockState(), null);
++            if (worldGenLevel instanceof org.bukkit.craftbukkit.util.TransformerGeneratorAccess transformerAccess) {
++                org.bukkit.craftbukkit.block.CraftBrushableBlock brushableState = (org.bukkit.craftbukkit.block.CraftBrushableBlock) org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(worldGenLevel, pos, Blocks.SUSPICIOUS_SAND.defaultBlockState(), null);
 +                brushableState.setLootTable(org.bukkit.craftbukkit.CraftLootTable.minecraftToBukkit(BuiltInLootTables.DESERT_PYRAMID_ARCHAEOLOGY));
 +                brushableState.setSeed(pos.asLong());
 +                transformerAccess.setCraftBlock(pos, brushableState, 2);
 +                return;
 +            }
 +            // CraftBukkit end
-             world.setBlock(pos, Blocks.SUSPICIOUS_SAND.defaultBlockState(), 2);
-             world.getBlockEntity(pos, BlockEntityType.BRUSHABLE_BLOCK).ifPresent((brushableblockentity) -> {
-                 brushableblockentity.setLootTable(BuiltInLootTables.DESERT_PYRAMID_ARCHAEOLOGY, pos.asLong());
+             worldGenLevel.setBlock(pos, Blocks.SUSPICIOUS_SAND.defaultBlockState(), 2);
+             worldGenLevel.getBlockEntity(pos, BlockEntityType.BRUSHABLE_BLOCK)
+                 .ifPresent(brushableBlockEntity -> brushableBlockEntity.setLootTable(BuiltInLootTables.DESERT_PYRAMID_ARCHAEOLOGY, pos.asLong()));
diff --git a/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/structures/EndCityPieces.java.patch b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/structures/EndCityPieces.java.patch
new file mode 100644
index 0000000000..a024b0bfeb
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/structures/EndCityPieces.java.patch
@@ -0,0 +1,14 @@
+--- a/net/minecraft/world/level/levelgen/structure/structures/EndCityPieces.java
++++ b/net/minecraft/world/level/levelgen/structure/structures/EndCityPieces.java
+@@ -390,7 +_,10 @@
+             if (name.startsWith("Chest")) {
+                 BlockPos blockPos = pos.below();
+                 if (box.isInside(blockPos)) {
+-                    RandomizableContainer.setBlockEntityLootTable(level, random, blockPos, BuiltInLootTables.END_CITY_TREASURE);
++                    // CraftBukkit start - ensure block transformation
++                    // RandomizableContainer.setBlockEntityLootTable(level, random, blockPos, BuiltInLootTables.END_CITY_TREASURE);
++                    this.setCraftLootTable(level, blockPos, random, BuiltInLootTables.END_CITY_TREASURE);
++                    // CraftBukkit end
+                 }
+             } else if (box.isInside(pos) && Level.isInSpawnableBounds(pos)) {
+                 if (name.startsWith("Sentry")) {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/structures/IglooPieces.java.patch b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/structures/IglooPieces.java.patch
new file mode 100644
index 0000000000..55007e0dc7
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/structures/IglooPieces.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/level/levelgen/structure/structures/IglooPieces.java
++++ b/net/minecraft/world/level/levelgen/structure/structures/IglooPieces.java
+@@ -102,10 +_,13 @@
+         protected void handleDataMarker(String name, BlockPos pos, ServerLevelAccessor level, RandomSource random, BoundingBox box) {
+             if ("chest".equals(name)) {
+                 level.setBlock(pos, Blocks.AIR.defaultBlockState(), 3);
+-                BlockEntity blockEntity = level.getBlockEntity(pos.below());
+-                if (blockEntity instanceof ChestBlockEntity) {
+-                    ((ChestBlockEntity)blockEntity).setLootTable(BuiltInLootTables.IGLOO_CHEST, random.nextLong());
+-                }
++                // CraftBukkit start - ensure block transformation
++                // BlockEntity blockEntity = level.getBlockEntity(pos.below());
++                // if (blockEntity instanceof ChestBlockEntity) {
++                //     ((ChestBlockEntity)blockEntity).setLootTable(BuiltInLootTables.IGLOO_CHEST, random.nextLong());
++                // }
++                this.setCraftLootTable(level, pos.below(), random, BuiltInLootTables.IGLOO_CHEST);
++                // CraftBukkit end
+             }
+         }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/structures/MineshaftPieces.java.patch b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/structures/MineshaftPieces.java.patch
new file mode 100644
index 0000000000..be468da02b
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/structures/MineshaftPieces.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/level/levelgen/structure/structures/MineshaftPieces.java
++++ b/net/minecraft/world/level/levelgen/structure/structures/MineshaftPieces.java
+@@ -401,10 +_,13 @@
+                         BlockPos worldPos = this.getWorldPos(1, 0, i8);
+                         if (box.isInside(worldPos) && this.isInterior(level, 1, 0, i8, box)) {
+                             this.hasPlacedSpider = true;
+-                            level.setBlock(worldPos, Blocks.SPAWNER.defaultBlockState(), 2);
+-                            if (level.getBlockEntity(worldPos) instanceof SpawnerBlockEntity spawnerBlockEntity) {
+-                                spawnerBlockEntity.setEntityId(EntityType.CAVE_SPIDER, random);
+-                            }
++                            // CraftBukkit start
++                            // level.setBlock(worldPos, Blocks.SPAWNER.defaultBlockState(), 2);
++                            // if (level.getBlockEntity(worldPos) instanceof SpawnerBlockEntity spawnerBlockEntity) {
++                            //     spawnerBlockEntity.setEntityId(EntityType.CAVE_SPIDER, random);
++                            // }
++                            this.placeCraftSpawner(level, worldPos, org.bukkit.entity.EntityType.CAVE_SPIDER, 2);
++                            // CraftBukkit end
+                         }
+                     }
+                 }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/structures/NetherFortressPieces.java.patch b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/structures/NetherFortressPieces.java.patch
new file mode 100644
index 0000000000..bfd5ae0532
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/structures/NetherFortressPieces.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/level/levelgen/structure/structures/NetherFortressPieces.java
++++ b/net/minecraft/world/level/levelgen/structure/structures/NetherFortressPieces.java
+@@ -1265,10 +_,13 @@
+                 BlockPos worldPos = this.getWorldPos(3, 5, 5);
+                 if (box.isInside(worldPos)) {
+                     this.hasPlacedSpawner = true;
+-                    level.setBlock(worldPos, Blocks.SPAWNER.defaultBlockState(), 2);
+-                    if (level.getBlockEntity(worldPos) instanceof SpawnerBlockEntity spawnerBlockEntity) {
+-                        spawnerBlockEntity.setEntityId(EntityType.BLAZE, random);
+-                    }
++                    // CraftBukkit start
++                    // level.setBlock(worldPos, Blocks.SPAWNER.defaultBlockState(), 2);
++                    // if (level.getBlockEntity(worldPos) instanceof SpawnerBlockEntity spawnerBlockEntity) {
++                    //     spawnerBlockEntity.setEntityId(EntityType.BLAZE, random);
++                    // }
++                    this.placeCraftSpawner(level, worldPos, org.bukkit.entity.EntityType.BLAZE, 2);
++                    // CraftBukkit end
+                 }
+             }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/structures/OceanRuinPieces.java.patch b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/structures/OceanRuinPieces.java.patch
new file mode 100644
index 0000000000..ed5789e0f5
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/structures/OceanRuinPieces.java.patch
@@ -0,0 +1,31 @@
+--- a/net/minecraft/world/level/levelgen/structure/structures/OceanRuinPieces.java
++++ b/net/minecraft/world/level/levelgen/structure/structures/OceanRuinPieces.java
+@@ -314,14 +_,20 @@
+         @Override
+         protected void handleDataMarker(String name, BlockPos pos, ServerLevelAccessor level, RandomSource random, BoundingBox box) {
+             if ("chest".equals(name)) {
+-                level.setBlock(
+-                    pos, Blocks.CHEST.defaultBlockState().setValue(ChestBlock.WATERLOGGED, Boolean.valueOf(level.getFluidState(pos).is(FluidTags.WATER))), 2
+-                );
+-                BlockEntity blockEntity = level.getBlockEntity(pos);
+-                if (blockEntity instanceof ChestBlockEntity) {
+-                    ((ChestBlockEntity)blockEntity)
+-                        .setLootTable(this.isLarge ? BuiltInLootTables.UNDERWATER_RUIN_BIG : BuiltInLootTables.UNDERWATER_RUIN_SMALL, random.nextLong());
+-                }
++                // CraftBukkit start - transform block to ensure loot table is accessible
++                // level.setBlock(
++                //     pos, Blocks.CHEST.defaultBlockState().setValue(ChestBlock.WATERLOGGED, Boolean.valueOf(level.getFluidState(pos).is(FluidTags.WATER))), 2
++                // );
++                // BlockEntity blockEntity = level.getBlockEntity(pos);
++                // if (blockEntity instanceof ChestBlockEntity) {
++                //     ((ChestBlockEntity)blockEntity)
++                //         .setLootTable(this.isLarge ? BuiltInLootTables.UNDERWATER_RUIN_BIG : BuiltInLootTables.UNDERWATER_RUIN_SMALL, random.nextLong());
++                // }
++                org.bukkit.craftbukkit.block.CraftChest craftChest = (org.bukkit.craftbukkit.block.CraftChest) org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(level, pos, Blocks.CHEST.defaultBlockState().setValue(ChestBlock.WATERLOGGED, level.getFluidState(pos).is(FluidTags.WATER)), null);
++                craftChest.setSeed(random.nextLong());
++                craftChest.setLootTable(org.bukkit.craftbukkit.CraftLootTable.minecraftToBukkit(this.isLarge ? BuiltInLootTables.UNDERWATER_RUIN_BIG : BuiltInLootTables.UNDERWATER_RUIN_SMALL));
++                this.placeCraftBlockEntity(level, pos, craftChest, 2);
++                // CraftBukkit end
+             } else if ("drowned".equals(name)) {
+                 Drowned drowned = EntityType.DROWNED.create(level.getLevel(), EntitySpawnReason.STRUCTURE);
+                 if (drowned != null) {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/structures/ShipwreckPieces.java.patch b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/structures/ShipwreckPieces.java.patch
new file mode 100644
index 0000000000..08c3ca9aad
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/structures/ShipwreckPieces.java.patch
@@ -0,0 +1,14 @@
+--- a/net/minecraft/world/level/levelgen/structure/structures/ShipwreckPieces.java
++++ b/net/minecraft/world/level/levelgen/structure/structures/ShipwreckPieces.java
+@@ -121,7 +_,10 @@
+         protected void handleDataMarker(String name, BlockPos pos, ServerLevelAccessor level, RandomSource random, BoundingBox box) {
+             ResourceKey<LootTable> resourceKey = ShipwreckPieces.MARKERS_TO_LOOT.get(name);
+             if (resourceKey != null) {
+-                RandomizableContainer.setBlockEntityLootTable(level, random, pos.below(), resourceKey);
++                // CraftBukkit start
++                // RandomizableContainer.setBlockEntityLootTable(level, random, pos.below(), resourceKey);
++                this.setCraftLootTable(level, pos.below(), random, resourceKey);
++                // CraftBukkit end
+             }
+         }
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/structures/StrongholdPieces.java.patch b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/structures/StrongholdPieces.java.patch
new file mode 100644
index 0000000000..75de4affeb
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/structures/StrongholdPieces.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/level/levelgen/structure/structures/StrongholdPieces.java
++++ b/net/minecraft/world/level/levelgen/structure/structures/StrongholdPieces.java
+@@ -870,10 +_,13 @@
+                 BlockPos worldPos = this.getWorldPos(5, 3, 6);
+                 if (box.isInside(worldPos)) {
+                     this.hasPlacedSpawner = true;
+-                    level.setBlock(worldPos, Blocks.SPAWNER.defaultBlockState(), 2);
+-                    if (level.getBlockEntity(worldPos) instanceof SpawnerBlockEntity spawnerBlockEntity) {
+-                        spawnerBlockEntity.setEntityId(EntityType.SILVERFISH, random);
+-                    }
++                    // CraftBukkit start
++                    // level.setBlock(worldPos, Blocks.SPAWNER.defaultBlockState(), 2);
++                    // if (level.getBlockEntity(worldPos) instanceof SpawnerBlockEntity spawnerBlockEntity) {
++                    //     spawnerBlockEntity.setEntityId(EntityType.SILVERFISH, random);
++                    // }
++                    this.placeCraftSpawner(level, worldPos, org.bukkit.entity.EntityType.SILVERFISH, 2);
++                    // CraftBukkit end
+                 }
+             }
+         }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/structures/SwampHutPiece.java.patch b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/structures/SwampHutPiece.java.patch
new file mode 100644
index 0000000000..fe1c847fa4
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/structures/SwampHutPiece.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/level/levelgen/structure/structures/SwampHutPiece.java
++++ b/net/minecraft/world/level/levelgen/structure/structures/SwampHutPiece.java
+@@ -97,7 +_,7 @@
+                         witch.setPersistenceRequired();
+                         witch.moveTo(worldPos.getX() + 0.5, worldPos.getY(), worldPos.getZ() + 0.5, 0.0F, 0.0F);
+                         witch.finalizeSpawn(level, level.getCurrentDifficultyAt(worldPos), EntitySpawnReason.STRUCTURE, null);
+-                        level.addFreshEntityWithPassengers(witch);
++                        level.addFreshEntityWithPassengers(witch, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.CHUNK_GEN); // CraftBukkit - add SpawnReason
+                     }
+                 }
+             }
+@@ -116,7 +_,7 @@
+                     cat.setPersistenceRequired();
+                     cat.moveTo(worldPos.getX() + 0.5, worldPos.getY(), worldPos.getZ() + 0.5, 0.0F, 0.0F);
+                     cat.finalizeSpawn(level, level.getCurrentDifficultyAt(worldPos), EntitySpawnReason.STRUCTURE, null);
+-                    level.addFreshEntityWithPassengers(cat);
++                    level.addFreshEntityWithPassengers(cat, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.CHUNK_GEN); // CraftBukkit - add SpawnReason
+                 }
+             }
+         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/templatesystem/StructurePlaceSettings.java.patch b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/templatesystem/StructurePlaceSettings.java.patch
similarity index 63%
rename from paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/templatesystem/StructurePlaceSettings.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/templatesystem/StructurePlaceSettings.java.patch
index 60b3e8e667..dc109e1327 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/templatesystem/StructurePlaceSettings.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/templatesystem/StructurePlaceSettings.java.patch
@@ -1,25 +1,25 @@
 --- a/net/minecraft/world/level/levelgen/structure/templatesystem/StructurePlaceSettings.java
 +++ b/net/minecraft/world/level/levelgen/structure/templatesystem/StructurePlaceSettings.java
-@@ -22,7 +22,7 @@
-     private LiquidSettings liquidSettings;
+@@ -21,7 +_,7 @@
+     private LiquidSettings liquidSettings = LiquidSettings.APPLY_WATERLOGGING;
      @Nullable
      private RandomSource random;
 -    private int palette;
 +    public int palette = -1; // CraftBukkit - Set initial value so we know if the palette has been set forcefully
-     private final List<StructureProcessor> processors;
+     private final List<StructureProcessor> processors = Lists.newArrayList();
      private boolean knownShape;
      private boolean finalizeEntities;
-@@ -149,6 +149,13 @@
- 
-         if (i == 0) {
+@@ -142,6 +_,13 @@
+         int size = palettes.size();
+         if (size == 0) {
              throw new IllegalStateException("No palettes");
 +        // CraftBukkit start
 +        } else if (this.palette >= 0) {
-+            if (this.palette >= i) {
-+                throw new IllegalArgumentException("Palette index out of bounds. Got " + this.palette + " where there are only " + i + " palettes available.");
++            if (this.palette >= size) {
++                throw new IllegalArgumentException("Palette index out of bounds. Got " + this.palette + " where there are only " + size + " palettes available.");
 +            }
-+            return infoLists.get(this.palette);
++            return palettes.get(this.palette);
 +        // CraftBukkit end
          } else {
-             return (StructureTemplate.Palette) infoLists.get(this.getRandom(pos).nextInt(i));
+             return palettes.get(this.getRandom(pos).nextInt(size));
          }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate.java.patch b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate.java.patch
new file mode 100644
index 0000000000..c88b4e61e3
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate.java.patch
@@ -0,0 +1,159 @@
+--- a/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate.java
++++ b/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate.java
+@@ -54,6 +_,10 @@
+ import net.minecraft.world.phys.Vec3;
+ import net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape;
+ import net.minecraft.world.phys.shapes.DiscreteVoxelShape;
++// CraftBukkit start
++import org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer;
++import org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry;
++// CraftBukkit end
+ 
+ public class StructureTemplate {
+     public static final String PALETTE_TAG = "palette";
+@@ -71,6 +_,10 @@
+     public final List<StructureTemplate.StructureEntityInfo> entityInfoList = Lists.newArrayList();
+     private Vec3i size = Vec3i.ZERO;
+     private String author = "?";
++    // CraftBukkit start - data containers
++    private static final CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new CraftPersistentDataTypeRegistry();
++    public CraftPersistentDataContainer persistentDataContainer = new CraftPersistentDataContainer(StructureTemplate.DATA_TYPE_REGISTRY);
++    // CraftBukkit end
+ 
+     public Vec3i getSize() {
+         return this.size;
+@@ -245,6 +_,19 @@
+         if (this.palettes.isEmpty()) {
+             return false;
+         } else {
++            // CraftBukkit start
++            // We only want the TransformerGeneratorAccess at certain locations because in here are many "block update" calls that shouldn't be transformed
++            ServerLevelAccessor wrappedAccess = serverLevel;
++            org.bukkit.craftbukkit.util.CraftStructureTransformer structureTransformer = null;
++            if (wrappedAccess instanceof org.bukkit.craftbukkit.util.TransformerGeneratorAccess transformerAccess) {
++                serverLevel = transformerAccess.getHandle();
++                structureTransformer = transformerAccess.getStructureTransformer();
++                // The structureTransformer is not needed if we can not transform blocks therefore we can save a little bit of performance doing this
++                if (structureTransformer != null && !structureTransformer.canTransformBlocks()) {
++                    structureTransformer = null;
++                }
++            }
++            // CraftBukkit end
+             List<StructureTemplate.StructureBlockInfo> list = settings.getRandomPalette(this.palettes, offset).blocks();
+             if ((!list.isEmpty() || !settings.isIgnoreEntities() && !this.entityInfoList.isEmpty())
+                 && this.size.getX() >= 1
+@@ -268,10 +_,29 @@
+                         BlockState blockState = structureBlockInfo.state.mirror(settings.getMirror()).rotate(settings.getRotation());
+                         if (structureBlockInfo.nbt != null) {
+                             BlockEntity blockEntity = serverLevel.getBlockEntity(blockPos);
+-                            Clearable.tryClear(blockEntity);
++                            // Paper start - Fix NBT pieces overriding a block entity during worldgen deadlock
++                            if (!(serverLevel instanceof net.minecraft.world.level.WorldGenLevel)) {
++                                Clearable.tryClear(blockEntity);
++                            }
++                            // Paper end - Fix NBT pieces overriding a block entity during worldgen deadlock
+                             serverLevel.setBlock(blockPos, Blocks.BARRIER.defaultBlockState(), 20);
+                         }
+ 
++                        // CraftBukkit start
++                        if (structureTransformer != null) {
++                            org.bukkit.craftbukkit.block.CraftBlockState craftBlockState = (org.bukkit.craftbukkit.block.CraftBlockState) org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(serverLevel, blockPos, blockState, null);
++                            if (structureBlockInfo.nbt != null && craftBlockState instanceof org.bukkit.craftbukkit.block.CraftBlockEntityState<?> entityState) {
++                                entityState.loadData(structureBlockInfo.nbt);
++                                if (craftBlockState instanceof org.bukkit.craftbukkit.block.CraftLootable<?> craftLootable) {
++                                    craftLootable.setSeed(random.nextLong());
++                                }
++                            }
++                            craftBlockState = structureTransformer.transformCraftState(craftBlockState);
++                            blockState = craftBlockState.getHandle();
++                            structureBlockInfo = new StructureTemplate.StructureBlockInfo(blockPos, blockState, (craftBlockState instanceof org.bukkit.craftbukkit.block.CraftBlockEntityState<?> craftBlockEntityState ? craftBlockEntityState.getSnapshotNBT() : null));
++                        }
++                        // CraftBukkit end
++
+                         if (serverLevel.setBlock(blockPos, blockState, flags)) {
+                             i = Math.min(i, blockPos.getX());
+                             i1 = Math.min(i1, blockPos.getY());
+@@ -283,7 +_,7 @@
+                             if (structureBlockInfo.nbt != null) {
+                                 BlockEntity blockEntity = serverLevel.getBlockEntity(blockPos);
+                                 if (blockEntity != null) {
+-                                    if (blockEntity instanceof RandomizableContainer) {
++                                    if (structureTransformer == null && blockEntity instanceof RandomizableContainer) { // CraftBukkit - only process if don't have a transformer access (Was already set above) - SPIGOT-7520: Use structureTransformer as check, so that it is the same as above
+                                         structureBlockInfo.nbt.putLong("LootTableSeed", random.nextLong());
+                                     }
+ 
+@@ -366,7 +_,11 @@
+                         if (pair1.getSecond() != null) {
+                             BlockEntity blockEntity = serverLevel.getBlockEntity(blockPos4);
+                             if (blockEntity != null) {
+-                                blockEntity.setChanged();
++                                // Paper start - Fix NBT pieces overriding a block entity during worldgen deadlock
++                                if (!(serverLevel instanceof net.minecraft.world.level.WorldGenLevel)) {
++                                    blockEntity.setChanged();
++                                }
++                                // Paper end - Fix NBT pieces overriding a block entity during worldgen deadlock
+                             }
+                         }
+                     }
+@@ -374,7 +_,7 @@
+ 
+                 if (!settings.isIgnoreEntities()) {
+                     this.placeEntities(
+-                        serverLevel,
++                        wrappedAccess, // CraftBukkit
+                         offset,
+                         settings.getMirror(),
+                         settings.getRotation(),
+@@ -491,11 +_,13 @@
+     }
+ 
+     private static Optional<Entity> createEntityIgnoreException(ServerLevelAccessor level, CompoundTag tag) {
+-        try {
+-            return EntityType.create(tag, level.getLevel(), EntitySpawnReason.STRUCTURE);
+-        } catch (Exception var3) {
+-            return Optional.empty();
+-        }
++        // CraftBukkit start
++        // try {
++        return EntityType.create(tag, level.getLevel(), EntitySpawnReason.STRUCTURE, true); // Paper - Don't fire sync event during generation
++        // } catch (Exception var3) {
++        //     return Optional.empty();
++        // }
++        // CraftBukkit end
+     }
+ 
+     public Vec3i getSize(Rotation rotation) {
+@@ -688,6 +_,11 @@
+ 
+         tag.put("entities", listTag3);
+         tag.put("size", this.newIntegerList(this.size.getX(), this.size.getY(), this.size.getZ()));
++        // CraftBukkit start - PDC
++        if (!this.persistentDataContainer.isEmpty()) {
++            tag.put("BukkitValues", this.persistentDataContainer.toTagCompound());
++        }
++        // CraftBukkit end
+         return NbtUtils.addCurrentDataVersion(tag);
+     }
+ 
+@@ -720,6 +_,12 @@
+                 this.entityInfoList.add(new StructureTemplate.StructureEntityInfo(vec3, blockPos, compound1));
+             }
+         }
++
++        // CraftBukkit start - PDC
++        if (tag.get("BukkitValues") instanceof CompoundTag compoundTag) {
++            this.persistentDataContainer.putAll(compoundTag);
++        }
++        // CraftBukkit end
+     }
+ 
+     private void loadPalette(HolderGetter<Block> blockGetter, ListTag paletteTag, ListTag blocksTag) {
+@@ -828,7 +_,7 @@
+ 
+     public static final class Palette {
+         private final List<StructureTemplate.StructureBlockInfo> blocks;
+-        private final Map<Block, List<StructureTemplate.StructureBlockInfo>> cache = Maps.newHashMap();
++        private final Map<Block, List<StructureTemplate.StructureBlockInfo>> cache = Maps.newConcurrentMap(); // Paper - Fix CME due to this collection being shared across threads
+         @Nullable
+         private List<StructureTemplate.JigsawBlockInfo> cachedJigsaws;
+ 
diff --git a/paper-server/patches/sources/net/minecraft/world/level/material/FlowingFluid.java.patch b/paper-server/patches/sources/net/minecraft/world/level/material/FlowingFluid.java.patch
new file mode 100644
index 0000000000..9457eaacf4
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/material/FlowingFluid.java.patch
@@ -0,0 +1,137 @@
+--- a/net/minecraft/world/level/material/FlowingFluid.java
++++ b/net/minecraft/world/level/material/FlowingFluid.java
+@@ -118,6 +_,15 @@
+                 FluidState newLiquid = this.getNewLiquid(level, blockPos, blockState1);
+                 Fluid type = newLiquid.getType();
+                 if (fluidState1.canBeReplacedWith(level, blockPos, type, Direction.DOWN) && canHoldSpecificFluid(level, blockPos, blockState1, type)) {
++                    // CraftBukkit start
++                    org.bukkit.block.Block source = org.bukkit.craftbukkit.block.CraftBlock.at(level, pos);
++                    org.bukkit.event.block.BlockFromToEvent event = new org.bukkit.event.block.BlockFromToEvent(source, org.bukkit.block.BlockFace.DOWN);
++                    level.getCraftServer().getPluginManager().callEvent(event);
++
++                    if (event.isCancelled()) {
++                        return;
++                    }
++                    // CraftBukkit end
+                     this.spreadTo(level, blockPos, blockState1, Direction.DOWN, newLiquid);
+                     if (this.sourceNeighborCount(level, pos) >= 3) {
+                         this.spreadToSides(level, pos, fluidState, blockState);
+@@ -146,7 +_,18 @@
+                 Direction direction = entry.getKey();
+                 FluidState fluidState1 = entry.getValue();
+                 BlockPos blockPos = pos.relative(direction);
+-                this.spreadTo(level, blockPos, level.getBlockState(blockPos), direction, fluidState1);
++                final BlockState blockStateIfLoaded = level.getBlockStateIfLoaded(blockPos); // Paper - Prevent chunk loading from fluid flowing
++                if (blockStateIfLoaded == null) continue; // Paper - Prevent chunk loading from fluid flowing
++                // CraftBukkit start
++                org.bukkit.block.Block source = org.bukkit.craftbukkit.block.CraftBlock.at(level, pos);
++                org.bukkit.event.block.BlockFromToEvent event = new org.bukkit.event.block.BlockFromToEvent(source, org.bukkit.craftbukkit.block.CraftBlock.notchToBlockFace(direction));
++                level.getCraftServer().getPluginManager().callEvent(event);
++
++                if (event.isCancelled()) {
++                    continue;
++                }
++                // CraftBukkit end
++                this.spreadTo(level, blockPos, blockStateIfLoaded, direction, fluidState1); // Paper - Prevent chunk loading from fluid flowing
+             }
+         }
+     }
+@@ -158,7 +_,8 @@
+ 
+         for (Direction direction : Direction.Plane.HORIZONTAL) {
+             BlockPos blockPos = mutableBlockPos.setWithOffset(pos, direction);
+-            BlockState blockState = level.getBlockState(blockPos);
++            BlockState blockState = level.getBlockStateIfLoaded(blockPos); // Paper - Prevent chunk loading from fluid flowing
++            if (blockState == null) continue; // Paper - Prevent chunk loading from fluid flowing
+             FluidState fluidState = blockState.getFluidState();
+             if (fluidState.getType().isSame(this) && canPassThroughWall(direction, level, pos, state, blockPos, blockState)) {
+                 if (fluidState.isSource()) {
+@@ -252,13 +_,14 @@
+             liquidBlockContainer.placeLiquid(level, pos, blockState, fluidState);
+         } else {
+             if (!blockState.isAir()) {
+-                this.beforeDestroyingBlock(level, pos, blockState);
++                this.beforeDestroyingBlock(level, pos, blockState, pos.relative(direction.getOpposite())); // Paper - Add BlockBreakBlockEvent
+             }
+ 
+             level.setBlock(pos, fluidState.createLegacyBlock(), 3);
+         }
+     }
+ 
++    protected void beforeDestroyingBlock(LevelAccessor world, BlockPos pos, BlockState state, BlockPos source) { beforeDestroyingBlock(world, pos, state); } // Paper - Add BlockBreakBlockEvent
+     protected abstract void beforeDestroyingBlock(LevelAccessor level, BlockPos pos, BlockState state);
+ 
+     protected int getSlopeDistance(LevelReader level, BlockPos pos, int depth, Direction direction, BlockState state, FlowingFluid.SpreadContext spreadContext) {
+@@ -267,7 +_,8 @@
+         for (Direction direction1 : Direction.Plane.HORIZONTAL) {
+             if (direction1 != direction) {
+                 BlockPos blockPos = pos.relative(direction1);
+-                BlockState blockState = spreadContext.getBlockState(blockPos);
++                BlockState blockState = spreadContext.getBlockStateIfLoaded(blockPos); // Paper - Prevent chunk loading from fluid flowing
++                if (blockState == null) continue; // Paper - Prevent chunk loading from fluid flowing
+                 FluidState fluidState = blockState.getFluidState();
+                 if (this.canPassThrough(level, this.getFlowing(), pos, state, direction1, blockPos, blockState, fluidState)) {
+                     if (spreadContext.isHole(blockPos)) {
+@@ -334,7 +_,8 @@
+ 
+         for (Direction direction : Direction.Plane.HORIZONTAL) {
+             BlockPos blockPos = pos.relative(direction);
+-            BlockState blockState = level.getBlockState(blockPos);
++            BlockState blockState = level.getBlockStateIfLoaded(blockPos); // Paper - Prevent chunk loading from fluid flowing
++            if (blockState == null) continue; // Paper - Prevent chunk loading from fluid flowing
+             FluidState fluidState = blockState.getFluidState();
+             if (this.canMaybePassThrough(level, pos, state, direction, blockPos, blockState, fluidState)) {
+                 FluidState newLiquid = this.getNewLiquid(level, blockPos, blockState);
+@@ -405,10 +_,24 @@
+             if (newLiquid.isEmpty()) {
+                 fluidState = newLiquid;
+                 blockState = Blocks.AIR.defaultBlockState();
++                // CraftBukkit start
++                org.bukkit.event.block.FluidLevelChangeEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callFluidLevelChangeEvent(level, pos, blockState);
++                if (event.isCancelled()) {
++                    return;
++                }
++                blockState = ((org.bukkit.craftbukkit.block.data.CraftBlockData) event.getNewData()).getState();
++                // CraftBukkit end
+                 level.setBlock(pos, blockState, 3);
+             } else if (!newLiquid.equals(fluidState)) {
+                 fluidState = newLiquid;
+                 blockState = newLiquid.createLegacyBlock();
++                // CraftBukkit start
++                org.bukkit.event.block.FluidLevelChangeEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callFluidLevelChangeEvent(level, pos, blockState);
++                if (event.isCancelled()) {
++                    return;
++                }
++                blockState = ((org.bukkit.craftbukkit.block.data.CraftBlockData) event.getNewData()).getState();
++                // CraftBukkit end
+                 level.setBlock(pos, blockState, 3);
+                 level.scheduleTick(pos, newLiquid.getType(), spreadDelay);
+             }
+@@ -476,9 +_,26 @@
+         public BlockState getBlockState(BlockPos pos) {
+             return this.getBlockState(pos, this.getCacheKey(pos));
+         }
++        // Paper start - Prevent chunk loading from fluid flowing
++        public @javax.annotation.Nullable BlockState getBlockStateIfLoaded(BlockPos pos) {
++            return this.getBlockState(pos, this.getCacheKey(pos), false);
++        }
++        // Paper end - Prevent chunk loading from fluid flowing
+ 
+         private BlockState getBlockState(BlockPos pos, short cacheKey) {
+-            return this.stateCache.computeIfAbsent(cacheKey, s -> this.level.getBlockState(pos));
++        // Paper start - Prevent chunk loading from fluid flowing
++            return getBlockState(pos, cacheKey, true);
++        }
++        private @javax.annotation.Nullable BlockState getBlockState(BlockPos pos, short packed, boolean load) {
++            BlockState blockState = this.stateCache.get(packed);
++            if (blockState == null) {
++                blockState = load ? level.getBlockState(pos) : level.getBlockStateIfLoaded(pos);
++                if (blockState != null) {
++                    this.stateCache.put(packed, blockState);
++                }
++            }
++            return blockState;
++        // Paper end - Prevent chunk loading from fluid flowing
+         }
+ 
+         public boolean isHole(BlockPos pos) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/material/FluidState.java.patch b/paper-server/patches/sources/net/minecraft/world/level/material/FluidState.java.patch
similarity index 64%
rename from paper-server/patches/unapplied/net/minecraft/world/level/material/FluidState.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/material/FluidState.java.patch
index 78b4ee0a64..c37c2e8733 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/material/FluidState.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/material/FluidState.java.patch
@@ -1,18 +1,18 @@
 --- a/net/minecraft/world/level/material/FluidState.java
 +++ b/net/minecraft/world/level/material/FluidState.java
-@@ -26,9 +26,11 @@
+@@ -26,9 +_,11 @@
      public static final Codec<FluidState> CODEC = codec(BuiltInRegistries.FLUID.byNameCodec(), Fluid::defaultFluidState).stable();
      public static final int AMOUNT_MAX = 9;
      public static final int AMOUNT_FULL = 8;
 +    protected final boolean isEmpty; // Paper - Perf: moved from isEmpty()
  
-     public FluidState(Fluid fluid, Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap, MapCodec<FluidState> codec) {
-         super(fluid, propertyMap, codec);
-+        this.isEmpty = fluid.isEmpty(); // Paper - Perf: moved from isEmpty()
+     public FluidState(Fluid owner, Reference2ObjectArrayMap<Property<?>, Comparable<?>> values, MapCodec<FluidState> propertiesCodec) {
+         super(owner, values, propertiesCodec);
++        this.isEmpty = owner.isEmpty(); // Paper - Perf: moved from isEmpty()
      }
  
      public Fluid getType() {
-@@ -44,7 +46,7 @@
+@@ -44,7 +_,7 @@
      }
  
      public boolean isEmpty() {
@@ -20,4 +20,4 @@
 +        return this.isEmpty; // Paper - Perf: moved into constructor
      }
  
-     public float getHeight(BlockGetter world, BlockPos pos) {
+     public float getHeight(BlockGetter level, BlockPos pos) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/material/LavaFluid.java.patch b/paper-server/patches/sources/net/minecraft/world/level/material/LavaFluid.java.patch
similarity index 50%
rename from paper-server/patches/unapplied/net/minecraft/world/level/material/LavaFluid.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/material/LavaFluid.java.patch
index de343949f4..ce90c9af15 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/material/LavaFluid.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/material/LavaFluid.java.patch
@@ -1,48 +1,48 @@
 --- a/net/minecraft/world/level/material/LavaFluid.java
 +++ b/net/minecraft/world/level/material/LavaFluid.java
-@@ -85,6 +85,13 @@
- 
-                     if (iblockdata.isAir()) {
-                         if (this.hasFlammableNeighbours(world, blockposition1)) {
+@@ -88,6 +_,13 @@
+                     BlockState blockState = level.getBlockState(blockPos);
+                     if (blockState.isAir()) {
+                         if (this.hasFlammableNeighbours(level, blockPos)) {
 +                            // CraftBukkit start - Prevent lava putting something on fire
-+                            if (world.getBlockState(blockposition1).getBlock() != Blocks.FIRE) {
-+                                if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(world, blockposition1, pos).isCancelled()) {
++                            if (level.getBlockState(blockPos).getBlock() != Blocks.FIRE) {
++                                if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(level, blockPos, pos).isCancelled()) {
 +                                    continue;
 +                                }
 +                            }
 +                            // CraftBukkit end
-                             world.setBlockAndUpdate(blockposition1, BaseFireBlock.getState(world, blockposition1));
+                             level.setBlockAndUpdate(blockPos, BaseFireBlock.getState(level, blockPos));
                              return;
                          }
-@@ -101,6 +108,14 @@
+@@ -103,6 +_,14 @@
                      }
  
-                     if (world.isEmptyBlock(blockposition2.above()) && this.isFlammable(world, blockposition2)) {
+                     if (level.isEmptyBlock(blockPos1.above()) && this.isFlammable(level, blockPos1)) {
 +                        // CraftBukkit start - Prevent lava putting something on fire
-+                        BlockPos up = blockposition2.above();
-+                        if (world.getBlockState(up).getBlock() != Blocks.FIRE) {
-+                            if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(world, up, pos).isCancelled()) {
++                        BlockPos up = blockPos1.above();
++                        if (level.getBlockState(up).getBlock() != Blocks.FIRE) {
++                            if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(level, up, pos).isCancelled()) {
 +                                continue;
 +                            }
 +                        }
 +                        // CraftBukkit end
-                         world.setBlockAndUpdate(blockposition2.above(), BaseFireBlock.getState(world, blockposition2));
+                         level.setBlockAndUpdate(blockPos1.above(), BaseFireBlock.getState(level, blockPos1));
                      }
                  }
-@@ -196,7 +211,11 @@
- 
-             if (this.is(FluidTags.LAVA) && fluid1.is(FluidTags.WATER)) {
-                 if (state.getBlock() instanceof LiquidBlock) {
--                    world.setBlock(pos, Blocks.STONE.defaultBlockState(), 3);
+@@ -195,7 +_,11 @@
+             FluidState fluidState1 = level.getFluidState(pos);
+             if (this.is(FluidTags.LAVA) && fluidState1.is(FluidTags.WATER)) {
+                 if (blockState.getBlock() instanceof LiquidBlock) {
+-                    level.setBlock(pos, Blocks.STONE.defaultBlockState(), 3);
 +                    // CraftBukkit start
-+                    if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(world.getMinecraftWorld(), pos, Blocks.STONE.defaultBlockState(), 3)) {
++                    if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(level.getMinecraftWorld(), pos, Blocks.STONE.defaultBlockState(), 3)) {
 +                        return;
 +                    }
 +                    // CraftBukkit end
                  }
  
-                 this.fizz(world, pos);
-@@ -214,7 +233,7 @@
+                 this.fizz(level, pos);
+@@ -213,7 +_,7 @@
  
      @Override
      protected float getExplosionResistance() {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/material/WaterFluid.java.patch b/paper-server/patches/sources/net/minecraft/world/level/material/WaterFluid.java.patch
similarity index 65%
rename from paper-server/patches/unapplied/net/minecraft/world/level/material/WaterFluid.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/material/WaterFluid.java.patch
index f615848dea..59826b7446 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/material/WaterFluid.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/material/WaterFluid.java.patch
@@ -1,21 +1,21 @@
 --- a/net/minecraft/world/level/material/WaterFluid.java
 +++ b/net/minecraft/world/level/material/WaterFluid.java
-@@ -81,7 +81,14 @@
-         return world.getGameRules().getBoolean(GameRules.RULE_WATER_SOURCE_CONVERSION);
+@@ -74,7 +_,13 @@
+     protected boolean canConvertToSource(ServerLevel level) {
+         return level.getGameRules().getBoolean(GameRules.RULE_WATER_SOURCE_CONVERSION);
      }
- 
+-
 +    // Paper start - Add BlockBreakBlockEvent
-     @Override
-+    protected void beforeDestroyingBlock(LevelAccessor world, BlockPos pos, BlockState state,  BlockPos source) {
++     @Override
++    protected void beforeDestroyingBlock(LevelAccessor world, BlockPos pos, BlockState state, BlockPos source) {
 +        BlockEntity tileentity = state.hasBlockEntity() ? world.getBlockEntity(pos) : null;
 +        Block.dropResources(state, world, pos, tileentity, source);
 +    }
 +    // Paper end - Add BlockBreakBlockEvent
-+    @Override
-     protected void beforeDestroyingBlock(LevelAccessor world, BlockPos pos, BlockState state) {
-         BlockEntity blockEntity = state.hasBlockEntity() ? world.getBlockEntity(pos) : null;
-         Block.dropResources(state, world, pos, blockEntity);
-@@ -119,7 +126,7 @@
+     @Override
+     protected void beforeDestroyingBlock(LevelAccessor level, BlockPos pos, BlockState state) {
+         BlockEntity blockEntity = state.hasBlockEntity() ? level.getBlockEntity(pos) : null;
+@@ -113,7 +_,7 @@
  
      @Override
      protected float getExplosionResistance() {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java.patch b/paper-server/patches/sources/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java.patch
similarity index 74%
rename from paper-server/patches/unapplied/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java.patch
index 96be982191..94a7ed2e86 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java.patch
@@ -1,12 +1,12 @@
 --- a/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java
 +++ b/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java
-@@ -478,7 +478,12 @@
+@@ -480,7 +_,12 @@
      }
  
-     protected static PathType getPathTypeFromState(BlockGetter world, BlockPos pos) {
--        BlockState blockState = world.getBlockState(pos);
+     protected static PathType getPathTypeFromState(BlockGetter level, BlockPos pos) {
+-        BlockState blockState = level.getBlockState(pos);
 +        // Paper start - Do not load chunks during pathfinding
-+        BlockState blockState = world.getBlockStateIfLoaded(pos);
++        BlockState blockState = level.getBlockStateIfLoaded(pos);
 +        if (blockState == null) {
 +            return PathType.BLOCKED;
 +        }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/portal/PortalForcer.java.patch b/paper-server/patches/sources/net/minecraft/world/level/portal/PortalForcer.java.patch
new file mode 100644
index 0000000000..76fbe68029
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/portal/PortalForcer.java.patch
@@ -0,0 +1,115 @@
+--- a/net/minecraft/world/level/portal/PortalForcer.java
++++ b/net/minecraft/world/level/portal/PortalForcer.java
+@@ -38,18 +_,32 @@
+         this.level = level;
+     }
+ 
++    @io.papermc.paper.annotation.DoNotUse // Paper
+     public Optional<BlockPos> findClosestPortalPosition(BlockPos exitPos, boolean isNether, WorldBorder worldBorder) {
++        // CraftBukkit start
++        return this.findClosestPortalPosition(exitPos, worldBorder, isNether ? 16 : 128); // Search Radius
++    }
++
++    public Optional<BlockPos> findClosestPortalPosition(BlockPos exitPos, WorldBorder worldBorder, int i) {
+         PoiManager poiManager = this.level.getPoiManager();
+-        int i = isNether ? 16 : 128;
++        // int i = isNether ? 16 : 128;
++        // CraftBukkit end
+         poiManager.ensureLoadedAndValid(this.level, exitPos, i);
+         return poiManager.getInSquare(holder -> holder.is(PoiTypes.NETHER_PORTAL), exitPos, i, PoiManager.Occupancy.ANY)
+             .map(PoiRecord::getPos)
+             .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))) // Paper - Configurable nether ceiling damage
+             .filter(blockPos -> this.level.getBlockState(blockPos).hasProperty(BlockStateProperties.HORIZONTAL_AXIS))
+             .min(Comparator.<BlockPos>comparingDouble(blockPos -> blockPos.distSqr(exitPos)).thenComparingInt(Vec3i::getY));
+     }
+ 
+     public Optional<BlockUtil.FoundRectangle> createPortal(BlockPos pos, Direction.Axis axis) {
++        // CraftBukkit start
++        return this.createPortal(pos, axis, null, 16);
++    }
++
++    public Optional<BlockUtil.FoundRectangle> createPortal(BlockPos pos, Direction.Axis axis, net.minecraft.world.entity.Entity entity, int createRadius) {
++        // CraftBukkit end
+         Direction direction = Direction.get(Direction.AxisDirection.POSITIVE, axis);
+         double d = -1.0;
+         BlockPos blockPos = null;
+@@ -57,10 +_,15 @@
+         BlockPos blockPos1 = null;
+         WorldBorder worldBorder = this.level.getWorldBorder();
+         int min = Math.min(this.level.getMaxY(), this.level.getMinY() + this.level.getLogicalHeight() - 1);
++        // Paper start - Configurable nether ceiling damage; make sure the max height doesn't exceed the void damage height
++        if (this.level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.enabled()) {
++            min = Math.min(min, this.level.paperConfig().environment.netherCeilingVoidDamageHeight.intValue() - 1);
++        }
++        // Paper end - Configurable nether ceiling damage
+         int i = 1;
+         BlockPos.MutableBlockPos mutableBlockPos = pos.mutable();
+ 
+-        for (BlockPos.MutableBlockPos mutableBlockPos1 : BlockPos.spiralAround(pos, 16, Direction.EAST, Direction.SOUTH)) {
++        for (BlockPos.MutableBlockPos mutableBlockPos1 : BlockPos.spiralAround(pos, createRadius, Direction.EAST, Direction.SOUTH)) { // CraftBukkit
+             int min1 = Math.min(min, this.level.getHeight(Heightmap.Types.MOTION_BLOCKING, mutableBlockPos1.getX(), mutableBlockPos1.getZ()));
+             if (worldBorder.isWithinBounds(mutableBlockPos1) && worldBorder.isWithinBounds(mutableBlockPos1.move(direction, 1))) {
+                 mutableBlockPos1.move(direction.getOpposite(), 1);
+@@ -104,6 +_,7 @@
+             d = d1;
+         }
+ 
++        org.bukkit.craftbukkit.util.BlockStateListPopulator blockList = new org.bukkit.craftbukkit.util.BlockStateListPopulator(this.level); // CraftBukkit - Use BlockStateListPopulator
+         if (d == -1.0) {
+             int max = Math.max(this.level.getMinY() - -1, 70);
+             int i4 = min - 9;
+@@ -122,7 +_,7 @@
+                         mutableBlockPos.setWithOffset(
+                             blockPos, i2 * direction.getStepX() + i1x * clockWise.getStepX(), i3, i2 * direction.getStepZ() + i1x * clockWise.getStepZ()
+                         );
+-                        this.level.setBlockAndUpdate(mutableBlockPos, blockState);
++                        blockList.setBlock(mutableBlockPos, blockState, 3); // CraftBukkit
+                     }
+                 }
+             }
+@@ -132,7 +_,7 @@
+             for (int i4 = -1; i4 < 4; i4++) {
+                 if (max == -1 || max == 2 || i4 == -1 || i4 == 3) {
+                     mutableBlockPos.setWithOffset(blockPos, max * direction.getStepX(), i4, max * direction.getStepZ());
+-                    this.level.setBlock(mutableBlockPos, Blocks.OBSIDIAN.defaultBlockState(), 3);
++                    blockList.setBlock(mutableBlockPos, Blocks.OBSIDIAN.defaultBlockState(), 3); // CraftBukkit
+                 }
+             }
+         }
+@@ -142,10 +_,20 @@
+         for (int i4x = 0; i4x < 2; i4x++) {
+             for (int min1 = 0; min1 < 3; min1++) {
+                 mutableBlockPos.setWithOffset(blockPos, i4x * direction.getStepX(), min1, i4x * direction.getStepZ());
+-                this.level.setBlock(mutableBlockPos, blockState1, 18);
++                blockList.setBlock(mutableBlockPos, blockState1, 18); // CraftBukkit
+             }
+         }
+ 
++        // CraftBukkit start
++        org.bukkit.World bworld = this.level.getWorld();
++        org.bukkit.event.world.PortalCreateEvent event = new org.bukkit.event.world.PortalCreateEvent((java.util.List<org.bukkit.block.BlockState>) (java.util.List) blockList.getList(), bworld, (entity == null) ? null : entity.getBukkitEntity(), org.bukkit.event.world.PortalCreateEvent.CreateReason.NETHER_PAIR);
++
++        this.level.getCraftServer().getPluginManager().callEvent(event);
++        if (event.isCancelled()) {
++            return Optional.empty();
++        }
++        blockList.updateList();
++        // CraftBukkit end
+         return Optional.of(new BlockUtil.FoundRectangle(blockPos.immutable(), 2, 3));
+     }
+ 
+@@ -165,6 +_,13 @@
+                     i1,
+                     direction.getStepZ() * i + clockWise.getStepZ() * offsetScale
+                 );
++                // Paper start - Protect Bedrock and End Portal/Frames from being destroyed
++                if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits) {
++                    if (!this.level.getBlockState(offsetPos).isDestroyable()) {
++                        return false;
++                    }
++                }
++                // Paper end - Protect Bedrock and End Portal/Frames from being destroyed
+                 if (i1 < 0 && !this.level.getBlockState(offsetPos).isSolid()) {
+                     return false;
+                 }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/portal/PortalShape.java.patch b/paper-server/patches/sources/net/minecraft/world/level/portal/PortalShape.java.patch
new file mode 100644
index 0000000000..d92f339f46
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/portal/PortalShape.java.patch
@@ -0,0 +1,154 @@
+--- a/net/minecraft/world/level/portal/PortalShape.java
++++ b/net/minecraft/world/level/portal/PortalShape.java
+@@ -37,8 +_,12 @@
+     private final BlockPos bottomLeft;
+     private final int height;
+     private final int width;
++    // CraftBukkit start - add field
++    private final org.bukkit.craftbukkit.util.BlockStateListPopulator blocks;
+ 
+-    private PortalShape(Direction.Axis axis, int numPortalBlocks, Direction rightDir, BlockPos bottomLeft, int width, int height) {
++    private PortalShape(Direction.Axis axis, int numPortalBlocks, Direction rightDir, BlockPos bottomLeft, int width, int height, org.bukkit.craftbukkit.util.BlockStateListPopulator blocks) {
++        this.blocks = blocks;
++        // CraftBukkit end
+         this.axis = axis;
+         this.numPortalBlocks = numPortalBlocks;
+         this.rightDir = rightDir;
+@@ -62,24 +_,25 @@
+     }
+ 
+     public static PortalShape findAnyShape(BlockGetter level, BlockPos bottomLeft, Direction.Axis axis) {
++        org.bukkit.craftbukkit.util.BlockStateListPopulator blocks = new org.bukkit.craftbukkit.util.BlockStateListPopulator(((LevelAccessor) level).getMinecraftWorld()); // CraftBukkit
+         Direction direction = axis == Direction.Axis.X ? Direction.WEST : Direction.SOUTH;
+-        BlockPos blockPos = calculateBottomLeft(level, direction, bottomLeft);
++        BlockPos blockPos = calculateBottomLeft(level, direction, bottomLeft, blocks); // CraftBukkit
+         if (blockPos == null) {
+-            return new PortalShape(axis, 0, direction, bottomLeft, 0, 0);
++            return new PortalShape(axis, 0, direction, bottomLeft, 0, 0, blocks); // CraftBukkit
+         } else {
+-            int i = calculateWidth(level, blockPos, direction);
++            int i = calculateWidth(level, blockPos, direction, blocks); // CraftBukkit
+             if (i == 0) {
+-                return new PortalShape(axis, 0, direction, blockPos, 0, 0);
++                return new PortalShape(axis, 0, direction, blockPos, 0, 0, blocks); // CraftBukkit
+             } else {
+                 MutableInt mutableInt = new MutableInt();
+-                int i1 = calculateHeight(level, blockPos, direction, i, mutableInt);
+-                return new PortalShape(axis, mutableInt.getValue(), direction, blockPos, i, i1);
++                int i1 = calculateHeight(level, blockPos, direction, i, mutableInt, blocks); // CraftBukkit
++                return new PortalShape(axis, mutableInt.getValue(), direction, blockPos, i, i1, blocks); // CraftBukkit
+             }
+         }
+     }
+ 
+     @Nullable
+-    private static BlockPos calculateBottomLeft(BlockGetter level, Direction direction, BlockPos pos) {
++    private static BlockPos calculateBottomLeft(BlockGetter level, Direction direction, BlockPos pos, org.bukkit.craftbukkit.util.BlockStateListPopulator blocks) { // CraftBukkit
+         int max = Math.max(level.getMinY(), pos.getY() - 21);
+ 
+         while (pos.getY() > max && isEmpty(level.getBlockState(pos.below()))) {
+@@ -87,16 +_,16 @@
+         }
+ 
+         Direction opposite = direction.getOpposite();
+-        int i = getDistanceUntilEdgeAboveFrame(level, pos, opposite) - 1;
++        int i = getDistanceUntilEdgeAboveFrame(level, pos, opposite, blocks) - 1; // CraftBukkit
+         return i < 0 ? null : pos.relative(opposite, i);
+     }
+ 
+-    private static int calculateWidth(BlockGetter level, BlockPos bottomLeft, Direction direction) {
+-        int distanceUntilEdgeAboveFrame = getDistanceUntilEdgeAboveFrame(level, bottomLeft, direction);
++    private static int calculateWidth(BlockGetter level, BlockPos bottomLeft, Direction direction, org.bukkit.craftbukkit.util.BlockStateListPopulator blocks) { // CraftBukkit
++        int distanceUntilEdgeAboveFrame = getDistanceUntilEdgeAboveFrame(level, bottomLeft, direction, blocks); // CraftBukkit
+         return distanceUntilEdgeAboveFrame >= 2 && distanceUntilEdgeAboveFrame <= 21 ? distanceUntilEdgeAboveFrame : 0;
+     }
+ 
+-    private static int getDistanceUntilEdgeAboveFrame(BlockGetter level, BlockPos pos, Direction direction) {
++    private static int getDistanceUntilEdgeAboveFrame(BlockGetter level, BlockPos pos, Direction direction, org.bukkit.craftbukkit.util.BlockStateListPopulator blocks) { // CraftBukkit
+         BlockPos.MutableBlockPos mutableBlockPos = new BlockPos.MutableBlockPos();
+ 
+         for (int i = 0; i <= 21; i++) {
+@@ -104,6 +_,7 @@
+             BlockState blockState = level.getBlockState(mutableBlockPos);
+             if (!isEmpty(blockState)) {
+                 if (FRAME.test(blockState, level, mutableBlockPos)) {
++                    blocks.setBlock(mutableBlockPos, blockState, 18); // CraftBukkit - lower left / right
+                     return i;
+                 }
+                 break;
+@@ -113,32 +_,34 @@
+             if (!FRAME.test(blockState1, level, mutableBlockPos)) {
+                 break;
+             }
++            blocks.setBlock(mutableBlockPos, blockState1, 18); // CraftBukkit - bottom row
+         }
+ 
+         return 0;
+     }
+ 
+-    private static int calculateHeight(BlockGetter level, BlockPos pos, Direction direction, int width, MutableInt portalBlocks) {
++    private static int calculateHeight(BlockGetter level, BlockPos pos, Direction direction, int width, MutableInt portalBlocks, org.bukkit.craftbukkit.util.BlockStateListPopulator blocks) { // CraftBukkit
+         BlockPos.MutableBlockPos mutableBlockPos = new BlockPos.MutableBlockPos();
+-        int distanceUntilTop = getDistanceUntilTop(level, pos, direction, mutableBlockPos, width, portalBlocks);
+-        return distanceUntilTop >= 3 && distanceUntilTop <= 21 && hasTopFrame(level, pos, direction, mutableBlockPos, width, distanceUntilTop)
++        int distanceUntilTop = getDistanceUntilTop(level, pos, direction, mutableBlockPos, width, portalBlocks, blocks); // CraftBukkit
++        return distanceUntilTop >= 3 && distanceUntilTop <= 21 && hasTopFrame(level, pos, direction, mutableBlockPos, width, distanceUntilTop, blocks) // CraftBukkit
+             ? distanceUntilTop
+             : 0;
+     }
+ 
+-    private static boolean hasTopFrame(BlockGetter level, BlockPos pos, Direction direction, BlockPos.MutableBlockPos checkPos, int width, int distanceUntilTop) {
++    private static boolean hasTopFrame(BlockGetter level, BlockPos pos, Direction direction, BlockPos.MutableBlockPos checkPos, int width, int distanceUntilTop, org.bukkit.craftbukkit.util.BlockStateListPopulator blocks) { // CraftBukkit
+         for (int i = 0; i < width; i++) {
+             BlockPos.MutableBlockPos mutableBlockPos = checkPos.set(pos).move(Direction.UP, distanceUntilTop).move(direction, i);
+             if (!FRAME.test(level.getBlockState(mutableBlockPos), level, mutableBlockPos)) {
+                 return false;
+             }
++            blocks.setBlock(mutableBlockPos, level.getBlockState(mutableBlockPos), 18); // CraftBukkit - upper row
+         }
+ 
+         return true;
+     }
+ 
+     private static int getDistanceUntilTop(
+-        BlockGetter level, BlockPos pos, Direction direction, BlockPos.MutableBlockPos checkPos, int width, MutableInt portalBlocks
++        BlockGetter level, BlockPos pos, Direction direction, BlockPos.MutableBlockPos checkPos, int width, MutableInt portalBlocks, org.bukkit.craftbukkit.util.BlockStateListPopulator blocks // CraftBukkit
+     ) {
+         for (int i = 0; i < 21; i++) {
+             checkPos.set(pos).move(Direction.UP, i).move(direction, -1);
+@@ -162,6 +_,10 @@
+                     portalBlocks.increment();
+                 }
+             }
++            // CraftBukkit start - left and right
++            blocks.setBlock(checkPos.set(pos).move(Direction.UP, i).move(direction, -1), level.getBlockState(checkPos), 18);
++            blocks.setBlock(checkPos.set(pos).move(Direction.UP, i).move(direction, i), level.getBlockState(checkPos), 18);
++            // CraftBukkit end
+         }
+ 
+         return 21;
+@@ -175,10 +_,23 @@
+         return this.width >= 2 && this.width <= 21 && this.height >= 3 && this.height <= 21;
+     }
+ 
+-    public void createPortalBlocks(LevelAccessor level) {
++    // CraftBukkit start - return boolean, add entity
++    public boolean createPortalBlocks(LevelAccessor level, Entity entity) {
++        org.bukkit.World bworld = level.getMinecraftWorld().getWorld();
++        // Copy below for loop
+         BlockState blockState = Blocks.NETHER_PORTAL.defaultBlockState().setValue(NetherPortalBlock.AXIS, this.axis);
+         BlockPos.betweenClosed(this.bottomLeft, this.bottomLeft.relative(Direction.UP, this.height - 1).relative(this.rightDir, this.width - 1))
++            .forEach(pos -> this.blocks.setBlock(pos, blockState, 18));
++        org.bukkit.event.world.PortalCreateEvent event = new org.bukkit.event.world.PortalCreateEvent((java.util.List<org.bukkit.block.BlockState>) (java.util.List) this.blocks.getList(), bworld, (entity == null) ? null : entity.getBukkitEntity(), org.bukkit.event.world.PortalCreateEvent.CreateReason.FIRE);
++        level.getMinecraftWorld().getServer().server.getPluginManager().callEvent(event);
++
++        if (event.isCancelled()) {
++            return false;
++        }
++        // CraftBukkit end
++        BlockPos.betweenClosed(this.bottomLeft, this.bottomLeft.relative(Direction.UP, this.height - 1).relative(this.rightDir, this.width - 1))
+             .forEach(pos -> level.setBlock(pos, blockState, 18));
++        return true; // CraftBukkit
+     }
+ 
+     public boolean isComplete() {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/portal/TeleportTransition.java.patch b/paper-server/patches/sources/net/minecraft/world/level/portal/TeleportTransition.java.patch
new file mode 100644
index 0000000000..b65e02e918
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/portal/TeleportTransition.java.patch
@@ -0,0 +1,80 @@
+--- a/net/minecraft/world/level/portal/TeleportTransition.java
++++ b/net/minecraft/world/level/portal/TeleportTransition.java
+@@ -19,15 +_,34 @@
+     boolean asPassenger,
+     Set<Relative> relatives,
+     TeleportTransition.PostTeleportTransition postTeleportTransition
++    , org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause // CraftBukkit
+ ) {
+     public static final TeleportTransition.PostTeleportTransition DO_NOTHING = entity -> {};
+     public static final TeleportTransition.PostTeleportTransition PLAY_PORTAL_SOUND = TeleportTransition::playPortalSound;
+     public static final TeleportTransition.PostTeleportTransition PLACE_PORTAL_TICKET = TeleportTransition::placePortalTicket;
+ 
++    // CraftBukkit start
++    public TeleportTransition(ServerLevel newLevel, Vec3 position, Vec3 deltaMovement, float yRot, float xRot, boolean missingRespawnBlock, boolean asPassenger, Set<Relative> relatives, TeleportTransition.PostTeleportTransition postTeleportTransition) {
++        this(newLevel, position, deltaMovement, yRot, xRot, missingRespawnBlock, asPassenger, relatives, postTeleportTransition, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.UNKNOWN);
++    }
++
++    public TeleportTransition(org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause) {
++        this(null, Vec3.ZERO, Vec3.ZERO, 0.0F, 0.0F, false, false, Set.of(), DO_NOTHING, cause);
++    }
++    // CraftBukkit end
++
+     public TeleportTransition(
+         ServerLevel newLevel, Vec3 position, Vec3 deltaMovement, float yRot, float xRot, TeleportTransition.PostTeleportTransition postTeleportTransition
+     ) {
+-        this(newLevel, position, deltaMovement, yRot, xRot, Set.of(), postTeleportTransition);
++        // CraftBukkit start
++        this(newLevel, position, deltaMovement, yRot, xRot, postTeleportTransition, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.UNKNOWN);
++    }
++
++    public TeleportTransition(
++        ServerLevel newLevel, Vec3 position, Vec3 deltaMovement, float yRot, float xRot, TeleportTransition.PostTeleportTransition postTeleportTransition, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause
++    ) {
++        this(newLevel, position, deltaMovement, yRot, xRot, Set.of(), postTeleportTransition, cause);
++        // CraftBukkit end
+     }
+ 
+     public TeleportTransition(
+@@ -39,11 +_,30 @@
+         Set<Relative> relatives,
+         TeleportTransition.PostTeleportTransition postTeleportTransition
+     ) {
+-        this(newLevel, position, deltaMovement, yRot, xRot, false, false, relatives, postTeleportTransition);
++        // CraftBukkit start
++        this(newLevel, position, deltaMovement, yRot, xRot, relatives, postTeleportTransition, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.UNKNOWN);
++    }
++    public TeleportTransition(
++        ServerLevel newLevel,
++        Vec3 position,
++        Vec3 deltaMovement,
++        float yRot,
++        float xRot,
++        Set<Relative> relatives,
++        TeleportTransition.PostTeleportTransition postTeleportTransition,
++        org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause
++    ) {
++        this(newLevel, position, deltaMovement, yRot, xRot, false, false, relatives, postTeleportTransition, cause);
++        // CraftBukkit end
+     }
+ 
+     public TeleportTransition(ServerLevel level, Entity entity, TeleportTransition.PostTeleportTransition postTeleportTransition) {
+-        this(level, findAdjustedSharedSpawnPos(level, entity), Vec3.ZERO, 0.0F, 0.0F, false, false, Set.of(), postTeleportTransition);
++        // CraftBukkit start
++        this(level, entity, postTeleportTransition, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.UNKNOWN);
++    }
++    public TeleportTransition(ServerLevel level, Entity entity, TeleportTransition.PostTeleportTransition postTeleportTransition, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause) {
++        this(level, findAdjustedSharedSpawnPos(level, entity), Vec3.ZERO, level.getSharedSpawnAngle(), 0.0F, false, false, Set.of(), postTeleportTransition, cause); // Paper - MC-200092 - fix first spawn pos yaw being ignored
++        // CraftBukkit end
+     }
+ 
+     private static void playPortalSound(Entity entity) {
+@@ -57,7 +_,7 @@
+     }
+ 
+     public static TeleportTransition missingRespawnBlock(ServerLevel level, Entity entity, TeleportTransition.PostTeleportTransition postTeleportTransition) {
+-        return new TeleportTransition(level, findAdjustedSharedSpawnPos(level, entity), Vec3.ZERO, 0.0F, 0.0F, true, false, Set.of(), postTeleportTransition);
++        return new TeleportTransition(level, findAdjustedSharedSpawnPos(level, entity), Vec3.ZERO, level.getSharedSpawnAngle(), 0.0F, true, false, Set.of(), postTeleportTransition); // Paper - MC-200092 - fix spawn pos yaw being ignored
+     }
+ 
+     private static Vec3 findAdjustedSharedSpawnPos(ServerLevel level, Entity entity) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java.patch b/paper-server/patches/sources/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java.patch
similarity index 79%
rename from paper-server/patches/unapplied/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java.patch
index d055cf38c0..3fd8f69544 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java.patch
@@ -1,11 +1,11 @@
 --- a/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java
 +++ b/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java
-@@ -135,7 +135,7 @@
+@@ -133,7 +_,7 @@
                  orientation = this.orientation.withFront(direction);
              }
  
--            NeighborUpdater.executeUpdate(world, blockState, blockPos, this.sourceBlock, orientation, false);
-+            NeighborUpdater.executeUpdate(world, blockState, blockPos, this.sourceBlock, orientation, false, this.sourcePos); // Paper - Add source block to BlockPhysicsEvent
+-            NeighborUpdater.executeUpdate(level, blockState, blockPos, this.sourceBlock, orientation, false);
++            NeighborUpdater.executeUpdate(level, blockState, blockPos, this.sourceBlock, orientation, false, this.sourcePos); // Paper - Add source block to BlockPhysicsEvent
              if (this.idx < NeighborUpdater.UPDATE_ORDER.length && NeighborUpdater.UPDATE_ORDER[this.idx] == this.skipDirection) {
                  this.idx++;
              }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java.patch b/paper-server/patches/sources/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java.patch
new file mode 100644
index 0000000000..f5b42d763b
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java
++++ b/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java
+@@ -17,7 +_,16 @@
+     @Override
+     public void updatePowerStrength(Level level, BlockPos pos, BlockState state, @Nullable Orientation orientation, boolean updateShape) {
+         int i = this.calculateTargetStrength(level, pos);
+-        if (state.getValue(RedStoneWireBlock.POWER) != i) {
++        // CraftBukkit start
++        int oldPower = state.getValue(RedStoneWireBlock.POWER);
++        if (oldPower != i) {
++            org.bukkit.event.block.BlockRedstoneEvent event = new org.bukkit.event.block.BlockRedstoneEvent(org.bukkit.craftbukkit.block.CraftBlock.at(level, pos), oldPower, i);
++            level.getCraftServer().getPluginManager().callEvent(event);
++
++            i = event.getNewCurrent();
++        }
++        if (oldPower != i) {
++            // CraftBukkit end
+             if (level.getBlockState(pos) == state) {
+                 level.setBlock(pos, state.setValue(RedStoneWireBlock.POWER, Integer.valueOf(i)), 2);
+             }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/redstone/ExperimentalRedstoneWireEvaluator.java.patch b/paper-server/patches/sources/net/minecraft/world/level/redstone/ExperimentalRedstoneWireEvaluator.java.patch
new file mode 100644
index 0000000000..b83293fbd0
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/redstone/ExperimentalRedstoneWireEvaluator.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/level/redstone/ExperimentalRedstoneWireEvaluator.java
++++ b/net/minecraft/world/level/redstone/ExperimentalRedstoneWireEvaluator.java
+@@ -36,7 +_,16 @@
+             int intValue = entry.getIntValue();
+             int i = unpackPower(intValue);
+             BlockState blockState = level.getBlockState(blockPos);
+-            if (blockState.is(this.wireBlock) && !blockState.getValue(RedStoneWireBlock.POWER).equals(i)) {
++            // CraftBukkit start
++            int oldPower = blockState.getValue(RedStoneWireBlock.POWER); // Paper - Call BlockRedstoneEvent properly; get the previous power from the right state
++            if (oldPower != i) {
++                org.bukkit.event.block.BlockRedstoneEvent event = new org.bukkit.event.block.BlockRedstoneEvent(org.bukkit.craftbukkit.block.CraftBlock.at(level, blockPos), oldPower, i);
++                level.getCraftServer().getPluginManager().callEvent(event);
++
++                i = event.getNewCurrent();
++            }
++            if (blockState.is(this.wireBlock) && oldPower != i) {
++                // CraftBukkit end
+                 int i1 = 2;
+                 if (!updateShape || !flag) {
+                     i1 |= 128;
diff --git a/paper-server/patches/sources/net/minecraft/world/level/redstone/NeighborUpdater.java.patch b/paper-server/patches/sources/net/minecraft/world/level/redstone/NeighborUpdater.java.patch
new file mode 100644
index 0000000000..7c2db6c3ac
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/redstone/NeighborUpdater.java.patch
@@ -0,0 +1,32 @@
+--- a/net/minecraft/world/level/redstone/NeighborUpdater.java
++++ b/net/minecraft/world/level/redstone/NeighborUpdater.java
+@@ -42,8 +_,29 @@
+     }
+ 
+     static void executeUpdate(Level level, BlockState state, BlockPos pos, Block neighborBlock, @Nullable Orientation orientation, boolean movedByPiston) {
++        // Paper start - Add source block to BlockPhysicsEvent
++        executeUpdate(level, state, pos, neighborBlock, orientation, movedByPiston, pos);
++    }
++
++    static void executeUpdate(Level level, BlockState state, BlockPos pos, Block neighborBlock, @Nullable Orientation orientation, boolean movedByPiston, BlockPos sourcePos) {
++        // Paper end - Add source block to BlockPhysicsEvent
+         try {
++            // CraftBukkit start
++            org.bukkit.craftbukkit.CraftWorld cworld = level.getWorld();
++            if (cworld != null) {
++                org.bukkit.event.block.BlockPhysicsEvent event = new org.bukkit.event.block.BlockPhysicsEvent(org.bukkit.craftbukkit.block.CraftBlock.at(level, pos), org.bukkit.craftbukkit.block.data.CraftBlockData.fromData(state), org.bukkit.craftbukkit.block.CraftBlock.at(level, sourcePos)); // Paper - Add source block to BlockPhysicsEvent
++                level.getCraftServer().getPluginManager().callEvent(event);
++
++                if (event.isCancelled()) {
++                    return;
++                }
++            }
++            // CraftBukkit end
+             state.handleNeighborChanged(level, pos, neighborBlock, orientation, movedByPiston);
++            // Spigot start
++        } catch (StackOverflowError ex) {
++            level.lastPhysicsProblem = new BlockPos(pos);
++            // Spigot end
+         } catch (Throwable var9) {
+             CrashReport crashReport = CrashReport.forThrowable(var9, "Exception while updating neighbours");
+             CrashReportCategory crashReportCategory = crashReport.addCategory("Block being updated");
diff --git a/paper-server/patches/sources/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java.patch b/paper-server/patches/sources/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java.patch
new file mode 100644
index 0000000000..9aa3e55368
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java.patch
@@ -0,0 +1,236 @@
+--- a/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java
++++ b/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java
+@@ -61,6 +_,7 @@
+     public byte scale;
+     public byte[] colors = new byte[16384];
+     public boolean locked;
++    private final org.bukkit.craftbukkit.map.RenderData vanillaRender = new org.bukkit.craftbukkit.map.RenderData(); // Paper - Use Vanilla map renderer when possible
+     public final List<MapItemSavedData.HoldingPlayer> carriedBy = Lists.newArrayList();
+     public final Map<Player, MapItemSavedData.HoldingPlayer> carriedByPlayers = Maps.newHashMap();
+     private final Map<String, MapBanner> bannerMarkers = Maps.newHashMap();
+@@ -68,6 +_,13 @@
+     private final Map<String, MapFrame> frameMarkers = Maps.newHashMap();
+     private int trackedDecorationCount;
+ 
++    // CraftBukkit start
++    public final org.bukkit.craftbukkit.map.CraftMapView mapView;
++    private final org.bukkit.craftbukkit.CraftServer server;
++    public java.util.UUID uniqueId;
++    public MapId id;
++    // CraftBukkit end
++
+     public static SavedData.Factory<MapItemSavedData> factory() {
+         return new SavedData.Factory<>(() -> {
+             throw new IllegalStateException("Should never create an empty map saved data");
+@@ -82,6 +_,11 @@
+         this.trackingPosition = trackingPosition;
+         this.unlimitedTracking = unlimitedTracking;
+         this.locked = locked;
++        // CraftBukkit start
++        this.mapView = new org.bukkit.craftbukkit.map.CraftMapView(this);
++        this.server = (org.bukkit.craftbukkit.CraftServer) org.bukkit.Bukkit.getServer();
++        this.vanillaRender.buffer = colors; // Paper - Use Vanilla map renderer when possible
++        // CraftBukkit end
+     }
+ 
+     public static MapItemSavedData createFresh(
+@@ -100,9 +_,47 @@
+     }
+ 
+     public static MapItemSavedData load(CompoundTag tag, HolderLookup.Provider levelRegistry) {
+-        ResourceKey<Level> resourceKey = DimensionType.parseLegacy(new Dynamic<>(NbtOps.INSTANCE, tag.get("dimension")))
+-            .resultOrPartial(LOGGER::error)
+-            .orElseThrow(() -> new IllegalArgumentException("Invalid map dimension: " + tag.get("dimension")));
++        // Paper start - fix "Not a string" spam
++        Tag dimension = tag.get("dimension");
++        if (dimension instanceof final net.minecraft.nbt.NumericTag numericTag && numericTag.getAsInt() >= org.bukkit.craftbukkit.CraftWorld.CUSTOM_DIMENSION_OFFSET) {
++            long least = tag.getLong("UUIDLeast");
++            long most = tag.getLong("UUIDMost");
++
++            if (least != 0L && most != 0L) {
++                java.util.UUID uuid = new java.util.UUID(most, least);
++                org.bukkit.craftbukkit.CraftWorld world = (org.bukkit.craftbukkit.CraftWorld) org.bukkit.Bukkit.getWorld(uuid);
++                if (world != null) {
++                    dimension = net.minecraft.nbt.StringTag.valueOf("minecraft:" + world.getName().toLowerCase(java.util.Locale.ENGLISH));
++                } else {
++                    dimension = net.minecraft.nbt.StringTag.valueOf("bukkit:_invalidworld_");
++                }
++            } else {
++                dimension = net.minecraft.nbt.StringTag.valueOf("bukkit:_invalidworld_");
++            }
++        }
++        com.mojang.serialization.DataResult<ResourceKey<Level>> dataresult = DimensionType.parseLegacy(new Dynamic(NbtOps.INSTANCE, dimension)); // CraftBukkit - decompile error
++        // Paper end - fix "Not a string" spam
++        // CraftBukkit start
++        ResourceKey<Level> resourceKey = dataresult.resultOrPartial(LOGGER::error).orElseGet(() -> {
++            long least = tag.getLong("UUIDLeast");
++            long most = tag.getLong("UUIDMost");
++
++            if (least != 0L && most != 0L) {
++                java.util.UUID uniqueId = new java.util.UUID(most, least);
++
++                org.bukkit.craftbukkit.CraftWorld world = (org.bukkit.craftbukkit.CraftWorld) org.bukkit.Bukkit.getWorld(uniqueId);
++                // Check if the stored world details are correct.
++                if (world == null) {
++                    /* All Maps which do not have their valid world loaded are set to a dimension which hopefully won't be reached.
++                       This is to prevent them being corrupted with the wrong map data. */
++                    // PAIL: Use Vanilla exception handling for now
++                } else {
++                    return world.getHandle().dimension();
++                }
++            }
++            throw new IllegalArgumentException("Invalid map dimension: " + String.valueOf(tag.get("dimension")));
++            // CraftBukkit end
++        });
+         int _int = tag.getInt("xCenter");
+         int _int1 = tag.getInt("zCenter");
+         byte b = (byte)Mth.clamp(tag.getByte("scale"), 0, 4);
+@@ -114,6 +_,7 @@
+         if (byteArray.length == 16384) {
+             mapItemSavedData.colors = byteArray;
+         }
++        mapItemSavedData.vanillaRender.buffer = byteArray; // Paper - Use Vanilla map renderer when possible
+ 
+         RegistryOps<Tag> registryOps = levelRegistry.createSerializationContext(NbtOps.INSTANCE);
+ 
+@@ -154,6 +_,25 @@
+             .encodeStart(NbtOps.INSTANCE, this.dimension.location())
+             .resultOrPartial(LOGGER::error)
+             .ifPresent(dimension -> tag.put("dimension", dimension));
++        // CraftBukkit start
++        if (true) {
++            if (this.uniqueId == null) {
++                for (org.bukkit.World world : this.server.getWorlds()) {
++                    org.bukkit.craftbukkit.CraftWorld cWorld = (org.bukkit.craftbukkit.CraftWorld) world;
++                    if (cWorld.getHandle().dimension() == this.dimension) {
++                        this.uniqueId = cWorld.getUID();
++                        break;
++                    }
++                }
++            }
++            /* Perform a second check to see if a matching world was found, this is a necessary
++               change incase Maps are forcefully unlinked from a World and lack a UID.*/
++            if (this.uniqueId != null) {
++                tag.putLong("UUIDLeast", this.uniqueId.getLeastSignificantBits());
++                tag.putLong("UUIDMost", this.uniqueId.getMostSignificantBits());
++            }
++        }
++        // CraftBukkit end
+         tag.putInt("xCenter", this.centerX);
+         tag.putInt("zCenter", this.centerZ);
+         tag.putByte("scale", this.scale);
+@@ -233,10 +_,12 @@
+             }
+ 
+             MapFrame mapFrame1 = new MapFrame(pos, frame.getDirection().get2DDataValue() * 90, frame.getId());
++            if (this.decorations.size() < player.level().paperConfig().maps.itemFrameCursorLimit) { // Paper - Limit item frame cursors on maps
+             this.addDecoration(
+                 MapDecorationTypes.FRAME, player.level(), getFrameKey(frame.getId()), pos.getX(), pos.getZ(), frame.getDirection().get2DDataValue() * 90, null
+             );
+             this.frameMarkers.put(mapFrame1.getId(), mapFrame1);
++            } // Paper - Limit item frame cursors on maps
+         }
+ 
+         MapDecorations mapDecorations = mapStack.getOrDefault(DataComponents.MAP_DECORATIONS, MapDecorations.EMPTY);
+@@ -267,7 +_,7 @@
+             this.trackedDecorationCount--;
+         }
+ 
+-        this.setDecorationsDirty();
++        if (mapDecoration != null) this.setDecorationsDirty(); // Paper - only mark dirty if a change occurs
+     }
+ 
+     public static void addTargetDecoration(ItemStack stack, BlockPos pos, String type, Holder<MapDecorationType> mapDecorationType) {
+@@ -421,7 +_,7 @@
+                 return true;
+             }
+ 
+-            if (!this.isTrackedCountOverLimit(256)) {
++            if (!this.isTrackedCountOverLimit(((Level) accessor).paperConfig().maps.itemFrameCursorLimit)) { // Paper - Limit item frame cursors on maps
+                 this.bannerMarkers.put(mapBanner.getId(), mapBanner);
+                 this.addDecoration(mapBanner.getDecoration(), accessor, mapBanner.getId(), d, d1, 180.0, mapBanner.name().orElse(null));
+                 return true;
+@@ -521,7 +_,7 @@
+             this.player = player;
+         }
+ 
+-        private MapItemSavedData.MapPatch createPatch() {
++        private MapItemSavedData.MapPatch createPatch(byte[] buffer) { // CraftBukkit
+             int i = this.minDirtyX;
+             int i1 = this.minDirtyY;
+             int i2 = this.maxDirtyX + 1 - this.minDirtyX;
+@@ -530,7 +_,7 @@
+ 
+             for (int i4 = 0; i4 < i2; i4++) {
+                 for (int i5 = 0; i5 < i3; i5++) {
+-                    bytes[i4 + i5 * i2] = MapItemSavedData.this.colors[i + i4 + (i1 + i5) * 128];
++                    bytes[i4 + i5 * i2] = buffer[i + i4 + (i1 + i5) * 128]; // CraftBukkit
+                 }
+             }
+ 
+@@ -540,17 +_,38 @@
+         @Nullable
+         Packet<?> nextUpdatePacket(MapId mapId) {
+             MapItemSavedData.MapPatch mapPatch;
++            // Paper start
++            if (!this.dirtyData && this.tick % 5 != 0) {
++                // this won't end up sending, so don't render it!
++                this.tick++;
++                return null;
++            }
++
++            final boolean vanillaMaps = this.shouldUseVanillaMap();
++            // Use Vanilla map renderer when possible - much simpler/faster than the CB rendering
++            org.bukkit.craftbukkit.map.RenderData render = !vanillaMaps ? MapItemSavedData.this.mapView.render((org.bukkit.craftbukkit.entity.CraftPlayer) this.player.getBukkitEntity()) : MapItemSavedData.this.vanillaRender;
++            // Paper end
+             if (this.dirtyData) {
+                 this.dirtyData = false;
+-                mapPatch = this.createPatch();
++                mapPatch = this.createPatch(render.buffer); // CraftBukkit
+             } else {
+                 mapPatch = null;
+             }
+ 
+             Collection<MapDecoration> collection;
+-            if (this.dirtyDecorations && this.tick++ % 5 == 0) {
++            if ((true || this.dirtyDecorations) && this.tick++ % 5 == 0) { // CraftBukkit - custom maps don't update this yet // TODO fix this
+                 this.dirtyDecorations = false;
+-                collection = MapItemSavedData.this.decorations.values();
++                // CraftBukkit start
++                java.util.Collection<MapDecoration> icons = new java.util.ArrayList<MapDecoration>();
++                if (vanillaMaps) this.addSeenPlayers(icons); // Paper
++
++                for (org.bukkit.map.MapCursor cursor : render.cursors) {
++                    if (cursor.isVisible()) {
++                        icons.add(new MapDecoration(org.bukkit.craftbukkit.map.CraftMapCursor.CraftType.bukkitToMinecraftHolder(cursor.getType()), cursor.getX(), cursor.getY(), cursor.getDirection(), Optional.ofNullable(io.papermc.paper.adventure.PaperAdventure.asVanilla(cursor.caption()))));
++                    }
++                }
++                collection = icons;
++                // CraftBukkit end
+             } else {
+                 collection = null;
+             }
+@@ -578,6 +_,23 @@
+         private void markDecorationsDirty() {
+             this.dirtyDecorations = true;
+         }
++
++        // Paper start
++        private void addSeenPlayers(java.util.Collection<MapDecoration> icons) {
++            org.bukkit.entity.Player player = (org.bukkit.entity.Player) this.player.getBukkitEntity();
++            MapItemSavedData.this.decorations.forEach((name, mapIcon) -> {
++                // If this cursor is for a player check visibility with vanish system
++                org.bukkit.entity.Player other = org.bukkit.Bukkit.getPlayerExact(name); // Spigot
++                if (other == null || player.canSee(other)) {
++                    icons.add(mapIcon);
++                }
++            });
++        }
++
++        private boolean shouldUseVanillaMap() {
++            return mapView.getRenderers().size() == 1 && mapView.getRenderers().getFirst().getClass() == org.bukkit.craftbukkit.map.CraftMapRenderer.class;
++        }
++        // Paper end
+     }
+ 
+     record MapDecorationLocation(Holder<MapDecorationType> type, byte x, byte y, byte rot) {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/storage/DimensionDataStorage.java.patch b/paper-server/patches/sources/net/minecraft/world/level/storage/DimensionDataStorage.java.patch
new file mode 100644
index 0000000000..41f17dad54
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/storage/DimensionDataStorage.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/level/storage/DimensionDataStorage.java
++++ b/net/minecraft/world/level/storage/DimensionDataStorage.java
+@@ -139,7 +_,7 @@
+         } else {
+             int i = Util.maxAllowedExecutorThreads();
+             int size = map.size();
+-            if (size > i) {
++            if (false && size > i) { // Paper - Separate dimension data IO pool; just throw them into the fixed pool queue
+                 this.pendingWriteFuture = this.pendingWriteFuture.thenCompose(object -> {
+                     List<CompletableFuture<?>> list = new ArrayList<>(i);
+                     int i1 = Mth.positiveCeilDiv(size, i);
+@@ -160,7 +_,7 @@
+                         object -> CompletableFuture.allOf(
+                             map.entrySet()
+                                 .stream()
+-                                .map(entry -> CompletableFuture.runAsync(() -> tryWrite(entry.getKey(), entry.getValue()), Util.ioPool()))
++                                .map(entry -> CompletableFuture.runAsync(() -> tryWrite(entry.getKey(), entry.getValue()), Util.DIMENSION_DATA_IO_POOL)) // Paper - Separate dimension data IO pool
+                                 .toArray(CompletableFuture[]::new)
+                         )
+                     );
diff --git a/paper-server/patches/sources/net/minecraft/world/level/storage/LevelStorageSource.java.patch b/paper-server/patches/sources/net/minecraft/world/level/storage/LevelStorageSource.java.patch
new file mode 100644
index 0000000000..1c0cac272a
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/storage/LevelStorageSource.java.patch
@@ -0,0 +1,78 @@
+--- a/net/minecraft/world/level/storage/LevelStorageSource.java
++++ b/net/minecraft/world/level/storage/LevelStorageSource.java
+@@ -145,6 +_,7 @@
+         PrimaryLevelData primaryLevelData = PrimaryLevelData.parse(
+             dynamic, levelSettings, complete.specialWorldProperty(), worldGenSettings.options(), lifecycle
+         );
++        primaryLevelData.pdc = (Tag) dynamic.getElement("BukkitValues", null); // CraftBukkit - Add PDC to world
+         return new LevelDataAndDimensions(primaryLevelData, complete);
+     }
+ 
+@@ -340,25 +_,39 @@
+         return this.backupDir;
+     }
+ 
+-    public LevelStorageSource.LevelStorageAccess validateAndCreateAccess(String saveName) throws IOException, ContentValidationException {
++    public LevelStorageSource.LevelStorageAccess validateAndCreateAccess(String saveName, ResourceKey<LevelStem> dimensionType) throws IOException, ContentValidationException { // CraftBukkit
+         Path levelPath = this.getLevelPath(saveName);
+-        List<ForbiddenSymlinkInfo> list = this.worldDirValidator.validateDirectory(levelPath, true);
++        List<ForbiddenSymlinkInfo> list = Boolean.getBoolean("paper.disableWorldSymlinkValidation") ? List.of() : this.worldDirValidator.validateDirectory(levelPath, true); // Paper - add skipping of symlinks scan
+         if (!list.isEmpty()) {
+             throw new ContentValidationException(levelPath, list);
+         } else {
+-            return new LevelStorageSource.LevelStorageAccess(saveName, levelPath);
++            return new LevelStorageSource.LevelStorageAccess(saveName, levelPath, dimensionType); // CraftBukkit
+         }
+     }
+ 
+-    public LevelStorageSource.LevelStorageAccess createAccess(String saveName) throws IOException {
++    public LevelStorageSource.LevelStorageAccess createAccess(String saveName, ResourceKey<LevelStem> dimensionType) throws IOException { // CraftBukkit
+         Path levelPath = this.getLevelPath(saveName);
+-        return new LevelStorageSource.LevelStorageAccess(saveName, levelPath);
++        return new LevelStorageSource.LevelStorageAccess(saveName, levelPath, dimensionType); // CraftBukkit
+     }
+ 
+     public DirectoryValidator getWorldDirValidator() {
+         return this.worldDirValidator;
+     }
+ 
++    // CraftBukkit start
++    public static Path getStorageFolder(Path path, ResourceKey<LevelStem> dimensionType) {
++        if (dimensionType == LevelStem.OVERWORLD) {
++            return path;
++        } else if (dimensionType == LevelStem.NETHER) {
++            return path.resolve("DIM-1");
++        } else if (dimensionType == LevelStem.END) {
++            return path.resolve("DIM1");
++        } else {
++            return path.resolve("dimensions").resolve(dimensionType.location().getNamespace()).resolve(dimensionType.location().getPath());
++        }
++    }
++    // CraftBukkit end
++
+     public record LevelCandidates(List<LevelStorageSource.LevelDirectory> levels) implements Iterable<LevelStorageSource.LevelDirectory> {
+         public boolean isEmpty() {
+             return this.levels.isEmpty();
+@@ -409,8 +_,12 @@
+         public final LevelStorageSource.LevelDirectory levelDirectory;
+         private final String levelId;
+         private final Map<LevelResource, Path> resources = Maps.newHashMap();
++        // CraftBukkit start
++        public final ResourceKey<LevelStem> dimensionType;
+ 
+-        LevelStorageAccess(final String levelId, final Path levelDir) throws IOException {
++        LevelStorageAccess(final String levelId, final Path levelDir, final ResourceKey<LevelStem> dimensionType) throws IOException {
++            this.dimensionType = dimensionType;
++            // CraftBukkit end
+             this.levelId = levelId;
+             this.levelDirectory = new LevelStorageSource.LevelDirectory(levelDir);
+             this.lock = DirectoryLock.create(levelDir);
+@@ -453,7 +_,7 @@
+         }
+ 
+         public Path getDimensionPath(ResourceKey<Level> dimensionPath) {
+-            return DimensionType.getStorageFolder(dimensionPath, this.levelDirectory.path());
++            return LevelStorageSource.getStorageFolder(this.levelDirectory.path(), this.dimensionType); // CraftBukkit
+         }
+ 
+         private void checkLock() {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/storage/PlayerDataStorage.java.patch b/paper-server/patches/sources/net/minecraft/world/level/storage/PlayerDataStorage.java.patch
new file mode 100644
index 0000000000..e452ec4ba4
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/storage/PlayerDataStorage.java.patch
@@ -0,0 +1,122 @@
+--- a/net/minecraft/world/level/storage/PlayerDataStorage.java
++++ b/net/minecraft/world/level/storage/PlayerDataStorage.java
+@@ -14,8 +_,10 @@
+ import net.minecraft.nbt.NbtAccounter;
+ import net.minecraft.nbt.NbtIo;
+ import net.minecraft.nbt.NbtUtils;
++import net.minecraft.server.level.ServerPlayer;
+ import net.minecraft.util.datafix.DataFixTypes;
+ import net.minecraft.world.entity.player.Player;
++import org.bukkit.craftbukkit.entity.CraftPlayer;
+ import org.slf4j.Logger;
+ 
+ public class PlayerDataStorage {
+@@ -31,6 +_,7 @@
+     }
+ 
+     public void save(Player player) {
++        if (org.spigotmc.SpigotConfig.disablePlayerDataSaving) return; // Spigot
+         try {
+             CompoundTag compoundTag = player.saveWithoutId(new CompoundTag());
+             Path path = this.playerDir.toPath();
+@@ -40,30 +_,46 @@
+             Path path3 = path.resolve(player.getStringUUID() + ".dat_old");
+             Util.safeReplaceFile(path2, path1, path3);
+         } catch (Exception var7) {
+-            LOGGER.warn("Failed to save player data for {}", player.getName().getString());
++            LOGGER.warn("Failed to save player data for {}", player.getScoreboardName(), var7); // Paper - Print exception
+         }
+     }
+ 
+-    private void backup(Player player, String suffix) {
++    private void backup(String name, String stringUuid, String suffix) { // CraftBukkit
+         Path path = this.playerDir.toPath();
+-        Path path1 = path.resolve(player.getStringUUID() + suffix);
+-        Path path2 = path.resolve(player.getStringUUID() + "_corrupted_" + LocalDateTime.now().format(FORMATTER) + suffix);
++        Path path1 = path.resolve(stringUuid + suffix); // CraftBukkit
++        Path path2 = path.resolve(stringUuid + "_corrupted_" + LocalDateTime.now().format(FORMATTER) + suffix); // CraftBukkit
+         if (Files.isRegularFile(path1)) {
+             try {
+                 Files.copy(path1, path2, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
+             } catch (Exception var7) {
+-                LOGGER.warn("Failed to copy the player.dat file for {}", player.getName().getString(), var7);
++                LOGGER.warn("Failed to copy the player.dat file for {}", name, var7); // CraftBukkit
+             }
+         }
+     }
+ 
+-    private Optional<CompoundTag> load(Player player, String suffix) {
+-        File file = new File(this.playerDir, player.getStringUUID() + suffix);
++    private Optional<CompoundTag> load(String name, String stringUuid, String suffix) { // CraftBukkit
++        File file = new File(this.playerDir, stringUuid + suffix); // CraftBukkit
++        // Spigot start
++        boolean usingWrongFile = false;
++        if (org.bukkit.Bukkit.getOnlineMode() && !file.exists()) { // Paper - Check online mode first
++            file = new File(this.playerDir, java.util.UUID.nameUUIDFromBytes(("OfflinePlayer:" + name).getBytes(java.nio.charset.StandardCharsets.UTF_8)).toString() + suffix);
++            if (file.exists()) {
++                usingWrongFile = true;
++                LOGGER.warn("Using offline mode UUID file for player {} as it is the only copy we can find.", name);
++            }
++        }
++        // Spigot end
+         if (file.exists() && file.isFile()) {
+             try {
+-                return Optional.of(NbtIo.readCompressed(file.toPath(), NbtAccounter.unlimitedHeap()));
++                // Spigot start
++                Optional<CompoundTag> optional = Optional.of(NbtIo.readCompressed(file.toPath(), NbtAccounter.unlimitedHeap()));
++                if (usingWrongFile) {
++                    file.renameTo(new File(file.getPath() + ".offline-read"));
++                }
++                return optional;
++                // Spigot end
+             } catch (Exception var5) {
+-                LOGGER.warn("Failed to load player data for {}", player.getName().getString());
++                LOGGER.warn("Failed to load player data for {}", name); // CraftBukkit
+             }
+         }
+ 
+@@ -71,16 +_,40 @@
+     }
+ 
+     public Optional<CompoundTag> load(Player player) {
+-        Optional<CompoundTag> optional = this.load(player, ".dat");
++        // CraftBukkit start
++        return this.load(player.getName().getString(), player.getStringUUID()).map((tag) -> {
++            if (player instanceof ServerPlayer) {
++                CraftPlayer player1 = (CraftPlayer) player.getBukkitEntity();
++                // Only update first played if it is older than the one we have
++                long modified = new File(this.playerDir, player.getStringUUID() + ".dat").lastModified();
++                if (modified < player1.getFirstPlayed()) {
++                    player1.setFirstPlayed(modified);
++                }
++            }
++
++            player.load(tag); // From below
++            return tag;
++        });
++    }
++
++    public Optional<CompoundTag> load(String name, String uuid) {
++        // CraftBukkit end
++        Optional<CompoundTag> optional = this.load(name, uuid, ".dat"); // CraftBukkit
+         if (optional.isEmpty()) {
+-            this.backup(player, ".dat");
++            this.backup(name, uuid, ".dat"); // CraftBukkit
+         }
+ 
+-        return optional.or(() -> this.load(player, ".dat_old")).map(compoundTag -> {
++        return optional.or(() -> this.load(name, uuid, ".dat_old")).map(compoundTag -> { // CraftBukkit
+             int dataVersion = NbtUtils.getDataVersion(compoundTag, -1);
+             compoundTag = DataFixTypes.PLAYER.updateToCurrentVersion(this.fixerUpper, compoundTag, dataVersion);
+-            player.load(compoundTag);
++            // player.load(compoundTag); // CraftBukkit - handled above
+             return compoundTag;
+         });
+     }
++
++    // CraftBukkit start
++    public File getPlayerDir() {
++        return this.playerDir;
++    }
++    // CraftBukkit end
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/storage/PrimaryLevelData.java.patch b/paper-server/patches/sources/net/minecraft/world/level/storage/PrimaryLevelData.java.patch
new file mode 100644
index 0000000000..8cd7249f86
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/storage/PrimaryLevelData.java.patch
@@ -0,0 +1,123 @@
+--- a/net/minecraft/world/level/storage/PrimaryLevelData.java
++++ b/net/minecraft/world/level/storage/PrimaryLevelData.java
+@@ -74,6 +_,21 @@
+     private final Set<String> removedFeatureFlags;
+     private final TimerQueue<MinecraftServer> scheduledEvents;
+ 
++    // CraftBukkit start - Add world and pdc
++    public net.minecraft.core.Registry<net.minecraft.world.level.dimension.LevelStem> customDimensions;
++    private net.minecraft.server.level.ServerLevel world;
++    protected net.minecraft.nbt.Tag pdc;
++
++    public void setWorld(net.minecraft.server.level.ServerLevel world) {
++        if (this.world != null) {
++            return;
++        }
++        this.world = world;
++        world.getWorld().readBukkitValues(this.pdc);
++        this.pdc = null;
++    }
++    // CraftBukkit end
++
+     private PrimaryLevelData(
+         @Nullable CompoundTag loadedPlayerTag,
+         boolean wasModded,
+@@ -237,7 +_,7 @@
+         nbt.put("Version", compoundTag);
+         NbtUtils.addCurrentDataVersion(nbt);
+         DynamicOps<Tag> dynamicOps = registry.createSerializationContext(NbtOps.INSTANCE);
+-        WorldGenSettings.encode(dynamicOps, this.worldOptions, registry)
++        WorldGenSettings.encode(dynamicOps, this.worldOptions, new net.minecraft.world.level.levelgen.WorldDimensions(this.customDimensions != null ? this.customDimensions : registry.lookupOrThrow(net.minecraft.core.registries.Registries.LEVEL_STEM))) // CraftBukkit
+             .resultOrPartial(Util.prefix("WorldGenSettings: ", LOGGER::error))
+             .ifPresent(worldOptionsTag -> nbt.put("WorldGenSettings", worldOptionsTag));
+         nbt.putInt("GameType", this.settings.gameType().getId());
+@@ -281,6 +_,8 @@
+         if (this.wanderingTraderId != null) {
+             nbt.putUUID("WanderingTraderId", this.wanderingTraderId);
+         }
++        nbt.putString("Bukkit.Version", org.bukkit.Bukkit.getName() + "/" + org.bukkit.Bukkit.getVersion() + "/" + org.bukkit.Bukkit.getBukkitVersion()); // CraftBukkit
++        this.world.getWorld().storeBukkitValues(nbt); // CraftBukkit - add pdc
+     }
+ 
+     private static ListTag stringCollectionToTag(Set<String> stringCollection) {
+@@ -358,6 +_,25 @@
+ 
+     @Override
+     public void setThundering(boolean thundering) {
++        // Paper start - Add cause to Weather/ThunderChangeEvents
++        this.setThundering(thundering, org.bukkit.event.weather.ThunderChangeEvent.Cause.UNKNOWN);
++    }
++    public void setThundering(boolean thundering, org.bukkit.event.weather.ThunderChangeEvent.Cause cause) {
++        // Paper end - Add cause to Weather/ThunderChangeEvents
++        // CraftBukkit start
++        if (this.thundering == thundering) {
++            return;
++        }
++
++        org.bukkit.World world = org.bukkit.Bukkit.getWorld(this.getLevelName());
++        if (world != null) {
++            org.bukkit.event.weather.ThunderChangeEvent thunder = new org.bukkit.event.weather.ThunderChangeEvent(world, thundering, cause); // Paper - Add cause to Weather/ThunderChangeEvents
++            org.bukkit.Bukkit.getServer().getPluginManager().callEvent(thunder);
++            if (thunder.isCancelled()) {
++                return;
++            }
++        }
++        // CraftBukkit end
+         this.thundering = thundering;
+     }
+ 
+@@ -378,6 +_,26 @@
+ 
+     @Override
+     public void setRaining(boolean isRaining) {
++        // Paper start - Add cause to Weather/ThunderChangeEvents
++        this.setRaining(isRaining, org.bukkit.event.weather.WeatherChangeEvent.Cause.UNKNOWN);
++    }
++
++    public void setRaining(boolean isRaining, org.bukkit.event.weather.WeatherChangeEvent.Cause cause) {
++        // Paper end - Add cause to Weather/ThunderChangeEvents
++        // CraftBukkit start
++        if (this.raining == isRaining) {
++            return;
++        }
++
++        org.bukkit.World world = org.bukkit.Bukkit.getWorld(this.getLevelName());
++        if (world != null) {
++            org.bukkit.event.weather.WeatherChangeEvent weather = new org.bukkit.event.weather.WeatherChangeEvent(world, isRaining, cause); // Paper - Add cause to Weather/ThunderChangeEvents
++            org.bukkit.Bukkit.getServer().getPluginManager().callEvent(weather);
++            if (weather.isCancelled()) {
++                return;
++            }
++        }
++        // CraftBukkit end
+         this.raining = isRaining;
+     }
+ 
+@@ -444,6 +_,12 @@
+     @Override
+     public void setDifficulty(Difficulty difficulty) {
+         this.settings = this.settings.withDifficulty(difficulty);
++        // CraftBukkit start
++        net.minecraft.network.protocol.game.ClientboundChangeDifficultyPacket packet = new net.minecraft.network.protocol.game.ClientboundChangeDifficultyPacket(this.getDifficulty(), this.isDifficultyLocked());
++        for (net.minecraft.server.level.ServerPlayer player : (java.util.List<net.minecraft.server.level.ServerPlayer>) (java.util.List) this.world.players()) {
++            player.connection.send(packet);
++        }
++        // CraftBukkit end
+     }
+ 
+     @Override
+@@ -579,6 +_,14 @@
+     public LevelSettings getLevelSettings() {
+         return this.settings.copy();
+     }
++
++    // CraftBukkit start - Check if the name stored in NBT is the correct one
++    public void checkName(String name) {
++        if (!this.settings.levelName.equals(name)) {
++            this.settings.levelName = name;
++        }
++    }
++    // CraftBukkit end
+ 
+     @Deprecated
+     public static enum SpecialWorldProperty {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/storage/loot/LootDataType.java.patch b/paper-server/patches/sources/net/minecraft/world/level/storage/loot/LootDataType.java.patch
new file mode 100644
index 0000000000..87eb699603
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/storage/loot/LootDataType.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/level/storage/loot/LootDataType.java
++++ b/net/minecraft/world/level/storage/loot/LootDataType.java
+@@ -31,9 +_,14 @@
+     }
+ 
+     private static LootDataType.Validator<LootTable> createLootTableValidator() {
+-        return (context, key, value) -> value.validate(
+-            context.setContextKeySet(value.getParamSet()).enterElement("{" + key.registry() + "/" + key.location() + "}", key)
+-        );
++        // CraftBukkit start
++        return (context, key, value) -> {
++            value.validate(
++                context.setContextKeySet(value.getParamSet()).enterElement("{" + key.registry() + "/" + key.location() + "}", key)
++            );
++            value.craftLootTable = new org.bukkit.craftbukkit.CraftLootTable(org.bukkit.craftbukkit.util.CraftNamespacedKey.fromMinecraft(key.location()), value);
++            // CraftBukkit end
++        };
+     }
+ 
+     @FunctionalInterface
diff --git a/paper-server/patches/sources/net/minecraft/world/level/storage/loot/LootTable.java.patch b/paper-server/patches/sources/net/minecraft/world/level/storage/loot/LootTable.java.patch
new file mode 100644
index 0000000000..21a8108d1a
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/storage/loot/LootTable.java.patch
@@ -0,0 +1,45 @@
+--- a/net/minecraft/world/level/storage/loot/LootTable.java
++++ b/net/minecraft/world/level/storage/loot/LootTable.java
+@@ -48,6 +_,7 @@
+     private final List<LootPool> pools;
+     private final List<LootItemFunction> functions;
+     private final BiFunction<ItemStack, LootContext, ItemStack> compositeFunction;
++    public org.bukkit.craftbukkit.CraftLootTable craftLootTable; // CraftBukkit
+ 
+     LootTable(ContextKeySet paramSet, Optional<ResourceLocation> randomSequence, List<LootPool> pools, List<LootItemFunction> functions) {
+         this.paramSet = paramSet;
+@@ -58,9 +_,10 @@
+     }
+ 
+     public static Consumer<ItemStack> createStackSplitter(ServerLevel level, Consumer<ItemStack> output) {
++        boolean skipSplitter = level != null && !level.paperConfig().fixes.splitOverstackedLoot; // Paper - preserve overstacked items
+         return itemStack -> {
+             if (itemStack.isItemEnabled(level.enabledFeatures())) {
+-                if (itemStack.getCount() < itemStack.getMaxStackSize()) {
++                if (skipSplitter || itemStack.getCount() < itemStack.getMaxStackSize()) { // Paper - preserve overstacked items
+                     output.accept(itemStack);
+                 } else {
+                     int count = itemStack.getCount();
+@@ -141,9 +_,22 @@
+     }
+ 
+     public void fill(Container container, LootParams params, long seed) {
++        // CraftBukkit start
++        this.fillInventory(container, params, seed, false);
++    }
++
++    public void fillInventory(Container container, LootParams params, long seed, boolean plugin) {
++        // CraftBukkit end
+         LootContext lootContext = new LootContext.Builder(params).withOptionalRandomSeed(seed).create(this.randomSequence);
+         ObjectArrayList<ItemStack> randomItems = this.getRandomItems(lootContext);
+         RandomSource random = lootContext.getRandom();
++        // CraftBukkit start
++        org.bukkit.event.world.LootGenerateEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callLootGenerateEvent(container, this, lootContext, randomItems, plugin);
++        if (event.isCancelled()) {
++            return;
++        }
++        randomItems = event.getLoot().stream().map(org.bukkit.craftbukkit.inventory.CraftItemStack::asNMSCopy).collect(ObjectArrayList.toList());
++        // CraftBukkit end
+         List<Integer> availableSlots = this.getAvailableSlots(container, random);
+         this.shuffleAndSplitItems(randomItems, availableSlots.size(), random);
+ 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/storage/loot/entries/LootPoolSingletonContainer.java.patch b/paper-server/patches/sources/net/minecraft/world/level/storage/loot/entries/LootPoolSingletonContainer.java.patch
similarity index 77%
rename from paper-server/patches/unapplied/net/minecraft/world/level/storage/loot/entries/LootPoolSingletonContainer.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/storage/loot/entries/LootPoolSingletonContainer.java.patch
index f97db2bfbc..baa99f7a0b 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/storage/loot/entries/LootPoolSingletonContainer.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/storage/loot/entries/LootPoolSingletonContainer.java.patch
@@ -1,10 +1,21 @@
 --- a/net/minecraft/world/level/storage/loot/entries/LootPoolSingletonContainer.java
 +++ b/net/minecraft/world/level/storage/loot/entries/LootPoolSingletonContainer.java
-@@ -126,9 +126,35 @@
+@@ -32,6 +_,10 @@
+             );
+         }
+     };
++    // Paper start - Configurable LootPool luck formula
++    private Float lastLuck;
++    private int lastWeight;
++    // Paper end - Configurable LootPool luck formula
+ 
+     protected LootPoolSingletonContainer(int weight, int quality, List<LootItemCondition> conditions, List<LootItemFunction> functions) {
+         super(conditions);
+@@ -126,7 +_,31 @@
      protected abstract class EntryBase implements LootPoolEntry {
          @Override
          public int getWeight(float luck) {
--            return Math.max(Mth.floor((float)LootPoolSingletonContainer.this.weight + (float)LootPoolSingletonContainer.this.quality * luck), 0);
+-            return Math.max(Mth.floor(LootPoolSingletonContainer.this.weight + LootPoolSingletonContainer.this.quality * luck), 0);
 +            // Paper start - Configurable LootPool luck formula
 +            // SEE: https://luckformula.emc.gs for details and data
 +            if (LootPoolSingletonContainer.this.lastLuck != null && LootPoolSingletonContainer.this.lastLuck == luck) {
@@ -29,11 +40,7 @@
 +            LootPoolSingletonContainer.this.lastLuck = luck;
 +            LootPoolSingletonContainer.this.lastWeight = (int) Math.max(Math.floor(baseWeight), 0);
 +            return lastWeight;
++            // Paper end - Configurable LootPool luck formula
          }
      }
-+    private Float lastLuck = null;
-+    private int lastWeight = 0;
-+    // Paper end - Configurable LootPool luck formula
  
-     @FunctionalInterface
-     protected interface EntryConstructor {
diff --git a/paper-server/patches/sources/net/minecraft/world/level/storage/loot/functions/ExplorationMapFunction.java.patch b/paper-server/patches/sources/net/minecraft/world/level/storage/loot/functions/ExplorationMapFunction.java.patch
new file mode 100644
index 0000000000..29f028e11a
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/storage/loot/functions/ExplorationMapFunction.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/level/storage/loot/functions/ExplorationMapFunction.java
++++ b/net/minecraft/world/level/storage/loot/functions/ExplorationMapFunction.java
+@@ -89,7 +_,16 @@
+             Vec3 vec3 = context.getOptionalParameter(LootContextParams.ORIGIN);
+             if (vec3 != null) {
+                 ServerLevel level = context.getLevel();
+-                BlockPos blockPos = level.findNearestMapStructure(this.destination, BlockPos.containing(vec3), this.searchRadius, this.skipKnownStructures);
++                // Paper start - Configurable cartographer treasure maps
++                if (!level.paperConfig().environment.treasureMaps.enabled) {
++                    /*
++                     * NOTE: I fear users will just get a plain map as their "treasure"
++                     * This is preferable to disrespecting the config.
++                     */
++                    return stack;
++                }
++                // Paper end - Configurable cartographer treasure maps
++                BlockPos blockPos = level.findNearestMapStructure(this.destination, BlockPos.containing(vec3), this.searchRadius, !level.paperConfig().environment.treasureMaps.findAlreadyDiscoveredLootTable.or(!this.skipKnownStructures)); // Paper - Configurable cartographer treasure maps
+                 if (blockPos != null) {
+                     ItemStack itemStack = MapItem.create(level, blockPos.getX(), blockPos.getZ(), this.zoom, true, true);
+                     MapItem.renderBiomePreviewMap(level, itemStack);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/storage/loot/predicates/ExplosionCondition.java.patch b/paper-server/patches/sources/net/minecraft/world/level/storage/loot/predicates/ExplosionCondition.java.patch
similarity index 58%
rename from paper-server/patches/unapplied/net/minecraft/world/level/storage/loot/predicates/ExplosionCondition.java.patch
rename to paper-server/patches/sources/net/minecraft/world/level/storage/loot/predicates/ExplosionCondition.java.patch
index 19cd841992..53cc48355e 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/level/storage/loot/predicates/ExplosionCondition.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/storage/loot/predicates/ExplosionCondition.java.patch
@@ -1,12 +1,12 @@
 --- a/net/minecraft/world/level/storage/loot/predicates/ExplosionCondition.java
 +++ b/net/minecraft/world/level/storage/loot/predicates/ExplosionCondition.java
-@@ -31,7 +31,8 @@
-             RandomSource randomsource = loottableinfo.getRandom();
-             float f = 1.0F / ofloat;
- 
--            return randomsource.nextFloat() <= f;
+@@ -30,7 +_,8 @@
+         if (_float != null) {
+             RandomSource random = context.getRandom();
+             float f = 1.0F / _float;
+-            return random.nextFloat() <= f;
 +            // CraftBukkit - <= to < to allow for plugins to completely disable block drops from explosions
-+            return randomsource.nextFloat() < f;
++            return random.nextFloat() < f;
          } else {
              return true;
          }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/scores/ScoreboardSaveData.java.patch b/paper-server/patches/sources/net/minecraft/world/scores/ScoreboardSaveData.java.patch
similarity index 89%
rename from paper-server/patches/unapplied/net/minecraft/world/scores/ScoreboardSaveData.java.patch
rename to paper-server/patches/sources/net/minecraft/world/scores/ScoreboardSaveData.java.patch
index d29c6da396..c8b1a7802f 100644
--- a/paper-server/patches/unapplied/net/minecraft/world/scores/ScoreboardSaveData.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/scores/ScoreboardSaveData.java.patch
@@ -1,10 +1,10 @@
 --- a/net/minecraft/world/scores/ScoreboardSaveData.java
 +++ b/net/minecraft/world/scores/ScoreboardSaveData.java
-@@ -148,6 +148,7 @@
+@@ -148,6 +_,7 @@
          ListTag listTag = new ListTag();
  
          for (PlayerTeam playerTeam : this.scoreboard.getPlayerTeams()) {
 +            if (!io.papermc.paper.configuration.GlobalConfiguration.get().scoreboards.saveEmptyScoreboardTeams && playerTeam.getPlayers().isEmpty()) continue; // Paper - Don't save empty scoreboard teams to scoreboard.dat
              CompoundTag compoundTag = new CompoundTag();
              compoundTag.putString("Name", playerTeam.getName());
-             compoundTag.putString("DisplayName", Component.Serializer.toJson(playerTeam.getDisplayName(), registries));
+             compoundTag.putString("DisplayName", Component.Serializer.toJson(playerTeam.getDisplayName(), levelRegistry));
diff --git a/paper-server/patches/sources/net/neoforged/art/internal/RenamerImpl.java.patch b/paper-server/patches/sources/net/neoforged/art/internal/RenamerImpl.java.patch
new file mode 100644
index 0000000000..69b4834654
--- /dev/null
+++ b/paper-server/patches/sources/net/neoforged/art/internal/RenamerImpl.java.patch
@@ -0,0 +1,67 @@
+--- a/net/neoforged/art/internal/RenamerImpl.java
++++ b/net/neoforged/art/internal/RenamerImpl.java
+@@ -35,7 +_,7 @@
+ import net.neoforged.cliutils.progress.ProgressReporter;
+ import org.objectweb.asm.Opcodes;
+ 
+-class RenamerImpl implements Renamer {
++public class RenamerImpl implements Renamer { // Paper - public
+     private static final ProgressReporter PROGRESS = ProgressReporter.getDefault();
+     static final int MAX_ASM_VERSION = Opcodes.ASM9;
+     private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";
+@@ -75,6 +_,11 @@
+ 
+     @Override
+     public void run(File input, File output) {
++        // Paper start - Add remappingSelf
++        this.run(input, output, false);
++    }
++    public void run(File input, File output, boolean remappingSelf) {
++        // Paper end
+         if (!this.setup)
+             this.setup();
+ 
+@@ -105,10 +_,10 @@
+                 String name = e.getName();
+                 byte[] data;
+                 try (InputStream entryInput = in.getInputStream(e)) {
+-                    data = readAllBytes(entryInput, e.getSize());
++                    data = entryInput.readAllBytes(); // Paper - Use readAllBytes
+                 }
+ 
+-                if (name.endsWith(".class"))
++                if (name.endsWith(".class") && !name.contains("META-INF/")) // Paper - Skip META-INF entries
+                     oldEntries.add(ClassEntry.create(name, e.getTime(), data));
+                 else if (name.equals(MANIFEST_NAME))
+                     oldEntries.add(ManifestEntry.create(e.getTime(), data));
+@@ -163,15 +_,29 @@
+             List<Entry> newEntries = async.invokeAll(oldEntries, Entry::getName, this::processEntry);
+ 
+             logger.accept("Adding extras");
+-            transformers.forEach(t -> newEntries.addAll(t.getExtras()));
++            // Paper start - I'm pretty sure the duplicates are because the input is already on the classpath
++            List<Entry> finalNewEntries = newEntries;
++            transformers.forEach(t -> finalNewEntries.addAll(t.getExtras()));
+ 
+             Set<String> seen = new HashSet<>();
++            if (remappingSelf) {
++                // deduplicate
++                List<Entry> n = new ArrayList<>();
++                for (final Entry e : newEntries) {
++                    if (seen.add(e.getName())) {
++                        n.add(e);
++                    }
++                }
++                newEntries = n;
++            } else {
+             String dupes = newEntries.stream().map(Entry::getName)
+                 .filter(n -> !seen.add(n))
+                 .sorted()
+                 .collect(Collectors.joining(", "));
+             if (!dupes.isEmpty())
+                 throw new IllegalStateException("Duplicate entries detected: " + dupes);
++            }
++            // Paper end
+ 
+             // We care about stable output, so sort, and single thread write.
+             logger.accept("Sorting");
diff --git a/paper-server/patches/unapplied/net/minecraft/advancements/AdvancementHolder.java.patch b/paper-server/patches/unapplied/net/minecraft/advancements/AdvancementHolder.java.patch
deleted file mode 100644
index d41639df88..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/advancements/AdvancementHolder.java.patch
+++ /dev/null
@@ -1,24 +0,0 @@
---- a/net/minecraft/advancements/AdvancementHolder.java
-+++ b/net/minecraft/advancements/AdvancementHolder.java
-@@ -5,6 +5,10 @@
- import net.minecraft.network.codec.ByteBufCodecs;
- import net.minecraft.network.codec.StreamCodec;
- import net.minecraft.resources.ResourceLocation;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.advancement.CraftAdvancement;
-+import org.bukkit.craftbukkit.util.CraftNamespacedKey;
-+// CraftBukkit end
- 
- public record AdvancementHolder(ResourceLocation id, Advancement value) {
- 
-@@ -38,4 +42,10 @@
-     public String toString() {
-         return this.id.toString();
-     }
-+
-+    // CraftBukkit start
-+    public final org.bukkit.advancement.Advancement toBukkit() {
-+        return new CraftAdvancement(this);
-+    }
-+    // CraftBukkit end
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/advancements/AdvancementTree.java.patch b/paper-server/patches/unapplied/net/minecraft/advancements/AdvancementTree.java.patch
deleted file mode 100644
index 4bec8c46eb..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/advancements/AdvancementTree.java.patch
+++ /dev/null
@@ -1,20 +0,0 @@
---- a/net/minecraft/advancements/AdvancementTree.java
-+++ b/net/minecraft/advancements/AdvancementTree.java
-@@ -35,7 +35,7 @@
-             this.remove(advancementnode1);
-         }
- 
--        AdvancementTree.LOGGER.info("Forgot about advancement {}", advancement.holder());
-+        AdvancementTree.LOGGER.debug("Forgot about advancement {}", advancement.holder()); // Paper - Improve logging and errors
-         this.nodes.remove(advancement.holder().id());
-         if (advancement.parent() == null) {
-             this.roots.remove(advancement);
-@@ -77,7 +77,7 @@
-             }
-         }
- 
--        AdvancementTree.LOGGER.info("Loaded {} advancements", this.nodes.size());
-+        // AdvancementTree.LOGGER.info("Loaded {} advancements", this.nodes.size()); // CraftBukkit - moved to AdvancementDataWorld#reload // Paper - Improve logging and errors; you say it was moved... but it wasn't :) it should be moved however, since this is called when the API creates an advancement
-     }
- 
-     private boolean tryInsert(AdvancementHolder advancement) {
diff --git a/paper-server/patches/unapplied/net/minecraft/commands/Commands.java.patch b/paper-server/patches/unapplied/net/minecraft/commands/Commands.java.patch
deleted file mode 100644
index 1fc19d5217..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/commands/Commands.java.patch
+++ /dev/null
@@ -1,372 +0,0 @@
---- a/net/minecraft/commands/Commands.java
-+++ b/net/minecraft/commands/Commands.java
-@@ -138,6 +138,14 @@
- import net.minecraft.world.flag.FeatureFlags;
- import net.minecraft.world.level.GameRules;
- import org.slf4j.Logger;
-+
-+// CraftBukkit start
-+import com.google.common.base.Joiner;
-+import java.util.Collection;
-+import java.util.LinkedHashSet;
-+import org.bukkit.event.player.PlayerCommandSendEvent;
-+import org.bukkit.event.server.ServerCommandEvent;
-+// CraftBukkit end
- 
- public class Commands {
- 
-@@ -151,6 +159,7 @@
-     private final com.mojang.brigadier.CommandDispatcher<CommandSourceStack> dispatcher = new com.mojang.brigadier.CommandDispatcher();
- 
-     public Commands(Commands.CommandSelection environment, CommandBuildContext commandRegistryAccess) {
-+        // Paper
-         AdvancementCommands.register(this.dispatcher);
-         AttributeCommand.register(this.dispatcher, commandRegistryAccess);
-         ExecuteCommand.register(this.dispatcher, commandRegistryAccess);
-@@ -250,8 +259,33 @@
- 
-         if (environment.includeIntegrated) {
-             PublishCommand.register(this.dispatcher);
-+        }
-+
-+        // Paper start - Vanilla command permission fixes
-+        for (final CommandNode<CommandSourceStack> node : this.dispatcher.getRoot().getChildren()) {
-+            if (node.getRequirement() == com.mojang.brigadier.builder.ArgumentBuilder.<CommandSourceStack>defaultRequirement()) {
-+                node.requirement = stack -> stack.source == CommandSource.NULL || stack.getBukkitSender().hasPermission(org.bukkit.craftbukkit.command.VanillaCommandWrapper.getPermission(node));
-+            }
-         }
-+        // Paper end - Vanilla command permission fixes
-+        // Paper start - Brigadier Command API
-+        // Create legacy minecraft namespace commands
-+        for (final CommandNode<CommandSourceStack> node : new java.util.ArrayList<>(this.dispatcher.getRoot().getChildren())) {
-+            // The brigadier dispatcher is not able to resolve nested redirects.
-+            // E.g. registering the alias minecraft:tp cannot redirect to tp, as tp itself redirects to teleport.
-+            // Instead, target the first none redirecting node.
-+            CommandNode<CommandSourceStack> flattenedAliasTarget = node;
-+            while (flattenedAliasTarget.getRedirect() != null) flattenedAliasTarget = flattenedAliasTarget.getRedirect();
- 
-+            this.dispatcher.register(
-+                com.mojang.brigadier.builder.LiteralArgumentBuilder.<CommandSourceStack>literal("minecraft:" + node.getName())
-+                    .executes(flattenedAliasTarget.getCommand())
-+                    .requires(flattenedAliasTarget.getRequirement())
-+                    .redirect(flattenedAliasTarget)
-+            );
-+        }
-+        // Paper end - Brigadier Command API
-+        // Paper - remove public constructor, no longer needed
-         this.dispatcher.setConsumer(ExecutionCommandSource.resultConsumer());
-     }
- 
-@@ -262,30 +296,72 @@
-         return new ParseResults(commandcontextbuilder1, parseResults.getReader(), parseResults.getExceptions());
-     }
- 
-+    // CraftBukkit start
-+    public void dispatchServerCommand(CommandSourceStack sender, String command) {
-+        Joiner joiner = Joiner.on(" ");
-+        if (command.startsWith("/")) {
-+            command = command.substring(1);
-+        }
-+
-+        ServerCommandEvent event = new ServerCommandEvent(sender.getBukkitSender(), command);
-+        org.bukkit.Bukkit.getPluginManager().callEvent(event);
-+        if (event.isCancelled()) {
-+            return;
-+        }
-+        command = event.getCommand();
-+
-+        String[] args = command.split(" ");
-+        if (args.length == 0) return; // Paper - empty commands shall not be dispatched
-+
-+        // Paper - Fix permission levels for command blocks
-+
-+        // Handle vanilla commands; // Paper - handled in CommandNode/CommandDispatcher
-+
-+        String newCommand = joiner.join(args);
-+        this.performPrefixedCommand(sender, newCommand, newCommand);
-+    }
-+    // CraftBukkit end
-+
-     public void performPrefixedCommand(CommandSourceStack source, String command) {
--        command = command.startsWith("/") ? command.substring(1) : command;
--        this.performCommand(this.dispatcher.parse(command, source), command);
-+        // CraftBukkit start
-+        this.performPrefixedCommand(source, command, command);
-+    }
-+
-+    public void performPrefixedCommand(CommandSourceStack commandlistenerwrapper, String s, String label) {
-+        s = s.startsWith("/") ? s.substring(1) : s;
-+        this.performCommand(this.dispatcher.parse(s, commandlistenerwrapper), s, label);
-+        // CraftBukkit end
-     }
- 
-     public void performCommand(ParseResults<CommandSourceStack> parseResults, String command) {
--        CommandSourceStack commandlistenerwrapper = (CommandSourceStack) parseResults.getContext().getSource();
-+        this.performCommand(parseResults, command, command);
-+    }
- 
-+    public void performCommand(ParseResults<CommandSourceStack> parseresults, String s, String label) { // CraftBukkit
-+        // Paper start
-+        this.performCommand(parseresults, s, label, false);
-+    }
-+    public void performCommand(ParseResults<CommandSourceStack> parseresults, String s, String label, boolean throwCommandError) {
-+        // Paper end
-+        CommandSourceStack commandlistenerwrapper = (CommandSourceStack) parseresults.getContext().getSource();
-+
-         Profiler.get().push(() -> {
--            return "/" + command;
-+            return "/" + s;
-         });
--        ContextChain<CommandSourceStack> contextchain = Commands.finishParsing(parseResults, command, commandlistenerwrapper);
-+        ContextChain contextchain = this.finishParsing(parseresults, s, commandlistenerwrapper, label); // CraftBukkit // Paper - Add UnknownCommandEvent
- 
-         try {
-             if (contextchain != null) {
-                 Commands.executeCommandInContext(commandlistenerwrapper, (executioncontext) -> {
--                    ExecutionContext.queueInitialCommandExecution(executioncontext, command, contextchain, commandlistenerwrapper, CommandResultCallback.EMPTY);
-+                    ExecutionContext.queueInitialCommandExecution(executioncontext, s, contextchain, commandlistenerwrapper, CommandResultCallback.EMPTY);
-                 });
-             }
-         } catch (Exception exception) {
-+            if (throwCommandError) throw exception;
-             MutableComponent ichatmutablecomponent = Component.literal(exception.getMessage() == null ? exception.getClass().getName() : exception.getMessage());
- 
--            if (Commands.LOGGER.isDebugEnabled()) {
--                Commands.LOGGER.error("Command exception: /{}", command, exception);
-+            Commands.LOGGER.error("Command exception: /{}", s, exception); // Paper - always show execution exception in console log
-+            if (commandlistenerwrapper.getServer().isDebugging() || Commands.LOGGER.isDebugEnabled()) { // Paper - Debugging
-                 StackTraceElement[] astacktraceelement = exception.getStackTrace();
- 
-                 for (int i = 0; i < Math.min(astacktraceelement.length, 3); ++i) {
-@@ -298,7 +374,7 @@
-             }));
-             if (SharedConstants.IS_RUNNING_IN_IDE) {
-                 commandlistenerwrapper.sendFailure(Component.literal(Util.describeError(exception)));
--                Commands.LOGGER.error("'/{}' threw an exception", command, exception);
-+                Commands.LOGGER.error("'/{}' threw an exception", s, exception);
-             }
-         } finally {
-             Profiler.get().pop();
-@@ -307,18 +383,22 @@
-     }
- 
-     @Nullable
--    private static ContextChain<CommandSourceStack> finishParsing(ParseResults<CommandSourceStack> parseResults, String command, CommandSourceStack source) {
-+    private ContextChain<CommandSourceStack> finishParsing(ParseResults<CommandSourceStack> parseresults, String s, CommandSourceStack commandlistenerwrapper, String label) { // CraftBukkit // Paper - Add UnknownCommandEvent
-         try {
--            Commands.validateParseResults(parseResults);
--            return (ContextChain) ContextChain.tryFlatten(parseResults.getContext().build(command)).orElseThrow(() -> {
--                return CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand().createWithContext(parseResults.getReader());
-+            Commands.validateParseResults(parseresults);
-+            return (ContextChain) ContextChain.tryFlatten(parseresults.getContext().build(s)).orElseThrow(() -> {
-+                return CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand().createWithContext(parseresults.getReader());
-             });
-         } catch (CommandSyntaxException commandsyntaxexception) {
--            source.sendFailure(ComponentUtils.fromMessage(commandsyntaxexception.getRawMessage()));
-+            // Paper start - Add UnknownCommandEvent
-+            final net.kyori.adventure.text.TextComponent.Builder builder = net.kyori.adventure.text.Component.text();
-+            // commandlistenerwrapper.sendFailure(ComponentUtils.fromMessage(commandsyntaxexception.getRawMessage()));
-+            builder.color(net.kyori.adventure.text.format.NamedTextColor.RED).append(io.papermc.paper.brigadier.PaperBrigadier.componentFromMessage(commandsyntaxexception.getRawMessage()));
-+            // Paper end - Add UnknownCommandEvent
-             if (commandsyntaxexception.getInput() != null && commandsyntaxexception.getCursor() >= 0) {
-                 int i = Math.min(commandsyntaxexception.getInput().length(), commandsyntaxexception.getCursor());
-                 MutableComponent ichatmutablecomponent = Component.empty().withStyle(ChatFormatting.GRAY).withStyle((chatmodifier) -> {
--                    return chatmodifier.withClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/" + command));
-+                    return chatmodifier.withClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/" + label)); // CraftBukkit // Paper
-                 });
- 
-                 if (i > 10) {
-@@ -333,8 +413,18 @@
-                 }
- 
-                 ichatmutablecomponent.append((Component) Component.translatable("command.context.here").withStyle(ChatFormatting.RED, ChatFormatting.ITALIC));
--                source.sendFailure(ichatmutablecomponent);
-+                // Paper start - Add UnknownCommandEvent
-+                // commandlistenerwrapper.sendFailure(ichatmutablecomponent);
-+                builder
-+                    .append(net.kyori.adventure.text.Component.newline())
-+                    .append(io.papermc.paper.adventure.PaperAdventure.asAdventure(ichatmutablecomponent));
-             }
-+            org.bukkit.event.command.UnknownCommandEvent event = new org.bukkit.event.command.UnknownCommandEvent(commandlistenerwrapper.getBukkitSender(), s, org.spigotmc.SpigotConfig.unknownCommandMessage.isEmpty() ? null : builder.build());
-+            org.bukkit.Bukkit.getServer().getPluginManager().callEvent(event);
-+            if (event.message() != null) {
-+                commandlistenerwrapper.sendFailure(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.message()), false);
-+                // Paper end - Add UnknownCommandEvent
-+            }
- 
-             return null;
-         }
-@@ -368,7 +458,7 @@
- 
-                 executioncontext1.close();
-             } finally {
--                Commands.CURRENT_EXECUTION_CONTEXT.set((Object) null);
-+                Commands.CURRENT_EXECUTION_CONTEXT.set(null); // CraftBukkit - decompile error
-             }
-         } else {
-             callback.accept(executioncontext);
-@@ -377,30 +467,133 @@
-     }
- 
-     public void sendCommands(ServerPlayer player) {
--        Map<CommandNode<CommandSourceStack>, CommandNode<SharedSuggestionProvider>> map = Maps.newHashMap();
-+        // Paper start - Send empty commands if tab completion is disabled
-+        if (org.spigotmc.SpigotConfig.tabComplete < 0) {
-+            player.connection.send(new ClientboundCommandsPacket(new RootCommandNode<>()));
-+            return;
-+        }
-+        // Paper end - Send empty commands if tab completion is disabled
-+        // CraftBukkit start
-+        // Register Vanilla commands into builtRoot as before
-+        // Paper start - Perf: Async command map building
-+        // Copy root children to avoid concurrent modification during building
-+        final Collection<CommandNode<CommandSourceStack>> commandNodes = new java.util.ArrayList<>(this.dispatcher.getRoot().getChildren());
-+        COMMAND_SENDING_POOL.execute(() -> this.sendAsync(player, commandNodes));
-+    }
-+
-+    // Fixed pool, but with discard policy
-+    public static final java.util.concurrent.ExecutorService COMMAND_SENDING_POOL = new java.util.concurrent.ThreadPoolExecutor(
-+        2, 2, 0, java.util.concurrent.TimeUnit.MILLISECONDS,
-+        new java.util.concurrent.LinkedBlockingQueue<>(),
-+        new com.google.common.util.concurrent.ThreadFactoryBuilder()
-+            .setNameFormat("Paper Async Command Builder Thread Pool - %1$d")
-+            .setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(net.minecraft.server.MinecraftServer.LOGGER))
-+            .build(),
-+        new java.util.concurrent.ThreadPoolExecutor.DiscardPolicy()
-+    );
-+
-+    private void sendAsync(ServerPlayer player, Collection<CommandNode<CommandSourceStack>> dispatcherRootChildren) {
-+        // Paper end - Perf: Async command map building
-+        Map<CommandNode<CommandSourceStack>, CommandNode<SharedSuggestionProvider>> map = Maps.newIdentityHashMap(); // Use identity to prevent aliasing issues
-+        // Paper - brigadier API removes the need to fill the map twice
-         RootCommandNode<SharedSuggestionProvider> rootcommandnode = new RootCommandNode();
- 
-         map.put(this.dispatcher.getRoot(), rootcommandnode);
--        this.fillUsableCommands(this.dispatcher.getRoot(), rootcommandnode, player.createCommandSourceStack(), map);
-+        this.fillUsableCommands(dispatcherRootChildren, rootcommandnode, player.createCommandSourceStack(), map); // Paper - Perf: Async command map building; pass copy of children
-+
-+        Collection<String> bukkit = new LinkedHashSet<>();
-+        for (CommandNode node : rootcommandnode.getChildren()) {
-+            bukkit.add(node.getName());
-+        }
-+        // Paper start - Perf: Async command map building
-+        new com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent<CommandSourceStack>(player.getBukkitEntity(), (RootCommandNode) rootcommandnode, false).callEvent(); // Paper - Brigadier API
-+        net.minecraft.server.MinecraftServer.getServer().execute(() -> {
-+           runSync(player, bukkit, rootcommandnode);
-+        });
-+    }
-+
-+    private void runSync(ServerPlayer player, Collection<String> bukkit, RootCommandNode<SharedSuggestionProvider> rootcommandnode) {
-+        // Paper end - Perf: Async command map building
-+        new com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent<CommandSourceStack>(player.getBukkitEntity(), (RootCommandNode) rootcommandnode, true).callEvent(); // Paper - Brigadier API
-+        PlayerCommandSendEvent event = new PlayerCommandSendEvent(player.getBukkitEntity(), new LinkedHashSet<>(bukkit));
-+        event.getPlayer().getServer().getPluginManager().callEvent(event);
-+
-+        // Remove labels that were removed during the event
-+        for (String orig : bukkit) {
-+            if (!event.getCommands().contains(orig)) {
-+                rootcommandnode.removeCommand(orig);
-+            }
-+        }
-+        // CraftBukkit end
-         player.connection.send(new ClientboundCommandsPacket(rootcommandnode));
-     }
- 
--    private void fillUsableCommands(CommandNode<CommandSourceStack> tree, CommandNode<SharedSuggestionProvider> result, CommandSourceStack source, Map<CommandNode<CommandSourceStack>, CommandNode<SharedSuggestionProvider>> resultNodes) {
--        Iterator iterator = tree.getChildren().iterator();
-+    // Paper start - Perf: Async command map building; pass copy of children
-+    private void fillUsableCommands(Collection<CommandNode<CommandSourceStack>> children, CommandNode<SharedSuggestionProvider> result, CommandSourceStack source, Map<CommandNode<CommandSourceStack>, CommandNode<SharedSuggestionProvider>> resultNodes) {
-+        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 = children.iterator();
-+        // Paper end - Perf: Async command map building
- 
-         while (iterator.hasNext()) {
-             CommandNode<CommandSourceStack> commandnode2 = (CommandNode) iterator.next();
-+            // Paper start - Brigadier API
-+            if (commandnode2.clientNode != null) {
-+                commandnode2 = commandnode2.clientNode;
-+            }
-+            // Paper end - Brigadier API
-+            if ( !org.spigotmc.SpigotConfig.sendNamespaced && commandnode2.getName().contains( ":" ) ) continue; // Spigot
- 
-             if (commandnode2.canUse(source)) {
--                ArgumentBuilder<SharedSuggestionProvider, ?> argumentbuilder = commandnode2.createBuilder();
-+                ArgumentBuilder argumentbuilder = commandnode2.createBuilder(); // CraftBukkit - decompile error
-+                // Paper start
-+                /*
-+                Because of how commands can be yeeted right left and center due to bad bukkit practices
-+                we need to be able to ensure that ALL commands are registered (even redirects).
- 
-+                What this will do is IF the redirect seems to be "dead" it will create a builder and essentially populate (flatten)
-+                all the children from the dead redirect to the node.
-+
-+                So, if minecraft:msg redirects to msg but the original msg node has been overriden minecraft:msg will now act as msg and will explicilty inherit its children.
-+
-+                The only way to fix this is to either:
-+                - Send EVERYTHING flattened, don't use redirects
-+                - Don't allow command nodes to be deleted
-+                - Do this :)
-+                 */
-+
-+                // Is there an invalid command redirect?
-+                if (argumentbuilder.getRedirect() != null && (CommandNode) resultNodes.get(argumentbuilder.getRedirect()) == null) {
-+                    // Create the argument builder with the same values as the specified node, but with a different literal and populated children
-+
-+                    CommandNode<CommandSourceStack> redirect = argumentbuilder.getRedirect();
-+                    // Diff copied from LiteralCommand#createBuilder
-+                    final com.mojang.brigadier.builder.LiteralArgumentBuilder<CommandSourceStack> builder = com.mojang.brigadier.builder.LiteralArgumentBuilder.literal(commandnode2.getName());
-+                    builder.requires(redirect.getRequirement());
-+                    // builder.forward(redirect.getRedirect(), redirect.getRedirectModifier(), redirect.isFork()); We don't want to migrate the forward, since it's invalid.
-+                    if (redirect.getCommand() != null) {
-+                        builder.executes(redirect.getCommand());
-+                    }
-+                    // Diff copied from LiteralCommand#createBuilder
-+                    for (CommandNode<CommandSourceStack> child : redirect.getChildren()) {
-+                        builder.then(child);
-+                    }
-+
-+                    argumentbuilder = builder;
-+                }
-+                // Paper end
-+
-                 argumentbuilder.requires((icompletionprovider) -> {
-                     return true;
-                 });
-                 if (argumentbuilder.getCommand() != null) {
--                    argumentbuilder.executes((commandcontext) -> {
--                        return 0;
-+                    // Paper start - fix suggestions due to falsely equal nodes
-+                    argumentbuilder.executes(new com.mojang.brigadier.Command<io.papermc.paper.command.brigadier.CommandSourceStack>() {
-+                        @Override
-+                        public int run(com.mojang.brigadier.context.CommandContext<io.papermc.paper.command.brigadier.CommandSourceStack> commandContext) throws CommandSyntaxException {
-+                            return 0;
-+                        }
-                     });
-+                    // Paper end
-                 }
- 
-                 if (argumentbuilder instanceof RequiredArgumentBuilder) {
-@@ -415,12 +608,12 @@
-                     argumentbuilder.redirect((CommandNode) resultNodes.get(argumentbuilder.getRedirect()));
-                 }
- 
--                CommandNode<SharedSuggestionProvider> commandnode3 = argumentbuilder.build();
-+                CommandNode commandnode3 = argumentbuilder.build(); // CraftBukkit - decompile error
- 
-                 resultNodes.put(commandnode2, commandnode3);
-                 result.addChild(commandnode3);
-                 if (!commandnode2.getChildren().isEmpty()) {
--                    this.fillUsableCommands(commandnode2, commandnode3, source, resultNodes);
-+                    this.fillUsableCommands(commandnode2.getChildren(), commandnode3, source, resultNodes); // Paper - Perf: Async command map building; pass children directly
-                 }
-             }
-         }
-@@ -481,7 +674,7 @@
-             }
- 
-             private <T> HolderLookup.RegistryLookup.Delegate<T> createLookup(final HolderLookup.RegistryLookup<T> original) {
--                return new HolderLookup.RegistryLookup.Delegate<T>(this) {
-+                return new HolderLookup.RegistryLookup.Delegate<T>() { // CraftBukkit - decompile error
-                     @Override
-                     public HolderLookup.RegistryLookup<T> parent() {
-                         return original;
diff --git a/paper-server/patches/unapplied/net/minecraft/commands/arguments/EntityArgument.java.patch b/paper-server/patches/unapplied/net/minecraft/commands/arguments/EntityArgument.java.patch
deleted file mode 100644
index 9c45cfda8f..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/commands/arguments/EntityArgument.java.patch
+++ /dev/null
@@ -1,52 +0,0 @@
---- a/net/minecraft/commands/arguments/EntityArgument.java
-+++ b/net/minecraft/commands/arguments/EntityArgument.java
-@@ -102,21 +102,27 @@
-     }
- 
-     private EntitySelector parse(StringReader reader, boolean allowAtSelectors) throws CommandSyntaxException {
-+        // CraftBukkit start
-+        return this.parse(reader, allowAtSelectors, false);
-+    }
-+
-+    public EntitySelector parse(StringReader stringreader, boolean flag, boolean overridePermissions) throws CommandSyntaxException {
-+        // CraftBukkit end
-         boolean flag1 = false;
--        EntitySelectorParser argumentparserselector = new EntitySelectorParser(reader, allowAtSelectors);
--        EntitySelector entityselector = argumentparserselector.parse();
-+        EntitySelectorParser argumentparserselector = new EntitySelectorParser(stringreader, flag);
-+        EntitySelector entityselector = argumentparserselector.parse(overridePermissions); // CraftBukkit
- 
-         if (entityselector.getMaxResults() > 1 && this.single) {
-             if (this.playersOnly) {
--                reader.setCursor(0);
--                throw EntityArgument.ERROR_NOT_SINGLE_PLAYER.createWithContext(reader);
-+                stringreader.setCursor(0);
-+                throw EntityArgument.ERROR_NOT_SINGLE_PLAYER.createWithContext(stringreader);
-             } else {
--                reader.setCursor(0);
--                throw EntityArgument.ERROR_NOT_SINGLE_ENTITY.createWithContext(reader);
-+                stringreader.setCursor(0);
-+                throw EntityArgument.ERROR_NOT_SINGLE_ENTITY.createWithContext(stringreader);
-             }
-         } else if (entityselector.includesEntities() && this.playersOnly && !entityselector.isSelfSelector()) {
--            reader.setCursor(0);
--            throw EntityArgument.ERROR_ONLY_PLAYERS_ALLOWED.createWithContext(reader);
-+            stringreader.setCursor(0);
-+            throw EntityArgument.ERROR_ONLY_PLAYERS_ALLOWED.createWithContext(stringreader);
-         } else {
-             return entityselector;
-         }
-@@ -129,7 +135,12 @@
-             StringReader stringreader = new StringReader(suggestionsbuilder.getInput());
- 
-             stringreader.setCursor(suggestionsbuilder.getStart());
--            EntitySelectorParser argumentparserselector = new EntitySelectorParser(stringreader, EntitySelectorParser.allowSelectors(icompletionprovider));
-+            // Paper start - Fix EntityArgument permissions
-+            final boolean permission = object instanceof CommandSourceStack stack
-+                    ? stack.bypassSelectorPermissions || stack.hasPermission(2, "minecraft.command.selector")
-+                    : icompletionprovider.hasPermission(2);
-+            EntitySelectorParser argumentparserselector = new EntitySelectorParser(stringreader, permission);
-+            // Paper end - Fix EntityArgument permissions
- 
-             try {
-                 argumentparserselector.parse();
diff --git a/paper-server/patches/unapplied/net/minecraft/commands/arguments/blocks/BlockStateParser.java.patch b/paper-server/patches/unapplied/net/minecraft/commands/arguments/blocks/BlockStateParser.java.patch
deleted file mode 100644
index 46961c4f64..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/commands/arguments/blocks/BlockStateParser.java.patch
+++ /dev/null
@@ -1,38 +0,0 @@
---- a/net/minecraft/commands/arguments/blocks/BlockStateParser.java
-+++ b/net/minecraft/commands/arguments/blocks/BlockStateParser.java
-@@ -67,7 +67,7 @@
-     private final StringReader reader;
-     private final boolean forTesting;
-     private final boolean allowNbt;
--    private final Map<Property<?>, Comparable<?>> properties = Maps.newHashMap();
-+    private final Map<Property<?>, Comparable<?>> properties = Maps.newLinkedHashMap(); // CraftBukkit - stable
-     private final Map<String, String> vagueProperties = Maps.newHashMap();
-     private ResourceLocation id = ResourceLocation.withDefaultNamespace("");
-     @Nullable
-@@ -275,7 +275,7 @@
-         Iterator iterator = property.getPossibleValues().iterator();
- 
-         while (iterator.hasNext()) {
--            T t0 = (Comparable) iterator.next();
-+            T t0 = (T) iterator.next(); // CraftBukkit - decompile error
- 
-             if (t0 instanceof Integer integer) {
-                 builder.suggest(integer);
-@@ -545,7 +545,7 @@
-         Optional<T> optional = property.getValue(value);
- 
-         if (optional.isPresent()) {
--            this.state = (BlockState) this.state.setValue(property, (Comparable) optional.get());
-+            this.state = (BlockState) this.state.setValue(property, (T) optional.get()); // CraftBukkit - decompile error
-             this.properties.put(property, (Comparable) optional.get());
-         } else {
-             this.reader.setCursor(cursor);
-@@ -581,7 +581,7 @@
-     private static <T extends Comparable<T>> void appendProperty(StringBuilder builder, Property<T> property, Comparable<?> value) {
-         builder.append(property.getName());
-         builder.append('=');
--        builder.append(property.getName(value));
-+        builder.append(property.getName((T) value)); // CraftBukkit - decompile error
-     }
- 
-     public static record BlockResult(BlockState blockState, Map<Property<?>, Comparable<?>> properties, @Nullable CompoundTag nbt) {
diff --git a/paper-server/patches/unapplied/net/minecraft/commands/arguments/item/ItemInput.java.patch b/paper-server/patches/unapplied/net/minecraft/commands/arguments/item/ItemInput.java.patch
deleted file mode 100644
index a20c412208..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/commands/arguments/item/ItemInput.java.patch
+++ /dev/null
@@ -1,10 +0,0 @@
---- a/net/minecraft/commands/arguments/item/ItemInput.java
-+++ b/net/minecraft/commands/arguments/item/ItemInput.java
-@@ -78,6 +78,6 @@
-     }
- 
-     private String getItemName() {
--        return this.item.unwrapKey().map(ResourceKey::location).orElseGet(() -> "unknown[" + this.item + "]").toString();
-+        return this.item.unwrapKey().<Object>map(ResourceKey::location).orElseGet(() -> "unknown[" + this.item + "]").toString(); // Paper - decompile fix
-     }
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/core/Holder.java.patch b/paper-server/patches/unapplied/net/minecraft/core/Holder.java.patch
deleted file mode 100644
index 634a2b2190..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/core/Holder.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/core/Holder.java
-+++ b/net/minecraft/core/Holder.java
-@@ -230,7 +230,7 @@
-         }
- 
-         void bindTags(Collection<TagKey<T>> tags) {
--            this.tags = Set.copyOf(tags);
-+            this.tags = java.util.Collections.unmodifiableSet(new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(tags)); // Paper
-         }
- 
-         @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/core/cauldron/CauldronInteraction.java.patch b/paper-server/patches/unapplied/net/minecraft/core/cauldron/CauldronInteraction.java.patch
deleted file mode 100644
index f0db760867..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/core/cauldron/CauldronInteraction.java.patch
+++ /dev/null
@@ -1,330 +0,0 @@
---- a/net/minecraft/core/cauldron/CauldronInteraction.java
-+++ b/net/minecraft/core/cauldron/CauldronInteraction.java
-@@ -35,20 +35,25 @@
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.level.material.FluidState;
-+// CraftBukkit start
-+import org.bukkit.event.block.CauldronLevelChangeEvent;
-+// CraftBukkit end
- 
- public interface CauldronInteraction {
- 
-     Map<String, CauldronInteraction.InteractionMap> INTERACTIONS = new Object2ObjectArrayMap();
--    Codec<CauldronInteraction.InteractionMap> CODEC;
--    CauldronInteraction.InteractionMap EMPTY;
--    CauldronInteraction.InteractionMap WATER;
--    CauldronInteraction.InteractionMap LAVA;
--    CauldronInteraction.InteractionMap POWDER_SNOW;
-+    // CraftBukkit start - decompile errors
-+    Codec<CauldronInteraction.InteractionMap> CODEC = Codec.stringResolver(CauldronInteraction.InteractionMap::name, CauldronInteraction.INTERACTIONS::get);
-+    CauldronInteraction.InteractionMap EMPTY = CauldronInteraction.newInteractionMap("empty");
-+    CauldronInteraction.InteractionMap WATER = CauldronInteraction.newInteractionMap("water");
-+    CauldronInteraction.InteractionMap LAVA = CauldronInteraction.newInteractionMap("lava");
-+    CauldronInteraction.InteractionMap POWDER_SNOW = CauldronInteraction.newInteractionMap("powder_snow");
-+    // CraftBukkit end
- 
-     static CauldronInteraction.InteractionMap newInteractionMap(String name) {
-         Object2ObjectOpenHashMap<Item, CauldronInteraction> object2objectopenhashmap = new Object2ObjectOpenHashMap();
- 
--        object2objectopenhashmap.defaultReturnValue((iblockdata, world, blockposition, entityhuman, enumhand, itemstack) -> {
-+        object2objectopenhashmap.defaultReturnValue((iblockdata, world, blockposition, entityhuman, enumhand, itemstack, hitDirection) -> { // Paper - add hitDirection
-             return InteractionResult.TRY_WITH_EMPTY_HAND;
-         });
-         CauldronInteraction.InteractionMap cauldroninteraction_a = new CauldronInteraction.InteractionMap(name, object2objectopenhashmap);
-@@ -57,23 +62,28 @@
-         return cauldroninteraction_a;
-     }
- 
--    InteractionResult interact(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack);
-+    InteractionResult interact(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, final net.minecraft.core.Direction hitDirection); // Paper - add hitDirection
- 
-     static void bootStrap() {
-         Map<Item, CauldronInteraction> map = CauldronInteraction.EMPTY.map();
- 
-         CauldronInteraction.addDefaultInteractions(map);
--        map.put(Items.POTION, (iblockdata, world, blockposition, entityhuman, enumhand, itemstack) -> {
-+        map.put(Items.POTION, (iblockdata, world, blockposition, entityhuman, enumhand, itemstack, hitDirection) -> { // Paper - add hitDirection
-             PotionContents potioncontents = (PotionContents) itemstack.get(DataComponents.POTION_CONTENTS);
- 
-             if (potioncontents != null && potioncontents.is(Potions.WATER)) {
-                 if (!world.isClientSide) {
-+                    // CraftBukkit start
-+                    if (!LayeredCauldronBlock.changeLevel(iblockdata, world, blockposition, Blocks.WATER_CAULDRON.defaultBlockState(), entityhuman, CauldronLevelChangeEvent.ChangeReason.BOTTLE_EMPTY, false)) { // Paper - Call CauldronLevelChangeEvent
-+                        return InteractionResult.SUCCESS;
-+                    }
-+                    // CraftBukkit end
-                     Item item = itemstack.getItem();
- 
-                     entityhuman.setItemInHand(enumhand, ItemUtils.createFilledResult(itemstack, entityhuman, new ItemStack(Items.GLASS_BOTTLE)));
-                     entityhuman.awardStat(Stats.USE_CAULDRON);
-                     entityhuman.awardStat(Stats.ITEM_USED.get(item));
--                    world.setBlockAndUpdate(blockposition, Blocks.WATER_CAULDRON.defaultBlockState());
-+                    // world.setBlockAndUpdate(blockposition, Blocks.WATER_CAULDRON.defaultBlockState()); // CraftBukkit
-                     world.playSound((Player) null, blockposition, SoundEvents.BOTTLE_EMPTY, SoundSource.BLOCKS, 1.0F, 1.0F);
-                     world.gameEvent((Entity) null, (Holder) GameEvent.FLUID_PLACE, blockposition);
-                 }
-@@ -86,26 +96,31 @@
-         Map<Item, CauldronInteraction> map1 = CauldronInteraction.WATER.map();
- 
-         CauldronInteraction.addDefaultInteractions(map1);
--        map1.put(Items.BUCKET, (iblockdata, world, blockposition, entityhuman, enumhand, itemstack) -> {
-+        map1.put(Items.BUCKET, (iblockdata, world, blockposition, entityhuman, enumhand, itemstack, hitDirection) -> { // Paper - add hitDirection
-             return CauldronInteraction.fillBucket(iblockdata, world, blockposition, entityhuman, enumhand, itemstack, new ItemStack(Items.WATER_BUCKET), (iblockdata1) -> {
-                 return (Integer) iblockdata1.getValue(LayeredCauldronBlock.LEVEL) == 3;
--            }, SoundEvents.BUCKET_FILL);
-+            }, SoundEvents.BUCKET_FILL, hitDirection); // Paper - add hitDirection
-         });
--        map1.put(Items.GLASS_BOTTLE, (iblockdata, world, blockposition, entityhuman, enumhand, itemstack) -> {
-+        map1.put(Items.GLASS_BOTTLE, (iblockdata, world, blockposition, entityhuman, enumhand, itemstack, hitDirection) -> { // Paper - add hitDirection
-             if (!world.isClientSide) {
-+                // CraftBukkit start
-+                if (!LayeredCauldronBlock.lowerFillLevel(iblockdata, world, blockposition, entityhuman, CauldronLevelChangeEvent.ChangeReason.BOTTLE_FILL)) {
-+                    return InteractionResult.SUCCESS;
-+                }
-+                // CraftBukkit end
-                 Item item = itemstack.getItem();
- 
-                 entityhuman.setItemInHand(enumhand, ItemUtils.createFilledResult(itemstack, entityhuman, PotionContents.createItemStack(Items.POTION, Potions.WATER)));
-                 entityhuman.awardStat(Stats.USE_CAULDRON);
-                 entityhuman.awardStat(Stats.ITEM_USED.get(item));
--                LayeredCauldronBlock.lowerFillLevel(iblockdata, world, blockposition);
-+                // LayeredCauldronBlock.lowerFillLevel(iblockdata, world, blockposition); // CraftBukkit
-                 world.playSound((Player) null, blockposition, SoundEvents.BOTTLE_FILL, SoundSource.BLOCKS, 1.0F, 1.0F);
-                 world.gameEvent((Entity) null, (Holder) GameEvent.FLUID_PICKUP, blockposition);
-             }
- 
-             return InteractionResult.SUCCESS;
-         });
--        map1.put(Items.POTION, (iblockdata, world, blockposition, entityhuman, enumhand, itemstack) -> {
-+        map1.put(Items.POTION, (iblockdata, world, blockposition, entityhuman, enumhand, itemstack, hitDirection) -> { // Paper - add hitDirection
-             if ((Integer) iblockdata.getValue(LayeredCauldronBlock.LEVEL) == 3) {
-                 return InteractionResult.TRY_WITH_EMPTY_HAND;
-             } else {
-@@ -113,10 +128,15 @@
- 
-                 if (potioncontents != null && potioncontents.is(Potions.WATER)) {
-                     if (!world.isClientSide) {
-+                        // CraftBukkit start
-+                        if (!LayeredCauldronBlock.changeLevel(iblockdata, world, blockposition, iblockdata.cycle(LayeredCauldronBlock.LEVEL), entityhuman, CauldronLevelChangeEvent.ChangeReason.BOTTLE_EMPTY, false)) { // Paper - Call CauldronLevelChangeEvent
-+                            return InteractionResult.SUCCESS;
-+                        }
-+                        // CraftBukkit end
-                         entityhuman.setItemInHand(enumhand, ItemUtils.createFilledResult(itemstack, entityhuman, new ItemStack(Items.GLASS_BOTTLE)));
-                         entityhuman.awardStat(Stats.USE_CAULDRON);
-                         entityhuman.awardStat(Stats.ITEM_USED.get(itemstack.getItem()));
--                        world.setBlockAndUpdate(blockposition, (BlockState) iblockdata.cycle(LayeredCauldronBlock.LEVEL));
-+                        // world.setBlockAndUpdate(blockposition, (IBlockData) iblockdata.cycle(LayeredCauldronBlock.LEVEL)); // CraftBukkit
-                         world.playSound((Player) null, blockposition, SoundEvents.BOTTLE_EMPTY, SoundSource.BLOCKS, 1.0F, 1.0F);
-                         world.gameEvent((Entity) null, (Holder) GameEvent.FLUID_PLACE, blockposition);
-                     }
-@@ -167,18 +187,18 @@
-         map1.put(Items.YELLOW_SHULKER_BOX, CauldronInteraction::shulkerBoxInteraction);
-         Map<Item, CauldronInteraction> map2 = CauldronInteraction.LAVA.map();
- 
--        map2.put(Items.BUCKET, (iblockdata, world, blockposition, entityhuman, enumhand, itemstack) -> {
-+        map2.put(Items.BUCKET, (iblockdata, world, blockposition, entityhuman, enumhand, itemstack, hitDirection) -> { // Paper - add hitDirection
-             return CauldronInteraction.fillBucket(iblockdata, world, blockposition, entityhuman, enumhand, itemstack, new ItemStack(Items.LAVA_BUCKET), (iblockdata1) -> {
-                 return true;
--            }, SoundEvents.BUCKET_FILL_LAVA);
-+            }, SoundEvents.BUCKET_FILL_LAVA, hitDirection); // Paper - add hitDirection
-         });
-         CauldronInteraction.addDefaultInteractions(map2);
-         Map<Item, CauldronInteraction> map3 = CauldronInteraction.POWDER_SNOW.map();
- 
--        map3.put(Items.BUCKET, (iblockdata, world, blockposition, entityhuman, enumhand, itemstack) -> {
-+        map3.put(Items.BUCKET, (iblockdata, world, blockposition, entityhuman, enumhand, itemstack, hitDirection) -> { // Paper - add hitDirection
-             return CauldronInteraction.fillBucket(iblockdata, world, blockposition, entityhuman, enumhand, itemstack, new ItemStack(Items.POWDER_SNOW_BUCKET), (iblockdata1) -> {
-                 return (Integer) iblockdata1.getValue(LayeredCauldronBlock.LEVEL) == 3;
--            }, SoundEvents.BUCKET_FILL_POWDER_SNOW);
-+            }, SoundEvents.BUCKET_FILL_POWDER_SNOW, hitDirection); // Paper - add hitDirection
-         });
-         CauldronInteraction.addDefaultInteractions(map3);
-     }
-@@ -190,16 +210,35 @@
-     }
- 
-     static InteractionResult fillBucket(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, ItemStack output, Predicate<BlockState> fullPredicate, SoundEvent soundEvent) {
-+        // Paper start - add hitDirection
-+        return fillBucket(state, world, pos, player, hand, stack, output, fullPredicate, soundEvent, null); // Paper - add hitDirection
-+    }
-+    static InteractionResult fillBucket(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, ItemStack output, Predicate<BlockState> fullPredicate, SoundEvent soundEvent, @javax.annotation.Nullable net.minecraft.core.Direction hitDirection) {
-+        // Paper end - add hitDirection
-         if (!fullPredicate.test(state)) {
-             return InteractionResult.TRY_WITH_EMPTY_HAND;
-         } else {
-             if (!world.isClientSide) {
-+                // Paper start - fire PlayerBucketFillEvent
-+                if (hitDirection != null) {
-+                    org.bukkit.event.player.PlayerBucketEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerBucketFillEvent((net.minecraft.server.level.ServerLevel) world, player, pos, pos, hitDirection, stack, output.getItem(), hand);
-+                    if (event.isCancelled()) {
-+                        return InteractionResult.PASS;
-+                    }
-+                    output = event.getItemStack() != null ? org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItemStack()) : ItemStack.EMPTY;
-+                }
-+                // Paper end - fire PlayerBucketFillEvent
-+                // CraftBukkit start
-+                if (!LayeredCauldronBlock.changeLevel(state, world, pos, Blocks.CAULDRON.defaultBlockState(), player, CauldronLevelChangeEvent.ChangeReason.BUCKET_FILL, false)) { // Paper - Call CauldronLevelChangeEvent
-+                    return InteractionResult.SUCCESS;
-+                }
-+                // CraftBukkit end
-                 Item item = stack.getItem();
- 
-                 player.setItemInHand(hand, ItemUtils.createFilledResult(stack, player, output));
-                 player.awardStat(Stats.USE_CAULDRON);
-                 player.awardStat(Stats.ITEM_USED.get(item));
--                world.setBlockAndUpdate(pos, Blocks.CAULDRON.defaultBlockState());
-+                // world.setBlockAndUpdate(blockposition, Blocks.CAULDRON.defaultBlockState()); // CraftBukkit
-                 world.playSound((Player) null, pos, soundEvent, SoundSource.BLOCKS, 1.0F, 1.0F);
-                 world.gameEvent((Entity) null, (Holder) GameEvent.FLUID_PICKUP, pos);
-             }
-@@ -209,13 +248,33 @@
-     }
- 
-     static InteractionResult emptyBucket(Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, BlockState state, SoundEvent soundEvent) {
-+        // Paper start - add hitDirection
-+        return emptyBucket(world, pos, player, hand, stack, state, soundEvent, null);
-+    }
-+    static InteractionResult emptyBucket(Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, BlockState state, SoundEvent soundEvent, @javax.annotation.Nullable net.minecraft.core.Direction hitDirection) {
-+        // Paper end - add hitDirection
-         if (!world.isClientSide) {
-+            // Paper start - fire PlayerBucketEmptyEvent
-+            ItemStack output = new ItemStack(Items.BUCKET);
-+            if (hitDirection != null) {
-+                org.bukkit.event.player.PlayerBucketEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerBucketEmptyEvent((net.minecraft.server.level.ServerLevel) world, player, pos, pos, hitDirection, stack, hand);
-+                if (event.isCancelled()) {
-+                    return InteractionResult.PASS;
-+                }
-+                output = event.getItemStack() != null ? org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItemStack()) : ItemStack.EMPTY;
-+            }
-+            // Paper end - fire PlayerBucketEmptyEvent
-+            // CraftBukkit start
-+            if (!LayeredCauldronBlock.changeLevel(state, world, pos, state, player, CauldronLevelChangeEvent.ChangeReason.BUCKET_EMPTY, false)) { // Paper - Call CauldronLevelChangeEvent
-+                return InteractionResult.SUCCESS;
-+            }
-+            // CraftBukkit end
-             Item item = stack.getItem();
- 
--            player.setItemInHand(hand, ItemUtils.createFilledResult(stack, player, new ItemStack(Items.BUCKET)));
-+            player.setItemInHand(hand, ItemUtils.createFilledResult(stack, player, output)); // Paper
-             player.awardStat(Stats.FILL_CAULDRON);
-             player.awardStat(Stats.ITEM_USED.get(item));
--            world.setBlockAndUpdate(pos, state);
-+            // world.setBlockAndUpdate(blockposition, iblockdata); // CraftBukkit
-             world.playSound((Player) null, pos, soundEvent, SoundSource.BLOCKS, 1.0F, 1.0F);
-             world.gameEvent((Entity) null, (Holder) GameEvent.FLUID_PLACE, pos);
-         }
-@@ -223,65 +282,79 @@
-         return InteractionResult.SUCCESS;
-     }
- 
--    private static InteractionResult fillWaterInteraction(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack) {
--        return CauldronInteraction.emptyBucket(world, pos, player, hand, stack, (BlockState) Blocks.WATER_CAULDRON.defaultBlockState().setValue(LayeredCauldronBlock.LEVEL, 3), SoundEvents.BUCKET_EMPTY);
-+    private static InteractionResult fillWaterInteraction(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, final net.minecraft.core.Direction hitDirection) { // Paper - add hitDirection
-+        return CauldronInteraction.emptyBucket(world, pos, player, hand, stack, (BlockState) Blocks.WATER_CAULDRON.defaultBlockState().setValue(LayeredCauldronBlock.LEVEL, 3), SoundEvents.BUCKET_EMPTY, hitDirection); // Paper - add hitDirection
-     }
- 
--    private static InteractionResult fillLavaInteraction(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack) {
--        return (InteractionResult) (CauldronInteraction.isUnderWater(world, pos) ? InteractionResult.CONSUME : CauldronInteraction.emptyBucket(world, pos, player, hand, stack, Blocks.LAVA_CAULDRON.defaultBlockState(), SoundEvents.BUCKET_EMPTY_LAVA));
-+    private static InteractionResult fillLavaInteraction(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, final net.minecraft.core.Direction hitDirection) { // Paper - add hitDirection
-+        return (InteractionResult) (CauldronInteraction.isUnderWater(world, pos) ? InteractionResult.CONSUME : CauldronInteraction.emptyBucket(world, pos, player, hand, stack, Blocks.LAVA_CAULDRON.defaultBlockState(), SoundEvents.BUCKET_EMPTY_LAVA, hitDirection)); // Paper - add hitDirection
-     }
- 
--    private static InteractionResult fillPowderSnowInteraction(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack) {
--        return (InteractionResult) (CauldronInteraction.isUnderWater(world, pos) ? InteractionResult.CONSUME : CauldronInteraction.emptyBucket(world, pos, player, hand, stack, (BlockState) Blocks.POWDER_SNOW_CAULDRON.defaultBlockState().setValue(LayeredCauldronBlock.LEVEL, 3), SoundEvents.BUCKET_EMPTY_POWDER_SNOW));
-+    private static InteractionResult fillPowderSnowInteraction(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, final net.minecraft.core.Direction hitDirection) { // Paper - add hitDirection
-+        return (InteractionResult) (CauldronInteraction.isUnderWater(world, pos) ? InteractionResult.CONSUME : CauldronInteraction.emptyBucket(world, pos, player, hand, stack, (BlockState) Blocks.POWDER_SNOW_CAULDRON.defaultBlockState().setValue(LayeredCauldronBlock.LEVEL, 3), SoundEvents.BUCKET_EMPTY_POWDER_SNOW, hitDirection)); // Paper - add hitDirection
-     }
- 
--    private static InteractionResult shulkerBoxInteraction(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack) {
-+    private static InteractionResult shulkerBoxInteraction(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, final net.minecraft.core.Direction hitDirection) { // Paper - add hitDirection
-         Block block = Block.byItem(stack.getItem());
- 
-         if (!(block instanceof ShulkerBoxBlock)) {
-             return InteractionResult.TRY_WITH_EMPTY_HAND;
-         } else {
-             if (!world.isClientSide) {
-+                // CraftBukkit start
-+                if (!LayeredCauldronBlock.lowerFillLevel(state, world, pos, player, CauldronLevelChangeEvent.ChangeReason.SHULKER_WASH)) {
-+                    return InteractionResult.SUCCESS;
-+                }
-+                // CraftBukkit end
-                 ItemStack itemstack1 = stack.transmuteCopy(Blocks.SHULKER_BOX, 1);
- 
-                 player.setItemInHand(hand, ItemUtils.createFilledResult(stack, player, itemstack1, false));
-                 player.awardStat(Stats.CLEAN_SHULKER_BOX);
--                LayeredCauldronBlock.lowerFillLevel(state, world, pos);
-+                // LayeredCauldronBlock.lowerFillLevel(iblockdata, world, blockposition); // CraftBukkit
-             }
--
-             return InteractionResult.SUCCESS;
-         }
-     }
- 
--    private static InteractionResult bannerInteraction(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack) {
-+    private static InteractionResult bannerInteraction(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, final net.minecraft.core.Direction hitDirection) { // Paper - add hitDirection
-         BannerPatternLayers bannerpatternlayers = (BannerPatternLayers) stack.getOrDefault(DataComponents.BANNER_PATTERNS, BannerPatternLayers.EMPTY);
- 
-         if (bannerpatternlayers.layers().isEmpty()) {
-             return InteractionResult.TRY_WITH_EMPTY_HAND;
-         } else {
-             if (!world.isClientSide) {
-+                // CraftBukkit start
-+                if (!LayeredCauldronBlock.lowerFillLevel(state, world, pos, player, CauldronLevelChangeEvent.ChangeReason.BANNER_WASH)) {
-+                    return InteractionResult.SUCCESS;
-+                }
-+                // CraftBukkit end
-                 ItemStack itemstack1 = stack.copyWithCount(1);
- 
-                 itemstack1.set(DataComponents.BANNER_PATTERNS, bannerpatternlayers.removeLast());
-                 player.setItemInHand(hand, ItemUtils.createFilledResult(stack, player, itemstack1, false));
-                 player.awardStat(Stats.CLEAN_BANNER);
--                LayeredCauldronBlock.lowerFillLevel(state, world, pos);
-+                // LayeredCauldronBlock.lowerFillLevel(iblockdata, world, blockposition); // CraftBukkit
-             }
- 
-             return InteractionResult.SUCCESS;
-         }
-     }
- 
--    private static InteractionResult dyedItemIteration(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack) {
-+    private static InteractionResult dyedItemIteration(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, ItemStack stack, final net.minecraft.core.Direction hitDirection) { // Paper - add hitDirection
-         if (!stack.is(ItemTags.DYEABLE)) {
-             return InteractionResult.TRY_WITH_EMPTY_HAND;
-         } else if (!stack.has(DataComponents.DYED_COLOR)) {
-             return InteractionResult.TRY_WITH_EMPTY_HAND;
-         } else {
-             if (!world.isClientSide) {
-+                // CraftBukkit start
-+                if (!LayeredCauldronBlock.lowerFillLevel(state, world, pos, player, CauldronLevelChangeEvent.ChangeReason.ARMOR_WASH)) {
-+                    return InteractionResult.SUCCESS;
-+                }
-+                // CraftBukkit end
-                 stack.remove(DataComponents.DYED_COLOR);
-                 player.awardStat(Stats.CLEAN_ARMOR);
--                LayeredCauldronBlock.lowerFillLevel(state, world, pos);
-+                // LayeredCauldronBlock.lowerFillLevel(iblockdata, world, blockposition); // CraftBukkit
-             }
- 
-             return InteractionResult.SUCCESS;
-@@ -294,8 +367,10 @@
-         return fluid.is(FluidTags.WATER);
-     }
- 
-+    // CraftBukkit start - decompile errors
-+    /*
-     static {
--        Function function = CauldronInteraction.InteractionMap::name;
-+        Function function = CauldronInteraction.a::name;
-         Map map = CauldronInteraction.INTERACTIONS;
- 
-         Objects.requireNonNull(map);
-@@ -305,6 +380,8 @@
-         LAVA = newInteractionMap("lava");
-         POWDER_SNOW = newInteractionMap("powder_snow");
-     }
-+     */
-+    // CraftBukkit end
- 
-     public static record InteractionMap(String name, Map<Item, CauldronInteraction> map) {
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java.patch b/paper-server/patches/unapplied/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java.patch
deleted file mode 100644
index a97acf2799..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java.patch
+++ /dev/null
@@ -1,58 +0,0 @@
---- a/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java
-+++ b/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java
-@@ -11,6 +11,11 @@
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.level.block.DispenserBlock;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.event.block.BlockDispenseEvent;
-+// CraftBukkit end
- 
- public class BoatDispenseItemBehavior extends DefaultDispenseItemBehavior {
- 
-@@ -43,14 +48,40 @@
-             d4 = 0.0D;
-         }
- 
-+        // CraftBukkit start
-+        ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink at end and single item in event
-+        org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos());
-+        CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
-+
-+        BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(d1, d2 + d4, d3));
-+        if (!DispenserBlock.eventFired) {
-+            worldserver.getCraftServer().getPluginManager().callEvent(event);
-+        }
-+
-+        if (event.isCancelled()) {
-+            // stack.grow(1); // Paper - shrink below
-+            return stack;
-+        }
-+
-+        boolean shrink = true; // Paper
-+        if (!event.getItem().equals(craftItem)) {
-+            shrink = false; // Paper - shrink below
-+            // Chain to handler for new item
-+            ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
-+            DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
-+            if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
-+                idispensebehavior.dispense(pointer, eventStack);
-+                return stack;
-+            }
-+        }
-+        // CraftBukkit end
-         AbstractBoat abstractboat = (AbstractBoat) this.type.create(worldserver, EntitySpawnReason.DISPENSER);
- 
-         if (abstractboat != null) {
--            abstractboat.setInitialPos(d1, d2 + d4, d3);
-+            abstractboat.setInitialPos(event.getVelocity().getX(), event.getVelocity().getY(), event.getVelocity().getZ()); // CraftBukkit
-             EntityType.createDefaultStackConfig(worldserver, stack, (Player) null).accept(abstractboat);
-             abstractboat.setYRot(enumdirection.toYRot());
--            worldserver.addFreshEntity(abstractboat);
--            stack.shrink(1);
-+            if (worldserver.addFreshEntity(abstractboat) && shrink) stack.shrink(1); // Paper - if entity add was successful and supposed to shrink
-         }
- 
-         return stack;
diff --git a/paper-server/patches/unapplied/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java.patch b/paper-server/patches/unapplied/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java.patch
deleted file mode 100644
index 9a3359fc85..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java.patch
+++ /dev/null
@@ -1,126 +0,0 @@
---- a/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java
-+++ b/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java
-@@ -6,47 +6,114 @@
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.block.DispenserBlock;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.craftbukkit.util.CraftVector;
-+import org.bukkit.event.block.BlockDispenseEvent;
-+// CraftBukkit end
- 
- public class DefaultDispenseItemBehavior implements DispenseItemBehavior {
-+    private Direction enumdirection; // Paper - cache facing direction
- 
-     private static final int DEFAULT_ACCURACY = 6;
- 
-+    // CraftBukkit start
-+    private boolean dropper;
-+
-+    public DefaultDispenseItemBehavior(boolean dropper) {
-+        this.dropper = dropper;
-+    }
-+    // CraftBukkit end
-+
-     public DefaultDispenseItemBehavior() {}
- 
-     @Override
-     public final ItemStack dispense(BlockSource pointer, ItemStack stack) {
-+        enumdirection = pointer.state().getValue(DispenserBlock.FACING); // Paper - cache facing direction
-         ItemStack itemstack1 = this.execute(pointer, stack);
- 
-         this.playSound(pointer);
--        this.playAnimation(pointer, (Direction) pointer.state().getValue(DispenserBlock.FACING));
-+        this.playAnimation(pointer, enumdirection); // Paper - cache facing direction
-         return itemstack1;
-     }
- 
-     protected ItemStack execute(BlockSource pointer, ItemStack stack) {
--        Direction enumdirection = (Direction) pointer.state().getValue(DispenserBlock.FACING);
-+        // Paper - cached enum direction
-         Position iposition = DispenserBlock.getDispensePosition(pointer);
-         ItemStack itemstack1 = stack.split(1);
- 
--        DefaultDispenseItemBehavior.spawnItem(pointer.level(), itemstack1, 6, enumdirection, iposition);
-+        // CraftBukkit start
-+        if (!DefaultDispenseItemBehavior.spawnItem(pointer.level(), itemstack1, 6, enumdirection, pointer, this.dropper)) {
-+            stack.grow(1);
-+        }
-+        // CraftBukkit end
-         return stack;
-     }
- 
-     public static void spawnItem(Level world, ItemStack stack, int speed, Direction side, Position pos) {
--        double d0 = pos.x();
--        double d1 = pos.y();
--        double d2 = pos.z();
-+        // CraftBukkit start
-+        ItemEntity entityitem = DefaultDispenseItemBehavior.prepareItem(world, stack, speed, side, pos);
-+        world.addFreshEntity(entityitem);
-+    }
- 
--        if (side.getAxis() == Direction.Axis.Y) {
-+    private static ItemEntity prepareItem(Level world, ItemStack itemstack, int i, Direction enumdirection, Position iposition) {
-+        // CraftBukkit end
-+        double d0 = iposition.x();
-+        double d1 = iposition.y();
-+        double d2 = iposition.z();
-+
-+        if (enumdirection.getAxis() == Direction.Axis.Y) {
-             d1 -= 0.125D;
-         } else {
-             d1 -= 0.15625D;
-         }
- 
--        ItemEntity entityitem = new ItemEntity(world, d0, d1, d2, stack);
-+        ItemEntity entityitem = new ItemEntity(world, d0, d1, d2, itemstack);
-         double d3 = world.random.nextDouble() * 0.1D + 0.2D;
- 
--        entityitem.setDeltaMovement(world.random.triangle((double) side.getStepX() * d3, 0.0172275D * (double) speed), world.random.triangle(0.2D, 0.0172275D * (double) speed), world.random.triangle((double) side.getStepZ() * d3, 0.0172275D * (double) speed));
-+        entityitem.setDeltaMovement(world.random.triangle((double) enumdirection.getStepX() * d3, 0.0172275D * (double) i), world.random.triangle(0.2D, 0.0172275D * (double) i), world.random.triangle((double) enumdirection.getStepZ() * d3, 0.0172275D * (double) i));
-+        // CraftBukkit start
-+        return entityitem;
-+    }
-+
-+    // CraftBukkit - void -> boolean return, IPosition -> ISourceBlock last argument, dropper
-+    public static boolean spawnItem(Level world, ItemStack itemstack, int i, Direction enumdirection, BlockSource sourceblock, boolean dropper) {
-+        if (itemstack.isEmpty()) return true;
-+        Position iposition = DispenserBlock.getDispensePosition(sourceblock);
-+        ItemEntity entityitem = DefaultDispenseItemBehavior.prepareItem(world, itemstack, i, enumdirection, iposition);
-+
-+        org.bukkit.block.Block block = CraftBlock.at(world, sourceblock.pos());
-+        CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack);
-+
-+        BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), CraftVector.toBukkit(entityitem.getDeltaMovement()));
-+        if (!DispenserBlock.eventFired) {
-+            world.getCraftServer().getPluginManager().callEvent(event);
-+        }
-+
-+        if (event.isCancelled()) {
-+            return false;
-+        }
-+
-+        entityitem.setItem(CraftItemStack.asNMSCopy(event.getItem()));
-+        entityitem.setDeltaMovement(CraftVector.toNMS(event.getVelocity()));
-+
-+        if (!dropper && !event.getItem().getType().equals(craftItem.getType())) {
-+            // Chain to handler for new item
-+            ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
-+            DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(sourceblock, eventStack); // Paper - Fix NPE with equippable and items without behavior
-+            if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior.getClass() != DefaultDispenseItemBehavior.class) {
-+                idispensebehavior.dispense(sourceblock, eventStack);
-+            } else {
-+                world.addFreshEntity(entityitem);
-+            }
-+            return false;
-+        }
-+
-         world.addFreshEntity(entityitem);
-+
-+        return true;
-+        // CraftBukkit end
-     }
- 
-     protected void playSound(BlockSource pointer) {
diff --git a/paper-server/patches/unapplied/net/minecraft/core/dispenser/DispenseItemBehavior.java.patch b/paper-server/patches/unapplied/net/minecraft/core/dispenser/DispenseItemBehavior.java.patch
deleted file mode 100644
index 824b11e0d1..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/core/dispenser/DispenseItemBehavior.java.patch
+++ /dev/null
@@ -1,651 +0,0 @@
---- a/net/minecraft/core/dispenser/DispenseItemBehavior.java
-+++ b/net/minecraft/core/dispenser/DispenseItemBehavior.java
-@@ -28,6 +28,7 @@
- import net.minecraft.world.entity.item.PrimedTnt;
- import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.item.BoneMealItem;
-+import net.minecraft.world.item.BucketItem;
- import net.minecraft.world.item.DispensibleContainerItem;
- import net.minecraft.world.item.DyeColor;
- import net.minecraft.world.item.HoneycombItem;
-@@ -47,7 +48,9 @@
- import net.minecraft.world.level.block.CandleCakeBlock;
- import net.minecraft.world.level.block.CarvedPumpkinBlock;
- import net.minecraft.world.level.block.DispenserBlock;
-+import net.minecraft.world.level.block.LiquidBlockContainer;
- import net.minecraft.world.level.block.RespawnAnchorBlock;
-+import net.minecraft.world.level.block.SaplingBlock;
- import net.minecraft.world.level.block.ShulkerBoxBlock;
- import net.minecraft.world.level.block.SkullBlock;
- import net.minecraft.world.level.block.TntBlock;
-@@ -62,6 +65,17 @@
- import net.minecraft.world.phys.AABB;
- import net.minecraft.world.phys.BlockHitResult;
- import org.slf4j.Logger;
-+import org.bukkit.Location;
-+import org.bukkit.TreeType;
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.craftbukkit.util.CraftLocation;
-+import org.bukkit.craftbukkit.util.DummyGeneratorAccess;
-+import org.bukkit.event.block.BlockDispenseArmorEvent;
-+import org.bukkit.event.block.BlockDispenseEvent;
-+import org.bukkit.event.block.BlockFertilizeEvent;
-+import org.bukkit.event.world.StructureGrowEvent;
-+// CraftBukkit end
- 
- public interface DispenseItemBehavior {
- 
-@@ -90,14 +104,47 @@
-                 Direction enumdirection = (Direction) pointer.state().getValue(DispenserBlock.FACING);
-                 EntityType<?> entitytypes = ((SpawnEggItem) stack.getItem()).getType(pointer.level().registryAccess(), stack);
- 
-+                // CraftBukkit start
-+                ServerLevel worldserver = pointer.level();
-+                ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below and single item in event
-+                org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos());
-+                CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
-+
-+                BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0));
-+                if (!DispenserBlock.eventFired) {
-+                    worldserver.getCraftServer().getPluginManager().callEvent(event);
-+                }
-+
-+                if (event.isCancelled()) {
-+                    // stack.grow(1); // Paper - shrink below
-+                    return stack;
-+                }
-+
-+                boolean shrink = true; // Paper
-+                if (!event.getItem().equals(craftItem)) {
-+                    shrink = false; // Paper - shrink below
-+                    // Chain to handler for new item
-+                    ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
-+                    DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
-+                    if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
-+                        idispensebehavior.dispense(pointer, eventStack);
-+                        return stack;
-+                    }
-+                    // Paper start - track changed items in the dispense event
-+                    itemstack1 = CraftItemStack.unwrap(event.getItem()); // unwrap is safe because the stack won't be modified
-+                    entitytypes = ((SpawnEggItem) itemstack1.getItem()).getType(worldserver.registryAccess(), itemstack1);
-+                    // Paper end - track changed item from dispense event
-+                }
-+
-                 try {
--                    entitytypes.spawn(pointer.level(), stack, (Player) null, pointer.pos().relative(enumdirection), EntitySpawnReason.DISPENSER, enumdirection != Direction.UP, false);
-+                    entitytypes.spawn(pointer.level(), itemstack1, (Player) null, pointer.pos().relative(enumdirection), EntitySpawnReason.DISPENSER, enumdirection != Direction.UP, false); // Paper - track changed item in dispense event
-                 } catch (Exception exception) {
--                    null.LOGGER.error("Error while dispensing spawn egg from dispenser at {}", pointer.pos(), exception);
-+                    DispenseItemBehavior.LOGGER.error("Error while dispensing spawn egg from dispenser at {}", pointer.pos(), exception); // CraftBukkit - decompile error
-                     return ItemStack.EMPTY;
-                 }
- 
--                stack.shrink(1);
-+                if (shrink) stack.shrink(1); // Paper - actually handle here
-+                // CraftBukkit end
-                 pointer.level().gameEvent((Entity) null, (Holder) GameEvent.ENTITY_PLACE, pointer.pos());
-                 return stack;
-             }
-@@ -116,13 +163,43 @@
-                 Direction enumdirection = (Direction) pointer.state().getValue(DispenserBlock.FACING);
-                 BlockPos blockposition = pointer.pos().relative(enumdirection);
-                 ServerLevel worldserver = pointer.level();
-+
-+                // CraftBukkit start
-+                ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below and single item in event
-+                org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos());
-+                CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
-+
-+                BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0));
-+                if (!DispenserBlock.eventFired) {
-+                    worldserver.getCraftServer().getPluginManager().callEvent(event);
-+                }
-+
-+                if (event.isCancelled()) {
-+                    // stack.grow(1); // Paper - shrink below
-+                    return stack;
-+                }
-+
-+                boolean shrink = true; // Paper
-+                if (!event.getItem().equals(craftItem)) {
-+                    shrink = false; // Paper - shrink below
-+                    // Chain to handler for new item
-+                    ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
-+                    DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
-+                    if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
-+                        idispensebehavior.dispense(pointer, eventStack);
-+                        return stack;
-+                    }
-+                }
-+                // CraftBukkit end
-+
-+                final ItemStack newStack = CraftItemStack.unwrap(event.getItem()); // Paper - use event itemstack (unwrap is fine here because the stack won't be modified)
-                 Consumer<ArmorStand> consumer = EntityType.appendDefaultStackConfig((entityarmorstand) -> {
-                     entityarmorstand.setYRot(enumdirection.toYRot());
--                }, worldserver, stack, (Player) null);
-+                }, worldserver, newStack, (Player) null); // Paper - track changed items in the dispense event
-                 ArmorStand entityarmorstand = (ArmorStand) EntityType.ARMOR_STAND.spawn(worldserver, consumer, blockposition, EntitySpawnReason.DISPENSER, false, false);
- 
-                 if (entityarmorstand != null) {
--                    stack.shrink(1);
-+                    if (shrink) stack.shrink(1); // Paper - actually handle here
-                 }
- 
-                 return stack;
-@@ -141,7 +218,36 @@
-                 });
- 
-                 if (!list.isEmpty()) {
--                    ((Saddleable) list.get(0)).equipSaddle(stack.split(1), SoundSource.BLOCKS);
-+                    // CraftBukkit start
-+                    ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below and single item in event
-+                    ServerLevel world = pointer.level();
-+                    org.bukkit.block.Block block = CraftBlock.at(world, pointer.pos());
-+                    CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
-+
-+                    BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) list.get(0).getBukkitEntity());
-+                    if (!DispenserBlock.eventFired) {
-+                        world.getCraftServer().getPluginManager().callEvent(event);
-+                    }
-+
-+                    if (event.isCancelled()) {
-+                        // stack.grow(1); // Paper - shrink below
-+                        return stack;
-+                    }
-+
-+                    boolean shrink = true; // Paper
-+                    if (!event.getItem().equals(craftItem)) {
-+                        shrink = false; // Paper - shrink below
-+                        // Chain to handler for new item
-+                        ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
-+                        DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
-+                        if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) { // Paper - fix possible StackOverflowError
-+                            idispensebehavior.dispense(pointer, eventStack);
-+                            return stack;
-+                        }
-+                    }
-+                    ((Saddleable) list.get(0)).equipSaddle(CraftItemStack.asNMSCopy(event.getItem()), SoundSource.BLOCKS); // Paper - track changed items in dispense event
-+                    // CraftBukkit end
-+                    if (shrink) stack.shrink(1); // Paper - actually handle here
-                     this.setSuccess(true);
-                     return stack;
-                 } else {
-@@ -166,9 +272,38 @@
-                     }
- 
-                     entityhorsechestedabstract = (AbstractChestedHorse) iterator1.next();
--                } while (!entityhorsechestedabstract.isTamed() || !entityhorsechestedabstract.getSlot(499).set(stack));
-+                    // CraftBukkit start
-+                } while (!entityhorsechestedabstract.isTamed());
-+                ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below
-+                ServerLevel world = pointer.level();
-+                org.bukkit.block.Block block = CraftBlock.at(world, pointer.pos());
-+                CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
- 
--                stack.shrink(1);
-+                BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) entityhorsechestedabstract.getBukkitEntity());
-+                if (!DispenserBlock.eventFired) {
-+                    world.getCraftServer().getPluginManager().callEvent(event);
-+                }
-+
-+                if (event.isCancelled()) {
-+                    // stack.grow(1); // Paper - shrink below (this was actually missing and should be here, added it commented out to be consistent)
-+                    return stack;
-+                }
-+
-+                boolean shrink = true; // Paper
-+                if (!event.getItem().equals(craftItem)) {
-+                    shrink = false; // Paper - shrink below
-+                    // Chain to handler for new item
-+                    ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
-+                    DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
-+                    if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) { // Paper - fix possible StackOverflowError
-+                        idispensebehavior.dispense(pointer, eventStack);
-+                        return stack;
-+                    }
-+                }
-+                entityhorsechestedabstract.getSlot(499).set(CraftItemStack.asNMSCopy(event.getItem()));
-+                // CraftBukkit end
-+
-+                if (shrink) stack.shrink(1); // Paper - actually handle here
-                 this.setSuccess(true);
-                 return stack;
-             }
-@@ -202,8 +337,50 @@
-                 BlockPos blockposition = pointer.pos().relative((Direction) pointer.state().getValue(DispenserBlock.FACING));
-                 ServerLevel worldserver = pointer.level();
- 
-+                // CraftBukkit start
-+                int x = blockposition.getX();
-+                int y = blockposition.getY();
-+                int z = blockposition.getZ();
-+                BlockState iblockdata = worldserver.getBlockState(blockposition);
-+                ItemStack dispensedItem = stack; // Paper - track changed item from the dispense event
-+                // Paper start - correctly check if the bucket place will succeed
-+                /* Taken from SolidBucketItem#emptyContents */
-+                boolean willEmptyContentsSolidBucketItem = dispensiblecontaineritem instanceof net.minecraft.world.item.SolidBucketItem && worldserver.isInWorldBounds(blockposition) && iblockdata.isAir();
-+                /* Taken from BucketItem#emptyContents */
-+                boolean willEmptyBucketItem = dispensiblecontaineritem instanceof final BucketItem bucketItem && bucketItem.content instanceof net.minecraft.world.level.material.FlowingFluid && (iblockdata.isAir() || iblockdata.canBeReplaced(bucketItem.content) || (iblockdata.getBlock() instanceof LiquidBlockContainer liquidBlockContainer && liquidBlockContainer.canPlaceLiquid(null, worldserver, blockposition, iblockdata, bucketItem.content)));
-+                if (willEmptyContentsSolidBucketItem || willEmptyBucketItem) {
-+                // Paper end - correctly check if the bucket place will succeed
-+                    org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos());
-+                    CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event
-+
-+                    BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(x, y, z));
-+                    if (!DispenserBlock.eventFired) {
-+                        worldserver.getCraftServer().getPluginManager().callEvent(event);
-+                    }
-+
-+                    if (event.isCancelled()) {
-+                        return stack;
-+                    }
-+
-+                    if (!event.getItem().equals(craftItem)) {
-+                        // Chain to handler for new item
-+                        ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
-+                        DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
-+                        if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
-+                            idispensebehavior.dispense(pointer, eventStack);
-+                            return stack;
-+                        }
-+                    }
-+
-+                    // Paper start - track changed item from dispense event
-+                    dispensedItem = CraftItemStack.unwrap(event.getItem()); // unwrap is safe here as the stack isn't mutated
-+                    dispensiblecontaineritem = (DispensibleContainerItem) dispensedItem.getItem();
-+                    // Paper end - track changed item from dispense event
-+                }
-+                // CraftBukkit end
-+
-                 if (dispensiblecontaineritem.emptyContents((Player) null, worldserver, blockposition, (BlockHitResult) null)) {
--                    dispensiblecontaineritem.checkExtraContent((Player) null, worldserver, stack, blockposition);
-+                    dispensiblecontaineritem.checkExtraContent((Player) null, worldserver, dispensedItem, blockposition); // Paper - track changed item from dispense event
-                     return this.consumeWithRemainder(pointer, stack, new ItemStack(Items.BUCKET));
-                 } else {
-                     return this.defaultDispenseItemBehavior.dispense(pointer, stack);
-@@ -229,7 +406,7 @@
-                 Block block = iblockdata.getBlock();
- 
-                 if (block instanceof BucketPickup ifluidsource) {
--                    ItemStack itemstack1 = ifluidsource.pickupBlock((Player) null, worldserver, blockposition, iblockdata);
-+                    ItemStack itemstack1 = ifluidsource.pickupBlock((Player) null, DummyGeneratorAccess.INSTANCE, blockposition, iblockdata); // CraftBukkit
- 
-                     if (itemstack1.isEmpty()) {
-                         return super.execute(pointer, stack);
-@@ -237,6 +414,32 @@
-                         worldserver.gameEvent((Entity) null, (Holder) GameEvent.FLUID_PICKUP, blockposition);
-                         Item item = itemstack1.getItem();
- 
-+                        // CraftBukkit start
-+                        org.bukkit.block.Block bukkitBlock = CraftBlock.at(worldserver, pointer.pos());
-+                        CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event
-+
-+                        BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ()));
-+                        if (!DispenserBlock.eventFired) {
-+                            worldserver.getCraftServer().getPluginManager().callEvent(event);
-+                        }
-+
-+                        if (event.isCancelled()) {
-+                            return stack;
-+                        }
-+
-+                        if (!event.getItem().equals(craftItem)) {
-+                            // Chain to handler for new item
-+                            ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
-+                            DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
-+                            if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
-+                                idispensebehavior.dispense(pointer, eventStack);
-+                                return stack;
-+                            }
-+                        }
-+
-+                        itemstack1 = ifluidsource.pickupBlock((Player) null, worldserver, blockposition, iblockdata); // From above
-+                        // CraftBukkit end
-+
-                         return this.consumeWithRemainder(pointer, stack, new ItemStack(item));
-                     }
-                 } else {
-@@ -249,16 +452,44 @@
-             protected ItemStack execute(BlockSource pointer, ItemStack stack) {
-                 ServerLevel worldserver = pointer.level();
- 
-+                // CraftBukkit start
-+                org.bukkit.block.Block bukkitBlock = CraftBlock.at(worldserver, pointer.pos());
-+                CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); // Paper - ignore stack size on damageable items
-+
-+                BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0));
-+                if (!DispenserBlock.eventFired) {
-+                    worldserver.getCraftServer().getPluginManager().callEvent(event);
-+                }
-+
-+                if (event.isCancelled()) {
-+                    return stack;
-+                }
-+
-+                if (!event.getItem().equals(craftItem)) {
-+                    // Chain to handler for new item
-+                    ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
-+                    DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
-+                    if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
-+                        idispensebehavior.dispense(pointer, eventStack);
-+                        return stack;
-+                    }
-+                }
-+                // CraftBukkit end
-+
-                 this.setSuccess(true);
-                 Direction enumdirection = (Direction) pointer.state().getValue(DispenserBlock.FACING);
-                 BlockPos blockposition = pointer.pos().relative(enumdirection);
-                 BlockState iblockdata = worldserver.getBlockState(blockposition);
- 
-                 if (BaseFireBlock.canBePlacedAt(worldserver, blockposition, enumdirection)) {
--                    worldserver.setBlockAndUpdate(blockposition, BaseFireBlock.getState(worldserver, blockposition));
--                    worldserver.gameEvent((Entity) null, (Holder) GameEvent.BLOCK_PLACE, blockposition);
-+                    // CraftBukkit start - Ignition by dispensing flint and steel
-+                    if (!org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(worldserver, blockposition, pointer.pos()).isCancelled()) {
-+                        worldserver.setBlockAndUpdate(blockposition, BaseFireBlock.getState(worldserver, blockposition));
-+                        worldserver.gameEvent((Entity) null, (Holder) GameEvent.BLOCK_PLACE, blockposition);
-+                    }
-+                    // CraftBukkit end
-                 } else if (!CampfireBlock.canLight(iblockdata) && !CandleBlock.canLight(iblockdata) && !CandleCakeBlock.canLight(iblockdata)) {
--                    if (iblockdata.getBlock() instanceof TntBlock) {
-+                    if (iblockdata.getBlock() instanceof TntBlock && org.bukkit.craftbukkit.event.CraftEventFactory.callTNTPrimeEvent(worldserver, blockposition, org.bukkit.event.block.TNTPrimeEvent.PrimeCause.DISPENSER, null, pointer.pos())) { // CraftBukkit - TNTPrimeEvent
-                         TntBlock.explode(worldserver, blockposition);
-                         worldserver.removeBlock(blockposition, false);
-                     } else {
-@@ -283,13 +514,64 @@
-                 this.setSuccess(true);
-                 ServerLevel worldserver = pointer.level();
-                 BlockPos blockposition = pointer.pos().relative((Direction) pointer.state().getValue(DispenserBlock.FACING));
-+                // CraftBukkit start
-+                org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos());
-+                CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event
- 
-+                BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0));
-+                if (!DispenserBlock.eventFired) {
-+                    worldserver.getCraftServer().getPluginManager().callEvent(event);
-+                }
-+
-+                if (event.isCancelled()) {
-+                    return stack;
-+                }
-+
-+                if (!event.getItem().equals(craftItem)) {
-+                    // Chain to handler for new item
-+                    ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
-+                    DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
-+                    if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
-+                        idispensebehavior.dispense(pointer, eventStack);
-+                        return stack;
-+                    }
-+                }
-+
-+                worldserver.captureTreeGeneration = true;
-+                // CraftBukkit end
-+
-                 if (!BoneMealItem.growCrop(stack, worldserver, blockposition) && !BoneMealItem.growWaterPlant(stack, worldserver, blockposition, (Direction) null)) {
-                     this.setSuccess(false);
-                 } else if (!worldserver.isClientSide) {
-                     worldserver.levelEvent(1505, blockposition, 15);
-                 }
-+                // CraftBukkit start
-+                worldserver.captureTreeGeneration = false;
-+                if (worldserver.capturedBlockStates.size() > 0) {
-+                    TreeType treeType = SaplingBlock.treeType;
-+                    SaplingBlock.treeType = null;
-+                    Location location = CraftLocation.toBukkit(blockposition, worldserver.getWorld());
-+                    List<org.bukkit.block.BlockState> blocks = new java.util.ArrayList<>(worldserver.capturedBlockStates.values());
-+                    worldserver.capturedBlockStates.clear();
-+                    StructureGrowEvent structureEvent = null;
-+                    if (treeType != null) {
-+                        structureEvent = new StructureGrowEvent(location, treeType, false, null, blocks);
-+                        org.bukkit.Bukkit.getPluginManager().callEvent(structureEvent);
-+                    }
- 
-+                    BlockFertilizeEvent fertilizeEvent = new BlockFertilizeEvent(location.getBlock(), null, blocks);
-+                    fertilizeEvent.setCancelled(structureEvent != null && structureEvent.isCancelled());
-+                    org.bukkit.Bukkit.getPluginManager().callEvent(fertilizeEvent);
-+
-+                    if (!fertilizeEvent.isCancelled()) {
-+                        for (org.bukkit.block.BlockState blockstate : blocks) {
-+                            blockstate.update(true);
-+                            worldserver.checkCapturedTreeStateForObserverNotify(blockposition, (org.bukkit.craftbukkit.block.CraftBlockState) blockstate); // Paper - notify observers even if grow failed
-+                        }
-+                    }
-+                }
-+                // CraftBukkit end
-+
-                 return stack;
-             }
-         });
-@@ -298,12 +580,42 @@
-             protected ItemStack execute(BlockSource pointer, ItemStack stack) {
-                 ServerLevel worldserver = pointer.level();
-                 BlockPos blockposition = pointer.pos().relative((Direction) pointer.state().getValue(DispenserBlock.FACING));
--                PrimedTnt entitytntprimed = new PrimedTnt(worldserver, (double) blockposition.getX() + 0.5D, (double) blockposition.getY(), (double) blockposition.getZ() + 0.5D, (LivingEntity) null);
-+                // CraftBukkit start
-+                // EntityTNTPrimed entitytntprimed = new EntityTNTPrimed(worldserver, (double) blockposition.getX() + 0.5D, (double) blockposition.getY(), (double) blockposition.getZ() + 0.5D, (EntityLiving) null);
- 
-+                ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink at end and single item in event
-+                org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos());
-+                CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
-+
-+                BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector((double) blockposition.getX() + 0.5D, (double) blockposition.getY(), (double) blockposition.getZ() + 0.5D));
-+                if (!DispenserBlock.eventFired) {
-+                   worldserver.getCraftServer().getPluginManager().callEvent(event);
-+                }
-+
-+                if (event.isCancelled()) {
-+                    // stack.grow(1); // Paper - shrink below
-+                    return stack;
-+                }
-+
-+                boolean shrink = true; // Paper
-+                if (!event.getItem().equals(craftItem)) {
-+                    shrink = false; // Paper - shrink below
-+                    // Chain to handler for new item
-+                    ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
-+                    DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
-+                    if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
-+                        idispensebehavior.dispense(pointer, eventStack);
-+                        return stack;
-+                    }
-+                }
-+
-+                PrimedTnt entitytntprimed = new PrimedTnt(worldserver, event.getVelocity().getX(), event.getVelocity().getY(), event.getVelocity().getZ(), (LivingEntity) null);
-+                // CraftBukkit end
-+
-                 worldserver.addFreshEntity(entitytntprimed);
-                 worldserver.playSound((Player) null, entitytntprimed.getX(), entitytntprimed.getY(), entitytntprimed.getZ(), SoundEvents.TNT_PRIMED, SoundSource.BLOCKS, 1.0F, 1.0F);
-                 worldserver.gameEvent((Entity) null, (Holder) GameEvent.ENTITY_PLACE, blockposition);
--                stack.shrink(1);
-+                if (shrink) stack.shrink(1); // Paper - actually handle here
-                 return stack;
-             }
-         });
-@@ -313,7 +625,31 @@
-                 ServerLevel worldserver = pointer.level();
-                 Direction enumdirection = (Direction) pointer.state().getValue(DispenserBlock.FACING);
-                 BlockPos blockposition = pointer.pos().relative(enumdirection);
-+
-+                // CraftBukkit start
-+                org.bukkit.block.Block bukkitBlock = CraftBlock.at(worldserver, pointer.pos());
-+                CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event
-+
-+                BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ()));
-+                if (!DispenserBlock.eventFired) {
-+                    worldserver.getCraftServer().getPluginManager().callEvent(event);
-+                }
- 
-+                if (event.isCancelled()) {
-+                    return stack;
-+                }
-+
-+                if (!event.getItem().equals(craftItem)) {
-+                    // Chain to handler for new item
-+                    ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
-+                    DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
-+                    if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
-+                        idispensebehavior.dispense(pointer, eventStack);
-+                        return stack;
-+                    }
-+                }
-+                // CraftBukkit end
-+
-                 if (worldserver.isEmptyBlock(blockposition) && WitherSkullBlock.canSpawnMob(worldserver, blockposition, stack)) {
-                     worldserver.setBlock(blockposition, (BlockState) Blocks.WITHER_SKELETON_SKULL.defaultBlockState().setValue(SkullBlock.ROTATION, RotationSegment.convertToSegment(enumdirection)), 3);
-                     worldserver.gameEvent((Entity) null, (Holder) GameEvent.BLOCK_PLACE, blockposition);
-@@ -326,7 +662,7 @@
-                     stack.shrink(1);
-                     this.setSuccess(true);
-                 } else {
--                    this.setSuccess(EquipmentDispenseItemBehavior.dispenseEquipment(pointer, stack));
-+                    this.setSuccess(EquipmentDispenseItemBehavior.dispenseEquipment(pointer, stack, this)); // Paper - fix possible StackOverflowError
-                 }
- 
-                 return stack;
-@@ -339,6 +675,30 @@
-                 BlockPos blockposition = pointer.pos().relative((Direction) pointer.state().getValue(DispenserBlock.FACING));
-                 CarvedPumpkinBlock blockpumpkincarved = (CarvedPumpkinBlock) Blocks.CARVED_PUMPKIN;
- 
-+                // CraftBukkit start
-+                org.bukkit.block.Block bukkitBlock = CraftBlock.at(worldserver, pointer.pos());
-+                CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event
-+
-+                BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ()));
-+                if (!DispenserBlock.eventFired) {
-+                    worldserver.getCraftServer().getPluginManager().callEvent(event);
-+                }
-+
-+                if (event.isCancelled()) {
-+                    return stack;
-+                }
-+
-+                if (!event.getItem().equals(craftItem)) {
-+                    // Chain to handler for new item
-+                    ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
-+                    DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
-+                    if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
-+                        idispensebehavior.dispense(pointer, eventStack);
-+                        return stack;
-+                    }
-+                }
-+                // CraftBukkit end
-+
-                 if (worldserver.isEmptyBlock(blockposition) && blockpumpkincarved.canSpawnGolem(worldserver, blockposition)) {
-                     if (!worldserver.isClientSide) {
-                         worldserver.setBlock(blockposition, blockpumpkincarved.defaultBlockState(), 3);
-@@ -348,7 +708,7 @@
-                     stack.shrink(1);
-                     this.setSuccess(true);
-                 } else {
--                    this.setSuccess(EquipmentDispenseItemBehavior.dispenseEquipment(pointer, stack));
-+                    this.setSuccess(EquipmentDispenseItemBehavior.dispenseEquipment(pointer, stack, this)); // Paper - fix possible StackOverflowError
-                 }
- 
-                 return stack;
-@@ -377,6 +737,30 @@
-                 BlockPos blockposition = pointer.pos().relative((Direction) pointer.state().getValue(DispenserBlock.FACING));
-                 BlockState iblockdata = worldserver.getBlockState(blockposition);
- 
-+                // CraftBukkit start
-+                org.bukkit.block.Block bukkitBlock = CraftBlock.at(worldserver, pointer.pos());
-+                CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - only single item in event
-+
-+                BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ()));
-+                if (!DispenserBlock.eventFired) {
-+                    worldserver.getCraftServer().getPluginManager().callEvent(event);
-+                }
-+
-+                if (event.isCancelled()) {
-+                    return stack;
-+                }
-+
-+                if (!event.getItem().equals(craftItem)) {
-+                    // Chain to handler for new item
-+                    ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
-+                    DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
-+                    if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
-+                        idispensebehavior.dispense(pointer, eventStack);
-+                        return stack;
-+                    }
-+                }
-+                // CraftBukkit end
-+
-                 if (iblockdata.is(BlockTags.BEEHIVES, (blockbase_blockdata) -> {
-                     return blockbase_blockdata.hasProperty(BeehiveBlock.HONEY_LEVEL) && blockbase_blockdata.getBlock() instanceof BeehiveBlock;
-                 }) && (Integer) iblockdata.getValue(BeehiveBlock.HONEY_LEVEL) >= 5) {
-@@ -402,6 +786,13 @@
-                 this.setSuccess(true);
-                 if (iblockdata.is(Blocks.RESPAWN_ANCHOR)) {
-                     if ((Integer) iblockdata.getValue(RespawnAnchorBlock.CHARGE) != 4) {
-+                        // Paper start - Call missing BlockDispenseEvent
-+                        ItemStack result = org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockDispenseEvent(pointer, blockposition, stack, this);
-+                        if (result != null) {
-+                            this.setSuccess(false);
-+                            return result;
-+                        }
-+                        // Paper end - Call missing BlockDispenseEvent
-                         RespawnAnchorBlock.charge((Entity) null, worldserver, blockposition, iblockdata);
-                         stack.shrink(1);
-                     } else {
-@@ -426,6 +817,31 @@
-                     this.setSuccess(false);
-                     return stack;
-                 } else {
-+                    // CraftBukkit start
-+                    ItemStack itemstack1 = stack;
-+                    ServerLevel world = pointer.level();
-+                    org.bukkit.block.Block block = CraftBlock.at(world, pointer.pos());
-+                    CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); // Paper - ignore stack size on damageable items
-+
-+                    BlockDispenseEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) list.get(0).getBukkitEntity());
-+                    if (!DispenserBlock.eventFired) {
-+                        world.getCraftServer().getPluginManager().callEvent(event);
-+                    }
-+
-+                    if (event.isCancelled()) {
-+                        return stack;
-+                    }
-+
-+                    if (!event.getItem().equals(craftItem)) {
-+                        // Chain to handler for new item
-+                        ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
-+                        DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
-+                        if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) { // Paper - fix possible StackOverflowError
-+                            idispensebehavior.dispense(pointer, eventStack);
-+                            return stack;
-+                        }
-+                    }
-+                    // CraftBukkit end
-                     Iterator iterator1 = list.iterator();
- 
-                     Armadillo armadillo;
-@@ -454,6 +870,13 @@
-                 Optional<BlockState> optional = HoneycombItem.getWaxed(iblockdata);
- 
-                 if (optional.isPresent()) {
-+                    // Paper start - Call missing BlockDispenseEvent
-+                    ItemStack result = org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockDispenseEvent(pointer, blockposition, stack, this);
-+                    if (result != null) {
-+                        this.setSuccess(false);
-+                        return result;
-+                    }
-+                    // Paper end - Call missing BlockDispenseEvent
-                     worldserver.setBlockAndUpdate(blockposition, (BlockState) optional.get());
-                     worldserver.levelEvent(3003, blockposition, 0);
-                     stack.shrink(1);
-@@ -481,6 +904,12 @@
-                     if (!worldserver.getBlockState(blockposition1).is(BlockTags.CONVERTABLE_TO_MUD)) {
-                         return this.defaultDispenseItemBehavior.dispense(pointer, stack);
-                     } else {
-+                        // Paper start - Call missing BlockDispenseEvent
-+                        ItemStack result = org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockDispenseEvent(pointer, blockposition1, stack, this);
-+                        if (result != null) {
-+                            return result;
-+                        }
-+                        // Paper end - Call missing BlockDispenseEvent
-                         if (!worldserver.isClientSide) {
-                             for (int k = 0; k < 5; ++k) {
-                                 worldserver.sendParticles(ParticleTypes.SPLASH, (double) blockposition.getX() + worldserver.random.nextDouble(), (double) (blockposition.getY() + 1), (double) blockposition.getZ() + worldserver.random.nextDouble(), 1, 0.0D, 0.0D, 0.0D, 1.0D);
diff --git a/paper-server/patches/unapplied/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java.patch b/paper-server/patches/unapplied/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java.patch
deleted file mode 100644
index 18eb499fba..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java.patch
+++ /dev/null
@@ -1,82 +0,0 @@
---- a/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java
-+++ b/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java
-@@ -7,8 +7,13 @@
- import net.minecraft.world.entity.LivingEntity;
- import net.minecraft.world.entity.Mob;
- import net.minecraft.world.item.ItemStack;
-+import net.minecraft.world.level.Level;
- import net.minecraft.world.level.block.DispenserBlock;
- import net.minecraft.world.phys.AABB;
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.event.block.BlockDispenseArmorEvent;
-+// CraftBukkit end
- 
- public class EquipmentDispenseItemBehavior extends DefaultDispenseItemBehavior {
- 
-@@ -18,10 +23,15 @@
- 
-     @Override
-     protected ItemStack execute(BlockSource pointer, ItemStack stack) {
--        return EquipmentDispenseItemBehavior.dispenseEquipment(pointer, stack) ? stack : super.execute(pointer, stack);
-+        return EquipmentDispenseItemBehavior.dispenseEquipment(pointer, stack, this) ? stack : super.execute(pointer, stack); // Paper - fix possible StackOverflowError
-     }
- 
--    public static boolean dispenseEquipment(BlockSource pointer, ItemStack stack) {
-+    @Deprecated @io.papermc.paper.annotation.DoNotUse // Paper
-+    public static boolean dispenseEquipment(BlockSource pointer, ItemStack armor) {
-+        // Paper start
-+        return dispenseEquipment(pointer, armor, null);
-+    }
-+    public static boolean dispenseEquipment(BlockSource pointer, ItemStack stack, @javax.annotation.Nullable DispenseItemBehavior currentBehavior) {
-         BlockPos blockposition = pointer.pos().relative((Direction) pointer.state().getValue(DispenserBlock.FACING));
-         List<LivingEntity> list = pointer.level().getEntitiesOfClass(LivingEntity.class, new AABB(blockposition), (entityliving) -> {
-             return entityliving.canEquipWithDispenser(stack);
-@@ -32,9 +42,37 @@
-         } else {
-             LivingEntity entityliving = (LivingEntity) list.getFirst();
-             EquipmentSlot enumitemslot = entityliving.getEquipmentSlotForItem(stack);
--            ItemStack itemstack1 = stack.split(1);
-+            ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below and single item in event
- 
--            entityliving.setItemSlot(enumitemslot, itemstack1);
-+            // CraftBukkit start
-+            Level world = pointer.level();
-+            org.bukkit.block.Block block = CraftBlock.at(world, pointer.pos());
-+            CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
-+
-+            BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) entityliving.getBukkitEntity());
-+            if (!DispenserBlock.eventFired) {
-+                world.getCraftServer().getPluginManager().callEvent(event);
-+            }
-+
-+            if (event.isCancelled()) {
-+                // stack.grow(1); // Paper - shrink below
-+                return false;
-+            }
-+
-+            boolean shrink = true; // Paper
-+            if (!event.getItem().equals(craftItem)) {
-+                shrink = false; // Paper - shrink below
-+                // Chain to handler for new item
-+                ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
-+                DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
-+                if (idispensebehavior != DispenseItemBehavior.NOOP && (currentBehavior == null || idispensebehavior != currentBehavior)) { // Paper - fix possible StackOverflowError
-+                    idispensebehavior.dispense(pointer, eventStack);
-+                    return true;
-+                }
-+            }
-+
-+            entityliving.setItemSlot(enumitemslot, CraftItemStack.asNMSCopy(event.getItem()));
-+            // CraftBukkit end
-             if (entityliving instanceof Mob) {
-                 Mob entityinsentient = (Mob) entityliving;
- 
-@@ -42,6 +80,7 @@
-                 entityinsentient.setPersistenceRequired();
-             }
- 
-+            if (shrink) stack.shrink(1); // Paper - shrink here
-             return true;
-         }
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java.patch b/paper-server/patches/unapplied/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java.patch
deleted file mode 100644
index ea22a6dd42..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java.patch
+++ /dev/null
@@ -1,58 +0,0 @@
---- a/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java
-+++ b/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java
-@@ -15,6 +15,11 @@
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.block.state.properties.RailShape;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.event.block.BlockDispenseEvent;
-+// CraftBukkit end
- 
- public class MinecartDispenseItemBehavior extends DefaultDispenseItemBehavior {
- 
-@@ -62,11 +67,40 @@
-         }
- 
-         Vec3 vec3d1 = new Vec3(d0, d1 + d3, d2);
--        AbstractMinecart entityminecartabstract = AbstractMinecart.createMinecart(worldserver, vec3d1.x, vec3d1.y, vec3d1.z, this.entityType, EntitySpawnReason.DISPENSER, stack, (Player) null);
-+        // CraftBukkit start
-+        // EntityMinecartAbstract entityminecartabstract = EntityMinecartAbstract.createMinecart(worldserver, vec3d1.x, vec3d1.y, vec3d1.z, this.entityType, EntitySpawnReason.DISPENSER, itemstack, (EntityHuman) null);
-+        ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below and single item in event
-+        org.bukkit.block.Block block2 = CraftBlock.at(worldserver, pointer.pos());
-+        CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
- 
-+        BlockDispenseEvent event = new BlockDispenseEvent(block2, craftItem.clone(), new org.bukkit.util.Vector(vec3d1.x, vec3d1.y, vec3d1.z));
-+        if (!DispenserBlock.eventFired) {
-+            worldserver.getCraftServer().getPluginManager().callEvent(event);
-+        }
-+
-+        if (event.isCancelled()) {
-+            // stack.grow(1); // Paper - shrink below
-+            return stack;
-+        }
-+
-+        boolean shrink = true; // Paper
-+        if (!event.getItem().equals(craftItem)) {
-+            shrink = false; // Paper - shrink below
-+            // Chain to handler for new item
-+            ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
-+            DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
-+            if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
-+                idispensebehavior.dispense(pointer, eventStack);
-+                return stack;
-+            }
-+        }
-+
-+        itemstack1 = CraftItemStack.asNMSCopy(event.getItem());
-+        AbstractMinecart entityminecartabstract = AbstractMinecart.createMinecart(worldserver, event.getVelocity().getX(), event.getVelocity().getY(), event.getVelocity().getZ(), this.entityType, EntitySpawnReason.DISPENSER, itemstack1, (Player) null);
-+
-         if (entityminecartabstract != null) {
--            worldserver.addFreshEntity(entityminecartabstract);
--            stack.shrink(1);
-+            if (worldserver.addFreshEntity(entityminecartabstract) && shrink) stack.shrink(1); // Paper - if entity add was successful and supposed to shrink
-+            // CraftBukkit end
-         }
- 
-         return stack;
diff --git a/paper-server/patches/unapplied/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java.patch b/paper-server/patches/unapplied/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java.patch
deleted file mode 100644
index 2061a38275..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java.patch
+++ /dev/null
@@ -1,58 +0,0 @@
---- a/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java
-+++ b/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java
-@@ -8,6 +8,11 @@
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.item.ProjectileItem;
- import net.minecraft.world.level.block.DispenserBlock;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.event.block.BlockDispenseEvent;
-+// CraftBukkit end
- 
- public class ProjectileDispenseBehavior extends DefaultDispenseItemBehavior {
- 
-@@ -31,8 +36,41 @@
-         Direction enumdirection = (Direction) pointer.state().getValue(DispenserBlock.FACING);
-         Position iposition = this.dispenseConfig.positionFunction().getDispensePosition(pointer, enumdirection);
- 
--        Projectile.spawnProjectileUsingShoot(this.projectileItem.asProjectile(worldserver, iposition, stack, enumdirection), worldserver, stack, (double) enumdirection.getStepX(), (double) enumdirection.getStepY(), (double) enumdirection.getStepZ(), this.dispenseConfig.power(), this.dispenseConfig.uncertainty());
--        stack.shrink(1);
-+        // CraftBukkit start
-+        // IProjectile.spawnProjectileUsingShoot(this.projectileItem.asProjectile(worldserver, iposition, itemstack, enumdirection), worldserver, itemstack, (double) enumdirection.getStepX(), (double) enumdirection.getStepY(), (double) enumdirection.getStepZ(), this.dispenseConfig.power(), this.dispenseConfig.uncertainty()); // CraftBukkit - call when finish the BlockDispenseEvent
-+        ItemStack itemstack1 = stack.copyWithCount(1); // Paper - shrink below and single item in event
-+        org.bukkit.block.Block block = CraftBlock.at(worldserver, pointer.pos());
-+        CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
-+
-+        BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector((double) enumdirection.getStepX(), (double) enumdirection.getStepY(), (double) enumdirection.getStepZ()));
-+        if (!DispenserBlock.eventFired) {
-+            worldserver.getCraftServer().getPluginManager().callEvent(event);
-+        }
-+
-+        if (event.isCancelled()) {
-+            // stack.grow(1); // Paper - shrink below
-+            return stack;
-+        }
-+
-+        boolean shrink = true; // Paper
-+        if (!event.getItem().equals(craftItem)) {
-+            shrink = false; // Paper - shrink below
-+            // Chain to handler for new item
-+            ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
-+            DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
-+            if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
-+                idispensebehavior.dispense(pointer, eventStack);
-+                return stack;
-+            }
-+        }
-+
-+        // SPIGOT-7923: Avoid create projectiles with empty item
-+        if (!itemstack1.isEmpty()) {
-+            Projectile iprojectile = Projectile.spawnProjectileUsingShoot(this.projectileItem.asProjectile(worldserver, iposition, CraftItemStack.unwrap(event.getItem()), enumdirection), worldserver, itemstack1, event.getVelocity().getX(), event.getVelocity().getY(), event.getVelocity().getZ(), this.dispenseConfig.power(), this.dispenseConfig.uncertainty()); // Paper - track changed items in the dispense event; unwrap is safe here because all uses of the stack make their own copies
-+            iprojectile.projectileSource = new org.bukkit.craftbukkit.projectiles.CraftBlockProjectileSource(pointer.blockEntity());
-+        }
-+        if (shrink) stack.shrink(1); // Paper - actually handle here
-+        // CraftBukkit end
-         return stack;
-     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java.patch b/paper-server/patches/unapplied/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java.patch
deleted file mode 100644
index bc579690dd..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java.patch
+++ /dev/null
@@ -1,81 +0,0 @@
---- a/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java
-+++ b/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java
-@@ -22,6 +22,12 @@
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.phys.AABB;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.event.block.BlockDispenseEvent;
-+// CraftBukkit end
- 
- public class ShearsDispenseItemBehavior extends OptionalDispenseItemBehavior {
- 
-@@ -30,11 +36,34 @@
-     @Override
-     protected ItemStack execute(BlockSource pointer, ItemStack stack) {
-         ServerLevel worldserver = pointer.level();
-+        // CraftBukkit start
-+        org.bukkit.block.Block bukkitBlock = CraftBlock.at(worldserver, pointer.pos());
-+        CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); // Paper - ignore stack size on damageable items
- 
-+        BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0));
-+        if (!DispenserBlock.eventFired) {
-+            worldserver.getCraftServer().getPluginManager().callEvent(event);
-+        }
-+
-+        if (event.isCancelled()) {
-+            return stack;
-+        }
-+
-+        if (!event.getItem().equals(craftItem)) {
-+            // Chain to handler for new item
-+            ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
-+            DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
-+            if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
-+                idispensebehavior.dispense(pointer, eventStack);
-+                return stack;
-+            }
-+        }
-+        // CraftBukkit end
-+
-         if (!worldserver.isClientSide()) {
-             BlockPos blockposition = pointer.pos().relative((Direction) pointer.state().getValue(DispenserBlock.FACING));
- 
--            this.setSuccess(ShearsDispenseItemBehavior.tryShearBeehive(worldserver, blockposition) || ShearsDispenseItemBehavior.tryShearLivingEntity(worldserver, blockposition, stack));
-+            this.setSuccess(ShearsDispenseItemBehavior.tryShearBeehive(worldserver, blockposition) || ShearsDispenseItemBehavior.tryShearLivingEntity(worldserver, blockposition, stack, bukkitBlock, craftItem)); // CraftBukkit
-             if (this.isSuccess()) {
-                 stack.hurtAndBreak(1, worldserver, (ServerPlayer) null, (item) -> {
-                 });
-@@ -64,8 +93,8 @@
-         return false;
-     }
- 
--    private static boolean tryShearLivingEntity(ServerLevel world, BlockPos pos, ItemStack shears) {
--        List<LivingEntity> list = world.getEntitiesOfClass(LivingEntity.class, new AABB(pos), EntitySelector.NO_SPECTATORS);
-+    private static boolean tryShearLivingEntity(ServerLevel worldserver, BlockPos blockposition, ItemStack itemstack, org.bukkit.block.Block bukkitBlock, CraftItemStack craftItem) { // CraftBukkit - add args
-+        List<LivingEntity> list = worldserver.getEntitiesOfClass(LivingEntity.class, new AABB(blockposition), EntitySelector.NO_SPECTATORS);
-         Iterator iterator = list.iterator();
- 
-         while (iterator.hasNext()) {
-@@ -73,8 +102,16 @@
- 
-             if (entityliving instanceof Shearable ishearable) {
-                 if (ishearable.readyForShearing()) {
--                    ishearable.shear(world, SoundSource.BLOCKS, shears);
--                    world.gameEvent((Entity) null, (Holder) GameEvent.SHEAR, pos);
-+                    // CraftBukkit start
-+                    // Paper start - Add drops to shear events
-+                    org.bukkit.event.block.BlockShearEntityEvent event = CraftEventFactory.callBlockShearEntityEvent(entityliving, bukkitBlock, craftItem, ishearable.generateDefaultDrops(worldserver, itemstack));
-+                    if (event.isCancelled()) {
-+                        // Paper end - Add drops to shear events
-+                        continue;
-+                    }
-+                    // CraftBukkit end
-+                    ishearable.shear(worldserver, SoundSource.BLOCKS, itemstack, CraftItemStack.asNMSCopy(event.getDrops())); // Paper - Add drops to shear events
-+                    worldserver.gameEvent((Entity) null, (Holder) GameEvent.SHEAR, blockposition);
-                     return true;
-                 }
-             }
diff --git a/paper-server/patches/unapplied/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java.patch b/paper-server/patches/unapplied/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java.patch
deleted file mode 100644
index bfa73fef42..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java.patch
+++ /dev/null
@@ -1,54 +0,0 @@
---- a/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java
-+++ b/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java
-@@ -10,6 +10,12 @@
- import net.minecraft.world.level.block.DispenserBlock;
- import org.slf4j.Logger;
- 
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.event.block.BlockDispenseEvent;
-+// CraftBukkit end
-+
- public class ShulkerBoxDispenseBehavior extends OptionalDispenseItemBehavior {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
-@@ -26,8 +32,37 @@
-             BlockPos blockposition = pointer.pos().relative(enumdirection);
-             Direction enumdirection1 = pointer.level().isEmptyBlock(blockposition.below()) ? enumdirection : Direction.UP;
- 
-+            // CraftBukkit start
-+            org.bukkit.block.Block bukkitBlock = CraftBlock.at(pointer.level(), pointer.pos());
-+            CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event
-+
-+            BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ()));
-+            if (!DispenserBlock.eventFired) {
-+                pointer.level().getCraftServer().getPluginManager().callEvent(event);
-+            }
-+
-+            if (event.isCancelled()) {
-+                return stack;
-+            }
-+
-+            if (!event.getItem().equals(craftItem)) {
-+                // Chain to handler for new item
-+                ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem());
-+                DispenseItemBehavior idispensebehavior = DispenserBlock.getDispenseBehavior(pointer, eventStack); // Paper - Fix NPE with equippable and items without behavior
-+                if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) {
-+                    idispensebehavior.dispense(pointer, eventStack);
-+                    return stack;
-+                }
-+            }
-+            // CraftBukkit end
-+
-             try {
--                this.setSuccess(((BlockItem) item).place(new DirectionalPlaceContext(pointer.level(), blockposition, enumdirection, stack, enumdirection1)).consumesAction());
-+                // Paper start - track changed items in the dispense event
-+                this.setSuccess(((BlockItem) item).place(new DirectionalPlaceContext(pointer.level(), blockposition, enumdirection, CraftItemStack.asNMSCopy(event.getItem()), enumdirection1)).consumesAction());
-+                if (this.isSuccess()) {
-+                    stack.shrink(1); // vanilla shrink is in the place function above, manually handle it here
-+                }
-+                // Paper end - track changed items in the dispense event
-             } catch (Exception exception) {
-                 ShulkerBoxDispenseBehavior.LOGGER.error("Error trying to place shulker box at {}", blockposition, exception);
-             }
diff --git a/paper-server/patches/unapplied/net/minecraft/nbt/ByteArrayTag.java.patch b/paper-server/patches/unapplied/net/minecraft/nbt/ByteArrayTag.java.patch
deleted file mode 100644
index d7c707925b..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/nbt/ByteArrayTag.java.patch
+++ /dev/null
@@ -1,15 +0,0 @@
---- a/net/minecraft/nbt/ByteArrayTag.java
-+++ b/net/minecraft/nbt/ByteArrayTag.java
-@@ -1,3 +1,4 @@
-+// mc-dev import
- package net.minecraft.nbt;
- 
- import java.io.DataInput;
-@@ -24,6 +25,7 @@
-         private static byte[] readAccounted(DataInput input, NbtAccounter tracker) throws IOException {
-             tracker.accountBytes(24L);
-             int i = input.readInt();
-+            com.google.common.base.Preconditions.checkArgument( i < 1 << 24); // Spigot
- 
-             tracker.accountBytes(1L, (long) i);
-             byte[] abyte = new byte[i];
diff --git a/paper-server/patches/unapplied/net/minecraft/nbt/IntArrayTag.java.patch b/paper-server/patches/unapplied/net/minecraft/nbt/IntArrayTag.java.patch
deleted file mode 100644
index 97872e3339..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/nbt/IntArrayTag.java.patch
+++ /dev/null
@@ -1,15 +0,0 @@
---- a/net/minecraft/nbt/IntArrayTag.java
-+++ b/net/minecraft/nbt/IntArrayTag.java
-@@ -1,3 +1,4 @@
-+// mc-dev import
- package net.minecraft.nbt;
- 
- import java.io.DataInput;
-@@ -24,6 +25,7 @@
-         private static int[] readAccounted(DataInput input, NbtAccounter tracker) throws IOException {
-             tracker.accountBytes(24L);
-             int i = input.readInt();
-+            com.google.common.base.Preconditions.checkArgument( i < 1 << 24); // Spigot
- 
-             tracker.accountBytes(4L, (long) i);
-             int[] aint = new int[i];
diff --git a/paper-server/patches/unapplied/net/minecraft/nbt/NbtIo.java.patch b/paper-server/patches/unapplied/net/minecraft/nbt/NbtIo.java.patch
deleted file mode 100644
index f6dc9e632c..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/nbt/NbtIo.java.patch
+++ /dev/null
@@ -1,20 +0,0 @@
---- a/net/minecraft/nbt/NbtIo.java
-+++ b/net/minecraft/nbt/NbtIo.java
-@@ -1,3 +1,4 @@
-+// mc-dev import
- package net.minecraft.nbt;
- 
- import java.io.BufferedOutputStream;
-@@ -324,6 +325,12 @@
-     }
- 
-     public static CompoundTag read(DataInput input, NbtAccounter tracker) throws IOException {
-+        // Spigot start
-+        if ( input instanceof io.netty.buffer.ByteBufInputStream )
-+        {
-+            input = new DataInputStream(new org.spigotmc.LimitStream((InputStream) input, tracker));
-+        }
-+        // Spigot end
-         Tag nbtbase = NbtIo.readUnnamedTag(input, tracker);
- 
-         if (nbtbase instanceof CompoundTag) {
diff --git a/paper-server/patches/unapplied/net/minecraft/network/FriendlyByteBuf.java.patch b/paper-server/patches/unapplied/net/minecraft/network/FriendlyByteBuf.java.patch
deleted file mode 100644
index c9d512f8d6..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/network/FriendlyByteBuf.java.patch
+++ /dev/null
@@ -1,105 +0,0 @@
---- a/net/minecraft/network/FriendlyByteBuf.java
-+++ b/net/minecraft/network/FriendlyByteBuf.java
-@@ -72,6 +72,7 @@
- 
-     public static final int DEFAULT_NBT_QUOTA = 2097152;
-     private final ByteBuf source;
-+    @Nullable public final java.util.Locale adventure$locale; // Paper - track player's locale for server-side translations
-     public static final short MAX_STRING_LENGTH = Short.MAX_VALUE;
-     public static final int MAX_COMPONENT_STRING_LENGTH = 262144;
-     private static final int PUBLIC_KEY_SIZE = 256;
-@@ -80,6 +81,7 @@
-     private static final Gson GSON = new Gson();
- 
-     public FriendlyByteBuf(ByteBuf parent) {
-+        this.adventure$locale = PacketEncoder.ADVENTURE_LOCALE.get(); // Paper - track player's locale for server-side translations
-         this.source = parent;
-     }
- 
-@@ -120,11 +122,16 @@
-     }
- 
-     public <T> void writeJsonWithCodec(Codec<T> codec, T value) {
-+        // Paper start - Adventure; add max length parameter
-+        this.writeJsonWithCodec(codec, value, MAX_STRING_LENGTH);
-+    }
-+    public <T> void writeJsonWithCodec(Codec<T> codec, T value, int maxLength) {
-+        // Paper end - Adventure; add max length parameter
-         DataResult<JsonElement> dataresult = codec.encodeStart(JsonOps.INSTANCE, value);
- 
-         this.writeUtf(FriendlyByteBuf.GSON.toJson((JsonElement) dataresult.getOrThrow((s) -> {
-             return new EncoderException("Failed to encode: " + s + " " + String.valueOf(value));
--        })));
-+        })), maxLength); // Paper - Adventure; add max length parameter
-     }
- 
-     public static <T> IntFunction<T> limitValue(IntFunction<T> applier, int max) {
-@@ -139,7 +146,7 @@
- 
-     public <T, C extends Collection<T>> C readCollection(IntFunction<C> collectionFactory, StreamDecoder<? super FriendlyByteBuf, T> reader) {
-         int i = this.readVarInt();
--        C c0 = (Collection) collectionFactory.apply(i);
-+        C c0 = collectionFactory.apply(i); // CraftBukkit - decompile error
- 
-         for (int j = 0; j < i; ++j) {
-             c0.add(reader.decode(this));
-@@ -150,7 +157,7 @@
- 
-     public <T> void writeCollection(Collection<T> collection, StreamEncoder<? super FriendlyByteBuf, T> writer) {
-         this.writeVarInt(collection.size());
--        Iterator iterator = collection.iterator();
-+        Iterator<T> iterator = collection.iterator(); // CraftBukkit - decompile error
- 
-         while (iterator.hasNext()) {
-             T t0 = iterator.next();
-@@ -177,12 +184,12 @@
- 
-     public void writeIntIdList(IntList list) {
-         this.writeVarInt(list.size());
--        list.forEach(this::writeVarInt);
-+        list.forEach((java.util.function.IntConsumer) this::writeVarInt); // CraftBukkit - decompile error
-     }
- 
-     public <K, V, M extends Map<K, V>> M readMap(IntFunction<M> mapFactory, StreamDecoder<? super FriendlyByteBuf, K> keyReader, StreamDecoder<? super FriendlyByteBuf, V> valueReader) {
-         int i = this.readVarInt();
--        M m0 = (Map) mapFactory.apply(i);
-+        M m0 = mapFactory.apply(i); // CraftBukkit - decompile error
- 
-         for (int j = 0; j < i; ++j) {
-             K k0 = keyReader.decode(this);
-@@ -216,7 +223,7 @@
-     }
- 
-     public <E extends Enum<E>> void writeEnumSet(EnumSet<E> enumSet, Class<E> type) {
--        E[] ae = (Enum[]) type.getEnumConstants();
-+        E[] ae = type.getEnumConstants(); // CraftBukkit - decompile error
-         BitSet bitset = new BitSet(ae.length);
- 
-         for (int i = 0; i < ae.length; ++i) {
-@@ -227,7 +234,7 @@
-     }
- 
-     public <E extends Enum<E>> EnumSet<E> readEnumSet(Class<E> type) {
--        E[] ae = (Enum[]) type.getEnumConstants();
-+        E[] ae = type.getEnumConstants(); // CraftBukkit - decompile error
-         BitSet bitset = this.readFixedBitSet(ae.length);
-         EnumSet<E> enumset = EnumSet.noneOf(type);
- 
-@@ -498,7 +505,7 @@
-     }
- 
-     public <T extends Enum<T>> T readEnum(Class<T> enumClass) {
--        return ((Enum[]) enumClass.getEnumConstants())[this.readVarInt()];
-+        return ((T[]) enumClass.getEnumConstants())[this.readVarInt()]; // CraftBukkit - fix decompile error
-     }
- 
-     public FriendlyByteBuf writeEnum(Enum<?> instance) {
-@@ -565,7 +572,7 @@
- 
-         try {
-             NbtIo.writeAnyTag((Tag) nbt, new ByteBufOutputStream(buf));
--        } catch (IOException ioexception) {
-+        } catch (Exception ioexception) { // CraftBukkit - IOException -> Exception
-             throw new EncoderException(ioexception);
-         }
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/network/chat/ChatDecorator.java.patch b/paper-server/patches/unapplied/net/minecraft/network/chat/ChatDecorator.java.patch
deleted file mode 100644
index 8fe79b8a75..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/network/chat/ChatDecorator.java.patch
+++ /dev/null
@@ -1,23 +0,0 @@
---- a/net/minecraft/network/chat/ChatDecorator.java
-+++ b/net/minecraft/network/chat/ChatDecorator.java
-@@ -2,10 +2,18 @@
- 
- import javax.annotation.Nullable;
- import net.minecraft.server.level.ServerPlayer;
-+import java.util.concurrent.CompletableFuture; // Paper
- 
- @FunctionalInterface
- public interface ChatDecorator {
--    ChatDecorator PLAIN = (sender, message) -> message;
-+    ChatDecorator PLAIN = (sender, message) -> CompletableFuture.completedFuture(message); // Paper - adventure; support async chat decoration events
- 
--    Component decorate(@Nullable ServerPlayer sender, Component message);
-+    @io.papermc.paper.annotation.DoNotUse @Deprecated // Paper - adventure; support chat decoration events (callers should use the overload with CommandSourceStack)
-+    CompletableFuture<Component> decorate(@Nullable ServerPlayer sender, Component message); // Paper - adventure; support async chat decoration events
-+
-+    // Paper start - adventure; support async chat decoration events
-+    default CompletableFuture<Component> decorate(@Nullable ServerPlayer sender, @Nullable net.minecraft.commands.CommandSourceStack commandSourceStack, Component message) {
-+        throw new UnsupportedOperationException("Must override this implementation");
-+    }
-+    // Paper end - adventure; support async chat decoration events
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/network/chat/OutgoingChatMessage.java.patch b/paper-server/patches/unapplied/net/minecraft/network/chat/OutgoingChatMessage.java.patch
deleted file mode 100644
index 3192e18db3..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/network/chat/OutgoingChatMessage.java.patch
+++ /dev/null
@@ -1,44 +0,0 @@
---- a/net/minecraft/network/chat/OutgoingChatMessage.java
-+++ b/net/minecraft/network/chat/OutgoingChatMessage.java
-@@ -7,6 +7,12 @@
- 
-     void sendToPlayer(ServerPlayer sender, boolean filterMaskEnabled, ChatType.Bound params);
- 
-+    // Paper start
-+    default void sendToPlayer(ServerPlayer sender, boolean filterMaskEnabled, ChatType.Bound params, @javax.annotation.Nullable Component unsigned) {
-+        this.sendToPlayer(sender, filterMaskEnabled, params);
-+    }
-+    // Paper end
-+
-     static OutgoingChatMessage create(PlayerChatMessage message) {
-         return (OutgoingChatMessage)(message.isSystem()
-             ? new OutgoingChatMessage.Disguised(message.decoratedContent())
-@@ -16,8 +22,13 @@
-     public static record Disguised(@Override Component content) implements OutgoingChatMessage {
-         @Override
-         public void sendToPlayer(ServerPlayer sender, boolean filterMaskEnabled, ChatType.Bound params) {
--            sender.connection.sendDisguisedChatMessage(this.content, params);
-+           // Paper start
-+            this.sendToPlayer(sender, filterMaskEnabled, params, null);
-         }
-+        public void sendToPlayer(ServerPlayer sender, boolean filterMaskEnabled, ChatType.Bound params, @javax.annotation.Nullable Component unsigned) {
-+            sender.connection.sendDisguisedChatMessage(unsigned != null ? unsigned : this.content, params);
-+            // Paper end
-+        }
-     }
- 
-     public static record Player(PlayerChatMessage message) implements OutgoingChatMessage {
-@@ -28,7 +39,13 @@
- 
-         @Override
-         public void sendToPlayer(ServerPlayer sender, boolean filterMaskEnabled, ChatType.Bound params) {
-+            // Paper start
-+            this.sendToPlayer(sender, filterMaskEnabled, params, null);
-+        }
-+        public void sendToPlayer(ServerPlayer sender, boolean filterMaskEnabled, ChatType.Bound params, @javax.annotation.Nullable Component unsigned) {
-+            // Paper end
-             PlayerChatMessage playerChatMessage = this.message.filter(filterMaskEnabled);
-+            playerChatMessage = unsigned != null ? playerChatMessage.withUnsignedContent(unsigned) : playerChatMessage; // Paper
-             if (!playerChatMessage.isFullyFiltered()) {
-                 sender.connection.sendPlayerChatMessage(playerChatMessage, params);
-             }
diff --git a/paper-server/patches/unapplied/net/minecraft/network/chat/TextColor.java.patch b/paper-server/patches/unapplied/net/minecraft/network/chat/TextColor.java.patch
deleted file mode 100644
index 014fd3eb10..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/network/chat/TextColor.java.patch
+++ /dev/null
@@ -1,37 +0,0 @@
---- a/net/minecraft/network/chat/TextColor.java
-+++ b/net/minecraft/network/chat/TextColor.java
-@@ -17,7 +17,7 @@
-     private static final String CUSTOM_COLOR_PREFIX = "#";
-     public static final Codec<TextColor> CODEC = Codec.STRING.comapFlatMap(TextColor::parseColor, TextColor::serialize);
-     private static final Map<ChatFormatting, TextColor> LEGACY_FORMAT_TO_COLOR = (Map) Stream.of(ChatFormatting.values()).filter(ChatFormatting::isColor).collect(ImmutableMap.toImmutableMap(Function.identity(), (enumchatformat) -> {
--        return new TextColor(enumchatformat.getColor(), enumchatformat.getName());
-+        return new TextColor(enumchatformat.getColor(), enumchatformat.getName(), enumchatformat); // CraftBukkit
-     }));
-     private static final Map<String, TextColor> NAMED_COLORS = (Map) TextColor.LEGACY_FORMAT_TO_COLOR.values().stream().collect(ImmutableMap.toImmutableMap((chathexcolor) -> {
-         return chathexcolor.name;
-@@ -25,16 +25,22 @@
-     private final int value;
-     @Nullable
-     public final String name;
-+    // CraftBukkit start
-+    @Nullable
-+    public final ChatFormatting format;
- 
--    private TextColor(int rgb, String name) {
--        this.value = rgb & 16777215;
--        this.name = name;
-+    private TextColor(int i, String s, ChatFormatting format) {
-+        this.value = i & 16777215;
-+        this.name = s;
-+        this.format = format;
-     }
- 
-     private TextColor(int rgb) {
-         this.value = rgb & 16777215;
-         this.name = null;
-+        this.format = null;
-     }
-+    // CraftBukkit end
- 
-     public int getValue() {
-         return this.value;
diff --git a/paper-server/patches/unapplied/net/minecraft/network/chat/contents/NbtContents.java.patch b/paper-server/patches/unapplied/net/minecraft/network/chat/contents/NbtContents.java.patch
deleted file mode 100644
index f30203349e..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/network/chat/contents/NbtContents.java.patch
+++ /dev/null
@@ -1,20 +0,0 @@
---- a/net/minecraft/network/chat/contents/NbtContents.java
-+++ b/net/minecraft/network/chat/contents/NbtContents.java
-@@ -120,7 +120,7 @@
-             }).map(Tag::getAsString);
-             if (this.interpreting) {
-                 Component component = DataFixUtils.orElse(
--                    ComponentUtils.updateForEntity(source, this.separator, sender, depth), ComponentUtils.DEFAULT_NO_STYLE_SEPARATOR
-+                    ComponentUtils.updateSeparatorForEntity(source, this.separator, sender, depth), ComponentUtils.DEFAULT_NO_STYLE_SEPARATOR // Paper - validate separator
-                 );
-                 return stream.flatMap(text -> {
-                     try {
-@@ -132,7 +132,7 @@
-                     }
-                 }).reduce((accumulator, current) -> accumulator.append(component).append(current)).orElseGet(Component::empty);
-             } else {
--                return ComponentUtils.updateForEntity(source, this.separator, sender, depth)
-+                return ComponentUtils.updateSeparatorForEntity(source, this.separator, sender, depth) // Paper - validate separator
-                     .map(
-                         text -> stream.map(Component::literal)
-                                 .reduce((accumulator, current) -> accumulator.append(text).append(current))
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/PacketUtils.java.patch b/paper-server/patches/unapplied/net/minecraft/network/protocol/PacketUtils.java.patch
deleted file mode 100644
index 67d49cd3f7..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/network/protocol/PacketUtils.java.patch
+++ /dev/null
@@ -1,27 +0,0 @@
---- a/net/minecraft/network/protocol/PacketUtils.java
-+++ b/net/minecraft/network/protocol/PacketUtils.java
-@@ -6,10 +6,15 @@
- import net.minecraft.CrashReportCategory;
- import net.minecraft.ReportedException;
- import net.minecraft.network.PacketListener;
-+import org.slf4j.Logger;
-+
-+// CraftBukkit start
-+import net.minecraft.server.MinecraftServer;
- import net.minecraft.server.RunningOnDifferentThreadException;
- import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.network.ServerCommonPacketListenerImpl;
-+// CraftBukkit end
- import net.minecraft.util.thread.BlockableEventLoop;
--import org.slf4j.Logger;
- 
- public class PacketUtils {
- 
-@@ -24,6 +29,7 @@
-     public static <T extends PacketListener> void ensureRunningOnSameThread(Packet<T> packet, T listener, BlockableEventLoop<?> engine) throws RunningOnDifferentThreadException {
-         if (!engine.isSameThread()) {
-             engine.executeIfPossible(() -> {
-+                if (listener instanceof ServerCommonPacketListenerImpl serverCommonPacketListener && serverCommonPacketListener.processedDisconnect) return; // CraftBukkit - Don't handle sync packets for kicked players
-                 if (listener.shouldHandleMessage(packet)) {
-                     try {
-                         packet.handle(listener);
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/common/ServerboundCustomPayloadPacket.java.patch b/paper-server/patches/unapplied/net/minecraft/network/protocol/common/ServerboundCustomPayloadPacket.java.patch
deleted file mode 100644
index e17dc6200b..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/network/protocol/common/ServerboundCustomPayloadPacket.java.patch
+++ /dev/null
@@ -1,20 +0,0 @@
---- a/net/minecraft/network/protocol/common/ServerboundCustomPayloadPacket.java
-+++ b/net/minecraft/network/protocol/common/ServerboundCustomPayloadPacket.java
-@@ -2,7 +2,6 @@
- 
- import com.google.common.collect.Lists;
- import java.util.List;
--import net.minecraft.Util;
- import net.minecraft.network.FriendlyByteBuf;
- import net.minecraft.network.codec.StreamCodec;
- import net.minecraft.network.protocol.Packet;
-@@ -16,8 +15,7 @@
-     private static final int MAX_PAYLOAD_SIZE = 32767;
-     public static final StreamCodec<FriendlyByteBuf, ServerboundCustomPayloadPacket> STREAM_CODEC = CustomPacketPayload.codec((minecraftkey) -> {
-         return DiscardedPayload.codec(minecraftkey, 32767);
--    }, (List) Util.make(Lists.newArrayList(new CustomPacketPayload.TypeAndCodec[]{new CustomPacketPayload.TypeAndCodec<>(BrandPayload.TYPE, BrandPayload.STREAM_CODEC)}), (arraylist) -> {
--    })).map(ServerboundCustomPayloadPacket::new, ServerboundCustomPayloadPacket::payload);
-+    }, java.util.Collections.emptyList()).map(ServerboundCustomPayloadPacket::new, ServerboundCustomPayloadPacket::payload); // CraftBukkit - treat all packets the same
- 
-     @Override
-     public PacketType<ServerboundCustomPayloadPacket> type() {
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSetEntityDataPacket.java.patch b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSetEntityDataPacket.java.patch
deleted file mode 100644
index 6a8d6c8ce8..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSetEntityDataPacket.java.patch
+++ /dev/null
@@ -1,14 +0,0 @@
---- a/net/minecraft/network/protocol/game/ClientboundSetEntityDataPacket.java
-+++ b/net/minecraft/network/protocol/game/ClientboundSetEntityDataPacket.java
-@@ -19,9 +19,11 @@
-     }
- 
-     private static void pack(List<SynchedEntityData.DataValue<?>> trackedValues, RegistryFriendlyByteBuf buf) {
-+        try (var ignored = io.papermc.paper.util.DataSanitizationUtil.start(true)) { // Paper - data sanitization
-         for (SynchedEntityData.DataValue<?> dataValue : trackedValues) {
-             dataValue.write(buf);
-         }
-+        } // Paper - data sanitization
- 
-         buf.writeByte(255);
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSetEquipmentPacket.java.patch b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSetEquipmentPacket.java.patch
deleted file mode 100644
index e13e6cd647..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSetEquipmentPacket.java.patch
+++ /dev/null
@@ -1,32 +0,0 @@
---- a/net/minecraft/network/protocol/game/ClientboundSetEquipmentPacket.java
-+++ b/net/minecraft/network/protocol/game/ClientboundSetEquipmentPacket.java
-@@ -19,6 +19,13 @@
-     private final List<Pair<EquipmentSlot, ItemStack>> slots;
- 
-     public ClientboundSetEquipmentPacket(int entityId, List<Pair<EquipmentSlot, ItemStack>> equipmentList) {
-+        // Paper start - data sanitization
-+        this(entityId, equipmentList, false);
-+    }
-+    private boolean sanitize;
-+    public ClientboundSetEquipmentPacket(int entityId, List<Pair<EquipmentSlot, ItemStack>> equipmentList, boolean sanitize) {
-+        this.sanitize = sanitize;
-+        // Paper end - data sanitization
-         this.entity = entityId;
-         this.slots = equipmentList;
-     }
-@@ -40,6 +47,7 @@
-         buf.writeVarInt(this.entity);
-         int i = this.slots.size();
- 
-+        try (var ignored = io.papermc.paper.util.DataSanitizationUtil.start(this.sanitize)) {  // Paper - data sanitization
-         for (int j = 0; j < i; j++) {
-             Pair<EquipmentSlot, ItemStack> pair = this.slots.get(j);
-             EquipmentSlot equipmentSlot = pair.getFirst();
-@@ -48,6 +56,7 @@
-             buf.writeByte(bl ? k | -128 : k);
-             ItemStack.OPTIONAL_STREAM_CODEC.encode(buf, pair.getSecond());
-         }
-+        } // Paper - data sanitization
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSetPlayerTeamPacket.java.patch b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSetPlayerTeamPacket.java.patch
deleted file mode 100644
index 1d4da793e7..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSetPlayerTeamPacket.java.patch
+++ /dev/null
@@ -1,23 +0,0 @@
---- a/net/minecraft/network/protocol/game/ClientboundSetPlayerTeamPacket.java
-+++ b/net/minecraft/network/protocol/game/ClientboundSetPlayerTeamPacket.java
-@@ -58,6 +58,11 @@
-         );
-     }
- 
-+    // Paper start - Multiple Entries with Scoreboards
-+    public static ClientboundSetPlayerTeamPacket createMultiplePlayerPacket(PlayerTeam team, Collection<String> players, ClientboundSetPlayerTeamPacket.Action operation) {
-+        return new ClientboundSetPlayerTeamPacket(team.getName(), operation == ClientboundSetPlayerTeamPacket.Action.ADD ? 3 : 4, Optional.empty(), players);
-+    }
-+    // Paper end - Multiple Entries with Scoreboards
-     private ClientboundSetPlayerTeamPacket(RegistryFriendlyByteBuf buf) {
-         this.name = buf.readUtf();
-         this.method = buf.readByte();
-@@ -200,7 +205,7 @@
-             ComponentSerialization.TRUSTED_STREAM_CODEC.encode(buf, this.displayName);
-             buf.writeByte(this.options);
-             buf.writeUtf(this.nametagVisibility);
--            buf.writeUtf(this.collisionRule);
-+            buf.writeUtf(!io.papermc.paper.configuration.GlobalConfiguration.get().collisions.enablePlayerCollisions ? "never" : this.collisionRule); // Paper - Configurable player collision
-             buf.writeEnum(this.color);
-             ComponentSerialization.TRUSTED_STREAM_CODEC.encode(buf, this.playerPrefix);
-             ComponentSerialization.TRUSTED_STREAM_CODEC.encode(buf, this.playerSuffix);
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/handshake/ClientIntentionPacket.java.patch b/paper-server/patches/unapplied/net/minecraft/network/protocol/handshake/ClientIntentionPacket.java.patch
deleted file mode 100644
index 071ea04212..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/network/protocol/handshake/ClientIntentionPacket.java.patch
+++ /dev/null
@@ -1,17 +0,0 @@
---- a/net/minecraft/network/protocol/handshake/ClientIntentionPacket.java
-+++ b/net/minecraft/network/protocol/handshake/ClientIntentionPacket.java
-@@ -1,3 +1,4 @@
-+// mc-dev import
- package net.minecraft.network.protocol.handshake;
- 
- import net.minecraft.network.FriendlyByteBuf;
-@@ -11,7 +12,8 @@
-     private static final int MAX_HOST_LENGTH = 255;
- 
-     private ClientIntentionPacket(FriendlyByteBuf buf) {
--        this(buf.readVarInt(), buf.readUtf(255), buf.readUnsignedShort(), ClientIntent.byId(buf.readVarInt()));
-+        // Spigot - increase max hostName length
-+        this(buf.readVarInt(), buf.readUtf(Short.MAX_VALUE), buf.readUnsignedShort(), ClientIntent.byId(buf.readVarInt()));
-     }
- 
-     private void write(FriendlyByteBuf buf) {
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/login/ClientboundLoginDisconnectPacket.java.patch b/paper-server/patches/unapplied/net/minecraft/network/protocol/login/ClientboundLoginDisconnectPacket.java.patch
deleted file mode 100644
index 7ad1ef4a1c..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/network/protocol/login/ClientboundLoginDisconnectPacket.java.patch
+++ /dev/null
@@ -1,21 +0,0 @@
---- a/net/minecraft/network/protocol/login/ClientboundLoginDisconnectPacket.java
-+++ b/net/minecraft/network/protocol/login/ClientboundLoginDisconnectPacket.java
-@@ -18,11 +18,16 @@
-     }
- 
-     private ClientboundLoginDisconnectPacket(FriendlyByteBuf buf) {
--        this.reason = Component.Serializer.fromJsonLenient(buf.readUtf(262144), RegistryAccess.EMPTY);
-+        this.reason = Component.Serializer.fromJsonLenient(buf.readUtf(FriendlyByteBuf.MAX_COMPONENT_STRING_LENGTH), RegistryAccess.EMPTY); // Paper - diff on change
-     }
- 
-     private void write(FriendlyByteBuf buf) {
--        buf.writeUtf(Component.Serializer.toJson(this.reason, RegistryAccess.EMPTY));
-+        // Paper start - Adventure
-+        // buf.writeUtf(Component.Serializer.toJson(this.reason, RegistryAccess.EMPTY));
-+        // In the login phase, buf.adventure$locale field is most likely null, but plugins may use internals to set it via the channel attribute
-+        java.util.Locale bufLocale = buf.adventure$locale;
-+        buf.writeJsonWithCodec(net.minecraft.network.chat.ComponentSerialization.localizedCodec(bufLocale == null ? java.util.Locale.US : bufLocale), this.reason, FriendlyByteBuf.MAX_COMPONENT_STRING_LENGTH);
-+        // Paper end - Adventure
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/resources/RegistryDataLoader.java.patch b/paper-server/patches/unapplied/net/minecraft/resources/RegistryDataLoader.java.patch
deleted file mode 100644
index 41773a3732..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/resources/RegistryDataLoader.java.patch
+++ /dev/null
@@ -1,79 +0,0 @@
---- a/net/minecraft/resources/RegistryDataLoader.java
-+++ b/net/minecraft/resources/RegistryDataLoader.java
-@@ -74,7 +74,7 @@
- 
- public class RegistryDataLoader {
-     private static final Logger LOGGER = LogUtils.getLogger();
--    private static final Comparator<ResourceKey<?>> ERROR_KEY_COMPARATOR = Comparator.comparing(ResourceKey::registry).thenComparing(ResourceKey::location);
-+    private static final Comparator<ResourceKey<?>> ERROR_KEY_COMPARATOR = Comparator.<ResourceKey<?>, ResourceLocation>comparing(ResourceKey::registry).thenComparing(ResourceKey::location); // Paper - decompile fix
-     private static final RegistrationInfo NETWORK_REGISTRATION_INFO = new RegistrationInfo(Optional.empty(), Lifecycle.experimental());
-     private static final Function<Optional<KnownPack>, RegistrationInfo> REGISTRATION_INFO_CACHE = Util.memoize(knownPacks -> {
-         Lifecycle lifecycle = knownPacks.map(KnownPack::isVanilla).map(vanilla -> Lifecycle.stable()).orElse(Lifecycle.experimental());
-@@ -238,13 +238,13 @@
-     }
- 
-     private static <E> void loadElementFromResource(
--        WritableRegistry<E> registry, Decoder<E> decoder, RegistryOps<JsonElement> ops, ResourceKey<E> key, Resource resource, RegistrationInfo entryInfo
-+        WritableRegistry<E> registry, Decoder<E> decoder, RegistryOps<JsonElement> ops, ResourceKey<E> key, Resource resource, RegistrationInfo entryInfo, io.papermc.paper.registry.data.util.Conversions conversions // Paper - pass conversions
-     ) throws IOException {
-         try (Reader reader = resource.openAsReader()) {
-             JsonElement jsonElement = JsonParser.parseReader(reader);
-             DataResult<E> 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
-         }
-     }
- 
-@@ -258,6 +258,7 @@
-         FileToIdConverter fileToIdConverter = FileToIdConverter.registry(registry.key());
-         RegistryOps<JsonElement> registryOps = RegistryOps.create(JsonOps.INSTANCE, infoGetter);
- 
-+        final io.papermc.paper.registry.data.util.Conversions conversions = new io.papermc.paper.registry.data.util.Conversions(infoGetter); // Paper - create conversions
-         for (Entry<ResourceLocation, Resource> entry : fileToIdConverter.listMatchingResources(resourceManager).entrySet()) {
-             ResourceLocation resourceLocation = entry.getKey();
-             ResourceKey<E> resourceKey = ResourceKey.create(registry.key(), fileToIdConverter.fileToId(resourceLocation));
-@@ -265,7 +266,7 @@
-             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 var14) {
-                 errors.put(
-                     resourceKey,
-@@ -274,7 +275,8 @@
-             }
-         }
- 
--        TagLoader.loadTagsForRegistry(resourceManager, registry);
-+        io.papermc.paper.registry.PaperRegistryListenerManager.INSTANCE.runFreezeListeners(registry.key(), conversions); // Paper - run pre-freeze listeners
-+        TagLoader.loadTagsForRegistry(resourceManager, registry, io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent.Cause.INITIAL); // Paper - tag lifecycle - add cause
-     }
- 
-     static <E> void loadContentsFromNetwork(
-@@ -291,6 +293,7 @@
-             RegistryOps<JsonElement> registryOps2 = RegistryOps.create(JsonOps.INSTANCE, infoGetter);
-             FileToIdConverter fileToIdConverter = FileToIdConverter.registry(registry.key());
- 
-+            final io.papermc.paper.registry.data.util.Conversions conversions = new io.papermc.paper.registry.data.util.Conversions(infoGetter); // Paper - create conversions
-             for (RegistrySynchronization.PackedRegistryEntry packedRegistryEntry : networkedRegistryData.elements) {
-                 ResourceKey<E> resourceKey = ResourceKey.create(registry.key(), packedRegistryEntry.id());
-                 Optional<Tag> optional = packedRegistryEntry.data();
-@@ -309,7 +312,7 @@
- 
-                     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 var17) {
-                         loadingErrors.put(resourceKey, new IllegalStateException("Failed to parse local data", var17));
-                     }
-@@ -349,6 +352,7 @@
- 
-         RegistryDataLoader.Loader<T> create(Lifecycle lifecycle, Map<ResourceKey<?>, Exception> errors) {
-             WritableRegistry<T> writableRegistry = new MappedRegistry<>(this.key, lifecycle);
-+            io.papermc.paper.registry.PaperRegistryAccess.instance().registerRegistry(this.key, writableRegistry); // Paper - initialize API registry
-             return new RegistryDataLoader.Loader<>(this, writableRegistry, errors);
-         }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/server/Bootstrap.java.patch b/paper-server/patches/unapplied/net/minecraft/server/Bootstrap.java.patch
deleted file mode 100644
index efce551cae..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/Bootstrap.java.patch
+++ /dev/null
@@ -1,129 +0,0 @@
---- a/net/minecraft/server/Bootstrap.java
-+++ b/net/minecraft/server/Bootstrap.java
-@@ -17,6 +17,9 @@
- import net.minecraft.core.dispenser.DispenseItemBehavior;
- import net.minecraft.core.registries.BuiltInRegistries;
- import net.minecraft.locale.Language;
-+import net.minecraft.util.datafix.fixes.BlockStateData;
-+import net.minecraft.util.datafix.fixes.ItemIdFix;
-+import net.minecraft.util.datafix.fixes.ItemSpawnEggFix;
- import net.minecraft.world.effect.MobEffect;
- import net.minecraft.world.entity.EntityType;
- import net.minecraft.world.entity.ai.attributes.Attribute;
-@@ -30,7 +33,8 @@
- import net.minecraft.world.level.block.state.BlockBehaviour;
- import org.slf4j.Logger;
- 
--@SuppressForbidden(a = "System.out setup")
-+@SuppressForbidden(reason = "System.out setup")
-+// CraftBukkit end
- public class Bootstrap {
- 
-     public static final PrintStream STDOUT = System.out;
-@@ -42,9 +46,27 @@
- 
-     public static void bootStrap() {
-         if (!Bootstrap.isBootstrapped) {
-+            // CraftBukkit start
-+            /*String name = Bootstrap.class.getSimpleName(); // Paper
-+            switch (name) {
-+                case "DispenserRegistry":
-+                    break;
-+                case "Bootstrap":
-+                    System.err.println("***************************************************************************");
-+                    System.err.println("*** WARNING: This server jar may only be used for development purposes. ***");
-+                    System.err.println("***************************************************************************");
-+                    break;
-+                default:
-+                    System.err.println("**********************************************************************");
-+                    System.err.println("*** WARNING: This server jar is unsupported, use at your own risk. ***");
-+                    System.err.println("**********************************************************************");
-+                    break;
-+            }*/ // Paper
-+            // CraftBukkit end
-             Bootstrap.isBootstrapped = true;
-             Instant instant = Instant.now();
- 
-+            io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler.enterBootstrappers(); // Paper - Entrypoint for bootstrapping
-             if (BuiltInRegistries.REGISTRY.keySet().isEmpty()) {
-                 throw new IllegalStateException("Unable to load registries");
-             } else {
-@@ -56,11 +78,77 @@
-                     EntitySelectorOptions.bootStrap();
-                     DispenseItemBehavior.bootStrap();
-                     CauldronInteraction.bootStrap();
--                    BuiltInRegistries.bootStrap();
-+                    // Paper start
-+                    BuiltInRegistries.bootStrap(() -> {
-+                    });
-+                    // Paper end
-                     CreativeModeTabs.validate();
-                     Bootstrap.wrapStreams();
-                     Bootstrap.bootstrapDuration.set(Duration.between(instant, Instant.now()).toMillis());
-                 }
-+                // CraftBukkit start - easier than fixing the decompile
-+                BlockStateData.register(1008, "{Name:'minecraft:oak_sign',Properties:{rotation:'0'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'0'}}");
-+                BlockStateData.register(1009, "{Name:'minecraft:oak_sign',Properties:{rotation:'1'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'1'}}");
-+                BlockStateData.register(1010, "{Name:'minecraft:oak_sign',Properties:{rotation:'2'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'2'}}");
-+                BlockStateData.register(1011, "{Name:'minecraft:oak_sign',Properties:{rotation:'3'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'3'}}");
-+                BlockStateData.register(1012, "{Name:'minecraft:oak_sign',Properties:{rotation:'4'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'4'}}");
-+                BlockStateData.register(1013, "{Name:'minecraft:oak_sign',Properties:{rotation:'5'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'5'}}");
-+                BlockStateData.register(1014, "{Name:'minecraft:oak_sign',Properties:{rotation:'6'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'6'}}");
-+                BlockStateData.register(1015, "{Name:'minecraft:oak_sign',Properties:{rotation:'7'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'7'}}");
-+                BlockStateData.register(1016, "{Name:'minecraft:oak_sign',Properties:{rotation:'8'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'8'}}");
-+                BlockStateData.register(1017, "{Name:'minecraft:oak_sign',Properties:{rotation:'9'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'9'}}");
-+                BlockStateData.register(1018, "{Name:'minecraft:oak_sign',Properties:{rotation:'10'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'10'}}");
-+                BlockStateData.register(1019, "{Name:'minecraft:oak_sign',Properties:{rotation:'11'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'11'}}");
-+                BlockStateData.register(1020, "{Name:'minecraft:oak_sign',Properties:{rotation:'12'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'12'}}");
-+                BlockStateData.register(1021, "{Name:'minecraft:oak_sign',Properties:{rotation:'13'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'13'}}");
-+                BlockStateData.register(1022, "{Name:'minecraft:oak_sign',Properties:{rotation:'14'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'14'}}");
-+                BlockStateData.register(1023, "{Name:'minecraft:oak_sign',Properties:{rotation:'15'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'15'}}");
-+                ItemIdFix.ITEM_NAMES.put(323, "minecraft:oak_sign");
-+
-+                BlockStateData.register(1440, "{Name:\'minecraft:portal\',Properties:{axis:\'x\'}}", new String[]{"{Name:\'minecraft:portal\',Properties:{axis:\'x\'}}"});
-+
-+                ItemIdFix.ITEM_NAMES.put(409, "minecraft:prismarine_shard");
-+                ItemIdFix.ITEM_NAMES.put(410, "minecraft:prismarine_crystals");
-+                ItemIdFix.ITEM_NAMES.put(411, "minecraft:rabbit");
-+                ItemIdFix.ITEM_NAMES.put(412, "minecraft:cooked_rabbit");
-+                ItemIdFix.ITEM_NAMES.put(413, "minecraft:rabbit_stew");
-+                ItemIdFix.ITEM_NAMES.put(414, "minecraft:rabbit_foot");
-+                ItemIdFix.ITEM_NAMES.put(415, "minecraft:rabbit_hide");
-+                ItemIdFix.ITEM_NAMES.put(416, "minecraft:armor_stand");
-+
-+                ItemIdFix.ITEM_NAMES.put(423, "minecraft:mutton");
-+                ItemIdFix.ITEM_NAMES.put(424, "minecraft:cooked_mutton");
-+                ItemIdFix.ITEM_NAMES.put(425, "minecraft:banner");
-+                ItemIdFix.ITEM_NAMES.put(426, "minecraft:end_crystal");
-+                ItemIdFix.ITEM_NAMES.put(427, "minecraft:spruce_door");
-+                ItemIdFix.ITEM_NAMES.put(428, "minecraft:birch_door");
-+                ItemIdFix.ITEM_NAMES.put(429, "minecraft:jungle_door");
-+                ItemIdFix.ITEM_NAMES.put(430, "minecraft:acacia_door");
-+                ItemIdFix.ITEM_NAMES.put(431, "minecraft:dark_oak_door");
-+                ItemIdFix.ITEM_NAMES.put(432, "minecraft:chorus_fruit");
-+                ItemIdFix.ITEM_NAMES.put(433, "minecraft:chorus_fruit_popped");
-+                ItemIdFix.ITEM_NAMES.put(434, "minecraft:beetroot");
-+                ItemIdFix.ITEM_NAMES.put(435, "minecraft:beetroot_seeds");
-+                ItemIdFix.ITEM_NAMES.put(436, "minecraft:beetroot_soup");
-+                ItemIdFix.ITEM_NAMES.put(437, "minecraft:dragon_breath");
-+                ItemIdFix.ITEM_NAMES.put(438, "minecraft:splash_potion");
-+                ItemIdFix.ITEM_NAMES.put(439, "minecraft:spectral_arrow");
-+                ItemIdFix.ITEM_NAMES.put(440, "minecraft:tipped_arrow");
-+                ItemIdFix.ITEM_NAMES.put(441, "minecraft:lingering_potion");
-+                ItemIdFix.ITEM_NAMES.put(442, "minecraft:shield");
-+                ItemIdFix.ITEM_NAMES.put(443, "minecraft:elytra");
-+                ItemIdFix.ITEM_NAMES.put(444, "minecraft:spruce_boat");
-+                ItemIdFix.ITEM_NAMES.put(445, "minecraft:birch_boat");
-+                ItemIdFix.ITEM_NAMES.put(446, "minecraft:jungle_boat");
-+                ItemIdFix.ITEM_NAMES.put(447, "minecraft:acacia_boat");
-+                ItemIdFix.ITEM_NAMES.put(448, "minecraft:dark_oak_boat");
-+                ItemIdFix.ITEM_NAMES.put(449, "minecraft:totem_of_undying");
-+                ItemIdFix.ITEM_NAMES.put(450, "minecraft:shulker_shell");
-+                ItemIdFix.ITEM_NAMES.put(452, "minecraft:iron_nugget");
-+                ItemIdFix.ITEM_NAMES.put(453, "minecraft:knowledge_book");
-+
-+                ItemSpawnEggFix.ID_TO_ENTITY[23] = "Arrow";
-+                // CraftBukkit end
-             }
-         }
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/Main.java.patch b/paper-server/patches/unapplied/net/minecraft/server/Main.java.patch
deleted file mode 100644
index a9ae49e37f..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/Main.java.patch
+++ /dev/null
@@ -1,290 +0,0 @@
---- a/net/minecraft/server/Main.java
-+++ b/net/minecraft/server/Main.java
-@@ -38,6 +38,7 @@
- import net.minecraft.server.dedicated.DedicatedServerProperties;
- import net.minecraft.server.dedicated.DedicatedServerSettings;
- import net.minecraft.server.level.progress.LoggerChunkProgressListener;
-+import net.minecraft.server.packs.PackType;
- import net.minecraft.server.packs.repository.PackRepository;
- import net.minecraft.server.packs.repository.ServerPacksSource;
- import net.minecraft.util.Mth;
-@@ -55,22 +56,31 @@
- import net.minecraft.world.level.levelgen.WorldOptions;
- import net.minecraft.world.level.levelgen.presets.WorldPresets;
- import net.minecraft.world.level.storage.LevelDataAndDimensions;
-+import net.minecraft.world.level.storage.LevelResource;
- import net.minecraft.world.level.storage.LevelStorageSource;
- import net.minecraft.world.level.storage.LevelSummary;
- import net.minecraft.world.level.storage.PrimaryLevelData;
--import net.minecraft.world.level.storage.WorldData;
- import org.slf4j.Logger;
- 
-+// CraftBukkit start
-+import com.google.common.base.Charsets;
-+import java.io.InputStreamReader;
-+import java.util.concurrent.atomic.AtomicReference;
-+import net.minecraft.SharedConstants;
-+import org.bukkit.configuration.file.YamlConfiguration;
-+// CraftBukkit end
-+
- public class Main {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
- 
-     public Main() {}
- 
--    @SuppressForbidden(a = "System.out needed before bootstrap")
-+    @SuppressForbidden(reason = "System.out needed before bootstrap") // CraftBukkit - decompile error
-     @DontObfuscate
--    public static void main(String[] args) {
-+    public static void main(final OptionSet optionset) { // CraftBukkit - replaces main(String[] astring)
-         SharedConstants.tryDetectVersion();
-+        /* CraftBukkit start - Replace everything
-         OptionParser optionparser = new OptionParser();
-         OptionSpec<Void> optionspec = optionparser.accepts("nogui");
-         OptionSpec<Void> optionspec1 = optionparser.accepts("initSettings", "Initializes 'server.properties' and 'eula.txt', then quits");
-@@ -90,50 +100,104 @@
-         OptionSpec<String> optionspec15 = optionparser.nonOptions();
- 
-         try {
--            OptionSet optionset = optionparser.parse(args);
-+            OptionSet optionset = optionparser.parse(astring);
- 
-             if (optionset.has(optionspec8)) {
-                 optionparser.printHelpOn(System.err);
-                 return;
-             }
-+            */ // CraftBukkit end
- 
--            Path path = (Path) optionset.valueOf(optionspec14);
-+        try {
- 
-+            Path path = (Path) optionset.valueOf("pidFile"); // CraftBukkit
-+
-             if (path != null) {
-                 Main.writePidFile(path);
-             }
- 
-             CrashReport.preload();
--            if (optionset.has(optionspec13)) {
-+            if (optionset.has("jfrProfile")) { // CraftBukkit
-                 JvmProfiler.INSTANCE.start(Environment.SERVER);
-             }
- 
-+            io.papermc.paper.plugin.PluginInitializerManager.load(optionset); // Paper
-             Bootstrap.bootStrap();
-             Bootstrap.validate();
-             Util.startTimerHackThread();
-             Path path1 = Paths.get("server.properties");
--            DedicatedServerSettings dedicatedserversettings = new DedicatedServerSettings(path1);
-+            DedicatedServerSettings dedicatedserversettings = new DedicatedServerSettings(optionset); // CraftBukkit - CLI argument support
- 
-             dedicatedserversettings.forceSave();
-             RegionFileVersion.configure(dedicatedserversettings.getProperties().regionFileComression);
-             Path path2 = Paths.get("eula.txt");
-             Eula eula = new Eula(path2);
-+            // Paper start - load config files early for access below if needed
-+            org.bukkit.configuration.file.YamlConfiguration bukkitConfiguration = io.papermc.paper.configuration.PaperConfigurations.loadLegacyConfigFile((File) optionset.valueOf("bukkit-settings"));
-+            org.bukkit.configuration.file.YamlConfiguration spigotConfiguration = io.papermc.paper.configuration.PaperConfigurations.loadLegacyConfigFile((File) optionset.valueOf("spigot-settings"));
-+            // Paper end - load config files early for access below if needed
- 
--            if (optionset.has(optionspec1)) {
-+            if (optionset.has("initSettings")) { // CraftBukkit
-+                // CraftBukkit start - SPIGOT-5761: Create bukkit.yml and commands.yml if not present
-+                File configFile = (File) optionset.valueOf("bukkit-settings");
-+                YamlConfiguration configuration = YamlConfiguration.loadConfiguration(configFile);
-+                configuration.options().copyDefaults(true);
-+                configuration.setDefaults(YamlConfiguration.loadConfiguration(new InputStreamReader(Main.class.getClassLoader().getResourceAsStream("configurations/bukkit.yml"), Charsets.UTF_8)));
-+                configuration.save(configFile);
-+
-+                File commandFile = (File) optionset.valueOf("commands-settings");
-+                YamlConfiguration commandsConfiguration = YamlConfiguration.loadConfiguration(commandFile);
-+                commandsConfiguration.options().copyDefaults(true);
-+                commandsConfiguration.setDefaults(YamlConfiguration.loadConfiguration(new InputStreamReader(Main.class.getClassLoader().getResourceAsStream("configurations/commands.yml"), Charsets.UTF_8)));
-+                commandsConfiguration.save(commandFile);
-+                // CraftBukkit end
-                 Main.LOGGER.info("Initialized '{}' and '{}'", path1.toAbsolutePath(), path2.toAbsolutePath());
-                 return;
-             }
- 
--            if (!eula.hasAgreedToEULA()) {
-+            // Spigot Start
-+            boolean eulaAgreed = Boolean.getBoolean( "com.mojang.eula.agree" );
-+            if ( eulaAgreed )
-+            {
-+                System.err.println( "You have used the Spigot command line EULA agreement flag." );
-+                System.err.println( "By using this setting you are indicating your agreement to Mojang's EULA (https://account.mojang.com/documents/minecraft_eula)." );
-+                System.err.println( "If you do not agree to the above EULA please stop your server and remove this flag immediately." );
-+            }
-+            // Spigot End
-+            if (!eula.hasAgreedToEULA() && !eulaAgreed) { // Spigot
-                 Main.LOGGER.info("You need to agree to the EULA in order to run the server. Go to eula.txt for more info.");
-                 return;
-             }
- 
--            File file = new File((String) optionset.valueOf(optionspec9));
--            Services services = Services.create(new YggdrasilAuthenticationService(Proxy.NO_PROXY), file);
--            String s = (String) Optional.ofNullable((String) optionset.valueOf(optionspec10)).orElse(dedicatedserversettings.getProperties().levelName);
-+            // Paper start - Detect headless JRE
-+            String awtException = io.papermc.paper.util.ServerEnvironment.awtDependencyCheck();
-+            if (awtException != null) {
-+                Main.LOGGER.error("You are using a headless JRE distribution.");
-+                Main.LOGGER.error("This distribution is missing certain graphic libraries that the Minecraft server needs to function.");
-+                Main.LOGGER.error("For instructions on how to install the non-headless JRE, see https://docs.papermc.io/misc/java-install");
-+                Main.LOGGER.error("");
-+                Main.LOGGER.error(awtException);
-+                return;
-+            }
-+            // Paper end - Detect headless JRE
-+
-+            org.spigotmc.SpigotConfig.disabledAdvancements = spigotConfiguration.getStringList("advancements.disabled"); // Paper - fix SPIGOT-5885, must be set early in init
-+            // Paper start - fix SPIGOT-5824
-+            File file;
-+            File userCacheFile = new File(Services.USERID_CACHE_FILE);
-+            if (optionset.has("universe")) {
-+                file = (File) optionset.valueOf("universe"); // CraftBukkit
-+                userCacheFile = new File(file, Services.USERID_CACHE_FILE);
-+            } else {
-+                file = new File(bukkitConfiguration.getString("settings.world-container", "."));
-+            }
-+            // Paper end - fix SPIGOT-5824
-+            Services services = Services.create(new com.destroystokyo.paper.profile.PaperAuthenticationService(Proxy.NO_PROXY), file, userCacheFile, optionset); // Paper - pass OptionSet to load paper config files; override authentication service; fix world-container
-+            // CraftBukkit start
-+            String s = (String) Optional.ofNullable((String) optionset.valueOf("world")).orElse(dedicatedserversettings.getProperties().levelName);
-             LevelStorageSource convertable = LevelStorageSource.createDefault(file.toPath());
--            LevelStorageSource.LevelStorageAccess convertable_conversionsession = convertable.validateAndCreateAccess(s);
-+            LevelStorageSource.LevelStorageAccess convertable_conversionsession = convertable.validateAndCreateAccess(s, LevelStem.OVERWORLD);
-+            // CraftBukkit end
-             Dynamic dynamic;
- 
-             if (convertable_conversionsession.hasWorldData()) {
-@@ -174,13 +238,31 @@
-             }
- 
-             Dynamic<?> dynamic1 = dynamic;
--            boolean flag = optionset.has(optionspec7);
-+            boolean flag = optionset.has("safeMode"); // CraftBukkit
- 
-             if (flag) {
-                 Main.LOGGER.warn("Safe mode active, only vanilla datapack will be loaded");
-             }
- 
-             PackRepository resourcepackrepository = ServerPacksSource.createPackRepository(convertable_conversionsession);
-+            // CraftBukkit start
-+            File bukkitDataPackFolder = new File(convertable_conversionsession.getLevelPath(LevelResource.DATAPACK_DIR).toFile(), "bukkit");
-+            if (!bukkitDataPackFolder.exists()) {
-+                bukkitDataPackFolder.mkdirs();
-+            }
-+            File mcMeta = new File(bukkitDataPackFolder, "pack.mcmeta");
-+            try {
-+                com.google.common.io.Files.write("{\n"
-+                        + "    \"pack\": {\n"
-+                        + "        \"description\": \"Data pack for resources provided by Bukkit plugins\",\n"
-+                        + "        \"pack_format\": " + SharedConstants.getCurrentVersion().getPackVersion(PackType.SERVER_DATA) + "\n"
-+                        + "    }\n"
-+                        + "}\n", mcMeta, com.google.common.base.Charsets.UTF_8);
-+            } catch (java.io.IOException ex) {
-+                throw new RuntimeException("Could not initialize Bukkit datapack", ex);
-+            }
-+            AtomicReference<WorldLoader.DataLoadContext> worldLoader = new AtomicReference<>();
-+            // CraftBukkit end
- 
-             WorldStem worldstem;
- 
-@@ -189,6 +271,7 @@
- 
-                 worldstem = (WorldStem) Util.blockUntilDone((executor) -> {
-                     return WorldLoader.load(worldloader_c, (worldloader_a) -> {
-+                        worldLoader.set(worldloader_a); // CraftBukkit
-                         Registry<LevelStem> iregistry = worldloader_a.datapackDimensions().lookupOrThrow(Registries.LEVEL_STEM);
- 
-                         if (dynamic1 != null) {
-@@ -201,7 +284,7 @@
-                             WorldOptions worldoptions;
-                             WorldDimensions worlddimensions;
- 
--                            if (optionset.has(optionspec2)) {
-+                            if (optionset.has("demo")) { // CraftBukkit
-                                 worldsettings = MinecraftServer.DEMO_SETTINGS;
-                                 worldoptions = WorldOptions.DEMO_OPTIONS;
-                                 worlddimensions = WorldPresets.createNormalWorldDimensions(worldloader_a.datapackWorldgen());
-@@ -209,7 +292,7 @@
-                                 DedicatedServerProperties dedicatedserverproperties = dedicatedserversettings.getProperties();
- 
-                                 worldsettings = new LevelSettings(dedicatedserverproperties.levelName, dedicatedserverproperties.gamemode, dedicatedserverproperties.hardcore, dedicatedserverproperties.difficulty, false, new GameRules(worldloader_a.dataConfiguration().enabledFeatures()), worldloader_a.dataConfiguration());
--                                worldoptions = optionset.has(optionspec3) ? dedicatedserverproperties.worldOptions.withBonusChest(true) : dedicatedserverproperties.worldOptions;
-+                                worldoptions = optionset.has("bonusChest") ? dedicatedserverproperties.worldOptions.withBonusChest(true) : dedicatedserverproperties.worldOptions; // CraftBukkit
-                                 worlddimensions = dedicatedserverproperties.createDimensions(worldloader_a.datapackWorldgen());
-                             }
- 
-@@ -225,32 +308,47 @@
-                 return;
-             }
- 
--            RegistryAccess.Frozen iregistrycustom_dimension = worldstem.registries().compositeAccess();
-+            /*
-+            IRegistryCustom.Dimension iregistrycustom_dimension = worldstem.registries().compositeAccess();
-             boolean flag1 = optionset.has(optionspec6);
- 
-             if (optionset.has(optionspec4) || flag1) {
--                Main.forceUpgrade(convertable_conversionsession, DataFixers.getDataFixer(), optionset.has(optionspec5), () -> {
-+                forceUpgrade(convertable_conversionsession, DataConverterRegistry.getDataFixer(), optionset.has(optionspec5), () -> {
-                     return true;
-                 }, iregistrycustom_dimension, flag1);
-             }
- 
--            WorldData savedata = worldstem.worldData();
-+            SaveData savedata = worldstem.worldData();
- 
-             convertable_conversionsession.saveDataTag(iregistrycustom_dimension, savedata);
-+            */
-             final DedicatedServer dedicatedserver = (DedicatedServer) MinecraftServer.spin((thread) -> {
--                DedicatedServer dedicatedserver1 = new DedicatedServer(thread, convertable_conversionsession, resourcepackrepository, worldstem, dedicatedserversettings, DataFixers.getDataFixer(), services, LoggerChunkProgressListener::createFromGameruleRadius);
-+                DedicatedServer dedicatedserver1 = new DedicatedServer(optionset, worldLoader.get(), thread, convertable_conversionsession, resourcepackrepository, worldstem, dedicatedserversettings, DataFixers.getDataFixer(), services, LoggerChunkProgressListener::createFromGameruleRadius);
- 
-+                /*
-                 dedicatedserver1.setPort((Integer) optionset.valueOf(optionspec11));
--                dedicatedserver1.setDemo(optionset.has(optionspec2));
-+                */
-+                dedicatedserver1.setDemo(optionset.has("demo")); // Paper
-+                /*
-                 dedicatedserver1.setId((String) optionset.valueOf(optionspec12));
--                boolean flag2 = !optionset.has(optionspec) && !optionset.valuesOf(optionspec15).contains("nogui");
-+                */
-+                boolean flag2 = !optionset.has("nogui") && !optionset.nonOptionArguments().contains("nogui");
- 
-+                if(!Boolean.parseBoolean(System.getenv().getOrDefault("PAPER_DISABLE_SERVER_GUI", String.valueOf(false)))) // Paper - Add environment variable to disable server gui
-                 if (flag2 && !GraphicsEnvironment.isHeadless()) {
-                     dedicatedserver1.showGui();
-                 }
- 
-+                if (optionset.has("port")) {
-+                    int port = (Integer) optionset.valueOf("port");
-+                    if (port > 0) {
-+                        dedicatedserver1.setPort(port);
-+                    }
-+                }
-+
-                 return dedicatedserver1;
-             });
-+            /* CraftBukkit start
-             Thread thread = new Thread("Server Shutdown Thread") {
-                 public void run() {
-                     dedicatedserver.halt(true);
-@@ -259,6 +357,7 @@
- 
-             thread.setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandler(Main.LOGGER));
-             Runtime.getRuntime().addShutdownHook(thread);
-+            */ // CraftBukkit end
-         } catch (Exception exception1) {
-             Main.LOGGER.error(LogUtils.FATAL_MARKER, "Failed to start the minecraft server", exception1);
-         }
-@@ -295,7 +394,7 @@
-     }
- 
-     public static void forceUpgrade(LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, boolean eraseCache, BooleanSupplier continueCheck, RegistryAccess dynamicRegistryManager, boolean recreateRegionFiles) {
--        Main.LOGGER.info("Forcing world upgrade!");
-+        Main.LOGGER.info("Forcing world upgrade! {}", session.getLevelId()); // CraftBukkit
-         WorldUpgrader worldupgrader = new WorldUpgrader(session, dataFixer, dynamicRegistryManager, eraseCache, recreateRegionFiles);
- 
-         try {
diff --git a/paper-server/patches/unapplied/net/minecraft/server/MinecraftServer.java.patch b/paper-server/patches/unapplied/net/minecraft/server/MinecraftServer.java.patch
deleted file mode 100644
index b2aec783a8..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/MinecraftServer.java.patch
+++ /dev/null
@@ -1,1501 +0,0 @@
---- a/net/minecraft/server/MinecraftServer.java
-+++ b/net/minecraft/server/MinecraftServer.java
-@@ -3,6 +3,9 @@
- import com.google.common.base.Preconditions;
- import com.google.common.base.Splitter;
- import com.google.common.collect.ImmutableList;
-+import co.aikar.timings.Timings;
-+import com.destroystokyo.paper.event.server.PaperServerListPingEvent;
-+import com.google.common.base.Stopwatch;
- import com.google.common.collect.Lists;
- import com.google.common.collect.Maps;
- import com.google.common.collect.Sets;
-@@ -45,7 +48,6 @@
- import java.util.UUID;
- import java.util.concurrent.CompletableFuture;
- import java.util.concurrent.Executor;
--import java.util.concurrent.RejectedExecutionException;
- import java.util.concurrent.atomic.AtomicReference;
- import java.util.concurrent.locks.LockSupport;
- import java.util.function.BooleanSupplier;
-@@ -84,17 +86,6 @@
- import net.minecraft.obfuscate.DontObfuscate;
- import net.minecraft.resources.ResourceKey;
- import net.minecraft.resources.ResourceLocation;
--import net.minecraft.server.bossevents.CustomBossEvents;
--import net.minecraft.server.level.DemoMode;
--import net.minecraft.server.level.PlayerRespawnLogic;
--import net.minecraft.server.level.ServerChunkCache;
--import net.minecraft.server.level.ServerLevel;
--import net.minecraft.server.level.ServerPlayer;
--import net.minecraft.server.level.ServerPlayerGameMode;
--import net.minecraft.server.level.progress.ChunkProgressListener;
--import net.minecraft.server.level.progress.ChunkProgressListenerFactory;
--import net.minecraft.server.network.ServerConnectionListener;
--import net.minecraft.server.network.TextFilter;
- import net.minecraft.server.packs.PackType;
- import net.minecraft.server.packs.repository.Pack;
- import net.minecraft.server.packs.repository.PackRepository;
-@@ -116,6 +107,7 @@
- import net.minecraft.util.RandomSource;
- import net.minecraft.util.SignatureValidator;
- import net.minecraft.util.TimeUtil;
-+import net.minecraft.util.datafix.DataFixers;
- import net.minecraft.util.debugchart.RemoteDebugSampleType;
- import net.minecraft.util.debugchart.SampleLogger;
- import net.minecraft.util.debugchart.TpsDebugDimensions;
-@@ -156,37 +148,71 @@
- import net.minecraft.world.level.biome.BiomeManager;
- import net.minecraft.world.level.block.Block;
- import net.minecraft.world.level.block.entity.FuelValues;
--import net.minecraft.world.level.border.BorderChangeListener;
- import net.minecraft.world.level.border.WorldBorder;
- import net.minecraft.world.level.chunk.storage.ChunkIOErrorReporter;
- import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
- import net.minecraft.world.level.dimension.LevelStem;
--import net.minecraft.world.level.levelgen.Heightmap;
--import net.minecraft.world.level.levelgen.PatrolSpawner;
--import net.minecraft.world.level.levelgen.PhantomSpawner;
- import net.minecraft.world.level.levelgen.WorldOptions;
- import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
- import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
-+import net.minecraft.world.level.storage.WorldData;
-+import org.slf4j.Logger;
-+
-+// CraftBukkit start
-+import com.mojang.serialization.Dynamic;
-+import com.mojang.serialization.Lifecycle;
-+import java.io.File;
-+import java.util.Random;
-+// import jline.console.ConsoleReader; // Paper
-+import joptsimple.OptionSet;
-+import net.minecraft.nbt.NbtException;
-+import net.minecraft.nbt.ReportedNbtException;
-+import net.minecraft.server.bossevents.CustomBossEvents;
-+import net.minecraft.server.dedicated.DedicatedServer;
-+import net.minecraft.server.dedicated.DedicatedServerProperties;
-+import net.minecraft.server.level.DemoMode;
-+import net.minecraft.server.level.PlayerRespawnLogic;
-+import net.minecraft.server.level.ServerChunkCache;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.server.level.ServerPlayerGameMode;
-+import net.minecraft.server.level.progress.ChunkProgressListener;
-+import net.minecraft.server.level.progress.ChunkProgressListenerFactory;
-+import net.minecraft.server.network.ServerConnectionListener;
-+import net.minecraft.server.network.TextFilter;
-+import net.minecraft.world.level.levelgen.Heightmap;
-+import net.minecraft.world.level.levelgen.PatrolSpawner;
-+import net.minecraft.world.level.levelgen.PhantomSpawner;
-+import net.minecraft.world.level.levelgen.WorldDimensions;
-+import net.minecraft.world.level.levelgen.presets.WorldPresets;
- import net.minecraft.world.level.storage.CommandStorage;
--import net.minecraft.world.level.storage.DerivedLevelData;
- import net.minecraft.world.level.storage.DimensionDataStorage;
- import net.minecraft.world.level.storage.LevelData;
-+import net.minecraft.world.level.storage.LevelDataAndDimensions;
- import net.minecraft.world.level.storage.LevelResource;
- import net.minecraft.world.level.storage.LevelStorageSource;
-+import net.minecraft.world.level.storage.LevelSummary;
- import net.minecraft.world.level.storage.PlayerDataStorage;
-+import net.minecraft.world.level.storage.PrimaryLevelData;
- import net.minecraft.world.level.storage.ServerLevelData;
--import net.minecraft.world.level.storage.WorldData;
-+import net.minecraft.world.level.validation.ContentValidationException;
- import net.minecraft.world.phys.Vec2;
- import net.minecraft.world.phys.Vec3;
--import org.slf4j.Logger;
-+import org.bukkit.Bukkit;
-+import org.bukkit.craftbukkit.CraftRegistry;
-+import org.bukkit.event.server.ServerLoadEvent;
-+// CraftBukkit end
- 
-+
- public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTask> implements ServerInfo, ChunkIOErrorReporter, CommandSource {
- 
-+    private static MinecraftServer SERVER; // Paper
-     public static final Logger LOGGER = LogUtils.getLogger();
-+    public static final net.kyori.adventure.text.logger.slf4j.ComponentLogger COMPONENT_LOGGER = net.kyori.adventure.text.logger.slf4j.ComponentLogger.logger(LOGGER.getName()); // Paper
-     public static final String VANILLA_BRAND = "vanilla";
-     private static final float AVERAGE_TICK_TIME_SMOOTHING = 0.8F;
-     private static final int TICK_STATS_SPAN = 100;
--    private static final long OVERLOADED_THRESHOLD_NANOS = 20L * TimeUtil.NANOSECONDS_PER_SECOND / 20L;
-+    private static final long OVERLOADED_THRESHOLD_NANOS = 30L * TimeUtil.NANOSECONDS_PER_SECOND / 20L; // CraftBukkit
-     private static final int OVERLOADED_TICKS_THRESHOLD = 20;
-     private static final long OVERLOADED_WARNING_INTERVAL_NANOS = 10L * TimeUtil.NANOSECONDS_PER_SECOND;
-     private static final int OVERLOADED_TICKS_WARNING_INTERVAL = 100;
-@@ -224,6 +250,7 @@
-     private Map<ResourceKey<Level>, ServerLevel> levels;
-     private PlayerList playerList;
-     private volatile boolean running;
-+    private volatile boolean isRestarting = false; // Paper - flag to signify we're attempting to restart
-     private boolean stopped;
-     private int tickCount;
-     private int ticksUntilAutosave;
-@@ -232,11 +259,15 @@
-     private boolean preventProxyConnections;
-     private boolean pvp;
-     private boolean allowFlight;
--    @Nullable
--    private String motd;
-+    private net.kyori.adventure.text.Component motd; // Paper - Adventure
-     private int playerIdleTimeout;
-     private final long[] tickTimesNanos;
-     private long aggregatedTickTimesNanos;
-+    // Paper start - Add tick times API and /mspt command
-+    public final TickTimes tickTimes5s = new TickTimes(100);
-+    public final TickTimes tickTimes10s = new TickTimes(200);
-+    public final TickTimes tickTimes60s = new TickTimes(1200);
-+    // Paper end - Add tick times API and /mspt command
-     @Nullable
-     private KeyPair keyPair;
-     @Nullable
-@@ -277,6 +308,28 @@
-     private final SuppressedExceptionCollector suppressedExceptions;
-     private final DiscontinuousFrame tickFrame;
- 
-+    // CraftBukkit start
-+    public final WorldLoader.DataLoadContext worldLoader;
-+    public org.bukkit.craftbukkit.CraftServer server;
-+    public OptionSet options;
-+    public org.bukkit.command.ConsoleCommandSender console;
-+    public static int currentTick; // Paper - improve tick loop
-+    public java.util.Queue<Runnable> processQueue = new java.util.concurrent.ConcurrentLinkedQueue<Runnable>();
-+    public int autosavePeriod;
-+    // Paper - don't store the vanilla dispatcher
-+    private boolean forceTicks;
-+    // CraftBukkit end
-+    // Spigot start
-+    public static final int TPS = 20;
-+    public static final int TICK_TIME = 1000000000 / MinecraftServer.TPS;
-+    private static final int SAMPLE_INTERVAL = 20; // Paper - improve server tick loop
-+    @Deprecated(forRemoval = true) // Paper
-+    public final double[] recentTps = new double[ 3 ];
-+    // Spigot end
-+    public final io.papermc.paper.configuration.PaperConfigurations paperConfigurations; // Paper - add paper configuration files
-+    public boolean isIteratingOverLevels = false; // Paper - Throw exception on world create while being ticked
-+    private final Set<String> pluginsBlockingSleep = new java.util.HashSet<>(); // Paper - API to allow/disallow tick sleeping
-+
-     public static <S extends MinecraftServer> S spin(Function<Thread, S> serverFactory) {
-         AtomicReference<S> atomicreference = new AtomicReference();
-         Thread thread = new Thread(() -> {
-@@ -286,19 +339,21 @@
-         thread.setUncaughtExceptionHandler((thread1, throwable) -> {
-             MinecraftServer.LOGGER.error("Uncaught exception in server thread", throwable);
-         });
-+        thread.setPriority(Thread.NORM_PRIORITY+2); // Paper - Perf: Boost priority
-         if (Runtime.getRuntime().availableProcessors() > 4) {
-             thread.setPriority(8);
-         }
- 
--        S s0 = (MinecraftServer) serverFactory.apply(thread);
-+        S s0 = serverFactory.apply(thread); // CraftBukkit - decompile error
- 
-         atomicreference.set(s0);
-         thread.start();
-         return s0;
-     }
- 
--    public MinecraftServer(Thread serverThread, LevelStorageSource.LevelStorageAccess session, PackRepository dataPackManager, WorldStem saveLoader, Proxy proxy, DataFixer dataFixer, Services apiServices, ChunkProgressListenerFactory worldGenerationProgressListenerFactory) {
-+    public MinecraftServer(OptionSet options, WorldLoader.DataLoadContext worldLoader, Thread thread, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PackRepository resourcepackrepository, WorldStem worldstem, Proxy proxy, DataFixer datafixer, Services services, ChunkProgressListenerFactory worldloadlistenerfactory) {
-         super("Server");
-+        SERVER = this; // Paper - better singleton
-         this.metricsRecorder = InactiveMetricsRecorder.INSTANCE;
-         this.onMetricsRecordingStopped = (methodprofilerresults) -> {
-             this.stopRecordingMetrics();
-@@ -319,36 +374,67 @@
-         this.scoreboard = new ServerScoreboard(this);
-         this.customBossEvents = new CustomBossEvents();
-         this.suppressedExceptions = new SuppressedExceptionCollector();
--        this.registries = saveLoader.registries();
--        this.worldData = saveLoader.worldData();
--        if (!this.registries.compositeAccess().lookupOrThrow(Registries.LEVEL_STEM).containsKey(LevelStem.OVERWORLD)) {
-+        this.registries = worldstem.registries();
-+        this.worldData = worldstem.worldData();
-+        if (false && !this.registries.compositeAccess().lookupOrThrow(Registries.LEVEL_STEM).containsKey(LevelStem.OVERWORLD)) { // CraftBukkit - initialised later
-             throw new IllegalStateException("Missing Overworld dimension data");
-         } else {
-             this.proxy = proxy;
--            this.packRepository = dataPackManager;
--            this.resources = new MinecraftServer.ReloadableResources(saveLoader.resourceManager(), saveLoader.dataPackResources());
--            this.services = apiServices;
--            if (apiServices.profileCache() != null) {
--                apiServices.profileCache().setExecutor(this);
-+            this.packRepository = resourcepackrepository;
-+            this.resources = new MinecraftServer.ReloadableResources(worldstem.resourceManager(), worldstem.dataPackResources());
-+            this.services = services;
-+            if (services.profileCache() != null) {
-+                services.profileCache().setExecutor(this);
-             }
- 
--            this.connection = new ServerConnectionListener(this);
-+            // this.connection = new ServerConnection(this); // Spigot
-             this.tickRateManager = new ServerTickRateManager(this);
--            this.progressListenerFactory = worldGenerationProgressListenerFactory;
--            this.storageSource = session;
--            this.playerDataStorage = session.createPlayerStorage();
--            this.fixerUpper = dataFixer;
-+            this.progressListenerFactory = worldloadlistenerfactory;
-+            this.storageSource = convertable_conversionsession;
-+            this.playerDataStorage = convertable_conversionsession.createPlayerStorage();
-+            this.fixerUpper = datafixer;
-             this.functionManager = new ServerFunctionManager(this, this.resources.managers.getFunctionLibrary());
-             HolderGetter<Block> holdergetter = this.registries.compositeAccess().lookupOrThrow(Registries.BLOCK).filterFeatures(this.worldData.enabledFeatures());
- 
--            this.structureTemplateManager = new StructureTemplateManager(saveLoader.resourceManager(), session, dataFixer, holdergetter);
--            this.serverThread = serverThread;
-+            this.structureTemplateManager = new StructureTemplateManager(worldstem.resourceManager(), convertable_conversionsession, datafixer, holdergetter);
-+            this.serverThread = thread;
-             this.executor = Util.backgroundExecutor();
-             this.potionBrewing = PotionBrewing.bootstrap(this.worldData.enabledFeatures());
-             this.resources.managers.getRecipeManager().finalizeRecipeLoading(this.worldData.enabledFeatures());
-             this.fuelValues = FuelValues.vanillaBurnTimes(this.registries.compositeAccess(), this.worldData.enabledFeatures());
-             this.tickFrame = TracyClient.createDiscontinuousFrame("Server Tick");
-         }
-+        // CraftBukkit start
-+        this.options = options;
-+        this.worldLoader = worldLoader;
-+        // Paper start - Handled by TerminalConsoleAppender
-+        // Try to see if we're actually running in a terminal, disable jline if not
-+        /*
-+        if (System.console() == null && System.getProperty("jline.terminal") == null) {
-+            System.setProperty("jline.terminal", "jline.UnsupportedTerminal");
-+            Main.useJline = false;
-+        }
-+
-+        try {
-+            this.reader = new ConsoleReader(System.in, System.out);
-+            this.reader.setExpandEvents(false); // Avoid parsing exceptions for uncommonly used event designators
-+        } catch (Throwable e) {
-+            try {
-+                // Try again with jline disabled for Windows users without C++ 2008 Redistributable
-+                System.setProperty("jline.terminal", "jline.UnsupportedTerminal");
-+                System.setProperty("user.language", "en");
-+                Main.useJline = false;
-+                this.reader = new ConsoleReader(System.in, System.out);
-+                this.reader.setExpandEvents(false);
-+            } catch (IOException ex) {
-+                MinecraftServer.LOGGER.warn((String) null, ex);
-+            }
-+        }
-+        */
-+        // Paper end
-+        Runtime.getRuntime().addShutdownHook(new org.bukkit.craftbukkit.util.ServerShutdownThread(this));
-+        // CraftBukkit end
-+        this.paperConfigurations = services.paperConfigurations(); // Paper - add paper configuration files
-     }
- 
-     private void readScoreboard(DimensionDataStorage persistentStateManager) {
-@@ -357,7 +443,7 @@
- 
-     protected abstract boolean initServer() throws IOException;
- 
--    protected void loadLevel() {
-+    protected void loadLevel(String s) { // CraftBukkit
-         if (!JvmProfiler.INSTANCE.isRunning()) {
-             ;
-         }
-@@ -365,12 +451,8 @@
-         boolean flag = false;
-         ProfiledDuration profiledduration = JvmProfiler.INSTANCE.onWorldLoadedStarted();
- 
--        this.worldData.setModdedInfo(this.getServerModName(), this.getModdedStatus().shouldReportAsModified());
--        ChunkProgressListener worldloadlistener = this.progressListenerFactory.create(this.worldData.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS));
-+        this.loadWorld0(s); // CraftBukkit
- 
--        this.createLevels(worldloadlistener);
--        this.forceDifficulty();
--        this.prepareLevels(worldloadlistener);
-         if (profiledduration != null) {
-             profiledduration.finish(true);
-         }
-@@ -387,23 +469,246 @@
- 
-     protected void forceDifficulty() {}
- 
--    protected void createLevels(ChunkProgressListener worldGenerationProgressListener) {
--        ServerLevelData iworlddataserver = this.worldData.overworldData();
--        boolean flag = this.worldData.isDebugWorld();
--        Registry<LevelStem> iregistry = this.registries.compositeAccess().lookupOrThrow(Registries.LEVEL_STEM);
--        WorldOptions worldoptions = this.worldData.worldGenOptions();
--        long i = worldoptions.seed();
--        long j = BiomeManager.obfuscateSeed(i);
--        List<CustomSpawner> list = ImmutableList.of(new PhantomSpawner(), new PatrolSpawner(), new CatSpawner(), new VillageSiege(), new WanderingTraderSpawner(iworlddataserver));
--        LevelStem worlddimension = (LevelStem) iregistry.getValue(LevelStem.OVERWORLD);
--        ServerLevel worldserver = new ServerLevel(this, this.executor, this.storageSource, iworlddataserver, Level.OVERWORLD, worlddimension, worldGenerationProgressListener, flag, j, list, true, (RandomSequences) null);
-+    // CraftBukkit start
-+    private void loadWorld0(String s) {
-+        LevelStorageSource.LevelStorageAccess worldSession = this.storageSource;
- 
--        this.levels.put(Level.OVERWORLD, worldserver);
--        DimensionDataStorage worldpersistentdata = worldserver.getDataStorage();
-+        RegistryAccess.Frozen iregistrycustom_dimension = this.registries.compositeAccess();
-+        Registry<LevelStem> dimensions = iregistrycustom_dimension.lookupOrThrow(Registries.LEVEL_STEM);
-+        for (LevelStem worldDimension : dimensions) {
-+            ResourceKey<LevelStem> dimensionKey = dimensions.getResourceKey(worldDimension).get();
- 
--        this.readScoreboard(worldpersistentdata);
--        this.commandStorage = new CommandStorage(worldpersistentdata);
-+            ServerLevel world;
-+            int dimension = 0;
-+
-+            if (dimensionKey == LevelStem.NETHER) {
-+                if (this.server.getAllowNether()) {
-+                    dimension = -1;
-+                } else {
-+                    continue;
-+                }
-+            } else if (dimensionKey == LevelStem.END) {
-+                if (this.server.getAllowEnd()) {
-+                    dimension = 1;
-+                } else {
-+                    continue;
-+                }
-+            } else if (dimensionKey != LevelStem.OVERWORLD) {
-+                dimension = -999;
-+            }
-+
-+            String worldType = (dimension == -999) ? dimensionKey.location().getNamespace() + "_" + dimensionKey.location().getPath() : org.bukkit.World.Environment.getEnvironment(dimension).toString().toLowerCase(Locale.ROOT);
-+            String name = (dimensionKey == LevelStem.OVERWORLD) ? s : s + "_" + worldType;
-+            if (dimension != 0) {
-+                File newWorld = LevelStorageSource.getStorageFolder(new File(name).toPath(), dimensionKey).toFile();
-+                File oldWorld = LevelStorageSource.getStorageFolder(new File(s).toPath(), dimensionKey).toFile();
-+                File oldLevelDat = new File(new File(s), "level.dat"); // The data folders exist on first run as they are created in the PersistentCollection constructor above, but the level.dat won't
-+
-+                if (!newWorld.isDirectory() && oldWorld.isDirectory() && oldLevelDat.isFile()) {
-+                    MinecraftServer.LOGGER.info("---- Migration of old " + worldType + " folder required ----");
-+                    MinecraftServer.LOGGER.info("Unfortunately due to the way that Minecraft implemented multiworld support in 1.6, Bukkit requires that you move your " + worldType + " folder to a new location in order to operate correctly.");
-+                    MinecraftServer.LOGGER.info("We will move this folder for you, but it will mean that you need to move it back should you wish to stop using Bukkit in the future.");
-+                    MinecraftServer.LOGGER.info("Attempting to move " + oldWorld + " to " + newWorld + "...");
-+
-+                    if (newWorld.exists()) {
-+                        MinecraftServer.LOGGER.warn("A file or folder already exists at " + newWorld + "!");
-+                        MinecraftServer.LOGGER.info("---- Migration of old " + worldType + " folder failed ----");
-+                    } else if (newWorld.getParentFile().mkdirs()) {
-+                        if (oldWorld.renameTo(newWorld)) {
-+                            MinecraftServer.LOGGER.info("Success! To restore " + worldType + " in the future, simply move " + newWorld + " to " + oldWorld);
-+                            // Migrate world data too.
-+                            try {
-+                                com.google.common.io.Files.copy(oldLevelDat, new File(new File(name), "level.dat"));
-+                                org.apache.commons.io.FileUtils.copyDirectory(new File(new File(s), "data"), new File(new File(name), "data"));
-+                            } catch (IOException exception) {
-+                                MinecraftServer.LOGGER.warn("Unable to migrate world data.");
-+                            }
-+                            MinecraftServer.LOGGER.info("---- Migration of old " + worldType + " folder complete ----");
-+                        } else {
-+                            MinecraftServer.LOGGER.warn("Could not move folder " + oldWorld + " to " + newWorld + "!");
-+                            MinecraftServer.LOGGER.info("---- Migration of old " + worldType + " folder failed ----");
-+                        }
-+                    } else {
-+                        MinecraftServer.LOGGER.warn("Could not create path for " + newWorld + "!");
-+                        MinecraftServer.LOGGER.info("---- Migration of old " + worldType + " folder failed ----");
-+                    }
-+                }
-+
-+                try {
-+                    worldSession = LevelStorageSource.createDefault(this.server.getWorldContainer().toPath()).validateAndCreateAccess(name, dimensionKey);
-+                } catch (IOException | ContentValidationException ex) {
-+                    throw new RuntimeException(ex);
-+                }
-+            }
-+
-+            Dynamic<?> dynamic;
-+            if (worldSession.hasWorldData()) {
-+                LevelSummary worldinfo;
-+
-+                try {
-+                    dynamic = worldSession.getDataTag();
-+                    worldinfo = worldSession.getSummary(dynamic);
-+                } catch (NbtException | ReportedNbtException | IOException ioexception) {
-+                    LevelStorageSource.LevelDirectory convertable_b = worldSession.getLevelDirectory();
-+
-+                    MinecraftServer.LOGGER.warn("Failed to load world data from {}", convertable_b.dataFile(), ioexception);
-+                    MinecraftServer.LOGGER.info("Attempting to use fallback");
-+
-+                    try {
-+                        dynamic = worldSession.getDataTagFallback();
-+                        worldinfo = worldSession.getSummary(dynamic);
-+                    } catch (NbtException | ReportedNbtException | IOException ioexception1) {
-+                        MinecraftServer.LOGGER.error("Failed to load world data from {}", convertable_b.oldDataFile(), ioexception1);
-+                        MinecraftServer.LOGGER.error("Failed to load world data from {} and {}. World files may be corrupted. Shutting down.", convertable_b.dataFile(), convertable_b.oldDataFile());
-+                        return;
-+                    }
-+
-+                    worldSession.restoreLevelDataFromOld();
-+                }
-+
-+                if (worldinfo.requiresManualConversion()) {
-+                    MinecraftServer.LOGGER.info("This world must be opened in an older version (like 1.6.4) to be safely converted");
-+                    return;
-+                }
-+
-+                if (!worldinfo.isCompatible()) {
-+                    MinecraftServer.LOGGER.info("This world was created by an incompatible version.");
-+                    return;
-+                }
-+            } else {
-+                dynamic = null;
-+            }
-+
-+            org.bukkit.generator.ChunkGenerator gen = this.server.getGenerator(name);
-+            org.bukkit.generator.BiomeProvider biomeProvider = this.server.getBiomeProvider(name);
-+
-+            PrimaryLevelData worlddata;
-+            WorldLoader.DataLoadContext worldloader_a = this.worldLoader;
-+            Registry<LevelStem> iregistry = worldloader_a.datapackDimensions().lookupOrThrow(Registries.LEVEL_STEM);
-+            if (dynamic != null) {
-+                LevelDataAndDimensions leveldataanddimensions = LevelStorageSource.getLevelDataAndDimensions(dynamic, worldloader_a.dataConfiguration(), iregistry, worldloader_a.datapackWorldgen());
-+
-+                worlddata = (PrimaryLevelData) leveldataanddimensions.worldData();
-+            } else {
-+                LevelSettings worldsettings;
-+                WorldOptions worldoptions;
-+                WorldDimensions worlddimensions;
-+
-+                if (this.isDemo()) {
-+                    worldsettings = MinecraftServer.DEMO_SETTINGS;
-+                    worldoptions = WorldOptions.DEMO_OPTIONS;
-+                    worlddimensions = WorldPresets.createNormalWorldDimensions(worldloader_a.datapackWorldgen());
-+                } else {
-+                    DedicatedServerProperties dedicatedserverproperties = ((DedicatedServer) this).getProperties();
-+
-+                    worldsettings = new LevelSettings(dedicatedserverproperties.levelName, dedicatedserverproperties.gamemode, dedicatedserverproperties.hardcore, dedicatedserverproperties.difficulty, false, new GameRules(worldloader_a.dataConfiguration().enabledFeatures()), worldloader_a.dataConfiguration());
-+                    worldoptions = this.options.has("bonusChest") ? dedicatedserverproperties.worldOptions.withBonusChest(true) : dedicatedserverproperties.worldOptions;
-+                    worlddimensions = dedicatedserverproperties.createDimensions(worldloader_a.datapackWorldgen());
-+                }
-+
-+                WorldDimensions.Complete worlddimensions_b = worlddimensions.bake(iregistry);
-+                Lifecycle lifecycle = worlddimensions_b.lifecycle().add(worldloader_a.datapackWorldgen().allRegistriesLifecycle());
-+
-+                worlddata = new PrimaryLevelData(worldsettings, worldoptions, worlddimensions_b.specialWorldProperty(), lifecycle);
-+            }
-+            worlddata.checkName(name); // CraftBukkit - Migration did not rewrite the level.dat; This forces 1.8 to take the last loaded world as respawn (in this case the end)
-+            if (this.options.has("forceUpgrade")) {
-+                net.minecraft.server.Main.forceUpgrade(worldSession, DataFixers.getDataFixer(), this.options.has("eraseCache"), () -> {
-+                    return true;
-+                }, iregistrycustom_dimension, this.options.has("recreateRegionFiles"));
-+            }
-+
-+            PrimaryLevelData iworlddataserver = worlddata;
-+            boolean flag = worlddata.isDebugWorld();
-+            WorldOptions worldoptions = worlddata.worldGenOptions();
-+            long i = worldoptions.seed();
-+            long j = BiomeManager.obfuscateSeed(i);
-+            List<CustomSpawner> list = ImmutableList.of(new PhantomSpawner(), new PatrolSpawner(), new CatSpawner(), new VillageSiege(), new WanderingTraderSpawner(iworlddataserver));
-+            LevelStem worlddimension = (LevelStem) dimensions.getValue(dimensionKey);
-+
-+            org.bukkit.generator.WorldInfo worldInfo = new org.bukkit.craftbukkit.generator.CraftWorldInfo(iworlddataserver, worldSession, org.bukkit.World.Environment.getEnvironment(dimension), worlddimension.type().value(), worlddimension.generator(), this.registryAccess()); // Paper - Expose vanilla BiomeProvider from WorldInfo
-+            if (biomeProvider == null && gen != null) {
-+                biomeProvider = gen.getDefaultBiomeProvider(worldInfo);
-+            }
-+
-+            ResourceKey<Level> worldKey = ResourceKey.create(Registries.DIMENSION, dimensionKey.location());
-+
-+            if (dimensionKey == LevelStem.OVERWORLD) {
-+                this.worldData = worlddata;
-+                this.worldData.setGameType(((DedicatedServer) this).getProperties().gamemode); // From DedicatedServer.init
-+
-+                ChunkProgressListener worldloadlistener = this.progressListenerFactory.create(this.worldData.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS));
-+
-+                world = new ServerLevel(this, this.executor, worldSession, iworlddataserver, worldKey, worlddimension, worldloadlistener, flag, j, list, true, (RandomSequences) null, org.bukkit.World.Environment.getEnvironment(dimension), gen, biomeProvider);
-+                DimensionDataStorage worldpersistentdata = world.getDataStorage();
-+                this.readScoreboard(worldpersistentdata);
-+                this.server.scoreboardManager = new org.bukkit.craftbukkit.scoreboard.CraftScoreboardManager(this, world.getScoreboard());
-+                this.commandStorage = new CommandStorage(worldpersistentdata);
-+            } else {
-+                ChunkProgressListener worldloadlistener = this.progressListenerFactory.create(this.worldData.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS));
-+                // Paper start - option to use the dimension_type to check if spawners should be added. I imagine mojang will add some datapack-y way of managing this in the future.
-+                final List<CustomSpawner> spawners;
-+                if (io.papermc.paper.configuration.GlobalConfiguration.get().misc.useDimensionTypeForCustomSpawners && this.registryAccess().lookupOrThrow(Registries.DIMENSION_TYPE).getResourceKey(worlddimension.type().value()).orElseThrow() == net.minecraft.world.level.dimension.BuiltinDimensionTypes.OVERWORLD) {
-+                    spawners = list;
-+                } else {
-+                    spawners = Collections.emptyList();
-+                }
-+                world = new ServerLevel(this, this.executor, worldSession, iworlddataserver, worldKey, worlddimension, worldloadlistener, flag, j, spawners, true, this.overworld().getRandomSequences(), org.bukkit.World.Environment.getEnvironment(dimension), gen, biomeProvider);
-+                // Paper end - option to use the dimension_type to check if spawners should be added
-+            }
-+
-+            worlddata.setModdedInfo(this.getServerModName(), this.getModdedStatus().shouldReportAsModified());
-+            this.addLevel(world); // Paper - Put world into worldlist before initing the world; move up
-+            this.initWorld(world, worlddata, this.worldData, worldoptions);
-+
-+            // Paper - Put world into worldlist before initing the world; move up
-+            this.getPlayerList().addWorldborderListener(world);
-+
-+            if (worlddata.getCustomBossEvents() != null) {
-+                this.getCustomBossEvents().load(worlddata.getCustomBossEvents(), this.registryAccess());
-+            }
-+        }
-+        this.forceDifficulty();
-+        for (ServerLevel worldserver : this.getAllLevels()) {
-+            this.prepareLevels(worldserver.getChunkSource().chunkMap.progressListener, worldserver);
-+            worldserver.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API
-+            this.server.getPluginManager().callEvent(new org.bukkit.event.world.WorldLoadEvent(worldserver.getWorld()));
-+        }
-+
-+        // Paper start - Configurable player collision; Handle collideRule team for player collision toggle
-+        final ServerScoreboard scoreboard = this.getScoreboard();
-+        final java.util.Collection<String> toRemove = scoreboard.getPlayerTeams().stream().filter(team -> team.getName().startsWith("collideRule_")).map(net.minecraft.world.scores.PlayerTeam::getName).collect(java.util.stream.Collectors.toList());
-+        for (String teamName : toRemove) {
-+            scoreboard.removePlayerTeam(scoreboard.getPlayerTeam(teamName)); // Clean up after ourselves
-+        }
-+
-+        if (!io.papermc.paper.configuration.GlobalConfiguration.get().collisions.enablePlayerCollisions) {
-+            this.getPlayerList().collideRuleTeamName = org.apache.commons.lang3.StringUtils.left("collideRule_" + java.util.concurrent.ThreadLocalRandom.current().nextInt(), 16);
-+            net.minecraft.world.scores.PlayerTeam collideTeam = scoreboard.addPlayerTeam(this.getPlayerList().collideRuleTeamName);
-+            collideTeam.setSeeFriendlyInvisibles(false); // Because we want to mimic them not being on a team at all
-+        }
-+        // Paper end - Configurable player collision
-+
-+        this.server.enablePlugins(org.bukkit.plugin.PluginLoadOrder.POSTWORLD);
-+        this.server.spark.registerCommandBeforePlugins(this.server); // Paper - spark
-+        this.server.spark.enableAfterPlugins(this.server); // Paper - spark
-+        if (io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper != null) io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper.pluginsEnabled(); // Paper - Remap plugins
-+        io.papermc.paper.command.brigadier.PaperCommands.INSTANCE.setValid(); // Paper - reset invalid state for event fire below
-+        io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner.INSTANCE.callReloadableRegistrarEvent(io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents.COMMANDS, io.papermc.paper.command.brigadier.PaperCommands.INSTANCE, org.bukkit.plugin.Plugin.class, io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent.Cause.INITIAL); // Paper - call commands event for regular plugins
-+        ((org.bukkit.craftbukkit.help.SimpleHelpMap) this.server.getHelpMap()).initializeCommands();
-+        this.server.getPluginManager().callEvent(new ServerLoadEvent(ServerLoadEvent.LoadType.STARTUP));
-+        this.connection.acceptConnections();
-+    }
-+
-+    public void initWorld(ServerLevel worldserver, ServerLevelData iworlddataserver, WorldData saveData, WorldOptions worldoptions) {
-+        boolean flag = saveData.isDebugWorld();
-+        // CraftBukkit start
-+        if (worldserver.generator != null) {
-+            worldserver.getWorld().getPopulators().addAll(worldserver.generator.getDefaultPopulators(worldserver.getWorld()));
-+        }
-         WorldBorder worldborder = worldserver.getWorldBorder();
-+        worldborder.applySettings(iworlddataserver.getWorldBorder()); // CraftBukkit - move up so that WorldBorder is set during WorldInitEvent
-+        this.server.getPluginManager().callEvent(new org.bukkit.event.world.WorldInitEvent(worldserver.getWorld())); // CraftBukkit - SPIGOT-5569: Call WorldInitEvent before any chunks are generated
- 
-         if (!iworlddataserver.isInitialized()) {
-             try {
-@@ -427,37 +732,31 @@
-             iworlddataserver.setInitialized(true);
-         }
- 
--        this.getPlayerList().addWorldborderListener(worldserver);
--        if (this.worldData.getCustomBossEvents() != null) {
--            this.getCustomBossEvents().load(this.worldData.getCustomBossEvents(), this.registryAccess());
--        }
--
--        RandomSequences randomsequences = worldserver.getRandomSequences();
--        Iterator iterator = iregistry.entrySet().iterator();
--
--        while (iterator.hasNext()) {
--            Entry<ResourceKey<LevelStem>, LevelStem> entry = (Entry) iterator.next();
--            ResourceKey<LevelStem> resourcekey = (ResourceKey) entry.getKey();
--
--            if (resourcekey != LevelStem.OVERWORLD) {
--                ResourceKey<Level> resourcekey1 = ResourceKey.create(Registries.DIMENSION, resourcekey.location());
--                DerivedLevelData secondaryworlddata = new DerivedLevelData(this.worldData, iworlddataserver);
--                ServerLevel worldserver1 = new ServerLevel(this, this.executor, this.storageSource, secondaryworlddata, resourcekey1, (LevelStem) entry.getValue(), worldGenerationProgressListener, flag, j, ImmutableList.of(), false, randomsequences);
--
--                worldborder.addListener(new BorderChangeListener.DelegateBorderChangeListener(worldserver1.getWorldBorder()));
--                this.levels.put(resourcekey1, worldserver1);
--            }
--        }
--
--        worldborder.applySettings(iworlddataserver.getWorldBorder());
-     }
-+    // CraftBukkit end
- 
-     private static void setInitialSpawn(ServerLevel world, ServerLevelData worldProperties, boolean bonusChest, boolean debugWorld) {
-         if (debugWorld) {
-             worldProperties.setSpawn(BlockPos.ZERO.above(80), 0.0F);
-         } else {
-             ServerChunkCache chunkproviderserver = world.getChunkSource();
--            ChunkPos chunkcoordintpair = new ChunkPos(chunkproviderserver.randomState().sampler().findSpawnPosition());
-+            // ChunkPos chunkcoordintpair = new ChunkPos(chunkproviderserver.randomState().sampler().findSpawnPosition()); // Paper - Move down, only attempt to find spawn position if there isn't a fixed spawn position set
-+            // CraftBukkit start
-+            if (world.generator != null) {
-+                Random rand = new Random(world.getSeed());
-+                org.bukkit.Location spawn = world.generator.getFixedSpawnLocation(world.getWorld(), rand);
-+
-+                if (spawn != null) {
-+                    if (spawn.getWorld() != world.getWorld()) {
-+                        throw new IllegalStateException("Cannot set spawn point for " + worldProperties.getLevelName() + " to be in another world (" + spawn.getWorld().getName() + ")");
-+                    } else {
-+                        worldProperties.setSpawn(new BlockPos(spawn.getBlockX(), spawn.getBlockY(), spawn.getBlockZ()), spawn.getYaw());
-+                        return;
-+                    }
-+                }
-+            }
-+            // CraftBukkit end
-+            ChunkPos chunkcoordintpair = new ChunkPos(chunkproviderserver.randomState().sampler().findSpawnPosition()); // Paper - Only attempt to find spawn position if there isn't a fixed spawn position set
-             int i = chunkproviderserver.getGenerator().getSpawnHeight(world);
- 
-             if (i < world.getMinY()) {
-@@ -516,31 +815,36 @@
-         iworlddataserver.setGameType(GameType.SPECTATOR);
-     }
- 
--    public void prepareLevels(ChunkProgressListener worldGenerationProgressListener) {
--        ServerLevel worldserver = this.overworld();
-+    // CraftBukkit start
-+    public void prepareLevels(ChunkProgressListener worldloadlistener, ServerLevel worldserver) {
-+        // WorldServer worldserver = this.overworld();
-+        this.forceTicks = true;
-+        // CraftBukkit end
- 
-         MinecraftServer.LOGGER.info("Preparing start region for dimension {}", worldserver.dimension().location());
-         BlockPos blockposition = worldserver.getSharedSpawnPos();
- 
--        worldGenerationProgressListener.updateSpawnPos(new ChunkPos(blockposition));
-+        worldloadlistener.updateSpawnPos(new ChunkPos(blockposition));
-         ServerChunkCache chunkproviderserver = worldserver.getChunkSource();
- 
-         this.nextTickTimeNanos = Util.getNanos();
-         worldserver.setDefaultSpawnPos(blockposition, worldserver.getSharedSpawnAngle());
--        int i = this.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS);
-+        int i = worldserver.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS); // CraftBukkit - per-world
-         int j = i > 0 ? Mth.square(ChunkProgressListener.calculateDiameter(i)) : 0;
- 
-         while (chunkproviderserver.getTickingGenerated() < j) {
--            this.nextTickTimeNanos = Util.getNanos() + MinecraftServer.PREPARE_LEVELS_DEFAULT_DELAY_NANOS;
--            this.waitUntilNextTick();
-+            // CraftBukkit start
-+            // this.nextTickTimeNanos = SystemUtils.getNanos() + MinecraftServer.PREPARE_LEVELS_DEFAULT_DELAY_NANOS;
-+            this.executeModerately();
-         }
- 
--        this.nextTickTimeNanos = Util.getNanos() + MinecraftServer.PREPARE_LEVELS_DEFAULT_DELAY_NANOS;
--        this.waitUntilNextTick();
--        Iterator iterator = this.levels.values().iterator();
-+        // this.nextTickTimeNanos = SystemUtils.getNanos() + MinecraftServer.PREPARE_LEVELS_DEFAULT_DELAY_NANOS;
-+        this.executeModerately();
-+        // Iterator iterator = this.levels.values().iterator();
- 
--        while (iterator.hasNext()) {
--            ServerLevel worldserver1 = (ServerLevel) iterator.next();
-+        if (true) {
-+            ServerLevel worldserver1 = worldserver;
-+            // CraftBukkit end
-             ForcedChunksSavedData forcedchunk = (ForcedChunksSavedData) worldserver1.getDataStorage().get(ForcedChunksSavedData.factory(), "chunks");
- 
-             if (forcedchunk != null) {
-@@ -555,10 +859,17 @@
-             }
-         }
- 
--        this.nextTickTimeNanos = Util.getNanos() + MinecraftServer.PREPARE_LEVELS_DEFAULT_DELAY_NANOS;
--        this.waitUntilNextTick();
--        worldGenerationProgressListener.stop();
--        this.updateMobSpawningFlags();
-+        // CraftBukkit start
-+        // this.nextTickTimeNanos = SystemUtils.getNanos() + MinecraftServer.PREPARE_LEVELS_DEFAULT_DELAY_NANOS;
-+        this.executeModerately();
-+        // CraftBukkit end
-+        worldloadlistener.stop();
-+        // CraftBukkit start
-+        // this.updateMobSpawningFlags();
-+        worldserver.setSpawnSettings(worldserver.serverLevelData.getDifficulty() != Difficulty.PEACEFUL && ((DedicatedServer) this).settings.getProperties().spawnMonsters); // Paper - per level difficulty (from setDifficulty(ServerLevel, Difficulty, boolean))
-+
-+        this.forceTicks = false;
-+        // CraftBukkit end
-     }
- 
-     public GameType getDefaultGameType() {
-@@ -588,12 +899,16 @@
-             worldserver.save((ProgressListener) null, flush, worldserver.noSave && !force);
-         }
- 
--        ServerLevel worldserver1 = this.overworld();
--        ServerLevelData iworlddataserver = this.worldData.overworldData();
-+        // CraftBukkit start - moved to WorldServer.save
-+        /*
-+        WorldServer worldserver1 = this.overworld();
-+        IWorldDataServer iworlddataserver = this.worldData.overworldData();
- 
-         iworlddataserver.setWorldBorder(worldserver1.getWorldBorder().createSettings());
-         this.worldData.setCustomBossEvents(this.getCustomBossEvents().save(this.registryAccess()));
-         this.storageSource.saveDataTag(this.registryAccess(), this.worldData, this.getPlayerList().getSingleplayerData());
-+        */
-+        // CraftBukkit end
-         if (flush) {
-             Iterator iterator1 = this.getAllLevels().iterator();
- 
-@@ -628,18 +943,46 @@
-         this.stopServer();
-     }
- 
-+    // CraftBukkit start
-+    private boolean hasStopped = false;
-+    private boolean hasLoggedStop = false; // Paper - Debugging
-+    private final Object stopLock = new Object();
-+    public final boolean hasStopped() {
-+        synchronized (this.stopLock) {
-+            return this.hasStopped;
-+        }
-+    }
-+    // CraftBukkit end
-+
-     public void stopServer() {
-+        // CraftBukkit start - prevent double stopping on multiple threads
-+        synchronized(this.stopLock) {
-+            if (this.hasStopped) return;
-+            this.hasStopped = true;
-+        }
-+        if (!hasLoggedStop && isDebugging()) io.papermc.paper.util.TraceUtil.dumpTraceForThread("Server stopped"); // Paper - Debugging
-+        // CraftBukkit end
-         if (this.metricsRecorder.isRecording()) {
-             this.cancelRecordingMetrics();
-         }
- 
-         MinecraftServer.LOGGER.info("Stopping server");
-+        Commands.COMMAND_SENDING_POOL.shutdownNow(); // Paper - Perf: Async command map building; Shutdown and don't bother finishing
-+        // CraftBukkit start
-+        if (this.server != null) {
-+            this.server.spark.disable(); // Paper - spark
-+            this.server.disablePlugins();
-+            this.server.waitForAsyncTasksShutdown(); // Paper - Wait for Async Tasks during shutdown
-+        }
-+        // CraftBukkit end
-+        if (io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper != null) io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper.shutdown(); // Paper - Plugin remapping
-         this.getConnection().stop();
-         this.isSaving = true;
-         if (this.playerList != null) {
-             MinecraftServer.LOGGER.info("Saving players");
-             this.playerList.saveAll();
--            this.playerList.removeAll();
-+            this.playerList.removeAll(this.isRestarting); // Paper
-+            try { Thread.sleep(100); } catch (InterruptedException ex) {} // CraftBukkit - SPIGOT-625 - give server at least a chance to send packets
-         }
- 
-         MinecraftServer.LOGGER.info("Saving worlds");
-@@ -693,6 +1036,15 @@
-         } catch (IOException ioexception1) {
-             MinecraftServer.LOGGER.error("Failed to unlock level {}", this.storageSource.getLevelId(), ioexception1);
-         }
-+        // Spigot start
-+        io.papermc.paper.util.MCUtil.ASYNC_EXECUTOR.shutdown(); // Paper
-+        try { io.papermc.paper.util.MCUtil.ASYNC_EXECUTOR.awaitTermination(30, java.util.concurrent.TimeUnit.SECONDS); // Paper
-+        } catch (java.lang.InterruptedException ignored) {} // Paper
-+        if (org.spigotmc.SpigotConfig.saveUserCacheOnStopOnly) {
-+            MinecraftServer.LOGGER.info("Saving usercache.json");
-+            this.getProfileCache().save(false); // Paper - Perf: Async GameProfileCache saving
-+        }
-+        // Spigot end
- 
-     }
- 
-@@ -709,6 +1061,14 @@
-     }
- 
-     public void halt(boolean waitForShutdown) {
-+        // Paper start - allow passing of the intent to restart
-+        this.safeShutdown(waitForShutdown, false);
-+    }
-+    public void safeShutdown(boolean waitForShutdown, boolean isRestarting) {
-+        this.isRestarting = isRestarting;
-+        this.hasLoggedStop = true; // Paper - Debugging
-+        if (isDebugging()) io.papermc.paper.util.TraceUtil.dumpTraceForThread("Server stopped"); // Paper - Debugging
-+        // Paper end
-         this.running = false;
-         if (waitForShutdown) {
-             try {
-@@ -720,6 +1080,64 @@
- 
-     }
- 
-+    // Spigot Start
-+    private static double calcTps(double avg, double exp, double tps)
-+    {
-+        return ( avg * exp ) + ( tps * ( 1 - exp ) );
-+    }
-+
-+    // Paper start - Further improve server tick loop
-+    private static final long SEC_IN_NANO = 1000000000;
-+    private static final long MAX_CATCHUP_BUFFER = TICK_TIME * TPS * 60L;
-+    private long lastTick = 0;
-+    private long catchupTime = 0;
-+    public final RollingAverage tps1 = new RollingAverage(60);
-+    public final RollingAverage tps5 = new RollingAverage(60 * 5);
-+    public final RollingAverage tps15 = new RollingAverage(60 * 15);
-+
-+    public static class RollingAverage {
-+        private final int size;
-+        private long time;
-+        private java.math.BigDecimal total;
-+        private int index = 0;
-+        private final java.math.BigDecimal[] samples;
-+        private final long[] times;
-+
-+        RollingAverage(int size) {
-+            this.size = size;
-+            this.time = size * SEC_IN_NANO;
-+            this.total = dec(TPS).multiply(dec(SEC_IN_NANO)).multiply(dec(size));
-+            this.samples = new java.math.BigDecimal[size];
-+            this.times = new long[size];
-+            for (int i = 0; i < size; i++) {
-+                this.samples[i] = dec(TPS);
-+                this.times[i] = SEC_IN_NANO;
-+            }
-+        }
-+
-+        private static java.math.BigDecimal dec(long t) {
-+            return new java.math.BigDecimal(t);
-+        }
-+        public void add(java.math.BigDecimal x, long t) {
-+            time -= times[index];
-+            total = total.subtract(samples[index].multiply(dec(times[index])));
-+            samples[index] = x;
-+            times[index] = t;
-+            time += t;
-+            total = total.add(x.multiply(dec(t)));
-+            if (++index == size) {
-+                index = 0;
-+            }
-+        }
-+
-+        public double getAverage() {
-+            return total.divide(dec(time), 30, java.math.RoundingMode.HALF_UP).doubleValue();
-+        }
-+    }
-+    private static final java.math.BigDecimal TPS_BASE = new java.math.BigDecimal(1E9).multiply(new java.math.BigDecimal(SAMPLE_INTERVAL));
-+    // Paper end
-+    // Spigot End
-+
-     protected void runServer() {
-         try {
-             if (!this.initServer()) {
-@@ -727,9 +1145,27 @@
-             }
- 
-             this.nextTickTimeNanos = Util.getNanos();
--            this.statusIcon = (ServerStatus.Favicon) this.loadStatusIcon().orElse((Object) null);
-+            this.statusIcon = (ServerStatus.Favicon) this.loadStatusIcon().orElse(null); // CraftBukkit - decompile error
-             this.status = this.buildServerStatus();
- 
-+            this.server.spark.enableBeforePlugins(); // Paper - spark
-+            // Spigot start
-+            org.spigotmc.WatchdogThread.hasStarted = true; // Paper
-+            Arrays.fill( this.recentTps, 20 );
-+            // Paper start - further improve server tick loop
-+            long tickSection = Util.getNanos();
-+            long currentTime;
-+            // Paper end - further improve server tick loop
-+            // Paper start - Add onboarding message for initial server start
-+            if (io.papermc.paper.configuration.GlobalConfiguration.isFirstStart) {
-+                LOGGER.info("*************************************************************************************");
-+                LOGGER.info("This is the first time you're starting this server.");
-+                LOGGER.info("It's recommended you read our 'Getting Started' documentation for guidance.");
-+                LOGGER.info("View this and more helpful information here: https://docs.papermc.io/paper/next-steps");
-+                LOGGER.info("*************************************************************************************");
-+            }
-+            // Paper end - Add onboarding message for initial server start
-+
-             while (this.running) {
-                 long i;
- 
-@@ -744,11 +1180,30 @@
-                     if (j > MinecraftServer.OVERLOADED_THRESHOLD_NANOS + 20L * i && this.nextTickTimeNanos - this.lastOverloadWarningNanos >= MinecraftServer.OVERLOADED_WARNING_INTERVAL_NANOS + 100L * i) {
-                         long k = j / i;
- 
-+                        if (this.server.getWarnOnOverload()) // CraftBukkit
-                         MinecraftServer.LOGGER.warn("Can't keep up! Is the server overloaded? Running {}ms or {} ticks behind", j / TimeUtil.NANOSECONDS_PER_MILLISECOND, k);
-                         this.nextTickTimeNanos += k * i;
-                         this.lastOverloadWarningNanos = this.nextTickTimeNanos;
-                     }
-+                }
-+                // Spigot start
-+                // Paper start - further improve server tick loop
-+                currentTime = Util.getNanos();
-+                if (++MinecraftServer.currentTick % MinecraftServer.SAMPLE_INTERVAL == 0) {
-+                    final long diff = currentTime - tickSection;
-+                    final java.math.BigDecimal currentTps = TPS_BASE.divide(new java.math.BigDecimal(diff), 30, java.math.RoundingMode.HALF_UP);
-+                    tps1.add(currentTps, diff);
-+                    tps5.add(currentTps, diff);
-+                    tps15.add(currentTps, diff);
-+
-+                    // Backwards compat with bad plugins
-+                    this.recentTps[0] = tps1.getAverage();
-+                    this.recentTps[1] = tps5.getAverage();
-+                    this.recentTps[2] = tps15.getAverage();
-+                    tickSection = currentTime;
-                 }
-+                // Paper end - further improve server tick loop
-+                // Spigot end
- 
-                 boolean flag = i == 0L;
- 
-@@ -757,6 +1212,8 @@
-                     this.debugCommandProfiler = new MinecraftServer.TimeProfiler(Util.getNanos(), this.tickCount);
-                 }
- 
-+                //MinecraftServer.currentTick = (int) (System.currentTimeMillis() / 50); // CraftBukkit // Paper - don't overwrite current tick time
-+                lastTick = currentTime;
-                 this.nextTickTimeNanos += i;
- 
-                 try {
-@@ -830,6 +1287,14 @@
-                     this.services.profileCache().clearExecutor();
-                 }
- 
-+                org.spigotmc.WatchdogThread.doStop(); // Spigot
-+                // CraftBukkit start - Restore terminal to original settings
-+                try {
-+                    net.minecrell.terminalconsole.TerminalConsoleAppender.close(); // Paper - Use TerminalConsoleAppender
-+                } catch (Exception ignored) {
-+                }
-+                // CraftBukkit end
-+                io.papermc.paper.log.CustomLogManager.forceReset(); // Paper - Reset loggers after shutdown
-                 this.onServerExit();
-             }
- 
-@@ -889,9 +1354,16 @@
-     }
- 
-     private boolean haveTime() {
--        return this.runningTask() || Util.getNanos() < (this.mayHaveDelayedTasks ? this.delayedTasksMaxNextTickTimeNanos : this.nextTickTimeNanos);
-+        // CraftBukkit start
-+        return this.forceTicks || this.runningTask() || Util.getNanos() < (this.mayHaveDelayedTasks ? this.delayedTasksMaxNextTickTimeNanos : this.nextTickTimeNanos);
-     }
- 
-+    private void executeModerately() {
-+        this.runAllTasks();
-+        java.util.concurrent.locks.LockSupport.parkNanos("executing tasks", 1000L);
-+        // CraftBukkit end
-+    }
-+
-     public static boolean throwIfFatalException() {
-         RuntimeException runtimeexception = (RuntimeException) MinecraftServer.fatalException.get();
- 
-@@ -903,7 +1375,7 @@
-     }
- 
-     public static void setFatalException(RuntimeException exception) {
--        MinecraftServer.fatalException.compareAndSet((Object) null, exception);
-+        MinecraftServer.fatalException.compareAndSet(null, exception); // CraftBukkit - decompile error
-     }
- 
-     @Override
-@@ -961,6 +1433,7 @@
-         if (super.pollTask()) {
-             return true;
-         } else {
-+            boolean ret = false; // Paper - force execution of all worlds, do not just bias the first
-             if (this.tickRateManager.isSprinting() || this.haveTime()) {
-                 Iterator iterator = this.getAllLevels().iterator();
- 
-@@ -968,16 +1441,16 @@
-                     ServerLevel worldserver = (ServerLevel) iterator.next();
- 
-                     if (worldserver.getChunkSource().pollTask()) {
--                        return true;
-+                        ret = true; // Paper - force execution of all worlds, do not just bias the first
-                     }
-                 }
-             }
- 
--            return false;
-+            return ret; // Paper - force execution of all worlds, do not just bias the first
-         }
-     }
- 
--    public void doRunTask(TickTask ticktask) {
-+    public void doRunTask(TickTask ticktask) { // CraftBukkit - decompile error
-         Profiler.get().incrementCounter("runTask");
-         super.doRunTask(ticktask);
-     }
-@@ -1025,27 +1498,45 @@
-     }
- 
-     public void tickServer(BooleanSupplier shouldKeepTicking) {
-+        org.spigotmc.WatchdogThread.tick(); // Spigot
-         long i = Util.getNanos();
-         int j = this.pauseWhileEmptySeconds() * 20;
- 
-+        this.removeDisabledPluginsBlockingSleep(); // Paper - API to allow/disallow tick sleeping
-         if (j > 0) {
--            if (this.playerList.getPlayerCount() == 0 && !this.tickRateManager.isSprinting()) {
-+            if (this.playerList.getPlayerCount() == 0 && !this.tickRateManager.isSprinting() && this.pluginsBlockingSleep.isEmpty()) { // Paper - API to allow/disallow tick sleeping
-                 ++this.emptyTicks;
-             } else {
-                 this.emptyTicks = 0;
-             }
- 
-             if (this.emptyTicks >= j) {
-+                this.server.spark.tickStart(); // Paper - spark
-                 if (this.emptyTicks == j) {
-                     MinecraftServer.LOGGER.info("Server empty for {} seconds, pausing", this.pauseWhileEmptySeconds());
-                     this.autoSave();
-                 }
- 
-+                this.server.getScheduler().mainThreadHeartbeat(); // CraftBukkit
-+                // Paper start - avoid issues with certain tasks not processing during sleep
-+                Runnable task;
-+                while ((task = this.processQueue.poll()) != null) {
-+                    task.run();
-+                }
-+                for (final ServerLevel level : this.levels.values()) {
-+                    // process unloads
-+                    level.getChunkSource().tick(() -> true, false);
-+                }
-+                // Paper end - avoid issues with certain tasks not processing during sleep
-+                this.server.spark.executeMainThreadTasks(); // Paper - spark
-                 this.tickConnection();
-+                this.server.spark.tickEnd(((double)(System.nanoTime() - lastTick) / 1000000D)); // Paper - spark
-                 return;
-             }
-         }
- 
-+        this.server.spark.tickStart(); // Paper - spark
-+        new com.destroystokyo.paper.event.server.ServerTickStartEvent(this.tickCount+1).callEvent(); // Paper - Server Tick Events
-         ++this.tickCount;
-         this.tickRateManager.tick();
-         this.tickChildren(shouldKeepTicking);
-@@ -1055,12 +1546,20 @@
-         }
- 
-         --this.ticksUntilAutosave;
--        if (this.ticksUntilAutosave <= 0) {
-+        if (this.autosavePeriod > 0 && this.ticksUntilAutosave <= 0) { // CraftBukkit
-             this.autoSave();
-         }
- 
-         ProfilerFiller gameprofilerfiller = Profiler.get();
- 
-+        this.runAllTasks(); // Paper - move runAllTasks() into full server tick (previously for timings)
-+        this.server.spark.executeMainThreadTasks(); // Paper - spark
-+        // Paper start - Server Tick Events
-+        long endTime = System.nanoTime();
-+        long remaining = (TICK_TIME - (endTime - lastTick)) - catchupTime;
-+        new com.destroystokyo.paper.event.server.ServerTickEndEvent(this.tickCount, ((double)(endTime - lastTick) / 1000000D), remaining).callEvent();
-+        // Paper end - Server Tick Events
-+        this.server.spark.tickEnd(((double)(endTime - lastTick) / 1000000D)); // Paper - spark
-         gameprofilerfiller.push("tallying");
-         long k = Util.getNanos() - i;
-         int l = this.tickCount % 100;
-@@ -1069,12 +1568,17 @@
-         this.aggregatedTickTimesNanos += k;
-         this.tickTimesNanos[l] = k;
-         this.smoothedTickTimeMillis = this.smoothedTickTimeMillis * 0.8F + (float) k / (float) TimeUtil.NANOSECONDS_PER_MILLISECOND * 0.19999999F;
-+        // Paper start - Add tick times API and /mspt command
-+        this.tickTimes5s.add(this.tickCount, k);
-+        this.tickTimes10s.add(this.tickCount, k);
-+        this.tickTimes60s.add(this.tickCount, k);
-+        // Paper end - Add tick times API and /mspt command
-         this.logTickMethodTime(i);
-         gameprofilerfiller.pop();
-     }
- 
-     private void autoSave() {
--        this.ticksUntilAutosave = this.computeNextAutosaveInterval();
-+        this.ticksUntilAutosave = this.autosavePeriod; // CraftBukkit
-         MinecraftServer.LOGGER.debug("Autosave started");
-         ProfilerFiller gameprofilerfiller = Profiler.get();
- 
-@@ -1123,7 +1627,7 @@
-     private ServerStatus buildServerStatus() {
-         ServerStatus.Players serverping_serverpingplayersample = this.buildPlayerStatus();
- 
--        return new ServerStatus(Component.nullToEmpty(this.motd), Optional.of(serverping_serverpingplayersample), Optional.of(ServerStatus.Version.current()), Optional.ofNullable(this.statusIcon), this.enforceSecureProfile());
-+        return new ServerStatus(io.papermc.paper.adventure.PaperAdventure.asVanilla(this.motd), Optional.of(serverping_serverpingplayersample), Optional.of(ServerStatus.Version.current()), Optional.ofNullable(this.statusIcon), this.enforceSecureProfile()); // Paper - Adventure
-     }
- 
-     private ServerStatus.Players buildPlayerStatus() {
-@@ -1133,7 +1637,7 @@
-         if (this.hidesOnlinePlayers()) {
-             return new ServerStatus.Players(i, list.size(), List.of());
-         } else {
--            int j = Math.min(list.size(), 12);
-+            int j = Math.min(list.size(), org.spigotmc.SpigotConfig.playerSample); // Paper - PaperServerListPingEvent
-             ObjectArrayList<GameProfile> objectarraylist = new ObjectArrayList(j);
-             int k = Mth.nextInt(this.random, 0, list.size() - j);
- 
-@@ -1154,24 +1658,72 @@
-         this.getPlayerList().getPlayers().forEach((entityplayer) -> {
-             entityplayer.connection.suspendFlushing();
-         });
-+        this.server.getScheduler().mainThreadHeartbeat(); // CraftBukkit
-+        // Paper start - Folia scheduler API
-+        ((io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler) Bukkit.getGlobalRegionScheduler()).tick();
-+        getAllLevels().forEach(level -> {
-+            for (final Entity entity : level.getEntities().getAll()) {
-+                if (entity.isRemoved()) {
-+                    continue;
-+                }
-+                final org.bukkit.craftbukkit.entity.CraftEntity bukkit = entity.getBukkitEntityRaw();
-+                if (bukkit != null) {
-+                    bukkit.taskScheduler.executeTick();
-+                }
-+            }
-+        });
-+        // Paper end - Folia scheduler API
-+        io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue(this.tickCount); // Paper
-         gameprofilerfiller.push("commandFunctions");
-         this.getFunctions().tick();
-         gameprofilerfiller.popPush("levels");
--        Iterator iterator = this.getAllLevels().iterator();
-+        //Iterator iterator = this.getAllLevels().iterator(); // Paper - Throw exception on world create while being ticked; moved down
- 
-+        // CraftBukkit start
-+        // Run tasks that are waiting on processing
-+        while (!this.processQueue.isEmpty()) {
-+            this.processQueue.remove().run();
-+        }
-+
-+        // Send time updates to everyone, it will get the right time from the world the player is in.
-+        // Paper start - Perf: Optimize time updates
-+        for (final ServerLevel level : this.getAllLevels()) {
-+            final boolean doDaylight = level.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT);
-+            final long dayTime = level.getDayTime();
-+            long worldTime = level.getGameTime();
-+            final ClientboundSetTimePacket worldPacket = new ClientboundSetTimePacket(worldTime, dayTime, doDaylight);
-+            for (Player entityhuman : level.players()) {
-+                if (!(entityhuman instanceof ServerPlayer) || (tickCount + entityhuman.getId()) % 20 != 0) {
-+                    continue;
-+                }
-+                ServerPlayer entityplayer = (ServerPlayer) entityhuman;
-+                long playerTime = entityplayer.getPlayerTime();
-+                ClientboundSetTimePacket packet = (playerTime == dayTime) ? worldPacket :
-+                    new ClientboundSetTimePacket(worldTime, playerTime, doDaylight);
-+                entityplayer.connection.send(packet); // Add support for per player time
-+                // Paper end - Perf: Optimize time updates
-+            }
-+        }
-+
-+        this.isIteratingOverLevels = true; // Paper - Throw exception on world create while being ticked
-+        Iterator iterator = this.getAllLevels().iterator(); // Paper - Throw exception on world create while being ticked; move down
-         while (iterator.hasNext()) {
-             ServerLevel worldserver = (ServerLevel) iterator.next();
-+            worldserver.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - BlockPhysicsEvent
-+            worldserver.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent
- 
-             gameprofilerfiller.push(() -> {
-                 String s = String.valueOf(worldserver);
- 
-                 return s + " " + String.valueOf(worldserver.dimension().location());
-             });
-+            /* Drop global time updates
-             if (this.tickCount % 20 == 0) {
-                 gameprofilerfiller.push("timeSync");
-                 this.synchronizeTime(worldserver);
-                 gameprofilerfiller.pop();
-             }
-+            // CraftBukkit end */
- 
-             gameprofilerfiller.push("tick");
- 
-@@ -1186,7 +1738,9 @@
- 
-             gameprofilerfiller.pop();
-             gameprofilerfiller.pop();
-+            worldserver.explosionDensityCache.clear(); // Paper - Optimize explosions
-         }
-+        this.isIteratingOverLevels = false; // Paper - Throw exception on world create while being ticked
- 
-         gameprofilerfiller.popPush("connection");
-         this.tickConnection();
-@@ -1267,6 +1821,22 @@
-         return (ServerLevel) this.levels.get(key);
-     }
- 
-+    // CraftBukkit start
-+    public void addLevel(ServerLevel level) {
-+        Map<ResourceKey<Level>, ServerLevel> oldLevels = this.levels;
-+        Map<ResourceKey<Level>, ServerLevel> newLevels = Maps.newLinkedHashMap(oldLevels);
-+        newLevels.put(level.dimension(), level);
-+        this.levels = Collections.unmodifiableMap(newLevels);
-+    }
-+
-+    public void removeLevel(ServerLevel level) {
-+        Map<ResourceKey<Level>, ServerLevel> oldLevels = this.levels;
-+        Map<ResourceKey<Level>, ServerLevel> newLevels = Maps.newLinkedHashMap(oldLevels);
-+        newLevels.remove(level.dimension());
-+        this.levels = Collections.unmodifiableMap(newLevels);
-+    }
-+    // CraftBukkit end
-+
-     public Set<ResourceKey<Level>> levelKeys() {
-         return this.levels.keySet();
-     }
-@@ -1296,7 +1866,7 @@
- 
-     @DontObfuscate
-     public String getServerModName() {
--        return "vanilla";
-+        return io.papermc.paper.ServerBuildInfo.buildInfo().brandName(); // Paper
-     }
- 
-     public SystemReport fillSystemReport(SystemReport details) {
-@@ -1347,7 +1917,7 @@
- 
-     @Override
-     public void sendSystemMessage(Component message) {
--        MinecraftServer.LOGGER.info(message.getString());
-+        MinecraftServer.LOGGER.info(io.papermc.paper.adventure.PaperAdventure.ANSI_SERIALIZER.serialize(io.papermc.paper.adventure.PaperAdventure.asAdventure(message))); // Paper - Log message with colors
-     }
- 
-     public KeyPair getKeyPair() {
-@@ -1385,11 +1955,14 @@
-         }
-     }
- 
--    public void setDifficulty(Difficulty difficulty, boolean forceUpdate) {
--        if (forceUpdate || !this.worldData.isDifficultyLocked()) {
--            this.worldData.setDifficulty(this.worldData.isHardcore() ? Difficulty.HARD : difficulty);
--            this.updateMobSpawningFlags();
--            this.getPlayerList().getPlayers().forEach(this::sendDifficultyUpdate);
-+    // Paper start - per level difficulty
-+    public void setDifficulty(ServerLevel level, Difficulty difficulty, boolean forceUpdate) {
-+        PrimaryLevelData worldData = level.serverLevelData;
-+        if (forceUpdate || !worldData.isDifficultyLocked()) {
-+            worldData.setDifficulty(worldData.isHardcore() ? Difficulty.HARD : difficulty);
-+            level.setSpawnSettings(worldData.getDifficulty() != Difficulty.PEACEFUL && ((DedicatedServer) this).settings.getProperties().spawnMonsters);
-+            // this.getPlayerList().getPlayers().forEach(this::sendDifficultyUpdate);
-+            // Paper end - per level difficulty
-         }
-     }
- 
-@@ -1403,7 +1976,7 @@
-         while (iterator.hasNext()) {
-             ServerLevel worldserver = (ServerLevel) iterator.next();
- 
--            worldserver.setSpawnSettings(this.isSpawningMonsters());
-+            worldserver.setSpawnSettings(worldserver.serverLevelData.getDifficulty() != Difficulty.PEACEFUL && ((DedicatedServer) this).settings.getProperties().spawnMonsters); // Paper - per level difficulty (from setDifficulty(ServerLevel, Difficulty, boolean))
-         }
- 
-     }
-@@ -1481,10 +2054,20 @@
- 
-     @Override
-     public String getMotd() {
--        return this.motd;
-+        return net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(this.motd); // Paper - Adventure
-     }
- 
-     public void setMotd(String motd) {
-+        // Paper start - Adventure
-+        this.motd = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserializeOr(motd, net.kyori.adventure.text.Component.empty());
-+    }
-+
-+    public net.kyori.adventure.text.Component motd() {
-+        return this.motd;
-+    }
-+
-+    public void motd(net.kyori.adventure.text.Component motd) {
-+        // Paper end - Adventure
-         this.motd = motd;
-     }
- 
-@@ -1507,7 +2090,7 @@
-     }
- 
-     public ServerConnectionListener getConnection() {
--        return this.connection;
-+        return this.connection == null ? this.connection = new ServerConnectionListener(this) : this.connection; // Spigot
-     }
- 
-     public boolean isReady() {
-@@ -1593,7 +2176,7 @@
-     @Override
-     public void executeIfPossible(Runnable runnable) {
-         if (this.isStopped()) {
--            throw new RejectedExecutionException("Server already shutting down");
-+            throw new io.papermc.paper.util.ServerStopRejectedExecutionException("Server already shutting down"); // Paper - do not prematurely disconnect players on stop
-         } else {
-             super.executeIfPossible(runnable);
-         }
-@@ -1632,16 +2215,22 @@
-         return this.functionManager;
-     }
- 
-+    // Paper start - Add ServerResourcesReloadedEvent
-+    @Deprecated @io.papermc.paper.annotation.DoNotUse
-     public CompletableFuture<Void> reloadResources(Collection<String> dataPacks) {
-+        return this.reloadResources(dataPacks, io.papermc.paper.event.server.ServerResourcesReloadedEvent.Cause.PLUGIN);
-+    }
-+    public CompletableFuture<Void> reloadResources(Collection<String> dataPacks, io.papermc.paper.event.server.ServerResourcesReloadedEvent.Cause cause) {
-+        // Paper end - Add ServerResourcesReloadedEvent
-         CompletableFuture<Void> completablefuture = CompletableFuture.supplyAsync(() -> {
--            Stream stream = dataPacks.stream();
-+            Stream<String> stream = dataPacks.stream(); // CraftBukkit - decompile error
-             PackRepository resourcepackrepository = this.packRepository;
- 
-             Objects.requireNonNull(this.packRepository);
--            return (ImmutableList) stream.map(resourcepackrepository::getPack).filter(Objects::nonNull).map(Pack::open).collect(ImmutableList.toImmutableList());
-+            return stream.<Pack>map(resourcepackrepository::getPack).filter(Objects::nonNull).map(Pack::open).collect(ImmutableList.toImmutableList()); // CraftBukkit - decompile error // Paper - decompile error // todo: is this needed anymore?
-         }, this).thenCompose((immutablelist) -> {
-             MultiPackResourceManager resourcemanager = new MultiPackResourceManager(PackType.SERVER_DATA, immutablelist);
--            List<Registry.PendingTags<?>> list = TagLoader.loadTagsForExistingRegistries(resourcemanager, this.registries.compositeAccess());
-+            List<Registry.PendingTags<?>> list = TagLoader.loadTagsForExistingRegistries(resourcemanager, this.registries.compositeAccess(), io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent.Cause.RELOAD); // Paper - tag lifecycle - add cause
- 
-             return ReloadableServerResources.loadResources(resourcemanager, this.registries, list, this.worldData.enabledFeatures(), this.isDedicatedServer() ? Commands.CommandSelection.DEDICATED : Commands.CommandSelection.INTEGRATED, this.getFunctionCompilationLevel(), this.executor, this).whenComplete((datapackresources, throwable) -> {
-                 if (throwable != null) {
-@@ -1652,6 +2241,7 @@
-                 return new MinecraftServer.ReloadableResources(resourcemanager, datapackresources);
-             });
-         }).thenAcceptAsync((minecraftserver_reloadableresources) -> {
-+            io.papermc.paper.command.brigadier.PaperBrigadier.moveBukkitCommands(this.resources.managers().getCommands(), minecraftserver_reloadableresources.managers().commands); // Paper
-             this.resources.close();
-             this.resources = minecraftserver_reloadableresources;
-             this.packRepository.setSelected(dataPacks);
-@@ -1660,11 +2250,23 @@
-             this.worldData.setDataConfiguration(worlddataconfiguration);
-             this.resources.managers.updateStaticRegistryTags();
-             this.resources.managers.getRecipeManager().finalizeRecipeLoading(this.worldData.enabledFeatures());
-+            this.potionBrewing = this.potionBrewing.reload(this.worldData.enabledFeatures()); // Paper - Custom Potion Mixes
-             this.getPlayerList().saveAll();
-             this.getPlayerList().reloadResources();
-             this.functionManager.replaceLibrary(this.resources.managers.getFunctionLibrary());
-             this.structureTemplateManager.onResourceManagerReload(this.resources.resourceManager);
-             this.fuelValues = FuelValues.vanillaBurnTimes(this.registries.compositeAccess(), this.worldData.enabledFeatures());
-+            org.bukkit.craftbukkit.block.data.CraftBlockData.reloadCache(); // Paper - cache block data strings; they can be defined by datapacks so refresh it here
-+            // Paper start - brigadier command API
-+            io.papermc.paper.command.brigadier.PaperCommands.INSTANCE.setValid(); // reset invalid state for event fire below
-+            io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner.INSTANCE.callReloadableRegistrarEvent(io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents.COMMANDS, io.papermc.paper.command.brigadier.PaperCommands.INSTANCE, org.bukkit.plugin.Plugin.class, io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent.Cause.RELOAD); // call commands event for regular plugins
-+            final org.bukkit.craftbukkit.help.SimpleHelpMap helpMap = (org.bukkit.craftbukkit.help.SimpleHelpMap) this.server.getHelpMap();
-+            helpMap.clear();
-+            helpMap.initializeGeneralTopics();
-+            helpMap.initializeCommands();
-+            this.server.syncCommands(); // Refresh commands after event
-+            // Paper end
-+            new io.papermc.paper.event.server.ServerResourcesReloadedEvent(cause).callEvent(); // Paper - Add ServerResourcesReloadedEvent; fire after everything has been reloaded
-         }, this);
- 
-         if (this.isSameThread()) {
-@@ -1789,14 +2391,15 @@
-         if (this.isEnforceWhitelist()) {
-             PlayerList playerlist = source.getServer().getPlayerList();
-             UserWhiteList whitelist = playerlist.getWhiteList();
-+            if (!((DedicatedServer) getServer()).getProperties().whiteList.get()) return; // Paper - whitelist not enabled
-             List<ServerPlayer> list = Lists.newArrayList(playerlist.getPlayers());
-             Iterator iterator = list.iterator();
- 
-             while (iterator.hasNext()) {
-                 ServerPlayer entityplayer = (ServerPlayer) iterator.next();
- 
--                if (!whitelist.isWhiteListed(entityplayer.getGameProfile())) {
--                    entityplayer.connection.disconnect((Component) Component.translatable("multiplayer.disconnect.not_whitelisted"));
-+                if (!whitelist.isWhiteListed(entityplayer.getGameProfile()) && !this.getPlayerList().isOp(entityplayer.getGameProfile())) { // Paper - Fix kicking ops when whitelist is reloaded (MC-171420)
-+                    entityplayer.connection.disconnect(net.kyori.adventure.text.Component.text(org.spigotmc.SpigotConfig.whitelistMessage), org.bukkit.event.player.PlayerKickEvent.Cause.WHITELIST); // Paper - use configurable message & kick event cause
-                 }
-             }
- 
-@@ -1952,7 +2555,7 @@
-             final List<String> list = Lists.newArrayList();
-             final GameRules gamerules = this.getGameRules();
- 
--            gamerules.visitGameRuleTypes(new GameRules.GameRuleTypeVisitor(this) {
-+            gamerules.visitGameRuleTypes(new GameRules.GameRuleTypeVisitor() { // CraftBukkit - decompile error
-                 @Override
-                 public <T extends GameRules.Value<T>> void visit(GameRules.Key<T> key, GameRules.Type<T> type) {
-                     list.add(String.format(Locale.ROOT, "%s=%s\n", key.getId(), gamerules.getRule(key)));
-@@ -2058,7 +2661,7 @@
-             try {
-                 label51:
-                 {
--                    ArrayList arraylist;
-+                    ArrayList<NativeModuleLister.NativeModuleInfo> arraylist; // CraftBukkit - decompile error
- 
-                     try {
-                         arraylist = Lists.newArrayList(NativeModuleLister.listModules());
-@@ -2105,8 +2708,23 @@
-         if (bufferedwriter != null) {
-             bufferedwriter.close();
-         }
-+
-+    }
-+
-+    // CraftBukkit start
-+    public boolean isDebugging() {
-+        return false;
-+    }
-+
-+    public static MinecraftServer getServer() {
-+        return SERVER; // Paper
-+    }
- 
-+    @Deprecated
-+    public static RegistryAccess getDefaultRegistryAccess() {
-+        return CraftRegistry.getMinecraftRegistry();
-     }
-+    // CraftBukkit end
- 
-     private ProfilerFiller createProfiler() {
-         if (this.willStartRecordingMetrics) {
-@@ -2225,18 +2843,24 @@
-     }
- 
-     public void logChatMessage(Component message, ChatType.Bound params, @Nullable String prefix) {
--        String s1 = params.decorate(message).getString();
-+        // Paper start
-+        net.kyori.adventure.text.Component s1 = io.papermc.paper.adventure.PaperAdventure.asAdventure(params.decorate(message));
- 
-         if (prefix != null) {
--            MinecraftServer.LOGGER.info("[{}] {}", prefix, s1);
-+            MinecraftServer.COMPONENT_LOGGER.info("[{}] {}", prefix, s1);
-         } else {
--            MinecraftServer.LOGGER.info("{}", s1);
-+            MinecraftServer.COMPONENT_LOGGER.info("{}", s1);
-+            // Paper end
-         }
- 
-     }
- 
-+    public final java.util.concurrent.ExecutorService chatExecutor = java.util.concurrent.Executors.newCachedThreadPool(
-+            new com.google.common.util.concurrent.ThreadFactoryBuilder().setDaemon(true).setNameFormat("Async Chat Thread - #%d").setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(net.minecraft.server.MinecraftServer.LOGGER)).build()); // Paper
-+
-+    public final ChatDecorator improvedChatDecorator = new io.papermc.paper.adventure.ImprovedChatDecorator(this); // Paper - adventure
-     public ChatDecorator getChatDecorator() {
--        return ChatDecorator.PLAIN;
-+        return this.improvedChatDecorator; // Paper - support async chat decoration events
-     }
- 
-     public boolean logIPs() {
-@@ -2379,4 +3003,53 @@
-     public static record ServerResourcePackInfo(UUID id, String url, String hash, boolean isRequired, @Nullable Component prompt) {
- 
-     }
-+
-+    // Paper start - Add tick times API and /mspt command
-+    public static class TickTimes {
-+        private final long[] times;
-+
-+        public TickTimes(int length) {
-+            times = new long[length];
-+        }
-+
-+        void add(int index, long time) {
-+            times[index % times.length] = time;
-+        }
-+
-+        public long[] getTimes() {
-+            return times.clone();
-+        }
-+
-+        public double getAverage() {
-+            long total = 0L;
-+            for (long value : times) {
-+                total += value;
-+            }
-+            return ((double) total / (double) times.length) * 1.0E-6D;
-+        }
-+    }
-+    // Paper end - Add tick times API and /mspt command
-+
-+    // Paper start - API to check if the server is sleeping
-+    public boolean isTickPaused() {
-+        return this.emptyTicks > 0 && this.emptyTicks >= this.pauseWhileEmptySeconds() * 20;
-+    }
-+
-+    public void addPluginAllowingSleep(final String pluginName, final boolean value) {
-+        if (!value) {
-+            this.pluginsBlockingSleep.add(pluginName);
-+        } else {
-+            this.pluginsBlockingSleep.remove(pluginName);
-+        }
-+    }
-+
-+    private void removeDisabledPluginsBlockingSleep() {
-+        if (this.pluginsBlockingSleep.isEmpty()) {
-+            return;
-+        }
-+        this.pluginsBlockingSleep.removeIf(plugin -> (
-+            !io.papermc.paper.plugin.manager.PaperPluginManagerImpl.getInstance().isPluginEnabled(plugin)
-+        ));
-+    }
-+    // Paper end - API to check if the server is sleeping
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/ReloadableServerRegistries.java.patch b/paper-server/patches/unapplied/net/minecraft/server/ReloadableServerRegistries.java.patch
deleted file mode 100644
index 6c906a073c..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/ReloadableServerRegistries.java.patch
+++ /dev/null
@@ -1,32 +0,0 @@
---- a/net/minecraft/server/ReloadableServerRegistries.java
-+++ b/net/minecraft/server/ReloadableServerRegistries.java
-@@ -50,8 +50,9 @@
-         );
-         HolderLookup.Provider provider = HolderLookup.Provider.create(list.stream());
-         RegistryOps<JsonElement> registryOps = provider.createSerializationContext(JsonOps.INSTANCE);
-+        final io.papermc.paper.registry.data.util.Conversions conversions = new io.papermc.paper.registry.data.util.Conversions(registryOps.lookupProvider); // Paper
-         List<CompletableFuture<WritableRegistry<?>>> list2 = LootDataType.values()
--            .map(type -> scheduleRegistryLoad((LootDataType<?>)type, registryOps, resourceManager, prepareExecutor))
-+            .map(type -> scheduleRegistryLoad((LootDataType<?>)type, registryOps, resourceManager, prepareExecutor, conversions)) // Paper
-             .toList();
-         CompletableFuture<List<WritableRegistry<?>>> completableFuture = Util.sequence(list2);
-         return completableFuture.thenApplyAsync(
-@@ -60,14 +61,15 @@
-     }
- 
-     private static <T> CompletableFuture<WritableRegistry<?>> scheduleRegistryLoad(
--        LootDataType<T> type, RegistryOps<JsonElement> ops, ResourceManager resourceManager, Executor prepareExecutor
-+        LootDataType<T> type, RegistryOps<JsonElement> ops, ResourceManager resourceManager, Executor prepareExecutor, io.papermc.paper.registry.data.util.Conversions conversions // Paper
-     ) {
-         return CompletableFuture.supplyAsync(() -> {
-             WritableRegistry<T> writableRegistry = new MappedRegistry<>(type.registryKey(), Lifecycle.experimental());
-+            io.papermc.paper.registry.PaperRegistryAccess.instance().registerReloadableRegistry(type.registryKey(), writableRegistry); // Paper - register reloadable registry
-             Map<ResourceLocation, T> map = new HashMap<>();
-             SimpleJsonResourceReloadListener.scanDirectory(resourceManager, type.registryKey(), ops, type.codec(), map);
--            map.forEach((id, value) -> writableRegistry.register(ResourceKey.create(type.registryKey(), id), (T)value, DEFAULT_REGISTRATION_INFO));
--            TagLoader.loadTagsForRegistry(resourceManager, writableRegistry);
-+            map.forEach((id, value) -> io.papermc.paper.registry.PaperRegistryListenerManager.INSTANCE.registerWithListeners(writableRegistry, ResourceKey.create(type.registryKey(), id), value, DEFAULT_REGISTRATION_INFO, conversions)); // Paper - register with listeners
-+            TagLoader.loadTagsForRegistry(resourceManager, writableRegistry, io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent.Cause.RELOAD); // Paper - tag life cycle - reload
-             return writableRegistry;
-         }, prepareExecutor);
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/ServerFunctionLibrary.java.patch b/paper-server/patches/unapplied/net/minecraft/server/ServerFunctionLibrary.java.patch
deleted file mode 100644
index a5f6fd5796..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/ServerFunctionLibrary.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/server/ServerFunctionLibrary.java
-+++ b/net/minecraft/server/ServerFunctionLibrary.java
-@@ -113,7 +113,7 @@
-                             return null;
-                         }).join());
-                     this.functions = builder.build();
--                    this.tags = this.tagsLoader.build((Map<ResourceLocation, List<TagLoader.EntryWithSource>>)intermediate.getFirst());
-+                    this.tags = this.tagsLoader.build((Map<ResourceLocation, List<TagLoader.EntryWithSource>>)intermediate.getFirst(), null); // Paper - command function tags are not implemented yet
-                 },
-                 applyExecutor
-             );
diff --git a/paper-server/patches/unapplied/net/minecraft/server/ServerFunctionManager.java.patch b/paper-server/patches/unapplied/net/minecraft/server/ServerFunctionManager.java.patch
deleted file mode 100644
index 45ee083997..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/ServerFunctionManager.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/server/ServerFunctionManager.java
-+++ b/net/minecraft/server/ServerFunctionManager.java
-@@ -37,7 +37,7 @@
-     }
- 
-     public CommandDispatcher<CommandSourceStack> getDispatcher() {
--        return this.server.getCommands().getDispatcher();
-+        return this.server.getCommands().getDispatcher(); // CraftBukkit // Paper - Don't override command dispatcher
-     }
- 
-     public void tick() {
diff --git a/paper-server/patches/unapplied/net/minecraft/server/ServerTickRateManager.java.patch b/paper-server/patches/unapplied/net/minecraft/server/ServerTickRateManager.java.patch
deleted file mode 100644
index 9720b17dc9..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/ServerTickRateManager.java.patch
+++ /dev/null
@@ -1,53 +0,0 @@
---- a/net/minecraft/server/ServerTickRateManager.java
-+++ b/net/minecraft/server/ServerTickRateManager.java
-@@ -59,8 +59,14 @@
-     }
- 
-     public boolean stopSprinting() {
-+        // CraftBukkit start, add sendLog parameter
-+        return this.stopSprinting(true);
-+    }
-+
-+    public boolean stopSprinting(boolean sendLog) {
-+        // CraftBukkit end
-         if (this.remainingSprintTicks > 0L) {
--            this.finishTickSprint();
-+            this.finishTickSprint(sendLog); // CraftBukkit - add sendLog parameter
-             return true;
-         } else {
-             return false;
-@@ -78,7 +84,7 @@
-         return flag;
-     }
- 
--    private void finishTickSprint() {
-+    private void finishTickSprint(boolean sendLog) { // CraftBukkit - add sendLog parameter
-         long i = this.scheduledCurrentSprintTicks - this.remainingSprintTicks;
-         double d0 = Math.max(1.0D, (double) this.sprintTimeSpend) / (double) TimeUtil.NANOSECONDS_PER_MILLISECOND;
-         int j = (int) ((double) (TimeUtil.MILLISECONDS_PER_SECOND * i) / d0);
-@@ -86,9 +92,13 @@
- 
-         this.scheduledCurrentSprintTicks = 0L;
-         this.sprintTimeSpend = 0L;
--        this.server.createCommandSourceStack().sendSuccess(() -> {
--            return Component.translatable("commands.tick.sprint.report", j, s);
--        }, true);
-+        // CraftBukkit start - add sendLog parameter
-+        if (sendLog) {
-+            this.server.createCommandSourceStack().sendSuccess(() -> {
-+                return Component.translatable("commands.tick.sprint.report", j, s);
-+            }, true);
-+        }
-+        // CraftBukkit end
-         this.remainingSprintTicks = 0L;
-         this.setFrozen(this.previousIsFrozen);
-         this.server.onTickRateChanged();
-@@ -102,7 +112,7 @@
-             --this.remainingSprintTicks;
-             return true;
-         } else {
--            this.finishTickSprint();
-+            this.finishTickSprint(true); // CraftBukkit - add sendLog parameter
-             return false;
-         }
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/bossevents/CustomBossEvent.java.patch b/paper-server/patches/unapplied/net/minecraft/server/bossevents/CustomBossEvent.java.patch
deleted file mode 100644
index 6e80d59765..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/bossevents/CustomBossEvent.java.patch
+++ /dev/null
@@ -1,31 +0,0 @@
---- a/net/minecraft/server/bossevents/CustomBossEvent.java
-+++ b/net/minecraft/server/bossevents/CustomBossEvent.java
-@@ -18,6 +18,10 @@
- import net.minecraft.server.level.ServerPlayer;
- import net.minecraft.util.Mth;
- import net.minecraft.world.BossEvent;
-+// CraftBukkit start
-+import org.bukkit.boss.KeyedBossBar;
-+import org.bukkit.craftbukkit.boss.CraftKeyedBossbar;
-+// CraftBukkit end
- 
- public class CustomBossEvent extends ServerBossEvent {
- 
-@@ -25,7 +29,17 @@
-     private final Set<UUID> players = Sets.newHashSet();
-     private int value;
-     private int max = 100;
-+    // CraftBukkit start
-+    private KeyedBossBar bossBar;
- 
-+    public KeyedBossBar getBukkitEntity() {
-+        if (this.bossBar == null) {
-+            this.bossBar = new CraftKeyedBossbar(this);
-+        }
-+        return this.bossBar;
-+    }
-+    // CraftBukkit end
-+
-     public CustomBossEvent(ResourceLocation id, Component displayName) {
-         super(displayName, BossEvent.BossBarColor.WHITE, BossEvent.BossBarOverlay.PROGRESS);
-         this.id = id;
diff --git a/paper-server/patches/unapplied/net/minecraft/server/commands/BanPlayerCommands.java.patch b/paper-server/patches/unapplied/net/minecraft/server/commands/BanPlayerCommands.java.patch
deleted file mode 100644
index a6e8c45f71..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/commands/BanPlayerCommands.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/server/commands/BanPlayerCommands.java
-+++ b/net/minecraft/server/commands/BanPlayerCommands.java
-@@ -55,7 +55,7 @@
-                 );
-                 ServerPlayer serverPlayer = source.getServer().getPlayerList().getPlayer(gameProfile.getId());
-                 if (serverPlayer != null) {
--                    serverPlayer.connection.disconnect(Component.translatable("multiplayer.disconnect.banned"));
-+                    serverPlayer.connection.disconnect(Component.translatable("multiplayer.disconnect.banned"), org.bukkit.event.player.PlayerKickEvent.Cause.BANNED); // Paper - kick event cause
-                 }
-             }
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/commands/DifficultyCommand.java.patch b/paper-server/patches/unapplied/net/minecraft/server/commands/DifficultyCommand.java.patch
deleted file mode 100644
index 0898d7e23d..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/commands/DifficultyCommand.java.patch
+++ /dev/null
@@ -1,17 +0,0 @@
---- a/net/minecraft/server/commands/DifficultyCommand.java
-+++ b/net/minecraft/server/commands/DifficultyCommand.java
-@@ -44,11 +44,12 @@
- 
-     public static int setDifficulty(CommandSourceStack source, Difficulty difficulty) throws CommandSyntaxException {
-         MinecraftServer minecraftserver = source.getServer();
-+        net.minecraft.server.level.ServerLevel worldServer = source.getLevel(); // CraftBukkit
- 
--        if (minecraftserver.getWorldData().getDifficulty() == difficulty) {
-+        if (worldServer.getDifficulty() == difficulty) { // CraftBukkit
-             throw DifficultyCommand.ERROR_ALREADY_DIFFICULT.create(difficulty.getKey());
-         } else {
--            minecraftserver.setDifficulty(difficulty, true);
-+            minecraftserver.setDifficulty(worldServer, difficulty, true); // Paper - per level difficulty; don't skip other difficulty-changing logic (fix upstream's fix)
-             source.sendSuccess(() -> {
-                 return Component.translatable("commands.difficulty.success", difficulty.getDisplayName());
-             }, true);
diff --git a/paper-server/patches/unapplied/net/minecraft/server/commands/EffectCommands.java.patch b/paper-server/patches/unapplied/net/minecraft/server/commands/EffectCommands.java.patch
deleted file mode 100644
index 94c98f3597..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/commands/EffectCommands.java.patch
+++ /dev/null
@@ -1,29 +0,0 @@
---- a/net/minecraft/server/commands/EffectCommands.java
-+++ b/net/minecraft/server/commands/EffectCommands.java
-@@ -84,7 +84,7 @@
-             if (entity instanceof LivingEntity) {
-                 MobEffectInstance mobeffect = new MobEffectInstance(statusEffect, k, amplifier, false, showParticles);
- 
--                if (((LivingEntity) entity).addEffect(mobeffect, source.getEntity())) {
-+                if (((LivingEntity) entity).addEffect(mobeffect, source.getEntity(), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit
-                     ++j;
-                 }
-             }
-@@ -114,7 +114,7 @@
-         while (iterator.hasNext()) {
-             Entity entity = (Entity) iterator.next();
- 
--            if (entity instanceof LivingEntity && ((LivingEntity) entity).removeAllEffects()) {
-+            if (entity instanceof LivingEntity && ((LivingEntity) entity).removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit
-                 ++i;
-             }
-         }
-@@ -144,7 +144,7 @@
-         while (iterator.hasNext()) {
-             Entity entity = (Entity) iterator.next();
- 
--            if (entity instanceof LivingEntity && ((LivingEntity) entity).removeEffect(statusEffect)) {
-+            if (entity instanceof LivingEntity && ((LivingEntity) entity).removeEffect(statusEffect, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit
-                 ++i;
-             }
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/commands/GameRuleCommand.java.patch b/paper-server/patches/unapplied/net/minecraft/server/commands/GameRuleCommand.java.patch
deleted file mode 100644
index 66f9df2b3b..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/commands/GameRuleCommand.java.patch
+++ /dev/null
@@ -1,23 +0,0 @@
---- a/net/minecraft/server/commands/GameRuleCommand.java
-+++ b/net/minecraft/server/commands/GameRuleCommand.java
-@@ -34,9 +34,9 @@
- 
-     static <T extends GameRules.Value<T>> int setRule(CommandContext<CommandSourceStack> context, GameRules.Key<T> key) {
-         CommandSourceStack commandlistenerwrapper = (CommandSourceStack) context.getSource();
--        T t0 = commandlistenerwrapper.getServer().getGameRules().getRule(key);
-+        T t0 = commandlistenerwrapper.getLevel().getGameRules().getRule(key); // CraftBukkit
- 
--        t0.setFromArgument(context, "value");
-+        t0.setFromArgument(context, "value", key); // Paper - Add WorldGameRuleChangeEvent
-         commandlistenerwrapper.sendSuccess(() -> {
-             return Component.translatable("commands.gamerule.set", key.getId(), t0.toString());
-         }, true);
-@@ -44,7 +44,7 @@
-     }
- 
-     static <T extends GameRules.Value<T>> int queryRule(CommandSourceStack source, GameRules.Key<T> key) {
--        T t0 = source.getServer().getGameRules().getRule(key);
-+        T t0 = source.getLevel().getGameRules().getRule(key); // CraftBukkit
- 
-         source.sendSuccess(() -> {
-             return Component.translatable("commands.gamerule.query", key.getId(), t0.toString());
diff --git a/paper-server/patches/unapplied/net/minecraft/server/commands/GiveCommand.java.patch b/paper-server/patches/unapplied/net/minecraft/server/commands/GiveCommand.java.patch
deleted file mode 100644
index 025f1543d5..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/commands/GiveCommand.java.patch
+++ /dev/null
@@ -1,33 +0,0 @@
---- a/net/minecraft/server/commands/GiveCommand.java
-+++ b/net/minecraft/server/commands/GiveCommand.java
-@@ -38,6 +38,7 @@
- 
-     private static int giveItem(CommandSourceStack source, ItemInput item, Collection<ServerPlayer> targets, int count) throws CommandSyntaxException {
-         ItemStack itemstack = item.createItemStack(1, false);
-+        final Component displayName = itemstack.getDisplayName(); // Paper - get display name early
-         int j = itemstack.getMaxStackSize();
-         int k = j * 100;
- 
-@@ -60,7 +61,7 @@
-                     ItemEntity entityitem;
- 
-                     if (flag && itemstack1.isEmpty()) {
--                        entityitem = entityplayer.drop(itemstack, false);
-+                        entityitem = entityplayer.drop(itemstack, false, false, false); // CraftBukkit - SPIGOT-2942: Add boolean to call event
-                         if (entityitem != null) {
-                             entityitem.makeFakeItem();
-                         }
-@@ -79,11 +80,11 @@
- 
-             if (targets.size() == 1) {
-                 source.sendSuccess(() -> {
--                    return Component.translatable("commands.give.success.single", count, itemstack.getDisplayName(), ((ServerPlayer) targets.iterator().next()).getDisplayName());
-+                    return Component.translatable("commands.give.success.single", count, displayName, ((ServerPlayer) targets.iterator().next()).getDisplayName()); // Paper - use cached display name
-                 }, true);
-             } else {
-                 source.sendSuccess(() -> {
--                    return Component.translatable("commands.give.success.single", count, itemstack.getDisplayName(), targets.size());
-+                    return Component.translatable("commands.give.success.single", count, displayName, targets.size()); // Paper - use cached display name
-                 }, true);
-             }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/server/commands/ListPlayersCommand.java.patch b/paper-server/patches/unapplied/net/minecraft/server/commands/ListPlayersCommand.java.patch
deleted file mode 100644
index 93aec260e3..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/commands/ListPlayersCommand.java.patch
+++ /dev/null
@@ -1,18 +0,0 @@
---- a/net/minecraft/server/commands/ListPlayersCommand.java
-+++ b/net/minecraft/server/commands/ListPlayersCommand.java
-@@ -35,7 +35,14 @@
- 
-     private static int format(CommandSourceStack source, Function<ServerPlayer, Component> nameProvider) {
-         PlayerList playerlist = source.getServer().getPlayerList();
--        List<ServerPlayer> list = playerlist.getPlayers();
-+        // CraftBukkit start
-+        List<ServerPlayer> players = playerlist.getPlayers();
-+        if (source.getBukkitSender() instanceof org.bukkit.entity.Player) {
-+            org.bukkit.entity.Player sender = (org.bukkit.entity.Player) source.getBukkitSender();
-+            players = players.stream().filter((ep) -> sender.canSee(ep.getBukkitEntity())).collect(java.util.stream.Collectors.toList());
-+        }
-+        List<ServerPlayer> list = players;
-+        // CraftBukkit end
-         Component ichatbasecomponent = ComponentUtils.formatList(list, nameProvider);
- 
-         source.sendSuccess(() -> {
diff --git a/paper-server/patches/unapplied/net/minecraft/server/commands/LootCommand.java.patch b/paper-server/patches/unapplied/net/minecraft/server/commands/LootCommand.java.patch
deleted file mode 100644
index c579471341..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/commands/LootCommand.java.patch
+++ /dev/null
@@ -1,19 +0,0 @@
---- a/net/minecraft/server/commands/LootCommand.java
-+++ b/net/minecraft/server/commands/LootCommand.java
-@@ -95,7 +95,7 @@
-     }
- 
-     private static <T extends ArgumentBuilder<CommandSourceStack, T>> T addTargets(T rootArgument, LootCommand.TailProvider sourceConstructor) {
--        return rootArgument.then(((LiteralArgumentBuilder) net.minecraft.commands.Commands.literal("replace").then(net.minecraft.commands.Commands.literal("entity").then(net.minecraft.commands.Commands.argument("entities", EntityArgument.entities()).then(sourceConstructor.construct(net.minecraft.commands.Commands.argument("slot", SlotArgument.slot()), (commandcontext, list, commandloot_a) -> {
-+        return (T) rootArgument.then(((LiteralArgumentBuilder) net.minecraft.commands.Commands.literal("replace").then(net.minecraft.commands.Commands.literal("entity").then(net.minecraft.commands.Commands.argument("entities", EntityArgument.entities()).then(sourceConstructor.construct(net.minecraft.commands.Commands.argument("slot", SlotArgument.slot()), (commandcontext, list, commandloot_a) -> { // CraftBukkit - decompile error
-             return LootCommand.entityReplace(EntityArgument.getEntities(commandcontext, "entities"), SlotArgument.getSlot(commandcontext, "slot"), list.size(), list, commandloot_a);
-         }).then(sourceConstructor.construct(net.minecraft.commands.Commands.argument("count", IntegerArgumentType.integer(0)), (commandcontext, list, commandloot_a) -> {
-             return LootCommand.entityReplace(EntityArgument.getEntities(commandcontext, "entities"), SlotArgument.getSlot(commandcontext, "slot"), IntegerArgumentType.getInteger(commandcontext, "count"), list, commandloot_a);
-@@ -250,6 +250,7 @@
-     private static int dropInWorld(CommandSourceStack source, Vec3 pos, List<ItemStack> stacks, LootCommand.Callback messageSender) throws CommandSyntaxException {
-         ServerLevel worldserver = source.getLevel();
- 
-+        stacks.removeIf(ItemStack::isEmpty); // CraftBukkit - SPIGOT-6959 Remove empty items for avoid throw an error in new EntityItem
-         stacks.forEach((itemstack) -> {
-             ItemEntity entityitem = new ItemEntity(worldserver, pos.x, pos.y, pos.z, itemstack.copy());
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/server/commands/PlaceCommand.java.patch b/paper-server/patches/unapplied/net/minecraft/server/commands/PlaceCommand.java.patch
deleted file mode 100644
index 1c1e4cacb4..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/commands/PlaceCommand.java.patch
+++ /dev/null
@@ -1,10 +0,0 @@
---- a/net/minecraft/server/commands/PlaceCommand.java
-+++ b/net/minecraft/server/commands/PlaceCommand.java
-@@ -132,6 +132,7 @@
-         if (!structurestart.isValid()) {
-             throw PlaceCommand.ERROR_STRUCTURE_FAILED.create();
-         } else {
-+            structurestart.generationEventCause = org.bukkit.event.world.AsyncStructureGenerateEvent.Cause.COMMAND; // CraftBukkit - set AsyncStructureGenerateEvent.Cause.COMMAND as generation cause
-             BoundingBox structureboundingbox = structurestart.getBoundingBox();
-             ChunkPos chunkcoordintpair = new ChunkPos(SectionPos.blockToSectionCoord(structureboundingbox.minX()), SectionPos.blockToSectionCoord(structureboundingbox.minZ()));
-             ChunkPos chunkcoordintpair1 = new ChunkPos(SectionPos.blockToSectionCoord(structureboundingbox.maxX()), SectionPos.blockToSectionCoord(structureboundingbox.maxZ()));
diff --git a/paper-server/patches/unapplied/net/minecraft/server/commands/ReloadCommand.java.patch b/paper-server/patches/unapplied/net/minecraft/server/commands/ReloadCommand.java.patch
deleted file mode 100644
index f0b9cdfcce..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/commands/ReloadCommand.java.patch
+++ /dev/null
@@ -1,28 +0,0 @@
---- a/net/minecraft/server/commands/ReloadCommand.java
-+++ b/net/minecraft/server/commands/ReloadCommand.java
-@@ -20,7 +20,7 @@
-     public ReloadCommand() {}
- 
-     public static void reloadPacks(Collection<String> dataPacks, CommandSourceStack source) {
--        source.getServer().reloadResources(dataPacks).exceptionally((throwable) -> {
-+        source.getServer().reloadResources(dataPacks, io.papermc.paper.event.server.ServerResourcesReloadedEvent.Cause.COMMAND).exceptionally((throwable) -> { // Paper - Add ServerResourcesReloadedEvent
-             ReloadCommand.LOGGER.warn("Failed to execute reload", throwable);
-             source.sendFailure(Component.translatable("commands.reload.failure"));
-             return null;
-@@ -44,6 +44,16 @@
-         return collection1;
-     }
- 
-+    // CraftBukkit start
-+    public static void reload(MinecraftServer minecraftserver) {
-+        PackRepository resourcepackrepository = minecraftserver.getPackRepository();
-+        WorldData savedata = minecraftserver.getWorldData();
-+        Collection<String> collection = resourcepackrepository.getSelectedIds();
-+        Collection<String> collection1 = ReloadCommand.discoverNewPacks(resourcepackrepository, savedata, collection);
-+        minecraftserver.reloadResources(collection1, io.papermc.paper.event.server.ServerResourcesReloadedEvent.Cause.PLUGIN); // Paper - Add ServerResourcesReloadedEvent
-+    }
-+    // CraftBukkit end
-+
-     public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
-         dispatcher.register((LiteralArgumentBuilder) ((LiteralArgumentBuilder) net.minecraft.commands.Commands.literal("reload").requires((commandlistenerwrapper) -> {
-             return commandlistenerwrapper.hasPermission(2);
diff --git a/paper-server/patches/unapplied/net/minecraft/server/commands/ScheduleCommand.java.patch b/paper-server/patches/unapplied/net/minecraft/server/commands/ScheduleCommand.java.patch
deleted file mode 100644
index 885d99adba..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/commands/ScheduleCommand.java.patch
+++ /dev/null
@@ -1,29 +0,0 @@
---- a/net/minecraft/server/commands/ScheduleCommand.java
-+++ b/net/minecraft/server/commands/ScheduleCommand.java
-@@ -33,7 +33,7 @@
-     });
-     private static final SimpleCommandExceptionType ERROR_MACRO = new SimpleCommandExceptionType(Component.translatableEscape("commands.schedule.macro"));
-     private static final SuggestionProvider<CommandSourceStack> SUGGEST_SCHEDULE = (commandcontext, suggestionsbuilder) -> {
--        return SharedSuggestionProvider.suggest((Iterable) ((CommandSourceStack) commandcontext.getSource()).getServer().getWorldData().overworldData().getScheduledEvents().getEventsIds(), suggestionsbuilder);
-+        return SharedSuggestionProvider.suggest((Iterable) ((net.minecraft.commands.CommandSourceStack) commandcontext.getSource()).getLevel().serverLevelData.getScheduledEvents().getEventsIds(), suggestionsbuilder); // Paper - Make schedule command per-world
-     };
- 
-     public ScheduleCommand() {}
-@@ -58,7 +58,7 @@
-         } else {
-             long j = source.getLevel().getGameTime() + (long) time;
-             ResourceLocation minecraftkey = (ResourceLocation) function.getFirst();
--            TimerQueue<MinecraftServer> customfunctioncallbacktimerqueue = source.getServer().getWorldData().overworldData().getScheduledEvents();
-+            TimerQueue<MinecraftServer> customfunctioncallbacktimerqueue = source.getLevel().serverLevelData.overworldData().getScheduledEvents(); // CraftBukkit - SPIGOT-6667: Use world specific function timer
-             Optional<net.minecraft.commands.functions.CommandFunction<CommandSourceStack>> optional = ((Either) function.getSecond()).left();
-             String s;
- 
-@@ -93,7 +93,7 @@
-     }
- 
-     private static int remove(CommandSourceStack source, String eventName) throws CommandSyntaxException {
--        int i = source.getServer().getWorldData().overworldData().getScheduledEvents().remove(eventName);
-+        int i = source.getLevel().serverLevelData.getScheduledEvents().remove(eventName); // Paper - Make schedule command per-world
- 
-         if (i == 0) {
-             throw ScheduleCommand.ERROR_CANT_REMOVE.create(eventName);
diff --git a/paper-server/patches/unapplied/net/minecraft/server/commands/SetSpawnCommand.java.patch b/paper-server/patches/unapplied/net/minecraft/server/commands/SetSpawnCommand.java.patch
deleted file mode 100644
index f4a5c32f80..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/commands/SetSpawnCommand.java.patch
+++ /dev/null
@@ -1,42 +0,0 @@
---- a/net/minecraft/server/commands/SetSpawnCommand.java
-+++ b/net/minecraft/server/commands/SetSpawnCommand.java
-@@ -38,24 +38,34 @@
-         ResourceKey<Level> resourcekey = source.getLevel().dimension();
-         Iterator iterator = targets.iterator();
- 
-+        final Collection<ServerPlayer> actualTargets = new java.util.ArrayList<>(); // Paper - Add PlayerSetSpawnEvent
-         while (iterator.hasNext()) {
-             ServerPlayer entityplayer = (ServerPlayer) iterator.next();
- 
--            entityplayer.setRespawnPosition(resourcekey, pos, angle, true, false);
-+            // Paper start - Add PlayerSetSpawnEvent
-+            if (entityplayer.setRespawnPosition(resourcekey, pos, angle, true, false, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.COMMAND)) {
-+                actualTargets.add(entityplayer);
-+            }
-+            // Paper end - Add PlayerSetSpawnEvent
-         }
-+        // Paper start - Add PlayerSetSpawnEvent
-+        if (actualTargets.isEmpty()) {
-+            return 0;
-+        }
-+        // Paper end - Add PlayerSetSpawnEvent
- 
-         String s = resourcekey.location().toString();
- 
--        if (targets.size() == 1) {
-+        if (actualTargets.size() == 1) { // Paper - Add PlayerSetSpawnEvent
-             source.sendSuccess(() -> {
--                return Component.translatable("commands.spawnpoint.success.single", pos.getX(), pos.getY(), pos.getZ(), angle, s, ((ServerPlayer) targets.iterator().next()).getDisplayName());
-+                return Component.translatable("commands.spawnpoint.success.single", pos.getX(), pos.getY(), pos.getZ(), angle, s, ((ServerPlayer) actualTargets.iterator().next()).getDisplayName()); // Paper - Add PlayerSetSpawnEvent
-             }, true);
-         } else {
-             source.sendSuccess(() -> {
--                return Component.translatable("commands.spawnpoint.success.multiple", pos.getX(), pos.getY(), pos.getZ(), angle, s, targets.size());
-+                return Component.translatable("commands.spawnpoint.success.multiple", pos.getX(), pos.getY(), pos.getZ(), angle, s, actualTargets.size()); // Paper - Add PlayerSetSpawnEvent
-             }, true);
-         }
- 
--        return targets.size();
-+        return actualTargets.size(); // Paper - Add PlayerSetSpawnEvent
-     }
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/commands/SpreadPlayersCommand.java.patch b/paper-server/patches/unapplied/net/minecraft/server/commands/SpreadPlayersCommand.java.patch
deleted file mode 100644
index 1827c81c69..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/commands/SpreadPlayersCommand.java.patch
+++ /dev/null
@@ -1,20 +0,0 @@
---- a/net/minecraft/server/commands/SpreadPlayersCommand.java
-+++ b/net/minecraft/server/commands/SpreadPlayersCommand.java
-@@ -93,7 +93,7 @@
-             if (entity instanceof Player) {
-                 set.add(entity.getTeam());
-             } else {
--                set.add((Object) null);
-+                set.add((Team) null); // CraftBukkit - decompile error
-             }
-         }
- 
-@@ -203,7 +203,7 @@
-                 commandspreadplayers_a = piles[j++];
-             }
- 
--            entity.teleportTo(world, (double) Mth.floor(commandspreadplayers_a.x) + 0.5D, (double) commandspreadplayers_a.getSpawnY(world, maxY), (double) Mth.floor(commandspreadplayers_a.z) + 0.5D, Set.of(), entity.getYRot(), entity.getXRot(), true);
-+            entity.teleportTo(world, (double) Mth.floor(commandspreadplayers_a.x) + 0.5D, (double) commandspreadplayers_a.getSpawnY(world, maxY), (double) Mth.floor(commandspreadplayers_a.z) + 0.5D, Set.of(), entity.getYRot(), entity.getXRot(), true, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.COMMAND); // CraftBukkit - handle teleport reason
-             d1 = Double.MAX_VALUE;
-             SpreadPlayersCommand.Position[] acommandspreadplayers_a1 = piles;
-             int k = piles.length;
diff --git a/paper-server/patches/unapplied/net/minecraft/server/commands/SummonCommand.java.patch b/paper-server/patches/unapplied/net/minecraft/server/commands/SummonCommand.java.patch
deleted file mode 100644
index 5a74f92631..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/commands/SummonCommand.java.patch
+++ /dev/null
@@ -1,19 +0,0 @@
---- a/net/minecraft/server/commands/SummonCommand.java
-+++ b/net/minecraft/server/commands/SummonCommand.java
-@@ -57,6 +57,7 @@
-             ServerLevel worldserver = source.getLevel();
-             Entity entity = EntityType.loadEntityRecursive(nbttagcompound1, worldserver, EntitySpawnReason.COMMAND, (entity1) -> {
-                 entity1.moveTo(pos.x, pos.y, pos.z, entity1.getYRot(), entity1.getXRot());
-+                entity1.spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.COMMAND; // Paper - Entity#getEntitySpawnReason
-                 return entity1;
-             });
- 
-@@ -67,7 +68,7 @@
-                     ((Mob) entity).finalizeSpawn(source.getLevel(), source.getLevel().getCurrentDifficultyAt(entity.blockPosition()), EntitySpawnReason.COMMAND, (SpawnGroupData) null);
-                 }
- 
--                if (!worldserver.tryAddFreshEntityWithPassengers(entity)) {
-+                if (!worldserver.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.COMMAND)) { // CraftBukkit - pass a spawn reason of "COMMAND"
-                     throw SummonCommand.ERROR_DUPLICATE_UUID.create();
-                 } else {
-                     return entity;
diff --git a/paper-server/patches/unapplied/net/minecraft/server/commands/TeleportCommand.java.patch b/paper-server/patches/unapplied/net/minecraft/server/commands/TeleportCommand.java.patch
deleted file mode 100644
index 0126e4d505..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/commands/TeleportCommand.java.patch
+++ /dev/null
@@ -1,55 +0,0 @@
---- a/net/minecraft/server/commands/TeleportCommand.java
-+++ b/net/minecraft/server/commands/TeleportCommand.java
-@@ -22,6 +22,7 @@
- import net.minecraft.core.BlockPos;
- import net.minecraft.network.chat.Component;
- import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
- import net.minecraft.util.Mth;
- import net.minecraft.world.entity.Entity;
- import net.minecraft.world.entity.LivingEntity;
-@@ -30,6 +31,11 @@
- import net.minecraft.world.level.Level;
- import net.minecraft.world.phys.Vec2;
- import net.minecraft.world.phys.Vec3;
-+import org.bukkit.Location;
-+import org.bukkit.craftbukkit.CraftWorld;
-+import org.bukkit.event.entity.EntityTeleportEvent;
-+import org.bukkit.event.player.PlayerTeleportEvent;
-+// CraftBukkit end
- 
- public class TeleportCommand {
- 
-@@ -167,7 +173,31 @@
-             float f4 = Mth.wrapDegrees(f2);
-             float f5 = Mth.wrapDegrees(f3);
- 
--            if (target.teleportTo(world, d3, d4, d5, movementFlags, f4, f5, true)) {
-+            // CraftBukkit start - Teleport event
-+            boolean result;
-+            if (target instanceof ServerPlayer player) {
-+                result = player.teleportTo(world, d3, d4, d5, movementFlags, f4, f5, true, PlayerTeleportEvent.TeleportCause.COMMAND);
-+            } else {
-+                Location to = new Location(world.getWorld(), d3, d4, d5, f4, f5);
-+                EntityTeleportEvent event = new EntityTeleportEvent(target.getBukkitEntity(), target.getBukkitEntity().getLocation(), to);
-+                world.getCraftServer().getPluginManager().callEvent(event);
-+                if (event.isCancelled() || event.getTo() == null) { // Paper
-+                    return;
-+                }
-+                to = event.getTo(); // Paper - actually track new location
-+
-+                d3 = to.getX();
-+                d4 = to.getY();
-+                d5 = to.getZ();
-+                f4 = to.getYaw();
-+                f5 = to.getPitch();
-+                world = ((CraftWorld) to.getWorld()).getHandle();
-+
-+                result = target.teleportTo(world, d3, d4, d5, movementFlags, f4, f5, true);
-+            }
-+
-+            if (result) {
-+                // CraftBukkit end
-                 if (facingLocation != null) {
-                     facingLocation.perform(source, target);
-                 }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/commands/TimeCommand.java.patch b/paper-server/patches/unapplied/net/minecraft/server/commands/TimeCommand.java.patch
deleted file mode 100644
index 52c7c73aa7..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/commands/TimeCommand.java.patch
+++ /dev/null
@@ -1,55 +0,0 @@
---- a/net/minecraft/server/commands/TimeCommand.java
-+++ b/net/minecraft/server/commands/TimeCommand.java
-@@ -8,6 +8,10 @@
- import net.minecraft.commands.arguments.TimeArgument;
- import net.minecraft.network.chat.Component;
- import net.minecraft.server.level.ServerLevel;
-+// CraftBukkit start
-+import org.bukkit.Bukkit;
-+import org.bukkit.event.world.TimeSkipEvent;
-+// CraftBukkit end
- 
- public class TimeCommand {
- 
-@@ -49,12 +53,18 @@
-     }
- 
-     public static int setTime(CommandSourceStack source, int time) {
--        Iterator iterator = source.getServer().getAllLevels().iterator();
-+        Iterator iterator = io.papermc.paper.configuration.GlobalConfiguration.get().commands.timeCommandAffectsAllWorlds ? source.getServer().getAllLevels().iterator() : com.google.common.collect.Iterators.singletonIterator(source.getLevel()); // CraftBukkit - SPIGOT-6496: Only set the time for the world the command originates in // Paper - add config option for spigot's change
- 
-         while (iterator.hasNext()) {
-             ServerLevel worldserver = (ServerLevel) iterator.next();
- 
--            worldserver.setDayTime((long) time);
-+            // CraftBukkit start
-+            TimeSkipEvent event = new TimeSkipEvent(worldserver.getWorld(), TimeSkipEvent.SkipReason.COMMAND, time - worldserver.getDayTime());
-+            Bukkit.getPluginManager().callEvent(event);
-+            if (!event.isCancelled()) {
-+                worldserver.setDayTime((long) worldserver.getDayTime() + event.getSkipAmount());
-+            }
-+            // CraftBukkit end
-         }
- 
-         source.getServer().forceTimeSynchronization();
-@@ -65,12 +75,18 @@
-     }
- 
-     public static int addTime(CommandSourceStack source, int time) {
--        Iterator iterator = source.getServer().getAllLevels().iterator();
-+        Iterator iterator = io.papermc.paper.configuration.GlobalConfiguration.get().commands.timeCommandAffectsAllWorlds ? source.getServer().getAllLevels().iterator() : com.google.common.collect.Iterators.singletonIterator(source.getLevel()); // CraftBukkit - SPIGOT-6496: Only set the time for the world the command originates in // Paper - add config option for spigot's change
- 
-         while (iterator.hasNext()) {
-             ServerLevel worldserver = (ServerLevel) iterator.next();
- 
--            worldserver.setDayTime(worldserver.getDayTime() + (long) time);
-+            // CraftBukkit start
-+            TimeSkipEvent event = new TimeSkipEvent(worldserver.getWorld(), TimeSkipEvent.SkipReason.COMMAND, time);
-+            Bukkit.getPluginManager().callEvent(event);
-+            if (!event.isCancelled()) {
-+                worldserver.setDayTime(worldserver.getDayTime() + event.getSkipAmount());
-+            }
-+            // CraftBukkit end
-         }
- 
-         source.getServer().forceTimeSynchronization();
diff --git a/paper-server/patches/unapplied/net/minecraft/server/commands/WeatherCommand.java.patch b/paper-server/patches/unapplied/net/minecraft/server/commands/WeatherCommand.java.patch
deleted file mode 100644
index 2f916fdf48..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/commands/WeatherCommand.java.patch
+++ /dev/null
@@ -1,34 +0,0 @@
---- a/net/minecraft/server/commands/WeatherCommand.java
-+++ b/net/minecraft/server/commands/WeatherCommand.java
-@@ -34,11 +34,11 @@
-     }
- 
-     private static int getDuration(CommandSourceStack source, int duration, IntProvider provider) {
--        return duration == -1 ? provider.sample(source.getServer().overworld().getRandom()) : duration;
-+        return duration == -1 ? provider.sample(source.getLevel().getRandom()) : duration; // CraftBukkit - SPIGOT-7680: per-world
-     }
- 
-     private static int setClear(CommandSourceStack source, int duration) {
--        source.getServer().overworld().setWeatherParameters(WeatherCommand.getDuration(source, duration, ServerLevel.RAIN_DELAY), 0, false, false);
-+        source.getLevel().setWeatherParameters(WeatherCommand.getDuration(source, duration, ServerLevel.RAIN_DELAY), 0, false, false); // CraftBukkit - SPIGOT-7680: per-world
-         source.sendSuccess(() -> {
-             return Component.translatable("commands.weather.set.clear");
-         }, true);
-@@ -46,7 +46,7 @@
-     }
- 
-     private static int setRain(CommandSourceStack source, int duration) {
--        source.getServer().overworld().setWeatherParameters(0, WeatherCommand.getDuration(source, duration, ServerLevel.RAIN_DURATION), true, false);
-+        source.getLevel().setWeatherParameters(0, WeatherCommand.getDuration(source, duration, ServerLevel.RAIN_DURATION), true, false); // CraftBukkit - SPIGOT-7680: per-world
-         source.sendSuccess(() -> {
-             return Component.translatable("commands.weather.set.rain");
-         }, true);
-@@ -54,7 +54,7 @@
-     }
- 
-     private static int setThunder(CommandSourceStack source, int duration) {
--        source.getServer().overworld().setWeatherParameters(0, WeatherCommand.getDuration(source, duration, ServerLevel.THUNDER_DURATION), true, true);
-+        source.getLevel().setWeatherParameters(0, WeatherCommand.getDuration(source, duration, ServerLevel.THUNDER_DURATION), true, true); // CraftBukkit - SPIGOT-7680: per-world
-         source.sendSuccess(() -> {
-             return Component.translatable("commands.weather.set.thunder");
-         }, true);
diff --git a/paper-server/patches/unapplied/net/minecraft/server/commands/WorldBorderCommand.java.patch b/paper-server/patches/unapplied/net/minecraft/server/commands/WorldBorderCommand.java.patch
deleted file mode 100644
index d64cdd2c7d..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/commands/WorldBorderCommand.java.patch
+++ /dev/null
@@ -1,65 +0,0 @@
---- a/net/minecraft/server/commands/WorldBorderCommand.java
-+++ b/net/minecraft/server/commands/WorldBorderCommand.java
-@@ -57,7 +57,7 @@
-     }
- 
-     private static int setDamageBuffer(CommandSourceStack source, float distance) throws CommandSyntaxException {
--        WorldBorder worldborder = source.getServer().overworld().getWorldBorder();
-+        WorldBorder worldborder = source.getLevel().getWorldBorder(); // CraftBukkit
- 
-         if (worldborder.getDamageSafeZone() == (double) distance) {
-             throw WorldBorderCommand.ERROR_SAME_DAMAGE_BUFFER.create();
-@@ -71,7 +71,7 @@
-     }
- 
-     private static int setDamageAmount(CommandSourceStack source, float damagePerBlock) throws CommandSyntaxException {
--        WorldBorder worldborder = source.getServer().overworld().getWorldBorder();
-+        WorldBorder worldborder = source.getLevel().getWorldBorder(); // CraftBukkit
- 
-         if (worldborder.getDamagePerBlock() == (double) damagePerBlock) {
-             throw WorldBorderCommand.ERROR_SAME_DAMAGE_AMOUNT.create();
-@@ -85,7 +85,7 @@
-     }
- 
-     private static int setWarningTime(CommandSourceStack source, int time) throws CommandSyntaxException {
--        WorldBorder worldborder = source.getServer().overworld().getWorldBorder();
-+        WorldBorder worldborder = source.getLevel().getWorldBorder(); // CraftBukkit
- 
-         if (worldborder.getWarningTime() == time) {
-             throw WorldBorderCommand.ERROR_SAME_WARNING_TIME.create();
-@@ -99,7 +99,7 @@
-     }
- 
-     private static int setWarningDistance(CommandSourceStack source, int distance) throws CommandSyntaxException {
--        WorldBorder worldborder = source.getServer().overworld().getWorldBorder();
-+        WorldBorder worldborder = source.getLevel().getWorldBorder(); // CraftBukkit
- 
-         if (worldborder.getWarningBlocks() == distance) {
-             throw WorldBorderCommand.ERROR_SAME_WARNING_DISTANCE.create();
-@@ -113,7 +113,7 @@
-     }
- 
-     private static int getSize(CommandSourceStack source) {
--        double d0 = source.getServer().overworld().getWorldBorder().getSize();
-+        double d0 = source.getLevel().getWorldBorder().getSize(); // CraftBukkit
- 
-         source.sendSuccess(() -> {
-             return Component.translatable("commands.worldborder.get", String.format(Locale.ROOT, "%.0f", d0));
-@@ -122,7 +122,7 @@
-     }
- 
-     private static int setCenter(CommandSourceStack source, Vec2 pos) throws CommandSyntaxException {
--        WorldBorder worldborder = source.getServer().overworld().getWorldBorder();
-+        WorldBorder worldborder = source.getLevel().getWorldBorder(); // CraftBukkit
- 
-         if (worldborder.getCenterX() == (double) pos.x && worldborder.getCenterZ() == (double) pos.y) {
-             throw WorldBorderCommand.ERROR_SAME_CENTER.create();
-@@ -138,7 +138,7 @@
-     }
- 
-     private static int setSize(CommandSourceStack source, double distance, long time) throws CommandSyntaxException {
--        WorldBorder worldborder = source.getServer().overworld().getWorldBorder();
-+        WorldBorder worldborder = source.getLevel().getWorldBorder(); // CraftBukkit
-         double d1 = worldborder.getSize();
- 
-         if (d1 == distance) {
diff --git a/paper-server/patches/unapplied/net/minecraft/server/dedicated/DedicatedServerProperties.java.patch b/paper-server/patches/unapplied/net/minecraft/server/dedicated/DedicatedServerProperties.java.patch
deleted file mode 100644
index 2020814765..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/dedicated/DedicatedServerProperties.java.patch
+++ /dev/null
@@ -1,106 +0,0 @@
---- a/net/minecraft/server/dedicated/DedicatedServerProperties.java
-+++ b/net/minecraft/server/dedicated/DedicatedServerProperties.java
-@@ -43,11 +43,16 @@
- import net.minecraft.world.level.levelgen.presets.WorldPresets;
- import org.slf4j.Logger;
- 
-+// CraftBukkit start
-+import joptsimple.OptionSet;
-+// CraftBukkit end
-+
- public class DedicatedServerProperties extends Settings<DedicatedServerProperties> {
- 
-     static final Logger LOGGER = LogUtils.getLogger();
-     private static final Pattern SHA1 = Pattern.compile("^[a-fA-F0-9]{40}$");
-     private static final Splitter COMMA_SPLITTER = Splitter.on(',').trimResults();
-+    public final boolean debug = this.get("debug", false); // CraftBukkit
-     public final boolean onlineMode = this.get("online-mode", true);
-     public final boolean preventProxyConnections = this.get("prevent-proxy-connections", false);
-     public final String serverIp = this.get("server-ip", "");
-@@ -100,13 +105,17 @@
-     public final Settings<DedicatedServerProperties>.MutableValue<Boolean> whiteList;
-     public final boolean enforceSecureProfile;
-     public final boolean logIPs;
--    public final int pauseWhenEmptySeconds;
-+    public int pauseWhenEmptySeconds;
-     private final DedicatedServerProperties.WorldDimensionData worldDimensionData;
-     public final WorldOptions worldOptions;
-     public boolean acceptsTransfers;
- 
--    public DedicatedServerProperties(Properties properties) {
--        super(properties);
-+    public final String rconIp; // Paper - Configurable rcon ip
-+
-+    // CraftBukkit start
-+    public DedicatedServerProperties(Properties properties, OptionSet optionset) {
-+        super(properties, optionset);
-+        // CraftBukkit end
-         this.difficulty = (Difficulty) this.get("difficulty", dispatchNumberOrString(Difficulty::byId, Difficulty::byName), Difficulty::getKey, Difficulty.EASY);
-         this.gamemode = (GameType) this.get("gamemode", dispatchNumberOrString(GameType::byId, GameType::byName), GameType::getName, GameType.SURVIVAL);
-         this.levelName = this.get("level-name", "world");
-@@ -137,7 +146,7 @@
-         this.maxWorldSize = this.get("max-world-size", (integer) -> {
-             return Mth.clamp(integer, 1, 29999984);
-         }, 29999984);
--        this.syncChunkWrites = this.get("sync-chunk-writes", true);
-+        this.syncChunkWrites = this.get("sync-chunk-writes", true) && Boolean.getBoolean("Paper.enable-sync-chunk-writes"); // Paper - Hide sync chunk writes behind flag
-         this.regionFileComression = this.get("region-file-compression", "deflate");
-         this.enableJmxMonitoring = this.get("enable-jmx-monitoring", false);
-         this.enableStatus = this.get("enable-status", true);
-@@ -151,7 +160,7 @@
-         this.whiteList = this.getMutable("white-list", false);
-         this.enforceSecureProfile = this.get("enforce-secure-profile", true);
-         this.logIPs = this.get("log-ips", true);
--        this.pauseWhenEmptySeconds = this.get("pause-when-empty-seconds", 60);
-+        this.pauseWhenEmptySeconds = this.get("pause-when-empty-seconds", -1); // Paper - disable tick sleeping by default
-         this.acceptsTransfers = this.get("accepts-transfers", false);
-         String s = this.get("level-seed", "");
-         boolean flag = this.get("generate-structures", true);
-@@ -165,15 +174,21 @@
-         }, WorldPresets.NORMAL.location().toString()));
-         this.serverResourcePackInfo = DedicatedServerProperties.getServerPackInfo(this.get("resource-pack-id", ""), this.get("resource-pack", ""), this.get("resource-pack-sha1", ""), this.getLegacyString("resource-pack-hash"), this.get("require-resource-pack", false), this.get("resource-pack-prompt", ""));
-         this.initialDataPackConfiguration = DedicatedServerProperties.getDatapackConfig(this.get("initial-enabled-packs", String.join(",", WorldDataConfiguration.DEFAULT.dataPacks().getEnabled())), this.get("initial-disabled-packs", String.join(",", WorldDataConfiguration.DEFAULT.dataPacks().getDisabled())));
-+        // Paper start - Configurable rcon ip
-+        final String rconIp = this.getStringRaw("rcon.ip");
-+        this.rconIp = rconIp == null ? this.serverIp : rconIp;
-+        // Paper end - Configurable rcon ip
-     }
- 
--    public static DedicatedServerProperties fromFile(Path path) {
--        return new DedicatedServerProperties(loadFromFile(path));
-+    // CraftBukkit start
-+    public static DedicatedServerProperties fromFile(Path path, OptionSet optionset) {
-+        return new DedicatedServerProperties(loadFromFile(path), optionset);
-     }
- 
-     @Override
--    protected DedicatedServerProperties reload(RegistryAccess registryManager, Properties properties) {
--        return new DedicatedServerProperties(properties);
-+    public DedicatedServerProperties reload(RegistryAccess iregistrycustom, Properties properties, OptionSet optionset) {
-+        return new DedicatedServerProperties(properties, optionset);
-+        // CraftBukkit end
-     }
- 
-     @Nullable
-@@ -254,10 +269,10 @@
-             }).orElseThrow(() -> {
-                 return new IllegalStateException("Invalid datapack contents: can't find default preset");
-             });
--            Optional optional = Optional.ofNullable(ResourceLocation.tryParse(this.levelType)).map((minecraftkey) -> {
-+            Optional<ResourceKey<WorldPreset>> optional = Optional.ofNullable(ResourceLocation.tryParse(this.levelType)).map((minecraftkey) -> { // CraftBukkit - decompile error
-                 return ResourceKey.create(Registries.WORLD_PRESET, minecraftkey);
-             }).or(() -> {
--                return Optional.ofNullable((ResourceKey) DedicatedServerProperties.WorldDimensionData.LEGACY_PRESET_NAMES.get(this.levelType));
-+                return Optional.ofNullable(DedicatedServerProperties.WorldDimensionData.LEGACY_PRESET_NAMES.get(this.levelType)); // CraftBukkit - decompile error
-             });
- 
-             Objects.requireNonNull(holderlookup);
-@@ -269,7 +284,7 @@
- 
-             if (holder.is(WorldPresets.FLAT)) {
-                 RegistryOps<JsonElement> registryops = registries.createSerializationContext(JsonOps.INSTANCE);
--                DataResult dataresult = FlatLevelGeneratorSettings.CODEC.parse(new Dynamic(registryops, this.generatorSettings()));
-+                DataResult<FlatLevelGeneratorSettings> dataresult = FlatLevelGeneratorSettings.CODEC.parse(new Dynamic(registryops, this.generatorSettings())); // CraftBukkit - decompile error
-                 Logger logger = DedicatedServerProperties.LOGGER;
- 
-                 Objects.requireNonNull(logger);
diff --git a/paper-server/patches/unapplied/net/minecraft/server/dedicated/DedicatedServerSettings.java.patch b/paper-server/patches/unapplied/net/minecraft/server/dedicated/DedicatedServerSettings.java.patch
deleted file mode 100644
index 0aaaffd34b..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/dedicated/DedicatedServerSettings.java.patch
+++ /dev/null
@@ -1,27 +0,0 @@
---- a/net/minecraft/server/dedicated/DedicatedServerSettings.java
-+++ b/net/minecraft/server/dedicated/DedicatedServerSettings.java
-@@ -3,14 +3,21 @@
- import java.nio.file.Path;
- import java.util.function.UnaryOperator;
- 
-+// CraftBukkit start
-+import java.io.File;
-+import joptsimple.OptionSet;
-+// CraftBukkit end
-+
- public class DedicatedServerSettings {
- 
-     private final Path source;
-     private DedicatedServerProperties properties;
- 
--    public DedicatedServerSettings(Path path) {
--        this.source = path;
--        this.properties = DedicatedServerProperties.fromFile(path);
-+    // CraftBukkit start
-+    public DedicatedServerSettings(OptionSet optionset) {
-+        this.source = ((File) optionset.valueOf("config")).toPath();
-+        this.properties = DedicatedServerProperties.fromFile(this.source, optionset);
-+        // CraftBukkit end
-     }
- 
-     public DedicatedServerProperties getProperties() {
diff --git a/paper-server/patches/unapplied/net/minecraft/server/dedicated/Settings.java.patch b/paper-server/patches/unapplied/net/minecraft/server/dedicated/Settings.java.patch
deleted file mode 100644
index 300eeaa029..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/dedicated/Settings.java.patch
+++ /dev/null
@@ -1,179 +0,0 @@
---- a/net/minecraft/server/dedicated/Settings.java
-+++ b/net/minecraft/server/dedicated/Settings.java
-@@ -20,20 +20,41 @@
- import java.util.function.Supplier;
- import java.util.function.UnaryOperator;
- import javax.annotation.Nullable;
--import net.minecraft.core.RegistryAccess;
- import org.slf4j.Logger;
- 
-+import joptsimple.OptionSet; // CraftBukkit
-+import net.minecraft.core.RegistryAccess;
-+
- public abstract class Settings<T extends Settings<T>> {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
-     public final Properties properties;
-+    private static final boolean skipComments = Boolean.getBoolean("Paper.skipServerPropertiesComments"); // Paper - allow skipping server.properties comments
-+    // CraftBukkit start
-+    private OptionSet options = null;
- 
--    public Settings(Properties properties) {
-+    public Settings(Properties properties, final OptionSet options) {
-         this.properties = properties;
-+
-+        this.options = options;
-     }
- 
-+    private String getOverride(String name, String value) {
-+        if ((this.options != null) && (this.options.has(name))) {
-+            return String.valueOf(this.options.valueOf(name));
-+        }
-+
-+        return value;
-+        // CraftBukkit end
-+    }
-+
-     public static Properties loadFromFile(Path path) {
-         try {
-+            // CraftBukkit start - SPIGOT-7465, MC-264979: Don't load if file doesn't exist
-+            if (!path.toFile().exists()) {
-+                return new Properties();
-+            }
-+            // CraftBukkit end
-             Properties properties;
-             Properties properties1;
- 
-@@ -97,8 +118,53 @@
- 
-     public void store(Path path) {
-         try {
--            BufferedWriter bufferedwriter = Files.newBufferedWriter(path, StandardCharsets.UTF_8);
-+            // CraftBukkit start - Don't attempt writing to file if it's read only
-+            if (path.toFile().exists() && !path.toFile().canWrite()) {
-+                Settings.LOGGER.warn("Can not write to file {}, skipping.", path); // Paper - log message file is read-only
-+                return;
-+            }
-+            // CraftBukkit end
-+            // Paper start - allow skipping server.properties comments
-+            java.io.OutputStream outputstream = Files.newOutputStream(path);
-+            java.io.BufferedOutputStream bufferedOutputStream =  !skipComments ? new java.io.BufferedOutputStream(outputstream) : new java.io.BufferedOutputStream(outputstream) {
-+                private boolean isRightAfterNewline = true; // If last written char was newline
-+                private boolean isComment = false; // Are we writing comment currently?
-+
-+                @Override
-+                public void write(@org.jetbrains.annotations.NotNull byte[] b) throws IOException {
-+                    this.write(b, 0, b.length);
-+                }
-+
-+                @Override
-+                public void write(@org.jetbrains.annotations.NotNull byte[] bbuf, int off, int len) throws IOException {
-+                    int latest_offset = off; // The latest offset, updated when comment ends
-+                    for (int index = off; index < off + len; ++index ) {
-+                        byte c = bbuf[index];
-+                        boolean isNewline = (c == '\n' || c == '\r');
-+                        if (isNewline && this.isComment) {
-+                            // Comment has ended
-+                            this.isComment = false;
-+                            latest_offset = index+1;
-+                        }
-+                        if (c == '#' && this.isRightAfterNewline) {
-+                            this.isComment = true;
-+                            if (index != latest_offset) {
-+                                // We got some non-comment data earlier
-+                                super.write(bbuf, latest_offset, index-latest_offset);
-+                            }
-+                        }
-+                        this.isRightAfterNewline = isNewline; // Store for next iteration
- 
-+                    }
-+                    if (latest_offset < off+len && !this.isComment) {
-+                        // We have some unwritten data, that isn't part of a comment
-+                        super.write(bbuf, latest_offset, (off + len) - latest_offset);
-+                    }
-+                }
-+            };
-+            BufferedWriter bufferedwriter = new BufferedWriter(new java.io.OutputStreamWriter(bufferedOutputStream, java.nio.charset.StandardCharsets.UTF_8.newEncoder()));
-+            // Paper end - allow skipping server.properties comments
-+
-             try {
-                 this.properties.store(bufferedwriter, "Minecraft server properties");
-             } catch (Throwable throwable) {
-@@ -125,7 +191,7 @@
-     private static <V extends Number> Function<String, V> wrapNumberDeserializer(Function<String, V> parser) {
-         return (s) -> {
-             try {
--                return (Number) parser.apply(s);
-+                return (V) parser.apply(s); // CraftBukkit - decompile error
-             } catch (NumberFormatException numberformatexception) {
-                 return null;
-             }
-@@ -144,7 +210,7 @@
- 
-     @Nullable
-     public String getStringRaw(String key) {
--        return (String) this.properties.get(key);
-+        return (String) this.getOverride(key, this.properties.getProperty(key)); // CraftBukkit
-     }
- 
-     @Nullable
-@@ -160,10 +226,20 @@
-     }
- 
-     protected <V> V get(String key, Function<String, V> parser, Function<V, String> stringifier, V fallback) {
--        String s1 = this.getStringRaw(key);
--        V v1 = MoreObjects.firstNonNull(s1 != null ? parser.apply(s1) : null, fallback);
-+        // CraftBukkit start
-+        try {
-+            return this.get0(key, parser, stringifier, fallback);
-+        } catch (Exception ex) {
-+            throw new RuntimeException("Could not load invalidly configured property '" + key + "'", ex);
-+        }
-+    }
- 
--        this.properties.put(key, stringifier.apply(v1));
-+    private <V> V get0(String s, Function<String, V> function, Function<V, String> function1, V v0) {
-+        // CraftBukkit end
-+        String s1 = this.getStringRaw(s);
-+        V v1 = MoreObjects.firstNonNull(s1 != null ? function.apply(s1) : null, v0);
-+
-+        this.properties.put(s, function1.apply(v1));
-         return v1;
-     }
- 
-@@ -172,7 +248,7 @@
-         V v1 = MoreObjects.firstNonNull(s1 != null ? parser.apply(s1) : null, fallback);
- 
-         this.properties.put(key, stringifier.apply(v1));
--        return new Settings.MutableValue<>(key, v1, stringifier);
-+        return new Settings.MutableValue(key, v1, stringifier); // CraftBukkit - decompile error
-     }
- 
-     protected <V> V get(String key, Function<String, V> parser, UnaryOperator<V> parsedTransformer, Function<V, String> stringifier, V fallback) {
-@@ -236,7 +312,7 @@
-         return properties;
-     }
- 
--    protected abstract T reload(RegistryAccess registryManager, Properties properties);
-+    protected abstract T reload(RegistryAccess iregistrycustom, Properties properties, OptionSet optionset); // CraftBukkit
- 
-     public class MutableValue<V> implements Supplier<V> {
- 
-@@ -244,7 +320,7 @@
-         private final V value;
-         private final Function<V, String> serializer;
- 
--        MutableValue(final String s, final Object object, final Function function) {
-+        MutableValue(final String s, final V object, final Function function) { // CraftBukkit - decompile error
-             this.key = s;
-             this.value = object;
-             this.serializer = function;
-@@ -258,7 +334,7 @@
-             Properties properties = Settings.this.cloneProperties();
- 
-             properties.put(this.key, this.serializer.apply(value));
--            return Settings.this.reload(registryManager, properties);
-+            return Settings.this.reload(registryManager, properties, Settings.this.options); // CraftBukkit
-         }
-     }
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/level/ChunkHolder.java.patch b/paper-server/patches/unapplied/net/minecraft/server/level/ChunkHolder.java.patch
deleted file mode 100644
index 4516416d44..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/level/ChunkHolder.java.patch
+++ /dev/null
@@ -1,245 +0,0 @@
---- a/net/minecraft/server/level/ChunkHolder.java
-+++ b/net/minecraft/server/level/ChunkHolder.java
-@@ -28,14 +28,18 @@
- import net.minecraft.world.level.chunk.status.ChunkStatus;
- import net.minecraft.world.level.lighting.LevelLightEngine;
- 
-+// CraftBukkit start
-+import net.minecraft.server.MinecraftServer;
-+// CraftBukkit end
-+
- public class ChunkHolder extends GenerationChunkHolder {
- 
-     public static final ChunkResult<LevelChunk> UNLOADED_LEVEL_CHUNK = ChunkResult.error("Unloaded level chunk");
-     private static final CompletableFuture<ChunkResult<LevelChunk>> UNLOADED_LEVEL_CHUNK_FUTURE = CompletableFuture.completedFuture(ChunkHolder.UNLOADED_LEVEL_CHUNK);
-     private final LevelHeightAccessor levelHeightAccessor;
--    private volatile CompletableFuture<ChunkResult<LevelChunk>> fullChunkFuture;
--    private volatile CompletableFuture<ChunkResult<LevelChunk>> tickingChunkFuture;
--    private volatile CompletableFuture<ChunkResult<LevelChunk>> entityTickingChunkFuture;
-+    private volatile CompletableFuture<ChunkResult<LevelChunk>> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage
-+    private volatile CompletableFuture<ChunkResult<LevelChunk>> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage
-+    private volatile CompletableFuture<ChunkResult<LevelChunk>> entityTickingChunkFuture; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage
-     public int oldTicketLevel;
-     private int ticketLevel;
-     private int queueLevel;
-@@ -58,9 +62,9 @@
-         this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
-         this.blockChangedLightSectionFilter = new BitSet();
-         this.skyChangedLightSectionFilter = new BitSet();
--        this.pendingFullStateConfirmation = CompletableFuture.completedFuture((Object) null);
--        this.sendSync = CompletableFuture.completedFuture((Object) null);
--        this.saveSync = CompletableFuture.completedFuture((Object) null);
-+        this.pendingFullStateConfirmation = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error
-+        this.sendSync = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error
-+        this.saveSync = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error
-         this.levelHeightAccessor = world;
-         this.lightEngine = lightingProvider;
-         this.onLevelChange = levelUpdateListener;
-@@ -72,6 +76,18 @@
-         this.changedBlocksPerSection = new ShortSet[world.getSectionsCount()];
-     }
- 
-+    // CraftBukkit start
-+    public LevelChunk getFullChunkNow() {
-+        // Note: We use the oldTicketLevel for isLoaded checks.
-+        if (!ChunkLevel.fullStatus(this.oldTicketLevel).isOrAfter(FullChunkStatus.FULL)) return null;
-+        return this.getFullChunkNowUnchecked();
-+    }
-+
-+    public LevelChunk getFullChunkNowUnchecked() {
-+        return (LevelChunk) this.getChunkIfPresentUnchecked(ChunkStatus.FULL);
-+    }
-+    // CraftBukkit end
-+
-     public CompletableFuture<ChunkResult<LevelChunk>> getTickingChunkFuture() {
-         return this.tickingChunkFuture;
-     }
-@@ -85,8 +101,8 @@
-     }
- 
-     @Nullable
--    public LevelChunk getTickingChunk() {
--        return (LevelChunk) ((ChunkResult) this.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).orElse((Object) null);
-+    public final LevelChunk getTickingChunk() { // Paper - final for inline
-+        return (LevelChunk) ((ChunkResult) this.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).orElse(null); // CraftBukkit - decompile error
-     }
- 
-     @Nullable
-@@ -138,6 +154,7 @@
-             boolean flag = this.hasChangedSections;
-             int i = this.levelHeightAccessor.getSectionIndex(pos.getY());
- 
-+            if (i < 0 || i >= this.changedBlocksPerSection.length) return false; // CraftBukkit - SPIGOT-6086, SPIGOT-6296
-             if (this.changedBlocksPerSection[i] == null) {
-                 this.hasChangedSections = true;
-                 this.changedBlocksPerSection[i] = new ShortOpenHashSet();
-@@ -224,8 +241,11 @@
-                                 ClientboundSectionBlocksUpdatePacket packetplayoutmultiblockchange = new ClientboundSectionBlocksUpdatePacket(sectionposition, shortset, chunksection);
- 
-                                 this.broadcast(list, packetplayoutmultiblockchange);
-+                                // CraftBukkit start
-+                                List finalList = list;
-                                 packetplayoutmultiblockchange.runUpdates((blockposition1, iblockdata1) -> {
--                                    this.broadcastBlockEntityIfNeeded(list, world, blockposition1, iblockdata1);
-+                                    this.broadcastBlockEntityIfNeeded(finalList, world, blockposition1, iblockdata1);
-+                                    // CraftBukkit end
-                                 });
-                             }
-                         }
-@@ -291,7 +311,7 @@
-         this.pendingFullStateConfirmation = completablefuture1;
-         chunkFuture.thenAccept((chunkresult) -> {
-             chunkresult.ifSuccess((chunk) -> {
--                completablefuture1.complete((Object) null);
-+                completablefuture1.complete(null); // CraftBukkit - decompile error
-             });
-         });
-     }
-@@ -301,6 +321,38 @@
-         chunkLoadingManager.onFullChunkStatusChange(this.pos, target);
-     }
- 
-+    // CraftBukkit start
-+    // ChunkUnloadEvent: Called before the chunk is unloaded: isChunkLoaded is still true and chunk can still be modified by plugins.
-+    // SPIGOT-7780: Moved out of updateFutures to call all chunk unload events before calling updateHighestAllowedStatus for all chunks
-+    protected void callEventIfUnloading(ChunkMap playerchunkmap) {
-+        FullChunkStatus oldFullChunkStatus = ChunkLevel.fullStatus(this.oldTicketLevel);
-+        FullChunkStatus newFullChunkStatus = ChunkLevel.fullStatus(this.ticketLevel);
-+        boolean oldIsFull = oldFullChunkStatus.isOrAfter(FullChunkStatus.FULL);
-+        boolean newIsFull = newFullChunkStatus.isOrAfter(FullChunkStatus.FULL);
-+        if (oldIsFull && !newIsFull) {
-+            this.getFullChunkFuture().thenAccept((either) -> {
-+                LevelChunk chunk = (LevelChunk) either.orElse(null);
-+                if (chunk != null) {
-+                    playerchunkmap.callbackExecutor.execute(() -> {
-+                        // Minecraft will apply the chunks tick lists to the world once the chunk got loaded, and then store the tick
-+                        // lists again inside the chunk once the chunk becomes inaccessible and set the chunk's needsSaving flag.
-+                        // These actions may however happen deferred, so we manually set the needsSaving flag already here.
-+                        chunk.markUnsaved();
-+                        chunk.unloadCallback();
-+                    });
-+                }
-+            }).exceptionally((throwable) -> {
-+                // ensure exceptions are printed, by default this is not the case
-+                MinecraftServer.LOGGER.error("Failed to schedule unload callback for chunk " + ChunkHolder.this.pos, throwable);
-+                return null;
-+            });
-+
-+            // Run callback right away if the future was already done
-+            playerchunkmap.callbackExecutor.run();
-+        }
-+    }
-+    // CraftBukkit end
-+
-     protected void updateFutures(ChunkMap chunkLoadingManager, Executor executor) {
-         FullChunkStatus fullchunkstatus = ChunkLevel.fullStatus(this.oldTicketLevel);
-         FullChunkStatus fullchunkstatus1 = ChunkLevel.fullStatus(this.ticketLevel);
-@@ -309,12 +361,28 @@
- 
-         this.wasAccessibleSinceLastSave |= flag1;
-         if (!flag && flag1) {
-+            int expectCreateCount = ++this.fullChunkCreateCount; // Paper
-             this.fullChunkFuture = chunkLoadingManager.prepareAccessibleChunk(this);
-             this.scheduleFullChunkPromotion(chunkLoadingManager, this.fullChunkFuture, executor, FullChunkStatus.FULL);
-+            // Paper start - cache ticking ready status
-+            this.fullChunkFuture.thenAccept(chunkResult -> {
-+                chunkResult.ifSuccess(chunk -> {
-+                    if (ChunkHolder.this.fullChunkCreateCount == expectCreateCount) {
-+                        ChunkHolder.this.isFullChunkReady = true;
-+                        ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkBorder(chunk, this);
-+                    }
-+                });
-+            });
-+            // Paper end - cache ticking ready status
-             this.addSaveDependency(this.fullChunkFuture);
-         }
- 
-         if (flag && !flag1) {
-+            // Paper start
-+            if (this.isFullChunkReady) {
-+                ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotBorder(this.fullChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
-+            }
-+            // Paper end
-             this.fullChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
-             this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
-         }
-@@ -325,11 +393,25 @@
-         if (!flag2 && flag3) {
-             this.tickingChunkFuture = chunkLoadingManager.prepareTickingChunk(this);
-             this.scheduleFullChunkPromotion(chunkLoadingManager, this.tickingChunkFuture, executor, FullChunkStatus.BLOCK_TICKING);
-+            // Paper start - cache ticking ready status
-+            this.tickingChunkFuture.thenAccept(chunkResult -> {
-+                chunkResult.ifSuccess(chunk -> {
-+                    // note: Here is a very good place to add callbacks to logic waiting on this.
-+                    ChunkHolder.this.isTickingReady = true;
-+                    ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkTicking(chunk, this);
-+                });
-+            });
-+            // Paper end
-             this.addSaveDependency(this.tickingChunkFuture);
-         }
- 
-         if (flag2 && !flag3) {
--            this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
-+            // Paper start
-+            if (this.isTickingReady) {
-+                ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotTicking(this.tickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
-+            }
-+            // Paper end
-+            this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isTickingReady = false; // Paper - cache chunk ticking stage
-             this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
-         }
- 
-@@ -343,11 +425,24 @@
- 
-             this.entityTickingChunkFuture = chunkLoadingManager.prepareEntityTickingChunk(this);
-             this.scheduleFullChunkPromotion(chunkLoadingManager, this.entityTickingChunkFuture, executor, FullChunkStatus.ENTITY_TICKING);
-+            // Paper start - cache ticking ready status
-+            this.entityTickingChunkFuture.thenAccept(chunkResult -> {
-+                chunkResult.ifSuccess(chunk -> {
-+                    ChunkHolder.this.isEntityTickingReady = true;
-+                    ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkEntityTicking(chunk, this);
-+                });
-+            });
-+            // Paper end
-             this.addSaveDependency(this.entityTickingChunkFuture);
-         }
- 
-         if (flag4 && !flag5) {
--            this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
-+            // Paper start
-+            if (this.isEntityTickingReady) {
-+                ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotEntityTicking(this.entityTickingChunkFuture.join().orElseThrow(IllegalStateException::new), this);
-+            }
-+            // Paper end
-+            this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isEntityTickingReady = false; // Paper - cache chunk ticking stage
-             this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
-         }
- 
-@@ -357,6 +452,26 @@
- 
-         this.onLevelChange.onLevelChange(this.pos, this::getQueueLevel, this.ticketLevel, this::setQueueLevel);
-         this.oldTicketLevel = this.ticketLevel;
-+        // CraftBukkit start
-+        // ChunkLoadEvent: Called after the chunk is loaded: isChunkLoaded returns true and chunk is ready to be modified by plugins.
-+        if (!fullchunkstatus.isOrAfter(FullChunkStatus.FULL) && fullchunkstatus1.isOrAfter(FullChunkStatus.FULL)) {
-+            this.getFullChunkFuture().thenAccept((either) -> {
-+                LevelChunk chunk = (LevelChunk) either.orElse(null);
-+                if (chunk != null) {
-+                    chunkLoadingManager.callbackExecutor.execute(() -> {
-+                        chunk.loadCallback();
-+                    });
-+                }
-+            }).exceptionally((throwable) -> {
-+                // ensure exceptions are printed, by default this is not the case
-+                MinecraftServer.LOGGER.error("Failed to schedule load callback for chunk " + ChunkHolder.this.pos, throwable);
-+                return null;
-+            });
-+
-+            // Run callback right away if the future was already done
-+            chunkLoadingManager.callbackExecutor.run();
-+        }
-+        // CraftBukkit end
-     }
- 
-     public boolean wasAccessibleSinceLastSave() {
diff --git a/paper-server/patches/unapplied/net/minecraft/server/level/ChunkMap.java.patch b/paper-server/patches/unapplied/net/minecraft/server/level/ChunkMap.java.patch
deleted file mode 100644
index 9a5c5895b8..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/level/ChunkMap.java.patch
+++ /dev/null
@@ -1,433 +0,0 @@
---- a/net/minecraft/server/level/ChunkMap.java
-+++ b/net/minecraft/server/level/ChunkMap.java
-@@ -104,6 +104,10 @@
- import org.apache.commons.lang3.mutable.MutableBoolean;
- import org.slf4j.Logger;
- 
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.generator.CustomChunkGenerator;
-+// CraftBukkit end
-+
- public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider, GeneratingChunkMap {
- 
-     private static final ChunkResult<List<ChunkAccess>> UNLOADED_CHUNK_LIST_RESULT = ChunkResult.error("Unloaded chunks found in range");
-@@ -149,6 +153,33 @@
-     public int serverViewDistance;
-     private final WorldGenContext worldGenContext;
- 
-+    // CraftBukkit start - recursion-safe executor for Chunk loadCallback() and unloadCallback()
-+    public final CallbackExecutor callbackExecutor = new CallbackExecutor();
-+    public static final class CallbackExecutor implements java.util.concurrent.Executor, Runnable {
-+
-+        private final java.util.Queue<Runnable> queue = new java.util.ArrayDeque<>();
-+
-+        @Override
-+        public void execute(Runnable runnable) {
-+            this.queue.add(runnable);
-+        }
-+
-+        @Override
-+        public void run() {
-+            Runnable task;
-+            while ((task = this.queue.poll()) != null) {
-+                task.run();
-+            }
-+        }
-+    };
-+    // CraftBukkit end
-+
-+    // Paper start
-+    public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) {
-+        return this.pendingUnloads.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+    }
-+    // Paper end
-+
-     public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop<Runnable> mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory, int viewDistance, boolean dsync) {
-         super(new RegionStorageInfo(session.getLevelId(), world.dimension(), "chunk"), session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync);
-         this.visibleChunkMap = this.updatingChunkMap.clone();
-@@ -170,13 +201,19 @@
-         RegistryAccess iregistrycustom = world.registryAccess();
-         long j = world.getSeed();
- 
--        if (chunkGenerator instanceof NoiseBasedChunkGenerator chunkgeneratorabstract) {
-+        // CraftBukkit start - SPIGOT-7051: It's a rigged game! Use delegate for random state creation, otherwise it is not so random.
-+        ChunkGenerator randomGenerator = chunkGenerator;
-+        if (randomGenerator instanceof CustomChunkGenerator customChunkGenerator) {
-+            randomGenerator = customChunkGenerator.getDelegate();
-+        }
-+        if (randomGenerator instanceof NoiseBasedChunkGenerator chunkgeneratorabstract) {
-+            // CraftBukkit end
-             this.randomState = RandomState.create((NoiseGeneratorSettings) chunkgeneratorabstract.generatorSettings().value(), (HolderGetter) iregistrycustom.lookupOrThrow(Registries.NOISE), j);
-         } else {
-             this.randomState = RandomState.create(NoiseGeneratorSettings.dummy(), (HolderGetter) iregistrycustom.lookupOrThrow(Registries.NOISE), j);
-         }
- 
--        this.chunkGeneratorState = chunkGenerator.createState(iregistrycustom.lookupOrThrow(Registries.STRUCTURE_SET), this.randomState, j);
-+        this.chunkGeneratorState = chunkGenerator.createState(iregistrycustom.lookupOrThrow(Registries.STRUCTURE_SET), this.randomState, j, world.spigotConfig); // Spigot
-         this.mainThreadExecutor = mainThreadExecutor;
-         ConsecutiveExecutor consecutiveexecutor = new ConsecutiveExecutor(executor, "worldgen");
- 
-@@ -198,6 +235,12 @@
-         this.chunksToEagerlySave.add(pos.toLong());
-     }
- 
-+    // Paper start
-+    public int getMobCountNear(final ServerPlayer player, final net.minecraft.world.entity.MobCategory mobCategory) {
-+        return -1;
-+    }
-+    // Paper end
-+
-     protected ChunkGenerator generator() {
-         return this.worldGenContext.generator();
-     }
-@@ -325,7 +368,7 @@
-                         throw this.debugFuturesAndCreateReportedException(new IllegalStateException("At least one of the chunk futures were null"), "n/a");
-                     }
- 
--                    ChunkAccess ichunkaccess = (ChunkAccess) chunkresult.orElse((Object) null);
-+                    ChunkAccess ichunkaccess = (ChunkAccess) chunkresult.orElse(null); // CraftBukkit - decompile error
- 
-                     if (ichunkaccess == null) {
-                         return ChunkMap.UNLOADED_CHUNK_LIST_RESULT;
-@@ -354,9 +397,9 @@
-         };
- 
-         stringbuilder.append("Updating:").append(System.lineSeparator());
--        this.updatingChunkMap.values().forEach(consumer);
-+        ca.spottedleaf.moonrise.common.util.ChunkSystem.getUpdatingChunkHolders(this.level).forEach(consumer); // Paper
-         stringbuilder.append("Visible:").append(System.lineSeparator());
--        this.visibleChunkMap.values().forEach(consumer);
-+        ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).forEach(consumer); // Paper
-         CrashReport crashreport = CrashReport.forThrowable(exception, "Chunk loading");
-         CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Chunk loading");
- 
-@@ -398,6 +441,9 @@
-                     holder.setTicketLevel(level);
-                 } else {
-                     holder = new ChunkHolder(new ChunkPos(pos), level, this.level, this.lightEngine, this::onLevelChange, this);
-+                    // Paper start
-+                    ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderCreate(this.level, holder);
-+                    // Paper end
-                 }
- 
-                 this.updatingChunkMap.put(pos, holder);
-@@ -427,7 +473,7 @@
- 
-     protected void saveAllChunks(boolean flush) {
-         if (flush) {
--            List<ChunkHolder> list = this.visibleChunkMap.values().stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).toList();
-+            List<ChunkHolder> list = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).toList(); // Paper
-             MutableBoolean mutableboolean = new MutableBoolean();
- 
-             do {
-@@ -453,7 +499,7 @@
-         } else {
-             this.nextChunkSaveTime.clear();
-             long i = Util.getMillis();
--            ObjectIterator objectiterator = this.visibleChunkMap.values().iterator();
-+            Iterator<ChunkHolder> objectiterator = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper
- 
-             while (objectiterator.hasNext()) {
-                 ChunkHolder playerchunk = (ChunkHolder) objectiterator.next();
-@@ -478,7 +524,7 @@
-     }
- 
-     public boolean hasWork() {
--        return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || !this.updatingChunkMap.isEmpty() || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.worldgenTaskDispatcher.hasWork() || this.lightTaskDispatcher.hasWork() || this.distanceManager.hasTickets();
-+        return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || ca.spottedleaf.moonrise.common.util.ChunkSystem.hasAnyChunkHolders(this.level) || !this.updatingChunkMap.isEmpty() || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.worldgenTaskDispatcher.hasWork() || this.lightTaskDispatcher.hasWork() || this.distanceManager.hasTickets();
-     }
- 
-     private void processUnloads(BooleanSupplier shouldKeepTicking) {
-@@ -537,8 +583,11 @@
-                 this.scheduleUnload(pos, chunk);
-             } else {
-                 ChunkAccess ichunkaccess = chunk.getLatestChunk();
--
--                if (this.pendingUnloads.remove(pos, chunk) && ichunkaccess != null) {
-+                // Paper start
-+                boolean removed;
-+                if ((removed = this.pendingUnloads.remove(pos, chunk)) && ichunkaccess != null) {
-+                    ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, chunk);
-+                    // Paper end
-                     LevelChunk chunk1;
- 
-                     if (ichunkaccess instanceof LevelChunk) {
-@@ -556,7 +605,9 @@
-                     this.lightEngine.tryScheduleUpdate();
-                     this.progressListener.onStatusChange(ichunkaccess.getPos(), (ChunkStatus) null);
-                     this.nextChunkSaveTime.remove(ichunkaccess.getPos().toLong());
--                }
-+                } else if (removed) { // Paper start
-+                    ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, chunk);
-+                } // Paper end
- 
-             }
-         };
-@@ -905,7 +956,7 @@
-         }
-     }
- 
--    protected void setServerViewDistance(int watchDistance) {
-+    public void setServerViewDistance(int watchDistance) { // Paper - public
-         int j = Mth.clamp(watchDistance, 2, 32);
- 
-         if (j != this.serverViewDistance) {
-@@ -922,7 +973,7 @@
- 
-     }
- 
--    int getPlayerViewDistance(ServerPlayer player) {
-+    public int getPlayerViewDistance(ServerPlayer player) { // Paper - public
-         return Mth.clamp(player.requestedViewDistance(), 2, this.serverViewDistance);
-     }
- 
-@@ -951,7 +1002,7 @@
-     }
- 
-     public int size() {
--        return this.visibleChunkMap.size();
-+        return ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolderCount(this.level); // Paper
-     }
- 
-     public DistanceManager getDistanceManager() {
-@@ -959,25 +1010,26 @@
-     }
- 
-     protected Iterable<ChunkHolder> getChunks() {
--        return Iterables.unmodifiableIterable(this.visibleChunkMap.values());
-+        return Iterables.unmodifiableIterable(ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level)); // Paper
-     }
- 
-     void dumpChunks(Writer writer) throws IOException {
-         CsvOutput csvwriter = CsvOutput.builder().addColumn("x").addColumn("z").addColumn("level").addColumn("in_memory").addColumn("status").addColumn("full_status").addColumn("accessible_ready").addColumn("ticking_ready").addColumn("entity_ticking_ready").addColumn("ticket").addColumn("spawning").addColumn("block_entity_count").addColumn("ticking_ticket").addColumn("ticking_level").addColumn("block_ticks").addColumn("fluid_ticks").build(writer);
-         TickingTracker tickingtracker = this.distanceManager.tickingTracker();
--        ObjectBidirectionalIterator objectbidirectionaliterator = this.visibleChunkMap.long2ObjectEntrySet().iterator();
-+        Iterator<ChunkHolder> objectbidirectionaliterator = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper
- 
-         while (objectbidirectionaliterator.hasNext()) {
--            Entry<ChunkHolder> entry = (Entry) objectbidirectionaliterator.next();
--            long i = entry.getLongKey();
-+            ChunkHolder playerchunk = objectbidirectionaliterator.next(); // Paper
-+            long i = playerchunk.pos.toLong(); // Paper
-             ChunkPos chunkcoordintpair = new ChunkPos(i);
--            ChunkHolder playerchunk = (ChunkHolder) entry.getValue();
-+            // Paper - move up
-             Optional<ChunkAccess> optional = Optional.ofNullable(playerchunk.getLatestChunk());
-             Optional<LevelChunk> optional1 = optional.flatMap((ichunkaccess) -> {
-                 return ichunkaccess instanceof LevelChunk ? Optional.of((LevelChunk) ichunkaccess) : Optional.empty();
-             });
- 
--            csvwriter.writeRow(chunkcoordintpair.x, chunkcoordintpair.z, playerchunk.getTicketLevel(), optional.isPresent(), optional.map(ChunkAccess::getPersistedStatus).orElse((Object) null), optional1.map(LevelChunk::getFullStatus).orElse((Object) null), ChunkMap.printFuture(playerchunk.getFullChunkFuture()), ChunkMap.printFuture(playerchunk.getTickingChunkFuture()), ChunkMap.printFuture(playerchunk.getEntityTickingChunkFuture()), this.distanceManager.getTicketDebugString(i), this.anyPlayerCloseEnoughForSpawning(chunkcoordintpair), optional1.map((chunk) -> {
-+            // CraftBukkit - decompile error
-+            csvwriter.writeRow(chunkcoordintpair.x, chunkcoordintpair.z, playerchunk.getTicketLevel(), optional.isPresent(), optional.map(ChunkAccess::getPersistedStatus).orElse(null), optional1.map(LevelChunk::getFullStatus).orElse(null), ChunkMap.printFuture(playerchunk.getFullChunkFuture()), ChunkMap.printFuture(playerchunk.getTickingChunkFuture()), ChunkMap.printFuture(playerchunk.getEntityTickingChunkFuture()), this.distanceManager.getTicketDebugString(i), this.anyPlayerCloseEnoughForSpawning(chunkcoordintpair), optional1.map((chunk) -> {
-                 return chunk.getBlockEntities().size();
-             }).orElse(0), tickingtracker.getTicketDebugString(i), tickingtracker.getLevel(i), optional1.map((chunk) -> {
-                 return chunk.getBlockTicks().count();
-@@ -990,7 +1042,7 @@
- 
-     private static String printFuture(CompletableFuture<ChunkResult<LevelChunk>> future) {
-         try {
--            ChunkResult<LevelChunk> chunkresult = (ChunkResult) future.getNow((Object) null);
-+            ChunkResult<LevelChunk> chunkresult = (ChunkResult) future.getNow(null); // CraftBukkit - decompile error
- 
-             return chunkresult != null ? (chunkresult.isSuccess() ? "done" : "unloaded") : "not completed";
-         } catch (CompletionException completionexception) {
-@@ -1002,12 +1054,14 @@
- 
-     private CompletableFuture<Optional<CompoundTag>> readChunk(ChunkPos chunkPos) {
-         return this.read(chunkPos).thenApplyAsync((optional) -> {
--            return optional.map(this::upgradeChunkTag);
-+            return optional.map((nbttagcompound) -> this.upgradeChunkTag(nbttagcompound, chunkPos)); // CraftBukkit
-         }, Util.backgroundExecutor().forName("upgradeChunk"));
-     }
- 
--    private CompoundTag upgradeChunkTag(CompoundTag nbt) {
--        return this.upgradeChunkTag(this.level.dimension(), this.overworldDataStorage, nbt, this.generator().getTypeNameForDataFixer());
-+    // CraftBukkit start
-+    private CompoundTag upgradeChunkTag(CompoundTag nbttagcompound, ChunkPos chunkcoordintpair) {
-+        return this.upgradeChunkTag(this.level.getTypeKey(), this.overworldDataStorage, nbttagcompound, this.generator().getTypeNameForDataFixer(), chunkcoordintpair, this.level);
-+        // CraftBukkit end
-     }
- 
-     void forEachSpawnCandidateChunk(Consumer<ChunkHolder> callback) {
-@@ -1025,10 +1079,23 @@
-     }
- 
-     public boolean anyPlayerCloseEnoughForSpawning(ChunkPos pos) {
--        return !this.distanceManager.hasPlayersNearby(pos.toLong()) ? false : this.anyPlayerCloseEnoughForSpawningInternal(pos);
-+        // Spigot start
-+        return this.anyPlayerCloseEnoughForSpawning(pos, false);
-     }
- 
-+    boolean anyPlayerCloseEnoughForSpawning(ChunkPos chunkcoordintpair, boolean reducedRange) {
-+        return !this.distanceManager.hasPlayersNearby(chunkcoordintpair.toLong()) ? false : this.anyPlayerCloseEnoughForSpawningInternal(chunkcoordintpair, reducedRange);
-+        // Spigot end
-+    }
-+
-     private boolean anyPlayerCloseEnoughForSpawningInternal(ChunkPos pos) {
-+        // Spigot start
-+        return this.anyPlayerCloseEnoughForSpawningInternal(pos, false);
-+    }
-+
-+    private boolean anyPlayerCloseEnoughForSpawningInternal(ChunkPos chunkcoordintpair, boolean reducedRange) {
-+        double blockRange; // Paper - use from event
-+        // Spigot end
-         Iterator iterator = this.playerMap.getAllPlayers().iterator();
- 
-         ServerPlayer entityplayer;
-@@ -1039,7 +1106,16 @@
-             }
- 
-             entityplayer = (ServerPlayer) iterator.next();
--        } while (!this.playerIsCloseEnoughForSpawning(entityplayer, pos));
-+            // Paper start - PlayerNaturallySpawnCreaturesEvent
-+            com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent event;
-+            blockRange = 16384.0D;
-+            if (reducedRange) {
-+                event = entityplayer.playerNaturallySpawnedEvent;
-+                if (event == null || event.isCancelled()) continue;
-+                blockRange = (double) ((event.getSpawnRadius() << 4) * (event.getSpawnRadius() << 4));
-+            }
-+            // Paper end - PlayerNaturallySpawnCreaturesEvent
-+        } while (!this.playerIsCloseEnoughForSpawning(entityplayer, chunkcoordintpair, blockRange)); // Spigot
- 
-         return true;
-     }
-@@ -1056,7 +1132,7 @@
-             while (iterator.hasNext()) {
-                 ServerPlayer entityplayer = (ServerPlayer) iterator.next();
- 
--                if (this.playerIsCloseEnoughForSpawning(entityplayer, pos)) {
-+                if (this.playerIsCloseEnoughForSpawning(entityplayer, pos, 16384.0D)) { // Spigot
-                     builder.add(entityplayer);
-                 }
-             }
-@@ -1065,13 +1141,13 @@
-         }
-     }
- 
--    private boolean playerIsCloseEnoughForSpawning(ServerPlayer player, ChunkPos pos) {
--        if (player.isSpectator()) {
-+    private boolean playerIsCloseEnoughForSpawning(ServerPlayer entityplayer, ChunkPos chunkcoordintpair, double range) { // Spigot
-+        if (entityplayer.isSpectator()) {
-             return false;
-         } else {
--            double d0 = ChunkMap.euclideanDistanceSquared(pos, player);
-+            double d0 = ChunkMap.euclideanDistanceSquared(chunkcoordintpair, entityplayer);
- 
--            return d0 < 16384.0D;
-+            return d0 < range; // Spigot
-         }
-     }
- 
-@@ -1215,9 +1291,19 @@
-     }
- 
-     public void addEntity(Entity entity) {
-+        org.spigotmc.AsyncCatcher.catchOp("entity track"); // Spigot
-+        // Paper start - ignore and warn about illegal addEntity calls instead of crashing server
-+        if (!entity.valid || entity.level() != this.level || this.entityMap.containsKey(entity.getId())) {
-+            LOGGER.error("Illegal ChunkMap::addEntity for world " + this.level.getWorld().getName()
-+                + ": " + entity  + (this.entityMap.containsKey(entity.getId()) ? " ALREADY CONTAINED (This would have crashed your server)" : ""), new Throwable());
-+            return;
-+        }
-+        // Paper end - ignore and warn about illegal addEntity calls instead of crashing server
-+        if (entity instanceof ServerPlayer && ((ServerPlayer) entity).supressTrackerForLogin) return; // Paper - Fire PlayerJoinEvent when Player is actually ready; Delay adding to tracker until after list packets
-         if (!(entity instanceof EnderDragonPart)) {
-             EntityType<?> entitytypes = entity.getType();
-             int i = entitytypes.clientTrackingRange() * 16;
-+            i = org.spigotmc.TrackingRange.getEntityTrackingRange(entity, i); // Spigot
- 
-             if (i != 0) {
-                 int j = entitytypes.updateInterval();
-@@ -1250,6 +1336,7 @@
-     }
- 
-     protected void removeEntity(Entity entity) {
-+        org.spigotmc.AsyncCatcher.catchOp("entity untrack"); // Spigot
-         if (entity instanceof ServerPlayer entityplayer) {
-             this.updatePlayerStatus(entityplayer, false);
-             ObjectIterator objectiterator = this.entityMap.values().iterator();
-@@ -1391,7 +1478,7 @@
-         });
-     }
- 
--    private class ChunkDistanceManager extends DistanceManager {
-+    public class ChunkDistanceManager extends DistanceManager { // Paper - public
- 
-         protected ChunkDistanceManager(final Executor workerExecutor, final Executor mainThreadExecutor) {
-             super(workerExecutor, mainThreadExecutor);
-@@ -1421,10 +1508,10 @@
-         final Entity entity;
-         private final int range;
-         SectionPos lastSectionPos;
--        public final Set<ServerPlayerConnection> seenBy = Sets.newIdentityHashSet();
-+        public final Set<ServerPlayerConnection> seenBy = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); // Paper - Perf: optimise map impl
- 
-         public TrackedEntity(final Entity entity, final int i, final int j, final boolean flag) {
--            this.serverEntity = new ServerEntity(ChunkMap.this.level, entity, j, flag, this::broadcast);
-+            this.serverEntity = new ServerEntity(ChunkMap.this.level, entity, j, flag, this::broadcast, this.seenBy); // CraftBukkit
-             this.entity = entity;
-             this.range = i;
-             this.lastSectionPos = SectionPos.of((EntityAccess) entity);
-@@ -1469,6 +1556,7 @@
-         }
- 
-         public void removePlayer(ServerPlayer player) {
-+            org.spigotmc.AsyncCatcher.catchOp("player tracker clear"); // Spigot
-             if (this.seenBy.remove(player.connection)) {
-                 this.serverEntity.removePairing(player);
-             }
-@@ -1476,17 +1564,41 @@
-         }
- 
-         public void updatePlayer(ServerPlayer player) {
-+            org.spigotmc.AsyncCatcher.catchOp("player tracker update"); // Spigot
-             if (player != this.entity) {
--                Vec3 vec3d = player.position().subtract(this.entity.position());
-+                // Paper start - remove allocation of Vec3D here
-+                // Vec3 vec3d = player.position().subtract(this.entity.position());
-+                double vec3d_dx = player.getX() - this.entity.getX();
-+                double vec3d_dz = player.getZ() - this.entity.getZ();
-+                // Paper end - remove allocation of Vec3D here
-                 int i = ChunkMap.this.getPlayerViewDistance(player);
-                 double d0 = (double) Math.min(this.getEffectiveRange(), i * 16);
--                double d1 = vec3d.x * vec3d.x + vec3d.z * vec3d.z;
-+                double d1 = vec3d_dx * vec3d_dx + vec3d_dz * vec3d_dz; // Paper
-                 double d2 = d0 * d0;
--                boolean flag = d1 <= d2 && this.entity.broadcastToPlayer(player) && ChunkMap.this.isChunkTracked(player, this.entity.chunkPosition().x, this.entity.chunkPosition().z);
-+                // Paper start - Configurable entity tracking range by Y
-+                boolean flag = d1 <= d2;
-+                if (flag && level.paperConfig().entities.trackingRangeY.enabled) {
-+                    double rangeY = level.paperConfig().entities.trackingRangeY.get(this.entity, -1);
-+                    if (rangeY != -1) {
-+                        double vec3d_dy = player.getY() - this.entity.getY();
-+                        flag = vec3d_dy * vec3d_dy <= rangeY * rangeY;
-+                    }
-+                }
-+                flag = flag && this.entity.broadcastToPlayer(player) && ChunkMap.this.isChunkTracked(player, this.entity.chunkPosition().x, this.entity.chunkPosition().z);
-+                // Paper end - Configurable entity tracking range by Y
- 
-+                // CraftBukkit start - respect vanish API
-+                if (flag && !player.getBukkitEntity().canSee(this.entity.getBukkitEntity())) { // Paper - only consider hits
-+                    flag = false;
-+                }
-+                // CraftBukkit end
-                 if (flag) {
-                     if (this.seenBy.add(player.connection)) {
-+                        // Paper start - entity tracking events
-+                        if (io.papermc.paper.event.player.PlayerTrackEntityEvent.getHandlerList().getRegisteredListeners().length == 0 || new io.papermc.paper.event.player.PlayerTrackEntityEvent(player.getBukkitEntity(), this.entity.getBukkitEntity()).callEvent()) {
-                         this.serverEntity.addPairing(player);
-+                        }
-+                        // Paper end - entity tracking events
-                     }
-                 } else if (this.seenBy.remove(player.connection)) {
-                     this.serverEntity.removePairing(player);
-@@ -1506,6 +1618,7 @@
-             while (iterator.hasNext()) {
-                 Entity entity = (Entity) iterator.next();
-                 int j = entity.getType().clientTrackingRange() * 16;
-+                j = org.spigotmc.TrackingRange.getEntityTrackingRange(entity, j); // Paper
- 
-                 if (j > i) {
-                     i = j;
diff --git a/paper-server/patches/unapplied/net/minecraft/server/level/DistanceManager.java.patch b/paper-server/patches/unapplied/net/minecraft/server/level/DistanceManager.java.patch
deleted file mode 100644
index 60e1437870..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/level/DistanceManager.java.patch
+++ /dev/null
@@ -1,152 +0,0 @@
---- a/net/minecraft/server/level/DistanceManager.java
-+++ b/net/minecraft/server/level/DistanceManager.java
-@@ -117,8 +117,17 @@
- 
-             ChunkHolder playerchunk;
- 
-+            // CraftBukkit start - SPIGOT-7780: Call chunk unload events before updateHighestAllowedStatus
-             while (iterator.hasNext()) {
-                 playerchunk = (ChunkHolder) iterator.next();
-+                playerchunk.callEventIfUnloading(chunkLoadingManager);
-+            }
-+
-+            iterator = this.chunksToUpdateFutures.iterator();
-+            // CraftBukkit end
-+
-+            while (iterator.hasNext()) {
-+                playerchunk = (ChunkHolder) iterator.next();
-                 playerchunk.updateHighestAllowedStatus(chunkLoadingManager);
-             }
- 
-@@ -165,30 +174,33 @@
-         }
-     }
- 
--    void addTicket(long position, Ticket<?> ticket) {
--        SortedArraySet<Ticket<?>> arraysetsorted = this.getTickets(position);
-+    boolean addTicket(long i, Ticket<?> ticket) { // CraftBukkit - void -> boolean
-+        SortedArraySet<Ticket<?>> arraysetsorted = this.getTickets(i);
-         int j = DistanceManager.getTicketLevelAt(arraysetsorted);
-         Ticket<?> ticket1 = (Ticket) arraysetsorted.addOrGet(ticket);
- 
-         ticket1.setCreatedTick(this.ticketTickCounter);
-         if (ticket.getTicketLevel() < j) {
--            this.ticketTracker.update(position, ticket.getTicketLevel(), true);
-+            this.ticketTracker.update(i, ticket.getTicketLevel(), true);
-         }
- 
-+        return ticket == ticket1; // CraftBukkit
-     }
- 
--    void removeTicket(long pos, Ticket<?> ticket) {
--        SortedArraySet<Ticket<?>> arraysetsorted = this.getTickets(pos);
-+    boolean removeTicket(long i, Ticket<?> ticket) { // CraftBukkit - void -> boolean
-+        SortedArraySet<Ticket<?>> arraysetsorted = this.getTickets(i);
- 
-+        boolean removed = false; // CraftBukkit
-         if (arraysetsorted.remove(ticket)) {
--            ;
-+            removed = true; // CraftBukkit
-         }
- 
-         if (arraysetsorted.isEmpty()) {
--            this.tickets.remove(pos);
-+            this.tickets.remove(i);
-         }
- 
--        this.ticketTracker.update(pos, DistanceManager.getTicketLevelAt(arraysetsorted), false);
-+        this.ticketTracker.update(i, DistanceManager.getTicketLevelAt(arraysetsorted), false);
-+        return removed; // CraftBukkit
-     }
- 
-     public <T> void addTicket(TicketType<T> type, ChunkPos pos, int level, T argument) {
-@@ -202,19 +214,33 @@
-     }
- 
-     public <T> void addRegionTicket(TicketType<T> type, ChunkPos pos, int radius, T argument) {
--        Ticket<T> ticket = new Ticket<>(type, ChunkLevel.byStatus(FullChunkStatus.FULL) - radius, argument);
--        long j = pos.toLong();
-+        // CraftBukkit start
-+        this.addRegionTicketAtDistance(type, pos, radius, argument);
-+    }
- 
--        this.addTicket(j, ticket);
-+    public <T> boolean addRegionTicketAtDistance(TicketType<T> tickettype, ChunkPos chunkcoordintpair, int i, T t0) {
-+        // CraftBukkit end
-+        Ticket<T> ticket = new Ticket<>(tickettype, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0);
-+        long j = chunkcoordintpair.toLong();
-+
-+        boolean added = this.addTicket(j, ticket); // CraftBukkit
-         this.tickingTicketsTracker.addTicket(j, ticket);
-+        return added; // CraftBukkit
-     }
- 
-     public <T> void removeRegionTicket(TicketType<T> type, ChunkPos pos, int radius, T argument) {
--        Ticket<T> ticket = new Ticket<>(type, ChunkLevel.byStatus(FullChunkStatus.FULL) - radius, argument);
--        long j = pos.toLong();
-+        // CraftBukkit start
-+        this.removeRegionTicketAtDistance(type, pos, radius, argument);
-+    }
- 
--        this.removeTicket(j, ticket);
-+    public <T> boolean removeRegionTicketAtDistance(TicketType<T> tickettype, ChunkPos chunkcoordintpair, int i, T t0) {
-+        // CraftBukkit end
-+        Ticket<T> ticket = new Ticket<>(tickettype, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0);
-+        long j = chunkcoordintpair.toLong();
-+
-+        boolean removed = this.removeTicket(j, ticket); // CraftBukkit
-         this.tickingTicketsTracker.removeTicket(j, ticket);
-+        return removed; // CraftBukkit
-     }
- 
-     private SortedArraySet<Ticket<?>> getTickets(long position) {
-@@ -253,9 +279,10 @@
-         ChunkPos chunkcoordintpair = pos.chunk();
-         long i = chunkcoordintpair.toLong();
-         ObjectSet<ServerPlayer> objectset = (ObjectSet) this.playersPerChunk.get(i);
-+        if (objectset == null) return; // CraftBukkit - SPIGOT-6208
- 
--        objectset.remove(player);
--        if (objectset.isEmpty()) {
-+        if (objectset != null) objectset.remove(player); // Paper - some state corruption happens here, don't crash, clean up gracefully
-+        if (objectset == null || objectset.isEmpty()) { // Paper
-             this.playersPerChunk.remove(i);
-             this.naturalSpawnChunkCounter.update(i, Integer.MAX_VALUE, false);
-             this.playerTicketManager.update(i, Integer.MAX_VALUE, false);
-@@ -358,7 +385,7 @@
-     }
- 
-     public void removeTicketsOnClosing() {
--        ImmutableSet<TicketType<?>> immutableset = ImmutableSet.of(TicketType.UNKNOWN);
-+        ImmutableSet<TicketType<?>> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.FUTURE_AWAIT); // Paper - add additional tickets to preserve
-         ObjectIterator<Entry<SortedArraySet<Ticket<?>>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator();
- 
-         while (objectiterator.hasNext()) {
-@@ -389,7 +416,27 @@
- 
-     public boolean hasTickets() {
-         return !this.tickets.isEmpty();
-+    }
-+
-+    // CraftBukkit start
-+    public <T> void removeAllTicketsFor(TicketType<T> ticketType, int ticketLevel, T ticketIdentifier) {
-+        Ticket<T> target = new Ticket<>(ticketType, ticketLevel, ticketIdentifier);
-+
-+        for (java.util.Iterator<Entry<SortedArraySet<Ticket<?>>>> iterator = this.tickets.long2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
-+            Entry<SortedArraySet<Ticket<?>>> entry = iterator.next();
-+            SortedArraySet<Ticket<?>> tickets = entry.getValue();
-+            if (tickets.remove(target)) {
-+                // copied from removeTicket
-+                this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt(tickets), false);
-+
-+                // can't use entry after it's removed
-+                if (tickets.isEmpty()) {
-+                    iterator.remove();
-+                }
-+            }
-+        }
-     }
-+    // CraftBukkit end
- 
-     private class ChunkTicketTracker extends ChunkTracker {
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/server/level/ServerEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/server/level/ServerEntity.java.patch
deleted file mode 100644
index f458422c50..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/level/ServerEntity.java.patch
+++ /dev/null
@@ -1,179 +0,0 @@
---- a/net/minecraft/server/level/ServerEntity.java
-+++ b/net/minecraft/server/level/ServerEntity.java
-@@ -31,7 +31,6 @@
- import net.minecraft.network.protocol.game.ClientboundUpdateAttributesPacket;
- import net.minecraft.network.protocol.game.VecDeltaCodec;
- import net.minecraft.network.syncher.SynchedEntityData;
--import net.minecraft.util.Mth;
- import net.minecraft.world.entity.Entity;
- import net.minecraft.world.entity.EquipmentSlot;
- import net.minecraft.world.entity.Leashable;
-@@ -50,6 +49,13 @@
- import net.minecraft.world.phys.Vec3;
- import org.slf4j.Logger;
- 
-+// CraftBukkit start
-+import net.minecraft.server.network.ServerPlayerConnection;
-+import net.minecraft.util.Mth;
-+import org.bukkit.entity.Player;
-+import org.bukkit.event.player.PlayerVelocityEvent;
-+// CraftBukkit end
-+
- public class ServerEntity {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
-@@ -69,18 +75,22 @@
-     private Vec3 lastSentMovement;
-     private int tickCount;
-     private int teleportDelay;
--    private List<Entity> lastPassengers = Collections.emptyList();
-+    private List<Entity> lastPassengers = com.google.common.collect.ImmutableList.of(); // Paper - optimize passenger checks
-     private boolean wasRiding;
-     private boolean wasOnGround;
-     @Nullable
-     private List<SynchedEntityData.DataValue<?>> trackedDataValues;
-+    // CraftBukkit start
-+    private final Set<ServerPlayerConnection> trackedPlayers;
- 
--    public ServerEntity(ServerLevel world, Entity entity, int tickInterval, boolean alwaysUpdateVelocity, Consumer<Packet<?>> receiver) {
--        this.level = world;
--        this.broadcast = receiver;
-+    public ServerEntity(ServerLevel worldserver, Entity entity, int i, boolean flag, Consumer<Packet<?>> consumer, Set<ServerPlayerConnection> trackedPlayers) {
-+        this.trackedPlayers = trackedPlayers;
-+        // CraftBukkit end
-+        this.level = worldserver;
-+        this.broadcast = consumer;
-         this.entity = entity;
--        this.updateInterval = tickInterval;
--        this.trackDelta = alwaysUpdateVelocity;
-+        this.updateInterval = i;
-+        this.trackDelta = flag;
-         this.positionCodec.setBase(entity.trackingPosition());
-         this.lastSentMovement = entity.getDeltaMovement();
-         this.lastSentYRot = Mth.packDegrees(entity.getYRot());
-@@ -94,7 +104,7 @@
-         List<Entity> list = this.entity.getPassengers();
- 
-         if (!list.equals(this.lastPassengers)) {
--            this.broadcast.accept(new ClientboundSetPassengersPacket(this.entity));
-+            this.broadcastAndSend(new ClientboundSetPassengersPacket(this.entity)); // CraftBukkit
-             ServerEntity.removedPassengers(list, this.lastPassengers).forEach((entity) -> {
-                 if (entity instanceof ServerPlayer entityplayer) {
-                     entityplayer.connection.teleport(entityplayer.getX(), entityplayer.getY(), entityplayer.getZ(), entityplayer.getYRot(), entityplayer.getXRot());
-@@ -106,19 +116,19 @@
- 
-         Entity entity = this.entity;
- 
--        if (entity instanceof ItemFrame entityitemframe) {
--            if (this.tickCount % 10 == 0) {
-+        if (!this.trackedPlayers.isEmpty() && entity instanceof ItemFrame entityitemframe) { // Paper - Perf: Only tick item frames if players can see it
-+            if (true || this.tickCount % 10 == 0) { // CraftBukkit - Moved below, should always enter this block
-                 ItemStack itemstack = entityitemframe.getItem();
- 
--                if (itemstack.getItem() instanceof MapItem) {
--                    MapId mapid = (MapId) itemstack.get(DataComponents.MAP_ID);
-+                if (this.level.paperConfig().maps.itemFrameCursorUpdateInterval > 0 && this.tickCount % this.level.paperConfig().maps.itemFrameCursorUpdateInterval == 0 && itemstack.getItem() instanceof MapItem) { // CraftBukkit - Moved this.tickCounter % 10 logic here so item frames do not enter the other blocks // Paper - Make item frame map cursor update interval configurable
-+                    MapId mapid = entityitemframe.cachedMapId; // Paper - Perf: Cache map ids on item frames
-                     MapItemSavedData worldmap = MapItem.getSavedData(mapid, this.level);
- 
-                     if (worldmap != null) {
--                        Iterator iterator = this.level.players().iterator();
-+                        Iterator<ServerPlayerConnection> iterator = this.trackedPlayers.iterator(); // CraftBukkit
- 
-                         while (iterator.hasNext()) {
--                            ServerPlayer entityplayer = (ServerPlayer) iterator.next();
-+                            ServerPlayer entityplayer = iterator.next().getPlayer(); // CraftBukkit
- 
-                             worldmap.tickCarriedBy(entityplayer, itemstack);
-                             Packet<?> packet = worldmap.getUpdatePacket(mapid, entityplayer);
-@@ -168,7 +178,13 @@
- 
-                     ++this.teleportDelay;
-                     Vec3 vec3d = this.entity.trackingPosition();
--                    boolean flag1 = this.positionCodec.delta(vec3d).lengthSqr() >= 7.62939453125E-6D;
-+                    // Paper start - reduce allocation of Vec3D here
-+                    Vec3 base = this.positionCodec.base;
-+                    double vec3d_dx = vec3d.x - base.x;
-+                    double vec3d_dy = vec3d.y - base.y;
-+                    double vec3d_dz = vec3d.z - base.z;
-+                    boolean flag1 = (vec3d_dx * vec3d_dx + vec3d_dy * vec3d_dy + vec3d_dz * vec3d_dz) >= 7.62939453125E-6D;
-+                    // Paper end - reduce allocation of Vec3D here
-                     Packet<?> packet1 = null;
-                     boolean flag2 = flag1 || this.tickCount % 60 == 0;
-                     boolean flag3 = false;
-@@ -248,6 +264,27 @@
- 
-         ++this.tickCount;
-         if (this.entity.hurtMarked) {
-+            // CraftBukkit start - Create PlayerVelocity event
-+            boolean cancelled = false;
-+
-+            if (this.entity instanceof ServerPlayer) {
-+                Player player = (Player) this.entity.getBukkitEntity();
-+                org.bukkit.util.Vector velocity = player.getVelocity();
-+
-+                PlayerVelocityEvent event = new PlayerVelocityEvent(player, velocity.clone());
-+                this.entity.level().getCraftServer().getPluginManager().callEvent(event);
-+
-+                if (event.isCancelled()) {
-+                    cancelled = true;
-+                } else if (!velocity.equals(event.getVelocity())) {
-+                    player.setVelocity(event.getVelocity());
-+                }
-+            }
-+
-+            if (cancelled) {
-+                return;
-+            }
-+            // CraftBukkit end
-             this.entity.hurtMarked = false;
-             this.broadcastAndSend(new ClientboundSetEntityMotionPacket(this.entity));
-         }
-@@ -298,7 +335,10 @@
- 
-     public void sendPairingData(ServerPlayer player, Consumer<Packet<ClientGamePacketListener>> sender) {
-         if (this.entity.isRemoved()) {
--            ServerEntity.LOGGER.warn("Fetching packet for removed entity {}", this.entity);
-+            // CraftBukkit start - Remove useless error spam, just return
-+            // EntityTrackerEntry.LOGGER.warn("Fetching packet for removed entity {}", this.entity);
-+            return;
-+            // CraftBukkit end
-         }
- 
-         Packet<ClientGamePacketListener> packet = this.entity.getAddEntityPacket(this);
-@@ -313,6 +353,12 @@
-         if (this.entity instanceof LivingEntity) {
-             Collection<AttributeInstance> collection = ((LivingEntity) this.entity).getAttributes().getSyncableAttributes();
- 
-+            // CraftBukkit start - If sending own attributes send scaled health instead of current maximum health
-+            if (this.entity.getId() == player.getId()) {
-+                ((ServerPlayer) this.entity).getBukkitEntity().injectScaledMaxHealth(collection, false);
-+            }
-+            // CraftBukkit end
-+
-             if (!collection.isEmpty()) {
-                 sender.accept(new ClientboundUpdateAttributesPacket(this.entity.getId(), collection));
-             }
-@@ -342,8 +388,9 @@
-             }
- 
-             if (!list.isEmpty()) {
--                sender.accept(new ClientboundSetEquipmentPacket(this.entity.getId(), list));
-+                sender.accept(new ClientboundSetEquipmentPacket(this.entity.getId(), list, true)); // Paper - data sanitization
-             }
-+            ((LivingEntity) this.entity).detectEquipmentUpdatesPublic(); // CraftBukkit - SPIGOT-3789: sync again immediately after sending
-         }
- 
-         if (!this.entity.getPassengers().isEmpty()) {
-@@ -396,6 +443,11 @@
-             Set<AttributeInstance> set = ((LivingEntity) this.entity).getAttributes().getAttributesToSync();
- 
-             if (!set.isEmpty()) {
-+                // CraftBukkit start - Send scaled max health
-+                if (this.entity instanceof ServerPlayer) {
-+                    ((ServerPlayer) this.entity).getBukkitEntity().injectScaledMaxHealth(set, false);
-+                }
-+                // CraftBukkit end
-                 this.broadcastAndSend(new ClientboundUpdateAttributesPacket(this.entity.getId(), set));
-             }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/server/level/ServerLevel.java.patch b/paper-server/patches/unapplied/net/minecraft/server/level/ServerLevel.java.patch
deleted file mode 100644
index d9915e3d7d..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/level/ServerLevel.java.patch
+++ /dev/null
@@ -1,1261 +0,0 @@
---- a/net/minecraft/server/level/ServerLevel.java
-+++ b/net/minecraft/server/level/ServerLevel.java
-@@ -58,7 +58,6 @@
- import net.minecraft.network.protocol.game.ClientboundDamageEventPacket;
- import net.minecraft.network.protocol.game.ClientboundEntityEventPacket;
- import net.minecraft.network.protocol.game.ClientboundExplodePacket;
--import net.minecraft.network.protocol.game.ClientboundGameEventPacket;
- import net.minecraft.network.protocol.game.ClientboundLevelEventPacket;
- import net.minecraft.network.protocol.game.ClientboundLevelParticlesPacket;
- import net.minecraft.network.protocol.game.ClientboundSetDefaultSpawnPositionPacket;
-@@ -124,6 +123,7 @@
- import net.minecraft.world.level.StructureManager;
- import net.minecraft.world.level.WorldGenLevel;
- import net.minecraft.world.level.biome.Biome;
-+import net.minecraft.world.level.biome.BiomeSource;
- import net.minecraft.world.level.block.Block;
- import net.minecraft.world.level.block.Blocks;
- import net.minecraft.world.level.block.SnowLayerBlock;
-@@ -149,7 +149,9 @@
- import net.minecraft.world.level.gameevent.DynamicGameEventListener;
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.level.gameevent.GameEventDispatcher;
-+import net.minecraft.world.level.levelgen.FlatLevelSource;
- import net.minecraft.world.level.levelgen.Heightmap;
-+import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
- import net.minecraft.world.level.levelgen.structure.BoundingBox;
- import net.minecraft.world.level.levelgen.structure.Structure;
- import net.minecraft.world.level.levelgen.structure.StructureCheck;
-@@ -165,7 +167,7 @@
- import net.minecraft.world.level.saveddata.maps.MapItemSavedData;
- import net.minecraft.world.level.storage.DimensionDataStorage;
- import net.minecraft.world.level.storage.LevelStorageSource;
--import net.minecraft.world.level.storage.ServerLevelData;
-+import net.minecraft.world.level.storage.PrimaryLevelData;
- import net.minecraft.world.phys.AABB;
- import net.minecraft.world.phys.Vec3;
- import net.minecraft.world.phys.shapes.BooleanOp;
-@@ -173,6 +175,16 @@
- import net.minecraft.world.phys.shapes.VoxelShape;
- import net.minecraft.world.ticks.LevelTicks;
- import org.slf4j.Logger;
-+import org.bukkit.Bukkit;
-+import org.bukkit.WeatherType;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.craftbukkit.generator.CustomWorldChunkManager;
-+import org.bukkit.craftbukkit.util.WorldUUID;
-+import org.bukkit.event.entity.CreatureSpawnEvent;
-+import org.bukkit.event.server.MapInitializeEvent;
-+import org.bukkit.event.weather.LightningStrikeEvent;
-+import org.bukkit.event.world.TimeSkipEvent;
-+// CraftBukkit end
- 
- public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLevel {
- 
-@@ -187,7 +199,7 @@
-     final List<ServerPlayer> players = Lists.newArrayList();
-     public final ServerChunkCache chunkSource;
-     private final MinecraftServer server;
--    public final ServerLevelData serverLevelData;
-+    public final PrimaryLevelData serverLevelData; // CraftBukkit - type
-     private int lastSpawnChunkRadius;
-     final EntityTickList entityTickList = new EntityTickList();
-     public final PersistentEntitySectionManager<Entity> entityManager;
-@@ -214,54 +226,204 @@
-     private final boolean tickTime;
-     private final RandomSequences randomSequences;
- 
--    public ServerLevel(MinecraftServer server, Executor workerExecutor, LevelStorageSource.LevelStorageAccess session, ServerLevelData properties, ResourceKey<Level> worldKey, LevelStem dimensionOptions, ChunkProgressListener worldGenerationProgressListener, boolean debugWorld, long seed, List<CustomSpawner> spawners, boolean shouldTickTime, @Nullable RandomSequences randomSequencesState) {
--        super(properties, worldKey, server.registryAccess(), dimensionOptions.type(), false, debugWorld, seed, server.getMaxChainedNeighborUpdates());
--        this.tickTime = shouldTickTime;
--        this.server = server;
--        this.customSpawners = spawners;
--        this.serverLevelData = properties;
--        ChunkGenerator chunkgenerator = dimensionOptions.generator();
--        boolean flag2 = server.forceSynchronousWrites();
--        DataFixer datafixer = server.getFixerUpper();
--        EntityPersistentStorage<Entity> entitypersistentstorage = new EntityStorage(new SimpleRegionStorage(new RegionStorageInfo(session.getLevelId(), worldKey, "entities"), session.getDimensionPath(worldKey).resolve("entities"), datafixer, flag2, DataFixTypes.ENTITY_CHUNK), this, server);
-+    // CraftBukkit start
-+    public final LevelStorageSource.LevelStorageAccess convertable;
-+    public final UUID uuid;
-+    public boolean hasPhysicsEvent = true; // Paper - BlockPhysicsEvent
-+    public boolean hasEntityMoveEvent; // Paper - Add EntityMoveEvent
-+
-+    public LevelChunk getChunkIfLoaded(int x, int z) {
-+        return this.chunkSource.getChunkAtIfLoadedImmediately(x, z); // Paper - Use getChunkIfLoadedImmediately
-+    }
-+
-+    @Override
-+    public ResourceKey<LevelStem> getTypeKey() {
-+        return this.convertable.dimensionType;
-+    }
-+
-+    // Paper start
-+    public final boolean areChunksLoadedForMove(AABB axisalignedbb) {
-+        // copied code from collision methods, so that we can guarantee that they wont load chunks (we don't override
-+        // ICollisionAccess methods for VoxelShapes)
-+        // be more strict too, add a block (dumb plugins in move events?)
-+        int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3;
-+        int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3;
-+
-+        int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3;
-+        int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3;
-+
-+        int minChunkX = minBlockX >> 4;
-+        int maxChunkX = maxBlockX >> 4;
-+
-+        int minChunkZ = minBlockZ >> 4;
-+        int maxChunkZ = maxBlockZ >> 4;
-+
-+        ServerChunkCache chunkProvider = this.getChunkSource();
-+
-+        for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
-+            for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
-+                if (chunkProvider.getChunkAtIfLoadedImmediately(cx, cz) == null) {
-+                    return false;
-+                }
-+            }
-+        }
-+
-+        return true;
-+    }
-+
-+    public final void loadChunksForMoveAsync(AABB axisalignedbb, ca.spottedleaf.concurrentutil.util.Priority priority,
-+                                             java.util.function.Consumer<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
-+        if (Thread.currentThread() != this.thread) {
-+            this.getChunkSource().mainThreadProcessor.execute(() -> {
-+                this.loadChunksForMoveAsync(axisalignedbb, priority, onLoad);
-+            });
-+            return;
-+        }
-+        int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3;
-+        int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3;
-+
-+        int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3;
-+        int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3;
-+
-+        int minChunkX = minBlockX >> 4;
-+        int minChunkZ = minBlockZ >> 4;
-+
-+        int maxChunkX = maxBlockX >> 4;
-+        int maxChunkZ = maxBlockZ >> 4;
-+
-+        this.loadChunks(minChunkX, minChunkZ, maxChunkX, maxChunkZ, priority, onLoad);
-+    }
-+
-+    public final void loadChunks(int minChunkX, int minChunkZ, int maxChunkX, int maxChunkZ,
-+                                 ca.spottedleaf.concurrentutil.util.Priority priority,
-+                                 java.util.function.Consumer<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
-+        List<net.minecraft.world.level.chunk.ChunkAccess> ret = new java.util.ArrayList<>();
-+        it.unimi.dsi.fastutil.ints.IntArrayList ticketLevels = new it.unimi.dsi.fastutil.ints.IntArrayList();
-+        ServerChunkCache chunkProvider = this.getChunkSource();
- 
-+        int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1);
-+        int[] loadedChunks = new int[1];
-+
-+        Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter++);
-+
-+        java.util.function.Consumer<net.minecraft.world.level.chunk.ChunkAccess> consumer = (net.minecraft.world.level.chunk.ChunkAccess chunk) -> {
-+            if (chunk != null) {
-+                int ticketLevel = Math.max(33, chunkProvider.chunkMap.getUpdatingChunkIfPresent(chunk.getPos().toLong()).getTicketLevel());
-+                ret.add(chunk);
-+                ticketLevels.add(ticketLevel);
-+                chunkProvider.addTicketAtLevel(TicketType.FUTURE_AWAIT, chunk.getPos(), ticketLevel, holderIdentifier);
-+            }
-+            if (++loadedChunks[0] == requiredChunks) {
-+                try {
-+                    onLoad.accept(java.util.Collections.unmodifiableList(ret));
-+                } finally {
-+                    for (int i = 0, len = ret.size(); i < len; ++i) {
-+                        ChunkPos chunkPos = ret.get(i).getPos();
-+                        int ticketLevel = ticketLevels.getInt(i);
-+
-+                        chunkProvider.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos);
-+                        chunkProvider.removeTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, holderIdentifier);
-+                    }
-+                }
-+            }
-+        };
-+
-+        for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
-+            for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
-+                ca.spottedleaf.moonrise.common.util.ChunkSystem.scheduleChunkLoad(
-+                    this, cx, cz, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true, priority, consumer
-+                );
-+            }
-+        }
-+    }
-+    // Paper end
-+
-+    // Paper start - optimise getPlayerByUUID
-+    @Nullable
-+    @Override
-+    public Player getPlayerByUUID(UUID uuid) {
-+        final Player player = this.getServer().getPlayerList().getPlayer(uuid);
-+        return player != null && player.level() == this ? player : null;
-+    }
-+    // Paper end - optimise getPlayerByUUID
-+
-+    // Add env and gen to constructor, IWorldDataServer -> WorldDataServer
-+    public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey<Level> resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List<CustomSpawner> list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) {
-+        super(iworlddataserver, resourcekey, minecraftserver.registryAccess(), worlddimension.type(), false, flag, i, minecraftserver.getMaxChainedNeighborUpdates(), gen, biomeProvider, env, spigotConfig -> minecraftserver.paperConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(convertable_conversionsession.levelDirectory.path(), iworlddataserver.getLevelName(), resourcekey.location(), spigotConfig, minecraftserver.registryAccess(), iworlddataserver.getGameRules()))); // Paper - create paper world configs
-+        this.pvpMode = minecraftserver.isPvpAllowed();
-+        this.convertable = convertable_conversionsession;
-+        this.uuid = WorldUUID.getUUID(convertable_conversionsession.levelDirectory.path().toFile());
-+        // CraftBukkit end
-+        this.tickTime = flag1;
-+        this.server = minecraftserver;
-+        this.customSpawners = list;
-+        this.serverLevelData = iworlddataserver;
-+        ChunkGenerator chunkgenerator = worlddimension.generator();
-+        // CraftBukkit start
-+        this.serverLevelData.setWorld(this);
-+
-+        if (biomeProvider != null) {
-+            BiomeSource worldChunkManager = new CustomWorldChunkManager(this.getWorld(), biomeProvider, this.server.registryAccess().lookupOrThrow(Registries.BIOME), chunkgenerator.getBiomeSource()); // Paper - add vanillaBiomeProvider
-+            if (chunkgenerator instanceof NoiseBasedChunkGenerator cga) {
-+                chunkgenerator = new NoiseBasedChunkGenerator(worldChunkManager, cga.settings);
-+            } else if (chunkgenerator instanceof FlatLevelSource cpf) {
-+                chunkgenerator = new FlatLevelSource(cpf.settings(), worldChunkManager);
-+            }
-+        }
-+
-+        if (gen != null) {
-+            chunkgenerator = new org.bukkit.craftbukkit.generator.CustomChunkGenerator(this, chunkgenerator, gen);
-+        }
-+        // CraftBukkit end
-+        boolean flag2 = minecraftserver.forceSynchronousWrites();
-+        DataFixer datafixer = minecraftserver.getFixerUpper();
-+        EntityPersistentStorage<Entity> entitypersistentstorage = new EntityStorage(new SimpleRegionStorage(new RegionStorageInfo(convertable_conversionsession.getLevelId(), resourcekey, "entities"), convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"), datafixer, flag2, DataFixTypes.ENTITY_CHUNK), this, minecraftserver);
-+
-         this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entitypersistentstorage);
--        StructureTemplateManager structuretemplatemanager = server.getStructureManager();
--        int j = server.getPlayerList().getViewDistance();
--        int k = server.getPlayerList().getSimulationDistance();
-+        StructureTemplateManager structuretemplatemanager = minecraftserver.getStructureManager();
-+        int j = this.spigotConfig.viewDistance; // Spigot
-+        int k = this.spigotConfig.simulationDistance; // Spigot
-         PersistentEntitySectionManager persistententitysectionmanager = this.entityManager;
- 
-         Objects.requireNonNull(this.entityManager);
--        this.chunkSource = new ServerChunkCache(this, session, datafixer, structuretemplatemanager, workerExecutor, chunkgenerator, j, k, flag2, worldGenerationProgressListener, persistententitysectionmanager::updateChunkStatus, () -> {
--            return server.overworld().getDataStorage();
-+        this.chunkSource = new ServerChunkCache(this, convertable_conversionsession, datafixer, structuretemplatemanager, executor, chunkgenerator, j, k, flag2, worldloadlistener, persistententitysectionmanager::updateChunkStatus, () -> {
-+            return minecraftserver.overworld().getDataStorage();
-         });
-         this.chunkSource.getGeneratorState().ensureStructuresGenerated();
-         this.portalForcer = new PortalForcer(this);
-         this.updateSkyBrightness();
-         this.prepareWeather();
--        this.getWorldBorder().setAbsoluteMaxSize(server.getAbsoluteMaxWorldSize());
-+        this.getWorldBorder().setAbsoluteMaxSize(minecraftserver.getAbsoluteMaxWorldSize());
-         this.raids = (Raids) this.getDataStorage().computeIfAbsent(Raids.factory(this), Raids.getFileId(this.dimensionTypeRegistration()));
--        if (!server.isSingleplayer()) {
--            properties.setGameType(server.getDefaultGameType());
-+        if (!minecraftserver.isSingleplayer()) {
-+            iworlddataserver.setGameType(minecraftserver.getDefaultGameType());
-         }
- 
--        long l = server.getWorldData().worldGenOptions().seed();
-+        long l = minecraftserver.getWorldData().worldGenOptions().seed();
- 
--        this.structureCheck = new StructureCheck(this.chunkSource.chunkScanner(), this.registryAccess(), server.getStructureManager(), worldKey, chunkgenerator, this.chunkSource.randomState(), this, chunkgenerator.getBiomeSource(), l, datafixer);
--        this.structureManager = new StructureManager(this, server.getWorldData().worldGenOptions(), this.structureCheck);
--        if (this.dimension() == Level.END && this.dimensionTypeRegistration().is(BuiltinDimensionTypes.END)) {
--            this.dragonFight = new EndDragonFight(this, l, server.getWorldData().endDragonFightData());
-+        this.structureCheck = new StructureCheck(this.chunkSource.chunkScanner(), this.registryAccess(), minecraftserver.getStructureManager(), this.getTypeKey(), chunkgenerator, this.chunkSource.randomState(), this, chunkgenerator.getBiomeSource(), l, datafixer); // Paper - Fix missing CB diff
-+        this.structureManager = new StructureManager(this, this.serverLevelData.worldGenOptions(), this.structureCheck); // CraftBukkit
-+        if ((this.dimension() == Level.END && this.dimensionTypeRegistration().is(BuiltinDimensionTypes.END)) || env == org.bukkit.World.Environment.THE_END) { // CraftBukkit - Allow to create EnderDragonBattle in default and custom END
-+            this.dragonFight = new EndDragonFight(this, this.serverLevelData.worldGenOptions().seed(), this.serverLevelData.endDragonFightData()); // CraftBukkit
-         } else {
-             this.dragonFight = null;
-         }
- 
-         this.sleepStatus = new SleepStatus();
-         this.gameEventDispatcher = new GameEventDispatcher(this);
--        this.randomSequences = (RandomSequences) Objects.requireNonNullElseGet(randomSequencesState, () -> {
-+        this.randomSequences = (RandomSequences) Objects.requireNonNullElseGet(randomsequences, () -> {
-             return (RandomSequences) this.getDataStorage().computeIfAbsent(RandomSequences.factory(l), "random_sequences");
-         });
-+        this.getCraftServer().addWorld(this.getWorld()); // CraftBukkit
-     }
- 
-+    // Paper start
-+    @Override
-+    public boolean hasChunk(int chunkX, int chunkZ) {
-+        return this.getChunkSource().getChunkAtIfLoadedImmediately(chunkX, chunkZ) != null;
-+    }
-+    // Paper end
-+
-     /** @deprecated */
-     @Deprecated
-     @VisibleForTesting
-@@ -273,8 +435,8 @@
-         this.serverLevelData.setClearWeatherTime(clearDuration);
-         this.serverLevelData.setRainTime(rainDuration);
-         this.serverLevelData.setThunderTime(rainDuration);
--        this.serverLevelData.setRaining(raining);
--        this.serverLevelData.setThundering(thundering);
-+        this.serverLevelData.setRaining(raining, org.bukkit.event.weather.WeatherChangeEvent.Cause.COMMAND); // Paper - Add cause to Weather/ThunderChangeEvents
-+        this.serverLevelData.setThundering(thundering, org.bukkit.event.weather.ThunderChangeEvent.Cause.COMMAND); // Paper - Add cause to Weather/ThunderChangeEvents
-     }
- 
-     @Override
-@@ -305,12 +467,20 @@
-         long j;
- 
-         if (this.sleepStatus.areEnoughSleeping(i) && this.sleepStatus.areEnoughDeepSleeping(i, this.players)) {
-+            // CraftBukkit start
-+            j = this.levelData.getDayTime() + 24000L;
-+            TimeSkipEvent event = new TimeSkipEvent(this.getWorld(), TimeSkipEvent.SkipReason.NIGHT_SKIP, (j - j % 24000L) - this.getDayTime());
-             if (this.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) {
--                j = this.levelData.getDayTime() + 24000L;
--                this.setDayTime(j - j % 24000L);
-+                this.getCraftServer().getPluginManager().callEvent(event);
-+                if (!event.isCancelled()) {
-+                    this.setDayTime(this.getDayTime() + event.getSkipAmount());
-+                }
-             }
- 
--            this.wakeUpAllPlayers();
-+            if (!event.isCancelled()) {
-+                this.wakeUpAllPlayers();
-+            }
-+            // CraftBukkit end
-             if (this.getGameRules().getBoolean(GameRules.RULE_WEATHER_CYCLE) && this.isRaining()) {
-                 this.resetWeatherCycle();
-             }
-@@ -325,9 +495,9 @@
-         if (!this.isDebug() && flag) {
-             j = this.getGameTime();
-             gameprofilerfiller.push("blockTicks");
--            this.blockTicks.tick(j, 65536, this::tickBlock);
-+            this.blockTicks.tick(j, paperConfig().environment.maxBlockTicks, this::tickBlock); // Paper - configurable max block ticks
-             gameprofilerfiller.popPush("fluidTicks");
--            this.fluidTicks.tick(j, 65536, this::tickFluid);
-+            this.fluidTicks.tick(j, paperConfig().environment.maxFluidTicks, this::tickFluid); // Paper - configurable max fluid ticks
-             gameprofilerfiller.pop();
-         }
- 
-@@ -345,7 +515,7 @@
- 
-         this.handlingTick = false;
-         gameprofilerfiller.pop();
--        boolean flag1 = !this.players.isEmpty() || !this.getForcedChunks().isEmpty();
-+        boolean flag1 = !paperConfig().unsupportedSettings.disableWorldTickingWhenEmpty || !this.players.isEmpty() || !this.getForcedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players // Paper - restore this
- 
-         if (flag1) {
-             this.resetEmptyTime();
-@@ -359,6 +529,7 @@
-                 gameprofilerfiller.pop();
-             }
- 
-+            org.spigotmc.ActivationRange.activateEntities(this); // Spigot
-             this.entityTickList.forEach((entity) -> {
-                 if (!entity.isRemoved()) {
-                     if (!tickratemanager.isEntityFrozen(entity)) {
-@@ -429,7 +600,7 @@
- 
-     private void wakeUpAllPlayers() {
-         this.sleepStatus.removeAllSleepers();
--        ((List) this.players.stream().filter(LivingEntity::isSleeping).collect(Collectors.toList())).forEach((entityplayer) -> {
-+        (this.players.stream().filter(LivingEntity::isSleeping).collect(Collectors.toList())).forEach((entityplayer) -> { // CraftBukkit - decompile error
-             entityplayer.stopSleepInBed(false, false);
-         });
-     }
-@@ -442,12 +613,12 @@
-         ProfilerFiller gameprofilerfiller = Profiler.get();
- 
-         gameprofilerfiller.push("thunder");
--        if (flag && this.isThundering() && this.random.nextInt(100000) == 0) {
-+        if (!this.paperConfig().environment.disableThunder && flag && this.isThundering() && this.spigotConfig.thunderChance > 0 && this.random.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot // Paper - Option to disable thunder
-             BlockPos blockposition = this.findLightningTargetAround(this.getBlockRandomPos(j, 0, k, 15));
- 
-             if (this.isRainingAt(blockposition)) {
-                 DifficultyInstance difficultydamagescaler = this.getCurrentDifficultyAt(blockposition);
--                boolean flag1 = this.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && this.random.nextDouble() < (double) difficultydamagescaler.getEffectiveDifficulty() * 0.01D && !this.getBlockState(blockposition.below()).is(Blocks.LIGHTNING_ROD);
-+                boolean flag1 = this.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && this.random.nextDouble() < (double) difficultydamagescaler.getEffectiveDifficulty() * this.paperConfig().entities.spawning.skeletonHorseThunderSpawnChance.or(0.01D) && !this.getBlockState(blockposition.below()).is(Blocks.LIGHTNING_ROD); // Paper - Configurable spawn chances for skeleton horses
- 
-                 if (flag1) {
-                     SkeletonHorse entityhorseskeleton = (SkeletonHorse) EntityType.SKELETON_HORSE.create(this, EntitySpawnReason.EVENT);
-@@ -456,7 +627,7 @@
-                         entityhorseskeleton.setTrap(true);
-                         entityhorseskeleton.setAge(0);
-                         entityhorseskeleton.setPos((double) blockposition.getX(), (double) blockposition.getY(), (double) blockposition.getZ());
--                        this.addFreshEntity(entityhorseskeleton);
-+                        this.addFreshEntity(entityhorseskeleton, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.LIGHTNING); // CraftBukkit
-                     }
-                 }
- 
-@@ -465,18 +636,20 @@
-                 if (entitylightning != null) {
-                     entitylightning.moveTo(Vec3.atBottomCenterOf(blockposition));
-                     entitylightning.setVisualOnly(flag1);
--                    this.addFreshEntity(entitylightning);
-+                    this.strikeLightning(entitylightning, org.bukkit.event.weather.LightningStrikeEvent.Cause.WEATHER); // CraftBukkit
-                 }
-             }
-         }
- 
-         gameprofilerfiller.popPush("iceandsnow");
- 
-+        if (!this.paperConfig().environment.disableIceAndSnow) { // Paper - Option to disable ice and snow
-         for (int l = 0; l < randomTickSpeed; ++l) {
-             if (this.random.nextInt(48) == 0) {
-                 this.tickPrecipitation(this.getBlockRandomPos(j, 0, k, 15));
-             }
-         }
-+        } // Paper - Option to disable ice and snow
- 
-         gameprofilerfiller.popPush("tickBlocks");
-         if (randomTickSpeed > 0) {
-@@ -521,7 +694,7 @@
-         Biome biomebase = (Biome) this.getBiome(blockposition1).value();
- 
-         if (biomebase.shouldFreeze(this, blockposition2)) {
--            this.setBlockAndUpdate(blockposition2, Blocks.ICE.defaultBlockState());
-+            org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, blockposition2, Blocks.ICE.defaultBlockState(), null); // CraftBukkit
-         }
- 
-         if (this.isRaining()) {
-@@ -537,10 +710,10 @@
-                         BlockState iblockdata1 = (BlockState) iblockdata.setValue(SnowLayerBlock.LAYERS, j + 1);
- 
-                         Block.pushEntitiesUp(iblockdata, iblockdata1, this, blockposition1);
--                        this.setBlockAndUpdate(blockposition1, iblockdata1);
-+                        org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, blockposition1, iblockdata1, null); // CraftBukkit
-                     }
-                 } else {
--                    this.setBlockAndUpdate(blockposition1, Blocks.SNOW.defaultBlockState());
-+                    org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, blockposition1, Blocks.SNOW.defaultBlockState(), null); // CraftBukkit
-                 }
-             }
- 
-@@ -568,6 +741,11 @@
-     }
- 
-     protected BlockPos findLightningTargetAround(BlockPos pos) {
-+        // Paper start - Add methods to find targets for lightning strikes
-+        return this.findLightningTargetAround(pos, false);
-+    }
-+    public BlockPos findLightningTargetAround(BlockPos pos, boolean returnNullWhenNoTarget) {
-+        // Paper end - Add methods to find targets for lightning strikes
-         BlockPos blockposition1 = this.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, pos);
-         Optional<BlockPos> optional = this.findLightningRod(blockposition1);
- 
-@@ -576,12 +754,13 @@
-         } else {
-             AABB axisalignedbb = AABB.encapsulatingFullBlocks(blockposition1, blockposition1.atY(this.getMaxY() + 1)).inflate(3.0D);
-             List<LivingEntity> list = this.getEntitiesOfClass(LivingEntity.class, axisalignedbb, (entityliving) -> {
--                return entityliving != null && entityliving.isAlive() && this.canSeeSky(entityliving.blockPosition());
-+                return entityliving != null && entityliving.isAlive() && this.canSeeSky(entityliving.blockPosition()) && !entityliving.isSpectator(); // Paper - Fix lightning being able to hit spectators (MC-262422)
-             });
- 
-             if (!list.isEmpty()) {
-                 return ((LivingEntity) list.get(this.random.nextInt(list.size()))).blockPosition();
-             } else {
-+                if (returnNullWhenNoTarget) return null; // Paper - Add methods to find targets for lightning strikes
-                 if (blockposition1.getY() == this.getMinY() - 1) {
-                     blockposition1 = blockposition1.above(2);
-                 }
-@@ -679,8 +858,8 @@
-                 this.serverLevelData.setThunderTime(j);
-                 this.serverLevelData.setRainTime(k);
-                 this.serverLevelData.setClearWeatherTime(i);
--                this.serverLevelData.setThundering(flag1);
--                this.serverLevelData.setRaining(flag2);
-+                this.serverLevelData.setThundering(flag1, org.bukkit.event.weather.ThunderChangeEvent.Cause.NATURAL); // Paper - Add cause to Weather/ThunderChangeEvents
-+                this.serverLevelData.setRaining(flag2, org.bukkit.event.weather.WeatherChangeEvent.Cause.NATURAL); // Paper - Add cause to Weather/ThunderChangeEvents
-             }
- 
-             this.oThunderLevel = this.thunderLevel;
-@@ -701,33 +880,67 @@
-             this.rainLevel = Mth.clamp(this.rainLevel, 0.0F, 1.0F);
-         }
- 
-+        /* CraftBukkit start
-         if (this.oRainLevel != this.rainLevel) {
--            this.server.getPlayerList().broadcastAll(new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, this.rainLevel), this.dimension());
-+            this.server.getPlayerList().broadcastAll(new PacketPlayOutGameStateChange(PacketPlayOutGameStateChange.RAIN_LEVEL_CHANGE, this.rainLevel), this.dimension());
-         }
- 
-         if (this.oThunderLevel != this.thunderLevel) {
--            this.server.getPlayerList().broadcastAll(new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, this.thunderLevel), this.dimension());
-+            this.server.getPlayerList().broadcastAll(new PacketPlayOutGameStateChange(PacketPlayOutGameStateChange.THUNDER_LEVEL_CHANGE, this.thunderLevel), this.dimension());
-         }
- 
-         if (flag != this.isRaining()) {
-             if (flag) {
--                this.server.getPlayerList().broadcastAll(new ClientboundGameEventPacket(ClientboundGameEventPacket.STOP_RAINING, 0.0F));
--            } else {
--                this.server.getPlayerList().broadcastAll(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0.0F));
-+                this.server.getPlayerList().broadcastAll(new PacketPlayOutGameStateChange(PacketPlayOutGameStateChange.STOP_RAINING, 0.0F));
-+            } else {
-+                this.server.getPlayerList().broadcastAll(new PacketPlayOutGameStateChange(PacketPlayOutGameStateChange.START_RAINING, 0.0F));
-             }
- 
--            this.server.getPlayerList().broadcastAll(new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, this.rainLevel));
--            this.server.getPlayerList().broadcastAll(new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, this.thunderLevel));
-+            this.server.getPlayerList().broadcastAll(new PacketPlayOutGameStateChange(PacketPlayOutGameStateChange.RAIN_LEVEL_CHANGE, this.rainLevel));
-+            this.server.getPlayerList().broadcastAll(new PacketPlayOutGameStateChange(PacketPlayOutGameStateChange.THUNDER_LEVEL_CHANGE, this.thunderLevel));
-         }
-+        // */
-+        for (int idx = 0; idx < this.players.size(); ++idx) {
-+            if (((ServerPlayer) this.players.get(idx)).level() == this) {
-+                ((ServerPlayer) this.players.get(idx)).tickWeather();
-+            }
-+        }
- 
-+        if (flag != this.isRaining()) {
-+            // Only send weather packets to those affected
-+            for (int idx = 0; idx < this.players.size(); ++idx) {
-+                if (((ServerPlayer) this.players.get(idx)).level() == this) {
-+                    ((ServerPlayer) this.players.get(idx)).setPlayerWeather((!flag ? WeatherType.DOWNFALL : WeatherType.CLEAR), false);
-+                }
-+            }
-+        }
-+        for (int idx = 0; idx < this.players.size(); ++idx) {
-+            if (((ServerPlayer) this.players.get(idx)).level() == this) {
-+                ((ServerPlayer) this.players.get(idx)).updateWeather(this.oRainLevel, this.rainLevel, this.oThunderLevel, this.thunderLevel);
-+            }
-+        }
-+        // CraftBukkit end
-+
-     }
- 
-     @VisibleForTesting
-     public void resetWeatherCycle() {
--        this.serverLevelData.setRainTime(0);
--        this.serverLevelData.setRaining(false);
--        this.serverLevelData.setThunderTime(0);
--        this.serverLevelData.setThundering(false);
-+        // CraftBukkit start
-+        this.serverLevelData.setRaining(false, org.bukkit.event.weather.WeatherChangeEvent.Cause.SLEEP); // Paper - Add cause to Weather/ThunderChangeEvents
-+        // If we stop due to everyone sleeping we should reset the weather duration to some other random value.
-+        // Not that everyone ever manages to get the whole server to sleep at the same time....
-+        if (!this.serverLevelData.isRaining()) {
-+            this.serverLevelData.setRainTime(0);
-+        }
-+        // CraftBukkit end
-+        this.serverLevelData.setThundering(false, org.bukkit.event.weather.ThunderChangeEvent.Cause.SLEEP); // Paper - Add cause to Weather/ThunderChangeEvents
-+        // CraftBukkit start
-+        // If we stop due to everyone sleeping we should reset the weather duration to some other random value.
-+        // Not that everyone ever manages to get the whole server to sleep at the same time....
-+        if (!this.serverLevelData.isThundering()) {
-+            this.serverLevelData.setThunderTime(0);
-+        }
-+        // CraftBukkit end
-     }
- 
-     public void resetEmptyTime() {
-@@ -754,6 +967,13 @@
-     }
- 
-     public void tickNonPassenger(Entity entity) {
-+        // Spigot start
-+        if (!org.spigotmc.ActivationRange.checkIfActive(entity)) {
-+            entity.tickCount++;
-+            entity.inactiveTick();
-+            return;
-+        }
-+        // Spigot end
-         entity.setOldPosAndRot();
-         ProfilerFiller gameprofilerfiller = Profiler.get();
- 
-@@ -763,6 +983,7 @@
-         });
-         gameprofilerfiller.incrementCounter("tickNonPassenger");
-         entity.tick();
-+        entity.postTick(); // CraftBukkit
-         gameprofilerfiller.pop();
-         Iterator iterator = entity.getPassengers().iterator();
- 
-@@ -786,6 +1007,7 @@
-                 });
-                 gameprofilerfiller.incrementCounter("tickPassenger");
-                 passenger.rideTick();
-+                passenger.postTick(); // CraftBukkit
-                 gameprofilerfiller.pop();
-                 Iterator iterator = passenger.getPassengers().iterator();
- 
-@@ -810,6 +1032,7 @@
-         ServerChunkCache chunkproviderserver = this.getChunkSource();
- 
-         if (!savingDisabled) {
-+            org.bukkit.Bukkit.getPluginManager().callEvent(new org.bukkit.event.world.WorldSaveEvent(this.getWorld())); // CraftBukkit
-             if (progressListener != null) {
-                 progressListener.progressStartNoAbort(Component.translatable("menu.savingLevel"));
-             }
-@@ -827,11 +1050,19 @@
-             }
- 
-         }
-+
-+        // CraftBukkit start - moved from MinecraftServer.saveChunks
-+        ServerLevel worldserver1 = this;
-+
-+        this.serverLevelData.setWorldBorder(worldserver1.getWorldBorder().createSettings());
-+        this.serverLevelData.setCustomBossEvents(this.server.getCustomBossEvents().save(this.registryAccess()));
-+        this.convertable.saveDataTag(this.server.registryAccess(), this.serverLevelData, this.server.getPlayerList().getSingleplayerData());
-+        // CraftBukkit end
-     }
- 
-     private void saveLevelData(boolean flush) {
-         if (this.dragonFight != null) {
--            this.server.getWorldData().setEndDragonFightData(this.dragonFight.saveData());
-+            this.serverLevelData.setEndDragonFightData(this.dragonFight.saveData()); // CraftBukkit
-         }
- 
-         DimensionDataStorage worldpersistentdata = this.getChunkSource().getDataStorage();
-@@ -903,18 +1134,40 @@
- 
-     @Override
-     public boolean addFreshEntity(Entity entity) {
--        return this.addEntity(entity);
-+        // CraftBukkit start
-+        return this.addFreshEntity(entity, CreatureSpawnEvent.SpawnReason.DEFAULT);
-     }
- 
-+    @Override
-+    public boolean addFreshEntity(Entity entity, CreatureSpawnEvent.SpawnReason reason) {
-+        return this.addEntity(entity, reason);
-+        // CraftBukkit end
-+    }
-+
-     public boolean addWithUUID(Entity entity) {
--        return this.addEntity(entity);
-+        // CraftBukkit start
-+        return this.addWithUUID(entity, CreatureSpawnEvent.SpawnReason.DEFAULT);
-+    }
-+
-+    public boolean addWithUUID(Entity entity, CreatureSpawnEvent.SpawnReason reason) {
-+        return this.addEntity(entity, reason);
-+        // CraftBukkit end
-     }
- 
-     public void addDuringTeleport(Entity entity) {
-+        // CraftBukkit start
-+        // SPIGOT-6415: Don't call spawn event for entities which travel trough worlds,
-+        // since it is only an implementation detail, that a new entity is created when
-+        // they are traveling between worlds.
-+        this.addDuringTeleport(entity, null);
-+    }
-+
-+    public void addDuringTeleport(Entity entity, CreatureSpawnEvent.SpawnReason reason) {
-+        // CraftBukkit end
-         if (entity instanceof ServerPlayer entityplayer) {
-             this.addPlayer(entityplayer);
-         } else {
--            this.addEntity(entity);
-+            this.addEntity(entity, reason); // CraftBukkit
-         }
- 
-     }
-@@ -939,41 +1192,116 @@
-         this.entityManager.addNewEntity(player);
-     }
- 
--    private boolean addEntity(Entity entity) {
-+    // CraftBukkit start
-+    private boolean addEntity(Entity entity, CreatureSpawnEvent.SpawnReason spawnReason) {
-+        org.spigotmc.AsyncCatcher.catchOp("entity add"); // Spigot
-+        entity.generation = false; // Paper - Don't fire sync event during generation; Reset flag if it was added during a ServerLevel generation process
-+        // Paper start - extra debug info
-+        if (entity.valid) {
-+            MinecraftServer.LOGGER.error("Attempted Double World add on {}", entity, new Throwable());
-+            return true;
-+        }
-+        // Paper end - extra debug info
-+        if (entity.spawnReason == null) entity.spawnReason = spawnReason; // Paper - Entity#getEntitySpawnReason
-         if (entity.isRemoved()) {
--            ServerLevel.LOGGER.warn("Tried to add entity {} but it was marked as removed already", EntityType.getKey(entity.getType()));
-+            // WorldServer.LOGGER.warn("Tried to add entity {} but it was marked as removed already", EntityTypes.getKey(entity.getType())); // CraftBukkit
-             return false;
-         } else {
-+            if (entity instanceof net.minecraft.world.entity.item.ItemEntity itemEntity && itemEntity.getItem().isEmpty()) return false; // Paper - Prevent empty items from being added
-+            // Paper start - capture all item additions to the world
-+            if (captureDrops != null && entity instanceof net.minecraft.world.entity.item.ItemEntity) {
-+                captureDrops.add((net.minecraft.world.entity.item.ItemEntity) entity);
-+                return true;
-+            }
-+            // Paper end - capture all item additions to the world
-+            // SPIGOT-6415: Don't call spawn event when reason is null. For example when an entity teleports to a new world.
-+            if (spawnReason != null && !CraftEventFactory.doEntityAddEventCalling(this, entity, spawnReason)) {
-+                return false;
-+            }
-+            // CraftBukkit end
-+
-             return this.entityManager.addNewEntity(entity);
-         }
-     }
- 
-     public boolean tryAddFreshEntityWithPassengers(Entity entity) {
--        Stream stream = entity.getSelfAndPassengers().map(Entity::getUUID);
-+        // CraftBukkit start
-+        return this.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT);
-+    }
-+
-+    public boolean tryAddFreshEntityWithPassengers(Entity entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) {
-+        // CraftBukkit end
-+        Stream<UUID> stream = entity.getSelfAndPassengers().map(Entity::getUUID); // CraftBukkit - decompile error
-         PersistentEntitySectionManager persistententitysectionmanager = this.entityManager;
- 
-         Objects.requireNonNull(this.entityManager);
-         if (stream.anyMatch(persistententitysectionmanager::isLoaded)) {
-             return false;
-         } else {
--            this.addFreshEntityWithPassengers(entity);
-+            this.addFreshEntityWithPassengers(entity, reason); // CraftBukkit
-             return true;
-         }
-     }
- 
-     public void unload(LevelChunk chunk) {
-+        // Spigot Start
-+        for (net.minecraft.world.level.block.entity.BlockEntity tileentity : chunk.getBlockEntities().values()) {
-+            if (tileentity instanceof net.minecraft.world.Container) {
-+                // Paper start - this area looks like it can load chunks, change the behavior
-+                // chests for example can apply physics to the world
-+                // so instead we just change the active container and call the event
-+                for (org.bukkit.entity.HumanEntity h : Lists.newArrayList(((net.minecraft.world.Container) tileentity).getViewers())) {
-+                    ((org.bukkit.craftbukkit.entity.CraftHumanEntity) h).getHandle().closeUnloadedInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason.UNLOADED); // Paper - Inventory close reason
-+                }
-+                // Paper end - this area looks like it can load chunks, change the behavior
-+            }
-+        }
-+        // Spigot End
-         chunk.clearAllBlockEntities();
-         chunk.unregisterTickContainerFromLevel(this);
-     }
- 
-     public void removePlayerImmediately(ServerPlayer player, Entity.RemovalReason reason) {
--        player.remove(reason);
-+        player.remove(reason, null); // CraftBukkit - add Bukkit remove cause
-     }
- 
-+    // CraftBukkit start
-+    public boolean strikeLightning(Entity entitylightning) {
-+        return this.strikeLightning(entitylightning, LightningStrikeEvent.Cause.UNKNOWN);
-+    }
-+
-+    public boolean strikeLightning(Entity entitylightning, LightningStrikeEvent.Cause cause) {
-+        LightningStrikeEvent lightning = CraftEventFactory.callLightningStrikeEvent((org.bukkit.entity.LightningStrike) entitylightning.getBukkitEntity(), cause);
-+
-+        if (lightning.isCancelled()) {
-+            return false;
-+        }
-+
-+        return this.addFreshEntity(entitylightning);
-+    }
-+    // CraftBukkit end
-+
-     @Override
-     public void destroyBlockProgress(int entityId, BlockPos pos, int progress) {
-         Iterator iterator = this.server.getPlayerList().getPlayers().iterator();
- 
-+        // CraftBukkit start
-+        Player entityhuman = null;
-+        Entity entity = this.getEntity(entityId);
-+        if (entity instanceof Player) entityhuman = (Player) entity;
-+        // CraftBukkit end
-+
-+        // Paper start - Add BlockBreakProgressUpdateEvent
-+        // If a plugin is using this method to send destroy packets for a client-side only entity id, no block progress occurred on the server.
-+        // Hence, do not call the event.
-+        if (entity != null) {
-+            float progressFloat = Mth.clamp(progress, 0, 10) / 10.0f;
-+            org.bukkit.craftbukkit.block.CraftBlock bukkitBlock = org.bukkit.craftbukkit.block.CraftBlock.at(this, pos);
-+            new io.papermc.paper.event.block.BlockBreakProgressUpdateEvent(bukkitBlock, progressFloat, entity.getBukkitEntity())
-+                .callEvent();
-+        }
-+        // Paper end - Add BlockBreakProgressUpdateEvent
-+
-         while (iterator.hasNext()) {
-             ServerPlayer entityplayer = (ServerPlayer) iterator.next();
- 
-@@ -982,6 +1310,12 @@
-                 double d1 = (double) pos.getY() - entityplayer.getY();
-                 double d2 = (double) pos.getZ() - entityplayer.getZ();
- 
-+                // CraftBukkit start
-+                if (entityhuman != null && !entityplayer.getBukkitEntity().canSee(entityhuman.getBukkitEntity())) {
-+                    continue;
-+                }
-+                // CraftBukkit end
-+
-                 if (d0 * d0 + d1 * d1 + d2 * d2 < 1024.0D) {
-                     entityplayer.connection.send(new ClientboundBlockDestructionPacket(entityId, pos, progress));
-                 }
-@@ -1030,7 +1364,7 @@
- 
-     @Override
-     public void levelEvent(@Nullable Player player, int eventId, BlockPos pos, int data) {
--        this.server.getPlayerList().broadcast(player, (double) pos.getX(), (double) pos.getY(), (double) pos.getZ(), 64.0D, this.dimension(), new ClientboundLevelEventPacket(eventId, pos, data, false));
-+        this.server.getPlayerList().broadcast(player, (double) pos.getX(), (double) pos.getY(), (double) pos.getZ(), 64.0D, this.dimension(), new ClientboundLevelEventPacket(eventId, pos, data, false)); // Paper - diff on change (the 64.0 distance is used as defaults for sound ranges in spigot config for ender dragon, end portal and wither)
-     }
- 
-     public int getLogicalHeight() {
-@@ -1039,6 +1373,11 @@
- 
-     @Override
-     public void gameEvent(Holder<GameEvent> event, Vec3 emitterPos, GameEvent.Context emitter) {
-+        // Paper start - Prevent GameEvents being fired from unloaded chunks
-+        if (this.getChunkIfLoadedImmediately((Mth.floor(emitterPos.x) >> 4), (Mth.floor(emitterPos.z) >> 4)) == null) {
-+            return;
-+        }
-+        // Paper end - Prevent GameEvents being fired from unloaded chunks
-         this.gameEventDispatcher.post(event, emitterPos, emitter);
-     }
- 
-@@ -1052,6 +1391,7 @@
- 
-         this.getChunkSource().blockChanged(pos);
-         this.pathTypesByPosCache.invalidate(pos);
-+        if (this.paperConfig().misc.updatePathfindingOnBlockUpdate) { // Paper - option to disable pathfinding updates
-         VoxelShape voxelshape = oldState.getCollisionShape(this, pos);
-         VoxelShape voxelshape1 = newState.getCollisionShape(this, pos);
- 
-@@ -1060,7 +1400,18 @@
-             Iterator iterator = this.navigatingMobs.iterator();
- 
-             while (iterator.hasNext()) {
--                Mob entityinsentient = (Mob) iterator.next();
-+                // CraftBukkit start - fix SPIGOT-6362
-+                Mob entityinsentient;
-+                try {
-+                    entityinsentient = (Mob) iterator.next();
-+                } catch (java.util.ConcurrentModificationException ex) {
-+                    // This can happen because the pathfinder update below may trigger a chunk load, which in turn may cause more navigators to register
-+                    // In this case we just run the update again across all the iterators as the chunk will then be loaded
-+                    // As this is a relative edge case it is much faster than copying navigators (on either read or write)
-+                    this.sendBlockUpdated(pos, oldState, newState, flags);
-+                    return;
-+                }
-+                // CraftBukkit end
-                 PathNavigation navigationabstract = entityinsentient.getNavigation();
- 
-                 if (navigationabstract.shouldRecomputePath(pos)) {
-@@ -1082,15 +1433,18 @@
-             }
- 
-         }
-+        } // Paper - option to disable pathfinding updates
-     }
- 
-     @Override
-     public void updateNeighborsAt(BlockPos pos, Block block) {
-+        if (captureBlockStates) { return; } // Paper - Cancel all physics during placement
-         this.updateNeighborsAt(pos, block, ExperimentalRedstoneUtils.initialOrientation(this, (Direction) null, (Direction) null));
-     }
- 
-     @Override
-     public void updateNeighborsAt(BlockPos pos, Block sourceBlock, @Nullable Orientation orientation) {
-+        if (captureBlockStates) { return; } // Paper - Cancel all physics during placement
-         this.neighborUpdater.updateNeighborsAtExceptFromFacing(pos, sourceBlock, (Direction) null, orientation);
-     }
- 
-@@ -1126,9 +1480,20 @@
- 
-     @Override
-     public void explode(@Nullable Entity entity, @Nullable DamageSource damageSource, @Nullable ExplosionDamageCalculator behavior, double x, double y, double z, float power, boolean createFire, Level.ExplosionInteraction explosionSourceType, ParticleOptions smallParticle, ParticleOptions largeParticle, Holder<SoundEvent> soundEvent) {
-+        // CraftBukkit start
-+        this.explode0(entity, damageSource, behavior, x, y, z, power, createFire, explosionSourceType, smallParticle, largeParticle, soundEvent);
-+    }
-+
-+    public ServerExplosion explode0(@Nullable Entity entity, @Nullable DamageSource damagesource, @Nullable ExplosionDamageCalculator explosiondamagecalculator, double d0, double d1, double d2, float f, boolean flag, Level.ExplosionInteraction world_a, ParticleOptions particleparam, ParticleOptions particleparam1, Holder<SoundEvent> holder) {
-+    // Paper start - Allow explosions to damage source
-+        return this.explode0(entity, damagesource, explosiondamagecalculator, d0, d1, d2, f, flag, world_a, particleparam, particleparam1, holder, null);
-+    }
-+    public ServerExplosion explode0(@Nullable Entity entity, @Nullable DamageSource damagesource, @Nullable ExplosionDamageCalculator explosiondamagecalculator, double d0, double d1, double d2, float f, boolean flag, Level.ExplosionInteraction world_a, ParticleOptions particleparam, ParticleOptions particleparam1, Holder<SoundEvent> holder, java.util.function.Consumer<ServerExplosion> configurator) {
-+    // Paper end - Allow explosions to damage source
-+        // CraftBukkit end
-         Explosion.BlockInteraction explosion_effect;
- 
--        switch (explosionSourceType) {
-+        switch (world_a) {
-             case NONE:
-                 explosion_effect = Explosion.BlockInteraction.KEEP;
-                 break;
-@@ -1144,16 +1509,27 @@
-             case TRIGGER:
-                 explosion_effect = Explosion.BlockInteraction.TRIGGER_BLOCK;
-                 break;
-+            // CraftBukkit start - handle custom explosion type
-+            case STANDARD:
-+                explosion_effect = Explosion.BlockInteraction.DESTROY;
-+                break;
-+            // CraftBukkit end
-             default:
-                 throw new MatchException((String) null, (Throwable) null);
-         }
- 
-         Explosion.BlockInteraction explosion_effect1 = explosion_effect;
--        Vec3 vec3d = new Vec3(x, y, z);
--        ServerExplosion serverexplosion = new ServerExplosion(this, entity, damageSource, behavior, vec3d, power, createFire, explosion_effect1);
-+        Vec3 vec3d = new Vec3(d0, d1, d2);
-+        ServerExplosion serverexplosion = new ServerExplosion(this, entity, damagesource, explosiondamagecalculator, vec3d, f, flag, explosion_effect1);
-+        if (configurator != null) configurator.accept(serverexplosion);// Paper - Allow explosions to damage source
- 
-         serverexplosion.explode();
--        ParticleOptions particleparam2 = serverexplosion.isSmall() ? smallParticle : largeParticle;
-+        // CraftBukkit start
-+        if (serverexplosion.wasCanceled) {
-+            return serverexplosion;
-+        }
-+        // CraftBukkit end
-+        ParticleOptions particleparam2 = serverexplosion.isSmall() ? particleparam : particleparam1;
-         Iterator iterator = this.players.iterator();
- 
-         while (iterator.hasNext()) {
-@@ -1162,10 +1538,11 @@
-             if (entityplayer.distanceToSqr(vec3d) < 4096.0D) {
-                 Optional<Vec3> optional = Optional.ofNullable((Vec3) serverexplosion.getHitPlayers().get(entityplayer));
- 
--                entityplayer.connection.send(new ClientboundExplodePacket(vec3d, optional, particleparam2, soundEvent));
-+                entityplayer.connection.send(new ClientboundExplodePacket(vec3d, optional, particleparam2, holder));
-             }
-         }
- 
-+        return serverexplosion; // CraftBukkit
-     }
- 
-     private Explosion.BlockInteraction getDestroyType(GameRules.Key<GameRules.BooleanValue> decayRule) {
-@@ -1226,17 +1603,29 @@
-     }
- 
-     public <T extends ParticleOptions> int sendParticles(T parameters, double x, double y, double z, int count, double offsetX, double offsetY, double offsetZ, double speed) {
--        return this.sendParticles(parameters, false, false, x, y, z, count, offsetX, offsetY, offsetZ, speed);
-+        return this.sendParticlesSource(null, parameters, false, false, x, y, z, count, offsetX, offsetY, offsetZ, speed); // CraftBukkit - visibility api support
-     }
- 
-     public <T extends ParticleOptions> int sendParticles(T parameters, boolean force, boolean important, double x, double y, double z, int count, double offsetX, double offsetY, double offsetZ, double speed) {
--        ClientboundLevelParticlesPacket packetplayoutworldparticles = new ClientboundLevelParticlesPacket(parameters, force, important, x, y, z, (float) offsetX, (float) offsetY, (float) offsetZ, (float) speed, count);
--        int j = 0;
-+        return this.sendParticlesSource(null, parameters, force, important, x, y, z, count, offsetX, offsetY, offsetZ, speed); // CraftBukkit - visibility api support
-+    }
- 
--        for (int k = 0; k < this.players.size(); ++k) {
--            ServerPlayer entityplayer = (ServerPlayer) this.players.get(k);
-+    // CraftBukkit start - visibility api support
-+    public <T extends ParticleOptions> int sendParticlesSource(ServerPlayer sender, T t0, boolean flag, boolean flag1, double d0, double d1, double d2, int i, double d3, double d4, double d5, double d6) {
-+        // Paper start - Particle API
-+        return this.sendParticlesSource(this.players, sender, t0, flag, flag1, d0, d1, d2, i, d3, d4, d5, d6);
-+    }
-+    public <T extends ParticleOptions> int sendParticlesSource(List<ServerPlayer> receivers, @Nullable ServerPlayer sender, T t0, boolean flag, boolean flag1, double d0, double d1, double d2, int i, double d3, double d4, double d5, double d6) {
-+        // Paper end - Particle API
-+        // CraftBukkit end
-+        ClientboundLevelParticlesPacket packetplayoutworldparticles = new ClientboundLevelParticlesPacket(t0, flag, flag1, d0, d1, d2, (float) d3, (float) d4, (float) d5, (float) d6, i);
-+        int j = 0;
- 
--            if (this.sendParticles(entityplayer, force, x, y, z, packetplayoutworldparticles)) {
-+        for (Player entityhuman : receivers) { // Paper - Particle API
-+            ServerPlayer entityplayer = (ServerPlayer) entityhuman; // Paper - Particle API
-+            if (sender != null && !entityplayer.getBukkitEntity().canSee(sender.getBukkitEntity())) continue; // CraftBukkit
-+
-+            if (this.sendParticles(entityplayer, flag, d0, d1, d2, packetplayoutworldparticles)) {
-                 ++j;
-             }
-         }
-@@ -1292,7 +1681,7 @@
- 
-     @Nullable
-     public BlockPos findNearestMapStructure(TagKey<Structure> structureTag, BlockPos pos, int radius, boolean skipReferencedStructures) {
--        if (!this.server.getWorldData().worldGenOptions().generateStructures()) {
-+        if (!this.serverLevelData.worldGenOptions().generateStructures()) { // CraftBukkit
-             return null;
-         } else {
-             Optional<HolderSet.Named<Structure>> optional = this.registryAccess().lookupOrThrow(Registries.STRUCTURE).get(structureTag);
-@@ -1334,11 +1723,38 @@
-     @Nullable
-     @Override
-     public MapItemSavedData getMapData(MapId id) {
--        return (MapItemSavedData) this.getServer().overworld().getDataStorage().get(MapItemSavedData.factory(), id.key());
-+        // Paper start - Call missing map initialize event and set id
-+        final DimensionDataStorage storage = this.getServer().overworld().getDataStorage();
-+
-+        final Optional<net.minecraft.world.level.saveddata.SavedData> cacheEntry = storage.cache.get(id.key());
-+        if (cacheEntry == null) { // Cache did not contain, try to load and may init
-+            final MapItemSavedData worldmap = storage.get(MapItemSavedData.factory(), id.key()); // get populates the cache
-+            if (worldmap != null) { // map was read, init it and return
-+                worldmap.id = id;
-+                new MapInitializeEvent(worldmap.mapView).callEvent();
-+                return worldmap;
-+            }
-+
-+            return null; // Map does not exist, reading failed.
-+        }
-+
-+        // Cache entry exists, update it with the id ref and return.
-+        if (cacheEntry.orElse(null) instanceof final MapItemSavedData mapItemSavedData) {
-+            mapItemSavedData.id = id;
-+            return mapItemSavedData;
-+        }
-+
-+        return null;
-+        // Paper end - Call missing map initialize event and set id
-     }
- 
-     @Override
-     public void setMapData(MapId id, MapItemSavedData state) {
-+        // CraftBukkit start
-+        state.id = id;
-+        MapInitializeEvent event = new MapInitializeEvent(state.mapView);
-+        Bukkit.getServer().getPluginManager().callEvent(event);
-+        // CraftBukkit end
-         this.getServer().overworld().getDataStorage().set(id.key(), state);
-     }
- 
-@@ -1352,18 +1768,28 @@
-         float f1 = this.levelData.getSpawnAngle();
- 
-         if (!blockposition1.equals(pos) || f1 != angle) {
-+            org.bukkit.Location prevSpawnLoc = this.getWorld().getSpawnLocation(); // Paper - Call SpawnChangeEvent
-             this.levelData.setSpawn(pos, angle);
-+            new org.bukkit.event.world.SpawnChangeEvent(this.getWorld(), prevSpawnLoc).callEvent(); // Paper - Call SpawnChangeEvent
-             this.getServer().getPlayerList().broadcastAll(new ClientboundSetDefaultSpawnPositionPacket(pos, angle));
-         }
- 
-         if (this.lastSpawnChunkRadius > 1) {
--            this.getChunkSource().removeRegionTicket(TicketType.START, new ChunkPos(blockposition1), this.lastSpawnChunkRadius, Unit.INSTANCE);
-+            // Paper start - allow disabling gamerule limits
-+            for (ChunkPos chunkPos : io.papermc.paper.util.MCUtil.getSpiralOutChunks(blockposition1, this.lastSpawnChunkRadius - 2)) {
-+                this.getChunkSource().removeTicketAtLevel(TicketType.START, chunkPos, net.minecraft.server.level.ChunkLevel.ENTITY_TICKING_LEVEL, Unit.INSTANCE);
-+            }
-+            // Paper end - allow disabling gamerule limits
-         }
- 
-         int i = this.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS) + 1;
- 
-         if (i > 1) {
--            this.getChunkSource().addRegionTicket(TicketType.START, new ChunkPos(pos), i, Unit.INSTANCE);
-+            // Paper start - allow disabling gamerule limits
-+            for (ChunkPos chunkPos : io.papermc.paper.util.MCUtil.getSpiralOutChunks(pos, i - 2)) {
-+                this.getChunkSource().addTicketAtLevel(TicketType.START, chunkPos, net.minecraft.server.level.ChunkLevel.ENTITY_TICKING_LEVEL, Unit.INSTANCE);
-+            }
-+            // Paper end - allow disabling gamerule limits
-         }
- 
-         this.lastSpawnChunkRadius = i;
-@@ -1419,6 +1845,11 @@
-             });
-             optional1.ifPresent((holder) -> {
-                 this.getServer().execute(() -> {
-+                    // Paper start - Remove stale POIs
-+                    if (optional.isEmpty() && this.getPoiManager().exists(blockposition1, poiType -> true)) {
-+                        this.getPoiManager().remove(blockposition1);
-+                    }
-+                    // Paper end - Remove stale POIs
-                     this.getPoiManager().add(blockposition1, holder);
-                     DebugPackets.sendPoiAddedPacket(this, blockposition1);
-                 });
-@@ -1649,6 +2080,11 @@
-     @Override
-     public void blockUpdated(BlockPos pos, Block block) {
-         if (!this.isDebug()) {
-+            // CraftBukkit start
-+            if (this.populating) {
-+                return;
-+            }
-+            // CraftBukkit end
-             this.updateNeighborsAt(pos, block);
-         }
- 
-@@ -1668,12 +2104,12 @@
-     }
- 
-     public boolean isFlat() {
--        return this.server.getWorldData().isFlatWorld();
-+        return this.serverLevelData.isFlatWorld(); // CraftBukkit
-     }
- 
-     @Override
-     public long getSeed() {
--        return this.server.getWorldData().worldGenOptions().seed();
-+        return this.serverLevelData.worldGenOptions().seed(); // CraftBukkit
-     }
- 
-     @Nullable
-@@ -1696,7 +2132,7 @@
-     private static <T> String getTypeCount(Iterable<T> items, Function<T, String> classifier) {
-         try {
-             Object2IntOpenHashMap<String> object2intopenhashmap = new Object2IntOpenHashMap();
--            Iterator iterator = items.iterator();
-+            Iterator<T> iterator = items.iterator(); // CraftBukkit - decompile error
- 
-             while (iterator.hasNext()) {
-                 T t0 = iterator.next();
-@@ -1705,7 +2141,7 @@
-                 object2intopenhashmap.addTo(s, 1);
-             }
- 
--            return (String) object2intopenhashmap.object2IntEntrySet().stream().sorted(Comparator.comparing(Entry::getIntValue).reversed()).limit(5L).map((entry) -> {
-+            return (String) object2intopenhashmap.object2IntEntrySet().stream().sorted(Comparator.comparing(Entry<String>::getIntValue).reversed()).limit(5L).map((entry) -> { // CraftBukkit - decompile error
-                 String s1 = (String) entry.getKey();
- 
-                 return s1 + ":" + entry.getIntValue();
-@@ -1717,6 +2153,7 @@
- 
-     @Override
-     public LevelEntityGetter<Entity> getEntities() {
-+        org.spigotmc.AsyncCatcher.catchOp("Chunk getEntities call"); // Spigot
-         return this.entityManager.getEntityGetter();
-     }
- 
-@@ -1800,7 +2237,28 @@
- 
-     public GameRules getGameRules() {
-         return this.serverLevelData.getGameRules();
-+    }
-+
-+    // Paper start - respect global sound events gamerule
-+    public List<net.minecraft.server.level.ServerPlayer> getPlayersForGlobalSoundGamerule() {
-+        return this.getGameRules().getBoolean(GameRules.RULE_GLOBAL_SOUND_EVENTS) ? ((ServerLevel) this).getServer().getPlayerList().players : ((ServerLevel) this).players();
-+    }
-+
-+    public double getGlobalSoundRangeSquared(java.util.function.Function<org.spigotmc.SpigotWorldConfig, Integer> rangeFunction) {
-+        final double range = rangeFunction.apply(this.spigotConfig);
-+        return range <= 0 ? 64.0 * 64.0 : range * range; // 64 is taken from default in ServerLevel#levelEvent
-+    }
-+    // Paper end - respect global sound events gamerule
-+    // Paper start - notify observers even if grow failed
-+    public void checkCapturedTreeStateForObserverNotify(final BlockPos pos, final org.bukkit.craftbukkit.block.CraftBlockState craftBlockState) {
-+        // notify observers if the block state is the same and the Y level equals the original y level (for mega trees)
-+        // blocks at the same Y level with the same state can be assumed to be saplings which trigger observers regardless of if the
-+        // tree grew or not
-+        if (craftBlockState.getPosition().getY() == pos.getY() && this.getBlockState(craftBlockState.getPosition()) == craftBlockState.getHandle()) {
-+            this.notifyAndUpdatePhysics(craftBlockState.getPosition(), null, craftBlockState.getHandle(), craftBlockState.getHandle(), craftBlockState.getHandle(), craftBlockState.getFlag(), 512);
-+        }
-     }
-+    // Paper end - notify observers even if grow failed
- 
-     @Override
-     public CrashReportCategory fillReportDetails(CrashReport report) {
-@@ -1828,22 +2286,30 @@
-         }
- 
-         public void onTickingStart(Entity entity) {
-+            if (entity instanceof net.minecraft.world.entity.Marker && !paperConfig().entities.markers.tick) return; // Paper - Configurable marker ticking
-             ServerLevel.this.entityTickList.add(entity);
-         }
- 
-         public void onTickingEnd(Entity entity) {
-             ServerLevel.this.entityTickList.remove(entity);
-+            // Paper start - Reset pearls when they stop being ticked
-+            if (ServerLevel.this.paperConfig().fixes.disableUnloadedChunkEnderpearlExploit && ServerLevel.this.paperConfig().misc.legacyEnderPearlBehavior && entity instanceof net.minecraft.world.entity.projectile.ThrownEnderpearl pearl) {
-+                pearl.cachedOwner = null;
-+                pearl.ownerUUID = null;
-+            }
-+            // Paper end - Reset pearls when they stop being ticked
-         }
- 
-         public void onTrackingStart(Entity entity) {
--            ServerLevel.this.getChunkSource().addEntity(entity);
-+            org.spigotmc.AsyncCatcher.catchOp("entity register"); // Spigot
-+            // ServerLevel.this.getChunkSource().addEntity(entity); // Paper - ignore and warn about illegal addEntity calls instead of crashing server; moved down below valid=true
-             if (entity instanceof ServerPlayer entityplayer) {
-                 ServerLevel.this.players.add(entityplayer);
-                 ServerLevel.this.updateSleepingPlayerList();
-             }
- 
-             if (entity instanceof Mob entityinsentient) {
--                if (ServerLevel.this.isUpdatingNavigations) {
-+                if (false && ServerLevel.this.isUpdatingNavigations) { // Paper - Remove unnecessary onTrackingStart during navigation warning
-                     String s = "onTrackingStart called during navigation iteration";
- 
-                     Util.logAndPauseIfInIde("onTrackingStart called during navigation iteration", new IllegalStateException("onTrackingStart called during navigation iteration"));
-@@ -1864,9 +2330,58 @@
-             }
- 
-             entity.updateDynamicGameEventListener(DynamicGameEventListener::add);
-+            entity.inWorld = true; // CraftBukkit - Mark entity as in world
-+            entity.valid = true; // CraftBukkit
-+            ServerLevel.this.getChunkSource().addEntity(entity); // Paper - ignore and warn about illegal addEntity calls instead of crashing server
-+            // Paper start - Entity origin API
-+            if (entity.getOriginVector() == null) {
-+                entity.setOrigin(entity.getBukkitEntity().getLocation());
-+            }
-+            // Default to current world if unknown, gross assumption but entities rarely change world
-+            if (entity.getOriginWorld() == null) {
-+                entity.setOrigin(entity.getOriginVector().toLocation(getWorld()));
-+            }
-+            // Paper end - Entity origin API
-+            new com.destroystokyo.paper.event.entity.EntityAddToWorldEvent(entity.getBukkitEntity(), ServerLevel.this.getWorld()).callEvent(); // Paper - fire while valid
-         }
- 
-         public void onTrackingEnd(Entity entity) {
-+            org.spigotmc.AsyncCatcher.catchOp("entity unregister"); // Spigot
-+            // Spigot start
-+            if ( entity instanceof Player )
-+            {
-+                com.google.common.collect.Streams.stream( ServerLevel.this.getServer().getAllLevels() ).map( ServerLevel::getDataStorage ).forEach( (worldData) ->
-+                {
-+                    for (Object o : worldData.cache.values() )
-+                    {
-+                        if ( o instanceof MapItemSavedData )
-+                        {
-+                            MapItemSavedData map = (MapItemSavedData) o;
-+                            map.carriedByPlayers.remove( (Player) entity );
-+                            for ( Iterator<MapItemSavedData.HoldingPlayer> iter = (Iterator<MapItemSavedData.HoldingPlayer>) map.carriedBy.iterator(); iter.hasNext(); )
-+                            {
-+                                if ( iter.next().player == entity )
-+                                {
-+                                    iter.remove();
-+                                }
-+                            }
-+                        }
-+                    }
-+                } );
-+            }
-+            // Spigot end
-+            // Spigot Start
-+            if (entity.getBukkitEntity() instanceof org.bukkit.inventory.InventoryHolder && (!(entity instanceof ServerPlayer) || entity.getRemovalReason() != Entity.RemovalReason.KILLED)) { // SPIGOT-6876: closeInventory clears death message
-+                // Paper start - Fix merchant inventory not closing on entity removal
-+                if (entity.getBukkitEntity() instanceof org.bukkit.inventory.Merchant merchant && merchant.getTrader() != null) {
-+                    merchant.getTrader().closeInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason.UNLOADED);
-+                }
-+                // Paper end - Fix merchant inventory not closing on entity removal
-+                for (org.bukkit.entity.HumanEntity h : Lists.newArrayList(((org.bukkit.inventory.InventoryHolder) entity.getBukkitEntity()).getInventory().getViewers())) {
-+                    h.closeInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason.UNLOADED); // Paper - Inventory close reason
-+                }
-+            }
-+            // Spigot End
-             ServerLevel.this.getChunkSource().removeEntity(entity);
-             if (entity instanceof ServerPlayer entityplayer) {
-                 ServerLevel.this.players.remove(entityplayer);
-@@ -1874,7 +2389,7 @@
-             }
- 
-             if (entity instanceof Mob entityinsentient) {
--                if (ServerLevel.this.isUpdatingNavigations) {
-+                if (false && ServerLevel.this.isUpdatingNavigations) { // Paper - Remove unnecessary onTrackingStart during navigation warning
-                     String s = "onTrackingStart called during navigation iteration";
- 
-                     Util.logAndPauseIfInIde("onTrackingStart called during navigation iteration", new IllegalStateException("onTrackingStart called during navigation iteration"));
-@@ -1895,10 +2410,27 @@
-             }
- 
-             entity.updateDynamicGameEventListener(DynamicGameEventListener::remove);
-+            // CraftBukkit start
-+            entity.valid = false;
-+            if (!(entity instanceof ServerPlayer)) {
-+                for (ServerPlayer player : ServerLevel.this.server.getPlayerList().players) { // Paper - call onEntityRemove for all online players
-+                    player.getBukkitEntity().onEntityRemove(entity);
-+                }
-+            }
-+            // CraftBukkit end
-+            new com.destroystokyo.paper.event.entity.EntityRemoveFromWorldEvent(entity.getBukkitEntity(), ServerLevel.this.getWorld()).callEvent(); // Paper - fire while valid
-         }
- 
-         public void onSectionChange(Entity entity) {
-             entity.updateDynamicGameEventListener(DynamicGameEventListener::move);
-         }
-     }
-+
-+    // Paper start - check global player list where appropriate
-+    @Override
-+    @Nullable
-+    public Player getGlobalPlayerByUUID(UUID uuid) {
-+        return this.server.getPlayerList().getPlayer(uuid);
-+    }
-+    // Paper end - check global player list where appropriate
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/level/ServerPlayer.java.patch b/paper-server/patches/unapplied/net/minecraft/server/level/ServerPlayer.java.patch
deleted file mode 100644
index 9daf2af366..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/level/ServerPlayer.java.patch
+++ /dev/null
@@ -1,1891 +0,0 @@
---- a/net/minecraft/server/level/ServerPlayer.java
-+++ b/net/minecraft/server/level/ServerPlayer.java
-@@ -103,10 +103,6 @@
- import net.minecraft.util.Unit;
- import net.minecraft.util.profiling.Profiler;
- import net.minecraft.util.profiling.ProfilerFiller;
--import net.minecraft.world.Container;
--import net.minecraft.world.Difficulty;
--import net.minecraft.world.InteractionHand;
--import net.minecraft.world.MenuProvider;
- import net.minecraft.world.damagesource.DamageSource;
- import net.minecraft.world.damagesource.DamageTypes;
- import net.minecraft.world.effect.MobEffectInstance;
-@@ -135,15 +131,16 @@
- import net.minecraft.world.entity.player.ChatVisiblity;
- import net.minecraft.world.entity.player.Input;
- import net.minecraft.world.entity.player.Inventory;
--import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.entity.projectile.AbstractArrow;
- import net.minecraft.world.entity.projectile.ThrownEnderpearl;
- import net.minecraft.world.entity.vehicle.AbstractBoat;
- import net.minecraft.world.entity.vehicle.AbstractMinecart;
-+import net.minecraft.world.food.FoodData;
- import net.minecraft.world.inventory.AbstractContainerMenu;
- import net.minecraft.world.inventory.ContainerListener;
- import net.minecraft.world.inventory.ContainerSynchronizer;
- import net.minecraft.world.inventory.HorseInventoryMenu;
-+import net.minecraft.world.inventory.InventoryMenu;
- import net.minecraft.world.inventory.ResultSlot;
- import net.minecraft.world.inventory.Slot;
- import net.minecraft.world.item.Item;
-@@ -154,8 +151,6 @@
- import net.minecraft.world.item.WrittenBookItem;
- import net.minecraft.world.item.crafting.Recipe;
- import net.minecraft.world.item.crafting.RecipeHolder;
--import net.minecraft.world.item.enchantment.EnchantmentHelper;
--import net.minecraft.world.item.trading.MerchantOffers;
- import net.minecraft.world.level.ChunkPos;
- import net.minecraft.world.level.GameRules;
- import net.minecraft.world.level.GameType;
-@@ -163,12 +158,14 @@
- import net.minecraft.world.level.biome.BiomeManager;
- import net.minecraft.world.level.block.BedBlock;
- import net.minecraft.world.level.block.Block;
-+import net.minecraft.world.level.block.ChestBlock;
- import net.minecraft.world.level.block.HorizontalDirectionalBlock;
- import net.minecraft.world.level.block.RespawnAnchorBlock;
- import net.minecraft.world.level.block.entity.BlockEntity;
- import net.minecraft.world.level.block.entity.CommandBlockEntity;
- import net.minecraft.world.level.block.entity.SignBlockEntity;
- import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.world.level.dimension.LevelStem;
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.level.portal.TeleportTransition;
- import net.minecraft.world.level.saveddata.maps.MapId;
-@@ -179,11 +176,48 @@
- import net.minecraft.world.scores.PlayerTeam;
- import net.minecraft.world.scores.ScoreAccess;
- import net.minecraft.world.scores.ScoreHolder;
-+import org.slf4j.Logger;
-+import net.minecraft.world.Container;
-+import net.minecraft.world.Difficulty;
-+import net.minecraft.world.InteractionHand;
-+import net.minecraft.world.MenuProvider;
-+// CraftBukkit start
-+import net.minecraft.world.damagesource.CombatTracker;
-+import net.minecraft.world.item.enchantment.EnchantmentEffectComponents;
-+import net.minecraft.world.item.enchantment.EnchantmentHelper;
-+import net.minecraft.world.item.trading.MerchantOffers;
-+import net.minecraft.world.scores.Scoreboard;
- import net.minecraft.world.scores.Team;
- import net.minecraft.world.scores.criteria.ObjectiveCriteria;
--import org.slf4j.Logger;
-+import io.papermc.paper.adventure.PaperAdventure; // Paper
-+import org.bukkit.Bukkit;
-+import org.bukkit.Location;
-+import org.bukkit.WeatherType;
-+import org.bukkit.command.CommandSender;
-+import org.bukkit.craftbukkit.CraftWorld;
-+import org.bukkit.craftbukkit.CraftWorldBorder;
-+import org.bukkit.craftbukkit.entity.CraftPlayer;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.craftbukkit.event.CraftPortalEvent;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.craftbukkit.util.CraftDimensionUtil;
-+import org.bukkit.craftbukkit.util.CraftLocation;
-+import org.bukkit.entity.Player;
-+import org.bukkit.event.entity.EntityExhaustionEvent;
-+import org.bukkit.event.player.PlayerBedLeaveEvent;
-+import org.bukkit.event.player.PlayerChangedMainHandEvent;
-+import org.bukkit.event.player.PlayerChangedWorldEvent;
-+import org.bukkit.event.player.PlayerDropItemEvent;
-+import org.bukkit.event.player.PlayerLocaleChangeEvent;
-+import org.bukkit.event.player.PlayerPortalEvent;
-+import org.bukkit.event.player.PlayerRespawnEvent;
-+import org.bukkit.event.player.PlayerSpawnChangeEvent;
-+import org.bukkit.event.player.PlayerTeleportEvent;
-+import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
-+import org.bukkit.inventory.MainHand;
-+// CraftBukkit end
- 
--public class ServerPlayer extends Player {
-+public class ServerPlayer extends net.minecraft.world.entity.player.Player {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
-     private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_XZ = 32;
-@@ -225,7 +259,8 @@
-     private int levitationStartTime;
-     private boolean disconnected;
-     private int requestedViewDistance;
--    public String language;
-+    public String language = null; // CraftBukkit - default  // Paper - default to null
-+    public java.util.Locale adventure$locale = java.util.Locale.US; // Paper
-     @Nullable
-     private Vec3 startingToFallPosition;
-     @Nullable
-@@ -258,7 +293,35 @@
-     private final CommandSource commandSource;
-     private int containerCounter;
-     public boolean wonGame;
-+    private int containerUpdateDelay; // Paper - Configurable container update tick rate
-+    public long loginTime; // Paper - Replace OfflinePlayer#getLastPlayed
-+    public int patrolSpawnDelay; // Paper - Pillager patrol spawn settings and per player options
-+    // Paper start - cancellable death event
-+    public boolean queueHealthUpdatePacket;
-+    public net.minecraft.network.protocol.game.ClientboundSetHealthPacket queuedHealthUpdatePacket;
-+    // Paper end - cancellable death event
- 
-+    // CraftBukkit start
-+    public CraftPlayer.TransferCookieConnection transferCookieConnection;
-+    public String displayName;
-+    public net.kyori.adventure.text.Component adventure$displayName; // Paper
-+    public Component listName;
-+    public int listOrder = 0;
-+    public org.bukkit.Location compassTarget;
-+    public int newExp = 0;
-+    public int newLevel = 0;
-+    public int newTotalExp = 0;
-+    public boolean keepLevel = false;
-+    public double maxHealthCache;
-+    public boolean joining = true;
-+    public boolean sentListPacket = false;
-+    public boolean supressTrackerForLogin = false; // Paper - Fire PlayerJoinEvent when Player is actually ready
-+    // CraftBukkit end
-+    public boolean isRealPlayer; // Paper
-+    public com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent playerNaturallySpawnedEvent; // Paper - PlayerNaturallySpawnCreaturesEvent
-+    public @Nullable String clientBrandName = null; // Paper - Brand support
-+    public org.bukkit.event.player.PlayerQuitEvent.QuitReason quitReason = null; // Paper - Add API for quit reason; there are a lot of changes to do if we change all methods leading to the event
-+
-     public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile, ClientInformation clientOptions) {
-         super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile);
-         this.chatVisibility = ChatVisiblity.FULL;
-@@ -266,7 +329,7 @@
-         this.canChatColor = true;
-         this.lastActionTime = Util.getMillis();
-         this.requestedViewDistance = 2;
--        this.language = "en_us";
-+        this.language =  null; // Paper - default to null
-         this.lastSectionPos = SectionPos.of(0, 0, 0);
-         this.chunkTrackingView = ChunkTrackingView.EMPTY;
-         this.respawnDimension = Level.OVERWORLD;
-@@ -285,7 +348,14 @@
- 
-             }
- 
-+            // Paper start - Sync offhand slot in menus
-             @Override
-+            public void sendOffHandSlotChange() {
-+                ServerPlayer.this.connection.send(new ClientboundContainerSetSlotPacket(ServerPlayer.this.inventoryMenu.containerId, ServerPlayer.this.inventoryMenu.incrementStateId(), net.minecraft.world.inventory.InventoryMenu.SHIELD_SLOT, ServerPlayer.this.inventoryMenu.getSlot(net.minecraft.world.inventory.InventoryMenu.SHIELD_SLOT).getItem().copy()));
-+            }
-+            // Paper end - Sync offhand slot in menus
-+
-+            @Override
-             public void sendSlotChange(AbstractContainerMenu handler, int slot, ItemStack stack) {
-                 ServerPlayer.this.connection.send(new ClientboundContainerSetSlotPacket(handler.containerId, handler.incrementStateId(), slot, stack));
-             }
-@@ -316,6 +386,25 @@
- 
-                 }
-             }
-+            // Paper start - Add PlayerInventorySlotChangeEvent
-+            @Override
-+            public void slotChanged(AbstractContainerMenu handler, int slotId, ItemStack oldStack, ItemStack stack) {
-+                Slot slot = handler.getSlot(slotId);
-+                if (!(slot instanceof ResultSlot)) {
-+                    if (slot.container == ServerPlayer.this.getInventory()) {
-+                        if (io.papermc.paper.event.player.PlayerInventorySlotChangeEvent.getHandlerList().getRegisteredListeners().length == 0) {
-+                            CriteriaTriggers.INVENTORY_CHANGED.trigger(ServerPlayer.this, ServerPlayer.this.getInventory(), stack);
-+                            return;
-+                        }
-+                        io.papermc.paper.event.player.PlayerInventorySlotChangeEvent event = new io.papermc.paper.event.player.PlayerInventorySlotChangeEvent(ServerPlayer.this.getBukkitEntity(), slotId, CraftItemStack.asBukkitCopy(oldStack), CraftItemStack.asBukkitCopy(stack));
-+                        event.callEvent();
-+                        if (event.shouldTriggerAdvancements()) {
-+                            CriteriaTriggers.INVENTORY_CHANGED.trigger(ServerPlayer.this, ServerPlayer.this.getInventory(), stack);
-+                        }
-+                    }
-+                }
-+            }
-+            // Paper end - Add PlayerInventorySlotChangeEvent
- 
-             @Override
-             public void dataChanged(AbstractContainerMenu handler, int property, int value) {}
-@@ -340,6 +429,13 @@
-             public void sendSystemMessage(Component message) {
-                 ServerPlayer.this.sendSystemMessage(message);
-             }
-+
-+            // CraftBukkit start
-+            @Override
-+            public CommandSender getBukkitSender(CommandSourceStack wrapper) {
-+                return ServerPlayer.this.getBukkitEntity();
-+            }
-+            // CraftBukkit end
-         };
-         this.textFilter = server.createTextFilterForPlayer(this);
-         this.gameMode = server.createGameModeForPlayer(this);
-@@ -349,17 +445,72 @@
-         this.server = server;
-         this.stats = server.getPlayerList().getPlayerStats(this);
-         this.advancements = server.getPlayerList().getPlayerAdvancements(this);
--        this.moveTo(this.adjustSpawnLocation(world, world.getSharedSpawnPos()).getBottomCenter(), 0.0F, 0.0F);
--        this.updateOptions(clientOptions);
-+        // this.moveTo(this.adjustSpawnLocation(world, world.getSharedSpawnPos()).getBottomCenter(), 0.0F, 0.0F); // Paper - Don't move existing players to world spawn
-+        this.updateOptionsNoEvents(clientOptions); // Paper - don't call options events on login
-         this.object = null;
-+
-+        // CraftBukkit start
-+        this.displayName = this.getScoreboardName();
-+        this.adventure$displayName = net.kyori.adventure.text.Component.text(this.getScoreboardName()); // Paper
-+        this.bukkitPickUpLoot = true;
-+        this.maxHealthCache = this.getMaxHealth();
-+    }
-+
-+    // Use method to resend items in hands in case of client desync, because the item use got cancelled.
-+    // For example, when cancelling the leash event
-+    @Deprecated // Paper - this shouldn't be used, use the regular sendAllDataToRemote call to resync all
-+    public void resendItemInHands() {
-+        this.containerMenu.findSlot(this.getInventory(), this.getInventory().selected).ifPresent(s -> {
-+            this.containerSynchronizer.sendSlotChange(this.containerMenu, s, this.getMainHandItem());
-+        });
-+        this.containerSynchronizer.sendSlotChange(this.inventoryMenu, InventoryMenu.SHIELD_SLOT, this.getOffhandItem());
-+    }
-+
-+    // Yes, this doesn't match Vanilla, but it's the best we can do for now.
-+    // If this is an issue, PRs are welcome
-+    public final BlockPos getSpawnPoint(ServerLevel worldserver) {
-+        BlockPos blockposition = worldserver.getSharedSpawnPos();
-+
-+        if (worldserver.dimensionType().hasSkyLight() && worldserver.serverLevelData.getGameType() != GameType.ADVENTURE) {
-+            int i = Math.max(0, this.server.getSpawnRadius(worldserver));
-+            int j = Mth.floor(worldserver.getWorldBorder().getDistanceToBorder((double) blockposition.getX(), (double) blockposition.getZ()));
-+
-+            if (j < i) {
-+                i = j;
-+            }
-+
-+            if (j <= 1) {
-+                i = 1;
-+            }
-+
-+            long k = (long) (i * 2 + 1);
-+            long l = k * k;
-+            int i1 = l > 2147483647L ? Integer.MAX_VALUE : (int) l;
-+            int j1 = this.getCoprime(i1);
-+            int k1 = RandomSource.create().nextInt(i1);
-+
-+            for (int l1 = 0; l1 < i1; ++l1) {
-+                int i2 = (k1 + j1 * l1) % i1;
-+                int j2 = i2 % (i * 2 + 1);
-+                int k2 = i2 / (i * 2 + 1);
-+                BlockPos blockposition1 = PlayerRespawnLogic.getOverworldRespawnPos(worldserver, blockposition.getX() + j2 - i, blockposition.getZ() + k2 - i);
-+
-+                if (blockposition1 != null) {
-+                    return blockposition1;
-+                }
-+            }
-+        }
-+
-+        return blockposition;
-     }
-+    // CraftBukkit end
- 
-     @Override
-     public BlockPos adjustSpawnLocation(ServerLevel world, BlockPos basePos) {
-         AABB axisalignedbb = this.getDimensions(Pose.STANDING).makeBoundingBox(Vec3.ZERO);
-         BlockPos blockposition1 = basePos;
- 
--        if (world.dimensionType().hasSkyLight() && world.getServer().getWorldData().getGameType() != GameType.ADVENTURE) {
-+        if (world.dimensionType().hasSkyLight() && world.serverLevelData.getGameType() != GameType.ADVENTURE) { // CraftBukkit
-             int i = Math.max(0, this.server.getSpawnRadius(world));
-             int j = Mth.floor(world.getWorldBorder().getDistanceToBorder((double) basePos.getX(), (double) basePos.getZ()));
- 
-@@ -395,14 +546,20 @@
- 
-                     Objects.requireNonNull(basePos);
-                     crashreportsystemdetails.setDetail("Origin", basePos::toString);
-+                    // CraftBukkit start - decompile error
-+                    int finalI = i;
-                     crashreportsystemdetails.setDetail("Radius", () -> {
--                        return Integer.toString(i);
-+                        return Integer.toString(finalI);
-+                        // CraftBukkit end
-                     });
-                     crashreportsystemdetails.setDetail("Candidate", () -> {
-                         return "[" + l2 + "," + i3 + "]";
-                     });
-+                    // CraftBukkit start - decompile error
-+                    int finalL1 = l1;
-                     crashreportsystemdetails.setDetail("Progress", () -> {
--                        return "" + l1 + " out of " + i1;
-+                        return "" + finalL1 + " out of " + i1;
-+                        // CraftBukkit end
-                     });
-                     throw new ReportedException(crashreport);
-                 }
-@@ -440,7 +597,7 @@
-             dataresult = WardenSpawnTracker.CODEC.parse(new Dynamic(NbtOps.INSTANCE, nbt.get("warden_spawn_tracker")));
-             logger = ServerPlayer.LOGGER;
-             Objects.requireNonNull(logger);
--            dataresult.resultOrPartial(logger::error).ifPresent((wardenspawntracker) -> {
-+            ((DataResult<WardenSpawnTracker>) dataresult).resultOrPartial(logger::error).ifPresent((wardenspawntracker) -> {
-                 this.wardenSpawnTracker = wardenspawntracker;
-             });
-         }
-@@ -457,17 +614,26 @@
-                 return this.server.getRecipeManager().byKey(resourcekey).isPresent();
-             });
-         }
-+        this.getBukkitEntity().readExtraData(nbt); // CraftBukkit
- 
-         if (this.isSleeping()) {
-             this.stopSleeping();
-         }
- 
-+        // CraftBukkit start
-+        String spawnWorld = nbt.getString("SpawnWorld");
-+        CraftWorld oldWorld = (CraftWorld) Bukkit.getWorld(spawnWorld);
-+        if (oldWorld != null) {
-+            this.respawnDimension = oldWorld.getHandle().dimension();
-+        }
-+        // CraftBukkit end
-+
-         if (nbt.contains("SpawnX", 99) && nbt.contains("SpawnY", 99) && nbt.contains("SpawnZ", 99)) {
-             this.respawnPosition = new BlockPos(nbt.getInt("SpawnX"), nbt.getInt("SpawnY"), nbt.getInt("SpawnZ"));
-             this.respawnForced = nbt.getBoolean("SpawnForced");
-             this.respawnAngle = nbt.getFloat("SpawnAngle");
-             if (nbt.contains("SpawnDimension")) {
--                DataResult dataresult1 = Level.RESOURCE_KEY_CODEC.parse(NbtOps.INSTANCE, nbt.get("SpawnDimension"));
-+                DataResult<ResourceKey<Level>> dataresult1 = Level.RESOURCE_KEY_CODEC.parse(NbtOps.INSTANCE, nbt.get("SpawnDimension")); // CraftBukkit - decompile error
-                 Logger logger1 = ServerPlayer.LOGGER;
- 
-                 Objects.requireNonNull(logger1);
-@@ -482,7 +648,7 @@
-             dataresult = BlockPos.CODEC.parse(NbtOps.INSTANCE, nbtbase);
-             logger = ServerPlayer.LOGGER;
-             Objects.requireNonNull(logger);
--            dataresult.resultOrPartial(logger::error).ifPresent((blockposition) -> {
-+            ((DataResult<BlockPos>) dataresult).resultOrPartial(logger::error).ifPresent((blockposition) -> { // CraftBukkit - decompile error
-                 this.raidOmenPosition = blockposition;
-             });
-         }
-@@ -492,7 +658,7 @@
-     @Override
-     public void addAdditionalSaveData(CompoundTag nbt) {
-         super.addAdditionalSaveData(nbt);
--        DataResult dataresult = WardenSpawnTracker.CODEC.encodeStart(NbtOps.INSTANCE, this.wardenSpawnTracker);
-+        DataResult<Tag> dataresult = WardenSpawnTracker.CODEC.encodeStart(NbtOps.INSTANCE, this.wardenSpawnTracker); // CraftBukkit - decompile error
-         Logger logger = ServerPlayer.LOGGER;
- 
-         Objects.requireNonNull(logger);
-@@ -526,6 +692,7 @@
-                 nbt.put("SpawnDimension", nbtbase);
-             });
-         }
-+        this.getBukkitEntity().setExtraData(nbt); // CraftBukkit
- 
-         nbt.putBoolean("spawn_extra_particles_on_fall", this.spawnExtraParticlesOnFall);
-         if (this.raidOmenPosition != null) {
-@@ -544,7 +711,20 @@
-         Entity entity = this.getRootVehicle();
-         Entity entity1 = this.getVehicle();
- 
--        if (entity1 != null && entity != this && entity.hasExactlyOnePlayerPassenger()) {
-+        // CraftBukkit start - handle non-persistent vehicles
-+        boolean persistVehicle = true;
-+        if (entity1 != null) {
-+            Entity vehicle;
-+            for (vehicle = entity1; vehicle != null; vehicle = vehicle.getVehicle()) {
-+                if (!vehicle.persist) {
-+                    persistVehicle = false;
-+                    break;
-+                }
-+            }
-+        }
-+
-+        if (persistVehicle && entity1 != null && entity != this && entity.hasExactlyOnePlayerPassenger() && !entity.isRemoved()) { // Paper - Ensure valid vehicle status
-+            // CraftBukkit end
-             CompoundTag nbttagcompound1 = new CompoundTag();
-             CompoundTag nbttagcompound2 = new CompoundTag();
- 
-@@ -564,7 +744,7 @@
-                 ServerLevel worldserver = (ServerLevel) world;
-                 CompoundTag nbttagcompound = ((CompoundTag) nbt.get()).getCompound("RootVehicle");
-                 Entity entity = EntityType.loadEntityRecursive(nbttagcompound.getCompound("Entity"), worldserver, EntitySpawnReason.LOAD, (entity1) -> {
--                    return !worldserver.addWithUUID(entity1) ? null : entity1;
-+                    return !worldserver.addWithUUID(entity1, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.MOUNT) ? null : entity1; // CraftBukkit - decompile error // Paper - Entity#getEntitySpawnReason
-                 });
- 
-                 if (entity == null) {
-@@ -598,12 +778,12 @@
- 
-                 if (!this.isPassenger()) {
-                     ServerPlayer.LOGGER.warn("Couldn't reattach entity to player");
--                    entity.discard();
-+                    entity.discard(null); // CraftBukkit - add Bukkit remove cause
-                     iterator = entity.getIndirectPassengers().iterator();
- 
-                     while (iterator.hasNext()) {
-                         entity1 = (Entity) iterator.next();
--                        entity1.discard();
-+                        entity1.discard(null); // CraftBukkit - add Bukkit remove cause
-                     }
-                 }
-             }
-@@ -618,6 +798,7 @@
- 
-             while (iterator.hasNext()) {
-                 ThrownEnderpearl entityenderpearl = (ThrownEnderpearl) iterator.next();
-+                if (entityenderpearl.level().paperConfig().misc.legacyEnderPearlBehavior) continue; // Paper - Allow using old ender pearl behavior
- 
-                 if (entityenderpearl.isRemoved()) {
-                     ServerPlayer.LOGGER.warn("Trying to save removed ender pearl, skipping");
-@@ -625,7 +806,7 @@
-                     CompoundTag nbttagcompound1 = new CompoundTag();
- 
-                     entityenderpearl.save(nbttagcompound1);
--                    DataResult dataresult = ResourceLocation.CODEC.encodeStart(NbtOps.INSTANCE, entityenderpearl.level().dimension().location());
-+                    DataResult<Tag> dataresult = ResourceLocation.CODEC.encodeStart(NbtOps.INSTANCE, entityenderpearl.level().dimension().location()); // CraftBukkit - decompile error
-                     Logger logger = ServerPlayer.LOGGER;
- 
-                     Objects.requireNonNull(logger);
-@@ -651,7 +832,7 @@
-                 nbttaglist.forEach((nbtbase1) -> {
-                     if (nbtbase1 instanceof CompoundTag nbttagcompound) {
-                         if (nbttagcompound.contains("ender_pearl_dimension")) {
--                            DataResult dataresult = Level.RESOURCE_KEY_CODEC.parse(NbtOps.INSTANCE, nbttagcompound.get("ender_pearl_dimension"));
-+                            DataResult<ResourceKey<Level>> dataresult = Level.RESOURCE_KEY_CODEC.parse(NbtOps.INSTANCE, nbttagcompound.get("ender_pearl_dimension")); // CraftBukkit - decompile error
-                             Logger logger = ServerPlayer.LOGGER;
- 
-                             Objects.requireNonNull(logger);
-@@ -684,7 +865,30 @@
-             }
-         }
- 
-+    }
-+
-+    // CraftBukkit start - World fallback code, either respawn location or global spawn
-+    public void spawnIn(Level world) {
-+        this.setLevel(world);
-+        if (world == null) {
-+            this.unsetRemoved();
-+            Vec3 position = null;
-+            if (this.respawnDimension != null) {
-+                world = this.server.getLevel(this.respawnDimension);
-+                if (world != null && this.getRespawnPosition() != null) {
-+                    position = ServerPlayer.findRespawnAndUseSpawnBlock((ServerLevel) world, this.getRespawnPosition(), this.getRespawnAngle(), false, false).map(ServerPlayer.RespawnPosAngle::position).orElse(null);
-+                }
-+            }
-+            if (world == null || position == null) {
-+                world = ((CraftWorld) Bukkit.getServer().getWorlds().get(0)).getHandle();
-+                position = Vec3.atCenterOf(world.getSharedSpawnPos());
-+            }
-+            this.setLevel(world);
-+            this.setPosRaw(position.x(), position.y(), position.z()); // Paper - don't register to chunks yet
-+        }
-+        this.gameMode.setLevel((ServerLevel) world);
-     }
-+    // CraftBukkit end
- 
-     public void setExperiencePoints(int points) {
-         float f = (float) this.getXpNeededForNextLevel();
-@@ -744,6 +948,11 @@
- 
-     @Override
-     public void tick() {
-+        // CraftBukkit start
-+        if (this.joining) {
-+            this.joining = false;
-+        }
-+        // CraftBukkit end
-         this.tickClientLoadTimeout();
-         this.gameMode.tick();
-         this.wardenSpawnTracker.tick();
-@@ -751,9 +960,13 @@
-             --this.invulnerableTime;
-         }
- 
--        this.containerMenu.broadcastChanges();
--        if (!this.containerMenu.stillValid(this)) {
--            this.closeContainer();
-+        if (--this.containerUpdateDelay <= 0) {
-+            this.containerMenu.broadcastChanges();
-+            this.containerUpdateDelay = this.level().paperConfig().tickRates.containerUpdate;
-+        }
-+        // Paper end - Configurable container update tick rate
-+        if (this.containerMenu != this.inventoryMenu && (this.isImmobile() || !this.containerMenu.stillValid(this))) { // Paper - Prevent opening inventories when frozen
-+            this.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.CANT_USE); // Paper - Inventory close reason
-             this.containerMenu = this.inventoryMenu;
-         }
- 
-@@ -807,7 +1020,7 @@
- 
-     public void doTick() {
-         try {
--            if (!this.isSpectator() || !this.touchingUnloadedChunk()) {
-+            if (valid && !this.isSpectator() || !this.touchingUnloadedChunk()) { // Paper - don't tick dead players that are not in the world currently (pending respawn)
-                 super.tick();
-             }
- 
-@@ -820,7 +1033,7 @@
-             }
- 
-             if (this.getHealth() != this.lastSentHealth || this.lastSentFood != this.foodData.getFoodLevel() || this.foodData.getSaturationLevel() == 0.0F != this.lastFoodSaturationZero) {
--                this.connection.send(new ClientboundSetHealthPacket(this.getHealth(), this.foodData.getFoodLevel(), this.foodData.getSaturationLevel()));
-+                this.connection.send(new ClientboundSetHealthPacket(this.getBukkitEntity().getScaledHealth(), this.foodData.getFoodLevel(), this.foodData.getSaturationLevel())); // CraftBukkit
-                 this.lastSentHealth = this.getHealth();
-                 this.lastSentFood = this.foodData.getFoodLevel();
-                 this.lastFoodSaturationZero = this.foodData.getSaturationLevel() == 0.0F;
-@@ -851,6 +1064,12 @@
-                 this.updateScoreForCriteria(ObjectiveCriteria.EXPERIENCE, Mth.ceil((float) this.lastRecordedExperience));
-             }
- 
-+            // CraftBukkit start - Force max health updates
-+            if (this.maxHealthCache != this.getMaxHealth()) {
-+                this.getBukkitEntity().updateScaledHealth();
-+            }
-+            // CraftBukkit end
-+
-             if (this.experienceLevel != this.lastRecordedLevel) {
-                 this.lastRecordedLevel = this.experienceLevel;
-                 this.updateScoreForCriteria(ObjectiveCriteria.LEVEL, Mth.ceil((float) this.lastRecordedLevel));
-@@ -865,6 +1084,20 @@
-                 CriteriaTriggers.LOCATION.trigger(this);
-             }
- 
-+            // CraftBukkit start - initialize oldLevel, fire PlayerLevelChangeEvent, and tick client-sided world border
-+            if (this.oldLevel == -1) {
-+                this.oldLevel = this.experienceLevel;
-+            }
-+
-+            if (this.oldLevel != this.experienceLevel) {
-+                CraftEventFactory.callPlayerLevelChangeEvent(this.getBukkitEntity(), this.oldLevel, this.experienceLevel);
-+                this.oldLevel = this.experienceLevel;
-+            }
-+
-+            if (this.getBukkitEntity().hasClientWorldBorder()) {
-+                ((CraftWorldBorder) this.getBukkitEntity().getWorldBorder()).getHandle().tick();
-+            }
-+            // CraftBukkit end
-         } catch (Throwable throwable) {
-             CrashReport crashreport = CrashReport.forThrowable(throwable, "Ticking player");
-             CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Player being ticked");
-@@ -893,7 +1126,7 @@
-         if (this.level().getDifficulty() == Difficulty.PEACEFUL && this.serverLevel().getGameRules().getBoolean(GameRules.RULE_NATURAL_REGENERATION)) {
-             if (this.tickCount % 20 == 0) {
-                 if (this.getHealth() < this.getMaxHealth()) {
--                    this.heal(1.0F);
-+                    this.heal(1.0F, org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.REGEN); // CraftBukkit - added regain reason of "REGEN" for filtering purposes.
-                 }
- 
-                 float f = this.foodData.getSaturationLevel();
-@@ -946,19 +1179,105 @@
-     }
- 
-     private void updateScoreForCriteria(ObjectiveCriteria criterion, int score) {
--        this.getScoreboard().forAllObjectives(criterion, this, (scoreaccess) -> {
-+        // CraftBukkit - Use our scores instead
-+        this.level().getCraftServer().getScoreboardManager().forAllObjectives(criterion, this, (scoreaccess) -> {
-             scoreaccess.set(score);
-         });
-     }
- 
-+    // Paper start - PlayerDeathEvent#getItemsToKeep
-+    private static void processKeep(org.bukkit.event.entity.PlayerDeathEvent event, NonNullList<ItemStack> inv) {
-+        List<org.bukkit.inventory.ItemStack> itemsToKeep = event.getItemsToKeep();
-+        if (inv == null) {
-+            // remainder of items left in toKeep - plugin added stuff on death that wasn't in the initial loot?
-+            if (!itemsToKeep.isEmpty()) {
-+                for (org.bukkit.inventory.ItemStack itemStack : itemsToKeep) {
-+                    event.getEntity().getInventory().addItem(itemStack);
-+                }
-+            }
-+
-+            return;
-+        }
-+
-+        for (int i = 0; i < inv.size(); ++i) {
-+            ItemStack item = inv.get(i);
-+            if (EnchantmentHelper.has(item, EnchantmentEffectComponents.PREVENT_EQUIPMENT_DROP) || itemsToKeep.isEmpty() || item.isEmpty()) {
-+                inv.set(i, ItemStack.EMPTY);
-+                continue;
-+            }
-+
-+            final org.bukkit.inventory.ItemStack bukkitStack = item.getBukkitStack();
-+            boolean keep = false;
-+            final Iterator<org.bukkit.inventory.ItemStack> iterator = itemsToKeep.iterator();
-+            while (iterator.hasNext()) {
-+                final org.bukkit.inventory.ItemStack itemStack = iterator.next();
-+                if (bukkitStack.equals(itemStack)) {
-+                    iterator.remove();
-+                    keep = true;
-+                    break;
-+                }
-+            }
-+
-+            if (!keep) {
-+                inv.set(i, ItemStack.EMPTY);
-+            }
-+        }
-+    }
-+    // Paper end - PlayerDeathEvent#getItemsToKeep
-+
-     @Override
-     public void die(DamageSource damageSource) {
--        this.gameEvent(GameEvent.ENTITY_DIE);
-+        // this.gameEvent(GameEvent.ENTITY_DIE); // Paper - move below event cancellation check
-         boolean flag = this.serverLevel().getGameRules().getBoolean(GameRules.RULE_SHOWDEATHMESSAGES);
-+        // CraftBukkit start - fire PlayerDeathEvent
-+        if (this.isRemoved()) {
-+            return;
-+        }
-+        List<DefaultDrop> loot = new java.util.ArrayList<>(this.getInventory().getContainerSize()); // Paper - Restore vanilla drops behavior
-+        boolean keepInventory = this.serverLevel().getGameRules().getBoolean(GameRules.RULE_KEEPINVENTORY) || this.isSpectator();
- 
--        if (flag) {
--            Component ichatbasecomponent = this.getCombatTracker().getDeathMessage();
-+        if (!keepInventory) {
-+            for (ItemStack item : this.getInventory().getContents()) {
-+                if (!item.isEmpty() && !EnchantmentHelper.has(item, EnchantmentEffectComponents.PREVENT_EQUIPMENT_DROP)) {
-+                    loot.add(new DefaultDrop(item, stack -> this.drop(stack, true, false, false))); // Paper - Restore vanilla drops behavior; drop function taken from Inventory#dropAll (don't fire drop event)
-+                }
-+            }
-+        }
-+        if (this.shouldDropLoot() && this.serverLevel().getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) { // Paper - fix player loottables running when mob loot gamerule is false
-+        // SPIGOT-5071: manually add player loot tables (SPIGOT-5195 - ignores keepInventory rule)
-+        this.dropFromLootTable(this.serverLevel(), damageSource, this.lastHurtByPlayerTime > 0);
-+        // Paper - Restore vanilla drops behaviour; custom death loot is a noop on server player, remove.
- 
-+        loot.addAll(this.drops);
-+        this.drops.clear(); // SPIGOT-5188: make sure to clear
-+        } // Paper - fix player loottables running when mob loot gamerule is false
-+
-+        Component defaultMessage = this.getCombatTracker().getDeathMessage();
-+
-+        String deathmessage = defaultMessage.getString();
-+        this.keepLevel = keepInventory; // SPIGOT-2222: pre-set keepLevel
-+        org.bukkit.event.entity.PlayerDeathEvent event = CraftEventFactory.callPlayerDeathEvent(this, damageSource, loot, PaperAdventure.asAdventure(defaultMessage), keepInventory); // Paper - Adventure
-+        // Paper start - cancellable death event
-+        if (event.isCancelled()) {
-+            // make compatible with plugins that might have already set the health in an event listener
-+            if (this.getHealth() <= 0) {
-+                this.setHealth((float) event.getReviveHealth());
-+            }
-+            return;
-+        }
-+        this.gameEvent(GameEvent.ENTITY_DIE); // moved from the top of this method
-+        // Paper end
-+
-+        // SPIGOT-943 - only call if they have an inventory open
-+        if (this.containerMenu != this.inventoryMenu) {
-+            this.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.DEATH); // Paper - Inventory close reason
-+        }
-+
-+        net.kyori.adventure.text.Component deathMessage = event.deathMessage() != null ? event.deathMessage() : net.kyori.adventure.text.Component.empty(); // Paper - Adventure
-+
-+        if (deathMessage != null && deathMessage != net.kyori.adventure.text.Component.empty() && flag) { // Paper - Adventure // TODO: allow plugins to override?
-+            Component ichatbasecomponent = PaperAdventure.asVanilla(deathMessage); // Paper - Adventure
-+
-             this.connection.send(new ClientboundPlayerCombatKillPacket(this.getId(), ichatbasecomponent), PacketSendListener.exceptionallySend(() -> {
-                 boolean flag1 = true;
-                 String s = ichatbasecomponent.getString(256);
-@@ -988,12 +1307,23 @@
-         if (this.serverLevel().getGameRules().getBoolean(GameRules.RULE_FORGIVE_DEAD_PLAYERS)) {
-             this.tellNeutralMobsThatIDied();
-         }
--
--        if (!this.isSpectator()) {
--            this.dropAllDeathLoot(this.serverLevel(), damageSource);
-+        // SPIGOT-5478 must be called manually now
-+        if (event.shouldDropExperience()) this.dropExperience(this.serverLevel(), damageSource.getEntity()); // Paper - tie to event
-+        // we clean the player's inventory after the EntityDeathEvent is called so plugins can get the exact state of the inventory.
-+        if (!event.getKeepInventory()) {
-+            // Paper start - PlayerDeathEvent#getItemsToKeep
-+            for (NonNullList<ItemStack> inv : this.getInventory().compartments) {
-+                processKeep(event, inv);
-+            }
-+            processKeep(event, null);
-+            // Paper end - PlayerDeathEvent#getItemsToKeep
-         }
- 
--        this.getScoreboard().forAllObjectives(ObjectiveCriteria.DEATH_COUNT, this, ScoreAccess::increment);
-+        this.setCamera(this); // Remove spectated target
-+        // CraftBukkit end
-+
-+        // CraftBukkit - Get our scores instead
-+        this.level().getCraftServer().getScoreboardManager().forAllObjectives(ObjectiveCriteria.DEATH_COUNT, this, ScoreAccess::increment);
-         LivingEntity entityliving = this.getKillCredit();
- 
-         if (entityliving != null) {
-@@ -1028,10 +1358,12 @@
-     public void awardKillScore(Entity entityKilled, DamageSource damageSource) {
-         if (entityKilled != this) {
-             super.awardKillScore(entityKilled, damageSource);
--            this.getScoreboard().forAllObjectives(ObjectiveCriteria.KILL_COUNT_ALL, this, ScoreAccess::increment);
--            if (entityKilled instanceof Player) {
-+            // CraftBukkit - Get our scores instead
-+            this.level().getCraftServer().getScoreboardManager().forAllObjectives(ObjectiveCriteria.KILL_COUNT_ALL, this, ScoreAccess::increment);
-+            if (entityKilled instanceof net.minecraft.world.entity.player.Player) {
-                 this.awardStat(Stats.PLAYER_KILLS);
--                this.getScoreboard().forAllObjectives(ObjectiveCriteria.KILL_COUNT_PLAYERS, this, ScoreAccess::increment);
-+                // CraftBukkit - Get our scores instead
-+                this.level().getCraftServer().getScoreboardManager().forAllObjectives(ObjectiveCriteria.KILL_COUNT_PLAYERS, this, ScoreAccess::increment);
-             } else {
-                 this.awardStat(Stats.MOB_KILLS);
-             }
-@@ -1049,7 +1381,8 @@
-             int i = scoreboardteam.getColor().getId();
- 
-             if (i >= 0 && i < criterions.length) {
--                this.getScoreboard().forAllObjectives(criterions[i], targetScoreHolder, ScoreAccess::increment);
-+                // CraftBukkit - Get our scores instead
-+                this.level().getCraftServer().getScoreboardManager().forAllObjectives(criterions[i], targetScoreHolder, ScoreAccess::increment);
-             }
-         }
- 
-@@ -1062,8 +1395,8 @@
-         } else {
-             Entity entity = source.getEntity();
- 
--            if (entity instanceof Player) {
--                Player entityhuman = (Player) entity;
-+            if (entity instanceof net.minecraft.world.entity.player.Player) {
-+                net.minecraft.world.entity.player.Player entityhuman = (net.minecraft.world.entity.player.Player) entity;
- 
-                 if (!this.canHarmPlayer(entityhuman)) {
-                     return false;
-@@ -1074,8 +1407,8 @@
-                 AbstractArrow entityarrow = (AbstractArrow) entity;
-                 Entity entity1 = entityarrow.getOwner();
- 
--                if (entity1 instanceof Player) {
--                    Player entityhuman1 = (Player) entity1;
-+                if (entity1 instanceof net.minecraft.world.entity.player.Player) {
-+                    net.minecraft.world.entity.player.Player entityhuman1 = (net.minecraft.world.entity.player.Player) entity1;
- 
-                     if (!this.canHarmPlayer(entityhuman1)) {
-                         return false;
-@@ -1083,38 +1416,84 @@
-                 }
-             }
- 
--            return super.hurtServer(world, source, amount);
-+            // Paper start - cancellable death events
-+            // return super.hurtServer(world, source, amount);
-+            this.queueHealthUpdatePacket = true;
-+            boolean damaged = super.hurtServer(world, source, amount);
-+            this.queueHealthUpdatePacket = false;
-+            if (this.queuedHealthUpdatePacket != null) {
-+                this.connection.send(this.queuedHealthUpdatePacket);
-+                this.queuedHealthUpdatePacket = null;
-+            }
-+            return damaged;
-+            // Paper end - cancellable death events
-         }
-     }
- 
-     @Override
--    public boolean canHarmPlayer(Player player) {
-+    public boolean canHarmPlayer(net.minecraft.world.entity.player.Player player) {
-         return !this.isPvpAllowed() ? false : super.canHarmPlayer(player);
-     }
- 
-     private boolean isPvpAllowed() {
--        return this.server.isPvpAllowed();
-+        // CraftBukkit - this.server.isPvpAllowed() -> this.world.pvpMode
-+        return this.level().pvpMode;
-     }
- 
--    public TeleportTransition findRespawnPositionAndUseSpawnBlock(boolean alive, TeleportTransition.PostTeleportTransition postDimensionTransition) {
-+    // CraftBukkit start
-+    public TeleportTransition findRespawnPositionAndUseSpawnBlock(boolean flag, TeleportTransition.PostTeleportTransition teleporttransition_a, PlayerRespawnEvent.RespawnReason reason) {
-+        TeleportTransition teleportTransition;
-+        boolean isBedSpawn = false;
-+        boolean isAnchorSpawn = false;
-+        // CraftBukkit end
-         BlockPos blockposition = this.getRespawnPosition();
-         float f = this.getRespawnAngle();
-         boolean flag1 = this.isRespawnForced();
-         ServerLevel worldserver = this.server.getLevel(this.getRespawnDimension());
- 
-         if (worldserver != null && blockposition != null) {
--            Optional<ServerPlayer.RespawnPosAngle> optional = ServerPlayer.findRespawnAndUseSpawnBlock(worldserver, blockposition, f, flag1, alive);
-+            Optional<ServerPlayer.RespawnPosAngle> optional = ServerPlayer.findRespawnAndUseSpawnBlock(worldserver, blockposition, f, flag1, flag);
- 
-             if (optional.isPresent()) {
-                 ServerPlayer.RespawnPosAngle entityplayer_respawnposangle = (ServerPlayer.RespawnPosAngle) optional.get();
- 
--                return new TeleportTransition(worldserver, entityplayer_respawnposangle.position(), Vec3.ZERO, entityplayer_respawnposangle.yaw(), 0.0F, postDimensionTransition);
-+                // CraftBukkit start
-+                isBedSpawn = entityplayer_respawnposangle.isBedSpawn();
-+                isAnchorSpawn = entityplayer_respawnposangle.isAnchorSpawn();
-+                teleportTransition = new TeleportTransition(worldserver, entityplayer_respawnposangle.position(), Vec3.ZERO, entityplayer_respawnposangle.yaw(), 0.0F, teleporttransition_a);
-+                // CraftBukkit end
-             } else {
--                return TeleportTransition.missingRespawnBlock(this.server.overworld(), this, postDimensionTransition);
-+                teleportTransition = TeleportTransition.missingRespawnBlock(this.server.overworld(), this, teleporttransition_a); // CraftBukkit
-             }
-         } else {
--            return new TeleportTransition(this.server.overworld(), this, postDimensionTransition);
-+            teleportTransition = new TeleportTransition(this.server.overworld(), this, teleporttransition_a); // CraftBukkit
-         }
-+        // CraftBukkit start
-+        if (reason == null) {
-+            return teleportTransition;
-+        }
-+
-+        Player respawnPlayer = this.getBukkitEntity();
-+        Location location = CraftLocation.toBukkit(teleportTransition.position(), teleportTransition.newLevel().getWorld(), teleportTransition.yRot(), teleportTransition.xRot());
-+
-+        // Paper start - respawn flags
-+        com.google.common.collect.ImmutableSet.Builder<org.bukkit.event.player.PlayerRespawnEvent.RespawnFlag> builder = com.google.common.collect.ImmutableSet.builder();
-+        if (reason == org.bukkit.event.player.PlayerRespawnEvent.RespawnReason.END_PORTAL) {
-+            builder.add(org.bukkit.event.player.PlayerRespawnEvent.RespawnFlag.END_PORTAL);
-+        }
-+        PlayerRespawnEvent respawnEvent = new PlayerRespawnEvent(respawnPlayer, location, isBedSpawn, isAnchorSpawn, reason, builder);
-+        // Paper end - respawn flags
-+        this.level().getCraftServer().getPluginManager().callEvent(respawnEvent);
-+        // Spigot Start
-+        if (this.connection.isDisconnected()) {
-+            return null;
-+        }
-+        // Spigot End
-+
-+        location = respawnEvent.getRespawnLocation();
-+
-+        return new TeleportTransition(((CraftWorld) location.getWorld()).getHandle(), CraftLocation.toVec3D(location), teleportTransition.deltaMovement(), location.getYaw(), location.getPitch(), teleportTransition.missingRespawnBlock(), teleportTransition.asPassenger(), teleportTransition.relatives(), teleportTransition.postTeleportTransition(), teleportTransition.cause());
-+        // CraftBukkit end
-     }
- 
-     public static Optional<ServerPlayer.RespawnPosAngle> findRespawnAndUseSpawnBlock(ServerLevel world, BlockPos pos, float spawnAngle, boolean spawnForced, boolean alive) {
-@@ -1129,11 +1508,11 @@
-             }
- 
-             return optional.map((vec3d) -> {
--                return ServerPlayer.RespawnPosAngle.of(vec3d, pos);
-+                return ServerPlayer.RespawnPosAngle.of(vec3d, pos, false, true); // CraftBukkit
-             });
-         } else if (block instanceof BedBlock && BedBlock.canSetSpawn(world)) {
-             return BedBlock.findStandUpPosition(EntityType.PLAYER, world, pos, (Direction) iblockdata.getValue(BedBlock.FACING), spawnAngle).map((vec3d) -> {
--                return ServerPlayer.RespawnPosAngle.of(vec3d, pos);
-+                return ServerPlayer.RespawnPosAngle.of(vec3d, pos, true, false); // CraftBukkit
-             });
-         } else if (!spawnForced) {
-             return Optional.empty();
-@@ -1142,7 +1521,7 @@
-             BlockState iblockdata1 = world.getBlockState(pos.above());
-             boolean flag3 = iblockdata1.getBlock().isPossibleToRespawnInThis(iblockdata1);
- 
--            return flag2 && flag3 ? Optional.of(new ServerPlayer.RespawnPosAngle(new Vec3((double) pos.getX() + 0.5D, (double) pos.getY() + 0.1D, (double) pos.getZ() + 0.5D), spawnAngle)) : Optional.empty();
-+            return flag2 && flag3 ? Optional.of(new ServerPlayer.RespawnPosAngle(new Vec3((double) pos.getX() + 0.5D, (double) pos.getY() + 0.1D, (double) pos.getZ() + 0.5D), spawnAngle, false, false)) : Optional.empty(); // CraftBukkit
-         }
-     }
- 
-@@ -1160,6 +1539,7 @@
-     @Nullable
-     @Override
-     public ServerPlayer teleport(TeleportTransition teleportTarget) {
-+        if (this.isSleeping()) return null; // CraftBukkit - SPIGOT-3154
-         if (this.isRemoved()) {
-             return null;
-         } else {
-@@ -1169,39 +1549,78 @@
- 
-             ServerLevel worldserver = teleportTarget.newLevel();
-             ServerLevel worldserver1 = this.serverLevel();
--            ResourceKey<Level> resourcekey = worldserver1.dimension();
-+            // CraftBukkit start
-+            ResourceKey<LevelStem> resourcekey = worldserver1.getTypeKey();
- 
-+            Location enter = this.getBukkitEntity().getLocation();
-+            PositionMoveRotation absolutePosition = PositionMoveRotation.calculateAbsolute(PositionMoveRotation.of(this), PositionMoveRotation.of(teleportTarget), teleportTarget.relatives());
-+            Location exit = /* (worldserver == null) ? null : // Paper - always non-null */CraftLocation.toBukkit(absolutePosition.position(), worldserver.getWorld(), absolutePosition.yRot(), absolutePosition.xRot());
-+            PlayerTeleportEvent tpEvent = new PlayerTeleportEvent(this.getBukkitEntity(), enter, exit, teleportTarget.cause());
-+            // Paper start - gateway-specific teleport event
-+            if (this.portalProcess != null && this.portalProcess.isSamePortal(((net.minecraft.world.level.block.EndGatewayBlock) net.minecraft.world.level.block.Blocks.END_GATEWAY)) && this.serverLevel().getBlockEntity(this.portalProcess.getEntryPosition()) instanceof net.minecraft.world.level.block.entity.TheEndGatewayBlockEntity theEndGatewayBlockEntity) {
-+                tpEvent = new com.destroystokyo.paper.event.player.PlayerTeleportEndGatewayEvent(this.getBukkitEntity(), enter, exit, new org.bukkit.craftbukkit.block.CraftEndGateway(this.serverLevel().getWorld(), theEndGatewayBlockEntity));
-+            }
-+            // Paper end - gateway-specific teleport event
-+            Bukkit.getServer().getPluginManager().callEvent(tpEvent);
-+            Location newExit = tpEvent.getTo();
-+            if (tpEvent.isCancelled() || newExit == null) {
-+                return null;
-+            }
-+            if (!newExit.equals(exit)) {
-+                worldserver = ((CraftWorld) newExit.getWorld()).getHandle();
-+                teleportTarget = new TeleportTransition(worldserver, CraftLocation.toVec3D(newExit), Vec3.ZERO, newExit.getYaw(), newExit.getPitch(), teleportTarget.missingRespawnBlock(), teleportTarget.asPassenger(), Set.of(), teleportTarget.postTeleportTransition(), teleportTarget.cause());
-+            }
-+            // CraftBukkit end
-+
-             if (!teleportTarget.asPassenger()) {
-                 this.stopRiding();
-             }
- 
--            if (worldserver.dimension() == resourcekey) {
--                this.connection.teleport(PositionMoveRotation.of(teleportTarget), teleportTarget.relatives());
-+            // CraftBukkit start
-+            if (worldserver != null && worldserver.dimension() == worldserver1.dimension()) {
-+                this.connection.internalTeleport(PositionMoveRotation.of(teleportTarget), teleportTarget.relatives());
-+                // CraftBukkit end
-                 this.connection.resetPosition();
-                 teleportTarget.postTeleportTransition().onTransition(this);
-                 return this;
-             } else {
-+                // CraftBukkit start
-+                /*
-                 this.isChangingDimension = true;
--                LevelData worlddata = worldserver.getLevelData();
-+                WorldData worlddata = worldserver.getLevelData();
- 
--                this.connection.send(new ClientboundRespawnPacket(this.createCommonSpawnInfo(worldserver), (byte) 3));
--                this.connection.send(new ClientboundChangeDifficultyPacket(worlddata.getDifficulty(), worlddata.isDifficultyLocked()));
-+                this.connection.send(new PacketPlayOutRespawn(this.createCommonSpawnInfo(worldserver), (byte) 3));
-+                this.connection.send(new PacketPlayOutServerDifficulty(worlddata.getDifficulty(), worlddata.isDifficultyLocked()));
-                 PlayerList playerlist = this.server.getPlayerList();
- 
-                 playerlist.sendPlayerPermissionLevel(this);
-                 worldserver1.removePlayerImmediately(this, Entity.RemovalReason.CHANGED_DIMENSION);
-                 this.unsetRemoved();
-+                */
-+                // CraftBukkit end
-                 ProfilerFiller gameprofilerfiller = Profiler.get();
- 
-                 gameprofilerfiller.push("moving");
--                if (resourcekey == Level.OVERWORLD && worldserver.dimension() == Level.NETHER) {
-+                if (worldserver != null && resourcekey == LevelStem.OVERWORLD && worldserver.getTypeKey() == LevelStem.NETHER) { // CraftBukkit - empty to fall through to null to event
-                     this.enteredNetherPosition = this.position();
-                 }
- 
-                 gameprofilerfiller.pop();
-                 gameprofilerfiller.push("placing");
-+                // CraftBukkit start
-+                this.isChangingDimension = true; // CraftBukkit - Set teleport invulnerability only if player changing worlds
-+                LevelData worlddata = worldserver.getLevelData();
-+
-+                this.connection.send(new ClientboundRespawnPacket(this.createCommonSpawnInfo(worldserver), (byte) 3));
-+                this.connection.send(new ClientboundChangeDifficultyPacket(worlddata.getDifficulty(), worlddata.isDifficultyLocked()));
-+                PlayerList playerlist = this.server.getPlayerList();
-+
-+                playerlist.sendPlayerPermissionLevel(this);
-+                worldserver1.removePlayerImmediately(this, Entity.RemovalReason.CHANGED_DIMENSION);
-+                this.unsetRemoved();
-+                // CraftBukkit end
-                 this.setServerLevel(worldserver);
--                this.connection.teleport(PositionMoveRotation.of(teleportTarget), teleportTarget.relatives());
-+                this.connection.internalTeleport(PositionMoveRotation.of(teleportTarget), teleportTarget.relatives()); // CraftBukkit - use internal teleport without event
-                 this.connection.resetPosition();
-                 worldserver.addDuringTeleport(this);
-                 gameprofilerfiller.pop();
-@@ -1215,10 +1634,33 @@
-                 this.lastSentExp = -1;
-                 this.lastSentHealth = -1.0F;
-                 this.lastSentFood = -1;
-+
-+                // CraftBukkit start
-+                PlayerChangedWorldEvent changeEvent = new PlayerChangedWorldEvent(this.getBukkitEntity(), worldserver1.getWorld());
-+                this.level().getCraftServer().getPluginManager().callEvent(changeEvent);
-+                // CraftBukkit end
-+                // Paper start - Reset shield blocking on dimension change
-+                if (this.isBlocking()) {
-+                    this.stopUsingItem();
-+                }
-+                // Paper end - Reset shield blocking on dimension change
-                 return this;
-             }
-+        }
-+    }
-+
-+    // CraftBukkit start
-+    @Override
-+    public CraftPortalEvent callPortalEvent(Entity entity, Location exit, TeleportCause cause, int searchRadius, int creationRadius) {
-+        Location enter = this.getBukkitEntity().getLocation();
-+        PlayerPortalEvent event = new PlayerPortalEvent(this.getBukkitEntity(), enter, exit, cause, searchRadius, true, creationRadius);
-+        Bukkit.getServer().getPluginManager().callEvent(event);
-+        if (event.isCancelled() || event.getTo() == null || event.getTo().getWorld() == null) {
-+            return null;
-         }
-+        return new CraftPortalEvent(event);
-     }
-+    // CraftBukkit end
- 
-     @Override
-     public void forceSetRotation(float yaw, float pitch) {
-@@ -1228,13 +1670,27 @@
-     public void triggerDimensionChangeTriggers(ServerLevel origin) {
-         ResourceKey<Level> resourcekey = origin.dimension();
-         ResourceKey<Level> resourcekey1 = this.level().dimension();
-+        // CraftBukkit start
-+        ResourceKey<Level> maindimensionkey = CraftDimensionUtil.getMainDimensionKey(origin);
-+        ResourceKey<Level> maindimensionkey1 = CraftDimensionUtil.getMainDimensionKey(this.level());
- 
--        CriteriaTriggers.CHANGED_DIMENSION.trigger(this, resourcekey, resourcekey1);
--        if (resourcekey == Level.NETHER && resourcekey1 == Level.OVERWORLD && this.enteredNetherPosition != null) {
-+        // Paper start - Add option for strict advancement dimension checks
-+        if (io.papermc.paper.configuration.GlobalConfiguration.get().misc.strictAdvancementDimensionCheck) {
-+            maindimensionkey = resourcekey;
-+            maindimensionkey1 = resourcekey1;
-+        }
-+        // Paper end - Add option for strict advancement dimension checks
-+        CriteriaTriggers.CHANGED_DIMENSION.trigger(this, maindimensionkey, maindimensionkey1);
-+        if (maindimensionkey != resourcekey || maindimensionkey1 != resourcekey1) {
-+            CriteriaTriggers.CHANGED_DIMENSION.trigger(this, resourcekey, resourcekey1);
-+        }
-+
-+        if (maindimensionkey == Level.NETHER && maindimensionkey1 == Level.OVERWORLD && this.enteredNetherPosition != null) {
-+            // CraftBukkit end
-             CriteriaTriggers.NETHER_TRAVEL.trigger(this, this.enteredNetherPosition);
-         }
- 
--        if (resourcekey1 != Level.NETHER) {
-+        if (maindimensionkey1 != Level.NETHER) { // CraftBukkit
-             this.enteredNetherPosition = null;
-         }
- 
-@@ -1251,36 +1707,63 @@
-         this.containerMenu.broadcastChanges();
-     }
- 
--    @Override
--    public Either<Player.BedSleepingProblem, Unit> startSleepInBed(BlockPos pos) {
--        Direction enumdirection = (Direction) this.level().getBlockState(pos).getValue(HorizontalDirectionalBlock.FACING);
--
-+    // CraftBukkit start - moved bed result checks from below into separate method
-+    private Either<net.minecraft.world.entity.player.Player.BedSleepingProblem, Unit> getBedResult(BlockPos blockposition, Direction enumdirection) {
-         if (!this.isSleeping() && this.isAlive()) {
--            if (!this.level().dimensionType().natural()) {
--                return Either.left(Player.BedSleepingProblem.NOT_POSSIBLE_HERE);
--            } else if (!this.bedInRange(pos, enumdirection)) {
--                return Either.left(Player.BedSleepingProblem.TOO_FAR_AWAY);
--            } else if (this.bedBlocked(pos, enumdirection)) {
--                return Either.left(Player.BedSleepingProblem.OBSTRUCTED);
-+            if (!this.level().dimensionType().natural() || !this.level().dimensionType().bedWorks()) {
-+                return Either.left(net.minecraft.world.entity.player.Player.BedSleepingProblem.NOT_POSSIBLE_HERE);
-+            } else if (!this.bedInRange(blockposition, enumdirection)) {
-+                return Either.left(net.minecraft.world.entity.player.Player.BedSleepingProblem.TOO_FAR_AWAY);
-+            } else if (this.bedBlocked(blockposition, enumdirection)) {
-+                return Either.left(net.minecraft.world.entity.player.Player.BedSleepingProblem.OBSTRUCTED);
-             } else {
--                this.setRespawnPosition(this.level().dimension(), pos, this.getYRot(), false, true);
-+                this.setRespawnPosition(this.level().dimension(), blockposition, this.getYRot(), false, true, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.BED); // Paper - Add PlayerSetSpawnEvent
-                 if (this.level().isDay()) {
--                    return Either.left(Player.BedSleepingProblem.NOT_POSSIBLE_NOW);
-+                    return Either.left(net.minecraft.world.entity.player.Player.BedSleepingProblem.NOT_POSSIBLE_NOW);
-                 } else {
-                     if (!this.isCreative()) {
-                         double d0 = 8.0D;
-                         double d1 = 5.0D;
--                        Vec3 vec3d = Vec3.atBottomCenterOf(pos);
-+                        Vec3 vec3d = Vec3.atBottomCenterOf(blockposition);
-                         List<Monster> list = this.level().getEntitiesOfClass(Monster.class, new AABB(vec3d.x() - 8.0D, vec3d.y() - 5.0D, vec3d.z() - 8.0D, vec3d.x() + 8.0D, vec3d.y() + 5.0D, vec3d.z() + 8.0D), (entitymonster) -> {
-                             return entitymonster.isPreventingPlayerRest(this.serverLevel(), this);
-                         });
- 
-                         if (!list.isEmpty()) {
--                            return Either.left(Player.BedSleepingProblem.NOT_SAFE);
-+                            return Either.left(net.minecraft.world.entity.player.Player.BedSleepingProblem.NOT_SAFE);
-                         }
-                     }
- 
--                    Either<Player.BedSleepingProblem, Unit> either = super.startSleepInBed(pos).ifRight((unit) -> {
-+                    return Either.right(Unit.INSTANCE);
-+                }
-+            }
-+        } else {
-+            return Either.left(net.minecraft.world.entity.player.Player.BedSleepingProblem.OTHER_PROBLEM);
-+        }
-+    }
-+
-+    @Override
-+    public Either<net.minecraft.world.entity.player.Player.BedSleepingProblem, Unit> startSleepInBed(BlockPos blockposition, boolean force) {
-+        Direction enumdirection = (Direction) this.level().getBlockState(blockposition).getValue(HorizontalDirectionalBlock.FACING);
-+        Either<net.minecraft.world.entity.player.Player.BedSleepingProblem, Unit> bedResult = this.getBedResult(blockposition, enumdirection);
-+
-+        if (bedResult.left().orElse(null) == net.minecraft.world.entity.player.Player.BedSleepingProblem.OTHER_PROBLEM) {
-+            return bedResult; // return immediately if the result is not bypassable by plugins
-+        }
-+
-+        if (force) {
-+            bedResult = Either.right(Unit.INSTANCE);
-+        }
-+
-+        bedResult = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerBedEnterEvent(this, blockposition, bedResult);
-+        if (bedResult.left().isPresent()) {
-+            return bedResult;
-+        }
-+
-+        {
-+            {
-+                {
-+                    Either<net.minecraft.world.entity.player.Player.BedSleepingProblem, Unit> either = super.startSleepInBed(blockposition, force).ifRight((unit) -> {
-                         this.awardStat(Stats.SLEEP_IN_BED);
-                         CriteriaTriggers.SLEPT_IN_BED.trigger(this);
-                     });
-@@ -1293,9 +1776,8 @@
-                     return either;
-                 }
-             }
--        } else {
--            return Either.left(Player.BedSleepingProblem.OTHER_PROBLEM);
-         }
-+        // CraftBukkit end
-     }
- 
-     @Override
-@@ -1322,13 +1804,31 @@
- 
-     @Override
-     public void stopSleepInBed(boolean skipSleepTimer, boolean updateSleepingPlayers) {
-+        if (!this.isSleeping()) return; // CraftBukkit - Can't leave bed if not in one!
-+        // CraftBukkit start - fire PlayerBedLeaveEvent
-+        CraftPlayer player = this.getBukkitEntity();
-+        BlockPos bedPosition = this.getSleepingPos().orElse(null);
-+
-+        org.bukkit.block.Block bed;
-+        if (bedPosition != null) {
-+            bed = this.level().getWorld().getBlockAt(bedPosition.getX(), bedPosition.getY(), bedPosition.getZ());
-+        } else {
-+            bed = this.level().getWorld().getBlockAt(player.getLocation());
-+        }
-+
-+        PlayerBedLeaveEvent event = new PlayerBedLeaveEvent(player, bed, true);
-+        this.level().getCraftServer().getPluginManager().callEvent(event);
-+        if (event.isCancelled()) {
-+            return;
-+        }
-+        // CraftBukkit end
-         if (this.isSleeping()) {
-             this.serverLevel().getChunkSource().broadcastAndSend(this, new ClientboundAnimatePacket(this, 2));
-         }
- 
-         super.stopSleepInBed(skipSleepTimer, updateSleepingPlayers);
-         if (this.connection != null) {
--            this.connection.teleport(this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot());
-+            this.connection.teleport(this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot(), TeleportCause.EXIT_BED); // CraftBukkit
-         }
- 
-     }
-@@ -1341,7 +1841,7 @@
- 
-     @Override
-     public boolean isInvulnerableTo(ServerLevel world, DamageSource source) {
--        return super.isInvulnerableTo(world, source) || this.isChangingDimension() && !source.is(DamageTypes.ENDER_PEARL) || !this.hasClientLoaded();
-+        return (super.isInvulnerableTo(world, source) || this.isChangingDimension() && !source.is(DamageTypes.ENDER_PEARL) || !this.hasClientLoaded()) || (!this.level().paperConfig().collisions.allowPlayerCrammingDamage && source.is(DamageTypes.CRAMMING)); // Paper - disable player cramming
-     }
- 
-     @Override
-@@ -1387,8 +1887,9 @@
-         this.connection.send(new ClientboundOpenSignEditorPacket(sign.getBlockPos(), front));
-     }
- 
--    public void nextContainerCounter() {
-+    public int nextContainerCounter() { // CraftBukkit - void -> int
-         this.containerCounter = this.containerCounter % 100 + 1;
-+        return this.containerCounter; // CraftBukkit
-     }
- 
-     @Override
-@@ -1396,13 +1897,44 @@
-         if (factory == null) {
-             return OptionalInt.empty();
-         } else {
-+            // CraftBukkit start - SPIGOT-6552: Handle inventory closing in CraftEventFactory#callInventoryOpenEvent(...)
-+            /*
-             if (this.containerMenu != this.inventoryMenu) {
-                 this.closeContainer();
-             }
-+            */
-+            // CraftBukkit end
- 
-             this.nextContainerCounter();
-             AbstractContainerMenu container = factory.createMenu(this.containerCounter, this.getInventory(), this);
- 
-+            Component title = null; // Paper - Add titleOverride to InventoryOpenEvent
-+            // CraftBukkit start - Inventory open hook
-+            if (container != null) {
-+                container.setTitle(factory.getDisplayName());
-+
-+                boolean cancelled = false;
-+                // Paper start - Add titleOverride to InventoryOpenEvent
-+                final com.mojang.datafixers.util.Pair<net.kyori.adventure.text.Component, AbstractContainerMenu> result = CraftEventFactory.callInventoryOpenEventWithTitle(this, container, cancelled);
-+                container = result.getSecond();
-+                title = PaperAdventure.asVanilla(result.getFirst());
-+                // Paper end - Add titleOverride to InventoryOpenEvent
-+                if (container == null && !cancelled) { // Let pre-cancelled events fall through
-+                    // SPIGOT-5263 - close chest if cancelled
-+                    if (factory instanceof Container) {
-+                        ((Container) factory).stopOpen(this);
-+                    } else if (factory instanceof ChestBlock.DoubleInventory) {
-+                        // SPIGOT-5355 - double chests too :(
-+                        ((ChestBlock.DoubleInventory) factory).inventorylargechest.stopOpen(this);
-+                        // Paper start - Fix InventoryOpenEvent cancellation
-+                    } else if (!this.enderChestInventory.isActiveChest(null)) {
-+                        this.enderChestInventory.stopOpen(this);
-+                        // Paper end - Fix InventoryOpenEvent cancellation
-+                    }
-+                    return OptionalInt.empty();
-+                }
-+            }
-+            // CraftBukkit end
-             if (container == null) {
-                 if (this.isSpectator()) {
-                     this.displayClientMessage(Component.translatable("container.spectatorCantOpen").withStyle(ChatFormatting.RED), true);
-@@ -1410,9 +1942,11 @@
- 
-                 return OptionalInt.empty();
-             } else {
--                this.connection.send(new ClientboundOpenScreenPacket(container.containerId, container.getType(), factory.getDisplayName()));
--                this.initMenu(container);
-+                // CraftBukkit start
-                 this.containerMenu = container;
-+                if (!this.isImmobile()) this.connection.send(new ClientboundOpenScreenPacket(container.containerId, container.getType(), Objects.requireNonNullElseGet(title, container::getTitle))); // Paper - Add titleOverride to InventoryOpenEvent
-+                // CraftBukkit end
-+                this.initMenu(container);
-                 return OptionalInt.of(this.containerCounter);
-             }
-         }
-@@ -1425,15 +1959,26 @@
- 
-     @Override
-     public void openHorseInventory(AbstractHorse horse, Container inventory) {
-+        // CraftBukkit start - Inventory open hook
-+        this.nextContainerCounter();
-+        AbstractContainerMenu container = new HorseInventoryMenu(this.containerCounter, this.getInventory(), inventory, horse, horse.getInventoryColumns());
-+        container.setTitle(horse.getDisplayName());
-+        container = CraftEventFactory.callInventoryOpenEvent(this, container);
-+
-+        if (container == null) {
-+            inventory.stopOpen(this);
-+            return;
-+        }
-+        // CraftBukkit end
-         if (this.containerMenu != this.inventoryMenu) {
--            this.closeContainer();
-+            this.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.OPEN_NEW); // Paper - Inventory close reason
-         }
- 
--        this.nextContainerCounter();
-+        // this.nextContainerCounter(); // CraftBukkit - moved up
-         int i = horse.getInventoryColumns();
- 
-         this.connection.send(new ClientboundHorseScreenOpenPacket(this.containerCounter, i, horse.getId()));
--        this.containerMenu = new HorseInventoryMenu(this.containerCounter, this.getInventory(), inventory, horse, i);
-+        this.containerMenu = container; // CraftBukkit
-         this.initMenu(this.containerMenu);
-     }
- 
-@@ -1456,9 +2001,28 @@
- 
-     @Override
-     public void closeContainer() {
-+        // Paper start - Inventory close reason
-+        this.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.UNKNOWN);
-+    }
-+    @Override
-+    public void closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason reason) {
-+        CraftEventFactory.handleInventoryCloseEvent(this, reason); // CraftBukkit
-+        // Paper end - Inventory close reason
-         this.connection.send(new ClientboundContainerClosePacket(this.containerMenu.containerId));
-         this.doCloseContainer();
-     }
-+    // Paper start - special close for unloaded inventory
-+    @Override
-+    public void closeUnloadedInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason reason) {
-+        // copied from above
-+        CraftEventFactory.handleInventoryCloseEvent(this, reason); // CraftBukkit
-+        // Paper end
-+        // copied from below
-+        this.connection.send(new ClientboundContainerClosePacket(this.containerMenu.containerId));
-+        this.containerMenu = this.inventoryMenu;
-+        // do not run close logic
-+    }
-+    // Paper end - special close for unloaded inventory
- 
-     @Override
-     public void doCloseContainer() {
-@@ -1485,19 +2049,19 @@
-                 i = Math.round((float) Math.sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ) * 100.0F);
-                 if (i > 0) {
-                     this.awardStat(Stats.SWIM_ONE_CM, i);
--                    this.causeFoodExhaustion(0.01F * (float) i * 0.01F);
-+                    this.causeFoodExhaustion(this.level().spigotConfig.swimMultiplier * (float) i * 0.01F, EntityExhaustionEvent.ExhaustionReason.SWIM); // CraftBukkit - EntityExhaustionEvent // Spigot
-                 }
-             } else if (this.isEyeInFluid(FluidTags.WATER)) {
-                 i = Math.round((float) Math.sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ) * 100.0F);
-                 if (i > 0) {
-                     this.awardStat(Stats.WALK_UNDER_WATER_ONE_CM, i);
--                    this.causeFoodExhaustion(0.01F * (float) i * 0.01F);
-+                    this.causeFoodExhaustion(this.level().spigotConfig.swimMultiplier * (float) i * 0.01F, EntityExhaustionEvent.ExhaustionReason.WALK_UNDERWATER); // CraftBukkit - EntityExhaustionEvent // Spigot
-                 }
-             } else if (this.isInWater()) {
-                 i = Math.round((float) Math.sqrt(deltaX * deltaX + deltaZ * deltaZ) * 100.0F);
-                 if (i > 0) {
-                     this.awardStat(Stats.WALK_ON_WATER_ONE_CM, i);
--                    this.causeFoodExhaustion(0.01F * (float) i * 0.01F);
-+                    this.causeFoodExhaustion(this.level().spigotConfig.swimMultiplier * (float) i * 0.01F, EntityExhaustionEvent.ExhaustionReason.WALK_ON_WATER); // CraftBukkit - EntityExhaustionEvent // Spigot
-                 }
-             } else if (this.onClimbable()) {
-                 if (deltaY > 0.0D) {
-@@ -1508,13 +2072,13 @@
-                 if (i > 0) {
-                     if (this.isSprinting()) {
-                         this.awardStat(Stats.SPRINT_ONE_CM, i);
--                        this.causeFoodExhaustion(0.1F * (float) i * 0.01F);
-+                        this.causeFoodExhaustion(this.level().spigotConfig.sprintMultiplier * (float) i * 0.01F, EntityExhaustionEvent.ExhaustionReason.SPRINT); // CraftBukkit - EntityExhaustionEvent // Spigot
-                     } else if (this.isCrouching()) {
-                         this.awardStat(Stats.CROUCH_ONE_CM, i);
--                        this.causeFoodExhaustion(0.0F * (float) i * 0.01F);
-+                        this.causeFoodExhaustion(this.level().spigotConfig.otherMultiplier * (float) i * 0.01F, EntityExhaustionEvent.ExhaustionReason.CROUCH); // CraftBukkit - EntityExhaustionEvent // Spigot
-                     } else {
-                         this.awardStat(Stats.WALK_ONE_CM, i);
--                        this.causeFoodExhaustion(0.0F * (float) i * 0.01F);
-+                        this.causeFoodExhaustion(this.level().spigotConfig.otherMultiplier * (float) i * 0.01F, EntityExhaustionEvent.ExhaustionReason.WALK); // CraftBukkit - EntityExhaustionEvent // Spigot
-                     }
-                 }
-             } else if (this.isFallFlying()) {
-@@ -1557,7 +2121,7 @@
-     @Override
-     public void awardStat(Stat<?> stat, int amount) {
-         this.stats.increment(this, stat, amount);
--        this.getScoreboard().forAllObjectives(stat, this, (scoreaccess) -> {
-+        this.level().getCraftServer().getScoreboardManager().forAllObjectives(stat, this, (scoreaccess) -> {
-             scoreaccess.add(amount);
-         });
-     }
-@@ -1565,7 +2129,7 @@
-     @Override
-     public void resetStat(Stat<?> stat) {
-         this.stats.setValue(this, stat, 0);
--        this.getScoreboard().forAllObjectives(stat, this, ScoreAccess::reset);
-+        this.level().getCraftServer().getScoreboardManager().forAllObjectives(stat, this, ScoreAccess::reset); // CraftBukkit - Get our scores instead
-     }
- 
-     @Override
-@@ -1597,9 +2161,9 @@
-         super.jumpFromGround();
-         this.awardStat(Stats.JUMP);
-         if (this.isSprinting()) {
--            this.causeFoodExhaustion(0.2F);
-+            this.causeFoodExhaustion(this.level().spigotConfig.jumpSprintExhaustion, EntityExhaustionEvent.ExhaustionReason.JUMP_SPRINT); // CraftBukkit - EntityExhaustionEvent // Spigot - Change to use configurable value
-         } else {
--            this.causeFoodExhaustion(0.05F);
-+            this.causeFoodExhaustion(this.level().spigotConfig.jumpWalkExhaustion, EntityExhaustionEvent.ExhaustionReason.JUMP); // CraftBukkit - EntityExhaustionEvent // Spigot - Change to use configurable value
-         }
- 
-     }
-@@ -1613,6 +2177,13 @@
-     public void disconnect() {
-         this.disconnected = true;
-         this.ejectPassengers();
-+
-+        // Paper start - Workaround vehicle not tracking the passenger disconnection dismount
-+        if (this.isPassenger() && this.getVehicle() instanceof ServerPlayer) {
-+            this.stopRiding();
-+        }
-+        // Paper end - Workaround vehicle not tracking the passenger disconnection dismount
-+
-         if (this.isSleeping()) {
-             this.stopSleepInBed(true, false);
-         }
-@@ -1625,6 +2196,7 @@
- 
-     public void resetSentInfo() {
-         this.lastSentHealth = -1.0E8F;
-+        this.lastSentExp = -1; // CraftBukkit - Added to reset
-     }
- 
-     @Override
-@@ -1661,7 +2233,7 @@
-         this.onUpdateAbilities();
-         if (alive) {
-             this.getAttributes().assignBaseValues(oldPlayer.getAttributes());
--            this.getAttributes().assignPermanentModifiers(oldPlayer.getAttributes());
-+            // this.getAttributes().assignPermanentModifiers(entityplayer.getAttributes()); // CraftBukkit
-             this.setHealth(oldPlayer.getHealth());
-             this.foodData = oldPlayer.foodData;
-             Iterator iterator = oldPlayer.getActiveEffects().iterator();
-@@ -1669,7 +2241,7 @@
-             while (iterator.hasNext()) {
-                 MobEffectInstance mobeffect = (MobEffectInstance) iterator.next();
- 
--                this.addEffect(new MobEffectInstance(mobeffect));
-+                // this.addEffect(new MobEffect(mobeffect)); // CraftBukkit
-             }
- 
-             this.getInventory().replaceWith(oldPlayer.getInventory());
-@@ -1680,7 +2252,7 @@
-             this.portalProcess = oldPlayer.portalProcess;
-         } else {
-             this.getAttributes().assignBaseValues(oldPlayer.getAttributes());
--            this.setHealth(this.getMaxHealth());
-+            // this.setHealth(this.getMaxHealth()); // CraftBukkit
-             if (this.serverLevel().getGameRules().getBoolean(GameRules.RULE_KEEPINVENTORY) || oldPlayer.isSpectator()) {
-                 this.getInventory().replaceWith(oldPlayer.getInventory());
-                 this.experienceLevel = oldPlayer.experienceLevel;
-@@ -1696,7 +2268,7 @@
-         this.lastSentExp = -1;
-         this.lastSentHealth = -1.0F;
-         this.lastSentFood = -1;
--        this.recipeBook.copyOverData(oldPlayer.recipeBook);
-+        // this.recipeBook.copyOverData(entityplayer.recipeBook); // CraftBukkit
-         this.seenCredits = oldPlayer.seenCredits;
-         this.enteredNetherPosition = oldPlayer.enteredNetherPosition;
-         this.chunkTrackingView = oldPlayer.chunkTrackingView;
-@@ -1752,19 +2324,19 @@
-     }
- 
-     @Override
--    public boolean teleportTo(ServerLevel world, double destX, double destY, double destZ, Set<Relative> flags, float yaw, float pitch, boolean resetCamera) {
-+    public boolean teleportTo(ServerLevel worldserver, double d0, double d1, double d2, Set<Relative> set, float f, float f1, boolean flag, TeleportCause cause) { // CraftBukkit
-         if (this.isSleeping()) {
-             this.stopSleepInBed(true, true);
-         }
- 
--        if (resetCamera) {
-+        if (flag) {
-             this.setCamera(this);
-         }
- 
--        boolean flag1 = super.teleportTo(world, destX, destY, destZ, flags, yaw, pitch, resetCamera);
-+        boolean flag1 = super.teleportTo(worldserver, d0, d1, d2, set, f, f1, flag, cause); // CraftBukkit
- 
-         if (flag1) {
--            this.setYHeadRot(flags.contains(Relative.Y_ROT) ? this.getYHeadRot() + yaw : yaw);
-+            this.setYHeadRot(set.contains(Relative.Y_ROT) ? this.getYHeadRot() + f : f);
-         }
- 
-         return flag1;
-@@ -1799,10 +2371,18 @@
-     }
- 
-     public boolean setGameMode(GameType gameMode) {
-+        // Paper start - Expand PlayerGameModeChangeEvent
-+        org.bukkit.event.player.PlayerGameModeChangeEvent event = this.setGameMode(gameMode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.UNKNOWN, null);
-+        return event == null ? false : event.isCancelled();
-+    }
-+    @Nullable
-+    public org.bukkit.event.player.PlayerGameModeChangeEvent setGameMode(GameType gameMode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause cause, @Nullable net.kyori.adventure.text.Component message) {
-         boolean flag = this.isSpectator();
- 
--        if (!this.gameMode.changeGameModeForPlayer(gameMode)) {
--            return false;
-+        org.bukkit.event.player.PlayerGameModeChangeEvent event = this.gameMode.changeGameModeForPlayer(gameMode, cause, message);
-+        if (event == null || event.isCancelled()) {
-+            return null;
-+            // Paper end - Expand PlayerGameModeChangeEvent
-         } else {
-             this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.CHANGE_GAME_MODE, (float) gameMode.getId()));
-             if (gameMode == GameType.SPECTATOR) {
-@@ -1818,7 +2398,7 @@
- 
-             this.onUpdateAbilities();
-             this.updateEffectVisibility();
--            return true;
-+            return event; // Paper - Expand PlayerGameModeChangeEvent
-         }
-     }
- 
-@@ -1861,8 +2441,13 @@
-     }
- 
-     public void sendChatMessage(OutgoingChatMessage message, boolean filterMaskEnabled, ChatType.Bound params) {
-+        // Paper start
-+        this.sendChatMessage(message, filterMaskEnabled, params, null);
-+    }
-+    public void sendChatMessage(OutgoingChatMessage message, boolean filterMaskEnabled, ChatType.Bound params, @Nullable Component unsigned) {
-+        // Paper end
-         if (this.acceptsChatMessages()) {
--            message.sendToPlayer(this, filterMaskEnabled, params);
-+            message.sendToPlayer(this, filterMaskEnabled, params, unsigned); // Paper
-         }
- 
-     }
-@@ -1878,7 +2463,36 @@
-     }
- 
-     public void updateOptions(ClientInformation clientOptions) {
-+        // Paper start - settings event
-+        new com.destroystokyo.paper.event.player.PlayerClientOptionsChangeEvent(this.getBukkitEntity(), Util.make(new java.util.IdentityHashMap<>(), map -> {
-+            map.put(com.destroystokyo.paper.ClientOption.LOCALE, clientOptions.language());
-+            map.put(com.destroystokyo.paper.ClientOption.VIEW_DISTANCE, clientOptions.viewDistance());
-+            map.put(com.destroystokyo.paper.ClientOption.CHAT_VISIBILITY, com.destroystokyo.paper.ClientOption.ChatVisibility.valueOf(clientOptions.chatVisibility().name()));
-+            map.put(com.destroystokyo.paper.ClientOption.CHAT_COLORS_ENABLED, clientOptions.chatColors());
-+            map.put(com.destroystokyo.paper.ClientOption.SKIN_PARTS, new com.destroystokyo.paper.PaperSkinParts(clientOptions.modelCustomisation()));
-+            map.put(com.destroystokyo.paper.ClientOption.MAIN_HAND, clientOptions.mainHand() == HumanoidArm.LEFT ? MainHand.LEFT : MainHand.RIGHT);
-+            map.put(com.destroystokyo.paper.ClientOption.TEXT_FILTERING_ENABLED, clientOptions.textFilteringEnabled());
-+            map.put(com.destroystokyo.paper.ClientOption.ALLOW_SERVER_LISTINGS, clientOptions.allowsListing());
-+            map.put(com.destroystokyo.paper.ClientOption.PARTICLE_VISIBILITY, com.destroystokyo.paper.ClientOption.ParticleVisibility.valueOf(clientOptions.particleStatus().name()));
-+        })).callEvent();
-+        // Paper end - settings event
-+        // CraftBukkit start
-+        if (this.getMainArm() != clientOptions.mainHand()) {
-+            PlayerChangedMainHandEvent event = new PlayerChangedMainHandEvent(this.getBukkitEntity(), this.getMainArm() == HumanoidArm.LEFT ? MainHand.LEFT : MainHand.RIGHT);
-+            this.server.server.getPluginManager().callEvent(event);
-+        }
-+        if (this.language == null || !this.language.equals(clientOptions.language())) { // Paper
-+            PlayerLocaleChangeEvent event = new PlayerLocaleChangeEvent(this.getBukkitEntity(), clientOptions.language());
-+            this.server.server.getPluginManager().callEvent(event);
-+        }
-+        // CraftBukkit end
-+        // Paper start - don't call options events on login
-+        this.updateOptionsNoEvents(clientOptions);
-+    }
-+    public void updateOptionsNoEvents(ClientInformation clientOptions) {
-+        // Paper end
-         this.language = clientOptions.language();
-+        this.adventure$locale = java.util.Objects.requireNonNullElse(net.kyori.adventure.translation.Translator.parseLocale(this.language), java.util.Locale.US); // Paper
-         this.requestedViewDistance = clientOptions.viewDistance();
-         this.chatVisibility = clientOptions.chatVisibility();
-         this.canChatColor = clientOptions.chatColors();
-@@ -1957,12 +2571,27 @@
- 
-         this.camera = (Entity) (entity == null ? this : entity);
-         if (entity1 != this.camera) {
-+            // Paper start - Add PlayerStartSpectatingEntityEvent and PlayerStopSpectatingEntity
-+            if (this.camera == this) {
-+                com.destroystokyo.paper.event.player.PlayerStopSpectatingEntityEvent playerStopSpectatingEntityEvent = new com.destroystokyo.paper.event.player.PlayerStopSpectatingEntityEvent(this.getBukkitEntity(), entity1.getBukkitEntity());
-+                if (!playerStopSpectatingEntityEvent.callEvent()) {
-+                    this.camera = entity1; // rollback camera entity again
-+                    return;
-+                }
-+            } else {
-+                com.destroystokyo.paper.event.player.PlayerStartSpectatingEntityEvent playerStartSpectatingEntityEvent = new com.destroystokyo.paper.event.player.PlayerStartSpectatingEntityEvent(this.getBukkitEntity(), entity1.getBukkitEntity(), entity.getBukkitEntity());
-+                if (!playerStartSpectatingEntityEvent.callEvent()) {
-+                    this.camera = entity1; // rollback camera entity again
-+                    return;
-+                }
-+            }
-+            // Paper end - Add PlayerStartSpectatingEntityEvent and PlayerStopSpectatingEntity
-             Level world = this.camera.level();
- 
-             if (world instanceof ServerLevel) {
-                 ServerLevel worldserver = (ServerLevel) world;
- 
--                this.teleportTo(worldserver, this.camera.getX(), this.camera.getY(), this.camera.getZ(), Set.of(), this.getYRot(), this.getXRot(), false);
-+                this.teleportTo(worldserver, this.camera.getX(), this.camera.getY(), this.camera.getZ(), Set.of(), this.getYRot(), this.getXRot(), false, TeleportCause.SPECTATE); // CraftBukkit
-             }
- 
-             if (entity != null) {
-@@ -1999,11 +2628,11 @@
- 
-     @Nullable
-     public Component getTabListDisplayName() {
--        return null;
-+        return this.listName; // CraftBukkit
-     }
- 
-     public int getTabListOrder() {
--        return 0;
-+        return this.listOrder; // CraftBukkit
-     }
- 
-     @Override
-@@ -2045,12 +2674,44 @@
-         this.setRespawnPosition(player.getRespawnDimension(), player.getRespawnPosition(), player.getRespawnAngle(), player.isRespawnForced(), false);
-     }
- 
-+    @Deprecated // Paper - Add PlayerSetSpawnEvent
-     public void setRespawnPosition(ResourceKey<Level> dimension, @Nullable BlockPos pos, float angle, boolean forced, boolean sendMessage) {
-+        // Paper start - Add PlayerSetSpawnEvent
-+        this.setRespawnPosition(dimension, pos, angle, forced, sendMessage, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.UNKNOWN);
-+    }
-+    @Deprecated
-+    public boolean setRespawnPosition(ResourceKey<Level> dimension, @Nullable BlockPos pos, float angle, boolean forced, boolean sendMessage, PlayerSpawnChangeEvent.Cause cause) {
-+        return this.setRespawnPosition(dimension, pos, angle, forced, sendMessage, cause == PlayerSpawnChangeEvent.Cause.RESET ?
-+            com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.PLAYER_RESPAWN : com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.valueOf(cause.name()));
-+    }
-+    public boolean setRespawnPosition(ResourceKey<Level> dimension, @Nullable BlockPos pos, float angle, boolean forced, boolean sendMessage, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause cause) {
-+        Location spawnLoc = null;
-+        boolean willNotify = false;
-         if (pos != null) {
-             boolean flag2 = pos.equals(this.respawnPosition) && dimension.equals(this.respawnDimension);
-+            spawnLoc = io.papermc.paper.util.MCUtil.toLocation(this.getServer().getLevel(dimension), pos);
-+            spawnLoc.setYaw(angle);
-+            willNotify = sendMessage && !flag2;
-+        }
- 
--            if (sendMessage && !flag2) {
--                this.sendSystemMessage(Component.translatable("block.minecraft.set_spawn"));
-+        PlayerSpawnChangeEvent dumbEvent = new PlayerSpawnChangeEvent(this.getBukkitEntity(), spawnLoc, forced,
-+            cause == com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.PLAYER_RESPAWN ? PlayerSpawnChangeEvent.Cause.RESET : PlayerSpawnChangeEvent.Cause.valueOf(cause.name()));
-+        dumbEvent.callEvent();
-+
-+        com.destroystokyo.paper.event.player.PlayerSetSpawnEvent event = new com.destroystokyo.paper.event.player.PlayerSetSpawnEvent(this.getBukkitEntity(), cause, dumbEvent.getNewSpawn(), dumbEvent.isForced(), willNotify, willNotify ? net.kyori.adventure.text.Component.translatable("block.minecraft.set_spawn") : null);
-+        event.setCancelled(dumbEvent.isCancelled());
-+        if (!event.callEvent()) {
-+            return false;
-+        }
-+        if (event.getLocation() != null) {
-+            dimension = event.getLocation().getWorld() != null ? ((CraftWorld) event.getLocation().getWorld()).getHandle().dimension() : dimension;
-+            pos = io.papermc.paper.util.MCUtil.toBlockPosition(event.getLocation());
-+            angle = event.getLocation().getYaw();
-+            forced = event.isForced();
-+            // Paper end - Add PlayerSetSpawnEvent
-+
-+            if (event.willNotifyPlayer() && event.getNotification() != null) { // Paper - Add PlayerSetSpawnEvent
-+                this.sendSystemMessage(PaperAdventure.asVanilla(event.getNotification())); // Paper - Add PlayerSetSpawnEvent
-             }
- 
-             this.respawnPosition = pos;
-@@ -2064,6 +2725,7 @@
-             this.respawnForced = false;
-         }
- 
-+        return true; // Paper - Add PlayerSetSpawnEvent
-     }
- 
-     public SectionPos getLastSectionPos() {
-@@ -2088,18 +2750,44 @@
-     }
- 
-     @Override
--    public ItemEntity drop(ItemStack stack, boolean throwRandomly, boolean retainOwnership) {
--        ItemEntity entityitem = this.createItemStackToDrop(stack, throwRandomly, retainOwnership);
-+    public ItemEntity drop(ItemStack itemstack, boolean flag, boolean flag1, boolean callEvent) { // CraftBukkit - SPIGOT-2942: Add boolean to call event
-+        ItemEntity entityitem = this.createItemStackToDrop(itemstack, flag, flag1);
- 
-         if (entityitem == null) {
-             return null;
-         } else {
-+            // CraftBukkit start - fire PlayerDropItemEvent
-+            if (callEvent) {
-+                Player player = (Player) this.getBukkitEntity();
-+                org.bukkit.entity.Item drop = (org.bukkit.entity.Item) entityitem.getBukkitEntity();
-+
-+                PlayerDropItemEvent event = new PlayerDropItemEvent(player, drop);
-+                this.level().getCraftServer().getPluginManager().callEvent(event);
-+
-+                if (event.isCancelled()) {
-+                    org.bukkit.inventory.ItemStack cur = player.getInventory().getItemInHand();
-+                    if (flag1 && (cur == null || cur.getAmount() == 0)) {
-+                        // The complete stack was dropped
-+                        player.getInventory().setItemInHand(drop.getItemStack());
-+                    } else if (flag1 && cur.isSimilar(drop.getItemStack()) && cur.getAmount() < cur.getMaxStackSize() && drop.getItemStack().getAmount() == 1) {
-+                        // Only one item is dropped
-+                        cur.setAmount(cur.getAmount() + 1);
-+                        player.getInventory().setItemInHand(cur);
-+                    } else {
-+                        // Fallback
-+                        player.getInventory().addItem(drop.getItemStack());
-+                    }
-+                    return null;
-+                }
-+            }
-+            // CraftBukkit end
-+
-             this.level().addFreshEntity(entityitem);
-             ItemStack itemstack1 = entityitem.getItem();
- 
--            if (retainOwnership) {
-+            if (flag1) {
-                 if (!itemstack1.isEmpty()) {
--                    this.awardStat(Stats.ITEM_DROPPED.get(itemstack1.getItem()), stack.getCount());
-+                    this.awardStat(Stats.ITEM_DROPPED.get(itemstack1.getItem()), itemstack1.getCount()); // Paper - Fix PlayerDropItemEvent using wrong item
-                 }
- 
-                 this.awardStat(Stats.DROP);
-@@ -2115,6 +2803,11 @@
-             return null;
-         } else {
-             double d0 = this.getEyeY() - 0.30000001192092896D;
-+            // Paper start
-+            ItemStack tmp = stack.copy();
-+            stack.setCount(0);
-+            stack = tmp;
-+            // Paper end
-             ItemEntity entityitem = new ItemEntity(this.level(), this.getX(), d0, this.getZ(), stack);
- 
-             entityitem.setPickUpDelay(40);
-@@ -2166,6 +2859,16 @@
-     }
- 
-     public void loadGameTypes(@Nullable CompoundTag nbt) {
-+        // Paper start - Expand PlayerGameModeChangeEvent
-+        if (this.server.getForcedGameType() != null && this.server.getForcedGameType() != ServerPlayer.readPlayerMode(nbt, "playerGameType")) {
-+            if (new org.bukkit.event.player.PlayerGameModeChangeEvent(this.getBukkitEntity(), org.bukkit.GameMode.getByValue(this.server.getDefaultGameType().getId()), org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.DEFAULT_GAMEMODE, null).callEvent()) {
-+                this.gameMode.setGameModeForPlayer(this.server.getForcedGameType(), GameType.DEFAULT_MODE);
-+            } else {
-+                this.gameMode.setGameModeForPlayer(ServerPlayer.readPlayerMode(nbt,"playerGameType"), ServerPlayer.readPlayerMode(nbt, "previousPlayerGameType"));
-+            }
-+            return;
-+        }
-+        // Paper end - Expand PlayerGameModeChangeEvent
-         this.gameMode.setGameModeForPlayer(this.calculateGameModeForNewPlayer(ServerPlayer.readPlayerMode(nbt, "playerGameType")), ServerPlayer.readPlayerMode(nbt, "previousPlayerGameType"));
-     }
- 
-@@ -2275,9 +2978,15 @@
- 
-     @Override
-     public void stopRiding() {
-+        // Paper start - Force entity dismount during teleportation
-+        this.stopRiding(false);
-+    }
-+    @Override
-+    public void stopRiding(boolean suppressCancellation) {
-+        // Paper end - Force entity dismount during teleportation
-         Entity entity = this.getVehicle();
- 
--        super.stopRiding();
-+        super.stopRiding(suppressCancellation); // Paper - Force entity dismount during teleportation
-         if (entity instanceof LivingEntity entityliving) {
-             Iterator iterator = entityliving.getActiveEffects().iterator();
- 
-@@ -2371,20 +3080,165 @@
-     }
- 
-     public static long placeEnderPearlTicket(ServerLevel world, ChunkPos chunkPos) {
--        world.getChunkSource().addRegionTicket(TicketType.ENDER_PEARL, chunkPos, 2, chunkPos);
-+        if (!world.paperConfig().misc.legacyEnderPearlBehavior) world.getChunkSource().addRegionTicket(TicketType.ENDER_PEARL, chunkPos, 2, chunkPos); // Paper - Allow using old ender pearl behavior
-         return TicketType.ENDER_PEARL.timeout();
-     }
- 
--    public static record RespawnPosAngle(Vec3 position, float yaw) {
-+    // CraftBukkit start
-+    public static record RespawnPosAngle(Vec3 position, float yaw, boolean isBedSpawn, boolean isAnchorSpawn) {
- 
--        public static ServerPlayer.RespawnPosAngle of(Vec3 respawnPos, BlockPos currentPos) {
--            return new ServerPlayer.RespawnPosAngle(respawnPos, calculateLookAtYaw(respawnPos, currentPos));
-+        public static ServerPlayer.RespawnPosAngle of(Vec3 vec3d, BlockPos blockposition, boolean isBedSpawn, boolean isAnchorSpawn) {
-+            return new ServerPlayer.RespawnPosAngle(vec3d, calculateLookAtYaw(vec3d, blockposition), isBedSpawn, isAnchorSpawn);
-+            // CraftBukkit end
-         }
- 
-         private static float calculateLookAtYaw(Vec3 respawnPos, BlockPos currentPos) {
-             Vec3 vec3d1 = Vec3.atBottomCenterOf(currentPos).subtract(respawnPos).normalize();
- 
-             return (float) Mth.wrapDegrees(Mth.atan2(vec3d1.z, vec3d1.x) * 57.2957763671875D - 90.0D);
-+        }
-+    }
-+
-+    // CraftBukkit start - Add per-player time and weather.
-+    public long timeOffset = 0;
-+    public boolean relativeTime = true;
-+
-+    public long getPlayerTime() {
-+        if (this.relativeTime) {
-+            // Adds timeOffset to the current server time.
-+            return this.level().getDayTime() + this.timeOffset;
-+        } else {
-+            // Adds timeOffset to the beginning of this day.
-+            return this.level().getDayTime() - (this.level().getDayTime() % 24000) + this.timeOffset;
-         }
-     }
-+
-+    public WeatherType weather = null;
-+
-+    public WeatherType getPlayerWeather() {
-+        return this.weather;
-+    }
-+
-+    public void setPlayerWeather(WeatherType type, boolean plugin) {
-+        if (!plugin && this.weather != null) {
-+            return;
-+        }
-+
-+        if (plugin) {
-+            this.weather = type;
-+        }
-+
-+        if (type == WeatherType.DOWNFALL) {
-+            this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.STOP_RAINING, 0));
-+        } else {
-+            this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0));
-+        }
-+    }
-+
-+    private float pluginRainPosition;
-+    private float pluginRainPositionPrevious;
-+
-+    public void updateWeather(float oldRain, float newRain, float oldThunder, float newThunder) {
-+        if (this.weather == null) {
-+            // Vanilla
-+            if (oldRain != newRain) {
-+                this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, newRain));
-+            }
-+        } else {
-+            // Plugin
-+            if (this.pluginRainPositionPrevious != this.pluginRainPosition) {
-+                this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, this.pluginRainPosition));
-+            }
-+        }
-+
-+        if (oldThunder != newThunder) {
-+            if (this.weather == WeatherType.DOWNFALL || this.weather == null) {
-+                this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, newThunder));
-+            } else {
-+                this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, 0));
-+            }
-+        }
-+    }
-+
-+    public void tickWeather() {
-+        if (this.weather == null) return;
-+
-+        this.pluginRainPositionPrevious = this.pluginRainPosition;
-+        if (this.weather == WeatherType.DOWNFALL) {
-+            this.pluginRainPosition += 0.01;
-+        } else {
-+            this.pluginRainPosition -= 0.01;
-+        }
-+
-+        this.pluginRainPosition = Mth.clamp(this.pluginRainPosition, 0.0F, 1.0F);
-+    }
-+
-+    public void resetPlayerWeather() {
-+        this.weather = null;
-+        this.setPlayerWeather(this.level().getLevelData().isRaining() ? WeatherType.DOWNFALL : WeatherType.CLEAR, false);
-+    }
-+
-+    @Override
-+    public String toString() {
-+        return super.toString() + "(" + this.getScoreboardName() + " at " + this.getX() + "," + this.getY() + "," + this.getZ() + ")";
-+    }
-+
-+    // SPIGOT-1903, MC-98153
-+    public void forceSetPositionRotation(double x, double y, double z, float yaw, float pitch) {
-+        this.moveTo(x, y, z, yaw, pitch);
-+        this.connection.resetPosition();
-+    }
-+
-+    @Override
-+    public boolean isImmobile() {
-+        return super.isImmobile() || (this.connection != null && this.connection.isDisconnected()); // Paper - Fix duplication bugs
-+    }
-+
-+    @Override
-+    public Scoreboard getScoreboard() {
-+        return this.getBukkitEntity().getScoreboard().getHandle();
-+    }
-+
-+    public void reset() {
-+        float exp = 0;
-+
-+        if (this.keepLevel) { // CraftBukkit - SPIGOT-6687: Only use keepLevel (was pre-set with RULE_KEEPINVENTORY value in PlayerDeathEvent)
-+            exp = this.experienceProgress;
-+            this.newTotalExp = this.totalExperience;
-+            this.newLevel = this.experienceLevel;
-+        }
-+
-+        this.setHealth(this.getMaxHealth());
-+        this.stopUsingItem(); // CraftBukkit - SPIGOT-6682: Clear active item on reset
-+        this.setAirSupply(this.getMaxAirSupply()); // Paper - Reset players airTicks on respawn
-+        this.setRemainingFireTicks(0);
-+        this.fallDistance = 0;
-+        this.foodData = new FoodData();
-+        this.experienceLevel = this.newLevel;
-+        this.totalExperience = this.newTotalExp;
-+        this.experienceProgress = 0;
-+        this.deathTime = 0;
-+        this.setArrowCount(0, true); // CraftBukkit - ArrowBodyCountChangeEvent
-+        this.removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.DEATH);
-+        this.effectsDirty = true;
-+        this.containerMenu = this.inventoryMenu;
-+        this.lastHurtByPlayer = null;
-+        this.lastHurtByMob = null;
-+        this.combatTracker = new CombatTracker(this);
-+        this.lastSentExp = -1;
-+        if (this.keepLevel) { // CraftBukkit - SPIGOT-6687: Only use keepLevel (was pre-set with RULE_KEEPINVENTORY value in PlayerDeathEvent)
-+            this.experienceProgress = exp;
-+        } else {
-+            this.giveExperiencePoints(this.newExp);
-+        }
-+        this.keepLevel = false;
-+        this.setDeltaMovement(0, 0, 0); // CraftBukkit - SPIGOT-6948: Reset velocity on death
-+        this.skipDropExperience = false; // CraftBukkit - SPIGOT-7462: Reset experience drop skip, so that further deaths drop xp
-+    }
-+
-+    @Override
-+    public CraftPlayer getBukkitEntity() {
-+        return (CraftPlayer) super.getBukkitEntity();
-+    }
-+    // CraftBukkit end
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/level/ServerPlayerGameMode.java.patch b/paper-server/patches/unapplied/net/minecraft/server/level/ServerPlayerGameMode.java.patch
deleted file mode 100644
index aba7ebeae1..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/level/ServerPlayerGameMode.java.patch
+++ /dev/null
@@ -1,463 +0,0 @@
---- a/net/minecraft/server/level/ServerPlayerGameMode.java
-+++ b/net/minecraft/server/level/ServerPlayerGameMode.java
-@@ -13,6 +13,7 @@
- import net.minecraft.world.InteractionResult;
- import net.minecraft.world.MenuProvider;
- import net.minecraft.world.entity.EquipmentSlot;
-+import net.minecraft.world.item.DoubleHighBlockItem;
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.item.context.UseOnContext;
- import net.minecraft.world.item.enchantment.EnchantmentHelper;
-@@ -20,12 +21,30 @@
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.block.Block;
- import net.minecraft.world.level.block.GameMasterBlock;
-+import net.minecraft.world.level.block.TrapDoorBlock;
- import net.minecraft.world.level.block.entity.BlockEntity;
- import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
- import net.minecraft.world.phys.BlockHitResult;
- import net.minecraft.world.phys.Vec3;
- import org.slf4j.Logger;
- 
-+// CraftBukkit start
-+import java.util.ArrayList;
-+import net.minecraft.server.MinecraftServer;
-+import net.minecraft.world.level.block.Blocks;
-+import net.minecraft.world.level.block.CakeBlock;
-+import net.minecraft.world.level.block.DoorBlock;
-+import org.bukkit.GameMode;
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.event.block.BlockBreakEvent;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.Event;
-+import org.bukkit.event.block.Action;
-+import org.bukkit.event.player.PlayerGameModeChangeEvent;
-+import org.bukkit.event.player.PlayerInteractEvent;
-+// CraftBukkit end
-+
- public class ServerPlayerGameMode {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
-@@ -42,6 +61,8 @@
-     private BlockPos delayedDestroyPos;
-     private int delayedTickStart;
-     private int lastSentState;
-+    public boolean captureSentBlockEntities = false; // Paper - Send block entities after destroy prediction
-+    public boolean capturedBlockEntity = false; // Paper - Send block entities after destroy prediction
- 
-     public ServerPlayerGameMode(ServerPlayer player) {
-         this.gameModeForPlayer = GameType.DEFAULT_MODE;
-@@ -53,18 +74,32 @@
-     }
- 
-     public boolean changeGameModeForPlayer(GameType gameMode) {
-+        // Paper start - Expand PlayerGameModeChangeEvent
-+        PlayerGameModeChangeEvent event = this.changeGameModeForPlayer(gameMode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.UNKNOWN, null);
-+        return event != null && event.isCancelled();
-+    }
-+    @Nullable
-+    public PlayerGameModeChangeEvent changeGameModeForPlayer(GameType gameMode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause cause, @Nullable net.kyori.adventure.text.Component cancelMessage) {
-+        // Paper end - Expand PlayerGameModeChangeEvent
-         if (gameMode == this.gameModeForPlayer) {
--            return false;
-+            return null; // Paper - Expand PlayerGameModeChangeEvent
-         } else {
--            this.setGameModeForPlayer(gameMode, this.previousGameModeForPlayer);
-+            // CraftBukkit start
-+            PlayerGameModeChangeEvent event = new PlayerGameModeChangeEvent(this.player.getBukkitEntity(), GameMode.getByValue(gameMode.getId()), cause, cancelMessage); // Paper
-+            this.level.getCraftServer().getPluginManager().callEvent(event);
-+            if (event.isCancelled()) {
-+                return event; // Paper - Expand PlayerGameModeChangeEvent
-+            }
-+            // CraftBukkit end
-+            this.setGameModeForPlayer(gameMode, this.gameModeForPlayer); // Paper - Fix MC-259571
-             this.player.onUpdateAbilities();
--            this.player.server.getPlayerList().broadcastAll(new ClientboundPlayerInfoUpdatePacket(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_GAME_MODE, this.player));
-+            this.player.server.getPlayerList().broadcastAll(new ClientboundPlayerInfoUpdatePacket(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_GAME_MODE, this.player), this.player); // CraftBukkit
-             this.level.updateSleepingPlayerList();
-             if (gameMode == GameType.CREATIVE) {
-                 this.player.resetCurrentImpulseContext();
-             }
- 
--            return true;
-+            return event; // Paper - Expand PlayerGameModeChangeEvent
-         }
-     }
- 
-@@ -92,12 +127,12 @@
-     }
- 
-     public void tick() {
--        ++this.gameTicks;
-+        this.gameTicks = MinecraftServer.currentTick; // CraftBukkit;
-         BlockState iblockdata;
- 
-         if (this.hasDelayedDestroy) {
--            iblockdata = this.level.getBlockState(this.delayedDestroyPos);
--            if (iblockdata.isAir()) {
-+            iblockdata = this.level.getBlockStateIfLoaded(this.delayedDestroyPos); // Paper - Don't allow digging into unloaded chunks
-+            if (iblockdata == null || iblockdata.isAir()) { // Paper - Don't allow digging into unloaded chunks
-                 this.hasDelayedDestroy = false;
-             } else {
-                 float f = this.incrementDestroyProgress(iblockdata, this.delayedDestroyPos, this.delayedTickStart);
-@@ -108,7 +143,13 @@
-                 }
-             }
-         } else if (this.isDestroyingBlock) {
--            iblockdata = this.level.getBlockState(this.destroyPos);
-+            // Paper start - Don't allow digging into unloaded chunks; don't want to do same logic as above, return instead
-+            iblockdata = this.level.getBlockStateIfLoaded(this.destroyPos);
-+            if (iblockdata == null) {
-+                this.isDestroyingBlock = false;
-+                return;
-+            }
-+            // Paper end - Don't allow digging into unloaded chunks
-             if (iblockdata.isAir()) {
-                 this.level.destroyBlockProgress(this.player.getId(), this.destroyPos, -1);
-                 this.lastSentState = -1;
-@@ -137,6 +178,7 @@
- 
-     public void handleBlockBreakAction(BlockPos pos, ServerboundPlayerActionPacket.Action action, Direction direction, int worldHeight, int sequence) {
-         if (!this.player.canInteractWithBlock(pos, 1.0D)) {
-+            if (true) return; // Paper - Don't allow digging into unloaded chunks; Don't notify if unreasonably far away
-             this.debugLogging(pos, false, sequence, "too far");
-         } else if (pos.getY() > worldHeight) {
-             this.player.connection.send(new ClientboundBlockUpdatePacket(pos, this.level.getBlockState(pos)));
-@@ -146,16 +188,40 @@
- 
-             if (action == ServerboundPlayerActionPacket.Action.START_DESTROY_BLOCK) {
-                 if (!this.level.mayInteract(this.player, pos)) {
-+                    // CraftBukkit start - fire PlayerInteractEvent
-+                    CraftEventFactory.callPlayerInteractEvent(this.player, Action.LEFT_CLICK_BLOCK, pos, direction, this.player.getInventory().getSelected(), InteractionHand.MAIN_HAND);
-                     this.player.connection.send(new ClientboundBlockUpdatePacket(pos, this.level.getBlockState(pos)));
-                     this.debugLogging(pos, false, sequence, "may not interact");
-+                    // Update any tile entity data for this block
-+                    capturedBlockEntity = true; // Paper - Send block entities after destroy prediction
-+                    // CraftBukkit end
-                     return;
-                 }
- 
-+                // CraftBukkit start
-+                PlayerInteractEvent event = CraftEventFactory.callPlayerInteractEvent(this.player, Action.LEFT_CLICK_BLOCK, pos, direction, this.player.getInventory().getSelected(), InteractionHand.MAIN_HAND);
-+                if (event.isCancelled()) {
-+                    // Let the client know the block still exists
-+                    // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); // Paper - Don't resync blocks
-+                    // Update any tile entity data for this block
-+                    capturedBlockEntity = true; // Paper - Send block entities after destroy prediction
-+                    return;
-+                }
-+                // CraftBukkit end
-+
-                 if (this.isCreative()) {
-                     this.destroyAndAck(pos, sequence, "creative destroy");
-                     return;
-                 }
- 
-+                // Spigot start - handle debug stick left click for non-creative
-+                if (this.player.getMainHandItem().is(net.minecraft.world.item.Items.DEBUG_STICK)
-+                        && ((net.minecraft.world.item.DebugStickItem) net.minecraft.world.item.Items.DEBUG_STICK).handleInteraction(this.player, this.level.getBlockState(pos), this.level, pos, false, this.player.getMainHandItem())) {
-+                    // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); // Paper - Don't resync block
-+                    return;
-+                }
-+                // Spigot end
-+
-                 if (this.player.blockActionRestricted(this.level, pos, this.gameModeForPlayer)) {
-                     this.player.connection.send(new ClientboundBlockUpdatePacket(pos, this.level.getBlockState(pos)));
-                     this.debugLogging(pos, false, sequence, "block action restricted");
-@@ -166,7 +232,21 @@
-                 float f = 1.0F;
- 
-                 iblockdata = this.level.getBlockState(pos);
--                if (!iblockdata.isAir()) {
-+                // CraftBukkit start - Swings at air do *NOT* exist.
-+                if (event.useInteractedBlock() == Event.Result.DENY) {
-+                    // If we denied a door from opening, we need to send a correcting update to the client, as it already opened the door.
-+                    // Paper start - Don't resync blocks
-+                    //BlockState data = this.level.getBlockState(pos);
-+                    //if (data.getBlock() instanceof DoorBlock) {
-+                    //    // For some reason *BOTH* the bottom/top part have to be marked updated.
-+                    //    boolean bottom = data.getValue(DoorBlock.HALF) == DoubleBlockHalf.LOWER;
-+                    //    this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos));
-+                    //    this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, bottom ? pos.above() : pos.below()));
-+                    //} else if (data.getBlock() instanceof TrapDoorBlock) {
-+                    //    this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos));
-+                    //}
-+                    // Paper end - Don't resync blocks
-+                } else if (!iblockdata.isAir()) {
-                     EnchantmentHelper.onHitBlock(this.level, this.player.getMainHandItem(), this.player, this.player, EquipmentSlot.MAINHAND, Vec3.atCenterOf(pos), iblockdata, (item) -> {
-                         this.player.onEquippedItemBroken(item, EquipmentSlot.MAINHAND);
-                     });
-@@ -174,6 +254,26 @@
-                     f = iblockdata.getDestroyProgress(this.player, this.player.level(), pos);
-                 }
- 
-+                if (event.useItemInHand() == Event.Result.DENY) {
-+                    // If we 'insta destroyed' then the client needs to be informed.
-+                    if (f > 1.0f) {
-+                        // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); // Paper - Don't resync blocks
-+                    }
-+                    return;
-+                }
-+                org.bukkit.event.block.BlockDamageEvent blockEvent = CraftEventFactory.callBlockDamageEvent(this.player, pos, direction, this.player.getInventory().getSelected(), f >= 1.0f); // Paper - Add BlockFace to BlockDamageEvent
-+
-+                if (blockEvent.isCancelled()) {
-+                    // Let the client know the block still exists
-+                    // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); // Paper - Don't resync block
-+                    return;
-+                }
-+
-+                if (blockEvent.getInstaBreak()) {
-+                    f = 2.0f;
-+                }
-+                // CraftBukkit end
-+
-                 if (!iblockdata.isAir() && f >= 1.0F) {
-                     this.destroyAndAck(pos, sequence, "insta mine");
-                 } else {
-@@ -217,14 +317,18 @@
-                 this.debugLogging(pos, true, sequence, "stopped destroying");
-             } else if (action == ServerboundPlayerActionPacket.Action.ABORT_DESTROY_BLOCK) {
-                 this.isDestroyingBlock = false;
--                if (!Objects.equals(this.destroyPos, pos)) {
--                    ServerPlayerGameMode.LOGGER.warn("Mismatch in destroy block pos: {} {}", this.destroyPos, pos);
--                    this.level.destroyBlockProgress(this.player.getId(), this.destroyPos, -1);
--                    this.debugLogging(pos, true, sequence, "aborted mismatched destroying");
-+                if (!Objects.equals(this.destroyPos, pos) && !BlockPos.ZERO.equals(this.destroyPos)) { // Paper
-+                    ServerPlayerGameMode.LOGGER.debug("Mismatch in destroy block pos: {} {}", this.destroyPos, pos); // CraftBukkit - SPIGOT-5457 sent by client when interact event cancelled
-+                    BlockState type = this.level.getBlockStateIfLoaded(this.destroyPos); // Paper - don't load unloaded chunks for stale records here
-+                    if (type != null) this.level.destroyBlockProgress(this.player.getId(), this.destroyPos, -1);
-+                    if (type != null) this.debugLogging(pos, true, sequence, "aborted mismatched destroying");
-+                    this.destroyPos = BlockPos.ZERO; // Paper
-                 }
- 
-                 this.level.destroyBlockProgress(this.player.getId(), pos, -1);
-                 this.debugLogging(pos, true, sequence, "aborted destroying");
-+
-+                CraftEventFactory.callBlockDamageAbortEvent(this.player, pos, this.player.getInventory().getSelected()); // CraftBukkit
-             }
- 
-         }
-@@ -242,19 +346,82 @@
- 
-     public boolean destroyBlock(BlockPos pos) {
-         BlockState iblockdata = this.level.getBlockState(pos);
-+        // CraftBukkit start - fire BlockBreakEvent
-+        org.bukkit.block.Block bblock = CraftBlock.at(this.level, pos);
-+        BlockBreakEvent event = null;
- 
--        if (!this.player.getMainHandItem().getItem().canAttackBlock(iblockdata, this.level, pos, this.player)) {
-+        if (this.player instanceof ServerPlayer) {
-+            // Sword + Creative mode pre-cancel
-+            boolean isSwordNoBreak = !this.player.getMainHandItem().getItem().canAttackBlock(iblockdata, this.level, pos, this.player);
-+
-+            // Tell client the block is gone immediately then process events
-+            // Don't tell the client if its a creative sword break because its not broken!
-+            if (false && this.level.getBlockEntity(pos) == null && !isSwordNoBreak) { // Paper - Don't resync block
-+                ClientboundBlockUpdatePacket packet = new ClientboundBlockUpdatePacket(pos, Blocks.AIR.defaultBlockState());
-+                this.player.connection.send(packet);
-+            }
-+
-+            event = new BlockBreakEvent(bblock, this.player.getBukkitEntity());
-+
-+            // Sword + Creative mode pre-cancel
-+            event.setCancelled(isSwordNoBreak);
-+
-+            // Calculate default block experience
-+            BlockState nmsData = this.level.getBlockState(pos);
-+            Block nmsBlock = nmsData.getBlock();
-+
-+            ItemStack itemstack = this.player.getItemBySlot(EquipmentSlot.MAINHAND);
-+
-+            if (nmsBlock != null && !event.isCancelled() && !this.isCreative() && this.player.hasCorrectToolForDrops(nmsBlock.defaultBlockState())) {
-+                event.setExpToDrop(nmsBlock.getExpDrop(nmsData, this.level, pos, itemstack, true));
-+            }
-+
-+            this.level.getCraftServer().getPluginManager().callEvent(event);
-+
-+            if (event.isCancelled()) {
-+                if (isSwordNoBreak) {
-+                    return false;
-+                }
-+                // Paper start - Don't resync blocks
-+                // Let the client know the block still exists
-+                //this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos));
-+
-+                // Brute force all possible updates
-+                //for (Direction dir : Direction.values()) {
-+                //    this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos.relative(dir)));
-+                //}
-+                // Paper end - Don't resync blocks
-+
-+                // Update any tile entity data for this block
-+                if (!captureSentBlockEntities) { // Paper - Send block entities after destroy prediction
-+                BlockEntity tileentity = this.level.getBlockEntity(pos);
-+                if (tileentity != null) {
-+                    this.player.connection.send(tileentity.getUpdatePacket());
-+                }
-+                } else {capturedBlockEntity = true;} // Paper - Send block entities after destroy prediction
-+                return false;
-+            }
-+        }
-+        // CraftBukkit end
-+
-+        if (false && !this.player.getMainHandItem().getItem().canAttackBlock(iblockdata, this.level, pos, this.player)) { // CraftBukkit - false
-             return false;
-         } else {
-+            iblockdata = this.level.getBlockState(pos); // CraftBukkit - update state from plugins
-+            if (iblockdata.isAir()) return false; // CraftBukkit - A plugin set block to air without cancelling
-             BlockEntity tileentity = this.level.getBlockEntity(pos);
-             Block block = iblockdata.getBlock();
- 
--            if (block instanceof GameMasterBlock && !this.player.canUseGameMasterBlocks()) {
-+            if (block instanceof GameMasterBlock && !this.player.canUseGameMasterBlocks() && !(block instanceof net.minecraft.world.level.block.CommandBlock && (this.player.isCreative() && this.player.getBukkitEntity().hasPermission("minecraft.commandblock")))) { // Paper - command block permission
-                 this.level.sendBlockUpdated(pos, iblockdata, iblockdata, 3);
-                 return false;
-             } else if (this.player.blockActionRestricted(this.level, pos, this.gameModeForPlayer)) {
-                 return false;
-             } else {
-+                // CraftBukkit start
-+                org.bukkit.block.BlockState state = bblock.getState();
-+                this.level.captureDrops = new ArrayList<>();
-+                // CraftBukkit end
-                 BlockState iblockdata1 = block.playerWillDestroy(this.level, pos, iblockdata, this.player);
-                 boolean flag = this.level.removeBlock(pos, false);
- 
-@@ -262,20 +429,46 @@
-                     block.destroy(this.level, pos, iblockdata1);
-                 }
- 
-+                ItemStack mainHandStack = null; // Paper - Trigger bee_nest_destroyed trigger in the correct place
-+                boolean isCorrectTool = false; // Paper - Trigger bee_nest_destroyed trigger in the correct place
-                 if (this.isCreative()) {
--                    return true;
-+                    // return true; // CraftBukkit
-                 } else {
-                     ItemStack itemstack = this.player.getMainHandItem();
-                     ItemStack itemstack1 = itemstack.copy();
-                     boolean flag1 = this.player.hasCorrectToolForDrops(iblockdata1);
-+                    mainHandStack = itemstack1; // Paper - Trigger bee_nest_destroyed trigger in the correct place
-+                    isCorrectTool = flag1; // Paper - Trigger bee_nest_destroyed trigger in the correct place
- 
-                     itemstack.mineBlock(this.level, iblockdata1, pos, this.player);
--                    if (flag && flag1) {
--                        block.playerDestroy(this.level, this.player, pos, iblockdata1, tileentity, itemstack1);
-+                    if (flag && flag1/* && event.isDropItems() */) { // CraftBukkit - Check if block should drop items // Paper - fix drops not preventing stats/food exhaustion
-+                        block.playerDestroy(this.level, this.player, pos, iblockdata1, tileentity, itemstack1, event.isDropItems(), false); // Paper - fix drops not preventing stats/food exhaustion
-                     }
- 
--                    return true;
-+                    // return true; // CraftBukkit
-                 }
-+                // CraftBukkit start
-+                java.util.List<net.minecraft.world.entity.item.ItemEntity> itemsToDrop = this.level.captureDrops; // Paper - capture all item additions to the world
-+                this.level.captureDrops = null; // Paper - capture all item additions to the world; Remove this earlier so that we can actually drop stuff
-+                if (event.isDropItems()) {
-+                    org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockDropItemEvent(bblock, state, this.player, itemsToDrop); // Paper - capture all item additions to the world
-+                }
-+                //this.level.captureDrops = null; // Paper - capture all item additions to the world; move up
-+
-+                // Drop event experience
-+                if (flag && event != null) {
-+                    iblockdata.getBlock().popExperience(this.level, pos, event.getExpToDrop(), this.player); // Paper
-+                }
-+                // Paper start - Trigger bee_nest_destroyed trigger in the correct place (check impls of block#playerDestroy)
-+                if (mainHandStack != null) {
-+                    if (flag && isCorrectTool && event.isDropItems() && block instanceof net.minecraft.world.level.block.BeehiveBlock && tileentity instanceof net.minecraft.world.level.block.entity.BeehiveBlockEntity beehiveBlockEntity) { // simulates the guard on block#playerDestroy above
-+                        CriteriaTriggers.BEE_NEST_DESTROYED.trigger(player, iblockdata, mainHandStack, beehiveBlockEntity.getOccupantCount());
-+                    }
-+                }
-+                // Paper end - Trigger bee_nest_destroyed trigger in the correct place
-+
-+                return true;
-+                // CraftBukkit end
-             }
-         }
-     }
-@@ -321,17 +514,63 @@
-         }
-     }
- 
-+    // CraftBukkit start - whole method
-+    public boolean interactResult = false;
-+    public boolean firedInteract = false;
-+    public BlockPos interactPosition;
-+    public InteractionHand interactHand;
-+    public ItemStack interactItemStack;
-     public InteractionResult useItemOn(ServerPlayer player, Level world, ItemStack stack, InteractionHand hand, BlockHitResult hitResult) {
-         BlockPos blockposition = hitResult.getBlockPos();
-         BlockState iblockdata = world.getBlockState(blockposition);
-+        boolean cancelledBlock = false;
-+        boolean cancelledItem = false; // Paper - correctly handle items on cooldown
- 
-         if (!iblockdata.getBlock().isEnabled(world.enabledFeatures())) {
-             return InteractionResult.FAIL;
-         } else if (this.gameModeForPlayer == GameType.SPECTATOR) {
-             MenuProvider itileinventory = iblockdata.getMenuProvider(world, blockposition);
-+            cancelledBlock = !(itileinventory instanceof MenuProvider);
-+        }
- 
--            if (itileinventory != null) {
--                player.openMenu(itileinventory);
-+        if (player.getCooldowns().isOnCooldown(stack)) {
-+            cancelledItem = true; // Paper - correctly handle items on cooldown
-+        }
-+
-+        PlayerInteractEvent event = CraftEventFactory.callPlayerInteractEvent(player, Action.RIGHT_CLICK_BLOCK, blockposition, hitResult.getDirection(), stack, cancelledBlock, cancelledItem, hand, hitResult.getLocation()); // Paper - correctly handle items on cooldown
-+        this.firedInteract = true;
-+        this.interactResult = event.useItemInHand() == Event.Result.DENY;
-+        this.interactPosition = blockposition.immutable();
-+        this.interactHand = hand;
-+        this.interactItemStack = stack.copy();
-+
-+        if (event.useInteractedBlock() == Event.Result.DENY) {
-+            // If we denied a door from opening, we need to send a correcting update to the client, as it already opened the door.
-+            if (iblockdata.getBlock() instanceof DoorBlock) {
-+                // Paper start - Don't resync blocks
-+                // boolean bottom = iblockdata.getValue(DoorBlock.HALF) == DoubleBlockHalf.LOWER;
-+                // player.connection.send(new ClientboundBlockUpdatePacket(world, bottom ? blockposition.above() : blockposition.below()));
-+                // Paper end - Don't resync blocks
-+            } else if (iblockdata.getBlock() instanceof CakeBlock) {
-+                player.getBukkitEntity().sendHealthUpdate(); // SPIGOT-1341 - reset health for cake
-+            } else if (this.interactItemStack.getItem() instanceof DoubleHighBlockItem) {
-+                // send a correcting update to the client, as it already placed the upper half of the bisected item
-+                //player.connection.send(new ClientboundBlockUpdatePacket(world, blockposition.relative(hitResult.getDirection()).above())); // Paper - Don't resync blocks
-+
-+                // send a correcting update to the client for the block above as well, this because of replaceable blocks (such as grass, sea grass etc)
-+                //player.connection.send(new ClientboundBlockUpdatePacket(world, blockposition.above())); // Paper - Don't resync blocks
-+            // Paper start - extend Player Interact cancellation // TODO: consider merging this into the extracted method
-+            } else if (iblockdata.is(Blocks.JIGSAW) || iblockdata.is(Blocks.STRUCTURE_BLOCK) || iblockdata.getBlock() instanceof net.minecraft.world.level.block.CommandBlock) {
-+                player.connection.send(new net.minecraft.network.protocol.game.ClientboundContainerClosePacket(this.player.containerMenu.containerId));
-+            }
-+            // 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);
-+
-+            if (itileinventory != null && player.openMenu(itileinventory).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
-                 return InteractionResult.CONSUME;
-             } else {
-                 return InteractionResult.PASS;
-@@ -359,7 +598,7 @@
-                 }
-             }
- 
--            if (!stack.isEmpty() && !player.getCooldowns().isOnCooldown(stack)) {
-+            if (!stack.isEmpty() && !this.interactResult) { // add !interactResult SPIGOT-764
-                 UseOnContext itemactioncontext = new UseOnContext(player, hand, hitResult);
- 
-                 if (this.isCreative()) {
-@@ -377,6 +616,11 @@
- 
-                 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/paper-server/patches/unapplied/net/minecraft/server/network/LegacyQueryHandler.java.patch b/paper-server/patches/unapplied/net/minecraft/server/network/LegacyQueryHandler.java.patch
deleted file mode 100644
index a4f704f9e9..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/network/LegacyQueryHandler.java.patch
+++ /dev/null
@@ -1,211 +0,0 @@
---- a/net/minecraft/server/network/LegacyQueryHandler.java
-+++ b/net/minecraft/server/network/LegacyQueryHandler.java
-@@ -15,6 +15,7 @@
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
-     private final ServerInfo server;
-+    private ByteBuf buf; // Paper
- 
-     public LegacyQueryHandler(ServerInfo server) {
-         this.server = server;
-@@ -23,6 +24,16 @@
-     public void channelRead(ChannelHandlerContext channelhandlercontext, Object object) {
-         ByteBuf bytebuf = (ByteBuf) object;
- 
-+        // Paper start - Make legacy ping handler more reliable
-+        if (this.buf != null) {
-+            try {
-+                readLegacy1_6(channelhandlercontext, bytebuf);
-+            } finally {
-+                bytebuf.release();
-+            }
-+            return;
-+        }
-+        // Paper end
-         bytebuf.markReaderIndex();
-         boolean flag = true;
- 
-@@ -34,11 +45,23 @@
- 
-                 SocketAddress socketaddress = channelhandlercontext.channel().remoteAddress();
-                 int i = bytebuf.readableBytes();
--                String s;
-+                String s = null; // Paper
-+                // org.bukkit.event.server.ServerListPingEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callServerListPingEvent(socketaddress, this.server.getMotd(), this.server.getPlayerCount(), this.server.getMaxPlayers()); // CraftBukkit // Paper
-+                com.destroystokyo.paper.event.server.PaperServerListPingEvent event; // Paper
- 
-                 if (i == 0) {
--                    LegacyQueryHandler.LOGGER.debug("Ping: (<1.3.x) from {}", socketaddress);
--                    s = LegacyQueryHandler.createVersion0Response(this.server);
-+                    LegacyQueryHandler.LOGGER.debug("Ping: (<1.3.x) from {}", net.minecraft.server.MinecraftServer.getServer().logIPs() ? socketaddress: "<ip address withheld>"); // Paper - Respect logIPs option
-+
-+                    // Paper start - Call PaperServerListPingEvent and use results
-+                    event = com.destroystokyo.paper.network.PaperLegacyStatusClient.processRequest(net.minecraft.server.MinecraftServer.getServer(), (java.net.InetSocketAddress) socketaddress, 39, null);
-+                    if (event == null) {
-+                        channelhandlercontext.close();
-+                        bytebuf.release();
-+                        flag = false;
-+                        return;
-+                    }
-+                    s = String.format(Locale.ROOT, "%s\u00a7%d\u00a7%d", com.destroystokyo.paper.network.PaperLegacyStatusClient.getUnformattedMotd(event), event.getNumPlayers(), event.getMaxPlayers());
-+                    // Paper end
-                     LegacyQueryHandler.sendFlushAndClose(channelhandlercontext, LegacyQueryHandler.createLegacyDisconnectPacket(channelhandlercontext.alloc(), s));
-                 } else {
-                     if (bytebuf.readUnsignedByte() != 1) {
-@@ -46,16 +69,35 @@
-                     }
- 
-                     if (bytebuf.isReadable()) {
--                        if (!LegacyQueryHandler.readCustomPayloadPacket(bytebuf)) {
--                            return;
-+                        // Paper start - Replace with improved version below
-+                        if (bytebuf.readUnsignedByte() != 250) {
-+                            s = this.readLegacy1_6(channelhandlercontext, bytebuf);
-+                            if (s == null) {
-+                                return;
-+                            }
-                         }
--
--                        LegacyQueryHandler.LOGGER.debug("Ping: (1.6) from {}", socketaddress);
-+                        // if (!LegacyQueryHandler.readCustomPayloadPacket(bytebuf)) {
-+                        //     return;
-+                        // }
-+                        //
-+                        // LegacyQueryHandler.LOGGER.debug("Ping: (1.6) from {}", socketaddress);
-+                        // Paper end
-                     } else {
--                        LegacyQueryHandler.LOGGER.debug("Ping: (1.4-1.5.x) from {}", socketaddress);
-+                        LegacyQueryHandler.LOGGER.debug("Ping: (1.4-1.5.x) from {}", net.minecraft.server.MinecraftServer.getServer().logIPs() ? socketaddress: "<ip address withheld>"); // Paper - Respect logIPs option
-                     }
- 
--                    s = LegacyQueryHandler.createVersion1Response(this.server);
-+                    if (s == null) {
-+                        // Paper start - Call PaperServerListPingEvent and use results
-+                        event = com.destroystokyo.paper.network.PaperLegacyStatusClient.processRequest(net.minecraft.server.MinecraftServer.getServer(), (java.net.InetSocketAddress) socketaddress, 127, null); // Paper
-+                        if (event == null) {
-+                            channelhandlercontext.close();
-+                            bytebuf.release();
-+                            flag = false;
-+                            return;
-+                        }
-+                        s = String.format(Locale.ROOT, "\u00a71\u0000%d\u0000%s\u0000%s\u0000%d\u0000%d", new Object[] { event.getProtocolVersion(), this.server.getServerVersion(), event.getMotd(), event.getNumPlayers(), event.getMaxPlayers()}); // CraftBukkit
-+                        // Paper end
-+                    }
-                     LegacyQueryHandler.sendFlushAndClose(channelhandlercontext, LegacyQueryHandler.createLegacyDisconnectPacket(channelhandlercontext.alloc(), s));
-                 }
- 
-@@ -106,14 +148,110 @@
-         }
-     }
- 
--    private static String createVersion0Response(ServerInfo server) {
--        return String.format(Locale.ROOT, "%s\u00a7%d\u00a7%d", server.getMotd(), server.getPlayerCount(), server.getMaxPlayers());
-+    // Paper start
-+    private static String readLegacyString(ByteBuf buf) {
-+        int size = buf.readShort() * Character.BYTES;
-+        if (!buf.isReadable(size)) {
-+            return null;
-+        }
-+
-+        String result = buf.toString(buf.readerIndex(), size, java.nio.charset.StandardCharsets.UTF_16BE);
-+        buf.skipBytes(size); // toString doesn't increase readerIndex automatically
-+        return result;
-     }
- 
--    private static String createVersion1Response(ServerInfo server) {
--        return String.format(Locale.ROOT, "\u00a71\u0000%d\u0000%s\u0000%s\u0000%d\u0000%d", 127, server.getServerVersion(), server.getMotd(), server.getPlayerCount(), server.getMaxPlayers());
-+    private String readLegacy1_6(ChannelHandlerContext ctx, ByteBuf part) {
-+        ByteBuf buf = this.buf;
-+
-+        if (buf == null) {
-+            this.buf = buf = ctx.alloc().buffer();
-+            buf.markReaderIndex();
-+        } else {
-+            buf.resetReaderIndex();
-+        }
-+
-+        buf.writeBytes(part);
-+
-+        if (!buf.isReadable(Short.BYTES + Short.BYTES + Byte.BYTES + Short.BYTES + Integer.BYTES)) {
-+            return null;
-+        }
-+
-+        String s = readLegacyString(buf);
-+        if (s == null) {
-+            return null;
-+        }
-+
-+        if (!s.equals("MC|PingHost")) {
-+            removeHandler(ctx);
-+            return null;
-+        }
-+
-+        if (!buf.isReadable(Short.BYTES) || !buf.isReadable(buf.readShort())) {
-+            return null;
-+        }
-+
-+        net.minecraft.server.MinecraftServer server = net.minecraft.server.MinecraftServer.getServer();
-+        int protocolVersion = buf.readByte();
-+        String host = readLegacyString(buf);
-+        if (host == null) {
-+            removeHandler(ctx);
-+            return null;
-+        }
-+        int port = buf.readInt();
-+
-+        if (buf.isReadable()) {
-+            removeHandler(ctx);
-+            return null;
-+        }
-+
-+        buf.release();
-+        this.buf = null;
-+
-+        LOGGER.debug("Ping: (1.6) from {}", net.minecraft.server.MinecraftServer.getServer().logIPs() ? ctx.channel().remoteAddress(): "<ip address withheld>"); // Paper - Respect logIPs option
-+
-+        java.net.InetSocketAddress virtualHost = com.destroystokyo.paper.network.PaperNetworkClient.prepareVirtualHost(host, port);
-+        com.destroystokyo.paper.event.server.PaperServerListPingEvent event = com.destroystokyo.paper.network.PaperLegacyStatusClient.processRequest(
-+                server, (java.net.InetSocketAddress) ctx.channel().remoteAddress(), protocolVersion, virtualHost);
-+        if (event == null) {
-+            ctx.close();
-+            return null;
-+        }
-+
-+        String response = String.format("\u00a71\u0000%d\u0000%s\u0000%s\u0000%d\u0000%d", event.getProtocolVersion(), event.getVersion(),
-+            com.destroystokyo.paper.network.PaperLegacyStatusClient.getMotd(event), event.getNumPlayers(), event.getMaxPlayers());
-+        return response;
-     }
- 
-+    private void removeHandler(ChannelHandlerContext ctx) {
-+        ByteBuf buf = this.buf;
-+        this.buf = null;
-+
-+        buf.resetReaderIndex();
-+        ctx.pipeline().remove(this);
-+        ctx.fireChannelRead(buf);
-+    }
-+
-+    @Override
-+    public void handlerRemoved(ChannelHandlerContext ctx) {
-+        if (this.buf != null) {
-+            this.buf.release();
-+            this.buf = null;
-+        }
-+    }
-+    // Paper end
-+
-+    // CraftBukkit start
-+    private static String createVersion0Response(ServerInfo serverinfo, org.bukkit.event.server.ServerListPingEvent event) {
-+        return String.format(Locale.ROOT, "%s\u00a7%d\u00a7%d", event.getMotd(), event.getNumPlayers(), event.getMaxPlayers());
-+        // CraftBukkit end
-+    }
-+
-+    // CraftBukkit start
-+    private static String createVersion1Response(ServerInfo serverinfo, org.bukkit.event.server.ServerListPingEvent event) {
-+        return String.format(Locale.ROOT, "\u00a71\u0000%d\u0000%s\u0000%s\u0000%d\u0000%d", 127, serverinfo.getServerVersion(), event.getMotd(), event.getNumPlayers(), event.getMaxPlayers());
-+        // CraftBukkit end
-+    }
-+
-     private static void sendFlushAndClose(ChannelHandlerContext context, ByteBuf buf) {
-         context.pipeline().firstContext().writeAndFlush(buf).addListener(ChannelFutureListener.CLOSE);
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java.patch b/paper-server/patches/unapplied/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java.patch
deleted file mode 100644
index 813b398d03..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java.patch
+++ /dev/null
@@ -1,89 +0,0 @@
---- a/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java
-+++ b/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java
-@@ -38,6 +38,11 @@
- import net.minecraft.world.flag.FeatureFlags;
- import org.slf4j.Logger;
- 
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.CraftServerLinks;
-+import org.bukkit.event.player.PlayerLinksSendEvent;
-+// CraftBukkit end
-+
- public class ServerConfigurationPacketListenerImpl extends ServerCommonPacketListenerImpl implements ServerConfigurationPacketListener, TickablePacketListener {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
-@@ -50,10 +55,12 @@
-     @Nullable
-     private SynchronizeRegistriesTask synchronizeRegistriesTask;
- 
--    public ServerConfigurationPacketListenerImpl(MinecraftServer server, Connection connection, CommonListenerCookie clientData) {
--        super(server, connection, clientData);
--        this.gameProfile = clientData.gameProfile();
--        this.clientInformation = clientData.clientInformation();
-+    // CraftBukkit start
-+    public ServerConfigurationPacketListenerImpl(MinecraftServer minecraftserver, Connection networkmanager, CommonListenerCookie commonlistenercookie, ServerPlayer player) {
-+        super(minecraftserver, networkmanager, commonlistenercookie, player);
-+        // CraftBukkit end
-+        this.gameProfile = commonlistenercookie.gameProfile();
-+        this.clientInformation = commonlistenercookie.clientInformation();
-     }
- 
-     @Override
-@@ -63,6 +70,10 @@
- 
-     @Override
-     public void onDisconnect(DisconnectionDetails info) {
-+        // Paper start - Debugging
-+        if (net.minecraft.server.MinecraftServer.getServer().isDebugging()) {
-+            ServerConfigurationPacketListenerImpl.LOGGER.info("{} lost connection: {}, while in configuration phase {}", this.gameProfile, info.reason().getString(), currentTask != null ? currentTask.type().id() : "null");
-+        } else // Paper end
-         ServerConfigurationPacketListenerImpl.LOGGER.info("{} lost connection: {}", this.gameProfile, info.reason().getString());
-         super.onDisconnect(info);
-     }
-@@ -75,6 +86,12 @@
-     public void startConfiguration() {
-         this.send(new ClientboundCustomPayloadPacket(new BrandPayload(this.server.getServerModName())));
-         ServerLinks serverlinks = this.server.serverLinks();
-+        // CraftBukkit start
-+        CraftServerLinks wrapper = new CraftServerLinks(serverlinks);
-+        PlayerLinksSendEvent event = new PlayerLinksSendEvent(this.player.getBukkitEntity(), wrapper);
-+        this.player.getBukkitEntity().getServer().getPluginManager().callEvent(event);
-+        serverlinks = wrapper.getServerLinks();
-+        // CraftBukkit end
- 
-         if (!serverlinks.isEmpty()) {
-             this.send(new ClientboundServerLinksPacket(serverlinks.untrust()));
-@@ -107,6 +124,7 @@
-     @Override
-     public void handleClientInformation(ServerboundClientInformationPacket packet) {
-         this.clientInformation = packet.information();
-+        this.connection.channel.attr(io.papermc.paper.adventure.PaperAdventure.LOCALE_ATTRIBUTE).set(net.kyori.adventure.translation.Translator.parseLocale(packet.information().language())); // Paper
-     }
- 
-     @Override
-@@ -143,18 +161,23 @@
-                 return;
-             }
- 
--            Component ichatbasecomponent = playerlist.canPlayerLogin(this.connection.getRemoteAddress(), this.gameProfile);
-+            Component ichatbasecomponent = null; // CraftBukkit - login checks already completed
- 
-             if (ichatbasecomponent != null) {
-                 this.disconnect(ichatbasecomponent);
-                 return;
-             }
- 
--            ServerPlayer entityplayer = playerlist.getPlayerForLogin(this.gameProfile, this.clientInformation);
-+            ServerPlayer entityplayer = playerlist.getPlayerForLogin(this.gameProfile, this.clientInformation, this.player); // CraftBukkit
- 
-             playerlist.placeNewPlayer(this.connection, entityplayer, this.createCookie(this.clientInformation));
-         } catch (Exception exception) {
-             ServerConfigurationPacketListenerImpl.LOGGER.error("Couldn't place player in world", exception);
-+            // Paper start - Debugging
-+            if (MinecraftServer.getServer().isDebugging()) {
-+                exception.printStackTrace();
-+            }
-+            // Paper end - Debugging
-             this.connection.send(new ClientboundDisconnectPacket(ServerConfigurationPacketListenerImpl.DISCONNECT_REASON_INVALID_DATA));
-             this.connection.disconnect(ServerConfigurationPacketListenerImpl.DISCONNECT_REASON_INVALID_DATA);
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/network/ServerConnectionListener.java.patch b/paper-server/patches/unapplied/net/minecraft/server/network/ServerConnectionListener.java.patch
deleted file mode 100644
index adc3cc15f4..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/network/ServerConnectionListener.java.patch
+++ /dev/null
@@ -1,156 +0,0 @@
---- a/net/minecraft/server/network/ServerConnectionListener.java
-+++ b/net/minecraft/server/network/ServerConnectionListener.java
-@@ -52,22 +52,36 @@
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
-     public static final Supplier<NioEventLoopGroup> SERVER_EVENT_GROUP = Suppliers.memoize(() -> {
--        return new NioEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Server IO #%d").setDaemon(true).build());
-+        return new NioEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Server IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper
-     });
-     public static final Supplier<EpollEventLoopGroup> SERVER_EPOLL_EVENT_GROUP = Suppliers.memoize(() -> {
--        return new EpollEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Epoll Server IO #%d").setDaemon(true).build());
-+        return new EpollEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Epoll Server IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper
-     });
-     final MinecraftServer server;
-     public volatile boolean running;
-     private final List<ChannelFuture> channels = Collections.synchronizedList(Lists.newArrayList());
-     final List<Connection> connections = Collections.synchronizedList(Lists.newArrayList());
-+    // Paper start - prevent blocking on adding a new connection while the server is ticking
-+    private final java.util.Queue<Connection> pending = new java.util.concurrent.ConcurrentLinkedQueue<>();
-+    private final void addPending() {
-+        Connection connection;
-+        while ((connection = pending.poll()) != null) {
-+            connections.add(connection);
-+        }
-+    }
-+    // Paper end - prevent blocking on adding a new connection while the server is ticking
- 
-     public ServerConnectionListener(MinecraftServer server) {
-         this.server = server;
-         this.running = true;
-     }
- 
-+    // Paper start - Unix domain socket support
-     public void startTcpServerListener(@Nullable InetAddress address, int port) throws IOException {
-+        bind(new java.net.InetSocketAddress(address, port));
-+    }
-+    public void bind(java.net.SocketAddress address) throws IOException {
-+    // Paper end - Unix domain socket support
-         List list = this.channels;
- 
-         synchronized (this.channels) {
-@@ -75,7 +89,13 @@
-             EventLoopGroup eventloopgroup;
- 
-             if (Epoll.isAvailable() && this.server.isEpollEnabled()) {
-+                // Paper start - Unix domain socket support
-+                if (address instanceof io.netty.channel.unix.DomainSocketAddress) {
-+                    oclass = io.netty.channel.epoll.EpollServerDomainSocketChannel.class;
-+                } else {
-                 oclass = EpollServerSocketChannel.class;
-+                }
-+                // Paper end - Unix domain socket support
-                 eventloopgroup = (EventLoopGroup) ServerConnectionListener.SERVER_EPOLL_EVENT_GROUP.get();
-                 ServerConnectionListener.LOGGER.info("Using epoll channel type");
-             } else {
-@@ -84,6 +104,12 @@
-                 ServerConnectionListener.LOGGER.info("Using default channel type");
-             }
- 
-+            // Paper start - Warn people with console access that HAProxy is in use.
-+            if (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.proxyProtocol) {
-+                ServerConnectionListener.LOGGER.warn("Using HAProxy, please ensure the server port is adequately firewalled.");
-+            }
-+            // Paper end - Warn people with console access that HAProxy is in use.
-+
-             this.channels.add(((ServerBootstrap) ((ServerBootstrap) (new ServerBootstrap()).channel(oclass)).childHandler(new ChannelInitializer<Channel>() {
-                 protected void initChannel(Channel channel) {
-                     try {
-@@ -100,16 +126,58 @@
- 
-                     Connection.configureSerialization(channelpipeline, PacketFlow.SERVERBOUND, false, (BandwidthDebugMonitor) null);
-                     int j = ServerConnectionListener.this.server.getRateLimitPacketsPerSecond();
--                    Object object = j > 0 ? new RateKickingConnection(j) : new Connection(PacketFlow.SERVERBOUND);
-+                    Connection object = j > 0 ? new RateKickingConnection(j) : new Connection(PacketFlow.SERVERBOUND); // CraftBukkit - decompile error
- 
--                    ServerConnectionListener.this.connections.add(object);
-+                    //ServerConnectionListener.this.connections.add(object); // Paper
-+                    // Paper start - Add support for Proxy Protocol
-+                    if (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.proxyProtocol) {
-+                        channel.pipeline().addAfter("timeout", "haproxy-decoder", new io.netty.handler.codec.haproxy.HAProxyMessageDecoder());
-+                        channel.pipeline().addAfter("haproxy-decoder", "haproxy-handler", new ChannelInboundHandlerAdapter() {
-+                            @Override
-+                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
-+                                if (msg instanceof io.netty.handler.codec.haproxy.HAProxyMessage message) {
-+                                    if (message.command() == io.netty.handler.codec.haproxy.HAProxyCommand.PROXY) {
-+                                        String realaddress = message.sourceAddress();
-+                                        int realport = message.sourcePort();
-+
-+                                        SocketAddress socketaddr = new java.net.InetSocketAddress(realaddress, realport);
-+
-+                                        Connection connection = (Connection) channel.pipeline().get("packet_handler");
-+                                        connection.address = socketaddr;
-+
-+                                        // Paper start - Add API to get player's proxy address
-+                                        final String proxyAddress = message.destinationAddress();
-+                                        final int proxyPort = message.destinationPort();
-+
-+                                        connection.haProxyAddress = new java.net.InetSocketAddress(proxyAddress, proxyPort);
-+                                        // Paper end - Add API to get player's proxy address
-+                                    }
-+                                } else {
-+                                    super.channelRead(ctx, msg);
-+                                }
-+                            }
-+                        });
-+                    }
-+                    // Paper end - Add support for proxy protocol
-+                    pending.add(object); // Paper - prevent blocking on adding a new connection while the server is ticking
-                     ((Connection) object).configurePacketHandler(channelpipeline);
-                     ((Connection) object).setListenerForServerboundHandshake(new ServerHandshakePacketListenerImpl(ServerConnectionListener.this.server, (Connection) object));
-+                    io.papermc.paper.network.ChannelInitializeListenerHolder.callListeners(channel); // Paper - Add Channel initialization listeners
-                 }
--            }).group(eventloopgroup).localAddress(address, port)).bind().syncUninterruptibly());
-+            }).group(eventloopgroup).localAddress(address)).option(ChannelOption.AUTO_READ, false).bind().syncUninterruptibly()); // CraftBukkit // Paper - Unix domain socket support
-         }
-     }
- 
-+    // CraftBukkit start
-+    public void acceptConnections() {
-+        synchronized (this.channels) {
-+            for (ChannelFuture future : this.channels) {
-+                future.channel().config().setAutoRead(true);
-+            }
-+        }
-+    }
-+    // CraftBukkit end
-+
-     public SocketAddress startMemoryChannel() {
-         List list = this.channels;
-         ChannelFuture channelfuture;
-@@ -153,6 +221,14 @@
-         List list = this.connections;
- 
-         synchronized (this.connections) {
-+            // Spigot Start
-+            this.addPending(); // Paper - prevent blocking on adding a new connection while the server is ticking
-+            // This prevents players from 'gaming' the server, and strategically relogging to increase their position in the tick order
-+            if ( org.spigotmc.SpigotConfig.playerShuffle > 0 && MinecraftServer.currentTick % org.spigotmc.SpigotConfig.playerShuffle == 0 )
-+            {
-+                Collections.shuffle( this.connections );
-+            }
-+            // Spigot End
-             Iterator<Connection> iterator = this.connections.iterator();
- 
-             while (iterator.hasNext()) {
-@@ -176,6 +252,10 @@
-                             networkmanager.setReadOnly();
-                         }
-                     } else {
-+                        // Spigot Start
-+                        // Fix a race condition where a NetworkManager could be unregistered just before connection.
-+                        if (networkmanager.preparing) continue;
-+                        // Spigot End
-                         iterator.remove();
-                         networkmanager.handleDisconnection();
-                     }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/network/ServerStatusPacketListenerImpl.java.patch b/paper-server/patches/unapplied/net/minecraft/server/network/ServerStatusPacketListenerImpl.java.patch
deleted file mode 100644
index 7ea7e46622..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/network/ServerStatusPacketListenerImpl.java.patch
+++ /dev/null
@@ -1,137 +0,0 @@
---- a/net/minecraft/server/network/ServerStatusPacketListenerImpl.java
-+++ b/net/minecraft/server/network/ServerStatusPacketListenerImpl.java
-@@ -9,6 +9,19 @@
- import net.minecraft.network.protocol.status.ServerStatus;
- import net.minecraft.network.protocol.status.ServerStatusPacketListener;
- import net.minecraft.network.protocol.status.ServerboundStatusRequestPacket;
-+// CraftBukkit start
-+import com.mojang.authlib.GameProfile;
-+import java.net.InetSocketAddress;
-+import java.util.Collections;
-+import java.util.Iterator;
-+import java.util.Optional;
-+import net.minecraft.SharedConstants;
-+import net.minecraft.server.MinecraftServer;
-+import net.minecraft.server.level.ServerPlayer;
-+import org.bukkit.craftbukkit.util.CraftChatMessage;
-+import org.bukkit.craftbukkit.util.CraftIconCache;
-+import org.bukkit.entity.Player;
-+// CraftBukkit end
- 
- public class ServerStatusPacketListenerImpl implements ServerStatusPacketListener {
- 
-@@ -36,7 +49,113 @@
-             this.connection.disconnect(ServerStatusPacketListenerImpl.DISCONNECT_REASON);
-         } else {
-             this.hasRequestedStatus = true;
--            this.connection.send(new ClientboundStatusResponsePacket(this.status));
-+            // Paper start - Replace everything
-+            /*
-+            // CraftBukkit start
-+            // this.connection.send(new PacketStatusOutServerInfo(this.status));
-+            MinecraftServer server = MinecraftServer.getServer();
-+            final Object[] players = server.getPlayerList().players.toArray();
-+            class ServerListPingEvent extends org.bukkit.event.server.ServerListPingEvent {
-+
-+                CraftIconCache icon = server.server.getServerIcon();
-+
-+                ServerListPingEvent() {
-+                    super(ServerStatusPacketListenerImpl.this.connection.hostname, ((InetSocketAddress) ServerStatusPacketListenerImpl.this.connection.getRemoteAddress()).getAddress(), server.server.motd(), server.getPlayerList().getMaxPlayers()); // Paper - Adventure
-+                }
-+
-+                @Override
-+                public void setServerIcon(org.bukkit.util.CachedServerIcon icon) {
-+                    if (!(icon instanceof CraftIconCache)) {
-+                        throw new IllegalArgumentException(icon + " was not created by " + org.bukkit.craftbukkit.CraftServer.class);
-+                    }
-+                    this.icon = (CraftIconCache) icon;
-+                }
-+
-+                @Override
-+                public Iterator<Player> iterator() throws UnsupportedOperationException {
-+                    return new Iterator<Player>() {
-+                        int i;
-+                        int ret = Integer.MIN_VALUE;
-+                        ServerPlayer player;
-+
-+                        @Override
-+                        public boolean hasNext() {
-+                            if (this.player != null) {
-+                                return true;
-+                            }
-+                            final Object[] currentPlayers = players;
-+                            for (int length = currentPlayers.length, i = this.i; i < length; i++) {
-+                                final ServerPlayer player = (ServerPlayer) currentPlayers[i];
-+                                if (player != null) {
-+                                    this.i = i + 1;
-+                                    this.player = player;
-+                                    return true;
-+                                }
-+                            }
-+                            return false;
-+                        }
-+
-+                        @Override
-+                        public Player next() {
-+                            if (!this.hasNext()) {
-+                                throw new java.util.NoSuchElementException();
-+                            }
-+                            final ServerPlayer player = this.player;
-+                            this.player = null;
-+                            this.ret = this.i - 1;
-+                            return player.getBukkitEntity();
-+                        }
-+
-+                        @Override
-+                        public void remove() {
-+                            final Object[] currentPlayers = players;
-+                            final int i = this.ret;
-+                            if (i < 0 || currentPlayers[i] == null) {
-+                                throw new IllegalStateException();
-+                            }
-+                            currentPlayers[i] = null;
-+                        }
-+                    };
-+                }
-+            }
-+
-+            ServerListPingEvent event = new ServerListPingEvent();
-+            server.server.getPluginManager().callEvent(event);
-+
-+            java.util.List<GameProfile> profiles = new java.util.ArrayList<GameProfile>(players.length);
-+            for (Object player : players) {
-+                if (player != null) {
-+                    ServerPlayer entityPlayer = ((ServerPlayer) player);
-+                    if (entityPlayer.allowsListing()) {
-+                        profiles.add(entityPlayer.getGameProfile());
-+                    } else {
-+                        profiles.add(MinecraftServer.ANONYMOUS_PLAYER_PROFILE);
-+                    }
-+                }
-+            }
-+
-+            // Spigot Start
-+            if ( !server.hidesOnlinePlayers() && !profiles.isEmpty() )
-+            {
-+                java.util.Collections.shuffle( profiles ); // This sucks, its inefficient but we have no simple way of doing it differently
-+                profiles = profiles.subList( 0, Math.min( profiles.size(), org.spigotmc.SpigotConfig.playerSample ) ); // Cap the sample to n (or less) displayed players, ie: Vanilla behaviour
-+            }
-+            // Spigot End
-+            ServerStatus.Players playerSample = new ServerStatus.Players(event.getMaxPlayers(), event.getNumPlayers(), (server.hidesOnlinePlayers()) ? Collections.emptyList() : profiles);
-+
-+            ServerStatus ping = new ServerStatus(
-+                    CraftChatMessage.fromString(event.getMotd(), true)[0],
-+                    Optional.of(playerSample),
-+                    Optional.of(new ServerStatus.Version(server.getServerModName() + " " + server.getServerVersion(), SharedConstants.getCurrentVersion().getProtocolVersion())),
-+                    (event.icon.value != null) ? Optional.of(new ServerStatus.Favicon(event.icon.value)) : Optional.empty(),
-+                    server.enforceSecureProfile()
-+            );
-+
-+            this.connection.send(new ClientboundStatusResponsePacket(ping));
-+            // CraftBukkit end
-+            */
-+            com.destroystokyo.paper.network.StandardPaperServerListPingEventImpl.processRequest(MinecraftServer.getServer(), this.connection);
-+            // Paper end
-         }
-     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/server/players/GameProfileCache.java.patch b/paper-server/patches/unapplied/net/minecraft/server/players/GameProfileCache.java.patch
deleted file mode 100644
index 8b31e2142b..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/players/GameProfileCache.java.patch
+++ /dev/null
@@ -1,217 +0,0 @@
---- a/net/minecraft/server/players/GameProfileCache.java
-+++ b/net/minecraft/server/players/GameProfileCache.java
-@@ -60,6 +60,11 @@
-     @Nullable
-     private Executor executor;
- 
-+    // Paper start - Fix GameProfileCache concurrency
-+    protected final java.util.concurrent.locks.ReentrantLock stateLock = new java.util.concurrent.locks.ReentrantLock();
-+    protected final java.util.concurrent.locks.ReentrantLock lookupLock = new java.util.concurrent.locks.ReentrantLock();
-+    // Paper end - Fix GameProfileCache concurrency
-+
-     public GameProfileCache(GameProfileRepository profileRepository, File cacheFile) {
-         this.profileRepository = profileRepository;
-         this.file = cacheFile;
-@@ -67,11 +72,13 @@
-     }
- 
-     private void safeAdd(GameProfileCache.GameProfileInfo entry) {
-+        try { this.stateLock.lock(); // Paper - Fix GameProfileCache concurrency
-         GameProfile gameprofile = entry.getProfile();
- 
-         entry.setLastAccess(this.getNextOperation());
-         this.profilesByName.put(gameprofile.getName().toLowerCase(Locale.ROOT), entry);
-         this.profilesByUUID.put(gameprofile.getId(), entry);
-+        } finally { this.stateLock.unlock(); } // Paper - Fix GameProfileCache concurrency
-     }
- 
-     private static Optional<GameProfile> lookupGameProfile(GameProfileRepository repository, String name) {
-@@ -85,10 +92,12 @@
-                 }
- 
-                 public void onProfileLookupFailed(String s1, Exception exception) {
--                    atomicreference.set((Object) null);
-+                    atomicreference.set(null); // CraftBukkit - decompile error
-                 }
-             };
- 
-+        if (!org.apache.commons.lang3.StringUtils.isBlank(name) // Paper - Don't lookup a profile with a blank name
-+                && io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode()) // Paper - Add setting for proxy online mode status
-             repository.findProfilesByNames(new String[]{name}, profilelookupcallback);
-             GameProfile gameprofile = (GameProfile) atomicreference.get();
- 
-@@ -105,7 +114,7 @@
-     }
- 
-     private static boolean usesAuthentication() {
--        return GameProfileCache.usesAuthentication;
-+        return io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode(); // Paper - Add setting for proxy online mode status
-     }
- 
-     public void add(GameProfile profile) {
-@@ -117,15 +126,29 @@
-         GameProfileCache.GameProfileInfo usercache_usercacheentry = new GameProfileCache.GameProfileInfo(profile, date);
- 
-         this.safeAdd(usercache_usercacheentry);
--        this.save();
-+        if( !org.spigotmc.SpigotConfig.saveUserCacheOnStopOnly ) this.save(true); // Spigot - skip saving if disabled // Paper - Perf: Async GameProfileCache saving
-     }
- 
-     private long getNextOperation() {
-         return this.operationCount.incrementAndGet();
-     }
- 
-+    // Paper start
-+    public @Nullable GameProfile getProfileIfCached(String name) {
-+        try { this.stateLock.lock(); // Paper - Fix GameProfileCache concurrency
-+        GameProfileCache.GameProfileInfo entry = this.profilesByName.get(name.toLowerCase(Locale.ROOT));
-+        if (entry == null) {
-+            return null;
-+        }
-+        entry.setLastAccess(this.getNextOperation());
-+        return entry.getProfile();
-+        } finally { this.stateLock.unlock(); } // Paper - Fix GameProfileCache concurrency
-+    }
-+    // Paper end
-+
-     public Optional<GameProfile> get(String name) {
-         String s1 = name.toLowerCase(Locale.ROOT);
-+        boolean stateLocked = true; try { this.stateLock.lock(); // Paper - Fix GameProfileCache concurrency
-         GameProfileCache.GameProfileInfo usercache_usercacheentry = (GameProfileCache.GameProfileInfo) this.profilesByName.get(s1);
-         boolean flag = false;
- 
-@@ -141,19 +164,24 @@
-         if (usercache_usercacheentry != null) {
-             usercache_usercacheentry.setLastAccess(this.getNextOperation());
-             optional = Optional.of(usercache_usercacheentry.getProfile());
-+            stateLocked = false; this.stateLock.unlock(); // Paper - Fix GameProfileCache concurrency
-         } else {
--            optional = GameProfileCache.lookupGameProfile(this.profileRepository, s1);
-+            stateLocked = false; this.stateLock.unlock(); // Paper - Fix GameProfileCache concurrency
-+            try { this.lookupLock.lock(); // Paper - Fix GameProfileCache concurrency
-+            optional = GameProfileCache.lookupGameProfile(this.profileRepository, name); // CraftBukkit - use correct case for offline players
-+            } finally { this.lookupLock.unlock(); } // Paper - Fix GameProfileCache concurrency
-             if (optional.isPresent()) {
-                 this.add((GameProfile) optional.get());
-                 flag = false;
-             }
-         }
- 
--        if (flag) {
--            this.save();
-+        if (flag && !org.spigotmc.SpigotConfig.saveUserCacheOnStopOnly) { // Spigot - skip saving if disabled
-+            this.save(true); // Paper - Perf: Async GameProfileCache saving
-         }
- 
-         return optional;
-+        } finally { if (stateLocked) {  this.stateLock.unlock(); } } // Paper - Fix GameProfileCache concurrency
-     }
- 
-     public CompletableFuture<Optional<GameProfile>> getAsync(String username) {
-@@ -167,7 +195,7 @@
-             } else {
-                 CompletableFuture<Optional<GameProfile>> completablefuture1 = CompletableFuture.supplyAsync(() -> {
-                     return this.get(username);
--                }, Util.backgroundExecutor().forName("getProfile")).whenCompleteAsync((optional, throwable) -> {
-+                }, Util.PROFILE_EXECUTOR).whenCompleteAsync((optional, throwable) -> { // Paper - don't submit BLOCKING PROFILE LOOKUPS to the world gen thread
-                     this.requests.remove(username);
-                 }, this.executor);
- 
-@@ -178,6 +206,7 @@
-     }
- 
-     public Optional<GameProfile> get(UUID uuid) {
-+        try { this.stateLock.lock(); // Paper - Fix GameProfileCache concurrency
-         GameProfileCache.GameProfileInfo usercache_usercacheentry = (GameProfileCache.GameProfileInfo) this.profilesByUUID.get(uuid);
- 
-         if (usercache_usercacheentry == null) {
-@@ -186,6 +215,7 @@
-             usercache_usercacheentry.setLastAccess(this.getNextOperation());
-             return Optional.of(usercache_usercacheentry.getProfile());
-         }
-+        } finally { this.stateLock.unlock(); } // Paper - Fix GameProfileCache concurrency
-     }
- 
-     public void setExecutor(Executor executor) {
-@@ -208,7 +238,7 @@
- 
-             label54:
-             {
--                ArrayList arraylist;
-+                List<GameProfileCache.GameProfileInfo> arraylist; // CraftBukkit - decompile error
- 
-                 try {
-                     JsonArray jsonarray = (JsonArray) this.gson.fromJson(bufferedreader, JsonArray.class);
-@@ -217,7 +247,7 @@
-                         DateFormat dateformat = GameProfileCache.createDateFormat();
- 
-                         jsonarray.forEach((jsonelement) -> {
--                            Optional optional = GameProfileCache.readGameProfile(jsonelement, dateformat);
-+                            Optional<GameProfileCache.GameProfileInfo> optional = GameProfileCache.readGameProfile(jsonelement, dateformat); // CraftBukkit - decompile error
- 
-                             Objects.requireNonNull(list);
-                             optional.ifPresent(list::add);
-@@ -250,6 +280,11 @@
-             }
-         } catch (FileNotFoundException filenotfoundexception) {
-             ;
-+        // Spigot Start
-+        } catch (com.google.gson.JsonSyntaxException | NullPointerException ex) {
-+            GameProfileCache.LOGGER.warn( "Usercache.json is corrupted or has bad formatting. Deleting it to prevent further issues." );
-+            this.file.delete();
-+        // Spigot End
-         } catch (JsonParseException | IOException ioexception) {
-             GameProfileCache.LOGGER.warn("Failed to load profile cache {}", this.file, ioexception);
-         }
-@@ -257,14 +292,15 @@
-         return list;
-     }
- 
--    public void save() {
-+    public void save(boolean asyncSave) { // Paper - Perf: Async GameProfileCache saving
-         JsonArray jsonarray = new JsonArray();
-         DateFormat dateformat = GameProfileCache.createDateFormat();
- 
--        this.getTopMRUProfiles(1000).forEach((usercache_usercacheentry) -> {
-+        this.listTopMRUProfiles(org.spigotmc.SpigotConfig.userCacheCap).forEach((usercache_usercacheentry) -> { // Spigot // Paper - Fix GameProfileCache concurrency
-             jsonarray.add(GameProfileCache.writeGameProfile(usercache_usercacheentry, dateformat));
-         });
-         String s = this.gson.toJson(jsonarray);
-+        Runnable save = () -> { // Paper - Perf: Async GameProfileCache saving
- 
-         try {
-             BufferedWriter bufferedwriter = Files.newWriter(this.file, StandardCharsets.UTF_8);
-@@ -289,13 +325,32 @@
-         } catch (IOException ioexception) {
-             ;
-         }
-+        // Paper start - Perf: Async GameProfileCache saving
-+        };
-+        if (asyncSave) {
-+            io.papermc.paper.util.MCUtil.scheduleAsyncTask(save);
-+        } else {
-+            save.run();
-+        }
-+        // Paper end - Perf: Async GameProfileCache saving
- 
-     }
- 
-     private Stream<GameProfileCache.GameProfileInfo> getTopMRUProfiles(int limit) {
--        return ImmutableList.copyOf(this.profilesByUUID.values()).stream().sorted(Comparator.comparing(GameProfileCache.GameProfileInfo::getLastAccess).reversed()).limit((long) limit);
-+        // Paper start - Fix GameProfileCache concurrency
-+        return this.listTopMRUProfiles(limit).stream();
-     }
- 
-+    private List<GameProfileCache.GameProfileInfo> listTopMRUProfiles(int limit) {
-+        try {
-+            this.stateLock.lock();
-+            return this.profilesByUUID.values().stream().sorted(Comparator.comparing(GameProfileCache.GameProfileInfo::getLastAccess).reversed()).limit(limit).toList();
-+        } finally {
-+            this.stateLock.unlock();
-+        }
-+    }
-+    // Paper end - Fix GameProfileCache concurrency
-+
-     private static JsonElement writeGameProfile(GameProfileCache.GameProfileInfo entry, DateFormat dateFormat) {
-         JsonObject jsonobject = new JsonObject();
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/server/players/PlayerList.java.patch b/paper-server/patches/unapplied/net/minecraft/server/players/PlayerList.java.patch
deleted file mode 100644
index 06288fc4a0..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/players/PlayerList.java.patch
+++ /dev/null
@@ -1,1276 +0,0 @@
---- a/net/minecraft/server/players/PlayerList.java
-+++ b/net/minecraft/server/players/PlayerList.java
-@@ -76,6 +76,7 @@
- import net.minecraft.server.level.ServerPlayer;
- import net.minecraft.server.network.CommonListenerCookie;
- import net.minecraft.server.network.ServerGamePacketListenerImpl;
-+import net.minecraft.server.network.ServerLoginPacketListenerImpl;
- import net.minecraft.sounds.SoundEvents;
- import net.minecraft.sounds.SoundSource;
- import net.minecraft.stats.ServerStatsCounter;
-@@ -84,7 +85,6 @@
- import net.minecraft.world.effect.MobEffectInstance;
- import net.minecraft.world.entity.Entity;
- import net.minecraft.world.entity.LivingEntity;
--import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.entity.projectile.ThrownEnderpearl;
- import net.minecraft.world.item.crafting.RecipeManager;
- import net.minecraft.world.level.GameRules;
-@@ -104,6 +104,26 @@
- import net.minecraft.world.scores.PlayerTeam;
- import org.slf4j.Logger;
- 
-+// CraftBukkit start
-+import java.util.stream.Collectors;
-+import net.minecraft.server.dedicated.DedicatedServer;
-+import org.bukkit.Location;
-+import org.bukkit.craftbukkit.CraftServer;
-+import org.bukkit.craftbukkit.CraftWorld;
-+import org.bukkit.craftbukkit.entity.CraftPlayer;
-+import org.bukkit.craftbukkit.util.CraftChatMessage;
-+import org.bukkit.craftbukkit.util.CraftLocation;
-+import org.bukkit.entity.Player;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.event.player.PlayerChangedWorldEvent;
-+import org.bukkit.event.player.PlayerJoinEvent;
-+import org.bukkit.event.player.PlayerLoginEvent;
-+import org.bukkit.event.player.PlayerQuitEvent;
-+import org.bukkit.event.player.PlayerRespawnEvent;
-+import org.bukkit.event.player.PlayerRespawnEvent.RespawnReason;
-+import org.bukkit.event.player.PlayerSpawnChangeEvent;
-+// CraftBukkit end
-+
- public abstract class PlayerList {
- 
-     public static final File USERBANLIST_FILE = new File("banned-players.json");
-@@ -116,14 +136,16 @@
-     private static final int SEND_PLAYER_INFO_INTERVAL = 600;
-     private static final SimpleDateFormat BAN_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z");
-     private final MinecraftServer server;
--    public final List<ServerPlayer> players = Lists.newArrayList();
-+    public final List<ServerPlayer> players = new java.util.concurrent.CopyOnWriteArrayList(); // CraftBukkit - ArrayList -> CopyOnWriteArrayList: Iterator safety
-     private final Map<UUID, ServerPlayer> playersByUUID = Maps.newHashMap();
-     private final UserBanList bans;
-     private final IpBanList ipBans;
-     private final ServerOpList ops;
-     private final UserWhiteList whitelist;
--    private final Map<UUID, ServerStatsCounter> stats;
--    private final Map<UUID, PlayerAdvancements> advancements;
-+    // CraftBukkit start
-+    // private final Map<UUID, ServerStatisticManager> stats;
-+    // private final Map<UUID, AdvancementDataPlayer> advancements;
-+    // CraftBukkit end
-     public final PlayerDataStorage playerIo;
-     private boolean doWhiteList;
-     private final LayeredRegistryAccess<RegistryLayer> registries;
-@@ -134,58 +156,137 @@
-     private static final boolean ALLOW_LOGOUTIVATOR = false;
-     private int sendAllPlayerInfoIn;
- 
-+    // CraftBukkit start
-+    private CraftServer cserver;
-+    private final Map<String,ServerPlayer> playersByName = new java.util.HashMap<>();
-+    public @Nullable String collideRuleTeamName; // Paper - Configurable player collision
-+
-     public PlayerList(MinecraftServer server, LayeredRegistryAccess<RegistryLayer> registryManager, PlayerDataStorage saveHandler, int maxPlayers) {
-+        this.cserver = server.server = new CraftServer((DedicatedServer) server, this);
-+        server.console = new com.destroystokyo.paper.console.TerminalConsoleCommandSender(); // Paper
-+        // CraftBukkit end
-+
-         this.bans = new UserBanList(PlayerList.USERBANLIST_FILE);
-         this.ipBans = new IpBanList(PlayerList.IPBANLIST_FILE);
-         this.ops = new ServerOpList(PlayerList.OPLIST_FILE);
-         this.whitelist = new UserWhiteList(PlayerList.WHITELIST_FILE);
--        this.stats = Maps.newHashMap();
--        this.advancements = Maps.newHashMap();
-+        // CraftBukkit start
-+        // this.stats = Maps.newHashMap();
-+        // this.advancements = Maps.newHashMap();
-+        // CraftBukkit end
-         this.server = server;
-         this.registries = registryManager;
-         this.maxPlayers = maxPlayers;
-         this.playerIo = saveHandler;
-     }
-+    abstract public void loadAndSaveFiles(); // Paper - fix converting txt to json file; moved from DedicatedPlayerList constructor
- 
-     public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie clientData) {
-+        player.isRealPlayer = true; // Paper
-+        player.loginTime = System.currentTimeMillis(); // Paper - Replace OfflinePlayer#getLastPlayed
-         GameProfile gameprofile = player.getGameProfile();
-         GameProfileCache usercache = this.server.getProfileCache();
--        Optional optional;
-+        // Optional optional; // CraftBukkit - decompile error
-         String s;
- 
-         if (usercache != null) {
--            optional = usercache.get(gameprofile.getId());
-+            Optional<GameProfile> optional = usercache.get(gameprofile.getId()); // CraftBukkit - decompile error
-             s = (String) optional.map(GameProfile::getName).orElse(gameprofile.getName());
-             usercache.add(gameprofile);
-         } else {
-             s = gameprofile.getName();
-         }
- 
--        optional = this.load(player);
--        ResourceKey<Level> resourcekey = (ResourceKey) optional.flatMap((nbttagcompound) -> {
--            DataResult dataresult = DimensionType.parseLegacy(new Dynamic(NbtOps.INSTANCE, nbttagcompound.get("Dimension")));
-+        Optional<CompoundTag> optional = this.load(player); // CraftBukkit - decompile error
-+        ResourceKey<Level> resourcekey = null; // Paper
-+        // CraftBukkit start - Better rename detection
-+        if (optional.isPresent()) {
-+            CompoundTag nbttagcompound = optional.get();
-+            if (nbttagcompound.contains("bukkit")) {
-+                CompoundTag bukkit = nbttagcompound.getCompound("bukkit");
-+                s = bukkit.contains("lastKnownName", 8) ? bukkit.getString("lastKnownName") : s;
-+            }
-+        }
-+        // CraftBukkit end
-+        // Paper start - move logic in Entity to here, to use bukkit supplied world UUID & reset to main world spawn if no valid world is found
-+        boolean[] invalidPlayerWorld = {false};
-+        bukkitData: if (optional.isPresent()) {
-+            // The main way for bukkit worlds to store the world is the world UUID despite mojang adding custom worlds
-+            final org.bukkit.World bWorld;
-+            if (optional.get().contains("WorldUUIDMost") && optional.get().contains("WorldUUIDLeast")) {
-+                bWorld = org.bukkit.Bukkit.getServer().getWorld(new UUID(optional.get().getLong("WorldUUIDMost"), optional.get().getLong("WorldUUIDLeast")));
-+            } else if (optional.get().contains("world", net.minecraft.nbt.Tag.TAG_STRING)) { // Paper - legacy bukkit world name
-+                bWorld = org.bukkit.Bukkit.getServer().getWorld(optional.get().getString("world"));
-+            } else {
-+                break bukkitData; // if neither of the bukkit data points exist, proceed to the vanilla migration section
-+            }
-+            if (bWorld != null) {
-+                resourcekey = ((CraftWorld) bWorld).getHandle().dimension();
-+            } else {
-+                resourcekey = Level.OVERWORLD;
-+                invalidPlayerWorld[0] = true;
-+            }
-+        }
-+        if (resourcekey == null) { // only run the vanilla logic if we haven't found a world from the bukkit data
-+        // Below is the vanilla way of getting the dimension, this is for migration from vanilla servers
-+        resourcekey = optional.flatMap((nbttagcompound) -> {
-+            // Paper end
-+            DataResult<ResourceKey<Level>> dataresult = DimensionType.parseLegacy(new Dynamic(NbtOps.INSTANCE, nbttagcompound.get("Dimension"))); // CraftBukkit - decompile error
-             Logger logger = PlayerList.LOGGER;
- 
-             Objects.requireNonNull(logger);
--            return dataresult.resultOrPartial(logger::error);
--        }).orElse(Level.OVERWORLD);
-+            // Paper start - reset to main world spawn if no valid world is found
-+            final Optional<ResourceKey<Level>> result = dataresult.resultOrPartial(logger::error);
-+            invalidPlayerWorld[0] = result.isEmpty();
-+            return result;
-+        }).orElse(Level.OVERWORLD); // Paper - revert to vanilla default main world, this isn't an "invalid world" since no player data existed
-+        }
-+        // Paper end
-         ServerLevel worldserver = this.server.getLevel(resourcekey);
-         ServerLevel worldserver1;
- 
-         if (worldserver == null) {
-             PlayerList.LOGGER.warn("Unknown respawn dimension {}, defaulting to overworld", resourcekey);
-             worldserver1 = this.server.overworld();
-+            invalidPlayerWorld[0] = true; // Paper - reset to main world if no world with parsed value is found
-         } else {
-             worldserver1 = worldserver;
-         }
- 
-+        // Paper start - Entity#getEntitySpawnReason
-+        if (optional.isEmpty()) {
-+            player.spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT; // set Player SpawnReason to DEFAULT on first login
-+            // Paper start - reset to main world spawn if first spawn or invalid world
-+        }
-+        if (optional.isEmpty() || invalidPlayerWorld[0]) {
-+            // Paper end - reset to main world spawn if first spawn or invalid world
-+            player.moveTo(player.adjustSpawnLocation(worldserver1, worldserver1.getSharedSpawnPos()).getBottomCenter(), worldserver1.getSharedSpawnAngle(), 0.0F); // Paper - MC-200092 - fix first spawn pos yaw being ignored
-+        }
-+        // Paper end - Entity#getEntitySpawnReason
-         player.setServerLevel(worldserver1);
-         String s1 = connection.getLoggableAddress(this.server.logIPs());
- 
--        PlayerList.LOGGER.info("{}[{}] logged in with entity id {} at ({}, {}, {})", new Object[]{player.getName().getString(), s1, player.getId(), player.getX(), player.getY(), player.getZ()});
-+        // Spigot start - spawn location event
-+        Player spawnPlayer = player.getBukkitEntity();
-+        org.spigotmc.event.player.PlayerSpawnLocationEvent ev = new org.spigotmc.event.player.PlayerSpawnLocationEvent(spawnPlayer, spawnPlayer.getLocation());
-+        this.cserver.getPluginManager().callEvent(ev);
-+
-+        Location loc = ev.getSpawnLocation();
-+        worldserver1 = ((CraftWorld) loc.getWorld()).getHandle();
-+
-+        player.spawnIn(worldserver1);
-+        player.gameMode.setLevel((ServerLevel) player.level());
-+        // Paper start - set raw so we aren't fully joined to the world (not added to chunk or world)
-+        player.setPosRaw(loc.getX(), loc.getY(), loc.getZ());
-+        player.setRot(loc.getYaw(), loc.getPitch());
-+        // Paper end - set raw so we aren't fully joined to the world
-+        // Spigot end
-+
-+        // CraftBukkit - Moved message to after join
-+        // PlayerList.LOGGER.info("{}[{}] logged in with entity id {} at ({}, {}, {})", new Object[]{entityplayer.getName().getString(), s1, entityplayer.getId(), entityplayer.getX(), entityplayer.getY(), entityplayer.getZ()});
-         LevelData worlddata = worldserver1.getLevelData();
- 
--        player.loadGameTypes((CompoundTag) optional.orElse((Object) null));
-+        player.loadGameTypes((CompoundTag) optional.orElse(null)); // CraftBukkit - decompile error
-         ServerGamePacketListenerImpl playerconnection = new ServerGamePacketListenerImpl(this.server, connection, player, clientData);
- 
-         connection.setupInboundProtocol(GameProtocols.SERVERBOUND_TEMPLATE.bind(RegistryFriendlyByteBuf.decorator(this.server.registryAccess())), playerconnection);
-@@ -194,7 +295,9 @@
-         boolean flag1 = gamerules.getBoolean(GameRules.RULE_REDUCEDDEBUGINFO);
-         boolean flag2 = gamerules.getBoolean(GameRules.RULE_LIMITED_CRAFTING);
- 
--        playerconnection.send(new ClientboundLoginPacket(player.getId(), worlddata.isHardcore(), this.server.levelKeys(), this.getMaxPlayers(), this.viewDistance, this.simulationDistance, flag1, !flag, flag2, player.createCommonSpawnInfo(worldserver1), this.server.enforceSecureProfile()));
-+        // Spigot - view distance
-+        playerconnection.send(new ClientboundLoginPacket(player.getId(), worlddata.isHardcore(), this.server.levelKeys(), this.getMaxPlayers(), worldserver1.spigotConfig.viewDistance, worldserver1.spigotConfig.simulationDistance, flag1, !flag, flag2, player.createCommonSpawnInfo(worldserver1), this.server.enforceSecureProfile()));
-+        player.getBukkitEntity().sendSupportedChannels(); // CraftBukkit
-         playerconnection.send(new ClientboundChangeDifficultyPacket(worlddata.getDifficulty(), worlddata.isDifficultyLocked()));
-         playerconnection.send(new ClientboundPlayerAbilitiesPacket(player.getAbilities()));
-         playerconnection.send(new ClientboundSetHeldSlotPacket(player.getInventory().selected));
-@@ -213,8 +316,10 @@
-         } else {
-             ichatmutablecomponent = Component.translatable("multiplayer.player.joined.renamed", player.getDisplayName(), s);
-         }
-+        // CraftBukkit start
-+        ichatmutablecomponent.withStyle(ChatFormatting.YELLOW);
-+        Component joinMessage = ichatmutablecomponent; // Paper - Adventure
- 
--        this.broadcastSystemMessage(ichatmutablecomponent.withStyle(ChatFormatting.YELLOW), false);
-         playerconnection.teleport(player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot());
-         ServerStatus serverping = this.server.getStatus();
- 
-@@ -222,17 +327,109 @@
-             player.sendServerStatus(serverping);
-         }
- 
--        player.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(this.players));
-+        // entityplayer.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(this.players)); // CraftBukkit - replaced with loop below
-         this.players.add(player);
-+        this.playersByName.put(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT), player); // Spigot
-         this.playersByUUID.put(player.getUUID(), player);
--        this.broadcastAll(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(player)));
--        this.sendLevelInfo(player, worldserver1);
-+        // this.broadcastAll(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(entityplayer))); // CraftBukkit - replaced with loop below
-+
-+        // Paper start - Fire PlayerJoinEvent when Player is actually ready; correctly register player BEFORE PlayerJoinEvent, so the entity is valid and doesn't require tick delay hacks
-+        player.supressTrackerForLogin = true;
-         worldserver1.addNewPlayer(player);
--        this.server.getCustomBossEvents().onPlayerConnect(player);
--        this.sendActivePlayerEffects(player);
-+        this.server.getCustomBossEvents().onPlayerConnect(player); // see commented out section below worldserver.addPlayerJoin(entityplayer);
-         player.loadAndSpawnEnderpearls(optional);
-         player.loadAndSpawnParentVehicle(optional);
-+        // Paper end - Fire PlayerJoinEvent when Player is actually ready
-+        // CraftBukkit start
-+        CraftPlayer bukkitPlayer = player.getBukkitEntity();
-+
-+        // Ensure that player inventory is populated with its viewer
-+        player.containerMenu.transferTo(player.containerMenu, bukkitPlayer);
-+
-+        PlayerJoinEvent playerJoinEvent = new PlayerJoinEvent(bukkitPlayer, io.papermc.paper.adventure.PaperAdventure.asAdventure(ichatmutablecomponent)); // Paper - Adventure
-+        this.cserver.getPluginManager().callEvent(playerJoinEvent);
-+
-+        if (!player.connection.isAcceptingMessages()) {
-+            return;
-+        }
-+
-+        final net.kyori.adventure.text.Component jm = playerJoinEvent.joinMessage();
-+
-+        if (jm != null && !jm.equals(net.kyori.adventure.text.Component.empty())) { // Paper - Adventure
-+            joinMessage = io.papermc.paper.adventure.PaperAdventure.asVanilla(jm); // Paper - Adventure
-+            this.server.getPlayerList().broadcastSystemMessage(joinMessage, false); // Paper - Adventure
-+        }
-+        // CraftBukkit end
-+
-+        // CraftBukkit start - sendAll above replaced with this loop
-+        ClientboundPlayerInfoUpdatePacket packet = ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(player)); // Paper - Add Listing API for Player
-+
-+        final List<ServerPlayer> onlinePlayers = Lists.newArrayListWithExpectedSize(this.players.size() - 1); // Paper - Use single player info update packet on join
-+        for (int i = 0; i < this.players.size(); ++i) {
-+            ServerPlayer entityplayer1 = (ServerPlayer) this.players.get(i);
-+
-+            if (entityplayer1.getBukkitEntity().canSee(bukkitPlayer)) {
-+                // Paper start - Add Listing API for Player
-+                if (entityplayer1.getBukkitEntity().isListed(bukkitPlayer)) {
-+                // Paper end - Add Listing API for Player
-+                entityplayer1.connection.send(packet);
-+                // Paper start - Add Listing API for Player
-+                } else {
-+                    entityplayer1.connection.send(ClientboundPlayerInfoUpdatePacket.createSinglePlayerInitializing(player, false));
-+                }
-+                // Paper end - Add Listing API for Player
-+            }
-+
-+            if (entityplayer1 == player || !bukkitPlayer.canSee(entityplayer1.getBukkitEntity())) { // Paper - Use single player info update packet on join; Don't include joining player
-+                continue;
-+            }
-+
-+            onlinePlayers.add(entityplayer1); // Paper - Use single player info update packet on join
-+        }
-+        // Paper start - Use single player info update packet on join
-+        if (!onlinePlayers.isEmpty()) {
-+            player.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(onlinePlayers, player)); // Paper - Add Listing API for Player
-+        }
-+        // Paper end - Use single player info update packet on join
-+        player.sentListPacket = true;
-+        player.supressTrackerForLogin = false; // Paper - Fire PlayerJoinEvent when Player is actually ready
-+        ((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 // Paper - THIS IS NOT NEEDED ANYMORE
-+
-+        this.sendLevelInfo(player, worldserver1);
-+
-+        // CraftBukkit start - Only add if the player wasn't moved in the event
-+        if (player.level() == worldserver1 && !worldserver1.players().contains(player)) {
-+            worldserver1.addNewPlayer(player);
-+            this.server.getCustomBossEvents().onPlayerConnect(player);
-+        }
-+
-+        worldserver1 = player.serverLevel(); // CraftBukkit - Update in case join event changed it
-+        // CraftBukkit end
-+        this.sendActivePlayerEffects(player);
-+        // Paper - move loading pearls / parent vehicle up
-         player.initInventoryMenu();
-+        // CraftBukkit - Moved from above, added world
-+        // Paper start - Configurable player collision; Add to collideRule team if needed
-+        final net.minecraft.world.scores.Scoreboard scoreboard = this.getServer().getLevel(Level.OVERWORLD).getScoreboard();
-+        final PlayerTeam collideRuleTeam = scoreboard.getPlayerTeam(this.collideRuleTeamName);
-+        if (this.collideRuleTeamName != null && collideRuleTeam != null && player.getTeam() == null) {
-+            scoreboard.addPlayerToTeam(player.getScoreboardName(), collideRuleTeam);
-+        }
-+        // Paper end - Configurable player collision
-+        PlayerList.LOGGER.info("{}[{}] logged in with entity id {} at ([{}]{}, {}, {})", player.getName().getString(), s1, player.getId(), worldserver1.serverLevelData.getLevelName(), player.getX(), player.getY(), player.getZ());
-+        // Paper start - Send empty chunk, so players aren't stuck in the world loading screen with our chunk system not sending chunks when dead
-+        if (player.isDeadOrDying()) {
-+            net.minecraft.core.Holder<net.minecraft.world.level.biome.Biome> plains = worldserver1.registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.BIOME)
-+                    .getOrThrow(net.minecraft.world.level.biome.Biomes.PLAINS);
-+            player.connection.send(new net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket(
-+                    new net.minecraft.world.level.chunk.EmptyLevelChunk(worldserver1, player.chunkPosition(), plains),
-+                    worldserver1.getLightEngine(), (java.util.BitSet)null, (java.util.BitSet) null)
-+            );
-+        }
-+        // Paper end - Send empty chunk
-     }
- 
-     public void updateEntireScoreboard(ServerScoreboard scoreboard, ServerPlayer player) {
-@@ -269,30 +466,31 @@
-     }
- 
-     public void addWorldborderListener(ServerLevel world) {
-+        if (this.playerIo != null) return; // CraftBukkit
-         world.getWorldBorder().addListener(new BorderChangeListener() {
-             @Override
-             public void onBorderSizeSet(WorldBorder border, double size) {
--                PlayerList.this.broadcastAll(new ClientboundSetBorderSizePacket(border));
-+                PlayerList.this.broadcastAll(new ClientboundSetBorderSizePacket(border), border.world); // CraftBukkit
-             }
- 
-             @Override
-             public void onBorderSizeLerping(WorldBorder border, double fromSize, double toSize, long time) {
--                PlayerList.this.broadcastAll(new ClientboundSetBorderLerpSizePacket(border));
-+                PlayerList.this.broadcastAll(new ClientboundSetBorderLerpSizePacket(border), border.world); // CraftBukkit
-             }
- 
-             @Override
-             public void onBorderCenterSet(WorldBorder border, double centerX, double centerZ) {
--                PlayerList.this.broadcastAll(new ClientboundSetBorderCenterPacket(border));
-+                PlayerList.this.broadcastAll(new ClientboundSetBorderCenterPacket(border), border.world); // CraftBukkit
-             }
- 
-             @Override
-             public void onBorderSetWarningTime(WorldBorder border, int warningTime) {
--                PlayerList.this.broadcastAll(new ClientboundSetBorderWarningDelayPacket(border));
-+                PlayerList.this.broadcastAll(new ClientboundSetBorderWarningDelayPacket(border), border.world); // CraftBukkit
-             }
- 
-             @Override
-             public void onBorderSetWarningBlocks(WorldBorder border, int warningBlockDistance) {
--                PlayerList.this.broadcastAll(new ClientboundSetBorderWarningDistancePacket(border));
-+                PlayerList.this.broadcastAll(new ClientboundSetBorderWarningDistancePacket(border), border.world); // CraftBukkit
-             }
- 
-             @Override
-@@ -319,14 +517,15 @@
-     }
- 
-     protected void save(ServerPlayer player) {
-+        if (!player.getBukkitEntity().isPersistent()) return; // CraftBukkit
-         this.playerIo.save(player);
--        ServerStatsCounter serverstatisticmanager = (ServerStatsCounter) this.stats.get(player.getUUID());
-+        ServerStatsCounter serverstatisticmanager = (ServerStatsCounter) player.getStats(); // CraftBukkit
- 
-         if (serverstatisticmanager != null) {
-             serverstatisticmanager.save();
-         }
- 
--        PlayerAdvancements advancementdataplayer = (PlayerAdvancements) this.advancements.get(player.getUUID());
-+        PlayerAdvancements advancementdataplayer = (PlayerAdvancements) player.getAdvancements(); // CraftBukkit
- 
-         if (advancementdataplayer != null) {
-             advancementdataplayer.save();
-@@ -334,95 +533,216 @@
- 
-     }
- 
--    public void remove(ServerPlayer player) {
--        ServerLevel worldserver = player.serverLevel();
-+    public net.kyori.adventure.text.Component remove(ServerPlayer entityplayer) { // CraftBukkit - return string // Paper - return Component
-+        // Paper start - Fix kick event leave message not being sent
-+        return this.remove(entityplayer, net.kyori.adventure.text.Component.translatable("multiplayer.player.left", net.kyori.adventure.text.format.NamedTextColor.YELLOW, io.papermc.paper.configuration.GlobalConfiguration.get().messages.useDisplayNameInQuitMessage ? entityplayer.getBukkitEntity().displayName() : io.papermc.paper.adventure.PaperAdventure.asAdventure(entityplayer.getDisplayName())));
-+    }
-+    public net.kyori.adventure.text.Component remove(ServerPlayer entityplayer, net.kyori.adventure.text.Component leaveMessage) {
-+        // Paper end - Fix kick event leave message not being sent
-+        ServerLevel worldserver = entityplayer.serverLevel();
- 
--        player.awardStat(Stats.LEAVE_GAME);
--        this.save(player);
--        if (player.isPassenger()) {
--            Entity entity = player.getRootVehicle();
-+        entityplayer.awardStat(Stats.LEAVE_GAME);
- 
-+        // CraftBukkit start - Quitting must be before we do final save of data, in case plugins need to modify it
-+        // See SPIGOT-5799, SPIGOT-6145
-+        if (entityplayer.containerMenu != entityplayer.inventoryMenu) {
-+            entityplayer.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.DISCONNECT); // Paper - Inventory close reason
-+        }
-+
-+        PlayerQuitEvent playerQuitEvent = new PlayerQuitEvent(entityplayer.getBukkitEntity(), leaveMessage, entityplayer.quitReason); // Paper - Adventure & Add API for quit reason
-+        this.cserver.getPluginManager().callEvent(playerQuitEvent);
-+        entityplayer.getBukkitEntity().disconnect(playerQuitEvent.getQuitMessage());
-+
-+        entityplayer.doTick(); // SPIGOT-924
-+        // CraftBukkit end
-+
-+        // Paper start - Configurable player collision; Remove from collideRule team if needed
-+        if (this.collideRuleTeamName != null) {
-+            final net.minecraft.world.scores.Scoreboard scoreBoard = this.server.getLevel(Level.OVERWORLD).getScoreboard();
-+            final PlayerTeam team = scoreBoard.getPlayersTeam(this.collideRuleTeamName);
-+            if (entityplayer.getTeam() == team && team != null) {
-+                scoreBoard.removePlayerFromTeam(entityplayer.getScoreboardName(), team);
-+            }
-+        }
-+        // Paper end - Configurable player collision
-+
-+        // Paper - Drop carried item when player has disconnected
-+        if (!entityplayer.containerMenu.getCarried().isEmpty()) {
-+            net.minecraft.world.item.ItemStack carried = entityplayer.containerMenu.getCarried();
-+            entityplayer.containerMenu.setCarried(net.minecraft.world.item.ItemStack.EMPTY);
-+            entityplayer.drop(carried, false);
-+        }
-+        // Paper end - Drop carried item when player has disconnected
-+
-+        this.save(entityplayer);
-+        if (entityplayer.isPassenger()) {
-+            Entity entity = entityplayer.getRootVehicle();
-+
-             if (entity.hasExactlyOnePlayerPassenger()) {
-                 PlayerList.LOGGER.debug("Removing player mount");
--                player.stopRiding();
-+                entityplayer.stopRiding();
-                 entity.getPassengersAndSelf().forEach((entity1) -> {
--                    entity1.setRemoved(Entity.RemovalReason.UNLOADED_WITH_PLAYER);
-+                    // Paper start - Fix villager boat exploit
-+                    if (entity1 instanceof net.minecraft.world.entity.npc.AbstractVillager villager) {
-+                        final net.minecraft.world.entity.player.Player human = villager.getTradingPlayer();
-+                        if (human != null) {
-+                            villager.setTradingPlayer(null);
-+                        }
-+                    }
-+                    // Paper end - Fix villager boat exploit
-+                    entity1.setRemoved(Entity.RemovalReason.UNLOADED_WITH_PLAYER, EntityRemoveEvent.Cause.PLAYER_QUIT); // CraftBukkit - add Bukkit remove cause
-                 });
-             }
-         }
- 
--        player.unRide();
--        Iterator iterator = player.getEnderPearls().iterator();
-+        entityplayer.unRide();
-+        Iterator iterator = entityplayer.getEnderPearls().iterator();
- 
-         while (iterator.hasNext()) {
-             ThrownEnderpearl entityenderpearl = (ThrownEnderpearl) iterator.next();
- 
--            entityenderpearl.setRemoved(Entity.RemovalReason.UNLOADED_WITH_PLAYER);
-+            // Paper start - Allow using old ender pearl behavior
-+            if (!entityenderpearl.level().paperConfig().misc.legacyEnderPearlBehavior) {
-+            entityenderpearl.setRemoved(Entity.RemovalReason.UNLOADED_WITH_PLAYER, EntityRemoveEvent.Cause.PLAYER_QUIT); // CraftBukkit - add Bukkit remove cause
-+            } else {
-+                entityenderpearl.cachedOwner = null;
-+            }
-+            // Paper end - Allow using old ender pearl behavior
-         }
- 
--        worldserver.removePlayerImmediately(player, Entity.RemovalReason.UNLOADED_WITH_PLAYER);
--        player.getAdvancements().stopListening();
--        this.players.remove(player);
--        this.server.getCustomBossEvents().onPlayerDisconnect(player);
--        UUID uuid = player.getUUID();
-+        worldserver.removePlayerImmediately(entityplayer, Entity.RemovalReason.UNLOADED_WITH_PLAYER);
-+        entityplayer.retireScheduler(); // Paper - Folia schedulers
-+        entityplayer.getAdvancements().stopListening();
-+        this.players.remove(entityplayer);
-+        this.playersByName.remove(entityplayer.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot
-+        this.server.getCustomBossEvents().onPlayerDisconnect(entityplayer);
-+        UUID uuid = entityplayer.getUUID();
-         ServerPlayer entityplayer1 = (ServerPlayer) this.playersByUUID.get(uuid);
- 
--        if (entityplayer1 == player) {
-+        if (entityplayer1 == entityplayer) {
-             this.playersByUUID.remove(uuid);
--            this.stats.remove(uuid);
--            this.advancements.remove(uuid);
-+            // CraftBukkit start
-+            // this.stats.remove(uuid);
-+            // this.advancements.remove(uuid);
-+            // CraftBukkit end
-         }
- 
--        this.broadcastAll(new ClientboundPlayerInfoRemovePacket(List.of(player.getUUID())));
-+        // CraftBukkit start
-+        // this.broadcastAll(new ClientboundPlayerInfoRemovePacket(List.of(entityplayer.getUUID())));
-+        ClientboundPlayerInfoRemovePacket packet = new ClientboundPlayerInfoRemovePacket(List.of(entityplayer.getUUID()));
-+        for (int i = 0; i < this.players.size(); i++) {
-+            ServerPlayer entityplayer2 = (ServerPlayer) this.players.get(i);
-+
-+            if (entityplayer2.getBukkitEntity().canSee(entityplayer.getBukkitEntity())) {
-+                entityplayer2.connection.send(packet);
-+            } else {
-+                entityplayer2.getBukkitEntity().onEntityRemove(entityplayer);
-+            }
-+        }
-+        // This removes the scoreboard (and player reference) for the specific player in the manager
-+        this.cserver.getScoreboardManager().removePlayer(entityplayer.getBukkitEntity());
-+        // CraftBukkit end
-+
-+        return playerQuitEvent.quitMessage(); // Paper - Adventure
-     }
- 
--    @Nullable
--    public Component canPlayerLogin(SocketAddress address, GameProfile profile) {
-+    // CraftBukkit start - Whole method, SocketAddress to LoginListener, added hostname to signature, return EntityPlayer
-+    public ServerPlayer canPlayerLogin(ServerLoginPacketListenerImpl loginlistener, GameProfile gameprofile) {
-         MutableComponent ichatmutablecomponent;
- 
--        if (this.bans.isBanned(profile)) {
--            UserBanListEntry gameprofilebanentry = (UserBanListEntry) this.bans.get(profile);
-+        // Moved from processLogin
-+        UUID uuid = gameprofile.getId();
-+        List<ServerPlayer> list = Lists.newArrayList();
- 
-+        ServerPlayer entityplayer;
-+
-+        for (int i = 0; i < this.players.size(); ++i) {
-+            entityplayer = (ServerPlayer) this.players.get(i);
-+            if (entityplayer.getUUID().equals(uuid) || (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode() && entityplayer.getGameProfile().getName().equalsIgnoreCase(gameprofile.getName()))) { // Paper - validate usernames
-+                list.add(entityplayer);
-+            }
-+        }
-+
-+        Iterator iterator = list.iterator();
-+
-+        while (iterator.hasNext()) {
-+            entityplayer = (ServerPlayer) iterator.next();
-+            this.save(entityplayer); // CraftBukkit - Force the player's inventory to be saved
-+            entityplayer.connection.disconnect(Component.translatable("multiplayer.disconnect.duplicate_login"), org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN); // Paper - kick event cause
-+        }
-+
-+        // Instead of kicking then returning, we need to store the kick reason
-+        // in the event, check with plugins to see if it's ok, and THEN kick
-+        // depending on the outcome.
-+        SocketAddress socketaddress = loginlistener.connection.getRemoteAddress();
-+
-+        ServerPlayer entity = new ServerPlayer(this.server, this.server.getLevel(Level.OVERWORLD), gameprofile, ClientInformation.createDefault());
-+        entity.transferCookieConnection = loginlistener;
-+        Player player = entity.getBukkitEntity();
-+        PlayerLoginEvent event = new PlayerLoginEvent(player, loginlistener.connection.hostname, ((java.net.InetSocketAddress) socketaddress).getAddress(), ((java.net.InetSocketAddress) loginlistener.connection.channel.remoteAddress()).getAddress());
-+
-+        // Paper start - Fix MC-158900
-+        UserBanListEntry gameprofilebanentry;
-+        if (this.bans.isBanned(gameprofile) && (gameprofilebanentry = this.bans.get(gameprofile)) != null) {
-+            // Paper end - Fix MC-158900
-+
-             ichatmutablecomponent = Component.translatable("multiplayer.disconnect.banned.reason", gameprofilebanentry.getReason());
-             if (gameprofilebanentry.getExpires() != null) {
-                 ichatmutablecomponent.append((Component) Component.translatable("multiplayer.disconnect.banned.expiration", PlayerList.BAN_DATE_FORMAT.format(gameprofilebanentry.getExpires())));
-             }
- 
--            return ichatmutablecomponent;
--        } else if (!this.isWhiteListed(profile)) {
--            return Component.translatable("multiplayer.disconnect.not_whitelisted");
--        } else if (this.ipBans.isBanned(address)) {
--            IpBanListEntry ipbanentry = this.ipBans.get(address);
-+            // return chatmessage;
-+            event.disallow(PlayerLoginEvent.Result.KICK_BANNED, io.papermc.paper.adventure.PaperAdventure.asAdventure(ichatmutablecomponent)); // Paper - Adventure
-+        } else if (!this.isWhiteListed(gameprofile, event)) { // Paper - ProfileWhitelistVerifyEvent
-+            //ichatmutablecomponent = Component.translatable("multiplayer.disconnect.not_whitelisted"); // Paper
-+            //event.disallow(PlayerLoginEvent.Result.KICK_WHITELIST, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.whitelistMessage)); // Spigot // Paper - Adventure - moved to isWhitelisted
-+        } else if (this.getIpBans().isBanned(socketaddress) && getIpBans().get(socketaddress) != null && !this.getIpBans().get(socketaddress).hasExpired()) { // Paper - fix NPE with temp ip bans
-+            IpBanListEntry ipbanentry = this.ipBans.get(socketaddress);
- 
-             ichatmutablecomponent = Component.translatable("multiplayer.disconnect.banned_ip.reason", ipbanentry.getReason());
-             if (ipbanentry.getExpires() != null) {
-                 ichatmutablecomponent.append((Component) Component.translatable("multiplayer.disconnect.banned_ip.expiration", PlayerList.BAN_DATE_FORMAT.format(ipbanentry.getExpires())));
-             }
- 
--            return ichatmutablecomponent;
-+            // return chatmessage;
-+            event.disallow(PlayerLoginEvent.Result.KICK_BANNED, io.papermc.paper.adventure.PaperAdventure.asAdventure(ichatmutablecomponent)); // Paper - Adventure
-         } else {
--            return this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(profile) ? Component.translatable("multiplayer.disconnect.server_full") : null;
-+            // return this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameprofile) ? IChatBaseComponent.translatable("multiplayer.disconnect.server_full") : null;
-+            if (this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameprofile)) {
-+                event.disallow(PlayerLoginEvent.Result.KICK_FULL, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.serverFullMessage)); // Spigot // Paper - Adventure
-+            }
-         }
-+
-+        this.cserver.getPluginManager().callEvent(event);
-+        if (event.getResult() != PlayerLoginEvent.Result.ALLOWED) {
-+            loginlistener.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.kickMessage())); // Paper - Adventure
-+            return null;
-+        }
-+        return entity;
-     }
- 
--    public ServerPlayer getPlayerForLogin(GameProfile profile, ClientInformation syncedOptions) {
--        return new ServerPlayer(this.server, this.server.overworld(), profile, syncedOptions);
-+    // CraftBukkit start - added EntityPlayer
-+    public ServerPlayer getPlayerForLogin(GameProfile gameprofile, ClientInformation clientinformation, ServerPlayer player) {
-+        player.updateOptions(clientinformation);
-+        return player;
-+        // CraftBukkit end
-     }
- 
--    public boolean disconnectAllPlayersWithProfile(GameProfile profile) {
--        UUID uuid = profile.getId();
--        Set<ServerPlayer> set = Sets.newIdentityHashSet();
-+    public boolean disconnectAllPlayersWithProfile(GameProfile gameprofile, ServerPlayer player) { // CraftBukkit - added EntityPlayer
-+        /* CraftBukkit startMoved up
-+        UUID uuid = gameprofile.getId();
-+        Set<EntityPlayer> set = Sets.newIdentityHashSet();
-         Iterator iterator = this.players.iterator();
- 
-         while (iterator.hasNext()) {
--            ServerPlayer entityplayer = (ServerPlayer) iterator.next();
-+            EntityPlayer entityplayer = (EntityPlayer) iterator.next();
- 
-             if (entityplayer.getUUID().equals(uuid)) {
-                 set.add(entityplayer);
-             }
-         }
- 
--        ServerPlayer entityplayer1 = (ServerPlayer) this.playersByUUID.get(profile.getId());
-+        EntityPlayer entityplayer1 = (EntityPlayer) this.playersByUUID.get(gameprofile.getId());
- 
-         if (entityplayer1 != null) {
-             set.add(entityplayer1);
-@@ -431,72 +751,160 @@
-         Iterator iterator1 = set.iterator();
- 
-         while (iterator1.hasNext()) {
--            ServerPlayer entityplayer2 = (ServerPlayer) iterator1.next();
-+            EntityPlayer entityplayer2 = (EntityPlayer) iterator1.next();
- 
-             entityplayer2.connection.disconnect(PlayerList.DUPLICATE_LOGIN_DISCONNECT_MESSAGE);
-         }
- 
-         return !set.isEmpty();
-+        */
-+        return player == null;
-+        // CraftBukkit end
-     }
- 
--    public ServerPlayer respawn(ServerPlayer player, boolean alive, Entity.RemovalReason removalReason) {
--        this.players.remove(player);
--        player.serverLevel().removePlayerImmediately(player, removalReason);
--        TeleportTransition teleporttransition = player.findRespawnPositionAndUseSpawnBlock(!alive, TeleportTransition.DO_NOTHING);
--        ServerLevel worldserver = teleporttransition.newLevel();
--        ServerPlayer entityplayer1 = new ServerPlayer(this.server, worldserver, player.getGameProfile(), player.clientInformation());
-+    // CraftBukkit start
-+    public ServerPlayer respawn(ServerPlayer entityplayer, boolean flag, Entity.RemovalReason entity_removalreason, RespawnReason reason) {
-+        return this.respawn(entityplayer, flag, entity_removalreason, reason, null);
-+    }
- 
--        entityplayer1.connection = player.connection;
--        entityplayer1.restoreFrom(player, alive);
--        entityplayer1.setId(player.getId());
--        entityplayer1.setMainArm(player.getMainArm());
-+    public ServerPlayer respawn(ServerPlayer entityplayer, boolean flag, Entity.RemovalReason entity_removalreason, RespawnReason reason, Location location) {
-+        entityplayer.stopRiding(); // CraftBukkit
-+        this.players.remove(entityplayer);
-+        this.playersByName.remove(entityplayer.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot
-+        entityplayer.serverLevel().removePlayerImmediately(entityplayer, entity_removalreason);
-+        /* CraftBukkit start
-+        TeleportTransition teleporttransition = entityplayer.findRespawnPositionAndUseSpawnBlock(!flag, TeleportTransition.DO_NOTHING);
-+        WorldServer worldserver = teleporttransition.newLevel();
-+        EntityPlayer entityplayer1 = new EntityPlayer(this.server, worldserver, entityplayer.getGameProfile(), entityplayer.clientInformation());
-+        // */
-+        ServerPlayer entityplayer1 = entityplayer;
-+        Level fromWorld = entityplayer.level();
-+        entityplayer.wonGame = false;
-+        // CraftBukkit end
-+
-+        entityplayer1.connection = entityplayer.connection;
-+        entityplayer1.restoreFrom(entityplayer, flag);
-+        entityplayer1.setId(entityplayer.getId());
-+        entityplayer1.setMainArm(entityplayer.getMainArm());
-+        // CraftBukkit - not required, just copies old location into reused entity
-+        /*
-         if (!teleporttransition.missingRespawnBlock()) {
--            entityplayer1.copyRespawnPosition(player);
-+            entityplayer1.copyRespawnPosition(entityplayer);
-         }
-+         */
-+        // CraftBukkit end
- 
--        Iterator iterator = player.getTags().iterator();
-+        Iterator iterator = entityplayer.getTags().iterator();
- 
-         while (iterator.hasNext()) {
-             String s = (String) iterator.next();
- 
-             entityplayer1.addTag(s);
-         }
-+        // Paper start - Add PlayerPostRespawnEvent
-+        boolean isBedSpawn = false;
-+        boolean isRespawn = false;
-+        // Paper end - Add PlayerPostRespawnEvent
- 
-+        // CraftBukkit start - fire PlayerRespawnEvent
-+        TeleportTransition teleporttransition;
-+        if (location == null) {
-+            teleporttransition = entityplayer.findRespawnPositionAndUseSpawnBlock(!flag, TeleportTransition.DO_NOTHING, reason);
-+
-+            if (!flag) entityplayer.reset(); // SPIGOT-4785
-+           // Paper start - Add PlayerPostRespawnEvent
-+           if (teleporttransition == null) return entityplayer; // Early exit, mirrors belows early return for disconnected players in respawn event
-+           isRespawn = true;
-+           location = CraftLocation.toBukkit(teleporttransition.position(), teleporttransition.newLevel().getWorld(), teleporttransition.yRot(), teleporttransition.xRot());
-+           // Paper end - Add PlayerPostRespawnEvent
-+        } else {
-+            teleporttransition = new TeleportTransition(((CraftWorld) location.getWorld()).getHandle(), CraftLocation.toVec3D(location), Vec3.ZERO, location.getYaw(), location.getPitch(), TeleportTransition.DO_NOTHING);
-+        }
-+        // Spigot Start
-+        if (teleporttransition == null) { // Paper - Add PlayerPostRespawnEvent - diff on change - spigot early returns if respawn pos is null, that is how they handle disconnected player in respawn event
-+            return entityplayer;
-+        }
-+        // Spigot End
-+        ServerLevel worldserver = teleporttransition.newLevel();
-+        entityplayer1.spawnIn(worldserver);
-+        entityplayer1.unsetRemoved();
-+        entityplayer1.setShiftKeyDown(false);
-         Vec3 vec3d = teleporttransition.position();
- 
--        entityplayer1.moveTo(vec3d.x, vec3d.y, vec3d.z, teleporttransition.yRot(), teleporttransition.xRot());
-+        entityplayer1.forceSetPositionRotation(vec3d.x, vec3d.y, vec3d.z, teleporttransition.yRot(), teleporttransition.xRot());
-+        worldserver.getChunkSource().addRegionTicket(net.minecraft.server.level.TicketType.POST_TELEPORT, new net.minecraft.world.level.ChunkPos(net.minecraft.util.Mth.floor(vec3d.x()) >> 4, net.minecraft.util.Mth.floor(vec3d.z()) >> 4), 1, entityplayer.getId()); // Paper - post teleport ticket type
-+        // CraftBukkit end
-         if (teleporttransition.missingRespawnBlock()) {
-             entityplayer1.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.NO_RESPAWN_BLOCK_AVAILABLE, 0.0F));
-+            entityplayer1.setRespawnPosition(null, null, 0f, false, false, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.PLAYER_RESPAWN); // CraftBukkit - SPIGOT-5988: Clear respawn location when obstructed // Paper - PlayerSetSpawnEvent
-         }
- 
--        int i = alive ? 1 : 0;
-+        int i = flag ? 1 : 0;
-         ServerLevel worldserver1 = entityplayer1.serverLevel();
-         LevelData worlddata = worldserver1.getLevelData();
- 
-         entityplayer1.connection.send(new ClientboundRespawnPacket(entityplayer1.createCommonSpawnInfo(worldserver1), (byte) i));
--        entityplayer1.connection.teleport(entityplayer1.getX(), entityplayer1.getY(), entityplayer1.getZ(), entityplayer1.getYRot(), entityplayer1.getXRot());
-+        entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.spigotConfig.viewDistance)); // Spigot
-+        entityplayer1.connection.send(new ClientboundSetSimulationDistancePacket(worldserver1.spigotConfig.simulationDistance)); // Spigot
-+        entityplayer1.connection.teleport(CraftLocation.toBukkit(entityplayer1.position(), worldserver1.getWorld(), entityplayer1.getYRot(), entityplayer1.getXRot())); // CraftBukkit
-         entityplayer1.connection.send(new ClientboundSetDefaultSpawnPositionPacket(worldserver.getSharedSpawnPos(), worldserver.getSharedSpawnAngle()));
-         entityplayer1.connection.send(new ClientboundChangeDifficultyPacket(worlddata.getDifficulty(), worlddata.isDifficultyLocked()));
-         entityplayer1.connection.send(new ClientboundSetExperiencePacket(entityplayer1.experienceProgress, entityplayer1.totalExperience, entityplayer1.experienceLevel));
-         this.sendActivePlayerEffects(entityplayer1);
-         this.sendLevelInfo(entityplayer1, worldserver);
-         this.sendPlayerPermissionLevel(entityplayer1);
--        worldserver.addRespawnedPlayer(entityplayer1);
--        this.players.add(entityplayer1);
--        this.playersByUUID.put(entityplayer1.getUUID(), entityplayer1);
--        entityplayer1.initInventoryMenu();
-+        if (!entityplayer.connection.isDisconnected()) {
-+            worldserver.addRespawnedPlayer(entityplayer1);
-+            this.players.add(entityplayer1);
-+            this.playersByName.put(entityplayer1.getScoreboardName().toLowerCase(java.util.Locale.ROOT), entityplayer1); // Spigot
-+            this.playersByUUID.put(entityplayer1.getUUID(), entityplayer1);
-+        }
-+        // entityplayer1.initInventoryMenu();
-         entityplayer1.setHealth(entityplayer1.getHealth());
-         BlockPos blockposition = entityplayer1.getRespawnPosition();
-         ServerLevel worldserver2 = this.server.getLevel(entityplayer1.getRespawnDimension());
- 
--        if (!alive && blockposition != null && worldserver2 != null) {
-+        if (!flag && blockposition != null && worldserver2 != null) {
-             BlockState iblockdata = worldserver2.getBlockState(blockposition);
- 
-             if (iblockdata.is(Blocks.RESPAWN_ANCHOR)) {
-                 entityplayer1.connection.send(new ClientboundSoundPacket(SoundEvents.RESPAWN_ANCHOR_DEPLETE, SoundSource.BLOCKS, (double) blockposition.getX(), (double) blockposition.getY(), (double) blockposition.getZ(), 1.0F, 1.0F, worldserver.getRandom().nextLong()));
-             }
-+            // Paper start - Add PlayerPostRespawnEvent
-+            if (iblockdata.is(net.minecraft.tags.BlockTags.BEDS) && !teleporttransition.missingRespawnBlock()) {
-+                isBedSpawn = true;
-+            }
-+            // Paper end - Add PlayerPostRespawnEvent
-         }
-+        // Added from changeDimension
-+        this.sendAllPlayerInfo(entityplayer); // Update health, etc...
-+        entityplayer.onUpdateAbilities();
-+        for (MobEffectInstance mobEffect : entityplayer.getActiveEffects()) {
-+            entityplayer.connection.send(new ClientboundUpdateMobEffectPacket(entityplayer.getId(), mobEffect, false)); // blend = false
-+        }
- 
-+        // Fire advancement trigger
-+        entityplayer.triggerDimensionChangeTriggers(worldserver);
-+
-+        // Don't fire on respawn
-+        if (fromWorld != worldserver) {
-+            PlayerChangedWorldEvent event = new PlayerChangedWorldEvent(entityplayer.getBukkitEntity(), fromWorld.getWorld());
-+            this.server.server.getPluginManager().callEvent(event);
-+        }
-+
-+        // Save player file again if they were disconnected
-+        if (entityplayer.connection.isDisconnected()) {
-+            this.save(entityplayer);
-+        }
-+
-+        // Paper start - Add PlayerPostRespawnEvent
-+        if (isRespawn) {
-+            cserver.getPluginManager().callEvent(new com.destroystokyo.paper.event.player.PlayerPostRespawnEvent(entityplayer.getBukkitEntity(), location, isBedSpawn));
-+        }
-+        // Paper end - Add PlayerPostRespawnEvent
-+
-+        // CraftBukkit end
-+
-         return entityplayer1;
-     }
- 
-@@ -505,26 +913,48 @@
-     }
- 
-     public void sendActiveEffects(LivingEntity entity, ServerGamePacketListenerImpl networkHandler) {
-+        // Paper start - collect packets
-+        this.sendActiveEffects(entity, networkHandler::send);
-+    }
-+    public void sendActiveEffects(LivingEntity entity, java.util.function.Consumer<Packet<? super net.minecraft.network.protocol.game.ClientGamePacketListener>> packetConsumer) {
-+        // Paper end - collect packets
-         Iterator iterator = entity.getActiveEffects().iterator();
- 
-         while (iterator.hasNext()) {
-             MobEffectInstance mobeffect = (MobEffectInstance) iterator.next();
- 
--            networkHandler.send(new ClientboundUpdateMobEffectPacket(entity.getId(), mobeffect, false));
-+            packetConsumer.accept(new ClientboundUpdateMobEffectPacket(entity.getId(), mobeffect, false)); // Paper - collect packets
-         }
- 
-     }
- 
-     public void sendPlayerPermissionLevel(ServerPlayer player) {
-+    // Paper start - avoid recalculating permissions if possible
-+        this.sendPlayerPermissionLevel(player, true);
-+    }
-+
-+    public void sendPlayerPermissionLevel(ServerPlayer player, boolean recalculatePermissions) {
-+    // Paper end - avoid recalculating permissions if possible
-         GameProfile gameprofile = player.getGameProfile();
-         int i = this.server.getProfilePermissions(gameprofile);
- 
--        this.sendPlayerPermissionLevel(player, i);
-+        this.sendPlayerPermissionLevel(player, i, recalculatePermissions); // Paper - avoid recalculating permissions if possible
-     }
- 
-     public void tick() {
-         if (++this.sendAllPlayerInfoIn > 600) {
--            this.broadcastAll(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY), this.players));
-+            // CraftBukkit start
-+            for (int i = 0; i < this.players.size(); ++i) {
-+                final ServerPlayer target = (ServerPlayer) this.players.get(i);
-+
-+                target.connection.send(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY), this.players.stream().filter(new Predicate<ServerPlayer>() {
-+                    @Override
-+                    public boolean test(ServerPlayer input) {
-+                        return target.getBukkitEntity().canSee(input.getBukkitEntity());
-+                    }
-+                }).collect(Collectors.toList())));
-+            }
-+            // CraftBukkit end
-             this.sendAllPlayerInfoIn = 0;
-         }
- 
-@@ -541,6 +971,25 @@
- 
-     }
- 
-+    // CraftBukkit start - add a world/entity limited version
-+    public void broadcastAll(Packet packet, net.minecraft.world.entity.player.Player entityhuman) {
-+        for (int i = 0; i < this.players.size(); ++i) {
-+            ServerPlayer entityplayer =  this.players.get(i);
-+            if (entityhuman != null && !entityplayer.getBukkitEntity().canSee(entityhuman.getBukkitEntity())) {
-+                continue;
-+            }
-+            ((ServerPlayer) this.players.get(i)).connection.send(packet);
-+        }
-+    }
-+
-+    public void broadcastAll(Packet packet, Level world) {
-+        for (int i = 0; i < world.players().size(); ++i) {
-+            ((ServerPlayer) world.players().get(i)).connection.send(packet);
-+        }
-+
-+    }
-+    // CraftBukkit end
-+
-     public void broadcastAll(Packet<?> packet, ResourceKey<Level> dimension) {
-         Iterator iterator = this.players.iterator();
- 
-@@ -554,7 +1003,7 @@
- 
-     }
- 
--    public void broadcastSystemToTeam(Player source, Component message) {
-+    public void broadcastSystemToTeam(net.minecraft.world.entity.player.Player source, Component message) {
-         PlayerTeam scoreboardteam = source.getTeam();
- 
-         if (scoreboardteam != null) {
-@@ -573,7 +1022,7 @@
-         }
-     }
- 
--    public void broadcastSystemToAllExceptTeam(Player source, Component message) {
-+    public void broadcastSystemToAllExceptTeam(net.minecraft.world.entity.player.Player source, Component message) {
-         PlayerTeam scoreboardteam = source.getTeam();
- 
-         if (scoreboardteam == null) {
-@@ -619,7 +1068,7 @@
-     }
- 
-     public void deop(GameProfile profile) {
--        this.ops.remove((Object) profile);
-+        this.ops.remove(profile); // CraftBukkit - decompile error
-         ServerPlayer entityplayer = this.getPlayer(profile.getId());
- 
-         if (entityplayer != null) {
-@@ -629,6 +1078,11 @@
-     }
- 
-     private void sendPlayerPermissionLevel(ServerPlayer player, int permissionLevel) {
-+        // Paper start - Add sendOpLevel API
-+        this.sendPlayerPermissionLevel(player, permissionLevel, true);
-+    }
-+    public void sendPlayerPermissionLevel(ServerPlayer player, int permissionLevel, boolean recalculatePermissions) {
-+        // Paper end - Add sendOpLevel API
-         if (player.connection != null) {
-             byte b0;
- 
-@@ -643,36 +1097,53 @@
-             player.connection.send(new ClientboundEntityEventPacket(player, b0));
-         }
- 
-+        if (recalculatePermissions) { // Paper - Add sendOpLevel API
-+        player.getBukkitEntity().recalculatePermissions(); // CraftBukkit
-         this.server.getCommands().sendCommands(player);
-+        } // Paper - Add sendOpLevel API
-     }
- 
-     public boolean isWhiteListed(GameProfile profile) {
--        return !this.doWhiteList || this.ops.contains(profile) || this.whitelist.contains(profile);
-+        // Paper start - ProfileWhitelistVerifyEvent
-+        return this.isWhiteListed(profile, null);
-     }
-+    public boolean isWhiteListed(GameProfile gameprofile, @Nullable org.bukkit.event.player.PlayerLoginEvent loginEvent) {
-+        boolean isOp = this.ops.contains(gameprofile);
-+        boolean isWhitelisted = !this.doWhiteList || isOp || this.whitelist.contains(gameprofile);
-+        final com.destroystokyo.paper.event.profile.ProfileWhitelistVerifyEvent event;
- 
-+        final net.kyori.adventure.text.Component configuredMessage = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.whitelistMessage);
-+        event = new com.destroystokyo.paper.event.profile.ProfileWhitelistVerifyEvent(com.destroystokyo.paper.profile.CraftPlayerProfile.asBukkitMirror(gameprofile), this.doWhiteList, isWhitelisted, isOp, configuredMessage);
-+        event.callEvent();
-+        if (!event.isWhitelisted()) {
-+            if (loginEvent != null) {
-+                loginEvent.disallow(PlayerLoginEvent.Result.KICK_WHITELIST, event.kickMessage() == null ? configuredMessage : event.kickMessage());
-+            }
-+            return false;
-+        }
-+        return true;
-+        // Paper end - ProfileWhitelistVerifyEvent
-+    }
-+
-     public boolean isOp(GameProfile profile) {
-         return this.ops.contains(profile) || this.server.isSingleplayerOwner(profile) && this.server.getWorldData().isAllowCommands() || this.allowCommandsForAllPlayers;
-     }
- 
-     @Nullable
-     public ServerPlayer getPlayerByName(String name) {
--        int i = this.players.size();
--
--        for (int j = 0; j < i; ++j) {
--            ServerPlayer entityplayer = (ServerPlayer) this.players.get(j);
--
--            if (entityplayer.getGameProfile().getName().equalsIgnoreCase(name)) {
--                return entityplayer;
--            }
--        }
--
--        return null;
-+        return this.playersByName.get(name.toLowerCase(java.util.Locale.ROOT)); // Spigot
-     }
- 
--    public void broadcast(@Nullable Player player, double x, double y, double z, double distance, ResourceKey<Level> worldKey, Packet<?> packet) {
-+    public void broadcast(@Nullable net.minecraft.world.entity.player.Player player, double x, double y, double z, double distance, ResourceKey<Level> worldKey, Packet<?> packet) {
-         for (int i = 0; i < this.players.size(); ++i) {
-             ServerPlayer entityplayer = (ServerPlayer) this.players.get(i);
- 
-+            // CraftBukkit start - Test if player receiving packet can see the source of the packet
-+            if (player != null && !entityplayer.getBukkitEntity().canSee(player.getBukkitEntity())) {
-+               continue;
-+            }
-+            // CraftBukkit end
-+
-             if (entityplayer != player && entityplayer.level().dimension() == worldKey) {
-                 double d4 = x - entityplayer.getX();
-                 double d5 = y - entityplayer.getY();
-@@ -687,10 +1158,12 @@
-     }
- 
-     public void saveAll() {
-+        io.papermc.paper.util.MCUtil.ensureMain("Save Players" , () -> { // Paper - Ensure main
-         for (int i = 0; i < this.players.size(); ++i) {
-             this.save((ServerPlayer) this.players.get(i));
-         }
- 
-+        return null; }); // Paper - ensure main
-     }
- 
-     public UserWhiteList getWhiteList() {
-@@ -712,15 +1185,19 @@
-     public void reloadWhiteList() {}
- 
-     public void sendLevelInfo(ServerPlayer player, ServerLevel world) {
--        WorldBorder worldborder = this.server.overworld().getWorldBorder();
-+        WorldBorder worldborder = player.level().getWorldBorder(); // CraftBukkit
- 
-         player.connection.send(new ClientboundInitializeBorderPacket(worldborder));
-         player.connection.send(new ClientboundSetTimePacket(world.getGameTime(), world.getDayTime(), world.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)));
-         player.connection.send(new ClientboundSetDefaultSpawnPositionPacket(world.getSharedSpawnPos(), world.getSharedSpawnAngle()));
-         if (world.isRaining()) {
--            player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0.0F));
--            player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, world.getRainLevel(1.0F)));
--            player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, world.getThunderLevel(1.0F)));
-+            // CraftBukkit start - handle player weather
-+            // entityplayer.connection.send(new PacketPlayOutGameStateChange(PacketPlayOutGameStateChange.START_RAINING, 0.0F));
-+            // entityplayer.connection.send(new PacketPlayOutGameStateChange(PacketPlayOutGameStateChange.RAIN_LEVEL_CHANGE, worldserver.getRainLevel(1.0F)));
-+            // entityplayer.connection.send(new PacketPlayOutGameStateChange(PacketPlayOutGameStateChange.THUNDER_LEVEL_CHANGE, worldserver.getThunderLevel(1.0F)));
-+            player.setPlayerWeather(org.bukkit.WeatherType.DOWNFALL, false);
-+            player.updateWeather(-world.rainLevel, world.rainLevel, -world.thunderLevel, world.thunderLevel);
-+            // CraftBukkit end
-         }
- 
-         player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.LEVEL_CHUNKS_LOAD_START, 0.0F));
-@@ -729,8 +1206,16 @@
- 
-     public void sendAllPlayerInfo(ServerPlayer player) {
-         player.inventoryMenu.sendAllDataToRemote();
--        player.resetSentInfo();
-+        // entityplayer.resetSentInfo();
-+        player.getBukkitEntity().updateScaledHealth(); // CraftBukkit - Update scaled health on respawn and worldchange
-+        player.refreshEntityData(player); // CraftBukkkit - SPIGOT-7218: sync metadata
-         player.connection.send(new ClientboundSetHeldSlotPacket(player.getInventory().selected));
-+        // CraftBukkit start - from GameRules
-+        int i = player.serverLevel().getGameRules().getBoolean(GameRules.RULE_REDUCEDDEBUGINFO) ? 22 : 23;
-+        player.connection.send(new ClientboundEntityEventPacket(player, (byte) i));
-+        float immediateRespawn = player.serverLevel().getGameRules().getBoolean(GameRules.RULE_DO_IMMEDIATE_RESPAWN) ? 1.0F: 0.0F;
-+        player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.IMMEDIATE_RESPAWN, immediateRespawn));
-+        // CraftBukkit end
-     }
- 
-     public int getPlayerCount() {
-@@ -746,6 +1231,7 @@
-     }
- 
-     public void setUsingWhiteList(boolean whitelistEnabled) {
-+        new com.destroystokyo.paper.event.server.WhitelistToggleEvent(whitelistEnabled).callEvent(); // Paper - WhitelistToggleEvent
-         this.doWhiteList = whitelistEnabled;
-     }
- 
-@@ -786,11 +1272,35 @@
-     }
- 
-     public void removeAll() {
--        for (int i = 0; i < this.players.size(); ++i) {
--            ((ServerPlayer) this.players.get(i)).connection.disconnect((Component) Component.translatable("multiplayer.disconnect.server_shutdown"));
-+        // Paper start - Extract method to allow for restarting flag
-+        this.removeAll(false);
-+    }
-+
-+    public void removeAll(boolean isRestarting) {
-+        // Paper end
-+        // CraftBukkit start - disconnect safely
-+        for (ServerPlayer player : this.players) {
-+            if (isRestarting) player.connection.disconnect(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.restartMessage), org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN); else // Paper - kick event cause (cause is never used here)
-+            player.connection.disconnect(java.util.Objects.requireNonNullElseGet(this.server.server.shutdownMessage(), net.kyori.adventure.text.Component::empty)); // CraftBukkit - add custom shutdown message // Paper - Adventure
-+        }
-+        // CraftBukkit end
-+
-+        // Paper start - Configurable player collision; Remove collideRule team if it exists
-+        if (this.collideRuleTeamName != null) {
-+            final net.minecraft.world.scores.Scoreboard scoreboard = this.getServer().getLevel(Level.OVERWORLD).getScoreboard();
-+            final PlayerTeam team = scoreboard.getPlayersTeam(this.collideRuleTeamName);
-+            if (team != null) scoreboard.removePlayerTeam(team);
-         }
-+        // Paper end - Configurable player collision
-+    }
- 
-+    // CraftBukkit start
-+    public void broadcastMessage(Component[] iChatBaseComponents) {
-+        for (Component component : iChatBaseComponents) {
-+            this.broadcastSystemMessage(component, false);
-+        }
-     }
-+    // CraftBukkit end
- 
-     public void broadcastSystemMessage(Component message, boolean overlay) {
-         this.broadcastSystemMessage(message, (entityplayer) -> {
-@@ -819,24 +1329,43 @@
-     }
- 
-     public void broadcastChatMessage(PlayerChatMessage message, ServerPlayer sender, ChatType.Bound params) {
-+        // Paper start
-+        this.broadcastChatMessage(message, sender, params, null);
-+    }
-+    public void broadcastChatMessage(PlayerChatMessage message, ServerPlayer sender, ChatType.Bound params, @Nullable Function<net.kyori.adventure.audience.Audience, Component> unsignedFunction) {
-+        // Paper end
-         Objects.requireNonNull(sender);
--        this.broadcastChatMessage(message, sender::shouldFilterMessageTo, sender, params);
-+        this.broadcastChatMessage(message, sender::shouldFilterMessageTo, sender, params, unsignedFunction); // Paper
-     }
- 
-     private void broadcastChatMessage(PlayerChatMessage message, Predicate<ServerPlayer> shouldSendFiltered, @Nullable ServerPlayer sender, ChatType.Bound params) {
-+        // Paper start
-+        this.broadcastChatMessage(message, shouldSendFiltered, sender, params, null);
-+    }
-+    public void broadcastChatMessage(PlayerChatMessage message, Predicate<ServerPlayer> shouldSendFiltered, @Nullable ServerPlayer sender, ChatType.Bound params, @Nullable Function<net.kyori.adventure.audience.Audience, Component> unsignedFunction) {
-+        // Paper end
-         boolean flag = this.verifyChatTrusted(message);
- 
--        this.server.logChatMessage(message.decoratedContent(), params, flag ? null : "Not Secure");
-+        this.server.logChatMessage((unsignedFunction == null ? message.decoratedContent() : unsignedFunction.apply(this.server.console)), params, flag ? null : "Not Secure"); // Paper
-         OutgoingChatMessage outgoingchatmessage = OutgoingChatMessage.create(message);
-         boolean flag1 = false;
- 
-         boolean flag2;
-+        Packet<?> disguised = sender != null && unsignedFunction == null ? new net.minecraft.network.protocol.game.ClientboundDisguisedChatPacket(outgoingchatmessage.content(), params) : null; // Paper - don't send player chat packets from vanished players
- 
-         for (Iterator iterator = this.players.iterator(); iterator.hasNext(); flag1 |= flag2 && message.isFullyFiltered()) {
-             ServerPlayer entityplayer1 = (ServerPlayer) iterator.next();
- 
-             flag2 = shouldSendFiltered.test(entityplayer1);
--            entityplayer1.sendChatMessage(outgoingchatmessage, flag2, params);
-+            // Paper start - don't send player chat packets from vanished players
-+            if (sender != null && !entityplayer1.getBukkitEntity().canSee(sender.getBukkitEntity())) {
-+                entityplayer1.connection.send(unsignedFunction != null
-+                    ? new net.minecraft.network.protocol.game.ClientboundDisguisedChatPacket(unsignedFunction.apply(entityplayer1.getBukkitEntity()), params)
-+                    : disguised);
-+                continue;
-+            }
-+            // Paper end
-+            entityplayer1.sendChatMessage(outgoingchatmessage, flag2, params, unsignedFunction == null ? null : unsignedFunction.apply(entityplayer1.getBukkitEntity())); // Paper
-         }
- 
-         if (flag1 && sender != null) {
-@@ -845,20 +1374,27 @@
- 
-     }
- 
--    private boolean verifyChatTrusted(PlayerChatMessage message) {
-+    public boolean verifyChatTrusted(PlayerChatMessage message) { // Paper - private -> public
-         return message.hasSignature() && !message.hasExpiredServer(Instant.now());
-     }
- 
--    public ServerStatsCounter getPlayerStats(Player player) {
--        UUID uuid = player.getUUID();
--        ServerStatsCounter serverstatisticmanager = (ServerStatsCounter) this.stats.get(uuid);
-+    // CraftBukkit start
-+    public ServerStatsCounter getPlayerStats(ServerPlayer entityhuman) {
-+        ServerStatsCounter serverstatisticmanager = entityhuman.getStats();
-+        return serverstatisticmanager == null ? this.getPlayerStats(entityhuman.getUUID(), entityhuman.getGameProfile().getName()) : serverstatisticmanager; // Paper - use username and not display name
-+    }
- 
-+    public ServerStatsCounter getPlayerStats(UUID uuid, String displayName) {
-+        ServerPlayer entityhuman = this.getPlayer(uuid);
-+        ServerStatsCounter serverstatisticmanager = entityhuman == null ? null : (ServerStatsCounter) entityhuman.getStats();
-+        // CraftBukkit end
-+
-         if (serverstatisticmanager == null) {
-             File file = this.server.getWorldPath(LevelResource.PLAYER_STATS_DIR).toFile();
-             File file1 = new File(file, String.valueOf(uuid) + ".json");
- 
-             if (!file1.exists()) {
--                File file2 = new File(file, player.getName().getString() + ".json");
-+                File file2 = new File(file, displayName + ".json"); // CraftBukkit
-                 Path path = file2.toPath();
- 
-                 if (FileUtil.isPathNormalized(path) && FileUtil.isPathPortable(path) && path.startsWith(file.getPath()) && file2.isFile()) {
-@@ -867,7 +1403,7 @@
-             }
- 
-             serverstatisticmanager = new ServerStatsCounter(this.server, file1);
--            this.stats.put(uuid, serverstatisticmanager);
-+            // this.stats.put(uuid, serverstatisticmanager); // CraftBukkit
-         }
- 
-         return serverstatisticmanager;
-@@ -875,13 +1411,13 @@
- 
-     public PlayerAdvancements getPlayerAdvancements(ServerPlayer player) {
-         UUID uuid = player.getUUID();
--        PlayerAdvancements advancementdataplayer = (PlayerAdvancements) this.advancements.get(uuid);
-+        PlayerAdvancements advancementdataplayer = (PlayerAdvancements) player.getAdvancements(); // CraftBukkit
- 
-         if (advancementdataplayer == null) {
-             Path path = this.server.getWorldPath(LevelResource.PLAYER_ADVANCEMENTS_DIR).resolve(String.valueOf(uuid) + ".json");
- 
-             advancementdataplayer = new PlayerAdvancements(this.server.getFixerUpper(), this, this.server.getAdvancements(), path, player);
--            this.advancements.put(uuid, advancementdataplayer);
-+            // this.advancements.put(uuid, advancementdataplayer); // CraftBukkit
-         }
- 
-         advancementdataplayer.setPlayer(player);
-@@ -932,15 +1468,39 @@
-     }
- 
-     public void reloadResources() {
--        Iterator iterator = this.advancements.values().iterator();
-+        // Paper start - API for updating recipes on clients
-+        this.reloadAdvancementData();
-+        this.reloadTagData();
-+        this.reloadRecipes();
-+    }
-+    public void reloadAdvancementData() {
-+        // Paper end - API for updating recipes on clients
-+        // CraftBukkit start
-+        /*Iterator iterator = this.advancements.values().iterator();
- 
-         while (iterator.hasNext()) {
--            PlayerAdvancements advancementdataplayer = (PlayerAdvancements) iterator.next();
-+            AdvancementDataPlayer advancementdataplayer = (AdvancementDataPlayer) iterator.next();
- 
-             advancementdataplayer.reload(this.server.getAdvancements());
-+        }*/
-+
-+        for (ServerPlayer player : this.players) {
-+            player.getAdvancements().reload(this.server.getAdvancements());
-+            player.getAdvancements().flushDirty(player); // CraftBukkit - trigger immediate flush of advancements
-         }
-+        // CraftBukkit end
- 
-+        // Paper start - API for updating recipes on clients
-+    }
-+    public void reloadTagData() {
-         this.broadcastAll(new ClientboundUpdateTagsPacket(TagNetworkSerialization.serializeTagsToNetwork(this.registries)));
-+        // CraftBukkit start
-+        // this.reloadRecipes(); // Paper - do not reload recipes just because tag data was reloaded
-+        // Paper end - API for updating recipes on clients
-+    }
-+
-+    public void reloadRecipes() {
-+        // CraftBukkit end
-         RecipeManager craftingmanager = this.server.getRecipeManager();
-         ClientboundUpdateRecipesPacket packetplayoutrecipeupdate = new ClientboundUpdateRecipesPacket(craftingmanager.getSynchronizedItemProperties(), craftingmanager.getSynchronizedStonecutterRecipes());
-         Iterator iterator1 = this.players.iterator();
diff --git a/paper-server/patches/unapplied/net/minecraft/server/players/SleepStatus.java.patch b/paper-server/patches/unapplied/net/minecraft/server/players/SleepStatus.java.patch
deleted file mode 100644
index 568224fe25..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/players/SleepStatus.java.patch
+++ /dev/null
@@ -1,44 +0,0 @@
---- a/net/minecraft/server/players/SleepStatus.java
-+++ b/net/minecraft/server/players/SleepStatus.java
-@@ -18,9 +18,12 @@
-     }
- 
-     public boolean areEnoughDeepSleeping(int percentage, List<ServerPlayer> players) {
--        int j = (int) players.stream().filter(Player::isSleepingLongEnough).count();
-+        // CraftBukkit start
-+        int j = (int) players.stream().filter((eh) -> { return eh.isSleepingLongEnough() || eh.fauxSleeping; }).count();
-+        boolean anyDeepSleep = players.stream().anyMatch(Player::isSleepingLongEnough);
- 
--        return j >= this.sleepersNeeded(percentage);
-+        return anyDeepSleep && j >= this.sleepersNeeded(percentage);
-+        // CraftBukkit end
-     }
- 
-     public int sleepersNeeded(int percentage) {
-@@ -42,18 +45,24 @@
-         this.activePlayers = 0;
-         this.sleepingPlayers = 0;
-         Iterator iterator = players.iterator();
-+        boolean anySleep = false; // CraftBukkit
- 
-         while (iterator.hasNext()) {
-             ServerPlayer entityplayer = (ServerPlayer) iterator.next();
- 
-             if (!entityplayer.isSpectator()) {
-                 ++this.activePlayers;
--                if (entityplayer.isSleeping()) {
-+                if (entityplayer.isSleeping() || entityplayer.fauxSleeping) { // CraftBukkit
-                     ++this.sleepingPlayers;
-                 }
-+                // CraftBukkit start
-+                if (entityplayer.isSleeping()) {
-+                    anySleep = true;
-+                }
-+                // CraftBukkit end
-             }
-         }
- 
--        return (j > 0 || this.sleepingPlayers > 0) && (i != this.activePlayers || j != this.sleepingPlayers);
-+        return anySleep && (j > 0 || this.sleepingPlayers > 0) && (i != this.activePlayers || j != this.sleepingPlayers); // CraftBukkit
-     }
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/players/StoredUserList.java.patch b/paper-server/patches/unapplied/net/minecraft/server/players/StoredUserList.java.patch
deleted file mode 100644
index bea194db95..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/players/StoredUserList.java.patch
+++ /dev/null
@@ -1,96 +0,0 @@
---- a/net/minecraft/server/players/StoredUserList.java
-+++ b/net/minecraft/server/players/StoredUserList.java
-@@ -30,7 +30,7 @@
-     private static final Logger LOGGER = LogUtils.getLogger();
-     private static final Gson GSON = (new GsonBuilder()).setPrettyPrinting().create();
-     private final File file;
--    private final Map<String, V> map = Maps.newHashMap();
-+    private final Map<String, V> map = Maps.newConcurrentMap(); // Paper - Use ConcurrentHashMap in JsonList
- 
-     public StoredUserList(File file) {
-         this.file = file;
-@@ -53,8 +53,11 @@
- 
-     @Nullable
-     public V get(K key) {
--        this.removeExpired();
--        return (StoredUserEntry) this.map.get(this.getKeyForUser(key));
-+        // Paper start - Use ConcurrentHashMap in JsonList
-+        return (V) this.map.computeIfPresent(this.getKeyForUser(key), (k, v) -> {
-+            return v.hasExpired() ? null : v;
-+        });
-+        // Paper end - Use ConcurrentHashMap in JsonList
-     }
- 
-     public void remove(K key) {
-@@ -77,7 +80,7 @@
-     }
- 
-     public boolean isEmpty() {
--        return this.map.size() < 1;
-+        return this.map.isEmpty(); // Paper - Use ConcurrentHashMap in JsonList
-     }
- 
-     protected String getKeyForUser(K profile) {
-@@ -85,29 +88,12 @@
-     }
- 
-     protected boolean contains(K k0) {
-+        this.removeExpired(); // CraftBukkit - SPIGOT-7589: Consistently remove expired entries to mirror .get(...)
-         return this.map.containsKey(this.getKeyForUser(k0));
-     }
- 
-     private void removeExpired() {
--        List<K> list = Lists.newArrayList();
--        Iterator iterator = this.map.values().iterator();
--
--        while (iterator.hasNext()) {
--            V v0 = (StoredUserEntry) iterator.next();
--
--            if (v0.hasExpired()) {
--                list.add(v0.getUser());
--            }
--        }
--
--        iterator = list.iterator();
--
--        while (iterator.hasNext()) {
--            K k0 = iterator.next();
--
--            this.map.remove(this.getKeyForUser(k0));
--        }
--
-+        this.map.values().removeIf(StoredUserEntry::hasExpired); // Paper - Use ConcurrentHashMap in JsonList
-     }
- 
-     protected abstract StoredUserEntry<K> createEntry(JsonObject json);
-@@ -117,8 +103,9 @@
-     }
- 
-     public void save() throws IOException {
-+        this.removeExpired(); // Paper - remove expired values before saving
-         JsonArray jsonarray = new JsonArray();
--        Stream stream = this.map.values().stream().map((jsonlistentry) -> {
-+        Stream<JsonObject> stream = this.map.values().stream().map((jsonlistentry) -> { // CraftBukkit - decompile error
-             JsonObject jsonobject = new JsonObject();
- 
-             Objects.requireNonNull(jsonlistentry);
-@@ -171,9 +158,17 @@
-                         StoredUserEntry<K> jsonlistentry = this.createEntry(jsonobject);
- 
-                         if (jsonlistentry.getUser() != null) {
--                            this.map.put(this.getKeyForUser(jsonlistentry.getUser()), jsonlistentry);
-+                            this.map.put(this.getKeyForUser(jsonlistentry.getUser()), (V) jsonlistentry); // CraftBukkit - decompile error
-                         }
-                     }
-+                // Spigot Start
-+                } catch ( com.google.gson.JsonParseException | NullPointerException ex )
-+                {
-+                    org.bukkit.Bukkit.getLogger().log( java.util.logging.Level.WARNING, "Unable to read file " + this.file + ", backing it up to {0}.backup and creating new copy.", ex );
-+                    File backup = new File( this.file + ".backup" );
-+                    this.file.renameTo( backup );
-+                    this.file.delete();
-+                // Spigot End
-                 } catch (Throwable throwable) {
-                     if (bufferedreader != null) {
-                         try {
diff --git a/paper-server/patches/unapplied/net/minecraft/server/rcon/RconConsoleSource.java.patch b/paper-server/patches/unapplied/net/minecraft/server/rcon/RconConsoleSource.java.patch
deleted file mode 100644
index 589c29e384..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/rcon/RconConsoleSource.java.patch
+++ /dev/null
@@ -1,49 +0,0 @@
---- a/net/minecraft/server/rcon/RconConsoleSource.java
-+++ b/net/minecraft/server/rcon/RconConsoleSource.java
-@@ -8,16 +8,24 @@
- import net.minecraft.world.entity.Entity;
- import net.minecraft.world.phys.Vec2;
- import net.minecraft.world.phys.Vec3;
--
-+// CraftBukkit start
-+import java.net.SocketAddress;
-+import org.bukkit.craftbukkit.command.CraftRemoteConsoleCommandSender;
-+// CraftBukkit end
- public class RconConsoleSource implements CommandSource {
- 
-     private static final String RCON = "Rcon";
-     private static final Component RCON_COMPONENT = Component.literal("Rcon");
-     private final StringBuffer buffer = new StringBuffer();
-     private final MinecraftServer server;
-+    // CraftBukkit start
-+    public final SocketAddress socketAddress;
-+    private final CraftRemoteConsoleCommandSender remoteConsole = new CraftRemoteConsoleCommandSender(this);
- 
--    public RconConsoleSource(MinecraftServer server) {
--        this.server = server;
-+    public RconConsoleSource(MinecraftServer minecraftserver, SocketAddress socketAddress) {
-+        this.socketAddress = socketAddress;
-+        // CraftBukkit end
-+        this.server = minecraftserver;
-     }
- 
-     public void prepareForCommand() {
-@@ -34,7 +42,18 @@
-         return new CommandSourceStack(this, Vec3.atLowerCornerOf(worldserver.getSharedSpawnPos()), Vec2.ZERO, worldserver, 4, "Rcon", RconConsoleSource.RCON_COMPONENT, this.server, (Entity) null);
-     }
- 
-+    // CraftBukkit start - Send a String
-+    public void sendMessage(String message) {
-+        this.buffer.append(message);
-+    }
-+
-     @Override
-+    public org.bukkit.command.CommandSender getBukkitSender(CommandSourceStack wrapper) {
-+        return this.remoteConsole;
-+    }
-+    // CraftBukkit end
-+
-+    @Override
-     public void sendSystemMessage(Component message) {
-         this.buffer.append(message.getString());
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/rcon/thread/RconClient.java.patch b/paper-server/patches/unapplied/net/minecraft/server/rcon/thread/RconClient.java.patch
deleted file mode 100644
index 1aa09ff9d7..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/server/rcon/thread/RconClient.java.patch
+++ /dev/null
@@ -1,80 +0,0 @@
---- a/net/minecraft/server/rcon/thread/RconClient.java
-+++ b/net/minecraft/server/rcon/thread/RconClient.java
-@@ -8,9 +8,12 @@
- import java.net.Socket;
- import java.nio.charset.StandardCharsets;
- import java.util.Locale;
-+import org.slf4j.Logger;
- import net.minecraft.server.ServerInterface;
-+// CraftBukkit start
-+import net.minecraft.server.dedicated.DedicatedServer;
- import net.minecraft.server.rcon.PktUtils;
--import org.slf4j.Logger;
-+import net.minecraft.server.rcon.RconConsoleSource;
- 
- public class RconClient extends GenericThread {
- 
-@@ -24,11 +27,14 @@
-     private final Socket client;
-     private final byte[] buf = new byte[1460];
-     private final String rconPassword;
--    private final ServerInterface serverInterface;
-+    // CraftBukkit start
-+    private final DedicatedServer serverInterface;
-+    private final RconConsoleSource rconConsoleSource;
-+    // CraftBukkit end
- 
-     RconClient(ServerInterface server, String password, Socket socket) {
-         super("RCON Client " + String.valueOf(socket.getInetAddress()));
--        this.serverInterface = server;
-+        this.serverInterface = (DedicatedServer) server; // CraftBukkit
-         this.client = socket;
- 
-         try {
-@@ -38,11 +44,14 @@
-         }
- 
-         this.rconPassword = password;
-+        this.rconConsoleSource = new net.minecraft.server.rcon.RconConsoleSource(this.serverInterface, socket.getRemoteSocketAddress()); // CraftBukkit
-     }
- 
-     public void run() {
--        while (true) {
--            try {
-+        // CraftBukkit start - decompile error: switch try / while statement
-+        try {
-+            while (true) {
-+                // CraftBukkit end
-                 if (!this.running) {
-                     return;
-                 }
-@@ -71,7 +80,7 @@
-                                 String s = PktUtils.stringFromByteArray(this.buf, j, i);
- 
-                                 try {
--                                    this.sendCmdResponse(l, this.serverInterface.runCommand(s));
-+                                    this.sendCmdResponse(l, this.serverInterface.runCommand(this.rconConsoleSource, s)); // CraftBukkit
-                                 } catch (Exception exception) {
-                                     this.sendCmdResponse(l, "Error executing: " + s + " (" + exception.getMessage() + ")");
-                                 }
-@@ -98,6 +107,7 @@
-                             continue;
-                     }
-                 }
-+        } // CraftBukkit - decompile error: switch try / while statement
-             } catch (IOException ioexception) {
-                 return;
-             } catch (Exception exception1) {
-@@ -109,8 +119,10 @@
-                 this.running = false;
-             }
- 
--            return;
--        }
-+            // CraftBukkit start - decompile error: switch try / while statement
-+            // return;
-+        // }
-+        // CraftBukkit end
-     }
- 
-     private void send(int sessionToken, int responseType, String message) throws IOException {
diff --git a/paper-server/patches/unapplied/net/minecraft/stats/ServerRecipeBook.java.patch b/paper-server/patches/unapplied/net/minecraft/stats/ServerRecipeBook.java.patch
deleted file mode 100644
index f95e98f66a..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/stats/ServerRecipeBook.java.patch
+++ /dev/null
@@ -1,38 +0,0 @@
---- a/net/minecraft/stats/ServerRecipeBook.java
-+++ b/net/minecraft/stats/ServerRecipeBook.java
-@@ -29,6 +29,8 @@
- import net.minecraft.world.item.crafting.display.RecipeDisplayId;
- import org.slf4j.Logger;
- 
-+import org.bukkit.craftbukkit.event.CraftEventFactory; // CraftBukkit
-+
- public class ServerRecipeBook extends RecipeBook {
- 
-     public static final String RECIPE_BOOK_TAG = "recipeBook";
-@@ -72,7 +74,7 @@
-             RecipeHolder<?> recipeholder = (RecipeHolder) iterator.next();
-             ResourceKey<Recipe<?>> resourcekey = recipeholder.id();
- 
--            if (!this.known.contains(resourcekey) && !recipeholder.value().isSpecial()) {
-+            if (!this.known.contains(resourcekey) && !recipeholder.value().isSpecial() && CraftEventFactory.handlePlayerRecipeListUpdateEvent(player, resourcekey.location())) { // CraftBukkit
-                 this.add(resourcekey);
-                 this.addHighlight(resourcekey);
-                 this.displayResolver.displaysForRecipe(resourcekey, (recipedisplayentry) -> {
-@@ -82,7 +84,7 @@
-             }
-         }
- 
--        if (!list.isEmpty()) {
-+        if (!list.isEmpty() && player.connection != null) { // SPIGOT-4478 during PlayerLoginEvent
-             player.connection.send(new ClientboundRecipeBookAddPacket(list, false));
-         }
- 
-@@ -105,7 +107,7 @@
-             }
-         }
- 
--        if (!list.isEmpty()) {
-+        if (!list.isEmpty() && player.connection != null) { // SPIGOT-4478 during PlayerLoginEvent
-             player.connection.send(new ClientboundRecipeBookRemovePacket(list));
-         }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/stats/ServerStatsCounter.java.patch b/paper-server/patches/unapplied/net/minecraft/stats/ServerStatsCounter.java.patch
deleted file mode 100644
index f82072300e..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/stats/ServerStatsCounter.java.patch
+++ /dev/null
@@ -1,58 +0,0 @@
---- a/net/minecraft/stats/ServerStatsCounter.java
-+++ b/net/minecraft/stats/ServerStatsCounter.java
-@@ -1,3 +1,4 @@
-+// mc-dev import
- package net.minecraft.stats;
- 
- import com.google.common.collect.Maps;
-@@ -57,9 +58,22 @@
-             }
-         }
- 
-+        // Paper start - Moved after stat fetching for player state file
-+        // Moves the loading after vanilla loading, so it overrides the values.
-+        // Disables saving any forced stats, so it stays at the same value (without enabling disableStatSaving)
-+        // Fixes stat initialization to not cause a NullPointerException
-+        // Spigot start
-+        for ( Map.Entry<ResourceLocation, Integer> entry : org.spigotmc.SpigotConfig.forcedStats.entrySet() )
-+        {
-+            Stat<ResourceLocation> wrapper = Stats.CUSTOM.get(java.util.Objects.requireNonNull(BuiltInRegistries.CUSTOM_STAT.getValue(entry.getKey()))); // Paper - ensured by SpigotConfig#stats
-+            this.stats.put( wrapper, entry.getValue().intValue() );
-+        }
-+        // Spigot end
-+        // Paper end - Moved after stat fetching for player state file
-     }
- 
-     public void save() {
-+        if ( org.spigotmc.SpigotConfig.disableStatSaving ) return; // Spigot
-         try {
-             FileUtils.writeStringToFile(this.file, this.toJson());
-         } catch (IOException ioexception) {
-@@ -70,6 +84,8 @@
- 
-     @Override
-     public void setValue(Player player, Stat<?> stat, int value) {
-+        if ( org.spigotmc.SpigotConfig.disableStatSaving ) return; // Spigot
-+        if (stat.getType() == Stats.CUSTOM && stat.getValue() instanceof final ResourceLocation resourceLocation && org.spigotmc.SpigotConfig.forcedStats.get(resourceLocation) != null) return; // Paper - disable saving forced stats
-         super.setValue(player, stat, value);
-         this.dirty.add(stat);
-     }
-@@ -158,13 +174,12 @@
-     }
- 
-     private <T> Optional<Stat<T>> getStat(StatType<T> type, String id) {
--        Optional optional = Optional.ofNullable(ResourceLocation.tryParse(id));
--        Registry iregistry = type.getRegistry();
-+        // CraftBukkit - decompile error start
-+        Optional<ResourceLocation> optional = Optional.ofNullable(ResourceLocation.tryParse(id));
-+        Registry<T> iregistry = type.getRegistry();
- 
--        Objects.requireNonNull(iregistry);
--        optional = optional.flatMap(iregistry::getOptional);
--        Objects.requireNonNull(type);
--        return optional.map(type::get);
-+        return optional.flatMap(iregistry::getOptional).map(type::get);
-+        // CraftBukkit - decompile error end
-     }
- 
-     private static CompoundTag fromJson(JsonObject json) {
diff --git a/paper-server/patches/unapplied/net/minecraft/util/SortedArraySet.java.patch b/paper-server/patches/unapplied/net/minecraft/util/SortedArraySet.java.patch
deleted file mode 100644
index 00af73f775..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/util/SortedArraySet.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/util/SortedArraySet.java
-+++ b/net/minecraft/util/SortedArraySet.java
-@@ -28,7 +28,7 @@
-     }
- 
-     public static <T extends Comparable<T>> SortedArraySet<T> create(int initialCapacity) {
--        return new SortedArraySet<>(initialCapacity, Comparator.naturalOrder());
-+        return new SortedArraySet<>(initialCapacity, Comparator.<T>naturalOrder()); // Paper - decompile fix
-     }
- 
-     public static <T> SortedArraySet<T> create(Comparator<T> comparator) {
diff --git a/paper-server/patches/unapplied/net/minecraft/util/SpawnUtil.java.patch b/paper-server/patches/unapplied/net/minecraft/util/SpawnUtil.java.patch
deleted file mode 100644
index 803fd149f1..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/util/SpawnUtil.java.patch
+++ /dev/null
@@ -1,60 +0,0 @@
---- a/net/minecraft/util/SpawnUtil.java
-+++ b/net/minecraft/util/SpawnUtil.java
-@@ -21,24 +21,47 @@
-     public SpawnUtil() {}
- 
-     public static <T extends Mob> Optional<T> trySpawnMob(EntityType<T> entityType, EntitySpawnReason reason, ServerLevel world, BlockPos pos, int tries, int horizontalRange, int verticalRange, SpawnUtil.Strategy requirements, boolean requireEmptySpace) {
--        BlockPos.MutableBlockPos blockposition_mutableblockposition = pos.mutable();
-+        // CraftBukkit start
-+        return SpawnUtil.trySpawnMob(entityType, reason, world, pos, tries, horizontalRange, verticalRange, requirements, requireEmptySpace, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT, null); // Paper - pre creature spawn event
-+    }
- 
--        for (int l = 0; l < tries; ++l) {
--            int i1 = Mth.randomBetweenInclusive(world.random, -horizontalRange, horizontalRange);
--            int j1 = Mth.randomBetweenInclusive(world.random, -horizontalRange, horizontalRange);
-+    public static <T extends Mob> Optional<T> trySpawnMob(EntityType<T> entitytypes, EntitySpawnReason entityspawnreason, ServerLevel worldserver, BlockPos blockposition, int i, int j, int k, SpawnUtil.Strategy spawnutil_a, boolean flag, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason, @javax.annotation.Nullable Runnable onAbort) {  // Paper - pre creature spawn event
-+        // CraftBukkit end
-+        BlockPos.MutableBlockPos blockposition_mutableblockposition = blockposition.mutable();
- 
--            blockposition_mutableblockposition.setWithOffset(pos, i1, verticalRange, j1);
--            if (world.getWorldBorder().isWithinBounds((BlockPos) blockposition_mutableblockposition) && SpawnUtil.moveToPossibleSpawnPosition(world, verticalRange, blockposition_mutableblockposition, requirements) && (!requireEmptySpace || world.noCollision(entityType.getSpawnAABB((double) blockposition_mutableblockposition.getX() + 0.5D, (double) blockposition_mutableblockposition.getY(), (double) blockposition_mutableblockposition.getZ() + 0.5D)))) {
--                T t0 = (Mob) entityType.create(world, (Consumer) null, blockposition_mutableblockposition, reason, false, false);
-+        for (int l = 0; l < i; ++l) {
-+            int i1 = Mth.randomBetweenInclusive(worldserver.random, -j, j);
-+            int j1 = Mth.randomBetweenInclusive(worldserver.random, -j, j);
- 
-+            blockposition_mutableblockposition.setWithOffset(blockposition, i1, k, j1);
-+            if (worldserver.getWorldBorder().isWithinBounds((BlockPos) blockposition_mutableblockposition) && SpawnUtil.moveToPossibleSpawnPosition(worldserver, k, blockposition_mutableblockposition, spawnutil_a) && (!flag || worldserver.noCollision(entitytypes.getSpawnAABB((double) blockposition_mutableblockposition.getX() + 0.5D, (double) blockposition_mutableblockposition.getY(), (double) blockposition_mutableblockposition.getZ() + 0.5D)))) {
-+                // Paper start - PreCreatureSpawnEvent
-+                final com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent event = new com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent(
-+                    io.papermc.paper.util.MCUtil.toLocation(worldserver, blockposition),
-+                    org.bukkit.craftbukkit.entity.CraftEntityType.minecraftToBukkit(entitytypes),
-+                    reason
-+                );
-+                if (!event.callEvent()) {
-+                    if (event.shouldAbortSpawn()) {
-+                        if (onAbort != null) {
-+                            onAbort.run();
-+                        }
-+                        return Optional.empty();
-+                    }
-+                    break;
-+                }
-+                // Paper end - PreCreatureSpawnEvent
-+                T t0 = entitytypes.create(worldserver, (Consumer<T>) null, blockposition_mutableblockposition, entityspawnreason, false, false); // CraftBukkit - decompile error
-+
-                 if (t0 != null) {
--                    if (t0.checkSpawnRules(world, reason) && t0.checkSpawnObstruction(world)) {
--                        world.addFreshEntityWithPassengers(t0);
-+                    if (t0.checkSpawnRules(worldserver, entityspawnreason) && t0.checkSpawnObstruction(worldserver)) {
-+                        worldserver.addFreshEntityWithPassengers(t0, reason); // CraftBukkit
-+                        if (t0.isRemoved()) return Optional.empty(); // CraftBukkit
-                         t0.playAmbientSound();
-                         return Optional.of(t0);
-                     }
- 
--                    t0.discard();
-+                    t0.discard(null); // CraftBukkit - add Bukkit remove cause
-                 }
-             }
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/util/datafix/DataFixers.java.patch b/paper-server/patches/unapplied/net/minecraft/util/datafix/DataFixers.java.patch
deleted file mode 100644
index 86e8fe50b7..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/util/datafix/DataFixers.java.patch
+++ /dev/null
@@ -1,82 +0,0 @@
---- a/net/minecraft/util/datafix/DataFixers.java
-+++ b/net/minecraft/util/datafix/DataFixers.java
-@@ -302,6 +302,18 @@
-         builder.addFixer(new EntityItemFrameDirectionFix(schema44, false));
-         Schema schema45 = builder.addSchema(1458, DataFixers.SAME_NAMESPACED);
- 
-+        // CraftBukkit start
-+        builder.addFixer(new com.mojang.datafixers.DataFix(schema45, false) {
-+            @Override
-+            protected com.mojang.datafixers.TypeRewriteRule makeRule() {
-+                return this.fixTypeEverywhereTyped("Player CustomName", this.getInputSchema().getType(References.PLAYER), (typed) -> {
-+                    return typed.update(DSL.remainderFinder(), (dynamic) -> {
-+                        return EntityCustomNameToComponentFix.fixTagCustomName(dynamic);
-+                    });
-+                });
-+            }
-+        });
-+        // CraftBukkit end
-         builder.addFixer(new EntityCustomNameToComponentFix(schema45, false));
-         builder.addFixer(new ItemCustomNameToComponentFix(schema45, false));
-         builder.addFixer(new BlockEntityCustomNameToComponentFix(schema45, false));
-@@ -560,7 +572,8 @@
-         builder.addFixer(new AddNewChoices(schema110, "Added Zoglin", References.ENTITY));
-         Schema schema111 = builder.addSchema(2523, DataFixers.SAME_NAMESPACED);
- 
--        builder.addFixer(new AttributesRenameLegacy(schema111, "Attribute renames", DataFixers.createRenamerNoNamespace(ImmutableMap.builder().put("generic.maxHealth", "minecraft:generic.max_health").put("Max Health", "minecraft:generic.max_health").put("zombie.spawnReinforcements", "minecraft:zombie.spawn_reinforcements").put("Spawn Reinforcements Chance", "minecraft:zombie.spawn_reinforcements").put("horse.jumpStrength", "minecraft:horse.jump_strength").put("Jump Strength", "minecraft:horse.jump_strength").put("generic.followRange", "minecraft:generic.follow_range").put("Follow Range", "minecraft:generic.follow_range").put("generic.knockbackResistance", "minecraft:generic.knockback_resistance").put("Knockback Resistance", "minecraft:generic.knockback_resistance").put("generic.movementSpeed", "minecraft:generic.movement_speed").put("Movement Speed", "minecraft:generic.movement_speed").put("generic.flyingSpeed", "minecraft:generic.flying_speed").put("Flying Speed", "minecraft:generic.flying_speed").put("generic.attackDamage", "minecraft:generic.attack_damage").put("generic.attackKnockback", "minecraft:generic.attack_knockback").put("generic.attackSpeed", "minecraft:generic.attack_speed").put("generic.armorToughness", "minecraft:generic.armor_toughness").build())));
-+        // CraftBukkit - decompile error
-+        builder.addFixer(new AttributesRenameLegacy(schema111, "Attribute renames", DataFixers.createRenamerNoNamespace(ImmutableMap.<String, String>builder().put("generic.maxHealth", "minecraft:generic.max_health").put("Max Health", "minecraft:generic.max_health").put("zombie.spawnReinforcements", "minecraft:zombie.spawn_reinforcements").put("Spawn Reinforcements Chance", "minecraft:zombie.spawn_reinforcements").put("horse.jumpStrength", "minecraft:horse.jump_strength").put("Jump Strength", "minecraft:horse.jump_strength").put("generic.followRange", "minecraft:generic.follow_range").put("Follow Range", "minecraft:generic.follow_range").put("generic.knockbackResistance", "minecraft:generic.knockback_resistance").put("Knockback Resistance", "minecraft:generic.knockback_resistance").put("generic.movementSpeed", "minecraft:generic.movement_speed").put("Movement Speed", "minecraft:generic.movement_speed").put("generic.flyingSpeed", "minecraft:generic.flying_speed").put("Flying Speed", "minecraft:generic.flying_speed").put("generic.attackDamage", "minecraft:generic.attack_damage").put("generic.attackKnockback", "minecraft:generic.attack_knockback").put("generic.attackSpeed", "minecraft:generic.attack_speed").put("generic.armorToughness", "minecraft:generic.armor_toughness").build())));
-         Schema schema112 = builder.addSchema(2527, DataFixers.SAME_NAMESPACED);
- 
-         builder.addFixer(new BitStorageAlignFix(schema112));
-@@ -623,12 +636,14 @@
-         builder.addFixer(new AddNewChoices(schema130, "Added Glow Squid", References.ENTITY));
-         builder.addFixer(new AddNewChoices(schema130, "Added Glow Item Frame", References.ENTITY));
-         Schema schema131 = builder.addSchema(2690, DataFixers.SAME_NAMESPACED);
--        ImmutableMap<String, String> immutablemap = ImmutableMap.builder().put("minecraft:weathered_copper_block", "minecraft:oxidized_copper_block").put("minecraft:semi_weathered_copper_block", "minecraft:weathered_copper_block").put("minecraft:lightly_weathered_copper_block", "minecraft:exposed_copper_block").put("minecraft:weathered_cut_copper", "minecraft:oxidized_cut_copper").put("minecraft:semi_weathered_cut_copper", "minecraft:weathered_cut_copper").put("minecraft:lightly_weathered_cut_copper", "minecraft:exposed_cut_copper").put("minecraft:weathered_cut_copper_stairs", "minecraft:oxidized_cut_copper_stairs").put("minecraft:semi_weathered_cut_copper_stairs", "minecraft:weathered_cut_copper_stairs").put("minecraft:lightly_weathered_cut_copper_stairs", "minecraft:exposed_cut_copper_stairs").put("minecraft:weathered_cut_copper_slab", "minecraft:oxidized_cut_copper_slab").put("minecraft:semi_weathered_cut_copper_slab", "minecraft:weathered_cut_copper_slab").put("minecraft:lightly_weathered_cut_copper_slab", "minecraft:exposed_cut_copper_slab").put("minecraft:waxed_semi_weathered_copper", "minecraft:waxed_weathered_copper").put("minecraft:waxed_lightly_weathered_copper", "minecraft:waxed_exposed_copper").put("minecraft:waxed_semi_weathered_cut_copper", "minecraft:waxed_weathered_cut_copper").put("minecraft:waxed_lightly_weathered_cut_copper", "minecraft:waxed_exposed_cut_copper").put("minecraft:waxed_semi_weathered_cut_copper_stairs", "minecraft:waxed_weathered_cut_copper_stairs").put("minecraft:waxed_lightly_weathered_cut_copper_stairs", "minecraft:waxed_exposed_cut_copper_stairs").put("minecraft:waxed_semi_weathered_cut_copper_slab", "minecraft:waxed_weathered_cut_copper_slab").put("minecraft:waxed_lightly_weathered_cut_copper_slab", "minecraft:waxed_exposed_cut_copper_slab").build();
-+        // CraftBukkit - decompile error
-+        ImmutableMap<String, String> immutablemap = ImmutableMap.<String, String>builder().put("minecraft:weathered_copper_block", "minecraft:oxidized_copper_block").put("minecraft:semi_weathered_copper_block", "minecraft:weathered_copper_block").put("minecraft:lightly_weathered_copper_block", "minecraft:exposed_copper_block").put("minecraft:weathered_cut_copper", "minecraft:oxidized_cut_copper").put("minecraft:semi_weathered_cut_copper", "minecraft:weathered_cut_copper").put("minecraft:lightly_weathered_cut_copper", "minecraft:exposed_cut_copper").put("minecraft:weathered_cut_copper_stairs", "minecraft:oxidized_cut_copper_stairs").put("minecraft:semi_weathered_cut_copper_stairs", "minecraft:weathered_cut_copper_stairs").put("minecraft:lightly_weathered_cut_copper_stairs", "minecraft:exposed_cut_copper_stairs").put("minecraft:weathered_cut_copper_slab", "minecraft:oxidized_cut_copper_slab").put("minecraft:semi_weathered_cut_copper_slab", "minecraft:weathered_cut_copper_slab").put("minecraft:lightly_weathered_cut_copper_slab", "minecraft:exposed_cut_copper_slab").put("minecraft:waxed_semi_weathered_copper", "minecraft:waxed_weathered_copper").put("minecraft:waxed_lightly_weathered_copper", "minecraft:waxed_exposed_copper").put("minecraft:waxed_semi_weathered_cut_copper", "minecraft:waxed_weathered_cut_copper").put("minecraft:waxed_lightly_weathered_cut_copper", "minecraft:waxed_exposed_cut_copper").put("minecraft:waxed_semi_weathered_cut_copper_stairs", "minecraft:waxed_weathered_cut_copper_stairs").put("minecraft:waxed_lightly_weathered_cut_copper_stairs", "minecraft:waxed_exposed_cut_copper_stairs").put("minecraft:waxed_semi_weathered_cut_copper_slab", "minecraft:waxed_weathered_cut_copper_slab").put("minecraft:waxed_lightly_weathered_cut_copper_slab", "minecraft:waxed_exposed_cut_copper_slab").build();
- 
-         builder.addFixer(ItemRenameFix.create(schema131, "Renamed copper block items to new oxidized terms", DataFixers.createRenamer(immutablemap)));
-         builder.addFixer(BlockRenameFix.create(schema131, "Renamed copper blocks to new oxidized terms", DataFixers.createRenamer(immutablemap)));
-         Schema schema132 = builder.addSchema(2691, DataFixers.SAME_NAMESPACED);
--        ImmutableMap<String, String> immutablemap1 = ImmutableMap.builder().put("minecraft:waxed_copper", "minecraft:waxed_copper_block").put("minecraft:oxidized_copper_block", "minecraft:oxidized_copper").put("minecraft:weathered_copper_block", "minecraft:weathered_copper").put("minecraft:exposed_copper_block", "minecraft:exposed_copper").build();
-+        // CraftBukkit - decompile error
-+        ImmutableMap<String, String> immutablemap1 = ImmutableMap.<String, String>builder().put("minecraft:waxed_copper", "minecraft:waxed_copper_block").put("minecraft:oxidized_copper_block", "minecraft:oxidized_copper").put("minecraft:weathered_copper_block", "minecraft:weathered_copper").put("minecraft:exposed_copper_block", "minecraft:exposed_copper").build();
- 
-         builder.addFixer(ItemRenameFix.create(schema132, "Rename copper item suffixes", DataFixers.createRenamer(immutablemap1)));
-         builder.addFixer(BlockRenameFix.create(schema132, "Rename copper blocks suffixes", DataFixers.createRenamer(immutablemap1)));
-@@ -636,7 +651,8 @@
- 
-         builder.addFixer(new AddFlagIfNotPresentFix(schema133, References.WORLD_GEN_SETTINGS, "has_increased_height_already", false));
-         Schema schema134 = builder.addSchema(2696, DataFixers.SAME_NAMESPACED);
--        ImmutableMap<String, String> immutablemap2 = ImmutableMap.builder().put("minecraft:grimstone", "minecraft:deepslate").put("minecraft:grimstone_slab", "minecraft:cobbled_deepslate_slab").put("minecraft:grimstone_stairs", "minecraft:cobbled_deepslate_stairs").put("minecraft:grimstone_wall", "minecraft:cobbled_deepslate_wall").put("minecraft:polished_grimstone", "minecraft:polished_deepslate").put("minecraft:polished_grimstone_slab", "minecraft:polished_deepslate_slab").put("minecraft:polished_grimstone_stairs", "minecraft:polished_deepslate_stairs").put("minecraft:polished_grimstone_wall", "minecraft:polished_deepslate_wall").put("minecraft:grimstone_tiles", "minecraft:deepslate_tiles").put("minecraft:grimstone_tile_slab", "minecraft:deepslate_tile_slab").put("minecraft:grimstone_tile_stairs", "minecraft:deepslate_tile_stairs").put("minecraft:grimstone_tile_wall", "minecraft:deepslate_tile_wall").put("minecraft:grimstone_bricks", "minecraft:deepslate_bricks").put("minecraft:grimstone_brick_slab", "minecraft:deepslate_brick_slab").put("minecraft:grimstone_brick_stairs", "minecraft:deepslate_brick_stairs").put("minecraft:grimstone_brick_wall", "minecraft:deepslate_brick_wall").put("minecraft:chiseled_grimstone", "minecraft:chiseled_deepslate").build();
-+        // CraftBukkit - decompile error
-+        ImmutableMap<String, String> immutablemap2 = ImmutableMap.<String, String>builder().put("minecraft:grimstone", "minecraft:deepslate").put("minecraft:grimstone_slab", "minecraft:cobbled_deepslate_slab").put("minecraft:grimstone_stairs", "minecraft:cobbled_deepslate_stairs").put("minecraft:grimstone_wall", "minecraft:cobbled_deepslate_wall").put("minecraft:polished_grimstone", "minecraft:polished_deepslate").put("minecraft:polished_grimstone_slab", "minecraft:polished_deepslate_slab").put("minecraft:polished_grimstone_stairs", "minecraft:polished_deepslate_stairs").put("minecraft:polished_grimstone_wall", "minecraft:polished_deepslate_wall").put("minecraft:grimstone_tiles", "minecraft:deepslate_tiles").put("minecraft:grimstone_tile_slab", "minecraft:deepslate_tile_slab").put("minecraft:grimstone_tile_stairs", "minecraft:deepslate_tile_stairs").put("minecraft:grimstone_tile_wall", "minecraft:deepslate_tile_wall").put("minecraft:grimstone_bricks", "minecraft:deepslate_bricks").put("minecraft:grimstone_brick_slab", "minecraft:deepslate_brick_slab").put("minecraft:grimstone_brick_stairs", "minecraft:deepslate_brick_stairs").put("minecraft:grimstone_brick_wall", "minecraft:deepslate_brick_wall").put("minecraft:chiseled_grimstone", "minecraft:chiseled_deepslate").build();
- 
-         builder.addFixer(ItemRenameFix.create(schema134, "Renamed grimstone block items to deepslate", DataFixers.createRenamer(immutablemap2)));
-         builder.addFixer(BlockRenameFix.create(schema134, "Renamed grimstone blocks to deepslate", DataFixers.createRenamer(immutablemap2)));
-@@ -723,10 +739,11 @@
-         builder.addFixer(new AddNewChoices(schema159, "Added Allay", References.ENTITY));
-         Schema schema160 = builder.addSchema(3084, DataFixers.SAME_NAMESPACED);
- 
--        builder.addFixer(new NamespacedTypeRenameFix(schema160, "game_event_renames_3084", References.GAME_EVENT_NAME, DataFixers.createRenamer(ImmutableMap.builder().put("minecraft:block_press", "minecraft:block_activate").put("minecraft:block_switch", "minecraft:block_activate").put("minecraft:block_unpress", "minecraft:block_deactivate").put("minecraft:block_unswitch", "minecraft:block_deactivate").put("minecraft:drinking_finish", "minecraft:drink").put("minecraft:elytra_free_fall", "minecraft:elytra_glide").put("minecraft:entity_damaged", "minecraft:entity_damage").put("minecraft:entity_dying", "minecraft:entity_die").put("minecraft:entity_killed", "minecraft:entity_die").put("minecraft:mob_interact", "minecraft:entity_interact").put("minecraft:ravager_roar", "minecraft:entity_roar").put("minecraft:ring_bell", "minecraft:block_change").put("minecraft:shulker_close", "minecraft:container_close").put("minecraft:shulker_open", "minecraft:container_open").put("minecraft:wolf_shaking", "minecraft:entity_shake").build())));
-+        // CraftBukkit - decompile error
-+        builder.addFixer(new NamespacedTypeRenameFix(schema160, "game_event_renames_3084", References.GAME_EVENT_NAME, DataFixers.createRenamer(ImmutableMap.<String, String>builder().put("minecraft:block_press", "minecraft:block_activate").put("minecraft:block_switch", "minecraft:block_activate").put("minecraft:block_unpress", "minecraft:block_deactivate").put("minecraft:block_unswitch", "minecraft:block_deactivate").put("minecraft:drinking_finish", "minecraft:drink").put("minecraft:elytra_free_fall", "minecraft:elytra_glide").put("minecraft:entity_damaged", "minecraft:entity_damage").put("minecraft:entity_dying", "minecraft:entity_die").put("minecraft:entity_killed", "minecraft:entity_die").put("minecraft:mob_interact", "minecraft:entity_interact").put("minecraft:ravager_roar", "minecraft:entity_roar").put("minecraft:ring_bell", "minecraft:block_change").put("minecraft:shulker_close", "minecraft:container_close").put("minecraft:shulker_open", "minecraft:container_open").put("minecraft:wolf_shaking", "minecraft:entity_shake").build())));
-         Schema schema161 = builder.addSchema(3086, DataFixers.SAME_NAMESPACED);
-         TypeReference typereference = References.ENTITY;
--        Int2ObjectOpenHashMap int2objectopenhashmap = (Int2ObjectOpenHashMap) Util.make(new Int2ObjectOpenHashMap(), (int2objectopenhashmap1) -> {
-+        Int2ObjectOpenHashMap<String> int2objectopenhashmap = (Int2ObjectOpenHashMap) Util.make(new Int2ObjectOpenHashMap(), (int2objectopenhashmap1) -> { // CraftBukkit - decompile error
-             int2objectopenhashmap1.defaultReturnValue("minecraft:tabby");
-             int2objectopenhashmap1.put(0, "minecraft:tabby");
-             int2objectopenhashmap1.put(1, "minecraft:black");
-@@ -743,7 +760,8 @@
- 
-         Objects.requireNonNull(int2objectopenhashmap);
-         builder.addFixer(new EntityVariantFix(schema161, "Change cat variant type", typereference, "minecraft:cat", "CatType", int2objectopenhashmap::get));
--        ImmutableMap<String, String> immutablemap3 = ImmutableMap.builder().put("textures/entity/cat/tabby.png", "minecraft:tabby").put("textures/entity/cat/black.png", "minecraft:black").put("textures/entity/cat/red.png", "minecraft:red").put("textures/entity/cat/siamese.png", "minecraft:siamese").put("textures/entity/cat/british_shorthair.png", "minecraft:british").put("textures/entity/cat/calico.png", "minecraft:calico").put("textures/entity/cat/persian.png", "minecraft:persian").put("textures/entity/cat/ragdoll.png", "minecraft:ragdoll").put("textures/entity/cat/white.png", "minecraft:white").put("textures/entity/cat/jellie.png", "minecraft:jellie").put("textures/entity/cat/all_black.png", "minecraft:all_black").build();
-+        // CraftBukkit - decompile error
-+        ImmutableMap<String, String> immutablemap3 = ImmutableMap.<String, String>builder().put("textures/entity/cat/tabby.png", "minecraft:tabby").put("textures/entity/cat/black.png", "minecraft:black").put("textures/entity/cat/red.png", "minecraft:red").put("textures/entity/cat/siamese.png", "minecraft:siamese").put("textures/entity/cat/british_shorthair.png", "minecraft:british").put("textures/entity/cat/calico.png", "minecraft:calico").put("textures/entity/cat/persian.png", "minecraft:persian").put("textures/entity/cat/ragdoll.png", "minecraft:ragdoll").put("textures/entity/cat/white.png", "minecraft:white").put("textures/entity/cat/jellie.png", "minecraft:jellie").put("textures/entity/cat/all_black.png", "minecraft:all_black").build();
- 
-         builder.addFixer(new CriteriaRenameFix(schema161, "Migrate cat variant advancement", "minecraft:husbandry/complete_catalogue", (s) -> {
-             return (String) immutablemap3.getOrDefault(s, s);
diff --git a/paper-server/patches/unapplied/net/minecraft/util/datafix/fixes/ItemStackMapIdFix.java.patch b/paper-server/patches/unapplied/net/minecraft/util/datafix/fixes/ItemStackMapIdFix.java.patch
deleted file mode 100644
index 139090af75..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/util/datafix/fixes/ItemStackMapIdFix.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/util/datafix/fixes/ItemStackMapIdFix.java
-+++ b/net/minecraft/util/datafix/fixes/ItemStackMapIdFix.java
-@@ -32,7 +32,7 @@
-                 Typed<?> typed1 = typed.getOrCreateTyped(opticfinder1);
-                 Dynamic<?> dynamic1 = (Dynamic) typed1.get(DSL.remainderFinder());
- 
--                dynamic1 = dynamic1.set("map", dynamic1.createInt(dynamic.get("Damage").asInt(0)));
-+                if (!dynamic1.getElement("map").result().isPresent()) dynamic1 = dynamic1.set("map", dynamic1.createInt(dynamic.get("Damage").asInt(0))); // CraftBukkit
-                 return typed.set(opticfinder1, typed1.set(DSL.remainderFinder(), dynamic1));
-             } else {
-                 return typed;
diff --git a/paper-server/patches/unapplied/net/minecraft/util/datafix/fixes/ItemStackTheFlatteningFix.java.patch b/paper-server/patches/unapplied/net/minecraft/util/datafix/fixes/ItemStackTheFlatteningFix.java.patch
deleted file mode 100644
index a56a92ae47..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/util/datafix/fixes/ItemStackTheFlatteningFix.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/util/datafix/fixes/ItemStackTheFlatteningFix.java
-+++ b/net/minecraft/util/datafix/fixes/ItemStackTheFlatteningFix.java
-@@ -376,7 +376,7 @@
-                     Typed<?> typed2 = typed.getOrCreateTyped(opticfinder1);
-                     Dynamic<?> dynamic1 = (Dynamic) typed2.get(DSL.remainderFinder());
- 
--                    dynamic1 = dynamic1.set("Damage", dynamic1.createInt(i));
-+                    if (i != 0) dynamic1 = dynamic1.set("Damage", dynamic1.createInt(i)); // CraftBukkit
-                     typed1 = typed1.set(opticfinder1, typed2.set(DSL.remainderFinder(), dynamic1));
-                 }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/util/worldupdate/WorldUpgrader.java.patch b/paper-server/patches/unapplied/net/minecraft/util/worldupdate/WorldUpgrader.java.patch
deleted file mode 100644
index de3432baee..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/util/worldupdate/WorldUpgrader.java.patch
+++ /dev/null
@@ -1,32 +0,0 @@
---- a/net/minecraft/util/worldupdate/WorldUpgrader.java
-+++ b/net/minecraft/util/worldupdate/WorldUpgrader.java
-@@ -80,7 +80,7 @@
- 
-     public WorldUpgrader(LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, RegistryAccess dynamicRegistryManager, boolean eraseCache, boolean recreateRegionFiles) {
-         this.dimensions = dynamicRegistryManager.lookupOrThrow(Registries.LEVEL_STEM);
--        this.levels = (Set) this.dimensions.registryKeySet().stream().map(Registries::levelStemToLevel).collect(Collectors.toUnmodifiableSet());
-+        this.levels = (Set) java.util.stream.Stream.of(session.dimensionType).map(Registries::levelStemToLevel).collect(Collectors.toUnmodifiableSet()); // CraftBukkit
-         this.eraseCache = eraseCache;
-         this.dataFixer = dataFixer;
-         this.levelStorage = session;
-@@ -197,9 +197,9 @@
-             if (nbttagcompound != null) {
-                 int i = ChunkStorage.getVersion(nbttagcompound);
-                 ChunkGenerator chunkgenerator = ((LevelStem) WorldUpgrader.this.dimensions.getValueOrThrow(Registries.levelToLevelStem(worldKey))).generator();
--                CompoundTag nbttagcompound1 = storage.upgradeChunkTag(worldKey, () -> {
-+                CompoundTag nbttagcompound1 = storage.upgradeChunkTag(Registries.levelToLevelStem(worldKey), () -> { // CraftBukkit
-                     return WorldUpgrader.this.overworldDataStorage;
--                }, nbttagcompound, chunkgenerator.getTypeNameForDataFixer());
-+                }, nbttagcompound, chunkgenerator.getTypeNameForDataFixer(), chunkPos, null); // CraftBukkit
-                 ChunkPos chunkcoordintpair1 = new ChunkPos(nbttagcompound1.getInt("xPos"), nbttagcompound1.getInt("zPos"));
- 
-                 if (!chunkcoordintpair1.equals(chunkPos)) {
-@@ -321,7 +321,7 @@
-                         WorldUpgrader.DimensionToUpgrade<T> worldupgrader_c = (WorldUpgrader.DimensionToUpgrade) iterator.next();
-                         ResourceKey<Level> resourcekey = worldupgrader_c.dimensionKey;
-                         ListIterator<WorldUpgrader.FileToUpgrade> listiterator = worldupgrader_c.files;
--                        T t0 = (AutoCloseable) worldupgrader_c.storage;
-+                        T t0 = (T) worldupgrader_c.storage; // CraftBukkit - decompile error
- 
-                         if (listiterator.hasNext()) {
-                             WorldUpgrader.FileToUpgrade worldupgrader_e = (WorldUpgrader.FileToUpgrade) listiterator.next();
diff --git a/paper-server/patches/unapplied/net/minecraft/world/CompoundContainer.java.patch b/paper-server/patches/unapplied/net/minecraft/world/CompoundContainer.java.patch
deleted file mode 100644
index 1a360b99ad..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/CompoundContainer.java.patch
+++ /dev/null
@@ -1,74 +0,0 @@
---- a/net/minecraft/world/CompoundContainer.java
-+++ b/net/minecraft/world/CompoundContainer.java
-@@ -3,11 +3,62 @@
- import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.item.ItemStack;
- 
-+// CraftBukkit start
-+import java.util.ArrayList;
-+import java.util.List;
-+import org.bukkit.Location;
-+
-+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
-+import org.bukkit.entity.HumanEntity;
-+// CraftBukkit end
-+
- public class CompoundContainer implements Container {
- 
-     public final Container container1;
-     public final Container container2;
- 
-+    // CraftBukkit start - add fields and methods
-+    public List<HumanEntity> transaction = new java.util.ArrayList<HumanEntity>();
-+
-+    public List<ItemStack> getContents() {
-+        List<ItemStack> result = new ArrayList<ItemStack>(this.getContainerSize());
-+        for (int i = 0; i < this.getContainerSize(); i++) {
-+            result.add(this.getItem(i));
-+        }
-+        return result;
-+    }
-+
-+    public void onOpen(CraftHumanEntity who) {
-+        this.container1.onOpen(who);
-+        this.container2.onOpen(who);
-+        this.transaction.add(who);
-+    }
-+
-+    public void onClose(CraftHumanEntity who) {
-+        this.container1.onClose(who);
-+        this.container2.onClose(who);
-+        this.transaction.remove(who);
-+    }
-+
-+    public List<HumanEntity> getViewers() {
-+        return this.transaction;
-+    }
-+
-+    public org.bukkit.inventory.InventoryHolder getOwner() {
-+        return null; // This method won't be called since CraftInventoryDoubleChest doesn't defer to here
-+    }
-+
-+    public void setMaxStackSize(int size) {
-+        this.container1.setMaxStackSize(size);
-+        this.container2.setMaxStackSize(size);
-+    }
-+
-+    @Override
-+    public Location getLocation() {
-+        return this.container1.getLocation(); // TODO: right?
-+    }
-+    // CraftBukkit end
-+
-     public CompoundContainer(Container first, Container second) {
-         this.container1 = first;
-         this.container2 = second;
-@@ -54,7 +105,7 @@
- 
-     @Override
-     public int getMaxStackSize() {
--        return this.container1.getMaxStackSize();
-+        return Math.min(this.container1.getMaxStackSize(), this.container2.getMaxStackSize()); // CraftBukkit - check both sides
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/Container.java.patch b/paper-server/patches/unapplied/net/minecraft/world/Container.java.patch
deleted file mode 100644
index cc552af92d..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/Container.java.patch
+++ /dev/null
@@ -1,49 +0,0 @@
---- a/net/minecraft/world/Container.java
-+++ b/net/minecraft/world/Container.java
-@@ -6,8 +6,12 @@
- import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.item.Item;
- import net.minecraft.world.item.ItemStack;
-+// CraftBukkit start
-+import net.minecraft.world.item.crafting.RecipeHolder;
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.block.entity.BlockEntity;
-+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
-+// CraftBukkit end
- 
- public interface Container extends Clearable {
- 
-@@ -25,9 +29,7 @@
- 
-     void setItem(int slot, ItemStack stack);
- 
--    default int getMaxStackSize() {
--        return 99;
--    }
-+    int getMaxStackSize(); // CraftBukkit
- 
-     default int getMaxStackSize(ItemStack stack) {
-         return Math.min(this.getMaxStackSize(), stack.getMaxStackSize());
-@@ -91,4 +93,22 @@
- 
-         return world == null ? false : (world.getBlockEntity(blockposition) != blockEntity ? false : player.canInteractWithBlock(blockposition, (double) range));
-     }
-+
-+    // CraftBukkit start
-+    java.util.List<ItemStack> getContents();
-+
-+    void onOpen(CraftHumanEntity who);
-+
-+    void onClose(CraftHumanEntity who);
-+
-+    java.util.List<org.bukkit.entity.HumanEntity> getViewers();
-+
-+    org.bukkit.inventory.@org.jetbrains.annotations.Nullable InventoryHolder getOwner(); // Paper - annotation
-+
-+    void setMaxStackSize(int size);
-+
-+    org.bukkit.Location getLocation();
-+
-+    int MAX_STACK = 99;
-+    // CraftBukkit end
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/effect/HealOrHarmMobEffect.java.patch b/paper-server/patches/unapplied/net/minecraft/world/effect/HealOrHarmMobEffect.java.patch
deleted file mode 100644
index d8a62a429d..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/effect/HealOrHarmMobEffect.java.patch
+++ /dev/null
@@ -1,20 +0,0 @@
---- a/net/minecraft/world/effect/HealOrHarmMobEffect.java
-+++ b/net/minecraft/world/effect/HealOrHarmMobEffect.java
-@@ -17,7 +17,7 @@
-     @Override
-     public boolean applyEffectTick(ServerLevel world, LivingEntity entity, int amplifier) {
-         if (this.isHarm == entity.isInvertedHealAndHarm()) {
--            entity.heal((float) Math.max(4 << amplifier, 0));
-+            entity.heal((float) Math.max(4 << amplifier, 0), org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.MAGIC); // CraftBukkit
-         } else {
-             entity.hurtServer(world, entity.damageSources().magic(), (float) (6 << amplifier));
-         }
-@@ -31,7 +31,7 @@
- 
-         if (this.isHarm == target.isInvertedHealAndHarm()) {
-             j = (int) (proximity * (double) (4 << amplifier) + 0.5D);
--            target.heal((float) j);
-+            target.heal((float) j, org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.MAGIC); // CraftBukkit
-         } else {
-             j = (int) (proximity * (double) (6 << amplifier) + 0.5D);
-             if (effectEntity == null) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/effect/HungerMobEffect.java.patch b/paper-server/patches/unapplied/net/minecraft/world/effect/HungerMobEffect.java.patch
deleted file mode 100644
index 9757b68e1a..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/effect/HungerMobEffect.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/effect/HungerMobEffect.java
-+++ b/net/minecraft/world/effect/HungerMobEffect.java
-@@ -13,7 +13,7 @@
-     @Override
-     public boolean applyEffectTick(ServerLevel world, LivingEntity entity, int amplifier) {
-         if (entity instanceof Player entityhuman) {
--            entityhuman.causeFoodExhaustion(0.005F * (float) (amplifier + 1));
-+            entityhuman.causeFoodExhaustion(0.005F * (float) (amplifier + 1), org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.HUNGER_EFFECT); // CraftBukkit - EntityExhaustionEvent
-         }
- 
-         return true;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/effect/InfestedMobEffect.java.patch b/paper-server/patches/unapplied/net/minecraft/world/effect/InfestedMobEffect.java.patch
deleted file mode 100644
index 98497613dc..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/effect/InfestedMobEffect.java.patch
+++ /dev/null
@@ -1,15 +0,0 @@
---- a/net/minecraft/world/effect/InfestedMobEffect.java
-+++ b/net/minecraft/world/effect/InfestedMobEffect.java
-@@ -48,7 +48,11 @@
- 
-             entitysilverfish.moveTo(x, y, z, world.getRandom().nextFloat() * 360.0F, 0.0F);
-             entitysilverfish.setDeltaMovement(new Vec3(vector3f));
--            world.addFreshEntity(entitysilverfish);
-+            // CraftBukkit start
-+            if (!world.addFreshEntity(entitysilverfish, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.POTION_EFFECT)) {
-+                return;
-+            }
-+            // CraftBukkit end
-             entitysilverfish.playSound(SoundEvents.SILVERFISH_HURT);
-         }
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/effect/MobEffectUtil.java.patch b/paper-server/patches/unapplied/net/minecraft/world/effect/MobEffectUtil.java.patch
deleted file mode 100644
index 6c259deb9b..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/effect/MobEffectUtil.java.patch
+++ /dev/null
@@ -1,39 +0,0 @@
---- a/net/minecraft/world/effect/MobEffectUtil.java
-+++ b/net/minecraft/world/effect/MobEffectUtil.java
-@@ -50,13 +50,32 @@
-     }
- 
-     public static List<ServerPlayer> addEffectToPlayersAround(ServerLevel world, @Nullable Entity entity, Vec3 origin, double range, MobEffectInstance statusEffectInstance, int duration) {
--        Holder<MobEffect> holder = statusEffectInstance.getEffect();
--        List<ServerPlayer> list = world.getPlayers((entityplayer) -> {
--            return entityplayer.gameMode.isSurvival() && (entity == null || !entity.isAlliedTo((Entity) entityplayer)) && origin.closerThan(entityplayer.position(), range) && (!entityplayer.hasEffect(holder) || entityplayer.getEffect(holder).getAmplifier() < statusEffectInstance.getAmplifier() || entityplayer.getEffect(holder).endsWithin(duration - 1));
-+        // CraftBukkit start
-+        return MobEffectUtil.addEffectToPlayersAround(world, entity, origin, range, statusEffectInstance, duration, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.UNKNOWN);
-+    }
-+
-+    public static List<ServerPlayer> addEffectToPlayersAround(ServerLevel worldserver, @Nullable Entity entity, Vec3 vec3d, double d0, MobEffectInstance mobeffect, int i, org.bukkit.event.entity.EntityPotionEffectEvent.Cause cause) {
-+        // Paper start - Add ElderGuardianAppearanceEvent
-+        return addEffectToPlayersAround(worldserver, entity, vec3d, d0, mobeffect, i, cause, null);
-+    }
-+
-+    public static List<ServerPlayer> addEffectToPlayersAround(ServerLevel worldserver, @Nullable Entity entity, Vec3 vec3d, double d0, MobEffectInstance mobeffect, int i, org.bukkit.event.entity.EntityPotionEffectEvent.Cause cause, @Nullable java.util.function.Predicate<ServerPlayer> playerPredicate) {
-+        // Paper end - Add ElderGuardianAppearanceEvent
-+        // CraftBukkit end
-+        Holder<MobEffect> holder = mobeffect.getEffect();
-+        List<ServerPlayer> list = worldserver.getPlayers((entityplayer) -> {
-+            // Paper start - Add ElderGuardianAppearanceEvent
-+            boolean condition = entityplayer.gameMode.isSurvival() && (entity == null || !entity.isAlliedTo((Entity) entityplayer)) && vec3d.closerThan(entityplayer.position(), d0) && (!entityplayer.hasEffect(holder) || entityplayer.getEffect(holder).getAmplifier() < mobeffect.getAmplifier() || entityplayer.getEffect(holder).endsWithin(i - 1));
-+            if (condition) {
-+                return playerPredicate == null || playerPredicate.test(entityplayer); // Only test the player AFTER it is true
-+            } else {
-+                return false;
-+            }
-+            // Paper ned - Add ElderGuardianAppearanceEvent
-         });
- 
-         list.forEach((entityplayer) -> {
--            entityplayer.addEffect(new MobEffectInstance(statusEffectInstance), entity);
-+            entityplayer.addEffect(new MobEffectInstance(mobeffect), entity, cause); // CraftBukkit
-         });
-         return list;
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/effect/OozingMobEffect.java.patch b/paper-server/patches/unapplied/net/minecraft/world/effect/OozingMobEffect.java.patch
deleted file mode 100644
index 4a2bef9952..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/effect/OozingMobEffect.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/effect/OozingMobEffect.java
-+++ b/net/minecraft/world/effect/OozingMobEffect.java
-@@ -52,7 +52,7 @@
-         if (entityslime != null) {
-             entityslime.setSize(2, true);
-             entityslime.moveTo(x, y, z, world.getRandom().nextFloat() * 360.0F, 0.0F);
--            world.addFreshEntity(entityslime);
-+            world.addFreshEntity(entityslime, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.POTION_EFFECT); // CraftBukkit
-         }
-     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/effect/PoisonMobEffect.java.patch b/paper-server/patches/unapplied/net/minecraft/world/effect/PoisonMobEffect.java.patch
deleted file mode 100644
index 367358c2d8..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/effect/PoisonMobEffect.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/effect/PoisonMobEffect.java
-+++ b/net/minecraft/world/effect/PoisonMobEffect.java
-@@ -14,7 +14,7 @@
-     @Override
-     public boolean applyEffectTick(ServerLevel world, LivingEntity entity, int amplifier) {
-         if (entity.getHealth() > 1.0F) {
--            entity.hurtServer(world, entity.damageSources().magic(), 1.0F);
-+            entity.hurtServer(world, entity.damageSources().poison(), 1.0F);  // CraftBukkit - DamageSource.MAGIC -> CraftEventFactory.POISON
-         }
- 
-         return true;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/effect/SaturationMobEffect.java.patch b/paper-server/patches/unapplied/net/minecraft/world/effect/SaturationMobEffect.java.patch
deleted file mode 100644
index 5d38f090c8..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/effect/SaturationMobEffect.java.patch
+++ /dev/null
@@ -1,30 +0,0 @@
---- a/net/minecraft/world/effect/SaturationMobEffect.java
-+++ b/net/minecraft/world/effect/SaturationMobEffect.java
-@@ -3,6 +3,10 @@
- import net.minecraft.server.level.ServerLevel;
- import net.minecraft.world.entity.LivingEntity;
- import net.minecraft.world.entity.player.Player;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.entity.CraftPlayer;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+// CraftBukkit end
- 
- class SaturationMobEffect extends InstantenousMobEffect {
- 
-@@ -13,7 +17,15 @@
-     @Override
-     public boolean applyEffectTick(ServerLevel world, LivingEntity entity, int amplifier) {
-         if (entity instanceof Player entityhuman) {
--            entityhuman.getFoodData().eat(amplifier + 1, 1.0F);
-+            // CraftBukkit start
-+            int oldFoodLevel = entityhuman.getFoodData().foodLevel;
-+            org.bukkit.event.entity.FoodLevelChangeEvent event = CraftEventFactory.callFoodLevelChangeEvent(entityhuman, amplifier + 1 + oldFoodLevel);
-+            if (!event.isCancelled()) {
-+                entityhuman.getFoodData().eat(event.getFoodLevel() - oldFoodLevel, 1.0F);
-+            }
-+
-+            ((CraftPlayer) entityhuman.getBukkitEntity()).sendHealthUpdate();
-+            // CraftBukkit end
-         }
- 
-         return true;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/AgeableMob.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/AgeableMob.java.patch
deleted file mode 100644
index 5748ed58a8..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/AgeableMob.java.patch
+++ /dev/null
@@ -1,74 +0,0 @@
---- a/net/minecraft/world/entity/AgeableMob.java
-+++ b/net/minecraft/world/entity/AgeableMob.java
-@@ -21,12 +21,38 @@
-     protected int age;
-     protected int forcedAge;
-     protected int forcedAgeTimer;
-+    public boolean ageLocked; // CraftBukkit
- 
-     protected AgeableMob(EntityType<? extends AgeableMob> type, Level world) {
-         super(type, world);
-     }
- 
-+    // Spigot start
-     @Override
-+    public void inactiveTick()
-+    {
-+        super.inactiveTick();
-+        if ( this.level().isClientSide || this.ageLocked )
-+        { // CraftBukkit
-+            this.refreshDimensions();
-+        } else
-+        {
-+            int i = this.getAge();
-+
-+            if ( i < 0 )
-+            {
-+                ++i;
-+                this.setAge( i );
-+            } else if ( i > 0 )
-+            {
-+                --i;
-+                this.setAge( i );
-+            }
-+        }
-+    }
-+    // Spigot end
-+
-+    @Override
-     public SpawnGroupData finalizeSpawn(ServerLevelAccessor world, DifficultyInstance difficulty, EntitySpawnReason spawnReason, @Nullable SpawnGroupData entityData) {
-         if (entityData == null) {
-             entityData = new AgeableMob.AgeableMobGroupData(true);
-@@ -60,6 +86,7 @@
-     }
- 
-     public void ageUp(int age, boolean overGrow) {
-+        if (this.ageLocked) return; // Paper - Honor ageLock
-         int j = this.getAge();
-         int k = j;
- 
-@@ -104,6 +131,7 @@
-         super.addAdditionalSaveData(nbt);
-         nbt.putInt("Age", this.getAge());
-         nbt.putInt("ForcedAge", this.forcedAge);
-+        nbt.putBoolean("AgeLocked", this.ageLocked); // CraftBukkit
-     }
- 
-     @Override
-@@ -111,6 +139,7 @@
-         super.readAdditionalSaveData(nbt);
-         this.setAge(nbt.getInt("Age"));
-         this.forcedAge = nbt.getInt("ForcedAge");
-+        this.ageLocked = nbt.getBoolean("AgeLocked"); // CraftBukkit
-     }
- 
-     @Override
-@@ -125,7 +154,7 @@
-     @Override
-     public void aiStep() {
-         super.aiStep();
--        if (this.level().isClientSide) {
-+        if (this.level().isClientSide || this.ageLocked) { // CraftBukkit
-             if (this.forcedAgeTimer > 0) {
-                 if (this.forcedAgeTimer % 4 == 0) {
-                     this.level().addParticle(ParticleTypes.HAPPY_VILLAGER, this.getRandomX(1.0D), this.getRandomY() + 0.5D, this.getRandomZ(1.0D), 0.0D, 0.0D, 0.0D);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/AreaEffectCloud.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/AreaEffectCloud.java.patch
deleted file mode 100644
index 835489e22c..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/AreaEffectCloud.java.patch
+++ /dev/null
@@ -1,160 +0,0 @@
---- a/net/minecraft/world/entity/AreaEffectCloud.java
-+++ b/net/minecraft/world/entity/AreaEffectCloud.java
-@@ -33,6 +33,12 @@
- import net.minecraft.world.level.material.PushReaction;
- import org.slf4j.Logger;
- 
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.entity.CraftLivingEntity;
-+import org.bukkit.entity.LivingEntity;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
-+
- public class AreaEffectCloud extends Entity implements TraceableEntity {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
-@@ -54,7 +60,7 @@
-     public float radiusOnUse;
-     public float radiusPerTick;
-     @Nullable
--    private LivingEntity owner;
-+    private net.minecraft.world.entity.LivingEntity owner;
-     @Nullable
-     public UUID ownerUUID;
- 
-@@ -145,7 +151,19 @@
-         this.duration = duration;
-     }
- 
-+    // Spigot start - copied from below
-     @Override
-+    public void inactiveTick() {
-+        super.inactiveTick();
-+
-+        if (this.tickCount >= this.waitTime + this.duration) {
-+            this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-+            return;
-+        }
-+    }
-+    // Spigot end
-+
-+    @Override
-     public void tick() {
-         super.tick();
-         Level world = this.level();
-@@ -200,7 +218,7 @@
- 
-     private void serverTick(ServerLevel world) {
-         if (this.tickCount >= this.waitTime + this.duration) {
--            this.discard();
-+            this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-         } else {
-             boolean flag = this.isWaiting();
-             boolean flag1 = this.tickCount < this.waitTime;
-@@ -215,7 +233,7 @@
-                 if (this.radiusPerTick != 0.0F) {
-                     f += this.radiusPerTick;
-                     if (f < 0.5F) {
--                        this.discard();
-+                        this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-                         return;
-                     }
- 
-@@ -244,16 +262,17 @@
-                         }
- 
-                         list.addAll(this.potionContents.customEffects());
--                        List<LivingEntity> list1 = this.level().getEntitiesOfClass(LivingEntity.class, this.getBoundingBox());
-+                        List<net.minecraft.world.entity.LivingEntity> list1 = this.level().getEntitiesOfClass(net.minecraft.world.entity.LivingEntity.class, this.getBoundingBox());
- 
-                         if (!list1.isEmpty()) {
-                             Iterator iterator1 = list1.iterator();
- 
-+                            List<LivingEntity> entities = new java.util.ArrayList<LivingEntity>(); // CraftBukkit
-                             while (iterator1.hasNext()) {
--                                LivingEntity entityliving = (LivingEntity) iterator1.next();
-+                                net.minecraft.world.entity.LivingEntity entityliving = (net.minecraft.world.entity.LivingEntity) iterator1.next();
- 
-                                 if (!this.victims.containsKey(entityliving) && entityliving.isAffectedByPotions()) {
--                                    Stream stream = list.stream();
-+                                    Stream<MobEffectInstance> stream = list.stream(); // CraftBukkit - decompile error
- 
-                                     Objects.requireNonNull(entityliving);
-                                     if (!stream.noneMatch(entityliving::canBeAffected)) {
-@@ -262,6 +281,19 @@
-                                         double d2 = d0 * d0 + d1 * d1;
- 
-                                         if (d2 <= (double) (f * f)) {
-+                                            // CraftBukkit start
-+                                            entities.add((LivingEntity) entityliving.getBukkitEntity());
-+                                        }
-+                                    }
-+                                }
-+                            }
-+                            {
-+                                org.bukkit.event.entity.AreaEffectCloudApplyEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callAreaEffectCloudApplyEvent(this, entities);
-+                                if (!event.isCancelled()) {
-+                                    for (LivingEntity entity : event.getAffectedEntities()) {
-+                                        if (entity instanceof CraftLivingEntity) {
-+                                            net.minecraft.world.entity.LivingEntity entityliving = ((CraftLivingEntity) entity).getHandle();
-+                                            // CraftBukkit end
-                                             this.victims.put(entityliving, this.tickCount + this.reapplicationDelay);
-                                             Iterator iterator2 = list.iterator();
- 
-@@ -271,14 +303,14 @@
-                                                 if (((MobEffect) mobeffect1.getEffect().value()).isInstantenous()) {
-                                                     ((MobEffect) mobeffect1.getEffect().value()).applyInstantenousEffect(world, this, this.getOwner(), entityliving, mobeffect1.getAmplifier(), 0.5D);
-                                                 } else {
--                                                    entityliving.addEffect(new MobEffectInstance(mobeffect1), this);
-+                                                    entityliving.addEffect(new MobEffectInstance(mobeffect1), this, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.AREA_EFFECT_CLOUD); // CraftBukkit
-                                                 }
-                                             }
- 
-                                             if (this.radiusOnUse != 0.0F) {
-                                                 f += this.radiusOnUse;
-                                                 if (f < 0.5F) {
--                                                    this.discard();
-+                                                    this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-                                                     return;
-                                                 }
- 
-@@ -288,7 +320,7 @@
-                                             if (this.durationOnUse != 0) {
-                                                 this.duration += this.durationOnUse;
-                                                 if (this.duration <= 0) {
--                                                    this.discard();
-+                                                    this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-                                                     return;
-                                                 }
-                                             }
-@@ -336,14 +368,14 @@
-         this.waitTime = waitTime;
-     }
- 
--    public void setOwner(@Nullable LivingEntity owner) {
-+    public void setOwner(@Nullable net.minecraft.world.entity.LivingEntity owner) {
-         this.owner = owner;
-         this.ownerUUID = owner == null ? null : owner.getUUID();
-     }
- 
-     @Nullable
-     @Override
--    public LivingEntity getOwner() {
-+    public net.minecraft.world.entity.LivingEntity getOwner() {
-         if (this.owner != null && !this.owner.isRemoved()) {
-             return this.owner;
-         } else {
-@@ -353,10 +385,10 @@
-                 if (world instanceof ServerLevel) {
-                     ServerLevel worldserver = (ServerLevel) world;
-                     Entity entity = worldserver.getEntity(this.ownerUUID);
--                    LivingEntity entityliving;
-+                    net.minecraft.world.entity.LivingEntity entityliving;
- 
--                    if (entity instanceof LivingEntity) {
--                        LivingEntity entityliving1 = (LivingEntity) entity;
-+                    if (entity instanceof net.minecraft.world.entity.LivingEntity) {
-+                        net.minecraft.world.entity.LivingEntity entityliving1 = (net.minecraft.world.entity.LivingEntity) entity;
- 
-                         entityliving = entityliving1;
-                     } else {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ConversionType.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ConversionType.java.patch
deleted file mode 100644
index ed6daf39bf..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ConversionType.java.patch
+++ /dev/null
@@ -1,46 +0,0 @@
---- a/net/minecraft/world/entity/ConversionType.java
-+++ b/net/minecraft/world/entity/ConversionType.java
-@@ -4,6 +4,7 @@
- import java.util.Objects;
- import java.util.Optional;
- import java.util.Set;
-+import net.minecraft.core.BlockPos;
- import net.minecraft.world.effect.MobEffectInstance;
- import net.minecraft.world.entity.ai.Brain;
- import net.minecraft.world.entity.ai.memory.MemoryModuleType;
-@@ -11,6 +12,8 @@
- import net.minecraft.world.entity.monster.Zombie;
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.scores.Scoreboard;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public enum ConversionType {
- 
-@@ -31,7 +34,7 @@
-                 while (iterator.hasNext()) {
-                     entity1 = (Entity) iterator.next();
-                     entity1.stopRiding();
--                    entity1.remove(Entity.RemovalReason.DISCARDED);
-+                    entity1.remove(Entity.RemovalReason.DISCARDED, EntityRemoveEvent.Cause.TRANSFORMATION); // CraftBukkit - add Bukkit remove cause
-                 }
- 
-                 entity.startRiding(newEntity);
-@@ -64,7 +67,7 @@
-             newEntity.hurtTime = oldEntity.hurtTime;
-             newEntity.yBodyRot = oldEntity.yBodyRot;
-             newEntity.setOnGround(oldEntity.onGround());
--            Optional optional = oldEntity.getSleepingPos();
-+            Optional<BlockPos> optional = oldEntity.getSleepingPos(); // CraftBukkit - decompile error
- 
-             Objects.requireNonNull(newEntity);
-             optional.ifPresent(newEntity::setSleepingPos);
-@@ -156,7 +159,7 @@
-         newEntity.setNoGravity(oldEntity.isNoGravity());
-         newEntity.setPortalCooldown(oldEntity.getPortalCooldown());
-         newEntity.setSilent(oldEntity.isSilent());
--        Set set = oldEntity.getTags();
-+        Set<String> set = oldEntity.getTags(); // CraftBukkit - decompile error
- 
-         Objects.requireNonNull(newEntity);
-         set.forEach(newEntity::addTag);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/Display.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/Display.java.patch
deleted file mode 100644
index 893b7a911c..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/Display.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/entity/Display.java
-+++ b/net/minecraft/world/entity/Display.java
-@@ -903,7 +903,7 @@
-             b = loadFlag(b, nbt, "default_background", (byte)4);
-             Optional<Display.TextDisplay.Align> optional = Display.TextDisplay.Align.CODEC
-                 .decode(NbtOps.INSTANCE, nbt.get("alignment"))
--                .resultOrPartial(Util.prefix("Display entity", Display.LOGGER::error))
-+                .result() // Paper - Hide text display error on spawn
-                 .map(Pair::getFirst);
-             if (optional.isPresent()) {
-                 b = switch ((Display.TextDisplay.Align)optional.get()) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/Entity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/Entity.java.patch
deleted file mode 100644
index a274089f56..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/Entity.java.patch
+++ /dev/null
@@ -1,1994 +0,0 @@
---- a/net/minecraft/world/entity/Entity.java
-+++ b/net/minecraft/world/entity/Entity.java
-@@ -59,6 +59,8 @@
- import net.minecraft.network.protocol.Packet;
- import net.minecraft.network.protocol.game.ClientGamePacketListener;
- import net.minecraft.network.protocol.game.ClientboundAddEntityPacket;
-+import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket;
-+import net.minecraft.network.protocol.game.ClientboundSetEntityLinkPacket;
- import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket;
- import net.minecraft.network.protocol.game.VecDeltaCodec;
- import net.minecraft.network.syncher.EntityDataAccessor;
-@@ -101,8 +103,6 @@
- import net.minecraft.world.level.ChunkPos;
- import net.minecraft.world.level.ClipContext;
- import net.minecraft.world.level.Explosion;
--import net.minecraft.world.level.ItemLike;
--import net.minecraft.world.level.Level;
- import net.minecraft.world.level.block.Block;
- import net.minecraft.world.level.block.Blocks;
- import net.minecraft.world.level.block.FenceGateBlock;
-@@ -138,9 +138,153 @@
- import net.minecraft.world.scores.ScoreHolder;
- import net.minecraft.world.scores.Team;
- import org.slf4j.Logger;
-+import net.minecraft.world.level.GameRules;
-+import net.minecraft.world.level.ItemLike;
-+import net.minecraft.world.level.Level;
-+import org.bukkit.Bukkit;
-+import org.bukkit.Location;
-+import org.bukkit.Server;
-+import org.bukkit.block.BlockFace;
-+import org.bukkit.command.CommandSender;
-+import org.bukkit.entity.Hanging;
-+import org.bukkit.entity.LivingEntity;
-+import org.bukkit.entity.Vehicle;
-+import org.bukkit.event.entity.EntityCombustByEntityEvent;
-+import org.bukkit.event.hanging.HangingBreakByEntityEvent;
-+import org.bukkit.event.vehicle.VehicleBlockCollisionEvent;
-+import org.bukkit.event.vehicle.VehicleEnterEvent;
-+import org.bukkit.event.vehicle.VehicleExitEvent;
-+import org.bukkit.craftbukkit.CraftWorld;
-+import org.bukkit.craftbukkit.entity.CraftEntity;
-+import org.bukkit.craftbukkit.entity.CraftPlayer;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.craftbukkit.event.CraftPortalEvent;
-+import org.bukkit.craftbukkit.util.CraftLocation;
-+import org.bukkit.entity.Pose;
-+import org.bukkit.event.entity.EntityAirChangeEvent;
-+import org.bukkit.event.entity.EntityCombustEvent;
-+import org.bukkit.event.entity.EntityDismountEvent;
-+import org.bukkit.event.entity.EntityDropItemEvent;
-+import org.bukkit.event.entity.EntityMountEvent;
-+import org.bukkit.event.entity.EntityPortalEvent;
-+import org.bukkit.event.entity.EntityPoseChangeEvent;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.event.entity.EntityTeleportEvent;
-+import org.bukkit.event.entity.EntityUnleashEvent;
-+import org.bukkit.event.entity.EntityUnleashEvent.UnleashReason;
-+import org.bukkit.event.player.PlayerTeleportEvent;
-+import org.bukkit.plugin.PluginManager;
-+// CraftBukkit end
- 
- public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess, ScoreHolder {
- 
-+    // CraftBukkit start
-+    private static final int CURRENT_LEVEL = 2;
-+    public boolean preserveMotion = true; // Paper - Fix Entity Teleportation and cancel velocity if teleported; keep initial motion on first setPositionRotation
-+    static boolean isLevelAtLeast(CompoundTag tag, int level) {
-+        return tag.contains("Bukkit.updateLevel") && tag.getInt("Bukkit.updateLevel") >= level;
-+    }
-+
-+    // Paper start - Share random for entities to make them more random
-+    public static RandomSource SHARED_RANDOM = new RandomRandomSource();
-+    private static final class RandomRandomSource extends java.util.Random implements net.minecraft.world.level.levelgen.BitRandomSource {
-+        private boolean locked = false;
-+
-+        @Override
-+        public synchronized void setSeed(long seed) {
-+            if (locked) {
-+                LOGGER.error("Ignoring setSeed on Entity.SHARED_RANDOM", new Throwable());
-+            } else {
-+                super.setSeed(seed);
-+                locked = true;
-+            }
-+        }
-+
-+        @Override
-+        public RandomSource fork() {
-+            return new net.minecraft.world.level.levelgen.LegacyRandomSource(this.nextLong());
-+        }
-+
-+        @Override
-+        public net.minecraft.world.level.levelgen.PositionalRandomFactory forkPositional() {
-+            return new net.minecraft.world.level.levelgen.LegacyRandomSource.LegacyPositionalRandomFactory(this.nextLong());
-+        }
-+
-+        // these below are added to fix reobf issues that I don't wanna deal with right now
-+        @Override
-+        public int next(int bits) {
-+            return super.next(bits);
-+        }
-+
-+        @Override
-+        public int nextInt(int origin, int bound) {
-+            return net.minecraft.world.level.levelgen.BitRandomSource.super.nextInt(origin, bound);
-+        }
-+
-+        @Override
-+        public long nextLong() {
-+            return net.minecraft.world.level.levelgen.BitRandomSource.super.nextLong();
-+        }
-+
-+        @Override
-+        public int nextInt() {
-+            return net.minecraft.world.level.levelgen.BitRandomSource.super.nextInt();
-+        }
-+
-+        @Override
-+        public int nextInt(int bound) {
-+            return net.minecraft.world.level.levelgen.BitRandomSource.super.nextInt(bound);
-+        }
-+
-+        @Override
-+        public boolean nextBoolean() {
-+            return net.minecraft.world.level.levelgen.BitRandomSource.super.nextBoolean();
-+        }
-+
-+        @Override
-+        public float nextFloat() {
-+            return net.minecraft.world.level.levelgen.BitRandomSource.super.nextFloat();
-+        }
-+
-+        @Override
-+        public double nextDouble() {
-+            return net.minecraft.world.level.levelgen.BitRandomSource.super.nextDouble();
-+        }
-+
-+        @Override
-+        public double nextGaussian() {
-+            return super.nextGaussian();
-+        }
-+    }
-+    // Paper end - Share random for entities to make them more random
-+    public org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason spawnReason; // Paper - Entity#getEntitySpawnReason
-+
-+    private CraftEntity bukkitEntity;
-+
-+    public CraftEntity getBukkitEntity() {
-+        if (this.bukkitEntity == null) {
-+            // Paper start - Folia schedulers
-+            synchronized (this) {
-+                if (this.bukkitEntity == null) {
-+                    return this.bukkitEntity = CraftEntity.getEntity(this.level.getCraftServer(), this);
-+                }
-+            }
-+            // Paper end - Folia schedulers
-+        }
-+        return this.bukkitEntity;
-+    }
-+    // Paper start
-+    public CraftEntity getBukkitEntityRaw() {
-+        return this.bukkitEntity;
-+    }
-+    // Paper end
-+
-+    // CraftBukkit - SPIGOT-6907: re-implement LivingEntity#setMaximumAir()
-+    public int getDefaultMaxAirSupply() {
-+        return Entity.TOTAL_AIR_SUPPLY;
-+    }
-+    // CraftBukkit end
-+
-     private static final Logger LOGGER = LogUtils.getLogger();
-     public static final String ID_TAG = "id";
-     public static final String PASSENGERS_TAG = "Passengers";
-@@ -224,7 +368,7 @@
-     private static final EntityDataAccessor<Boolean> DATA_CUSTOM_NAME_VISIBLE = SynchedEntityData.defineId(Entity.class, EntityDataSerializers.BOOLEAN);
-     private static final EntityDataAccessor<Boolean> DATA_SILENT = SynchedEntityData.defineId(Entity.class, EntityDataSerializers.BOOLEAN);
-     private static final EntityDataAccessor<Boolean> DATA_NO_GRAVITY = SynchedEntityData.defineId(Entity.class, EntityDataSerializers.BOOLEAN);
--    protected static final EntityDataAccessor<Pose> DATA_POSE = SynchedEntityData.defineId(Entity.class, EntityDataSerializers.POSE);
-+    protected static final EntityDataAccessor<net.minecraft.world.entity.Pose> DATA_POSE = SynchedEntityData.defineId(Entity.class, EntityDataSerializers.POSE);
-     private static final EntityDataAccessor<Integer> DATA_TICKS_FROZEN = SynchedEntityData.defineId(Entity.class, EntityDataSerializers.INT);
-     private EntityInLevelCallback levelCallback;
-     private final VecDeltaCodec packetPositionCodec;
-@@ -253,15 +397,78 @@
-     private final List<Entity.Movement> movementThisTick;
-     private final Set<BlockState> blocksInside;
-     private final LongSet visitedBlocks;
-+    // CraftBukkit start
-+    public boolean forceDrops;
-+    public boolean persist = true;
-+    public boolean visibleByDefault = true;
-+    public boolean valid;
-+    public boolean inWorld = false;
-+    public boolean generation;
-+    public int maxAirTicks = this.getDefaultMaxAirSupply(); // CraftBukkit - SPIGOT-6907: re-implement LivingEntity#setMaximumAir()
-+    @Nullable // Paper - Refresh ProjectileSource for projectiles
-+    public org.bukkit.projectiles.ProjectileSource projectileSource; // For projectiles only
-+    public boolean lastDamageCancelled; // SPIGOT-5339, SPIGOT-6252, SPIGOT-6777: Keep track if the event was canceled
-+    public boolean persistentInvisibility = false;
-+    public BlockPos lastLavaContact;
-+    // Marks an entity, that it was removed by a plugin via Entity#remove
-+    // Main use case currently is for SPIGOT-7487, preventing dropping of leash when leash is removed
-+    public boolean pluginRemoved = false;
-+    // Spigot start
-+    public final org.spigotmc.ActivationRange.ActivationType activationType = org.spigotmc.ActivationRange.initializeEntityActivationType(this);
-+    public final boolean defaultActivationState;
-+    public long activatedTick = Integer.MIN_VALUE;
-+    public void inactiveTick() { }
-+    // Spigot end
-+    protected int numCollisions = 0; // Paper - Cap entity collisions
-+    public boolean fromNetherPortal; // Paper - Add option to nerf pigmen from nether portals
-+    public boolean spawnedViaMobSpawner; // Paper - Yes this name is similar to above, upstream took the better one
-+    // Paper start - Entity origin API
-+    @javax.annotation.Nullable
-+    private org.bukkit.util.Vector origin;
-+    @javax.annotation.Nullable
-+    private UUID originWorld;
-+    public boolean freezeLocked = false; // Paper - Freeze Tick Lock API
-+    public boolean fixedPose = false; // Paper - Expand Pose API
-+    private final int despawnTime; // Paper - entity despawn time limit
- 
-+    public void setOrigin(@javax.annotation.Nonnull Location location) {
-+        this.origin = location.toVector();
-+        this.originWorld = location.getWorld().getUID();
-+    }
-+
-+    @javax.annotation.Nullable
-+    public org.bukkit.util.Vector getOriginVector() {
-+        return this.origin != null ? this.origin.clone() : null;
-+    }
-+
-+    @javax.annotation.Nullable
-+    public UUID getOriginWorld() {
-+        return this.originWorld;
-+    }
-+    // Paper end - Entity origin API
-+    public float getBukkitYaw() {
-+        return this.yRot;
-+    }
-+
-+    public boolean isChunkLoaded() {
-+        return this.level.hasChunk((int) Math.floor(this.getX()) >> 4, (int) Math.floor(this.getZ()) >> 4);
-+    }
-+    // CraftBukkit end
-+    // Paper start
-+    public final AABB getBoundingBoxAt(double x, double y, double z) {
-+        return this.dimensions.makeBoundingBox(x, y, z);
-+    }
-+    // Paper end
-+
-     public Entity(EntityType<?> type, Level world) {
-         this.id = Entity.ENTITY_COUNTER.incrementAndGet();
-+        this.despawnTime = type == EntityType.PLAYER ? -1 : world.paperConfig().entities.spawning.despawnTime.getOrDefault(type, io.papermc.paper.configuration.type.number.IntOr.Disabled.DISABLED).or(-1); // Paper - entity despawn time limit
-         this.passengers = ImmutableList.of();
-         this.deltaMovement = Vec3.ZERO;
-         this.bb = Entity.INITIAL_AABB;
-         this.stuckSpeedMultiplier = Vec3.ZERO;
-         this.nextStep = 1.0F;
--        this.random = RandomSource.create();
-+        this.random = SHARED_RANDOM; // Paper - Share random for entities to make them more random
-         this.remainingFireTicks = -this.getFireImmuneTicks();
-         this.fluidHeight = new Object2DoubleArrayMap(2);
-         this.fluidOnEyes = new HashSet();
-@@ -270,7 +477,7 @@
-         this.packetPositionCodec = new VecDeltaCodec();
-         this.uuid = Mth.createInsecureUUID(this.random);
-         this.stringUUID = this.uuid.toString();
--        this.tags = Sets.newHashSet();
-+        this.tags = new io.papermc.paper.util.SizeLimitedSet<>(new it.unimi.dsi.fastutil.objects.ObjectOpenHashSet<>(), MAX_ENTITY_TAG_COUNT); // Paper - fully limit tag size - replace set impl
-         this.pistonDeltas = new double[]{0.0D, 0.0D, 0.0D};
-         this.mainSupportingBlockPos = Optional.empty();
-         this.onGroundNoBlocks = false;
-@@ -284,6 +491,13 @@
-         this.position = Vec3.ZERO;
-         this.blockPosition = BlockPos.ZERO;
-         this.chunkPosition = ChunkPos.ZERO;
-+        // Spigot start
-+        if (world != null) {
-+            this.defaultActivationState = org.spigotmc.ActivationRange.initializeEntityActivationState(this, world.spigotConfig);
-+        } else {
-+            this.defaultActivationState = false;
-+        }
-+        // Spigot end
-         SynchedEntityData.Builder datawatcher_a = new SynchedEntityData.Builder(this);
- 
-         datawatcher_a.define(Entity.DATA_SHARED_FLAGS_ID, (byte) 0);
-@@ -292,7 +506,7 @@
-         datawatcher_a.define(Entity.DATA_CUSTOM_NAME, Optional.empty());
-         datawatcher_a.define(Entity.DATA_SILENT, false);
-         datawatcher_a.define(Entity.DATA_NO_GRAVITY, false);
--        datawatcher_a.define(Entity.DATA_POSE, Pose.STANDING);
-+        datawatcher_a.define(Entity.DATA_POSE, net.minecraft.world.entity.Pose.STANDING);
-         datawatcher_a.define(Entity.DATA_TICKS_FROZEN, 0);
-         this.defineSynchedData(datawatcher_a);
-         this.entityData = datawatcher_a.build();
-@@ -354,7 +568,7 @@
-     }
- 
-     public boolean addTag(String tag) {
--        return this.tags.size() >= 1024 ? false : this.tags.add(tag);
-+        return this.tags.add(tag); // Paper - fully limit tag size - replace set impl
-     }
- 
-     public boolean removeTag(String tag) {
-@@ -362,20 +576,68 @@
-     }
- 
-     public void kill(ServerLevel world) {
--        this.remove(Entity.RemovalReason.KILLED);
-+        this.remove(Entity.RemovalReason.KILLED, EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause
-         this.gameEvent(GameEvent.ENTITY_DIE);
-     }
- 
-     public final void discard() {
--        this.remove(Entity.RemovalReason.DISCARDED);
-+        // CraftBukkit start - add Bukkit remove cause
-+        this.discard(null);
-     }
- 
-+    public final void discard(EntityRemoveEvent.Cause cause) {
-+        this.remove(Entity.RemovalReason.DISCARDED, cause);
-+        // CraftBukkit end
-+    }
-+
-     protected abstract void defineSynchedData(SynchedEntityData.Builder builder);
- 
-     public SynchedEntityData getEntityData() {
-         return this.entityData;
-     }
- 
-+    // CraftBukkit start
-+    public void refreshEntityData(ServerPlayer to) {
-+        List<SynchedEntityData.DataValue<?>> list = this.entityData.packAll(); // Paper - Update EVERYTHING not just not default
-+
-+        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 (player.getBukkitEntity().canSee(this.getBukkitEntity())) {
-+            ServerLevel world = (net.minecraft.server.level.ServerLevel)this.level();
-+            net.minecraft.server.level.ChunkMap.TrackedEntity tracker = world == null ? null : world.getChunkSource().chunkMap.entityMap.get(this.getId());
-+            if (tracker == null) {
-+                return;
-+            }
-+            final net.minecraft.server.level.ServerEntity serverEntity = tracker.serverEntity;
-+            final List<net.minecraft.network.protocol.Packet<? super net.minecraft.network.protocol.game.ClientGamePacketListener>> 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<EntityDataAccessor<?>> keys, ServerPlayer to) {
-+        if (!to.getBukkitEntity().canSee(this.getBukkitEntity())) {
-+            return;
-+        }
-+
-+        final List<SynchedEntityData.DataValue<?>> 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;
-     }
-@@ -385,22 +647,39 @@
-     }
- 
-     public void remove(Entity.RemovalReason reason) {
--        this.setRemoved(reason);
-+        // CraftBukkit start - add Bukkit remove cause
-+        this.setRemoved(reason, null);
-     }
- 
-+    public void remove(Entity.RemovalReason entity_removalreason, EntityRemoveEvent.Cause cause) {
-+        this.setRemoved(entity_removalreason, cause);
-+        // CraftBukkit end
-+    }
-+
-     public void onClientRemoval() {}
- 
-     public void onRemoval(Entity.RemovalReason reason) {}
- 
--    public void setPose(Pose pose) {
-+    public void setPose(net.minecraft.world.entity.Pose pose) {
-+        if (this.fixedPose) return; // Paper - Expand Pose API
-+        // CraftBukkit start
-+        if (pose == this.getPose()) {
-+            return;
-+        }
-+        // Paper start - Don't fire sync event during generation
-+        if (!this.generation) {
-+            this.level.getCraftServer().getPluginManager().callEvent(new EntityPoseChangeEvent(this.getBukkitEntity(), Pose.values()[pose.ordinal()]));
-+        }
-+        // Paper end - Don't fire sync event during generation
-+        // CraftBukkit end
-         this.entityData.set(Entity.DATA_POSE, pose);
-     }
- 
--    public Pose getPose() {
--        return (Pose) this.entityData.get(Entity.DATA_POSE);
-+    public net.minecraft.world.entity.Pose getPose() {
-+        return (net.minecraft.world.entity.Pose) this.entityData.get(Entity.DATA_POSE);
-     }
- 
--    public boolean hasPose(Pose pose) {
-+    public boolean hasPose(net.minecraft.world.entity.Pose pose) {
-         return this.getPose() == pose;
-     }
- 
-@@ -417,6 +696,33 @@
-     }
- 
-     public void setRot(float yaw, float pitch) {
-+        // CraftBukkit start - yaw was sometimes set to NaN, so we need to set it back to 0
-+        if (Float.isNaN(yaw)) {
-+            yaw = 0;
-+        }
-+
-+        if (yaw == Float.POSITIVE_INFINITY || yaw == Float.NEGATIVE_INFINITY) {
-+            if (this instanceof ServerPlayer) {
-+                this.level.getCraftServer().getLogger().warning(this.getScoreboardName() + " was caught trying to crash the server with an invalid yaw");
-+                ((CraftPlayer) this.getBukkitEntity()).kickPlayer("Infinite yaw (Hacking?)");
-+            }
-+            yaw = 0;
-+        }
-+
-+        // pitch was sometimes set to NaN, so we need to set it back to 0
-+        if (Float.isNaN(pitch)) {
-+            pitch = 0;
-+        }
-+
-+        if (pitch == Float.POSITIVE_INFINITY || pitch == Float.NEGATIVE_INFINITY) {
-+            if (this instanceof ServerPlayer) {
-+                this.level.getCraftServer().getLogger().warning(this.getScoreboardName() + " was caught trying to crash the server with an invalid pitch");
-+                ((CraftPlayer) this.getBukkitEntity()).kickPlayer("Infinite pitch (Hacking?)");
-+            }
-+            pitch = 0;
-+        }
-+        // CraftBukkit end
-+
-         this.setYRot(yaw % 360.0F);
-         this.setXRot(pitch % 360.0F);
-     }
-@@ -426,8 +732,8 @@
-     }
- 
-     public void setPos(double x, double y, double z) {
--        this.setPosRaw(x, y, z);
--        this.setBoundingBox(this.makeBoundingBox());
-+        this.setPosRaw(x, y, z, true); // Paper - Block invalid positions and bounding box; force update
-+        // this.setBoundingBox(this.makeBoundingBox()); // Paper - Block invalid positions and bounding box; move into setPosRaw
-     }
- 
-     protected final AABB makeBoundingBox() {
-@@ -459,13 +765,29 @@
-     }
- 
-     public void tick() {
-+        // Paper start - entity despawn time limit
-+        if (this.despawnTime >= 0 && this.tickCount >= this.despawnTime) {
-+            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN);
-+            return;
-+        }
-+        // Paper end - entity despawn time limit
-         this.baseTick();
-     }
- 
-+    // CraftBukkit start
-+    public void postTick() {
-+        // No clean way to break out of ticking once the entity has been copied to a new world, so instead we move the portalling later in the tick cycle
-+        if (!(this instanceof ServerPlayer) && this.isAlive()) { // Paper - don't attempt to teleport dead entities
-+            this.handlePortal();
-+        }
-+    }
-+    // CraftBukkit end
-+
-     public void baseTick() {
-         ProfilerFiller gameprofilerfiller = Profiler.get();
- 
-         gameprofilerfiller.push("entityBaseTick");
-+        if (firstTick && this instanceof net.minecraft.world.entity.NeutralMob neutralMob) neutralMob.tickInitialPersistentAnger(level); // Paper - Prevent entity loading causing async lookups
-         this.inBlockState = null;
-         if (this.isPassenger() && this.getVehicle().isRemoved()) {
-             this.stopRiding();
-@@ -475,7 +797,7 @@
-             --this.boardingCooldown;
-         }
- 
--        this.handlePortal();
-+        if (this instanceof ServerPlayer) this.handlePortal(); // CraftBukkit - // Moved up to postTick
-         if (this.canSpawnSprintParticle()) {
-             this.spawnSprintParticle();
-         }
-@@ -502,7 +824,7 @@
-                     this.setRemainingFireTicks(this.remainingFireTicks - 1);
-                 }
- 
--                if (this.getTicksFrozen() > 0) {
-+                if (this.getTicksFrozen() > 0 && !freezeLocked) { // Paper - Freeze Tick Lock API
-                     this.setTicksFrozen(0);
-                     this.level().levelEvent((Player) null, 1009, this.blockPosition, 1);
-                 }
-@@ -514,6 +836,10 @@
-         if (this.isInLava()) {
-             this.lavaHurt();
-             this.fallDistance *= 0.5F;
-+            // CraftBukkit start
-+        } else {
-+            this.lastLavaContact = null;
-+            // CraftBukkit end
-         }
- 
-         this.checkBelowWorld();
-@@ -525,7 +851,7 @@
-         world = this.level();
-         if (world instanceof ServerLevel worldserver) {
-             if (this instanceof Leashable) {
--                Leashable.tickLeash(worldserver, (Entity) ((Leashable) this));
-+                Leashable.tickLeash(worldserver, (Entity & Leashable) this); // CraftBukkit - decompile error
-             }
-         }
- 
-@@ -537,7 +863,12 @@
-     }
- 
-     public void checkBelowWorld() {
--        if (this.getY() < (double) (this.level().getMinY() - 64)) {
-+        if (!this.level.getWorld().isVoidDamageEnabled()) return; // Paper - check if void damage is enabled on the world
-+        // Paper start - Configurable nether ceiling damage
-+        if (this.getY() < (double) (this.level.getMinY() + this.level.getWorld().getVoidDamageMinBuildHeightOffset()) || (this.level.getWorld().getEnvironment() == org.bukkit.World.Environment.NETHER // Paper - use configured min build height offset
-+            && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.test(v -> this.getY() >= v)
-+            && (!(this instanceof Player player) || !player.getAbilities().invulnerable))) {
-+            // Paper end - Configurable nether ceiling damage
-             this.onBelowWorld();
-         }
- 
-@@ -568,15 +899,32 @@
- 
-     public void lavaHurt() {
-         if (!this.fireImmune()) {
--            this.igniteForSeconds(15.0F);
-+            // CraftBukkit start - Fallen in lava TODO: this event spams!
-+            if (this instanceof net.minecraft.world.entity.LivingEntity && this.remainingFireTicks <= 0) {
-+                // not on fire yet
-+                org.bukkit.block.Block damager = (this.lastLavaContact == null) ? null : org.bukkit.craftbukkit.block.CraftBlock.at(this.level, this.lastLavaContact);
-+                org.bukkit.entity.Entity damagee = this.getBukkitEntity();
-+                EntityCombustEvent combustEvent = new org.bukkit.event.entity.EntityCombustByBlockEvent(damager, damagee, 15);
-+                this.level.getCraftServer().getPluginManager().callEvent(combustEvent);
-+
-+                if (!combustEvent.isCancelled()) {
-+                    this.igniteForSeconds(combustEvent.getDuration(), false);
-+                }
-+            } else {
-+                // This will be called every single tick the entity is in lava, so don't throw an event
-+                this.igniteForSeconds(15.0F, false);
-+            }
-+            // CraftBukkit end
-             Level world = this.level();
- 
-             if (world instanceof ServerLevel) {
-                 ServerLevel worldserver = (ServerLevel) world;
- 
--                if (this.hurtServer(worldserver, this.damageSources().lava(), 4.0F) && this.shouldPlayLavaHurtSound() && !this.isSilent()) {
-+                // CraftBukkit start
-+                if (this.hurtServer(worldserver, this.damageSources().lava().directBlock(this.level, this.lastLavaContact), 4.0F) && this.shouldPlayLavaHurtSound() && !this.isSilent()) {
-                     worldserver.playSound((Player) null, this.getX(), this.getY(), this.getZ(), SoundEvents.GENERIC_BURN, this.getSoundSource(), 0.4F, 2.0F + this.random.nextFloat() * 0.4F);
-                 }
-+                // CraftBukkit end - we also don't throw an event unless the object in lava is living, to save on some event calls
-             }
- 
-         }
-@@ -587,9 +935,25 @@
-     }
- 
-     public final void igniteForSeconds(float seconds) {
--        this.igniteForTicks(Mth.floor(seconds * 20.0F));
-+        // CraftBukkit start
-+        this.igniteForSeconds(seconds, true);
-     }
- 
-+    public final void igniteForSeconds(float f, boolean callEvent) {
-+        if (callEvent) {
-+            EntityCombustEvent event = new EntityCombustEvent(this.getBukkitEntity(), f);
-+            this.level.getCraftServer().getPluginManager().callEvent(event);
-+
-+            if (event.isCancelled()) {
-+                return;
-+            }
-+
-+            f = event.getDuration();
-+        }
-+        // CraftBukkit end
-+        this.igniteForTicks(Mth.floor(f * 20.0F));
-+    }
-+
-     public void igniteForTicks(int ticks) {
-         if (this.remainingFireTicks < ticks) {
-             this.setRemainingFireTicks(ticks);
-@@ -610,7 +974,7 @@
-     }
- 
-     protected void onBelowWorld() {
--        this.discard();
-+        this.discard(EntityRemoveEvent.Cause.OUT_OF_WORLD); // CraftBukkit - add Bukkit remove cause
-     }
- 
-     public boolean isFree(double offsetX, double offsetY, double offsetZ) {
-@@ -672,6 +1036,7 @@
-     }
- 
-     public void move(MoverType type, Vec3 movement) {
-+        final Vec3 originalMovement = movement; // Paper - Expose pre-collision velocity
-         if (this.noPhysics) {
-             this.setPos(this.getX() + movement.x, this.getY() + movement.y, this.getZ() + movement.z);
-         } else {
-@@ -747,8 +1112,30 @@
- 
-                     if (movement.y != vec3d1.y) {
-                         block.updateEntityMovementAfterFallOn(this.level(), this);
-+                    }
-+                }
-+
-+                // CraftBukkit start
-+                if (this.horizontalCollision && this.getBukkitEntity() instanceof Vehicle) {
-+                    Vehicle vehicle = (Vehicle) this.getBukkitEntity();
-+                    org.bukkit.block.Block bl = this.level.getWorld().getBlockAt(Mth.floor(this.getX()), Mth.floor(this.getY()), Mth.floor(this.getZ()));
-+
-+                    if (movement.x > vec3d1.x) {
-+                        bl = bl.getRelative(BlockFace.EAST);
-+                    } else if (movement.x < vec3d1.x) {
-+                        bl = bl.getRelative(BlockFace.WEST);
-+                    } else if (movement.z > vec3d1.z) {
-+                        bl = bl.getRelative(BlockFace.SOUTH);
-+                    } else if (movement.z < vec3d1.z) {
-+                        bl = bl.getRelative(BlockFace.NORTH);
-+                    }
-+
-+                    if (!bl.getType().isAir()) {
-+                        VehicleBlockCollisionEvent event = new VehicleBlockCollisionEvent(vehicle, bl, org.bukkit.craftbukkit.util.CraftVector.toBukkit(originalMovement)); // Paper - Expose pre-collision velocity
-+                        this.level.getCraftServer().getPluginManager().callEvent(event);
-                     }
-                 }
-+                // CraftBukkit end
- 
-                 if (!this.level().isClientSide() || this.isControlledByLocalInstance()) {
-                     Entity.MovementEmission entity_movementemission = this.getMovementEmission();
-@@ -913,7 +1300,7 @@
-     }
- 
-     protected BlockPos getOnPos(float offset) {
--        if (this.mainSupportingBlockPos.isPresent()) {
-+        if (this.mainSupportingBlockPos.isPresent() && this.level().getChunkIfLoadedImmediately(this.mainSupportingBlockPos.get()) != null) { // Paper - ensure no loads
-             BlockPos blockposition = (BlockPos) this.mainSupportingBlockPos.get();
- 
-             if (offset <= 1.0E-5F) {
-@@ -1133,6 +1520,20 @@
-         return SoundEvents.GENERIC_SPLASH;
-     }
- 
-+    // CraftBukkit start - Add delegate methods
-+    public SoundEvent getSwimSound0() {
-+        return this.getSwimSound();
-+    }
-+
-+    public SoundEvent getSwimSplashSound0() {
-+        return this.getSwimSplashSound();
-+    }
-+
-+    public SoundEvent getSwimHighSpeedSplashSound0() {
-+        return this.getSwimHighSpeedSplashSound();
-+    }
-+    // CraftBukkit end
-+
-     public void recordMovementThroughBlocks(Vec3 oldPos, Vec3 newPos) {
-         this.movementThisTick.add(new Entity.Movement(oldPos, newPos));
-     }
-@@ -1599,6 +2000,7 @@
-         this.setXRot(Mth.clamp(pitch, -90.0F, 90.0F) % 360.0F);
-         this.yRotO = this.getYRot();
-         this.xRotO = this.getXRot();
-+        this.setYHeadRot(yaw); // Paper - Update head rotation
-     }
- 
-     public void absMoveTo(double x, double y, double z) {
-@@ -1609,6 +2011,7 @@
-         this.yo = y;
-         this.zo = d4;
-         this.setPos(d3, y, d4);
-+        if (this.valid) this.level.getChunk((int) Math.floor(this.getX()) >> 4, (int) Math.floor(this.getZ()) >> 4); // CraftBukkit
-     }
- 
-     public void moveTo(Vec3 pos) {
-@@ -1628,11 +2031,19 @@
-     }
- 
-     public void moveTo(double x, double y, double z, float yaw, float pitch) {
-+        // Paper start - Fix Entity Teleportation and cancel velocity if teleported
-+        if (!preserveMotion) {
-+            this.deltaMovement = Vec3.ZERO;
-+        } else {
-+            this.preserveMotion = false;
-+        }
-+        // Paper end - Fix Entity Teleportation and cancel velocity if teleported
-         this.setPosRaw(x, y, z);
-         this.setYRot(yaw);
-         this.setXRot(pitch);
-         this.setOldPosAndRot();
-         this.reapplyPosition();
-+        this.setYHeadRot(yaw); // Paper - Update head rotation
-     }
- 
-     public final void setOldPosAndRot() {
-@@ -1701,6 +2112,7 @@
-     public void push(Entity entity) {
-         if (!this.isPassengerOfSameVehicle(entity)) {
-             if (!entity.noPhysics && !this.noPhysics) {
-+                if (this.level.paperConfig().collisions.onlyPlayersCollide && !(entity instanceof ServerPlayer || this instanceof ServerPlayer)) return; // Paper - Collision option for requiring a player participant
-                 double d0 = entity.getX() - this.getX();
-                 double d1 = entity.getZ() - this.getZ();
-                 double d2 = Mth.absMax(d0, d1);
-@@ -1737,7 +2149,21 @@
-     }
- 
-     public void push(double deltaX, double deltaY, double deltaZ) {
--        this.setDeltaMovement(this.getDeltaMovement().add(deltaX, deltaY, deltaZ));
-+        // Paper start - Add EntityKnockbackByEntityEvent and EntityPushedByEntityAttackEvent
-+        this.push(deltaX, deltaY, deltaZ, null);
-+    }
-+
-+    public void push(double deltaX, double deltaY, double deltaZ, @Nullable Entity pushingEntity) {
-+        org.bukkit.util.Vector delta = new org.bukkit.util.Vector(deltaX, deltaY, deltaZ);
-+        if (pushingEntity != null) {
-+            io.papermc.paper.event.entity.EntityPushedByEntityAttackEvent event = new io.papermc.paper.event.entity.EntityPushedByEntityAttackEvent(this.getBukkitEntity(), io.papermc.paper.event.entity.EntityKnockbackEvent.Cause.PUSH, pushingEntity.getBukkitEntity(), delta);
-+            if (!event.callEvent()) {
-+                return;
-+            }
-+            delta = event.getKnockback();
-+        }
-+        this.setDeltaMovement(this.getDeltaMovement().add(delta.getX(), delta.getY(), delta.getZ()));
-+        // Paper end - Add EntityKnockbackByEntityEvent and EntityPushedByEntityAttackEvent
-         this.hasImpulse = true;
-     }
- 
-@@ -1858,9 +2284,21 @@
-     }
- 
-     public boolean isPushable() {
-+        // Paper start - Climbing should not bypass cramming gamerule
-+        return isCollidable(false);
-+    }
-+
-+    public boolean isCollidable(boolean ignoreClimbing) {
-+        // Paper end - Climbing should not bypass cramming gamerule
-         return false;
-     }
- 
-+    // CraftBukkit start - collidable API
-+    public boolean canCollideWithBukkit(Entity entity) {
-+        return this.isPushable();
-+    }
-+    // CraftBukkit end
-+
-     public void awardKillScore(Entity entityKilled, DamageSource damageSource) {
-         if (entityKilled instanceof ServerPlayer) {
-             CriteriaTriggers.ENTITY_KILLED_PLAYER.trigger((ServerPlayer) entityKilled, this, damageSource);
-@@ -1889,74 +2327,133 @@
-     }
- 
-     public boolean saveAsPassenger(CompoundTag nbt) {
-+        // CraftBukkit start - allow excluding certain data when saving
-+        return this.saveAsPassenger(nbt, true);
-+    }
-+
-+    public boolean saveAsPassenger(CompoundTag nbttagcompound, boolean includeAll) {
-+        // CraftBukkit end
-         if (this.removalReason != null && !this.removalReason.shouldSave()) {
-             return false;
-         } else {
-             String s = this.getEncodeId();
- 
--            if (s == null) {
-+            if (!this.persist || s == null) { // CraftBukkit - persist flag
-                 return false;
-             } else {
--                nbt.putString("id", s);
--                this.saveWithoutId(nbt);
-+                nbttagcompound.putString("id", s);
-+                this.saveWithoutId(nbttagcompound, includeAll); // CraftBukkit - pass on includeAll
-                 return true;
-             }
-         }
-     }
- 
-+    // Paper start - Entity serialization api
-+    public boolean serializeEntity(CompoundTag compound) {
-+        List<Entity> pass = new java.util.ArrayList<>(this.getPassengers());
-+        this.passengers = ImmutableList.of();
-+        boolean result = save(compound);
-+        this.passengers = ImmutableList.copyOf(pass);
-+        return result;
-+    }
-+    // Paper end - Entity serialization api
-     public boolean save(CompoundTag nbt) {
-         return this.isPassenger() ? false : this.saveAsPassenger(nbt);
-     }
- 
-     public CompoundTag saveWithoutId(CompoundTag nbt) {
-+        // CraftBukkit start - allow excluding certain data when saving
-+        return this.saveWithoutId(nbt, true);
-+    }
-+
-+    public CompoundTag saveWithoutId(CompoundTag nbttagcompound, boolean includeAll) {
-+        // CraftBukkit end
-         try {
--            if (this.vehicle != null) {
--                nbt.put("Pos", this.newDoubleList(this.vehicle.getX(), this.getY(), this.vehicle.getZ()));
--            } else {
--                nbt.put("Pos", this.newDoubleList(this.getX(), this.getY(), this.getZ()));
-+            // CraftBukkit start - selectively save position
-+            if (includeAll) {
-+                if (this.vehicle != null) {
-+                    nbttagcompound.put("Pos", this.newDoubleList(this.vehicle.getX(), this.getY(), this.vehicle.getZ()));
-+                } else {
-+                    nbttagcompound.put("Pos", this.newDoubleList(this.getX(), this.getY(), this.getZ()));
-+                }
-             }
-+            // CraftBukkit end
- 
-             Vec3 vec3d = this.getDeltaMovement();
- 
--            nbt.put("Motion", this.newDoubleList(vec3d.x, vec3d.y, vec3d.z));
--            nbt.put("Rotation", this.newFloatList(this.getYRot(), this.getXRot()));
--            nbt.putFloat("FallDistance", this.fallDistance);
--            nbt.putShort("Fire", (short) this.remainingFireTicks);
--            nbt.putShort("Air", (short) this.getAirSupply());
--            nbt.putBoolean("OnGround", this.onGround());
--            nbt.putBoolean("Invulnerable", this.invulnerable);
--            nbt.putInt("PortalCooldown", this.portalCooldown);
--            nbt.putUUID("UUID", this.getUUID());
-+            nbttagcompound.put("Motion", this.newDoubleList(vec3d.x, vec3d.y, vec3d.z));
-+
-+            // CraftBukkit start - Checking for NaN pitch/yaw and resetting to zero
-+            // TODO: make sure this is the best way to address this.
-+            if (Float.isNaN(this.yRot)) {
-+                this.yRot = 0;
-+            }
-+
-+            if (Float.isNaN(this.xRot)) {
-+                this.xRot = 0;
-+            }
-+            // CraftBukkit end
-+
-+            nbttagcompound.put("Rotation", this.newFloatList(this.getYRot(), this.getXRot()));
-+            nbttagcompound.putFloat("FallDistance", this.fallDistance);
-+            nbttagcompound.putShort("Fire", (short) this.remainingFireTicks);
-+            nbttagcompound.putShort("Air", (short) this.getAirSupply());
-+            nbttagcompound.putBoolean("OnGround", this.onGround());
-+            nbttagcompound.putBoolean("Invulnerable", this.invulnerable);
-+            nbttagcompound.putInt("PortalCooldown", this.portalCooldown);
-+            // CraftBukkit start - selectively save uuid and world
-+            if (includeAll) {
-+                nbttagcompound.putUUID("UUID", this.getUUID());
-+                // PAIL: Check above UUID reads 1.8 properly, ie: UUIDMost / UUIDLeast
-+                nbttagcompound.putLong("WorldUUIDLeast", ((ServerLevel) this.level).getWorld().getUID().getLeastSignificantBits());
-+                nbttagcompound.putLong("WorldUUIDMost", ((ServerLevel) this.level).getWorld().getUID().getMostSignificantBits());
-+            }
-+            nbttagcompound.putInt("Bukkit.updateLevel", Entity.CURRENT_LEVEL);
-+            if (!this.persist) {
-+                nbttagcompound.putBoolean("Bukkit.persist", this.persist);
-+            }
-+            if (!this.visibleByDefault) {
-+                nbttagcompound.putBoolean("Bukkit.visibleByDefault", this.visibleByDefault);
-+            }
-+            if (this.persistentInvisibility) {
-+                nbttagcompound.putBoolean("Bukkit.invisible", this.persistentInvisibility);
-+            }
-+            // SPIGOT-6907: re-implement LivingEntity#setMaximumAir()
-+            if (this.maxAirTicks != this.getDefaultMaxAirSupply()) {
-+                nbttagcompound.putInt("Bukkit.MaxAirSupply", this.getMaxAirSupply());
-+            }
-+            nbttagcompound.putInt("Spigot.ticksLived", this.tickCount);
-+            // CraftBukkit end
-             Component ichatbasecomponent = this.getCustomName();
- 
-             if (ichatbasecomponent != null) {
--                nbt.putString("CustomName", Component.Serializer.toJson(ichatbasecomponent, this.registryAccess()));
-+                nbttagcompound.putString("CustomName", Component.Serializer.toJson(ichatbasecomponent, this.registryAccess()));
-             }
- 
-             if (this.isCustomNameVisible()) {
--                nbt.putBoolean("CustomNameVisible", this.isCustomNameVisible());
-+                nbttagcompound.putBoolean("CustomNameVisible", this.isCustomNameVisible());
-             }
- 
-             if (this.isSilent()) {
--                nbt.putBoolean("Silent", this.isSilent());
-+                nbttagcompound.putBoolean("Silent", this.isSilent());
-             }
- 
-             if (this.isNoGravity()) {
--                nbt.putBoolean("NoGravity", this.isNoGravity());
-+                nbttagcompound.putBoolean("NoGravity", this.isNoGravity());
-             }
- 
-             if (this.hasGlowingTag) {
--                nbt.putBoolean("Glowing", true);
-+                nbttagcompound.putBoolean("Glowing", true);
-             }
- 
-             int i = this.getTicksFrozen();
- 
-             if (i > 0) {
--                nbt.putInt("TicksFrozen", this.getTicksFrozen());
-+                nbttagcompound.putInt("TicksFrozen", this.getTicksFrozen());
-             }
- 
-             if (this.hasVisualFire) {
--                nbt.putBoolean("HasVisualFire", this.hasVisualFire);
-+                nbttagcompound.putBoolean("HasVisualFire", this.hasVisualFire);
-             }
- 
-             ListTag nbttaglist;
-@@ -1972,10 +2469,10 @@
-                     nbttaglist.add(StringTag.valueOf(s));
-                 }
- 
--                nbt.put("Tags", nbttaglist);
-+                nbttagcompound.put("Tags", nbttaglist);
-             }
- 
--            this.addAdditionalSaveData(nbt);
-+            this.addAdditionalSaveData(nbttagcompound, includeAll); // CraftBukkit - pass on includeAll
-             if (this.isVehicle()) {
-                 nbttaglist = new ListTag();
-                 iterator = this.getPassengers().iterator();
-@@ -1984,17 +2481,44 @@
-                     Entity entity = (Entity) iterator.next();
-                     CompoundTag nbttagcompound1 = new CompoundTag();
- 
--                    if (entity.saveAsPassenger(nbttagcompound1)) {
-+                    if (entity.saveAsPassenger(nbttagcompound1, includeAll)) { // CraftBukkit - pass on includeAll
-                         nbttaglist.add(nbttagcompound1);
-                     }
-                 }
- 
-                 if (!nbttaglist.isEmpty()) {
--                    nbt.put("Passengers", nbttaglist);
-+                    nbttagcompound.put("Passengers", nbttaglist);
-                 }
-             }
- 
--            return nbt;
-+            // CraftBukkit start - stores eventually existing bukkit values
-+            if (this.bukkitEntity != null) {
-+                this.bukkitEntity.storeBukkitValues(nbttagcompound);
-+            }
-+            // CraftBukkit end
-+            // Paper start
-+            if (this.origin != null) {
-+                UUID originWorld = this.originWorld != null ? this.originWorld : this.level != null ? this.level.getWorld().getUID() : null;
-+                if (originWorld != null) {
-+                    nbttagcompound.putUUID("Paper.OriginWorld", originWorld);
-+                }
-+                nbttagcompound.put("Paper.Origin", this.newDoubleList(origin.getX(), origin.getY(), origin.getZ()));
-+            }
-+            if (spawnReason != null) {
-+                nbttagcompound.putString("Paper.SpawnReason", spawnReason.name());
-+            }
-+            // Save entity's from mob spawner status
-+            if (spawnedViaMobSpawner) {
-+                nbttagcompound.putBoolean("Paper.FromMobSpawner", true);
-+            }
-+            if (fromNetherPortal) {
-+                nbttagcompound.putBoolean("Paper.FromNetherPortal", true);
-+            }
-+            if (freezeLocked) {
-+                nbttagcompound.putBoolean("Paper.FreezeLock", true);
-+            }
-+            // Paper end
-+            return nbttagcompound;
-         } catch (Throwable throwable) {
-             CrashReport crashreport = CrashReport.forThrowable(throwable, "Saving entity NBT");
-             CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Entity being saved");
-@@ -2080,6 +2604,71 @@
-             } else {
-                 throw new IllegalStateException("Entity has invalid position");
-             }
-+
-+            // CraftBukkit start
-+            // Spigot start
-+            if (this instanceof net.minecraft.world.entity.LivingEntity) {
-+                this.tickCount = nbt.getInt("Spigot.ticksLived");
-+            }
-+            // Spigot end
-+            this.persist = !nbt.contains("Bukkit.persist") || nbt.getBoolean("Bukkit.persist");
-+            this.visibleByDefault = !nbt.contains("Bukkit.visibleByDefault") || nbt.getBoolean("Bukkit.visibleByDefault");
-+            // SPIGOT-6907: re-implement LivingEntity#setMaximumAir()
-+            if (nbt.contains("Bukkit.MaxAirSupply")) {
-+                this.maxAirTicks = nbt.getInt("Bukkit.MaxAirSupply");
-+            }
-+            // CraftBukkit end
-+
-+            // CraftBukkit start
-+            // Paper - move world parsing/loading to PlayerList#placeNewPlayer
-+            this.getBukkitEntity().readBukkitValues(nbt);
-+            if (nbt.contains("Bukkit.invisible")) {
-+                boolean bukkitInvisible = nbt.getBoolean("Bukkit.invisible");
-+                this.setInvisible(bukkitInvisible);
-+                this.persistentInvisibility = bukkitInvisible;
-+            }
-+            // CraftBukkit end
-+
-+            // Paper start
-+            ListTag originTag = nbt.getList("Paper.Origin", net.minecraft.nbt.Tag.TAG_DOUBLE);
-+            if (!originTag.isEmpty()) {
-+                UUID originWorld = null;
-+                if (nbt.contains("Paper.OriginWorld")) {
-+                    originWorld = nbt.getUUID("Paper.OriginWorld");
-+                } else if (this.level != null) {
-+                    originWorld = this.level.getWorld().getUID();
-+                }
-+                this.originWorld = originWorld;
-+                origin = new org.bukkit.util.Vector(originTag.getDouble(0), originTag.getDouble(1), originTag.getDouble(2));
-+            }
-+
-+            spawnedViaMobSpawner = nbt.getBoolean("Paper.FromMobSpawner"); // Restore entity's from mob spawner status
-+            fromNetherPortal = nbt.getBoolean("Paper.FromNetherPortal");
-+            if (nbt.contains("Paper.SpawnReason")) {
-+                String spawnReasonName = nbt.getString("Paper.SpawnReason");
-+                try {
-+                    spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.valueOf(spawnReasonName);
-+                } catch (Exception ignored) {
-+                    LOGGER.error("Unknown SpawnReason " + spawnReasonName + " for " + this);
-+                }
-+            }
-+            if (spawnReason == null) {
-+                if (spawnedViaMobSpawner) {
-+                    spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.SPAWNER;
-+                } else if (this instanceof Mob && (this instanceof net.minecraft.world.entity.animal.Animal || this instanceof net.minecraft.world.entity.animal.AbstractFish) && !((Mob) this).removeWhenFarAway(0.0)) {
-+                    if (!nbt.getBoolean("PersistenceRequired")) {
-+                        spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.NATURAL;
-+                    }
-+                }
-+            }
-+            if (spawnReason == null) {
-+                spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT;
-+            }
-+            if (nbt.contains("Paper.FreezeLock")) {
-+                freezeLocked = nbt.getBoolean("Paper.FreezeLock");
-+            }
-+            // Paper end
-+
-         } catch (Throwable throwable) {
-             CrashReport crashreport = CrashReport.forThrowable(throwable, "Loading entity NBT");
-             CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Entity being loaded");
-@@ -2099,7 +2688,13 @@
-         ResourceLocation minecraftkey = EntityType.getKey(entitytypes);
- 
-         return entitytypes.canSerialize() && minecraftkey != null ? minecraftkey.toString() : null;
-+    }
-+
-+    // CraftBukkit start - allow excluding certain data when saving
-+    protected void addAdditionalSaveData(CompoundTag nbttagcompound, boolean includeAll) {
-+        this.addAdditionalSaveData(nbttagcompound);
-     }
-+    // CraftBukkit end
- 
-     protected abstract void readAdditionalSaveData(CompoundTag nbt);
- 
-@@ -2150,12 +2745,60 @@
- 
-     @Nullable
-     public ItemEntity spawnAtLocation(ServerLevel world, ItemStack stack, float yOffset) {
-+        // Paper start - Restore vanilla drops behavior
-+        return this.spawnAtLocation(world, stack, yOffset, null);
-+    }
-+    public record DefaultDrop(Item item, org.bukkit.inventory.ItemStack stack, @Nullable java.util.function.Consumer<ItemStack> dropConsumer) {
-+        public DefaultDrop(final ItemStack stack, final java.util.function.Consumer<ItemStack> dropConsumer) {
-+            this(stack.getItem(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(stack), dropConsumer);
-+        }
-+
-+        public void runConsumer(final java.util.function.Consumer<org.bukkit.inventory.ItemStack> fallback) {
-+            if (this.dropConsumer == null || org.bukkit.craftbukkit.inventory.CraftItemType.bukkitToMinecraft(this.stack.getType()) != this.item) {
-+                fallback.accept(this.stack);
-+            } else {
-+                this.dropConsumer.accept(org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(this.stack));
-+            }
-+        }
-+    }
-+    @Nullable
-+    public ItemEntity spawnAtLocation(ServerLevel world, ItemStack stack, float yOffset, @Nullable java.util.function.Consumer<? super ItemEntity> delayedAddConsumer) {
-+        // Paper end - Restore vanilla drops behavior
-         if (stack.isEmpty()) {
-             return null;
-         } else {
--            ItemEntity entityitem = new ItemEntity(world, this.getX(), this.getY() + (double) yOffset, this.getZ(), stack);
-+            // CraftBukkit start - Capture drops for death event
-+            if (this instanceof net.minecraft.world.entity.LivingEntity && !((net.minecraft.world.entity.LivingEntity) this).forceDrops) {
-+                // Paper start - Restore vanilla drops behavior
-+                ((net.minecraft.world.entity.LivingEntity) this).drops.add(new net.minecraft.world.entity.Entity.DefaultDrop(stack, itemStack -> {
-+                    ItemEntity itemEntity = new ItemEntity(this.level, this.getX(), this.getY() + (double) yOffset, this.getZ(), itemStack); // stack is copied before consumer
-+                    itemEntity.setDefaultPickUpDelay();
-+                    this.level.addFreshEntity(itemEntity);
-+                    if (delayedAddConsumer != null) delayedAddConsumer.accept(itemEntity);
-+                }));
-+                // Paper end - Restore vanilla drops behavior
-+                return null;
-+            }
-+            // CraftBukkit end
-+            ItemEntity entityitem = new ItemEntity(world, this.getX(), this.getY() + (double) yOffset, this.getZ(), stack.copy()); // Paper - copy so we can destroy original
-+            stack.setCount(0); // Paper - destroy this item - if this ever leaks due to game bugs, ensure it doesn't dupe
- 
--            entityitem.setDefaultPickUpDelay();
-+            entityitem.setDefaultPickUpDelay(); // Paper - diff on change (in dropConsumer)
-+            // Paper start - Call EntityDropItemEvent
-+            return this.spawnAtLocation(world, entityitem);
-+        }
-+    }
-+    @Nullable
-+    public ItemEntity spawnAtLocation(ServerLevel world, ItemEntity entityitem) {
-+        {
-+            // Paper end - Call EntityDropItemEvent
-+            // CraftBukkit start
-+            EntityDropItemEvent event = new EntityDropItemEvent(this.getBukkitEntity(), (org.bukkit.entity.Item) entityitem.getBukkitEntity());
-+            Bukkit.getPluginManager().callEvent(event);
-+            if (event.isCancelled()) {
-+                return null;
-+            }
-+            // CraftBukkit end
-             world.addFreshEntity(entityitem);
-             return entityitem;
-         }
-@@ -2184,7 +2827,16 @@
-         if (this.isAlive() && this instanceof Leashable leashable) {
-             if (leashable.getLeashHolder() == player) {
-                 if (!this.level().isClientSide()) {
--                    if (player.hasInfiniteMaterials()) {
-+                    // CraftBukkit start - fire PlayerUnleashEntityEvent
-+                    // Paper start - Expand EntityUnleashEvent
-+                    org.bukkit.event.player.PlayerUnleashEntityEvent event = CraftEventFactory.callPlayerUnleashEntityEvent(this, player, hand, !player.hasInfiniteMaterials());
-+                    if (event.isCancelled()) {
-+                        // Paper end - Expand EntityUnleashEvent
-+                        ((ServerPlayer) player).connection.send(new ClientboundSetEntityLinkPacket(this, leashable.getLeashHolder()));
-+                        return InteractionResult.PASS;
-+                    }
-+                    // CraftBukkit end
-+                    if (!event.isDropLeash()) { // Paper - Expand EntityUnleashEvent
-                         leashable.removeLeash();
-                     } else {
-                         leashable.dropLeash();
-@@ -2200,6 +2852,14 @@
- 
-             if (itemstack.is(Items.LEAD) && leashable.canHaveALeashAttachedToIt()) {
-                 if (!this.level().isClientSide()) {
-+                    // CraftBukkit start - fire PlayerLeashEntityEvent
-+                    if (CraftEventFactory.callPlayerLeashEntityEvent(this, player, player, hand).isCancelled()) {
-+                        // ((ServerPlayer) player).resendItemInHands(); // SPIGOT-7615: Resend to fix client desync with used item // Paper - Fix inventory desync
-+                        ((ServerPlayer) player).connection.send(new ClientboundSetEntityLinkPacket(this, leashable.getLeashHolder()));
-+                        player.containerMenu.sendAllDataToRemote(); // Paper - Fix inventory desync
-+                        return InteractionResult.PASS;
-+                    }
-+                    // CraftBukkit end
-                     leashable.setLeashedTo(player, true);
-                 }
- 
-@@ -2265,15 +2925,15 @@
-     }
- 
-     public boolean showVehicleHealth() {
--        return this instanceof LivingEntity;
-+        return this instanceof net.minecraft.world.entity.LivingEntity;
-     }
- 
-     public boolean startRiding(Entity entity, boolean force) {
--        if (entity == this.vehicle) {
-+        if (entity == this.vehicle || entity.level != this.level) { // Paper - Ensure entity passenger world matches ridden entity (bad plugins)
-             return false;
-         } else if (!entity.couldAcceptPassenger()) {
-             return false;
--        } else if (!this.level().isClientSide() && !entity.type.canSerialize()) {
-+        } else if (!force && !this.level().isClientSide() && !entity.type.canSerialize()) { // SPIGOT-7947: Allow force riding all entities
-             return false;
-         } else {
-             for (Entity entity1 = entity; entity1.vehicle != null; entity1 = entity1.vehicle) {
-@@ -2285,11 +2945,32 @@
-             if (!force && (!this.canRide(entity) || !entity.canAddPassenger(this))) {
-                 return false;
-             } else {
-+                // CraftBukkit start
-+                if (entity.getBukkitEntity() instanceof Vehicle && this.getBukkitEntity() instanceof LivingEntity) {
-+                    VehicleEnterEvent event = new VehicleEnterEvent((Vehicle) entity.getBukkitEntity(), this.getBukkitEntity());
-+                    // Suppress during worldgen
-+                    if (this.valid) {
-+                        Bukkit.getPluginManager().callEvent(event);
-+                    }
-+                    if (event.isCancelled()) {
-+                        return false;
-+                    }
-+                }
-+
-+                EntityMountEvent event = new EntityMountEvent(this.getBukkitEntity(), entity.getBukkitEntity());
-+                // Suppress during worldgen
-+                if (this.valid) {
-+                    Bukkit.getPluginManager().callEvent(event);
-+                }
-+                if (event.isCancelled()) {
-+                    return false;
-+                }
-+                // CraftBukkit end
-                 if (this.isPassenger()) {
-                     this.stopRiding();
-                 }
- 
--                this.setPose(Pose.STANDING);
-+                this.setPose(net.minecraft.world.entity.Pose.STANDING);
-                 this.vehicle = entity;
-                 this.vehicle.addPassenger(this);
-                 entity.getIndirectPassengersStream().filter((entity2) -> {
-@@ -2314,19 +2995,30 @@
-     }
- 
-     public void removeVehicle() {
-+        // Paper start - Force entity dismount during teleportation
-+        this.removeVehicle(false);
-+    }
-+    public void removeVehicle(boolean suppressCancellation) {
-+        // Paper end - Force entity dismount during teleportation
-         if (this.vehicle != null) {
-             Entity entity = this.vehicle;
- 
-             this.vehicle = null;
--            entity.removePassenger(this);
-+            if (!entity.removePassenger(this, suppressCancellation)) this.vehicle = entity; // CraftBukkit // Paper - Force entity dismount during teleportation
-         }
- 
-     }
- 
-     public void stopRiding() {
--        this.removeVehicle();
-+        // Paper start - Force entity dismount during teleportation
-+        this.stopRiding(false);
-     }
- 
-+    public void stopRiding(boolean suppressCancellation) {
-+        this.removeVehicle(suppressCancellation);
-+        // Paper end - Force entity dismount during teleportation
-+    }
-+
-     protected void addPassenger(Entity passenger) {
-         if (passenger.getVehicle() != this) {
-             throw new IllegalStateException("Use x.startRiding(y), not y.addPassenger(x)");
-@@ -2349,21 +3041,53 @@
-         }
-     }
- 
--    protected void removePassenger(Entity passenger) {
--        if (passenger.getVehicle() == this) {
-+    // Paper start - Force entity dismount during teleportation
-+    protected boolean removePassenger(Entity entity) { return removePassenger(entity, false);}
-+    protected boolean removePassenger(Entity entity, boolean suppressCancellation) { // CraftBukkit
-+        // Paper end - Force entity dismount during teleportation
-+        if (entity.getVehicle() == this) {
-             throw new IllegalStateException("Use x.stopRiding(y), not y.removePassenger(x)");
-         } else {
--            if (this.passengers.size() == 1 && this.passengers.get(0) == passenger) {
-+            // CraftBukkit start
-+            CraftEntity craft = (CraftEntity) entity.getBukkitEntity().getVehicle();
-+            Entity orig = craft == null ? null : craft.getHandle();
-+            if (this.getBukkitEntity() instanceof Vehicle && entity.getBukkitEntity() instanceof LivingEntity) {
-+                VehicleExitEvent event = new VehicleExitEvent(
-+                        (Vehicle) this.getBukkitEntity(),
-+                        (LivingEntity) entity.getBukkitEntity(), !suppressCancellation // Paper - Force entity dismount during teleportation
-+                );
-+                // Suppress during worldgen
-+                if (this.valid) {
-+                    Bukkit.getPluginManager().callEvent(event);
-+                }
-+                CraftEntity craftn = (CraftEntity) entity.getBukkitEntity().getVehicle();
-+                Entity n = craftn == null ? null : craftn.getHandle();
-+                if (event.isCancelled() || n != orig) {
-+                    return false;
-+                }
-+            }
-+
-+            EntityDismountEvent event = new EntityDismountEvent(entity.getBukkitEntity(), this.getBukkitEntity(), !suppressCancellation); // Paper - Force entity dismount during teleportation
-+            // Suppress during worldgen
-+            if (this.valid) {
-+                Bukkit.getPluginManager().callEvent(event);
-+            }
-+            if (event.isCancelled()) {
-+                return false;
-+            }
-+            // CraftBukkit end
-+            if (this.passengers.size() == 1 && this.passengers.get(0) == entity) {
-                 this.passengers = ImmutableList.of();
-             } else {
-                 this.passengers = (ImmutableList) this.passengers.stream().filter((entity1) -> {
--                    return entity1 != passenger;
-+                    return entity1 != entity;
-                 }).collect(ImmutableList.toImmutableList());
-             }
- 
--            passenger.boardingCooldown = 60;
--            this.gameEvent(GameEvent.ENTITY_DISMOUNT, passenger);
-+            entity.boardingCooldown = 60;
-+            this.gameEvent(GameEvent.ENTITY_DISMOUNT, entity);
-         }
-+        return true; // CraftBukkit
-     }
- 
-     protected boolean canAddPassenger(Entity passenger) {
-@@ -2464,7 +3188,7 @@
-                     if (teleporttransition != null) {
-                         ServerLevel worldserver1 = teleporttransition.newLevel();
- 
--                        if (worldserver.getServer().isLevelEnabled(worldserver1) && (worldserver1.dimension() == worldserver.dimension() || this.canTeleport(worldserver, worldserver1))) {
-+                        if (this instanceof ServerPlayer || (worldserver1 != null && (worldserver1.dimension() == worldserver.dimension() || this.canTeleport(worldserver, worldserver1)))) { // CraftBukkit - always call event for players
-                             this.teleport(teleporttransition);
-                         }
-                     }
-@@ -2547,7 +3271,7 @@
-     }
- 
-     public boolean isCrouching() {
--        return this.hasPose(Pose.CROUCHING);
-+        return this.hasPose(net.minecraft.world.entity.Pose.CROUCHING);
-     }
- 
-     public boolean isSprinting() {
-@@ -2563,7 +3287,7 @@
-     }
- 
-     public boolean isVisuallySwimming() {
--        return this.hasPose(Pose.SWIMMING);
-+        return this.hasPose(net.minecraft.world.entity.Pose.SWIMMING);
-     }
- 
-     public boolean isVisuallyCrawling() {
-@@ -2571,6 +3295,13 @@
-     }
- 
-     public void setSwimming(boolean swimming) {
-+        // CraftBukkit start
-+        if (this.valid && this.isSwimming() != swimming && this instanceof net.minecraft.world.entity.LivingEntity) {
-+            if (CraftEventFactory.callToggleSwimEvent((net.minecraft.world.entity.LivingEntity) this, swimming).isCancelled()) {
-+                return;
-+            }
-+        }
-+        // CraftBukkit end
-         this.setSharedFlag(4, swimming);
-     }
- 
-@@ -2609,6 +3340,7 @@
- 
-     @Nullable
-     public PlayerTeam getTeam() {
-+        if (!this.level().paperConfig().scoreboards.allowNonPlayerEntitiesOnScoreboards && !(this instanceof Player)) { return null; } // Paper - Perf: Disable Scoreboards for non players by default
-         return this.level().getScoreboard().getPlayersTeam(this.getScoreboardName());
-     }
- 
-@@ -2624,8 +3356,12 @@
-         return this.getTeam() != null ? this.getTeam().isAlliedTo(team) : false;
-     }
- 
-+    // CraftBukkit - start
-     public void setInvisible(boolean invisible) {
--        this.setSharedFlag(5, invisible);
-+        if (!this.persistentInvisibility) { // Prevent Minecraft from removing our invisibility flag
-+            this.setSharedFlag(5, invisible);
-+        }
-+        // CraftBukkit - end
-     }
- 
-     public boolean getSharedFlag(int index) {
-@@ -2644,7 +3380,7 @@
-     }
- 
-     public int getMaxAirSupply() {
--        return 300;
-+        return this.maxAirTicks; // CraftBukkit - SPIGOT-6907: re-implement LivingEntity#setMaximumAir()
-     }
- 
-     public int getAirSupply() {
-@@ -2652,7 +3388,18 @@
-     }
- 
-     public void setAirSupply(int air) {
--        this.entityData.set(Entity.DATA_AIR_SUPPLY_ID, air);
-+        // CraftBukkit start
-+        EntityAirChangeEvent event = new EntityAirChangeEvent(this.getBukkitEntity(), air);
-+        // Suppress during worldgen
-+        if (this.valid) {
-+            event.getEntity().getServer().getPluginManager().callEvent(event);
-+        }
-+        if (event.isCancelled() && this.getAirSupply() != air) {
-+            this.entityData.markDirty(Entity.DATA_AIR_SUPPLY_ID);
-+            return;
-+        }
-+        this.entityData.set(Entity.DATA_AIR_SUPPLY_ID, event.getAmount());
-+        // CraftBukkit end
-     }
- 
-     public int getTicksFrozen() {
-@@ -2679,11 +3426,44 @@
- 
-     public void thunderHit(ServerLevel world, LightningBolt lightning) {
-         this.setRemainingFireTicks(this.remainingFireTicks + 1);
-+        // CraftBukkit start
-+        final org.bukkit.entity.Entity thisBukkitEntity = this.getBukkitEntity();
-+        final org.bukkit.entity.Entity stormBukkitEntity = lightning.getBukkitEntity();
-+        final PluginManager pluginManager = Bukkit.getPluginManager();
-+        // CraftBukkit end
-+
-         if (this.remainingFireTicks == 0) {
--            this.igniteForSeconds(8.0F);
-+            // CraftBukkit start - Call a combust event when lightning strikes
-+            EntityCombustByEntityEvent entityCombustEvent = new EntityCombustByEntityEvent(stormBukkitEntity, thisBukkitEntity, 8.0F);
-+            pluginManager.callEvent(entityCombustEvent);
-+            if (!entityCombustEvent.isCancelled()) {
-+                this.igniteForSeconds(entityCombustEvent.getDuration(), false);
-+            // Paper start - fix EntityCombustEvent cancellation
-+            } else {
-+                this.setRemainingFireTicks(this.remainingFireTicks - 1);
-+            // Paper end - fix EntityCombustEvent cancellation
-+            }
-+            // CraftBukkit end
-         }
- 
--        this.hurtServer(world, this.damageSources().lightningBolt(), 5.0F);
-+        // CraftBukkit start
-+        if (thisBukkitEntity instanceof Hanging) {
-+            HangingBreakByEntityEvent hangingEvent = new HangingBreakByEntityEvent((Hanging) thisBukkitEntity, stormBukkitEntity);
-+            pluginManager.callEvent(hangingEvent);
-+
-+            if (hangingEvent.isCancelled()) {
-+                return;
-+            }
-+        }
-+
-+        if (this.fireImmune()) {
-+            return;
-+        }
-+
-+        if (!this.hurtServer(world, this.damageSources().lightningBolt().customEventDamager(lightning), 5.0F)) { // Paper - fix DamageSource API
-+            return;
-+        }
-+        // CraftBukkit end
-     }
- 
-     public void onAboveBubbleCol(boolean drag) {
-@@ -2713,7 +3493,7 @@
-         this.resetFallDistance();
-     }
- 
--    public boolean killedEntity(ServerLevel world, LivingEntity other) {
-+    public boolean killedEntity(ServerLevel world, net.minecraft.world.entity.LivingEntity other) {
-         return true;
-     }
- 
-@@ -2818,7 +3598,7 @@
-     public String toString() {
-         String s = this.level() == null ? "~NULL~" : this.level().toString();
- 
--        return this.removalReason != null ? String.format(Locale.ROOT, "%s['%s'/%d, l='%s', x=%.2f, y=%.2f, z=%.2f, removed=%s]", this.getClass().getSimpleName(), this.getName().getString(), this.id, s, this.getX(), this.getY(), this.getZ(), this.removalReason) : String.format(Locale.ROOT, "%s['%s'/%d, l='%s', x=%.2f, y=%.2f, z=%.2f]", this.getClass().getSimpleName(), this.getName().getString(), this.id, s, this.getX(), this.getY(), this.getZ());
-+        return this.removalReason != null ? String.format(Locale.ROOT, "%s['%s'/%d, uuid='%s', l='%s', x=%.2f, y=%.2f, z=%.2f, cpos=%s, tl=%d, v=%b, removed=%s]", this.getClass().getSimpleName(), this.getName().getString(), this.id, this.uuid, s, this.getX(), this.getY(), this.getZ(), this.chunkPosition(), this.tickCount, this.valid, this.removalReason) : String.format(Locale.ROOT, "%s['%s'/%d, uuid='%s', l='%s', x=%.2f, y=%.2f, z=%.2f, cpos=%s, tl=%d, v=%b]", this.getClass().getSimpleName(), this.getName().getString(), this.id, this.uuid, s, this.getX(), this.getY(), this.getZ(), this.chunkPosition(), this.tickCount, this.valid); // Paper - add more info
-     }
- 
-     public final boolean isInvulnerableToBase(DamageSource damageSource) {
-@@ -2838,6 +3618,13 @@
-     }
- 
-     public void restoreFrom(Entity original) {
-+        // Paper start - Forward CraftEntity in teleport command
-+        CraftEntity bukkitEntity = original.bukkitEntity;
-+        if (bukkitEntity != null) {
-+            bukkitEntity.setHandle(this);
-+            this.bukkitEntity = bukkitEntity;
-+        }
-+        // Paper end - Forward CraftEntity in teleport command
-         CompoundTag nbttagcompound = original.saveWithoutId(new CompoundTag());
- 
-         nbttagcompound.remove("Dimension");
-@@ -2850,8 +3637,57 @@
-     public Entity teleport(TeleportTransition teleportTarget) {
-         Level world = this.level();
- 
-+        // Paper start - Fix item duplication and teleport issues
-+        if ((!this.isAlive() || !this.valid) && (teleportTarget.newLevel() != world)) {
-+            LOGGER.warn("Illegal Entity Teleport " + this + " to " + teleportTarget.newLevel() + ":" + teleportTarget.position(), new Throwable());
-+            return null;
-+        }
-+        // Paper end - Fix item duplication and teleport issues
-         if (world instanceof ServerLevel worldserver) {
-             if (!this.isRemoved()) {
-+                // CraftBukkit start
-+                PositionMoveRotation absolutePosition = PositionMoveRotation.calculateAbsolute(PositionMoveRotation.of(this), PositionMoveRotation.of(teleportTarget), teleportTarget.relatives());
-+                Vec3 velocity = absolutePosition.deltaMovement(); // Paper
-+                Location to = CraftLocation.toBukkit(absolutePosition.position(), teleportTarget.newLevel().getWorld(), absolutePosition.yRot(), absolutePosition.xRot());
-+                // Paper start - gateway-specific teleport event
-+                final EntityTeleportEvent teleEvent;
-+                if (this.portalProcess != null && this.portalProcess.isSamePortal(((net.minecraft.world.level.block.EndGatewayBlock) net.minecraft.world.level.block.Blocks.END_GATEWAY)) && this.level.getBlockEntity(this.portalProcess.getEntryPosition()) instanceof net.minecraft.world.level.block.entity.TheEndGatewayBlockEntity theEndGatewayBlockEntity) {
-+                    teleEvent = new com.destroystokyo.paper.event.entity.EntityTeleportEndGatewayEvent(this.getBukkitEntity(), this.getBukkitEntity().getLocation(), to, new org.bukkit.craftbukkit.block.CraftEndGateway(to.getWorld(), theEndGatewayBlockEntity));
-+                    teleEvent.callEvent();
-+                } else {
-+                    teleEvent = CraftEventFactory.callEntityTeleportEvent(this, to);
-+                }
-+                // Paper end - gateway-specific teleport event
-+                if (teleEvent.isCancelled() || teleEvent.getTo() == null) {
-+                    return null;
-+                }
-+                if (!to.equals(teleEvent.getTo())) {
-+                    to = teleEvent.getTo();
-+                    teleportTarget = new TeleportTransition(((CraftWorld) to.getWorld()).getHandle(), CraftLocation.toVec3D(to), Vec3.ZERO, to.getYaw(), to.getPitch(), teleportTarget.missingRespawnBlock(), teleportTarget.asPassenger(), Set.of(), teleportTarget.postTeleportTransition(), teleportTarget.cause());
-+                    // Paper start - Call EntityPortalExitEvent
-+                    velocity = Vec3.ZERO;
-+                }
-+                if (this.portalProcess != null) { // if in a portal
-+                    CraftEntity bukkitEntity = this.getBukkitEntity();
-+                    org.bukkit.event.entity.EntityPortalExitEvent event = new org.bukkit.event.entity.EntityPortalExitEvent(
-+                        bukkitEntity,
-+                        bukkitEntity.getLocation(), to.clone(),
-+                        bukkitEntity.getVelocity(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(velocity)
-+                    );
-+                    event.callEvent();
-+
-+                    // Only change the target if actually needed, since we reset relative flags
-+                    if (!event.isCancelled() && event.getTo() != null && (!event.getTo().equals(event.getFrom()) || !event.getAfter().equals(event.getBefore()))) {
-+                        to = event.getTo().clone();
-+                        velocity = org.bukkit.craftbukkit.util.CraftVector.toNMS(event.getAfter());
-+                        teleportTarget = new TeleportTransition(((CraftWorld) to.getWorld()).getHandle(), CraftLocation.toVec3D(to), velocity, to.getYaw(), to.getPitch(), teleportTarget.missingRespawnBlock(), teleportTarget.asPassenger(), Set.of(), teleportTarget.postTeleportTransition(), teleportTarget.cause());
-+                    }
-+                }
-+                if (this.isRemoved()) {
-+                    return null;
-+                }
-+                // Paper end - Call EntityPortalExitEvent
-+                // CraftBukkit end
-                 ServerLevel worldserver1 = teleportTarget.newLevel();
-                 boolean flag = worldserver1.dimension() != worldserver.dimension();
- 
-@@ -2918,10 +3754,19 @@
-             gameprofilerfiller.pop();
-             return null;
-         } else {
-+            // Paper start - Fix item duplication and teleport issues
-+            if (this instanceof Leashable leashable) {
-+                leashable.dropLeash(); // Paper drop lead
-+            }
-+            // Paper end - Fix item duplication and teleport issues
-             entity.restoreFrom(this);
-             this.removeAfterChangingDimensions();
-+            // CraftBukkit start - Forward the CraftEntity to the new entity
-+            //this.getBukkitEntity().setHandle(entity);
-+            //entity.bukkitEntity = this.getBukkitEntity(); // Paper - forward CraftEntity in teleport command; moved to Entity#restoreFrom
-+            // CraftBukkit end
-             entity.teleportSetPosition(PositionMoveRotation.of(teleportTarget), teleportTarget.relatives());
--            world.addDuringTeleport(entity);
-+            if (this.inWorld) world.addDuringTeleport(entity); // CraftBukkit - Don't spawn the new entity if the current entity isn't spawned
-             Iterator iterator1 = list1.iterator();
- 
-             while (iterator1.hasNext()) {
-@@ -2947,7 +3792,7 @@
-     }
- 
-     private void sendTeleportTransitionToRidingPlayers(TeleportTransition teleportTarget) {
--        LivingEntity entityliving = this.getControllingPassenger();
-+        net.minecraft.world.entity.LivingEntity entityliving = this.getControllingPassenger();
-         Iterator iterator = this.getIndirectPassengers().iterator();
- 
-         while (iterator.hasNext()) {
-@@ -2995,9 +3840,17 @@
-     }
- 
-     protected void removeAfterChangingDimensions() {
--        this.setRemoved(Entity.RemovalReason.CHANGED_DIMENSION);
--        if (this instanceof Leashable leashable) {
--            leashable.removeLeash();
-+        this.setRemoved(Entity.RemovalReason.CHANGED_DIMENSION, null); // CraftBukkit - add Bukkit remove cause
-+        if (this instanceof Leashable leashable && leashable.isLeashed()) { // Paper - only call if it is leashed
-+            // Paper start - Expand EntityUnleashEvent
-+            final EntityUnleashEvent event = new EntityUnleashEvent(this.getBukkitEntity(), UnleashReason.UNKNOWN, false); // CraftBukkit
-+            event.callEvent();
-+            if (!event.isDropLeash()) {
-+                leashable.removeLeash();
-+            } else {
-+                leashable.dropLeash();
-+            }
-+            // Paper end - Expand EntityUnleashEvent
-         }
- 
-     }
-@@ -3005,12 +3858,35 @@
-     public Vec3 getRelativePortalPosition(Direction.Axis portalAxis, BlockUtil.FoundRectangle portalRect) {
-         return PortalShape.getRelativePosition(portalRect, portalAxis, this.position(), this.getDimensions(this.getPose()));
-     }
-+
-+    // CraftBukkit start
-+    public CraftPortalEvent callPortalEvent(Entity entity, Location exit, PlayerTeleportEvent.TeleportCause cause, int searchRadius, int creationRadius) {
-+        org.bukkit.entity.Entity bukkitEntity = entity.getBukkitEntity();
-+        Location enter = bukkitEntity.getLocation();
-+
-+        // Paper start
-+        final org.bukkit.PortalType portalType = switch (cause) {
-+            case END_PORTAL -> org.bukkit.PortalType.ENDER;
-+            case NETHER_PORTAL -> org.bukkit.PortalType.NETHER;
-+            case END_GATEWAY -> org.bukkit.PortalType.END_GATEWAY; // not actually used yet
-+            default -> org.bukkit.PortalType.CUSTOM;
-+        };
-+        EntityPortalEvent event = new EntityPortalEvent(bukkitEntity, enter, exit, searchRadius, true, creationRadius, portalType);
-+        // Paper end
-+        event.getEntity().getServer().getPluginManager().callEvent(event);
-+        if (event.isCancelled() || event.getTo() == null || event.getTo().getWorld() == null || !entity.isAlive()) {
-+            return null;
-+        }
-+        return new CraftPortalEvent(event);
-+    }
-+    // CraftBukkit end
- 
-     public boolean canUsePortal(boolean allowVehicles) {
-         return (allowVehicles || !this.isPassenger()) && this.isAlive();
-     }
- 
-     public boolean canTeleport(Level from, Level to) {
-+        if (!this.isAlive() || !this.valid) return false; // Paper - Fix item duplication and teleport issues
-         if (from.dimension() == Level.END && to.dimension() == Level.OVERWORLD) {
-             Iterator iterator = this.getPassengers().iterator();
- 
-@@ -3134,10 +4010,16 @@
-         return (Boolean) this.entityData.get(Entity.DATA_CUSTOM_NAME_VISIBLE);
-     }
- 
--    public boolean teleportTo(ServerLevel world, double destX, double destY, double destZ, Set<Relative> flags, float yaw, float pitch, boolean resetCamera) {
--        float f2 = Mth.clamp(pitch, -90.0F, 90.0F);
--        Entity entity = this.teleport(new TeleportTransition(world, new Vec3(destX, destY, destZ), Vec3.ZERO, yaw, f2, flags, TeleportTransition.DO_NOTHING));
-+    // CraftBukkit start
-+    public final boolean teleportTo(ServerLevel world, double destX, double destY, double destZ, Set<Relative> flags, float yaw, float pitch, boolean resetCamera) {
-+        return this.teleportTo(world, destX, destY, destZ, flags, yaw, pitch, resetCamera, PlayerTeleportEvent.TeleportCause.UNKNOWN);
-+    }
- 
-+    public boolean teleportTo(ServerLevel worldserver, double d0, double d1, double d2, Set<Relative> set, float f, float f1, boolean flag, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause) {
-+        float f2 = Mth.clamp(f1, -90.0F, 90.0F);
-+        Entity entity = this.teleport(new TeleportTransition(worldserver, new Vec3(d0, d1, d2), Vec3.ZERO, f, f2, set, TeleportTransition.DO_NOTHING, cause));
-+        // CraftBukkit end
-+
-         return entity != null;
-     }
- 
-@@ -3187,7 +4069,7 @@
-     /** @deprecated */
-     @Deprecated
-     protected void fixupDimensions() {
--        Pose entitypose = this.getPose();
-+        net.minecraft.world.entity.Pose entitypose = this.getPose();
-         EntityDimensions entitysize = this.getDimensions(entitypose);
- 
-         this.dimensions = entitysize;
-@@ -3196,7 +4078,7 @@
- 
-     public void refreshDimensions() {
-         EntityDimensions entitysize = this.dimensions;
--        Pose entitypose = this.getPose();
-+        net.minecraft.world.entity.Pose entitypose = this.getPose();
-         EntityDimensions entitysize1 = this.getDimensions(entitypose);
- 
-         this.dimensions = entitysize1;
-@@ -3258,10 +4140,29 @@
-     }
- 
-     public final void setBoundingBox(AABB boundingBox) {
--        this.bb = boundingBox;
-+        // CraftBukkit start - block invalid bounding boxes
-+        double minX = boundingBox.minX,
-+                minY = boundingBox.minY,
-+                minZ = boundingBox.minZ,
-+                maxX = boundingBox.maxX,
-+                maxY = boundingBox.maxY,
-+                maxZ = boundingBox.maxZ;
-+        double len = boundingBox.maxX - boundingBox.minX;
-+        if (len < 0) maxX = minX;
-+        if (len > 64) maxX = minX + 64.0;
-+
-+        len = boundingBox.maxY - boundingBox.minY;
-+        if (len < 0) maxY = minY;
-+        if (len > 64) maxY = minY + 64.0;
-+
-+        len = boundingBox.maxZ - boundingBox.minZ;
-+        if (len < 0) maxZ = minZ;
-+        if (len > 64) maxZ = minZ + 64.0;
-+        this.bb = new AABB(minX, minY, minZ, maxX, maxY, maxZ);
-+        // CraftBukkit end
-     }
- 
--    public final float getEyeHeight(Pose pose) {
-+    public final float getEyeHeight(net.minecraft.world.entity.Pose pose) {
-         return this.getDimensions(pose).eyeHeight();
-     }
- 
-@@ -3300,7 +4201,14 @@
- 
-     public void startSeenByPlayer(ServerPlayer player) {}
- 
--    public void stopSeenByPlayer(ServerPlayer player) {}
-+    // Paper start - entity tracking events
-+    public void stopSeenByPlayer(ServerPlayer player) {
-+        // Since this event cannot be cancelled, we should call it here to catch all "un-tracks"
-+        if (io.papermc.paper.event.player.PlayerUntrackEntityEvent.getHandlerList().getRegisteredListeners().length > 0) {
-+            new io.papermc.paper.event.player.PlayerUntrackEntityEvent(player.getBukkitEntity(), this.getBukkitEntity()).callEvent();
-+        }
-+    }
-+    // Paper end - entity tracking events
- 
-     public float rotate(Rotation rotation) {
-         float f = Mth.wrapDegrees(this.getYRot());
-@@ -3335,7 +4243,7 @@
-     }
- 
-     @Nullable
--    public LivingEntity getControllingPassenger() {
-+    public net.minecraft.world.entity.LivingEntity getControllingPassenger() {
-         return null;
-     }
- 
-@@ -3373,20 +4281,34 @@
-     }
- 
-     private Stream<Entity> getIndirectPassengersStream() {
-+        if (this.passengers.isEmpty()) { return Stream.of(); } // Paper - Optimize indirect passenger iteration
-         return this.passengers.stream().flatMap(Entity::getSelfAndPassengers);
-     }
- 
-     @Override
-     public Stream<Entity> getSelfAndPassengers() {
-+        if (this.passengers.isEmpty()) { return Stream.of(this); } // Paper - Optimize indirect passenger iteration
-         return Stream.concat(Stream.of(this), this.getIndirectPassengersStream());
-     }
- 
-     @Override
-     public Stream<Entity> getPassengersAndSelf() {
-+        if (this.passengers.isEmpty()) { return Stream.of(this); } // Paper - Optimize indirect passenger iteration
-         return Stream.concat(this.passengers.stream().flatMap(Entity::getPassengersAndSelf), Stream.of(this));
-     }
- 
-     public Iterable<Entity> getIndirectPassengers() {
-+        // Paper start - Optimize indirect passenger iteration
-+        if (this.passengers.isEmpty()) { return ImmutableList.of(); }
-+        ImmutableList.Builder<Entity> indirectPassengers = ImmutableList.builder();
-+        for (Entity passenger : this.passengers) {
-+            indirectPassengers.add(passenger);
-+            indirectPassengers.addAll(passenger.getIndirectPassengers());
-+        }
-+        return indirectPassengers.build();
-+    }
-+    private Iterable<Entity> getIndirectPassengers_old() {
-+        // Paper end - Optimize indirect passenger iteration
-         return () -> {
-             return this.getIndirectPassengersStream().iterator();
-         };
-@@ -3399,6 +4321,7 @@
-     }
- 
-     public boolean hasExactlyOnePlayerPassenger() {
-+        if (this.passengers.isEmpty()) { return false; } // Paper - Optimize indirect passenger iteration
-         return this.countPlayerPassengers() == 1;
-     }
- 
-@@ -3435,7 +4358,7 @@
-     }
- 
-     public boolean isControlledByLocalInstance() {
--        LivingEntity entityliving = this.getControllingPassenger();
-+        net.minecraft.world.entity.LivingEntity entityliving = this.getControllingPassenger();
- 
-         if (entityliving instanceof Player entityhuman) {
-             return entityhuman.isLocalPlayer();
-@@ -3445,7 +4368,7 @@
-     }
- 
-     public boolean isControlledByClient() {
--        LivingEntity entityliving = this.getControllingPassenger();
-+        net.minecraft.world.entity.LivingEntity entityliving = this.getControllingPassenger();
- 
-         return entityliving != null && entityliving.isControlledByClient();
-     }
-@@ -3463,7 +4386,7 @@
-         return new Vec3((double) f1 * d2 / (double) f3, 0.0D, (double) f2 * d2 / (double) f3);
-     }
- 
--    public Vec3 getDismountLocationForPassenger(LivingEntity passenger) {
-+    public Vec3 getDismountLocationForPassenger(net.minecraft.world.entity.LivingEntity passenger) {
-         return new Vec3(this.getX(), this.getBoundingBox().maxY, this.getZ());
-     }
- 
-@@ -3488,9 +4411,38 @@
-     public int getFireImmuneTicks() {
-         return 1;
-     }
-+
-+    // CraftBukkit start
-+    private final CommandSource commandSource = new CommandSource() {
-+
-+        @Override
-+        public void sendSystemMessage(Component message) {
-+        }
- 
-+        @Override
-+        public CommandSender getBukkitSender(CommandSourceStack wrapper) {
-+            return Entity.this.getBukkitEntity();
-+        }
-+
-+        @Override
-+        public boolean acceptsSuccess() {
-+            return ((ServerLevel) Entity.this.level()).getGameRules().getBoolean(GameRules.RULE_SENDCOMMANDFEEDBACK);
-+        }
-+
-+        @Override
-+        public boolean acceptsFailure() {
-+            return true;
-+        }
-+
-+        @Override
-+        public boolean shouldInformAdmins() {
-+            return true;
-+        }
-+    };
-+    // CraftBukkit end
-+
-     public CommandSourceStack createCommandSourceStackForNameResolution(ServerLevel world) {
--        return new CommandSourceStack(CommandSource.NULL, this.position(), this.getRotationVector(), world, 0, this.getName().getString(), this.getDisplayName(), world.getServer(), this);
-+        return new CommandSourceStack(this.commandSource, this.position(), this.getRotationVector(), world, 0, this.getName().getString(), this.getDisplayName(), world.getServer(), this); // CraftBukkit
-     }
- 
-     public void lookAt(EntityAnchorArgument.Anchor anchorPoint, Vec3 target) {
-@@ -3551,6 +4503,11 @@
-                                     vec3d = vec3d.add(vec3d1);
-                                     ++k1;
-                                 }
-+                                // CraftBukkit start - store last lava contact location
-+                                if (tag == FluidTags.LAVA) {
-+                                    this.lastLavaContact = blockposition_mutableblockposition.immutable();
-+                                }
-+                                // CraftBukkit end
-                             }
-                         }
-                     }
-@@ -3613,7 +4570,7 @@
-         return new ClientboundAddEntityPacket(this, entityTrackerEntry);
-     }
- 
--    public EntityDimensions getDimensions(Pose pose) {
-+    public EntityDimensions getDimensions(net.minecraft.world.entity.Pose pose) {
-         return this.type.getDimensions();
-     }
- 
-@@ -3713,8 +4670,40 @@
-     public double getRandomZ(double widthScale) {
-         return this.getZ((2.0D * this.random.nextDouble() - 1.0D) * widthScale);
-     }
-+
-+    // Paper start - Block invalid positions and bounding box
-+    public static boolean checkPosition(Entity entity, double newX, double newY, double newZ) {
-+        if (Double.isFinite(newX) && Double.isFinite(newY) && Double.isFinite(newZ)) {
-+            return true;
-+        }
- 
-+        String entityInfo;
-+        try {
-+            entityInfo = entity.toString();
-+        } catch (Exception ex) {
-+            entityInfo = "[Entity info unavailable] ";
-+        }
-+        LOGGER.error("New entity position is invalid! Tried to set invalid position ({},{},{}) for entity {} located at {}, entity info: {}", newX, newY, newZ, entity.getClass().getName(), entity.position, entityInfo, new Throwable());
-+        return false;
-+    }
-     public final void setPosRaw(double x, double y, double z) {
-+        this.setPosRaw(x, y, z, false);
-+    }
-+    public final void setPosRaw(double x, double y, double z, boolean forceBoundingBoxUpdate) {
-+        if (!checkPosition(this, x, y, z)) {
-+            return;
-+        }
-+        // Paper end - Block invalid positions and bounding box
-+        // Paper start - Fix MC-4
-+        if (this instanceof ItemEntity) {
-+            if (io.papermc.paper.configuration.GlobalConfiguration.get().misc.fixEntityPositionDesync) {
-+                // encode/decode from ClientboundMoveEntityPacket
-+                x = Mth.lfloor(x * 4096.0) * (1 / 4096.0);
-+                y = Mth.lfloor(y * 4096.0) * (1 / 4096.0);
-+                z = Mth.lfloor(z * 4096.0) * (1 / 4096.0);
-+            }
-+        }
-+        // Paper end - Fix MC-4
-         if (this.position.x != x || this.position.y != y || this.position.z != z) {
-             this.position = new Vec3(x, y, z);
-             int i = Mth.floor(x);
-@@ -3732,6 +4721,12 @@
-             this.levelCallback.onMove();
-         }
- 
-+        // Paper start - Block invalid positions and bounding box; don't allow desync of pos and AABB
-+        // hanging has its own special logic
-+        if (!(this instanceof net.minecraft.world.entity.decoration.HangingEntity) && (forceBoundingBoxUpdate || this.position.x != x || this.position.y != y || this.position.z != z)) {
-+            this.setBoundingBox(this.makeBoundingBox());
-+        }
-+        // Paper end - Block invalid positions and bounding box
-     }
- 
-     public void checkDespawn() {}
-@@ -3818,8 +4813,17 @@
- 
-     @Override
-     public final void setRemoved(Entity.RemovalReason reason) {
-+        // CraftBukkit start - add Bukkit remove cause
-+        this.setRemoved(reason, null);
-+    }
-+
-+    @Override
-+    public final void setRemoved(Entity.RemovalReason entity_removalreason, EntityRemoveEvent.Cause cause) {
-+        CraftEventFactory.callEntityRemoveEvent(this, cause);
-+        // CraftBukkit end
-+        final boolean alreadyRemoved = this.removalReason != null; // Paper - Folia schedulers
-         if (this.removalReason == null) {
--            this.removalReason = reason;
-+            this.removalReason = entity_removalreason;
-         }
- 
-         if (this.removalReason.shouldDestroy()) {
-@@ -3827,14 +4831,30 @@
-         }
- 
-         this.getPassengers().forEach(Entity::stopRiding);
--        this.levelCallback.onRemove(reason);
--        this.onRemoval(reason);
-+        this.levelCallback.onRemove(entity_removalreason);
-+        this.onRemoval(entity_removalreason);
-+        // Paper start - Folia schedulers
-+        if (!(this instanceof ServerPlayer) && entity_removalreason != RemovalReason.CHANGED_DIMENSION && !alreadyRemoved) {
-+            // Players need to be special cased, because they are regularly removed from the world
-+            this.retireScheduler();
-+        }
-+        // Paper end - Folia schedulers
-     }
- 
-     public void unsetRemoved() {
-         this.removalReason = null;
-     }
- 
-+    // Paper start - Folia schedulers
-+    /**
-+     * Invoked only when the entity is truly removed from the server, never to be added to any world.
-+     */
-+    public final void retireScheduler() {
-+        // we need to force create the bukkit entity so that the scheduler can be retired...
-+        this.getBukkitEntity().taskScheduler.retire();
-+    }
-+    // Paper end - Folia schedulers
-+
-     @Override
-     public void setLevelCallback(EntityInLevelCallback changeListener) {
-         this.levelCallback = changeListener;
-@@ -3887,7 +4907,7 @@
-     }
- 
-     public Vec3 getKnownMovement() {
--        LivingEntity entityliving = this.getControllingPassenger();
-+        net.minecraft.world.entity.LivingEntity entityliving = this.getControllingPassenger();
- 
-         if (entityliving instanceof Player entityhuman) {
-             if (this.isAlive()) {
-@@ -3962,4 +4982,14 @@
- 
-         void accept(Entity entity, double x, double y, double z);
-     }
-+
-+    // Paper start - Expose entity id counter
-+    public static int nextEntityId() {
-+        return ENTITY_COUNTER.incrementAndGet();
-+    }
-+
-+    public boolean isTicking() {
-+        return ((net.minecraft.server.level.ServerLevel) this.level).isPositionEntityTicking(this.blockPosition());
-+    }
-+    // Paper end - Expose entity id counter
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/EntitySelector.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/EntitySelector.java.patch
deleted file mode 100644
index e79a8f240b..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/EntitySelector.java.patch
+++ /dev/null
@@ -1,49 +0,0 @@
---- a/net/minecraft/world/entity/EntitySelector.java
-+++ b/net/minecraft/world/entity/EntitySelector.java
-@@ -27,8 +27,25 @@
-     };
-     public static final Predicate<Entity> CAN_BE_COLLIDED_WITH = EntitySelector.NO_SPECTATORS.and(Entity::canBeCollidedWith);
-     public static final Predicate<Entity> CAN_BE_PICKED = EntitySelector.NO_SPECTATORS.and(Entity::isPickable);
-+    // Paper start - Ability to control player's insomnia and phantoms
-+    public static Predicate<Player> IS_INSOMNIAC = (player) -> {
-+        net.minecraft.server.level.ServerPlayer serverPlayer = (net.minecraft.server.level.ServerPlayer) player;
-+        int playerInsomniaTicks = serverPlayer.level().paperConfig().entities.behavior.playerInsomniaStartTicks;
- 
-+        if (playerInsomniaTicks <= 0) {
-+            return false;
-+        }
-+
-+        return net.minecraft.util.Mth.clamp(serverPlayer.getStats().getValue(net.minecraft.stats.Stats.CUSTOM.get(net.minecraft.stats.Stats.TIME_SINCE_REST)), 1, Integer.MAX_VALUE) >= playerInsomniaTicks;
-+    };
-+    // Paper end - Ability to control player's insomnia and phantoms
-+
-     private EntitySelector() {}
-+    // Paper start - Affects Spawning API
-+    public static final Predicate<Entity> PLAYER_AFFECTS_SPAWNING = (entity) -> {
-+        return !entity.isSpectator() && entity.isAlive() && entity instanceof Player player && player.affectsSpawning;
-+    };
-+    // Paper end - Affects Spawning API
- 
-     public static Predicate<Entity> withinDistance(double x, double y, double z, double max) {
-         double d4 = max * max;
-@@ -39,13 +56,18 @@
-     }
- 
-     public static Predicate<Entity> pushableBy(Entity entity) {
-+        // Paper start - Climbing should not bypass cramming gamerule
-+        return pushable(entity, false);
-+    }
-+    public static Predicate<Entity> pushable(Entity entity, boolean ignoreClimbing) {
-+        // Paper end - Climbing should not bypass cramming gamerule
-         PlayerTeam scoreboardteam = entity.getTeam();
-         Team.CollisionRule scoreboardteambase_enumteampush = scoreboardteam == null ? Team.CollisionRule.ALWAYS : scoreboardteam.getCollisionRule();
- 
-         return (Predicate) (scoreboardteambase_enumteampush == Team.CollisionRule.NEVER ? Predicates.alwaysFalse() : EntitySelector.NO_SPECTATORS.and((entity1) -> {
--            if (!entity1.isPushable()) {
-+            if (!entity1.isCollidable(ignoreClimbing) || !entity1.canCollideWithBukkit(entity) || !entity.canCollideWithBukkit(entity1)) { // CraftBukkit - collidable API // Paper - Climbing should not bypass cramming gamerule
-                 return false;
--            } else if (entity.level().isClientSide && (!(entity1 instanceof Player) || !((Player) entity1).isLocalPlayer())) {
-+            } else if (entity1 instanceof Player && entity instanceof Player && !io.papermc.paper.configuration.GlobalConfiguration.get().collisions.enablePlayerCollisions) { // Paper - Configurable player collision
-                 return false;
-             } else {
-                 PlayerTeam scoreboardteam1 = entity1.getTeam();
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/EntityType.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/EntityType.java.patch
deleted file mode 100644
index 1ff8d4806a..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/EntityType.java.patch
+++ /dev/null
@@ -1,179 +0,0 @@
---- a/net/minecraft/world/entity/EntityType.java
-+++ b/net/minecraft/world/entity/EntityType.java
-@@ -176,6 +176,7 @@
- import net.minecraft.world.phys.Vec3;
- import net.minecraft.world.phys.shapes.Shapes;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+import org.bukkit.event.entity.CreatureSpawnEvent;
- import org.slf4j.Logger;
- 
- public class EntityType<T extends Entity> implements FeatureElement, EntityTypeTest<Entity, T> {
-@@ -191,7 +192,7 @@
-         return Items.ACACIA_CHEST_BOAT;
-     }), MobCategory.MISC).noLootTable().sized(1.375F, 0.5625F).eyeHeight(0.5625F).clientTrackingRange(10));
-     public static final EntityType<Allay> ALLAY = EntityType.register("allay", EntityType.Builder.of(Allay::new, MobCategory.CREATURE).sized(0.35F, 0.6F).eyeHeight(0.36F).ridingOffset(0.04F).clientTrackingRange(8).updateInterval(2));
--    public static final EntityType<AreaEffectCloud> AREA_EFFECT_CLOUD = EntityType.register("area_effect_cloud", EntityType.Builder.of(AreaEffectCloud::new, MobCategory.MISC).noLootTable().fireImmune().sized(6.0F, 0.5F).clientTrackingRange(10).updateInterval(Integer.MAX_VALUE));
-+    public static final EntityType<AreaEffectCloud> AREA_EFFECT_CLOUD = EntityType.register("area_effect_cloud", EntityType.Builder.of(AreaEffectCloud::new, MobCategory.MISC).noLootTable().fireImmune().sized(6.0F, 0.5F).clientTrackingRange(10).updateInterval(10)); // CraftBukkit - SPIGOT-3729: track area effect clouds
-     public static final EntityType<Armadillo> ARMADILLO = EntityType.register("armadillo", EntityType.Builder.of(Armadillo::new, MobCategory.CREATURE).sized(0.7F, 0.65F).eyeHeight(0.26F).clientTrackingRange(10));
-     public static final EntityType<ArmorStand> ARMOR_STAND = EntityType.register("armor_stand", EntityType.Builder.of(ArmorStand::new, MobCategory.MISC).sized(0.5F, 1.975F).eyeHeight(1.7775F).clientTrackingRange(10));
-     public static final EntityType<Arrow> ARROW = EntityType.register("arrow", EntityType.Builder.of(Arrow::new, MobCategory.MISC).noLootTable().sized(0.5F, 0.5F).eyeHeight(0.13F).clientTrackingRange(4).updateInterval(20));
-@@ -399,7 +400,7 @@
-         return ResourceKey.create(Registries.ENTITY_TYPE, ResourceLocation.withDefaultNamespace(id));
-     }
- 
--    private static <T extends Entity> EntityType<T> register(String id, EntityType.Builder<T> type) {
-+    private static <T extends Entity> EntityType<T> register(String id, EntityType.Builder type) { // CraftBukkit - decompile error
-         return EntityType.register(EntityType.vanillaEntityId(id), type);
-     }
- 
-@@ -431,16 +432,23 @@
- 
-     @Nullable
-     public T spawn(ServerLevel world, @Nullable ItemStack stack, @Nullable Player player, BlockPos pos, EntitySpawnReason spawnReason, boolean alignPosition, boolean invertY) {
--        Consumer consumer;
-+        // CraftBukkit start
-+        return this.spawn(world, stack, player, pos, spawnReason, alignPosition, invertY, spawnReason == EntitySpawnReason.DISPENSER ? org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DISPENSE_EGG : org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.SPAWNER_EGG); // Paper - use correct spawn reason for dispenser spawn eggs
-+    }
- 
--        if (stack != null) {
--            consumer = EntityType.createDefaultStackConfig(world, stack, player);
-+    @Nullable
-+    public T spawn(ServerLevel worldserver, @Nullable ItemStack itemstack, @Nullable Player entityhuman, BlockPos blockposition, EntitySpawnReason entityspawnreason, boolean flag, boolean flag1, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason spawnReason) {
-+        // CraftBukkit end
-+        Consumer<T> consumer; // CraftBukkit - decompile error
-+
-+        if (itemstack != null) {
-+            consumer = EntityType.createDefaultStackConfig(worldserver, itemstack, entityhuman);
-         } else {
-             consumer = (entity) -> {
-             };
-         }
- 
--        return this.spawn(world, consumer, pos, spawnReason, alignPosition, invertY);
-+        return this.spawn(worldserver, consumer, blockposition, entityspawnreason, flag, flag1, spawnReason); // CraftBukkit
-     }
- 
-     public static <T extends Entity> Consumer<T> createDefaultStackConfig(Level world, ItemStack stack, @Nullable Player player) {
-@@ -464,21 +472,50 @@
-         CustomData customdata = (CustomData) stack.getOrDefault(DataComponents.ENTITY_DATA, CustomData.EMPTY);
- 
-         return !customdata.isEmpty() ? chained.andThen((entity) -> {
--            EntityType.updateCustomEntityTag(world, player, entity, customdata);
-+            try { EntityType.updateCustomEntityTag(world, player, entity, customdata); } catch (Throwable t) { EntityType.LOGGER.warn("Error loading spawn egg NBT", t); } // CraftBukkit - SPIGOT-5665
-         }) : chained;
-     }
- 
-     @Nullable
-     public T spawn(ServerLevel world, BlockPos pos, EntitySpawnReason reason) {
--        return this.spawn(world, (Consumer) null, pos, reason, false, false);
-+        // CraftBukkit start
-+        return this.spawn(world, pos, reason, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT);
-     }
- 
-     @Nullable
-+    public T spawn(ServerLevel worldserver, BlockPos blockposition, EntitySpawnReason entityspawnreason, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason spawnReason) {
-+        return this.spawn(worldserver, (Consumer<T>) null, blockposition, entityspawnreason, false, false, spawnReason); // CraftBukkit - decompile error
-+        // CraftBukkit end
-+    }
-+
-+    @Nullable
-     public T spawn(ServerLevel world, @Nullable Consumer<T> afterConsumer, BlockPos pos, EntitySpawnReason reason, boolean alignPosition, boolean invertY) {
--        T t0 = this.create(world, afterConsumer, pos, reason, alignPosition, invertY);
-+        // CraftBukkit start
-+        return this.spawn(world, afterConsumer, pos, reason, alignPosition, invertY, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT);
-+    }
-+
-+    @Nullable
-+    public T spawn(ServerLevel worldserver, @Nullable Consumer<T> consumer, BlockPos blockposition, EntitySpawnReason entityspawnreason, boolean flag, boolean flag1, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason spawnReason) {
-+        // CraftBukkit end
-+        // Paper start - PreCreatureSpawnEvent
-+        com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent event = new com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent(
-+            io.papermc.paper.util.MCUtil.toLocation(worldserver, blockposition),
-+            org.bukkit.craftbukkit.entity.CraftEntityType.minecraftToBukkit(this),
-+            spawnReason
-+        );
-+        if (!event.callEvent()) {
-+            return null;
-+        }
-+        // Paper end - PreCreatureSpawnEvent
-+        T t0 = this.create(worldserver, consumer, blockposition, entityspawnreason, flag, flag1);
- 
-         if (t0 != null) {
--            world.addFreshEntityWithPassengers(t0);
-+            // CraftBukkit start
-+            worldserver.addFreshEntityWithPassengers(t0, spawnReason);
-+            if (t0.isRemoved()) {
-+                return null; // Don't return an entity when CreatureSpawnEvent is canceled
-+            }
-+            // CraftBukkit end
-             if (t0 instanceof Mob) {
-                 Mob entityinsentient = (Mob) t0;
- 
-@@ -542,6 +579,15 @@
- 
-             if (entity.getType() == entitytypes) {
-                 if (world.isClientSide || !entity.getType().onlyOpCanSetNbt() || player != null && minecraftserver.getPlayerList().isOp(player.getGameProfile())) {
-+                    // Paper start - filter out protected tags
-+                    if (player == null || !player.getBukkitEntity().hasPermission("minecraft.nbt.place")) {
-+                        nbt = nbt.update((compound) -> {
-+                            for (net.minecraft.commands.arguments.NbtPathArgument.NbtPath tag : world.paperConfig().entities.spawning.filteredEntityTagNbtPaths) {
-+                                tag.remove(compound);
-+                            }
-+                        });
-+                    }
-+                    // Paper end - filter out protected tags
-                     nbt.loadInto(entity);
-                 }
-             }
-@@ -613,9 +659,15 @@
-     }
- 
-     public static Optional<Entity> create(CompoundTag nbt, Level world, EntitySpawnReason reason) {
-+    // Paper start - Don't fire sync event during generation
-+        return create(nbt, world, reason, false);
-+    }
-+    public static Optional<Entity> create(CompoundTag nbt, Level world, EntitySpawnReason reason, boolean generation) {
-+    // Paper end - Don't fire sync event during generation
-         return Util.ifElse(EntityType.by(nbt).map((entitytypes) -> {
-             return entitytypes.create(world, reason);
-         }), (entity) -> {
-+            if (generation) entity.generation = true; // Paper - Don't fire sync event during generation
-             entity.load(nbt);
-         }, () -> {
-             EntityType.LOGGER.warn("Skipping Entity with id {}", nbt.getString("id"));
-@@ -638,7 +690,7 @@
-     }
- 
-     public static Optional<EntityType<?>> by(CompoundTag nbt) {
--        return BuiltInRegistries.ENTITY_TYPE.getOptional(ResourceLocation.parse(nbt.getString("id")));
-+        return BuiltInRegistries.ENTITY_TYPE.getOptional(ResourceLocation.tryParse(nbt.getString("id"))); // Paper - Validate ResourceLocation
-     }
- 
-     @Nullable
-@@ -657,7 +709,7 @@
-             }
- 
-             return entity;
--        }).orElse((Object) null);
-+        }).orElse(null); // CraftBukkit - decompile error
-     }
- 
-     public static Stream<Entity> loadEntitiesRecursive(final List<? extends Tag> entityNbtList, final Level world, final EntitySpawnReason reason) {
-@@ -718,7 +770,7 @@
- 
-     @Nullable
-     public T tryCast(Entity obj) {
--        return obj.getType() == this ? obj : null;
-+        return obj.getType() == this ? (T) obj : null; // CraftBukkit - decompile error
-     }
- 
-     @Override
-@@ -791,7 +843,7 @@
-             this.canSpawnFarFromPlayer = spawnGroup == MobCategory.CREATURE || spawnGroup == MobCategory.MISC;
-         }
- 
--        public static <T extends Entity> EntityType.Builder<T> of(EntityType.EntityFactory<T> factory, MobCategory spawnGroup) {
-+        public static <T extends Entity> EntityType.Builder<T> of(EntityType.EntityFactory factory, MobCategory spawnGroup) { // CraftBukkit - decompile error
-             return new EntityType.Builder<>(factory, spawnGroup);
-         }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/Interaction.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/Interaction.java.patch
deleted file mode 100644
index 4ee0117128..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/Interaction.java.patch
+++ /dev/null
@@ -1,42 +0,0 @@
---- a/net/minecraft/world/entity/Interaction.java
-+++ b/net/minecraft/world/entity/Interaction.java
-@@ -27,6 +27,12 @@
- import net.minecraft.world.phys.Vec3;
- import org.slf4j.Logger;
- 
-+// CraftBukkit start
-+import net.minecraft.world.damagesource.DamageSource;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.EntityDamageEvent;
-+// CraftBukkit end
-+
- public class Interaction extends Entity implements Attackable, Targeting {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
-@@ -65,7 +71,7 @@
-             this.setHeight(nbt.getFloat("height"));
-         }
- 
--        DataResult dataresult;
-+        DataResult<com.mojang.datafixers.util.Pair<Interaction.PlayerAction, net.minecraft.nbt.Tag>> dataresult; // CraftBukkit - decompile error
-         Logger logger;
- 
-         if (nbt.contains("attack")) {
-@@ -145,9 +151,16 @@
-     @Override
-     public boolean skipAttackInteraction(Entity attacker) {
-         if (attacker instanceof Player entityhuman) {
-+            // CraftBukkit start
-+            DamageSource source = entityhuman.damageSources().playerAttack(entityhuman);
-+            EntityDamageEvent event = CraftEventFactory.callNonLivingEntityDamageEvent(this, source, 1.0F, false);
-+            if (event.isCancelled()) {
-+                return true;
-+            }
-+            // CraftBukkit end
-             this.attack = new Interaction.PlayerAction(entityhuman.getUUID(), this.level().getGameTime());
-             if (entityhuman instanceof ServerPlayer entityplayer) {
--                CriteriaTriggers.PLAYER_HURT_ENTITY.trigger(entityplayer, this, entityhuman.damageSources().generic(), 1.0F, 1.0F, false);
-+                CriteriaTriggers.PLAYER_HURT_ENTITY.trigger(entityplayer, this, entityhuman.damageSources().generic(), 1.0F, (float) event.getFinalDamage(), false); // CraftBukkit // Paper - use correct source and fix taken/dealt param order
-             }
- 
-             return !this.getResponse();
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/Leashable.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/Leashable.java.patch
deleted file mode 100644
index aa36549d47..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/Leashable.java.patch
+++ /dev/null
@@ -1,157 +0,0 @@
---- a/net/minecraft/world/entity/Leashable.java
-+++ b/net/minecraft/world/entity/Leashable.java
-@@ -15,6 +15,10 @@
- import net.minecraft.world.level.GameRules;
- import net.minecraft.world.level.ItemLike;
- import net.minecraft.world.level.Level;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityUnleashEvent;
-+import org.bukkit.event.entity.EntityUnleashEvent.UnleashReason;
-+// CraftBukkit end
- 
- public interface Leashable {
- 
-@@ -45,7 +49,7 @@
- 
-     default void setDelayedLeashHolderId(int unresolvedLeashHolderId) {
-         this.setLeashData(new Leashable.LeashData(unresolvedLeashHolderId));
--        Leashable.dropLeash((Entity) this, false, false);
-+        Leashable.dropLeash((Entity & Leashable) this, false, false); // CraftBukkit - decompile error
-     }
- 
-     default void readLeashData(CompoundTag nbt) {
-@@ -61,10 +65,16 @@
-     @Nullable
-     private static Leashable.LeashData readLeashDataInternal(CompoundTag nbt) {
-         if (nbt.contains("leash", 10)) {
--            return new Leashable.LeashData(Either.left(nbt.getCompound("leash").getUUID("UUID")));
-+            // Paper start
-+            final CompoundTag leashTag = nbt.getCompound("leash");
-+            if (!leashTag.hasUUID("UUID")) {
-+                return null;
-+            }
-+            return new Leashable.LeashData(Either.left(leashTag.getUUID("UUID")));
-+            // Paper end
-         } else {
-             if (nbt.contains("leash", 11)) {
--                Either<UUID, BlockPos> either = (Either) NbtUtils.readBlockPos(nbt, "leash").map(Either::right).orElse((Object) null);
-+                Either<UUID, BlockPos> either = (Either) NbtUtils.readBlockPos(nbt, "leash").map(Either::right).orElse(null); // CraftBukkit - decompile error
- 
-                 if (either != null) {
-                     return new Leashable.LeashData(either);
-@@ -79,6 +89,11 @@
-         if (leashData != null) {
-             Either<UUID, BlockPos> either = leashData.delayedLeashInfo;
-             Entity entity = leashData.leashHolder;
-+            // CraftBukkit start - SPIGOT-7487: Don't save (and possible drop) leash, when the holder was removed by a plugin
-+            if (entity != null && entity.pluginRemoved) {
-+                return;
-+            }
-+            // CraftBukkit end
- 
-             if (entity instanceof LeashFenceKnotEntity) {
-                 LeashFenceKnotEntity entityleash = (LeashFenceKnotEntity) entity;
-@@ -121,7 +136,9 @@
-                 }
- 
-                 if (entity.tickCount > 100) {
-+                    entity.forceDrops = true; // CraftBukkit
-                     entity.spawnAtLocation(worldserver, (ItemLike) Items.LEAD);
-+                    entity.forceDrops = false; // CraftBukkit
-                     ((Leashable) entity).setLeashData((Leashable.LeashData) null);
-                 }
-             }
-@@ -130,11 +147,11 @@
-     }
- 
-     default void dropLeash() {
--        Leashable.dropLeash((Entity) this, true, true);
-+        Leashable.dropLeash((Entity & Leashable) this, true, true); // CraftBukkit - decompile error
-     }
- 
-     default void removeLeash() {
--        Leashable.dropLeash((Entity) this, true, false);
-+        Leashable.dropLeash((Entity & Leashable) this, true, false); // CraftBukkit - decompile error
-     }
- 
-     default void onLeashRemoved() {}
-@@ -151,7 +168,9 @@
-                 ServerLevel worldserver = (ServerLevel) world;
- 
-                 if (dropItem) {
-+                    entity.forceDrops = true; // CraftBukkit
-                     entity.spawnAtLocation(worldserver, (ItemLike) Items.LEAD);
-+                    entity.forceDrops = false; // CraftBukkit
-                 }
- 
-                 if (sendPacket) {
-@@ -171,7 +190,11 @@
- 
-         if (leashable_a != null && leashable_a.leashHolder != null) {
-             if (!entity.isAlive() || !leashable_a.leashHolder.isAlive()) {
--                if (world.getGameRules().getBoolean(GameRules.RULE_DOENTITYDROPS)) {
-+                // Paper start - Expand EntityUnleashEvent
-+                final EntityUnleashEvent event = new EntityUnleashEvent(entity.getBukkitEntity(), (!entity.isAlive()) ? UnleashReason.PLAYER_UNLEASH : UnleashReason.HOLDER_GONE, world.getGameRules().getBoolean(GameRules.RULE_DOENTITYDROPS) && !entity.pluginRemoved);
-+                event.callEvent();
-+                if (event.isDropLeash()) { // CraftBukkit - SPIGOT-7487: Don't drop leash, when the holder was removed by a plugin
-+                    // Paper end - Expand EntityUnleashEvent
-                     ((Leashable) entity).dropLeash();
-                 } else {
-                     ((Leashable) entity).removeLeash();
-@@ -187,7 +210,7 @@
-                     return;
-                 }
- 
--                if ((double) f > 10.0D) {
-+                if ((double) f > entity.level().paperConfig().misc.maxLeashDistance.or(LEASH_TOO_FAR_DIST)) { // Paper - Configurable max leash distance
-                     ((Leashable) entity).leashTooFarBehaviour();
-                 } else if ((double) f > 6.0D) {
-                     ((Leashable) entity).elasticRangeLeashBehaviour(entity1, f);
-@@ -205,13 +228,27 @@
-     }
- 
-     default void leashTooFarBehaviour() {
--        this.dropLeash();
-+        // CraftBukkit start
-+        boolean dropLeash = true; // Paper
-+        if (this instanceof Entity entity) {
-+            // Paper start - Expand EntityUnleashEvent
-+            final EntityUnleashEvent event = new EntityUnleashEvent(entity.getBukkitEntity(), EntityUnleashEvent.UnleashReason.DISTANCE, true);
-+            if (!event.callEvent()) return;
-+            dropLeash = event.isDropLeash();
-+        }
-+        // CraftBukkit end
-+        if (dropLeash) {
-+            this.dropLeash();
-+        } else {
-+            this.removeLeash();
-+        }
-+        // Paper end - Expand EntityUnleashEvent
-     }
- 
-     default void closeRangeLeashBehaviour(Entity entity) {}
- 
-     default void elasticRangeLeashBehaviour(Entity leashHolder, float distance) {
--        Leashable.legacyElasticRangeLeashBehaviour((Entity) this, leashHolder, distance);
-+        Leashable.legacyElasticRangeLeashBehaviour((Entity & Leashable) this, leashHolder, distance); // CraftBukkit - decompile error
-     }
- 
-     private static <E extends Entity & Leashable> void legacyElasticRangeLeashBehaviour(E entity, Entity leashHolder, float distance) {
-@@ -223,7 +260,7 @@
-     }
- 
-     default void setLeashedTo(Entity leashHolder, boolean sendPacket) {
--        this.setLeashedTo((Entity) this, leashHolder, sendPacket);
-+        Leashable.setLeashedTo((Entity & Leashable) this, leashHolder, sendPacket); // CraftBukkit - decompile error
-     }
- 
-     private static <E extends Entity & Leashable> void setLeashedTo(E entity, Entity leashHolder, boolean sendPacket) {
-@@ -254,7 +291,7 @@
- 
-     @Nullable
-     default Entity getLeashHolder() {
--        return Leashable.getLeashHolder((Entity) this);
-+        return Leashable.getLeashHolder((Entity & Leashable) this); // CraftBukkit - decompile error
-     }
- 
-     @Nullable
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/LightningBolt.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/LightningBolt.java.patch
deleted file mode 100644
index 1dc2b6bf90..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/LightningBolt.java.patch
+++ /dev/null
@@ -1,160 +0,0 @@
---- a/net/minecraft/world/entity/LightningBolt.java
-+++ b/net/minecraft/world/entity/LightningBolt.java
-@@ -30,6 +30,10 @@
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.phys.AABB;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public class LightningBolt extends Entity {
- 
-@@ -44,6 +48,7 @@
-     private ServerPlayer cause;
-     private final Set<Entity> hitEntities = Sets.newHashSet();
-     private int blocksSetOnFire;
-+    public boolean isEffect; // Paper - Properly handle lightning effects api
- 
-     public LightningBolt(EntityType<? extends LightningBolt> type, Level world) {
-         super(type, world);
-@@ -82,7 +87,7 @@
-     @Override
-     public void tick() {
-         super.tick();
--        if (this.life == 2) {
-+        if (!this.isEffect && this.life == 2) { // Paper - Properly handle lightning effects api
-             if (this.level().isClientSide()) {
-                 this.level().playLocalSound(this.getX(), this.getY(), this.getZ(), SoundEvents.LIGHTNING_BOLT_THUNDER, SoundSource.WEATHER, 10000.0F, 0.8F + this.random.nextFloat() * 0.2F, false);
-                 this.level().playLocalSound(this.getX(), this.getY(), this.getZ(), SoundEvents.LIGHTNING_BOLT_IMPACT, SoundSource.WEATHER, 2.0F, 0.5F + this.random.nextFloat() * 0.2F, false);
-@@ -94,7 +99,7 @@
-                 }
- 
-                 this.powerLightningRod();
--                LightningBolt.clearCopperOnLightningStrike(this.level(), this.getStrikePosition());
-+                LightningBolt.clearCopperOnLightningStrike(this.level(), this.getStrikePosition(), this); // Paper - Call EntityChangeBlockEvent
-                 this.gameEvent(GameEvent.LIGHTNING_STRIKE);
-             }
-         }
-@@ -120,7 +125,7 @@
-                     }
-                 }
- 
--                this.discard();
-+                this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-             } else if (this.life < -this.random.nextInt(10)) {
-                 --this.flashes;
-                 this.life = 1;
-@@ -129,7 +134,7 @@
-             }
-         }
- 
--        if (this.life >= 0) {
-+        if (this.life >= 0 && !this.isEffect) { // CraftBukkit - add !this.visualOnly // Paper - Properly handle lightning effects api
-             if (!(this.level() instanceof ServerLevel)) {
-                 this.level().setSkyFlashTime(2);
-             } else if (!this.visualOnly) {
-@@ -158,7 +163,7 @@
-     }
- 
-     private void spawnFire(int spreadAttempts) {
--        if (!this.visualOnly) {
-+        if (!this.visualOnly && !this.isEffect) {  // Paper - Properly handle lightning effects api
-             Level world = this.level();
- 
-             if (world instanceof ServerLevel) {
-@@ -169,8 +174,12 @@
-                     BlockState iblockdata = BaseFireBlock.getState(this.level(), blockposition);
- 
-                     if (this.level().getBlockState(blockposition).isAir() && iblockdata.canSurvive(this.level(), blockposition)) {
--                        this.level().setBlockAndUpdate(blockposition, iblockdata);
--                        ++this.blocksSetOnFire;
-+                        // CraftBukkit start - add "!visualOnly"
-+                        if (!this.visualOnly && !CraftEventFactory.callBlockIgniteEvent(this.level(), blockposition, this).isCancelled()) {
-+                            this.level().setBlockAndUpdate(blockposition, iblockdata);
-+                            ++this.blocksSetOnFire;
-+                        }
-+                        // CraftBukkit end
-                     }
- 
-                     for (int j = 0; j < spreadAttempts; ++j) {
-@@ -178,8 +187,12 @@
- 
-                         iblockdata = BaseFireBlock.getState(this.level(), blockposition1);
-                         if (this.level().getBlockState(blockposition1).isAir() && iblockdata.canSurvive(this.level(), blockposition1)) {
--                            this.level().setBlockAndUpdate(blockposition1, iblockdata);
--                            ++this.blocksSetOnFire;
-+                            // CraftBukkit start - add "!visualOnly"
-+                            if (!this.visualOnly && !CraftEventFactory.callBlockIgniteEvent(this.level(), blockposition1, this).isCancelled()) {
-+                                this.level().setBlockAndUpdate(blockposition1, iblockdata);
-+                                ++this.blocksSetOnFire;
-+                            }
-+                            // CraftBukkit end
-                         }
-                     }
- 
-@@ -190,7 +203,7 @@
- 
-     }
- 
--    private static void clearCopperOnLightningStrike(Level world, BlockPos pos) {
-+    private static void clearCopperOnLightningStrike(Level world, BlockPos pos, Entity lightning) { // Paper - Call EntityChangeBlockEvent
-         BlockState iblockdata = world.getBlockState(pos);
-         BlockPos blockposition1;
-         BlockState iblockdata1;
-@@ -204,24 +217,29 @@
-         }
- 
-         if (iblockdata1.getBlock() instanceof WeatheringCopper) {
--            world.setBlockAndUpdate(blockposition1, WeatheringCopper.getFirst(world.getBlockState(blockposition1)));
-+            // Paper start - Call EntityChangeBlockEvent
-+            BlockState newBlock = WeatheringCopper.getFirst(world.getBlockState(blockposition1));
-+            if (CraftEventFactory.callEntityChangeBlockEvent(lightning, blockposition1, newBlock)) {
-+                world.setBlockAndUpdate(blockposition1, newBlock);
-+            }
-+            // Paper end - Call EntityChangeBlockEvent
-             BlockPos.MutableBlockPos blockposition_mutableblockposition = pos.mutable();
-             int i = world.random.nextInt(3) + 3;
- 
-             for (int j = 0; j < i; ++j) {
-                 int k = world.random.nextInt(8) + 1;
- 
--                LightningBolt.randomWalkCleaningCopper(world, blockposition1, blockposition_mutableblockposition, k);
-+                LightningBolt.randomWalkCleaningCopper(world, blockposition1, blockposition_mutableblockposition, k, lightning); // Paper - transmit LightningBolt instance to call EntityChangeBlockEvent
-             }
- 
-         }
-     }
- 
--    private static void randomWalkCleaningCopper(Level world, BlockPos pos, BlockPos.MutableBlockPos mutablePos, int count) {
-+    private static void randomWalkCleaningCopper(Level world, BlockPos pos, BlockPos.MutableBlockPos mutablePos, int count, Entity lightning) { // Paper - transmit LightningBolt instance to call EntityChangeBlockEvent
-         mutablePos.set(pos);
- 
-         for (int j = 0; j < count; ++j) {
--            Optional<BlockPos> optional = LightningBolt.randomStepCleaningCopper(world, mutablePos);
-+            Optional<BlockPos> optional = LightningBolt.randomStepCleaningCopper(world, mutablePos, lightning); // Paper - transmit LightningBolt instance to call EntityChangeBlockEvent
- 
-             if (optional.isEmpty()) {
-                 break;
-@@ -232,7 +250,7 @@
- 
-     }
- 
--    private static Optional<BlockPos> randomStepCleaningCopper(Level world, BlockPos pos) {
-+    private static Optional<BlockPos> randomStepCleaningCopper(Level world, BlockPos pos, Entity lightning) { // Paper - transmit LightningBolt instance to call EntityChangeBlockEvent
-         Iterator iterator = BlockPos.randomInCube(world.random, 10, pos, 1).iterator();
- 
-         BlockPos blockposition1;
-@@ -247,8 +265,10 @@
-             iblockdata = world.getBlockState(blockposition1);
-         } while (!(iblockdata.getBlock() instanceof WeatheringCopper));
- 
-+        BlockPos blockposition1Final = blockposition1; // CraftBukkit - decompile error
-         WeatheringCopper.getPrevious(iblockdata).ifPresent((iblockdata1) -> {
--            world.setBlockAndUpdate(blockposition1, iblockdata1);
-+            if (CraftEventFactory.callEntityChangeBlockEvent(lightning, blockposition1Final, iblockdata1)) // Paper - call EntityChangeBlockEvent
-+            world.setBlockAndUpdate(blockposition1Final, iblockdata1); // CraftBukkit - decompile error
-         });
-         world.levelEvent(3002, blockposition1, -1);
-         return Optional.of(blockposition1);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/Mob.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/Mob.java.patch
deleted file mode 100644
index aeaafc3a1b..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/Mob.java.patch
+++ /dev/null
@@ -1,472 +0,0 @@
---- a/net/minecraft/world/entity/Mob.java
-+++ b/net/minecraft/world/entity/Mob.java
-@@ -84,6 +84,17 @@
- import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets;
- import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
- import net.minecraft.world.phys.AABB;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.craftbukkit.entity.CraftLivingEntity;
-+import org.bukkit.event.entity.CreatureSpawnEvent;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.event.entity.EntityTargetLivingEntityEvent;
-+import org.bukkit.event.entity.EntityTargetEvent;
-+import org.bukkit.event.entity.EntityTransformEvent;
-+import org.bukkit.event.entity.EntityUnleashEvent;
-+import org.bukkit.event.entity.EntityUnleashEvent.UnleashReason;
-+// CraftBukkit end
- 
- public abstract class Mob extends LivingEntity implements EquipmentUser, Leashable, Targeting {
- 
-@@ -112,6 +123,7 @@
-     private final BodyRotationControl bodyRotationControl;
-     protected PathNavigation navigation;
-     public GoalSelector goalSelector;
-+    @Nullable public net.minecraft.world.entity.ai.goal.FloatGoal goalFloat; // Paper - Allow nerfed mobs to jump and float
-     public GoalSelector targetSelector;
-     @Nullable
-     private LivingEntity target;
-@@ -132,6 +144,8 @@
-     private BlockPos restrictCenter;
-     private float restrictRadius;
- 
-+    public boolean aware = true; // CraftBukkit
-+
-     protected Mob(EntityType<? extends Mob> type, Level world) {
-         super(type, world);
-         this.handItems = NonNullList.withSize(2, ItemStack.EMPTY);
-@@ -157,8 +171,14 @@
-         if (world instanceof ServerLevel) {
-             this.registerGoals();
-         }
-+
-+    }
- 
-+    // CraftBukkit start
-+    public void setPersistenceRequired(boolean persistenceRequired) {
-+        this.persistenceRequired = persistenceRequired;
-     }
-+    // CraftBukkit end
- 
-     protected void registerGoals() {}
- 
-@@ -264,13 +284,44 @@
- 
-     @Nullable
-     protected final LivingEntity getTargetFromBrain() {
--        return (LivingEntity) this.getBrain().getMemory(MemoryModuleType.ATTACK_TARGET).orElse((Object) null);
-+        return (LivingEntity) this.getBrain().getMemory(MemoryModuleType.ATTACK_TARGET).orElse(null); // CraftBukkit - decompile error
-     }
- 
-     public void setTarget(@Nullable LivingEntity target) {
--        this.target = target;
-+        // CraftBukkit start - fire event
-+        this.setTarget(target, EntityTargetEvent.TargetReason.UNKNOWN, true);
-     }
- 
-+    public boolean setTarget(LivingEntity entityliving, EntityTargetEvent.TargetReason reason, boolean fireEvent) {
-+        if (this.getTarget() == entityliving) return false;
-+        if (fireEvent) {
-+            if (reason == EntityTargetEvent.TargetReason.UNKNOWN && this.getTarget() != null && entityliving == null) {
-+                reason = this.getTarget().isAlive() ? EntityTargetEvent.TargetReason.FORGOT_TARGET : EntityTargetEvent.TargetReason.TARGET_DIED;
-+            }
-+            if (reason == EntityTargetEvent.TargetReason.UNKNOWN) {
-+                this.level().getCraftServer().getLogger().log(java.util.logging.Level.WARNING, "Unknown target reason, please report on the issue tracker", new Exception());
-+            }
-+            CraftLivingEntity ctarget = null;
-+            if (entityliving != null) {
-+                ctarget = (CraftLivingEntity) entityliving.getBukkitEntity();
-+            }
-+            EntityTargetLivingEntityEvent event = new EntityTargetLivingEntityEvent(this.getBukkitEntity(), ctarget, reason);
-+            this.level().getCraftServer().getPluginManager().callEvent(event);
-+            if (event.isCancelled()) {
-+                return false;
-+            }
-+
-+            if (event.getTarget() != null) {
-+                entityliving = ((CraftLivingEntity) event.getTarget()).getHandle();
-+            } else {
-+                entityliving = null;
-+            }
-+        }
-+        this.target = entityliving;
-+        return true;
-+        // CraftBukkit end
-+    }
-+
-     @Override
-     public boolean canAttackType(EntityType<?> type) {
-         return type != EntityType.GHAST;
-@@ -399,6 +450,12 @@
-         return null;
-     }
- 
-+    // CraftBukkit start - Add delegate method
-+    public SoundEvent getAmbientSound0() {
-+        return this.getAmbientSound();
-+    }
-+    // CraftBukkit end
-+
-     @Override
-     public void addAdditionalSaveData(CompoundTag nbt) {
-         super.addAdditionalSaveData(nbt);
-@@ -473,13 +530,25 @@
-             nbt.putBoolean("NoAI", this.isNoAi());
-         }
- 
-+        nbt.putBoolean("Bukkit.Aware", this.aware); // CraftBukkit
-     }
- 
-     @Override
-     public void readAdditionalSaveData(CompoundTag nbt) {
-         super.readAdditionalSaveData(nbt);
--        this.setCanPickUpLoot(nbt.getBoolean("CanPickUpLoot"));
--        this.persistenceRequired = nbt.getBoolean("PersistenceRequired");
-+        // CraftBukkit start - If looting or persistence is false only use it if it was set after we started using it
-+        if (nbt.contains("CanPickUpLoot", 99)) {
-+            boolean data = nbt.getBoolean("CanPickUpLoot");
-+            if (isLevelAtLeast(nbt, 1) || data) {
-+                this.setCanPickUpLoot(data);
-+            }
-+        }
-+
-+        boolean data = nbt.getBoolean("PersistenceRequired");
-+        if (isLevelAtLeast(nbt, 1) || data) {
-+            this.persistenceRequired = data;
-+        }
-+        // CraftBukkit end
-         ListTag nbttaglist;
-         CompoundTag nbttagcompound1;
-         int i;
-@@ -540,13 +609,18 @@
-         this.readLeashData(nbt);
-         this.setLeftHanded(nbt.getBoolean("LeftHanded"));
-         if (nbt.contains("DeathLootTable", 8)) {
--            this.lootTable = Optional.of(ResourceKey.create(Registries.LOOT_TABLE, ResourceLocation.parse(nbt.getString("DeathLootTable"))));
-+            this.lootTable = Optional.ofNullable(ResourceLocation.tryParse(nbt.getString("DeathLootTable"))).map((rs) -> ResourceKey.create(Registries.LOOT_TABLE, rs)); // Paper - Validate ResourceLocation
-         } else {
-             this.lootTable = Optional.empty();
-         }
- 
-         this.lootTableSeed = nbt.getLong("DeathLootTableSeed");
-         this.setNoAi(nbt.getBoolean("NoAI"));
-+        // CraftBukkit start
-+        if (nbt.contains("Bukkit.Aware")) {
-+            this.aware = nbt.getBoolean("Bukkit.Aware");
-+        }
-+        // CraftBukkit end
-     }
- 
-     @Override
-@@ -608,6 +682,11 @@
-                     ItemEntity entityitem = (ItemEntity) iterator.next();
- 
-                     if (!entityitem.isRemoved() && !entityitem.getItem().isEmpty() && !entityitem.hasPickUpDelay() && this.wantsToPickUp(worldserver, entityitem.getItem())) {
-+                        // Paper start - Item#canEntityPickup
-+                        if (!entityitem.canMobPickup) {
-+                            continue;
-+                        }
-+                        // Paper end - Item#canEntityPickup
-                         this.pickUpItem(worldserver, entityitem);
-                     }
-                 }
-@@ -623,23 +702,29 @@
- 
-     protected void pickUpItem(ServerLevel world, ItemEntity itemEntity) {
-         ItemStack itemstack = itemEntity.getItem();
--        ItemStack itemstack1 = this.equipItemIfPossible(world, itemstack.copy());
-+        ItemStack itemstack1 = this.equipItemIfPossible(world, itemstack.copy(), itemEntity); // CraftBukkit - add item
- 
-         if (!itemstack1.isEmpty()) {
-             this.onItemPickup(itemEntity);
-             this.take(itemEntity, itemstack1.getCount());
-             itemstack.shrink(itemstack1.getCount());
-             if (itemstack.isEmpty()) {
--                itemEntity.discard();
-+                itemEntity.discard(EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
-             }
-         }
- 
-     }
- 
-     public ItemStack equipItemIfPossible(ServerLevel world, ItemStack stack) {
--        EquipmentSlot enumitemslot = this.getEquipmentSlotForItem(stack);
-+        // CraftBukkit start - add item
-+        return this.equipItemIfPossible(world, stack, null);
-+    }
-+
-+    public ItemStack equipItemIfPossible(ServerLevel worldserver, ItemStack itemstack, ItemEntity entityitem) {
-+        // CraftBukkit end
-+        EquipmentSlot enumitemslot = this.getEquipmentSlotForItem(itemstack);
-         ItemStack itemstack1 = this.getItemBySlot(enumitemslot);
--        boolean flag = this.canReplaceCurrentItem(stack, itemstack1, enumitemslot);
-+        boolean flag = this.canReplaceCurrentItem(itemstack, itemstack1, enumitemslot);
- 
-         if (enumitemslot.isArmor() && !flag) {
-             enumitemslot = EquipmentSlot.MAINHAND;
-@@ -647,14 +732,22 @@
-             flag = itemstack1.isEmpty();
-         }
- 
--        if (flag && this.canHoldItem(stack)) {
-+        // CraftBukkit start
-+        boolean canPickup = flag && this.canHoldItem(itemstack);
-+        if (entityitem != null) {
-+            canPickup = !org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPickupItemEvent(this, entityitem, 0, !canPickup).isCancelled();
-+        }
-+        if (canPickup) {
-+            // CraftBukkit end
-             double d0 = (double) this.getEquipmentDropChance(enumitemslot);
- 
-             if (!itemstack1.isEmpty() && (double) Math.max(this.random.nextFloat() - 0.1F, 0.0F) < d0) {
--                this.spawnAtLocation(world, itemstack1);
-+                this.forceDrops = true; // CraftBukkit
-+                this.spawnAtLocation(worldserver, itemstack1);
-+                this.forceDrops = false; // CraftBukkit
-             }
- 
--            ItemStack itemstack2 = enumitemslot.limit(stack);
-+            ItemStack itemstack2 = enumitemslot.limit(itemstack);
- 
-             this.setItemSlotAndDropWhenKilled(enumitemslot, itemstack2);
-             return itemstack2;
-@@ -768,25 +861,29 @@
-     @Override
-     public void checkDespawn() {
-         if (this.level().getDifficulty() == Difficulty.PEACEFUL && this.shouldDespawnInPeaceful()) {
--            this.discard();
-+            this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-         } else if (!this.isPersistenceRequired() && !this.requiresCustomPersistence()) {
--            Player entityhuman = this.level().getNearestPlayer(this, -1.0D);
-+            Player entityhuman = this.level().findNearbyPlayer(this, -1.0D, EntitySelector.PLAYER_AFFECTS_SPAWNING); // Paper - Affects Spawning API
- 
-             if (entityhuman != null) {
--                double d0 = entityhuman.distanceToSqr((Entity) this);
--                int i = this.getType().getCategory().getDespawnDistance();
--                int j = i * i;
--
--                if (d0 > (double) j && this.removeWhenFarAway(d0)) {
--                    this.discard();
-+                // Paper start - Configurable despawn distances
-+                final io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DespawnRangePair despawnRangePair = this.level().paperConfig().entities.spawning.despawnRanges.get(this.getType().getCategory());
-+                final io.papermc.paper.configuration.type.DespawnRange.Shape shape = this.level().paperConfig().entities.spawning.despawnRangeShape;
-+                final double dy = Math.abs(entityhuman.getY() - this.getY());
-+                final double dySqr = Math.pow(dy, 2);
-+                final double dxSqr = Math.pow(entityhuman.getX() - this.getX(), 2);
-+                final double dzSqr = Math.pow(entityhuman.getZ() - this.getZ(), 2);
-+                final double distanceSquared = dxSqr + dzSqr + dySqr;
-+                // Despawn if hard/soft limit is exceeded
-+                if (despawnRangePair.hard().shouldDespawn(shape, dxSqr, dySqr, dzSqr, dy) && this.removeWhenFarAway(distanceSquared)) {
-+                    this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-                 }
--
--                int k = this.getType().getCategory().getNoDespawnDistance();
--                int l = k * k;
--
--                if (this.noActionTime > 600 && this.random.nextInt(800) == 0 && d0 > (double) l && this.removeWhenFarAway(d0)) {
--                    this.discard();
--                } else if (d0 < (double) l) {
-+                if (despawnRangePair.soft().shouldDespawn(shape, dxSqr, dySqr, dzSqr, dy)) {
-+                    if (this.noActionTime > 600 && this.random.nextInt(800) == 0 && this.removeWhenFarAway(distanceSquared)) {
-+                        this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-+                    }
-+                } else {
-+                // Paper end - Configurable despawn distances
-                     this.noActionTime = 0;
-                 }
-             }
-@@ -799,6 +896,15 @@
-     @Override
-     protected final void serverAiStep() {
-         ++this.noActionTime;
-+        // Paper start - Allow nerfed mobs to jump and float
-+        if (!this.aware) {
-+            if (goalFloat != null) {
-+                if (goalFloat.canUse()) goalFloat.tick();
-+                this.getJumpControl().tick();
-+            }
-+            return;
-+        }
-+        // Paper end - Allow nerfed mobs to jump and float
-         ProfilerFiller gameprofilerfiller = Profiler.get();
- 
-         gameprofilerfiller.push("sensing");
-@@ -994,23 +1100,36 @@
- 
-     @Override
-     public void setItemSlot(EquipmentSlot slot, ItemStack stack) {
-+        // Paper start - Fix silent equipment change
-+        setItemSlot(slot, stack, false);
-+    }
-+
-+    @Override
-+    public void setItemSlot(EquipmentSlot slot, ItemStack stack, boolean silent) {
-+        // Paper end - Fix silent equipment change
-         this.verifyEquippedItem(stack);
-         switch (slot.getType()) {
-             case HAND:
--                this.onEquipItem(slot, (ItemStack) this.handItems.set(slot.getIndex(), stack), stack);
-+                this.onEquipItem(slot, (ItemStack) this.handItems.set(slot.getIndex(), stack), stack, silent); // Paper - Fix silent equipment change
-                 break;
-             case HUMANOID_ARMOR:
--                this.onEquipItem(slot, (ItemStack) this.armorItems.set(slot.getIndex(), stack), stack);
-+                this.onEquipItem(slot, (ItemStack) this.armorItems.set(slot.getIndex(), stack), stack, silent); // Paper - Fix silent equipment change
-                 break;
-             case ANIMAL_ARMOR:
-                 ItemStack itemstack1 = this.bodyArmorItem;
- 
-                 this.bodyArmorItem = stack;
--                this.onEquipItem(slot, itemstack1, stack);
-+                this.onEquipItem(slot, itemstack1, stack, silent); // Paper - Fix silent equipment change
-         }
- 
-     }
- 
-+    // Paper start
-+    protected boolean shouldSkipLoot(EquipmentSlot slot) { // method to avoid to fallback into the global mob loot logic (i.e fox)
-+        return false;
-+    }
-+    // Paper end
-+
-     @Override
-     protected void dropCustomDeathLoot(ServerLevel world, DamageSource source, boolean causedByPlayer) {
-         super.dropCustomDeathLoot(world, source, causedByPlayer);
-@@ -1018,6 +1137,7 @@
- 
-         while (iterator.hasNext()) {
-             EquipmentSlot enumitemslot = (EquipmentSlot) iterator.next();
-+            if (this.shouldSkipLoot(enumitemslot)) continue; // Paper
-             ItemStack itemstack = this.getItemBySlot(enumitemslot);
-             float f = this.getEquipmentDropChance(enumitemslot);
- 
-@@ -1042,7 +1162,13 @@
-                     }
- 
-                     this.spawnAtLocation(world, itemstack);
-+                    if (this.clearEquipmentSlots) { // Paper
-                     this.setItemSlot(enumitemslot, ItemStack.EMPTY);
-+                    // Paper start
-+                    } else {
-+                        this.clearedEquipmentSlots.add(enumitemslot);
-+                    }
-+                    // Paper end
-                 }
-             }
-         }
-@@ -1338,7 +1464,7 @@
-         if (itemstack.getItem() instanceof SpawnEggItem) {
-             if (this.level() instanceof ServerLevel) {
-                 SpawnEggItem itemmonsteregg = (SpawnEggItem) itemstack.getItem();
--                Optional<Mob> optional = itemmonsteregg.spawnOffspringFromSpawnEgg(player, this, this.getType(), (ServerLevel) this.level(), this.position(), itemstack);
-+                Optional<Mob> optional = itemmonsteregg.spawnOffspringFromSpawnEgg(player, this, (EntityType<? extends Mob>) this.getType(), (ServerLevel) this.level(), this.position(), itemstack); // CraftBukkit - decompile error
- 
-                 optional.ifPresent((entityinsentient) -> {
-                     this.onOffspringSpawnedFromEgg(player, entityinsentient);
-@@ -1389,28 +1515,51 @@
-         return this.restrictRadius != -1.0F;
-     }
- 
-+    // CraftBukkit start
-     @Nullable
-     public <T extends Mob> T convertTo(EntityType<T> entityType, ConversionParams context, EntitySpawnReason reason, ConversionParams.AfterConversion<T> finalizer) {
-+        return this.convertTo(entityType, context, reason, finalizer, EntityTransformEvent.TransformReason.UNKNOWN, CreatureSpawnEvent.SpawnReason.DEFAULT);
-+    }
-+
-+    @Nullable
-+    public <T extends Mob> T convertTo(EntityType<T> entitytypes, ConversionParams conversionparams, EntitySpawnReason entityspawnreason, ConversionParams.AfterConversion<T> conversionparams_a, EntityTransformEvent.TransformReason transformReason, CreatureSpawnEvent.SpawnReason spawnReason) {
-+    // Paper start - entity zap event - allow cancellation of conversion post creation
-+        return this.convertTo(entitytypes, conversionparams, entityspawnreason, e -> { conversionparams_a.finalizeConversion(e); return true; }, transformReason, spawnReason);
-+    }
-+    @Nullable
-+    public <T extends Mob> T convertTo(EntityType<T> entitytypes, ConversionParams conversionparams, EntitySpawnReason entityspawnreason, ConversionParams.CancellingAfterConversion<T> conversionparams_a, EntityTransformEvent.TransformReason transformReason, CreatureSpawnEvent.SpawnReason spawnReason) {
-+    // Paper end - entity zap event - allow cancellation of conversion post creation
-+        // CraftBukkit end
-         if (this.isRemoved()) {
-             return null;
-         } else {
--            T t0 = (Mob) entityType.create(this.level(), reason);
-+            T t0 = entitytypes.create(this.level(), EntitySpawnReason.CONVERSION); // CraftBukkit - decompile error
- 
-             if (t0 == null) {
-                 return null;
-             } else {
--                context.type().convert(this, t0, context);
--                finalizer.finalizeConversion(t0);
-+                conversionparams.type().convert(this, t0, conversionparams);
-+                if (!conversionparams_a.finalizeConversionOrCancel(t0)) return null; // Paper - entity zap event - return null if conversion was cancelled
-                 Level world = this.level();
- 
-+                // CraftBukkit start
-+                if (transformReason == null) {
-+                    // Special handling for slime split and pig lightning
-+                    return t0;
-+                }
-+
-+                if (CraftEventFactory.callEntityTransformEvent(this, t0, transformReason).isCancelled()) {
-+                    return null;
-+                }
-+                // CraftBukkit end
-                 if (world instanceof ServerLevel) {
-                     ServerLevel worldserver = (ServerLevel) world;
- 
--                    worldserver.addFreshEntity(t0);
-+                    worldserver.addFreshEntity(t0, spawnReason); // CraftBukkit
-                 }
- 
--                if (context.type().shouldDiscardAfterConversion()) {
--                    this.discard();
-+                if (conversionparams.type().shouldDiscardAfterConversion()) {
-+                    this.discard(EntityRemoveEvent.Cause.TRANSFORMATION); // CraftBukkit - add Bukkit remove cause
-                 }
- 
-                 return t0;
-@@ -1420,10 +1569,22 @@
- 
-     @Nullable
-     public <T extends Mob> T convertTo(EntityType<T> entityType, ConversionParams context, ConversionParams.AfterConversion<T> finalizer) {
--        return this.convertTo(entityType, context, EntitySpawnReason.CONVERSION, finalizer);
-+        // CraftBukkit start
-+        return this.convertTo(entityType, context, finalizer, EntityTransformEvent.TransformReason.UNKNOWN, CreatureSpawnEvent.SpawnReason.DEFAULT);
-     }
- 
-     @Nullable
-+    public <T extends Mob> T convertTo(EntityType<T> entitytypes, ConversionParams conversionparams, ConversionParams.AfterConversion<T> conversionparams_a, EntityTransformEvent.TransformReason transformReason, CreatureSpawnEvent.SpawnReason spawnReason) {
-+    // Paper start - entity zap event - allow cancellation of conversion post creation
-+        return this.convertTo(entitytypes, conversionparams, e -> { conversionparams_a.finalizeConversion(e); return true; }, transformReason, spawnReason);
-+    }
-+    public <T extends Mob> T convertTo(EntityType<T> entitytypes, ConversionParams conversionparams, ConversionParams.CancellingAfterConversion<T> conversionparams_a, EntityTransformEvent.TransformReason transformReason, CreatureSpawnEvent.SpawnReason spawnReason) {
-+    // Paper start - entity zap event - allow cancellation of conversion post creation
-+        return this.convertTo(entitytypes, conversionparams, EntitySpawnReason.CONVERSION, conversionparams_a, transformReason, spawnReason);
-+        // CraftBukkit end
-+    }
-+
-+    @Nullable
-     @Override
-     public Leashable.LeashData getLeashData() {
-         return this.leashData;
-@@ -1458,7 +1619,15 @@
-         boolean flag1 = super.startRiding(entity, force);
- 
-         if (flag1 && this.isLeashed()) {
--            this.dropLeash();
-+            // Paper start - Expand EntityUnleashEvent
-+            EntityUnleashEvent event = new EntityUnleashEvent(this.getBukkitEntity(), EntityUnleashEvent.UnleashReason.UNKNOWN, true);
-+            if (!event.callEvent()) { return flag1; }
-+            if (event.isDropLeash()) {
-+                this.dropLeash();
-+            } else {
-+                this.removeLeash();
-+            }
-+            // Paper end - Expand EntityUnleashEvent
-         }
- 
-         return flag1;
-@@ -1542,7 +1711,7 @@
- 
-             if (f1 > 0.0F && target instanceof LivingEntity) {
-                 entityliving = (LivingEntity) target;
--                entityliving.knockback((double) (f1 * 0.5F), (double) Mth.sin(this.getYRot() * 0.017453292F), (double) (-Mth.cos(this.getYRot() * 0.017453292F)));
-+                entityliving.knockback((double) (f1 * 0.5F), (double) Mth.sin(this.getYRot() * 0.017453292F), (double) (-Mth.cos(this.getYRot() * 0.017453292F)), this, io.papermc.paper.event.entity.EntityKnockbackEvent.Cause.ENTITY_ATTACK); // CraftBukkit // Paper - knockback events
-                 this.setDeltaMovement(this.getDeltaMovement().multiply(0.6D, 1.0D, 0.6D));
-             }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/NeutralMob.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/NeutralMob.java.patch
deleted file mode 100644
index 2088822118..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/NeutralMob.java.patch
+++ /dev/null
@@ -1,86 +0,0 @@
---- a/net/minecraft/world/entity/NeutralMob.java
-+++ b/net/minecraft/world/entity/NeutralMob.java
-@@ -8,6 +8,9 @@
- import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.level.GameRules;
- import net.minecraft.world.level.Level;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityTargetEvent;
-+// CraftBukkit end
- 
- public interface NeutralMob {
- 
-@@ -42,24 +45,11 @@
-                 UUID uuid = nbt.getUUID("AngryAt");
- 
-                 this.setPersistentAngerTarget(uuid);
--                Entity entity = ((ServerLevel) world).getEntity(uuid);
--
--                if (entity != null) {
--                    if (entity instanceof Mob) {
--                        Mob entityinsentient = (Mob) entity;
--
--                        this.setTarget(entityinsentient);
--                        this.setLastHurtByMob(entityinsentient);
--                    }
--
--                    if (entity instanceof Player) {
--                        Player entityhuman = (Player) entity;
--
--                        this.setTarget(entityhuman);
--                        this.setLastHurtByPlayer(entityhuman);
--                    }
--
--                }
-+                // Paper - Prevent entity loading causing async lookups; Moved diff to separate method
-+                // If this entity already survived its first tick, e.g. is loaded and ticked in sync, actively
-+                // tick the initial persistent anger.
-+                // If not, let the first tick on the baseTick call the method later down the line.
-+                if (this instanceof Entity entity && !entity.firstTick) this.tickInitialPersistentAnger(world);
-             }
-         }
-     }
-@@ -114,7 +104,7 @@
-     default void stopBeingAngry() {
-         this.setLastHurtByMob((LivingEntity) null);
-         this.setPersistentAngerTarget((UUID) null);
--        this.setTarget((LivingEntity) null);
-+        this.setTarget((LivingEntity) null, org.bukkit.event.entity.EntityTargetEvent.TargetReason.FORGOT_TARGET, true); // CraftBukkit
-         this.setRemainingPersistentAngerTime(0);
-     }
- 
-@@ -127,8 +117,34 @@
- 
-     void setTarget(@Nullable LivingEntity target);
- 
-+    boolean setTarget(@Nullable LivingEntity entityliving, org.bukkit.event.entity.EntityTargetEvent.TargetReason reason, boolean fireEvent); // CraftBukkit
-+
-     boolean canAttack(LivingEntity target);
- 
-     @Nullable
-     LivingEntity getTarget();
-+
-+    // Paper start - Prevent entity loading causing async lookups
-+    // Update last hurt when ticking
-+    default void tickInitialPersistentAnger(Level level) {
-+        UUID target = getPersistentAngerTarget();
-+        if (target == null) {
-+            return;
-+        }
-+
-+        Entity entity = ((ServerLevel) level).getEntity(target);
-+
-+        if (entity != null) {
-+            if (entity instanceof Mob mob) {
-+                this.setTarget(mob, EntityTargetEvent.TargetReason.UNKNOWN, false); // CraftBukkit
-+                this.setLastHurtByMob(mob);
-+            }
-+
-+            if (entity instanceof Player player) {
-+                this.setTarget(player, EntityTargetEvent.TargetReason.UNKNOWN, false); // CraftBukkit
-+                this.setLastHurtByPlayer(player);
-+            }
-+        }
-+    }
-+    // Paper end - Prevent entity loading causing async lookups
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/PathfinderMob.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/PathfinderMob.java.patch
deleted file mode 100644
index 24a880fc47..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/PathfinderMob.java.patch
+++ /dev/null
@@ -1,12 +0,0 @@
---- a/net/minecraft/world/entity/PathfinderMob.java
-+++ b/net/minecraft/world/entity/PathfinderMob.java
-@@ -10,6 +10,9 @@
- import net.minecraft.world.level.LevelAccessor;
- import net.minecraft.world.level.LevelReader;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityUnleashEvent;
-+// CraftBukkit end
- 
- public abstract class PathfinderMob extends Mob {
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/TamableAnimal.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/TamableAnimal.java.patch
deleted file mode 100644
index ae8be7d5d0..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/TamableAnimal.java.patch
+++ /dev/null
@@ -1,85 +0,0 @@
---- a/net/minecraft/world/entity/TamableAnimal.java
-+++ b/net/minecraft/world/entity/TamableAnimal.java
-@@ -27,6 +27,11 @@
- import net.minecraft.world.level.pathfinder.PathType;
- import net.minecraft.world.level.pathfinder.WalkNodeEvaluator;
- import net.minecraft.world.scores.PlayerTeam;
-+// CraftBukkit start
-+import org.bukkit.Location;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.EntityTeleportEvent;
-+// CraftBukkit end
- 
- public abstract class TamableAnimal extends Animal implements OwnableEntity {
- 
-@@ -85,7 +90,7 @@
-         }
- 
-         this.orderedToSit = nbt.getBoolean("Sitting");
--        this.setInSittingPose(this.orderedToSit);
-+        this.setInSittingPose(this.orderedToSit, false); // Paper - Add EntityToggleSitEvent
-     }
- 
-     @Override
-@@ -96,8 +101,16 @@
-     @Override
-     public boolean handleLeashAtDistance(Entity leashHolder, float distance) {
-         if (this.isInSittingPose()) {
--            if (distance > 10.0F) {
--                this.dropLeash();
-+            if (distance > (float) this.level().paperConfig().misc.maxLeashDistance.or(Leashable.LEASH_TOO_FAR_DIST)) { // Paper - Configurable max leash distance
-+                // Paper start - Expand EntityUnleashEvent
-+                org.bukkit.event.entity.EntityUnleashEvent event = new org.bukkit.event.entity.EntityUnleashEvent(this.getBukkitEntity(), org.bukkit.event.entity.EntityUnleashEvent.UnleashReason.DISTANCE, true);
-+                if (!event.callEvent()) return false;
-+                if (event.isDropLeash()) {
-+                    this.dropLeash();
-+                } else {
-+                    this.removeLeash();
-+                }
-+                // Paper end - Expand EntityUnleashEvent
-             }
- 
-             return false;
-@@ -161,6 +174,12 @@
-     }
- 
-     public void setInSittingPose(boolean inSittingPose) {
-+        // Paper start - Add EntityToggleSitEvent
-+        this.setInSittingPose(inSittingPose, true);
-+    }
-+    public void setInSittingPose(boolean inSittingPose, boolean callEvent) {
-+        if (callEvent && !new io.papermc.paper.event.entity.EntityToggleSitEvent(this.getBukkitEntity(), inSittingPose).callEvent()) return;
-+        // Paper end - Add EntityToggleSitEvent
-         byte b0 = (Byte) this.entityData.get(TamableAnimal.DATA_FLAGS_ID);
- 
-         if (inSittingPose) {
-@@ -244,7 +263,12 @@
-                 if (entityliving instanceof ServerPlayer) {
-                     ServerPlayer entityplayer = (ServerPlayer) entityliving;
- 
--                    entityplayer.sendSystemMessage(this.getCombatTracker().getDeathMessage());
-+                    // Paper start - Add TameableDeathMessageEvent
-+                    io.papermc.paper.event.entity.TameableDeathMessageEvent event = new io.papermc.paper.event.entity.TameableDeathMessageEvent((org.bukkit.entity.Tameable) getBukkitEntity(), io.papermc.paper.adventure.PaperAdventure.asAdventure(this.getCombatTracker().getDeathMessage()));
-+                    if (event.callEvent()) {
-+                        entityplayer.sendSystemMessage(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.deathMessage()));
-+                    }
-+                    // Paper end - Add TameableDeathMessageEvent
-                 }
-             }
-         }
-@@ -295,7 +319,14 @@
-         if (!this.canTeleportTo(new BlockPos(x, y, z))) {
-             return false;
-         } else {
--            this.moveTo((double) x + 0.5D, (double) y, (double) z + 0.5D, this.getYRot(), this.getXRot());
-+            // CraftBukkit start
-+            EntityTeleportEvent event = CraftEventFactory.callEntityTeleportEvent(this, (double) x + 0.5D, (double) y, (double) z + 0.5D);
-+            if (event.isCancelled() || event.getTo() == null) { // Paper - prevent NP on null event to location
-+                return false;
-+            }
-+            Location to = event.getTo();
-+            this.moveTo(to.getX(), to.getY(), to.getZ(), to.getYaw(), to.getPitch());
-+            // CraftBukkit end
-             this.navigation.stop();
-             return true;
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/attributes/AttributeInstance.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/attributes/AttributeInstance.java.patch
deleted file mode 100644
index d6d3313220..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/attributes/AttributeInstance.java.patch
+++ /dev/null
@@ -1,27 +0,0 @@
---- a/net/minecraft/world/entity/ai/attributes/AttributeInstance.java
-+++ b/net/minecraft/world/entity/ai/attributes/AttributeInstance.java
-@@ -153,20 +153,20 @@
-         double d = this.getBaseValue();
- 
-         for (AttributeModifier attributeModifier : this.getModifiersOrEmpty(AttributeModifier.Operation.ADD_VALUE)) {
--            d += attributeModifier.amount();
-+            d += attributeModifier.amount(); // Paper - destroy speed API - diff on change
-         }
- 
-         double e = d;
- 
-         for (AttributeModifier attributeModifier2 : this.getModifiersOrEmpty(AttributeModifier.Operation.ADD_MULTIPLIED_BASE)) {
--            e += d * attributeModifier2.amount();
-+            e += d * attributeModifier2.amount(); // Paper - destroy speed API - diff on change
-         }
- 
-         for (AttributeModifier attributeModifier3 : this.getModifiersOrEmpty(AttributeModifier.Operation.ADD_MULTIPLIED_TOTAL)) {
--            e *= 1.0 + attributeModifier3.amount();
-+            e *= 1.0 + attributeModifier3.amount(); // Paper - destroy speed API - diff on change
-         }
- 
--        return this.attribute.value().sanitizeValue(e);
-+        return attribute.value().sanitizeValue(e); // Paper - destroy speed API - diff on change
-     }
- 
-     private Collection<AttributeModifier> getModifiersOrEmpty(AttributeModifier.Operation operation) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/attributes/Attributes.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/attributes/Attributes.java.patch
deleted file mode 100644
index 51565ac321..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/attributes/Attributes.java.patch
+++ /dev/null
@@ -1,31 +0,0 @@
---- a/net/minecraft/world/entity/ai/attributes/Attributes.java
-+++ b/net/minecraft/world/entity/ai/attributes/Attributes.java
-@@ -1,3 +1,4 @@
-+// mc-dev import
- package net.minecraft.world.entity.ai.attributes;
- 
- import net.minecraft.core.Holder;
-@@ -9,7 +10,7 @@
- 
-     public static final Holder<Attribute> ARMOR = Attributes.register("armor", (new RangedAttribute("attribute.name.armor", 0.0D, 0.0D, 30.0D)).setSyncable(true));
-     public static final Holder<Attribute> ARMOR_TOUGHNESS = Attributes.register("armor_toughness", (new RangedAttribute("attribute.name.armor_toughness", 0.0D, 0.0D, 20.0D)).setSyncable(true));
--    public static final Holder<Attribute> ATTACK_DAMAGE = Attributes.register("attack_damage", new RangedAttribute("attribute.name.attack_damage", 2.0D, 0.0D, 2048.0D));
-+    public static final Holder<Attribute> ATTACK_DAMAGE = Attributes.register("attack_damage", new RangedAttribute("attribute.name.attack_damage", 2.0D, 0.0D, org.spigotmc.SpigotConfig.attackDamage));
-     public static final Holder<Attribute> ATTACK_KNOCKBACK = Attributes.register("attack_knockback", new RangedAttribute("attribute.name.attack_knockback", 0.0D, 0.0D, 5.0D));
-     public static final Holder<Attribute> ATTACK_SPEED = Attributes.register("attack_speed", (new RangedAttribute("attribute.name.attack_speed", 4.0D, 0.0D, 1024.0D)).setSyncable(true));
-     public static final Holder<Attribute> BLOCK_BREAK_SPEED = Attributes.register("block_break_speed", (new RangedAttribute("attribute.name.block_break_speed", 1.0D, 0.0D, 1024.0D)).setSyncable(true));
-@@ -24,11 +25,11 @@
-     public static final Holder<Attribute> JUMP_STRENGTH = Attributes.register("jump_strength", (new RangedAttribute("attribute.name.jump_strength", 0.41999998688697815D, 0.0D, 32.0D)).setSyncable(true));
-     public static final Holder<Attribute> KNOCKBACK_RESISTANCE = Attributes.register("knockback_resistance", new RangedAttribute("attribute.name.knockback_resistance", 0.0D, 0.0D, 1.0D));
-     public static final Holder<Attribute> LUCK = Attributes.register("luck", (new RangedAttribute("attribute.name.luck", 0.0D, -1024.0D, 1024.0D)).setSyncable(true));
--    public static final Holder<Attribute> MAX_ABSORPTION = Attributes.register("max_absorption", (new RangedAttribute("attribute.name.max_absorption", 0.0D, 0.0D, 2048.0D)).setSyncable(true));
--    public static final Holder<Attribute> MAX_HEALTH = Attributes.register("max_health", (new RangedAttribute("attribute.name.max_health", 20.0D, 1.0D, 1024.0D)).setSyncable(true));
-+    public static final Holder<Attribute> MAX_ABSORPTION = Attributes.register("max_absorption", (new RangedAttribute("attribute.name.max_absorption", 0.0D, 0.0D, org.spigotmc.SpigotConfig.maxAbsorption)).setSyncable(true));
-+    public static final Holder<Attribute> MAX_HEALTH = Attributes.register("max_health", (new RangedAttribute("attribute.name.max_health", 20.0D, 1.0D, org.spigotmc.SpigotConfig.maxHealth)).setSyncable(true));
-     public static final Holder<Attribute> MINING_EFFICIENCY = Attributes.register("mining_efficiency", (new RangedAttribute("attribute.name.mining_efficiency", 0.0D, 0.0D, 1024.0D)).setSyncable(true));
-     public static final Holder<Attribute> MOVEMENT_EFFICIENCY = Attributes.register("movement_efficiency", (new RangedAttribute("attribute.name.movement_efficiency", 0.0D, 0.0D, 1.0D)).setSyncable(true));
--    public static final Holder<Attribute> MOVEMENT_SPEED = Attributes.register("movement_speed", (new RangedAttribute("attribute.name.movement_speed", 0.7D, 0.0D, 1024.0D)).setSyncable(true));
-+    public static final Holder<Attribute> MOVEMENT_SPEED = Attributes.register("movement_speed", (new RangedAttribute("attribute.name.movement_speed", 0.7D, 0.0D, org.spigotmc.SpigotConfig.movementSpeed)).setSyncable(true));
-     public static final Holder<Attribute> OXYGEN_BONUS = Attributes.register("oxygen_bonus", (new RangedAttribute("attribute.name.oxygen_bonus", 0.0D, 0.0D, 1024.0D)).setSyncable(true));
-     public static final Holder<Attribute> SAFE_FALL_DISTANCE = Attributes.register("safe_fall_distance", (new RangedAttribute("attribute.name.safe_fall_distance", 3.0D, -1024.0D, 1024.0D)).setSyncable(true));
-     public static final Holder<Attribute> SCALE = Attributes.register("scale", (new RangedAttribute("attribute.name.scale", 1.0D, 0.0625D, 16.0D)).setSyncable(true).setSentiment(Attribute.Sentiment.NEUTRAL));
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/AcquirePoi.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/AcquirePoi.java.patch
deleted file mode 100644
index 64bab57963..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/AcquirePoi.java.patch
+++ /dev/null
@@ -1,10 +0,0 @@
---- a/net/minecraft/world/entity/ai/behavior/AcquirePoi.java
-+++ b/net/minecraft/world/entity/ai/behavior/AcquirePoi.java
-@@ -70,6 +70,7 @@
-                                     return false;
-                                 } else {
-                                     mutableLong.setValue(time + 20L + (long)world.getRandom().nextInt(20));
-+                                    if (entity.getNavigation().isStuck()) mutableLong.add(200); // Paper - Perf: Wait an additional 10s to check again if they're stuck
-                                     PoiManager poiManager = world.getPoiManager();
-                                     long2ObjectMap.long2ObjectEntrySet().removeIf(entry -> !entry.getValue().isStillValid(time));
-                                     Predicate<BlockPos> predicate2 = pos -> {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/AssignProfessionFromJobSite.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/AssignProfessionFromJobSite.java.patch
deleted file mode 100644
index a3aec14777..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/AssignProfessionFromJobSite.java.patch
+++ /dev/null
@@ -1,31 +0,0 @@
---- a/net/minecraft/world/entity/ai/behavior/AssignProfessionFromJobSite.java
-+++ b/net/minecraft/world/entity/ai/behavior/AssignProfessionFromJobSite.java
-@@ -9,6 +9,12 @@
- import net.minecraft.world.entity.npc.Villager;
- import net.minecraft.world.entity.npc.VillagerProfession;
- 
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.entity.CraftVillager;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.VillagerCareerChangeEvent;
-+// CraftBukkit end
-+
- public class AssignProfessionFromJobSite {
- 
-     public AssignProfessionFromJobSite() {}
-@@ -37,7 +43,14 @@
-                                     return villagerprofession.heldJobSite().test(holder);
-                                 }).findFirst();
-                             }).ifPresent((villagerprofession) -> {
--                                entityvillager.setVillagerData(entityvillager.getVillagerData().setProfession(villagerprofession));
-+                                // CraftBukkit start - Fire VillagerCareerChangeEvent where Villager gets employed
-+                                VillagerCareerChangeEvent event = CraftEventFactory.callVillagerCareerChangeEvent(entityvillager, CraftVillager.CraftProfession.minecraftToBukkit(villagerprofession), VillagerCareerChangeEvent.ChangeReason.EMPLOYED);
-+                                if (event.isCancelled()) {
-+                                    return;
-+                                }
-+
-+                                entityvillager.setVillagerData(entityvillager.getVillagerData().setProfession(CraftVillager.CraftProfession.bukkitToMinecraft(event.getProfession())));
-+                                // CraftBukkit end
-                                 entityvillager.refreshBrain(worldserver);
-                             });
-                             return true;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/BabyFollowAdult.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/BabyFollowAdult.java.patch
deleted file mode 100644
index fe0de69d00..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/BabyFollowAdult.java.patch
+++ /dev/null
@@ -1,37 +0,0 @@
---- a/net/minecraft/world/entity/ai/behavior/BabyFollowAdult.java
-+++ b/net/minecraft/world/entity/ai/behavior/BabyFollowAdult.java
-@@ -7,6 +7,12 @@
- import net.minecraft.world.entity.ai.behavior.declarative.BehaviorBuilder;
- import net.minecraft.world.entity.ai.memory.MemoryModuleType;
- import net.minecraft.world.entity.ai.memory.WalkTarget;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.entity.CraftLivingEntity;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.EntityTargetEvent;
-+import org.bukkit.event.entity.EntityTargetLivingEntityEvent;
-+// CraftBukkit end
- 
- public class BabyFollowAdult {
- 
-@@ -25,9 +31,20 @@
-                     if (!entityageable.isBaby()) {
-                         return false;
-                     } else {
--                        AgeableMob entityageable1 = (AgeableMob) behaviorbuilder_b.get(memoryaccessor);
-+                        LivingEntity entityageable1 = (AgeableMob) behaviorbuilder_b.get(memoryaccessor); // CraftBukkit - type
- 
-                         if (entityageable.closerThan(entityageable1, (double) (executionRange.getMaxValue() + 1)) && !entityageable.closerThan(entityageable1, (double) executionRange.getMinValue())) {
-+                            // CraftBukkit start
-+                            EntityTargetLivingEntityEvent event = CraftEventFactory.callEntityTargetLivingEvent(entityageable, entityageable1, EntityTargetEvent.TargetReason.FOLLOW_LEADER);
-+                            if (event.isCancelled()) {
-+                                return false;
-+                            }
-+                            if (event.getTarget() == null) {
-+                                memoryaccessor.erase();
-+                                return true;
-+                            }
-+                            entityageable1 = ((CraftLivingEntity) event.getTarget()).getHandle();
-+                            // CraftBukkit end
-                             WalkTarget memorytarget = new WalkTarget(new EntityTracker(entityageable1, false), (Float) speed.apply(entityageable), executionRange.getMinValue() - 1);
- 
-                             memoryaccessor1.set(new EntityTracker(entityageable1, true));
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java.patch
deleted file mode 100644
index 35afe8f0a4..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java.patch
+++ /dev/null
@@ -1,64 +0,0 @@
---- a/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java
-+++ b/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java
-@@ -60,7 +60,7 @@
-     }
- 
-     public static void lookAtEntity(LivingEntity entity, LivingEntity target) {
--        entity.getBrain().setMemory(MemoryModuleType.LOOK_TARGET, (Object) (new EntityTracker(target, true)));
-+        entity.getBrain().setMemory(MemoryModuleType.LOOK_TARGET, (new EntityTracker(target, true))); // CraftBukkit - decompile error
-     }
- 
-     private static void setWalkAndLookTargetMemoriesToEachOther(LivingEntity first, LivingEntity second, float speed, int completionRange) {
-@@ -79,8 +79,8 @@
-     public static void setWalkAndLookTargetMemories(LivingEntity entity, PositionTracker target, float speed, int completionRange) {
-         WalkTarget memorytarget = new WalkTarget(target, speed, completionRange);
- 
--        entity.getBrain().setMemory(MemoryModuleType.LOOK_TARGET, (Object) target);
--        entity.getBrain().setMemory(MemoryModuleType.WALK_TARGET, (Object) memorytarget);
-+        entity.getBrain().setMemory(MemoryModuleType.LOOK_TARGET, target); // CraftBukkit - decompile error
-+        entity.getBrain().setMemory(MemoryModuleType.WALK_TARGET, memorytarget); // CraftBukkit - decompile error
-     }
- 
-     public static void throwItem(LivingEntity entity, ItemStack stack, Vec3 targetLocation) {
-@@ -90,6 +90,7 @@
-     }
- 
-     public static void throwItem(LivingEntity entity, ItemStack stack, Vec3 targetLocation, Vec3 velocityFactor, float yOffset) {
-+        if (stack.isEmpty()) return; // CraftBukkit - SPIGOT-4940: no empty loot
-         double d0 = entity.getEyeY() - (double) yOffset;
-         ItemEntity entityitem = new ItemEntity(entity.level(), entity.getX(), d0, entity.getZ(), stack);
- 
-@@ -99,12 +100,19 @@
-         vec3d2 = vec3d2.normalize().multiply(velocityFactor.x, velocityFactor.y, velocityFactor.z);
-         entityitem.setDeltaMovement(vec3d2);
-         entityitem.setDefaultPickUpDelay();
-+        // CraftBukkit start
-+        org.bukkit.event.entity.EntityDropItemEvent event = new org.bukkit.event.entity.EntityDropItemEvent(entity.getBukkitEntity(), (org.bukkit.entity.Item) entityitem.getBukkitEntity());
-+        entityitem.level().getCraftServer().getPluginManager().callEvent(event);
-+        if (event.isCancelled()) {
-+            return;
-+        }
-+        // CraftBukkit end
-         entity.level().addFreshEntity(entityitem);
-     }
- 
-     public static SectionPos findSectionClosestToVillage(ServerLevel world, SectionPos center, int radius) {
-         int j = world.sectionsToVillage(center);
--        Stream stream = SectionPos.cube(center, radius).filter((sectionposition1) -> {
-+        Stream<SectionPos> stream = SectionPos.cube(center, radius).filter((sectionposition1) -> { // CraftBukkit - decompile error
-             return world.sectionsToVillage(sectionposition1) < j;
-         });
- 
-@@ -161,10 +169,10 @@
- 
-         return optional.map((uuid) -> {
-             return ((ServerLevel) entity.level()).getEntity(uuid);
--        }).map((entity) -> {
-+        }).map((entity1) -> { // Paper - remap fix
-             LivingEntity entityliving1;
- 
--            if (entity instanceof LivingEntity entityliving2) {
-+            if (entity1 instanceof LivingEntity entityliving2) { // Paper - remap fix
-                 entityliving1 = entityliving2;
-             } else {
-                 entityliving1 = null;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/GoToWantedItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/GoToWantedItem.java.patch
deleted file mode 100644
index d9692605cb..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/GoToWantedItem.java.patch
+++ /dev/null
@@ -1,24 +0,0 @@
---- a/net/minecraft/world/entity/ai/behavior/GoToWantedItem.java
-+++ b/net/minecraft/world/entity/ai/behavior/GoToWantedItem.java
-@@ -28,6 +28,21 @@
-                     ItemEntity entityitem = (ItemEntity) behaviorbuilder_b.get(memoryaccessor2);
- 
-                     if (behaviorbuilder_b.tryGet(memoryaccessor3).isEmpty() && startCondition.test(entityliving) && entityitem.closerThan(entityliving, (double) radius) && entityliving.level().getWorldBorder().isWithinBounds(entityitem.blockPosition()) && entityliving.canPickUpLoot()) {
-+                        // CraftBukkit start
-+                        if (entityliving instanceof net.minecraft.world.entity.animal.allay.Allay) {
-+                            org.bukkit.event.entity.EntityTargetEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTargetEvent(entityliving, entityitem, org.bukkit.event.entity.EntityTargetEvent.TargetReason.CLOSEST_ENTITY);
-+
-+                            if (event.isCancelled()) {
-+                                return false;
-+                            }
-+                            if (!(event.getTarget() instanceof org.bukkit.craftbukkit.entity.CraftItem)) { // Paper - only erase allay memory on non-item targets
-+                                memoryaccessor2.erase();
-+                                return false; // Paper - only erase allay memory on non-item targets
-+                            }
-+
-+                            entityitem = (ItemEntity) ((org.bukkit.craftbukkit.entity.CraftEntity) event.getTarget()).getHandle();
-+                        }
-+                        // CraftBukkit end
-                         WalkTarget memorytarget = new WalkTarget(new EntityTracker(entityitem, false), speed, 0);
- 
-                         memoryaccessor.set(new EntityTracker(entityitem, true));
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/HarvestFarmland.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/HarvestFarmland.java.patch
deleted file mode 100644
index 066bfd1863..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/HarvestFarmland.java.patch
+++ /dev/null
@@ -1,63 +0,0 @@
---- a/net/minecraft/world/entity/ai/behavior/HarvestFarmland.java
-+++ b/net/minecraft/world/entity/ai/behavior/HarvestFarmland.java
-@@ -22,10 +22,15 @@
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.level.GameRules;
- import net.minecraft.world.level.block.Block;
-+import net.minecraft.world.level.gameevent.GameEvent;
-+
-+// CraftBukkit start
-+import net.minecraft.world.level.block.Blocks;
- import net.minecraft.world.level.block.CropBlock;
- import net.minecraft.world.level.block.FarmBlock;
- import net.minecraft.world.level.block.state.BlockState;
--import net.minecraft.world.level.gameevent.GameEvent;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+// CraftBukkit end
- 
- public class HarvestFarmland extends Behavior<Villager> {
- 
-@@ -82,8 +87,8 @@
- 
-     protected void start(ServerLevel worldserver, Villager entityvillager, long i) {
-         if (i > this.nextOkStartTime && this.aboveFarmlandPos != null) {
--            entityvillager.getBrain().setMemory(MemoryModuleType.LOOK_TARGET, (Object) (new BlockPosTracker(this.aboveFarmlandPos)));
--            entityvillager.getBrain().setMemory(MemoryModuleType.WALK_TARGET, (Object) (new WalkTarget(new BlockPosTracker(this.aboveFarmlandPos), 0.5F, 1)));
-+            entityvillager.getBrain().setMemory(MemoryModuleType.LOOK_TARGET, (new BlockPosTracker(this.aboveFarmlandPos))); // CraftBukkit - decompile error
-+            entityvillager.getBrain().setMemory(MemoryModuleType.WALK_TARGET, (new WalkTarget(new BlockPosTracker(this.aboveFarmlandPos), 0.5F, 1))); // CraftBukkit - decompile error
-         }
- 
-     }
-@@ -103,7 +108,9 @@
-                 Block block1 = world.getBlockState(this.aboveFarmlandPos.below()).getBlock();
- 
-                 if (block instanceof CropBlock && ((CropBlock) block).isMaxAge(iblockdata)) {
-+                    if (CraftEventFactory.callEntityChangeBlockEvent(entity, this.aboveFarmlandPos, iblockdata.getFluidState().createLegacyBlock())) { // CraftBukkit // Paper - fix wrong block state
-                     world.destroyBlock(this.aboveFarmlandPos, true, entity);
-+                    } // CraftBukkit
-                 }
- 
-                 if (iblockdata.isAir() && block1 instanceof FarmBlock && entity.hasFarmSeeds()) {
-@@ -120,9 +127,11 @@
-                                 BlockItem itemblock = (BlockItem) item;
-                                 BlockState iblockdata1 = itemblock.getBlock().defaultBlockState();
- 
-+                                if (CraftEventFactory.callEntityChangeBlockEvent(entity, this.aboveFarmlandPos, iblockdata1)) { // CraftBukkit
-                                 world.setBlockAndUpdate(this.aboveFarmlandPos, iblockdata1);
-                                 world.gameEvent((Holder) GameEvent.BLOCK_PLACE, this.aboveFarmlandPos, GameEvent.Context.of(entity, iblockdata1));
-                                 flag = true;
-+                                } // CraftBukkit
-                             }
-                         }
- 
-@@ -142,8 +151,8 @@
-                     this.aboveFarmlandPos = this.getValidFarmland(world);
-                     if (this.aboveFarmlandPos != null) {
-                         this.nextOkStartTime = time + 20L;
--                        entity.getBrain().setMemory(MemoryModuleType.WALK_TARGET, (Object) (new WalkTarget(new BlockPosTracker(this.aboveFarmlandPos), 0.5F, 1)));
--                        entity.getBrain().setMemory(MemoryModuleType.LOOK_TARGET, (Object) (new BlockPosTracker(this.aboveFarmlandPos)));
-+                        entity.getBrain().setMemory(MemoryModuleType.WALK_TARGET, (new WalkTarget(new BlockPosTracker(this.aboveFarmlandPos), 0.5F, 1))); // CraftBukkit - decompile error
-+                        entity.getBrain().setMemory(MemoryModuleType.LOOK_TARGET, (new BlockPosTracker(this.aboveFarmlandPos))); // CraftBukkit - decompile error
-                     }
-                 }
-             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/InteractWithDoor.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/InteractWithDoor.java.patch
deleted file mode 100644
index 7348471be5..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/InteractWithDoor.java.patch
+++ /dev/null
@@ -1,39 +0,0 @@
---- a/net/minecraft/world/entity/ai/behavior/InteractWithDoor.java
-+++ b/net/minecraft/world/entity/ai/behavior/InteractWithDoor.java
-@@ -61,6 +61,13 @@
-                             DoorBlock blockdoor = (DoorBlock) iblockdata.getBlock();
- 
-                             if (!blockdoor.isOpen(iblockdata)) {
-+                                // CraftBukkit start - entities opening doors
-+                                org.bukkit.event.entity.EntityInteractEvent event = new org.bukkit.event.entity.EntityInteractEvent(entityliving.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(entityliving.level(), blockposition));
-+                                entityliving.level().getCraftServer().getPluginManager().callEvent(event);
-+                                if (event.isCancelled()) {
-+                                    return false;
-+                                }
-+                                // CraftBukkit end
-                                 blockdoor.setOpen(entityliving, worldserver, iblockdata, blockposition, true);
-                             }
- 
-@@ -76,6 +83,13 @@
-                             DoorBlock blockdoor1 = (DoorBlock) iblockdata1.getBlock();
- 
-                             if (!blockdoor1.isOpen(iblockdata1)) {
-+                                // CraftBukkit start - entities opening doors
-+                                org.bukkit.event.entity.EntityInteractEvent event = new org.bukkit.event.entity.EntityInteractEvent(entityliving.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(entityliving.level(), blockposition1));
-+                                entityliving.level().getCraftServer().getPluginManager().callEvent(event);
-+                                if (event.isCancelled()) {
-+                                    return false;
-+                                }
-+                                // CraftBukkit end
-                                 blockdoor1.setOpen(entityliving, worldserver, iblockdata1, blockposition1, true);
-                                 optional = InteractWithDoor.rememberDoorToClose(memoryaccessor1, optional, worldserver, blockposition1);
-                             }
-@@ -129,7 +143,7 @@
-     }
- 
-     private static boolean areOtherMobsComingThroughDoor(LivingEntity entity, BlockPos pos, Optional<List<LivingEntity>> otherMobs) {
--        return otherMobs.isEmpty() ? false : ((List) otherMobs.get()).stream().filter((entityliving1) -> {
-+        return otherMobs.isEmpty() ? false : (otherMobs.get()).stream().filter((entityliving1) -> { // CraftBukkit - decompile error
-             return entityliving1.getType() == entity.getType();
-         }).filter((entityliving1) -> {
-             return pos.closerToCenterThan(entityliving1.position(), 2.0D);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/PrepareRamNearestTarget.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/PrepareRamNearestTarget.java.patch
deleted file mode 100644
index 18aef70f25..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/PrepareRamNearestTarget.java.patch
+++ /dev/null
@@ -1,73 +0,0 @@
---- a/net/minecraft/world/entity/ai/behavior/PrepareRamNearestTarget.java
-+++ b/net/minecraft/world/entity/ai/behavior/PrepareRamNearestTarget.java
-@@ -13,6 +13,7 @@
- import net.minecraft.core.BlockPos;
- import net.minecraft.core.Direction;
- import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
- import net.minecraft.sounds.SoundEvent;
- import net.minecraft.sounds.SoundSource;
- import net.minecraft.util.Mth;
-@@ -30,6 +31,10 @@
- import net.minecraft.world.level.pathfinder.Path;
- import net.minecraft.world.level.pathfinder.WalkNodeEvaluator;
- import net.minecraft.world.phys.Vec3;
-+import org.bukkit.craftbukkit.entity.CraftLivingEntity;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.EntityTargetEvent;
-+// CraftBukkit end
- 
- public class PrepareRamNearestTarget<E extends PathfinderMob> extends Behavior<E> {
- 
-@@ -63,6 +68,13 @@
-                 return this.ramTargeting.test(worldserver, entitycreature, entityliving);
-             });
-         }).ifPresent((entityliving) -> {
-+            // CraftBukkit start
-+            EntityTargetEvent event = CraftEventFactory.callEntityTargetLivingEvent(entitycreature, entityliving, (entityliving instanceof ServerPlayer) ? EntityTargetEvent.TargetReason.CLOSEST_PLAYER : EntityTargetEvent.TargetReason.CLOSEST_ENTITY);
-+            if (event.isCancelled() || event.getTarget() == null) {
-+                return;
-+            }
-+            entityliving = ((CraftLivingEntity) event.getTarget()).getHandle();
-+            // CraftBukkit end
-             this.chooseRamPosition(entitycreature, entityliving);
-         });
-     }
-@@ -72,7 +84,7 @@
- 
-         if (!behaviorcontroller.hasMemoryValue(MemoryModuleType.RAM_TARGET)) {
-             world.broadcastEntityEvent(entity, (byte) 59);
--            behaviorcontroller.setMemory(MemoryModuleType.RAM_COOLDOWN_TICKS, (Object) this.getCooldownOnFail.applyAsInt(entity));
-+            behaviorcontroller.setMemory(MemoryModuleType.RAM_COOLDOWN_TICKS, this.getCooldownOnFail.applyAsInt(entity)); // CraftBukkit - decompile error
-         }
- 
-     }
-@@ -83,8 +95,8 @@
- 
-     protected void tick(ServerLevel worldserver, E e0, long i) {
-         if (!this.ramCandidate.isEmpty()) {
--            e0.getBrain().setMemory(MemoryModuleType.WALK_TARGET, (Object) (new WalkTarget(((PrepareRamNearestTarget.RamCandidate) this.ramCandidate.get()).getStartPosition(), this.walkSpeed, 0)));
--            e0.getBrain().setMemory(MemoryModuleType.LOOK_TARGET, (Object) (new EntityTracker(((PrepareRamNearestTarget.RamCandidate) this.ramCandidate.get()).getTarget(), true)));
-+            e0.getBrain().setMemory(MemoryModuleType.WALK_TARGET, (new WalkTarget(((PrepareRamNearestTarget.RamCandidate) this.ramCandidate.get()).getStartPosition(), this.walkSpeed, 0))); // CraftBukkit - decompile error
-+            e0.getBrain().setMemory(MemoryModuleType.LOOK_TARGET, (new EntityTracker(((PrepareRamNearestTarget.RamCandidate) this.ramCandidate.get()).getTarget(), true))); // CraftBukkit - decompile error
-             boolean flag = !((PrepareRamNearestTarget.RamCandidate) this.ramCandidate.get()).getTarget().blockPosition().equals(((PrepareRamNearestTarget.RamCandidate) this.ramCandidate.get()).getTargetPosition());
- 
-             if (flag) {
-@@ -101,7 +113,7 @@
-                     }
- 
-                     if (i - (Long) this.reachedRamPositionTimestamp.get() >= (long) this.ramPrepareTime) {
--                        e0.getBrain().setMemory(MemoryModuleType.RAM_TARGET, (Object) this.getEdgeOfBlock(blockposition, ((PrepareRamNearestTarget.RamCandidate) this.ramCandidate.get()).getTargetPosition()));
-+                        e0.getBrain().setMemory(MemoryModuleType.RAM_TARGET, this.getEdgeOfBlock(blockposition, ((PrepareRamNearestTarget.RamCandidate) this.ramCandidate.get()).getTargetPosition())); // CraftBukkit - decompile error
-                         worldserver.playSound((Player) null, (Entity) e0, (SoundEvent) this.getPrepareRamSound.apply(e0), SoundSource.NEUTRAL, 1.0F, e0.getVoicePitch());
-                         this.ramCandidate = Optional.empty();
-                     }
-@@ -153,7 +165,7 @@
-             }
- 
-             PathNavigation navigationabstract = entity.getNavigation();
--            Stream stream = list.stream();
-+            Stream<BlockPos> stream = list.stream(); // CraftBukkit - decompile error
-             BlockPos blockposition1 = entity.blockPosition();
- 
-             Objects.requireNonNull(blockposition1);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/RamTarget.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/RamTarget.java.patch
deleted file mode 100644
index 3c23484d4f..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/RamTarget.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/entity/ai/behavior/RamTarget.java
-+++ b/net/minecraft/world/entity/ai/behavior/RamTarget.java
-@@ -89,7 +89,7 @@
-             float f = 0.25F * (float)(i - j);
-             float g = Mth.clamp(entity.getSpeed() * 1.65F, 0.2F, 3.0F) + f;
-             float h = livingEntity.isDamageSourceBlocked(world.damageSources().mobAttack(entity)) ? 0.5F : 1.0F;
--            livingEntity.knockback((double)(h * g) * this.getKnockbackForce.applyAsDouble(entity), this.ramDirection.x(), this.ramDirection.z());
-+            livingEntity.knockback(h * g * this.getKnockbackForce.applyAsDouble(entity), this.ramDirection.x(), this.ramDirection.z(), entity, io.papermc.paper.event.entity.EntityKnockbackEvent.Cause.ENTITY_ATTACK); // Paper - Add EntityKnockbackByEntityEvent and EntityPushedByEntityAttackEvent
-             this.finishRam(world, entity);
-             world.playSound(null, entity, this.getImpactSound.apply(entity), SoundSource.NEUTRAL, 1.0F, 1.0F);
-         } else if (this.hasRammedHornBreakingBlock(world, entity)) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/ResetProfession.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/ResetProfession.java.patch
deleted file mode 100644
index f5dc7ba366..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/ResetProfession.java.patch
+++ /dev/null
@@ -1,31 +0,0 @@
---- a/net/minecraft/world/entity/ai/behavior/ResetProfession.java
-+++ b/net/minecraft/world/entity/ai/behavior/ResetProfession.java
-@@ -6,6 +6,12 @@
- import net.minecraft.world.entity.npc.VillagerData;
- import net.minecraft.world.entity.npc.VillagerProfession;
- 
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.entity.CraftVillager;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.VillagerCareerChangeEvent;
-+// CraftBukkit end
-+
- public class ResetProfession {
- 
-     public ResetProfession() {}
-@@ -17,7 +23,14 @@
-                     VillagerData villagerdata = entityvillager.getVillagerData();
- 
-                     if (villagerdata.getProfession() != VillagerProfession.NONE && villagerdata.getProfession() != VillagerProfession.NITWIT && entityvillager.getVillagerXp() == 0 && villagerdata.getLevel() <= 1) {
--                        entityvillager.setVillagerData(entityvillager.getVillagerData().setProfession(VillagerProfession.NONE));
-+                        // CraftBukkit start
-+                        VillagerCareerChangeEvent event = CraftEventFactory.callVillagerCareerChangeEvent(entityvillager, CraftVillager.CraftProfession.minecraftToBukkit(VillagerProfession.NONE), VillagerCareerChangeEvent.ChangeReason.LOSING_JOB);
-+                        if (event.isCancelled()) {
-+                            return false;
-+                        }
-+
-+                        entityvillager.setVillagerData(entityvillager.getVillagerData().setProfession(CraftVillager.CraftProfession.bukkitToMinecraft(event.getProfession())));
-+                        // CraftBukkit end
-                         entityvillager.refreshBrain(worldserver);
-                         return true;
-                     } else {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/StartAttacking.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/StartAttacking.java.patch
deleted file mode 100644
index 0a02e4eb2c..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/StartAttacking.java.patch
+++ /dev/null
@@ -1,36 +0,0 @@
---- a/net/minecraft/world/entity/ai/behavior/StartAttacking.java
-+++ b/net/minecraft/world/entity/ai/behavior/StartAttacking.java
-@@ -2,10 +2,15 @@
- 
- import java.util.Optional;
- import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
- import net.minecraft.world.entity.LivingEntity;
- import net.minecraft.world.entity.Mob;
- import net.minecraft.world.entity.ai.behavior.declarative.BehaviorBuilder;
- import net.minecraft.world.entity.ai.memory.MemoryModuleType;
-+import org.bukkit.craftbukkit.entity.CraftLivingEntity;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.EntityTargetEvent;
-+// CraftBukkit end
- 
- public class StartAttacking {
- 
-@@ -34,6 +39,17 @@
-                             if (!entityinsentient.canAttack(entityliving)) {
-                                 return false;
-                             } else {
-+                                // CraftBukkit start
-+                                EntityTargetEvent event = CraftEventFactory.callEntityTargetLivingEvent(entityinsentient, entityliving, (entityliving instanceof ServerPlayer) ? EntityTargetEvent.TargetReason.CLOSEST_PLAYER : EntityTargetEvent.TargetReason.CLOSEST_ENTITY);
-+                                if (event.isCancelled()) {
-+                                    return false;
-+                                }
-+                                if (event.getTarget() == null) {
-+                                    memoryaccessor.erase();
-+                                    return true;
-+                                }
-+                                entityliving = ((CraftLivingEntity) event.getTarget()).getHandle();
-+                                // CraftBukkit end
-                                 memoryaccessor.set(entityliving);
-                                 memoryaccessor1.erase();
-                                 return true;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/StopAttackingIfTargetInvalid.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/StopAttackingIfTargetInvalid.java.patch
deleted file mode 100644
index c156a35921..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/StopAttackingIfTargetInvalid.java.patch
+++ /dev/null
@@ -1,46 +0,0 @@
---- a/net/minecraft/world/entity/ai/behavior/StopAttackingIfTargetInvalid.java
-+++ b/net/minecraft/world/entity/ai/behavior/StopAttackingIfTargetInvalid.java
-@@ -7,6 +7,12 @@
- import net.minecraft.world.entity.ai.behavior.declarative.BehaviorBuilder;
- import net.minecraft.world.entity.ai.memory.MemoryModuleType;
- 
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.entity.CraftLivingEntity;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.EntityTargetEvent;
-+// CraftBukkit end
-+
- public class StopAttackingIfTargetInvalid {
- 
-     private static final int TIMEOUT_TO_GET_WITHIN_ATTACK_RANGE = 200;
-@@ -40,6 +46,30 @@
-                     if (entityinsentient.canAttack(entityliving) && (!shouldForgetIfTargetUnreachable || !StopAttackingIfTargetInvalid.isTiredOfTryingToReachTarget(entityinsentient, behaviorbuilder_b.tryGet(memoryaccessor1))) && entityliving.isAlive() && entityliving.level() == entityinsentient.level() && !condition.test(worldserver, entityliving)) {
-                         return true;
-                     } else {
-+                        // Paper start - better track target change reason
-+                        final EntityTargetEvent.TargetReason reason;
-+                        if (!entityinsentient.canAttack(entityliving)) {
-+                            reason = EntityTargetEvent.TargetReason.TARGET_INVALID;
-+                        } else if (shouldForgetIfTargetUnreachable && StopAttackingIfTargetInvalid.isTiredOfTryingToReachTarget(entityinsentient, behaviorbuilder_b.tryGet(memoryaccessor1))) {
-+                            reason = EntityTargetEvent.TargetReason.FORGOT_TARGET;
-+                        } else if (!entityliving.isAlive()) {
-+                            reason = EntityTargetEvent.TargetReason.TARGET_DIED;
-+                        } else if (entityliving.level() != entityinsentient.level()) {
-+                            reason = EntityTargetEvent.TargetReason.TARGET_OTHER_LEVEL;
-+                        } else {
-+                            reason = EntityTargetEvent.TargetReason.TARGET_INVALID;
-+                        }
-+                        // Paper end
-+                        // CraftBukkit start
-+                        EntityTargetEvent event = CraftEventFactory.callEntityTargetLivingEvent(entityinsentient, null, reason); // Paper
-+                        if (event.isCancelled()) {
-+                            return false;
-+                        }
-+                        if (event.getTarget() != null) {
-+                            entityinsentient.getBrain().setMemory(MemoryModuleType.ATTACK_TARGET, ((CraftLivingEntity) event.getTarget()).getHandle());
-+                            return true;
-+                        }
-+                        // CraftBukkit end
-                         callback.accept(worldserver, entityinsentient, entityliving);
-                         memoryaccessor.erase();
-                         return true;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/TryLaySpawnOnWaterNearLand.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/TryLaySpawnOnWaterNearLand.java.patch
deleted file mode 100644
index e0fa77caef..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/TryLaySpawnOnWaterNearLand.java.patch
+++ /dev/null
@@ -1,15 +0,0 @@
---- a/net/minecraft/world/entity/ai/behavior/TryLaySpawnOnWaterNearLand.java
-+++ b/net/minecraft/world/entity/ai/behavior/TryLaySpawnOnWaterNearLand.java
-@@ -39,6 +39,12 @@
-                                 if (worldserver.getBlockState(blockposition2).isAir()) {
-                                     BlockState iblockdata = frogSpawn.defaultBlockState();
- 
-+                                    // CraftBukkit start
-+                                    if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(entityliving, blockposition2, iblockdata)) {
-+                                        memoryaccessor2.erase();
-+                                        return true;
-+                                    }
-+                                    // CraftBukkit end
-                                     worldserver.setBlock(blockposition2, iblockdata, 3);
-                                     worldserver.gameEvent((Holder) GameEvent.BLOCK_PLACE, blockposition2, GameEvent.Context.of(entityliving, iblockdata));
-                                     worldserver.playSound((Player) null, (Entity) entityliving, SoundEvents.FROG_LAY_SPAWN, SoundSource.BLOCKS, 1.0F, 1.0F);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/VillagerMakeLove.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/VillagerMakeLove.java.patch
deleted file mode 100644
index 9875c853b9..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/VillagerMakeLove.java.patch
+++ /dev/null
@@ -1,42 +0,0 @@
---- a/net/minecraft/world/entity/ai/behavior/VillagerMakeLove.java
-+++ b/net/minecraft/world/entity/ai/behavior/VillagerMakeLove.java
-@@ -17,6 +17,10 @@
- import net.minecraft.world.entity.ai.village.poi.PoiTypes;
- import net.minecraft.world.entity.npc.Villager;
- import net.minecraft.world.level.pathfinder.Path;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.CreatureSpawnEvent;
-+// CraftBukkit end
- 
- public class VillagerMakeLove extends Behavior<Villager> {
- 
-@@ -114,11 +118,17 @@
-         if (entityvillager2 == null) {
-             return Optional.empty();
-         } else {
--            parent.setAge(6000);
--            partner.setAge(6000);
-             entityvillager2.setAge(-24000);
-             entityvillager2.moveTo(parent.getX(), parent.getY(), parent.getZ(), 0.0F, 0.0F);
--            world.addFreshEntityWithPassengers(entityvillager2);
-+            // CraftBukkit start - call EntityBreedEvent
-+            if (CraftEventFactory.callEntityBreedEvent(entityvillager2, parent, partner, null, null, 0).isCancelled()) {
-+                return Optional.empty();
-+            }
-+            // Move age setting down
-+            parent.setAge(6000);
-+            partner.setAge(6000);
-+            world.addFreshEntityWithPassengers(entityvillager2, CreatureSpawnEvent.SpawnReason.BREEDING);
-+            // CraftBukkit end
-             world.broadcastEntityEvent(entityvillager2, (byte) 12);
-             return Optional.of(entityvillager2);
-         }
-@@ -127,6 +137,6 @@
-     private void giveBedToChild(ServerLevel world, Villager child, BlockPos pos) {
-         GlobalPos globalpos = GlobalPos.of(world.dimension(), pos);
- 
--        child.getBrain().setMemory(MemoryModuleType.HOME, (Object) globalpos);
-+        child.getBrain().setMemory(MemoryModuleType.HOME, globalpos); // CraftBukkit - decompile error
-     }
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/warden/Digging.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/warden/Digging.java.patch
deleted file mode 100644
index 6f3b4a357c..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/warden/Digging.java.patch
+++ /dev/null
@@ -1,22 +0,0 @@
---- a/net/minecraft/world/entity/ai/behavior/warden/Digging.java
-+++ b/net/minecraft/world/entity/ai/behavior/warden/Digging.java
-@@ -10,6 +10,10 @@
- import net.minecraft.world.entity.ai.memory.MemoryStatus;
- import net.minecraft.world.entity.monster.warden.Warden;
- 
-+// CraftBukkit start - imports
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
-+
- public class Digging<E extends Warden> extends Behavior<E> {
- 
-     public Digging(int duration) {
-@@ -37,7 +41,7 @@
- 
-     protected void stop(ServerLevel worldserver, E e0, long i) {
-         if (e0.getRemovalReason() == null) {
--            e0.remove(Entity.RemovalReason.DISCARDED);
-+            e0.remove(Entity.RemovalReason.DISCARDED, EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - Add bukkit remove cause
-         }
- 
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/warden/SonicBoom.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/warden/SonicBoom.java.patch
deleted file mode 100644
index 4fbd06f256..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/warden/SonicBoom.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/entity/ai/behavior/warden/SonicBoom.java
-+++ b/net/minecraft/world/entity/ai/behavior/warden/SonicBoom.java
-@@ -83,7 +83,7 @@
-                     if (target.hurtServer(world, world.damageSources().sonicBoom(entity), 10.0F)) {
-                         double d = 0.5 * (1.0 - target.getAttributeValue(Attributes.KNOCKBACK_RESISTANCE));
-                         double e = 2.5 * (1.0 - target.getAttributeValue(Attributes.KNOCKBACK_RESISTANCE));
--                        target.push(vec33.x() * e, vec33.y() * d, vec33.z() * e);
-+                        target.push(vec33.x() * e, vec33.y() * d, vec33.z() * e, entity); // Paper - Add EntityKnockbackByEntityEvent and EntityPushedByEntityAttackEvent
-                     }
-                 });
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/EatBlockGoal.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/EatBlockGoal.java.patch
deleted file mode 100644
index 72de1e0703..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/EatBlockGoal.java.patch
+++ /dev/null
@@ -1,46 +0,0 @@
---- a/net/minecraft/world/entity/ai/goal/EatBlockGoal.java
-+++ b/net/minecraft/world/entity/ai/goal/EatBlockGoal.java
-@@ -11,6 +11,10 @@
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.block.state.predicate.BlockStatePredicate;
- 
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+// CraftBukkit end
-+
- public class EatBlockGoal extends Goal {
- 
-     private static final int EAT_ANIMATION_TICKS = 40;
-@@ -27,6 +31,11 @@
- 
-     @Override
-     public boolean canUse() {
-+        // Paper start - Fix MC-210802
-+        if (!((net.minecraft.server.level.ServerLevel) this.level).chunkSource.chunkMap.anyPlayerCloseEnoughForSpawning(this.mob.chunkPosition())) {
-+            return false;
-+        }
-+        // Paper end
-         if (this.mob.getRandom().nextInt(this.mob.isBaby() ? 50 : 1000) != 0) {
-             return false;
-         } else {
-@@ -63,8 +72,9 @@
-         if (this.eatAnimationTick == this.adjustedTickDelay(4)) {
-             BlockPos blockposition = this.mob.blockPosition();
- 
--            if (EatBlockGoal.IS_TALL_GRASS.test(this.level.getBlockState(blockposition))) {
--                if (getServerLevel(this.level).getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) {
-+            final BlockState blockState = this.level.getBlockState(blockposition); // Paper - fix wrong block state
-+            if (EatBlockGoal.IS_TALL_GRASS.test(blockState)) { // Paper - fix wrong block state
-+                if (CraftEventFactory.callEntityChangeBlockEvent(this.mob, blockposition, blockState.getFluidState().createLegacyBlock(), !getServerLevel(this.level).getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING))) { // CraftBukkit // Paper - fix wrong block state
-                     this.level.destroyBlock(blockposition, false);
-                 }
- 
-@@ -73,7 +83,7 @@
-                 BlockPos blockposition1 = blockposition.below();
- 
-                 if (this.level.getBlockState(blockposition1).is(Blocks.GRASS_BLOCK)) {
--                    if (getServerLevel(this.level).getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) {
-+                    if (CraftEventFactory.callEntityChangeBlockEvent(this.mob, blockposition1, Blocks.DIRT.defaultBlockState(), !getServerLevel(this.level).getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING))) { // CraftBukkit // Paper - Fix wrong block state
-                         this.level.levelEvent(2001, blockposition1, Block.getId(Blocks.GRASS_BLOCK.defaultBlockState()));
-                         this.level.setBlock(blockposition1, Blocks.DIRT.defaultBlockState(), 2);
-                     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java.patch
deleted file mode 100644
index 2111952674..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java.patch
+++ /dev/null
@@ -1,55 +0,0 @@
---- a/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java
-+++ b/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java
-@@ -21,6 +21,10 @@
- import net.minecraft.world.level.chunk.ChunkAccess;
- import net.minecraft.world.level.chunk.status.ChunkStatus;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+// CraftBukkit end
- 
- public class RemoveBlockGoal extends MoveToBlockGoal {
- 
-@@ -97,6 +101,11 @@
-             }
- 
-             if (this.ticksSinceReachedGoal > 60) {
-+                // CraftBukkit start - Step on eggs
-+                if (!CraftEventFactory.callEntityInteractEvent(this.removerMob, CraftBlock.at(world, blockposition1))) {
-+                    return;
-+                }
-+                // CraftBukkit end
-                 world.removeBlock(blockposition1, false);
-                 if (!world.isClientSide) {
-                     for (int i = 0; i < 20; ++i) {
-@@ -118,7 +127,9 @@
- 
-     @Nullable
-     private BlockPos getPosWithBlock(BlockPos pos, BlockGetter world) {
--        if (world.getBlockState(pos).is(this.blockToRemove)) {
-+        net.minecraft.world.level.block.state.BlockState block = world.getBlockStateIfLoaded(pos); // Paper - Prevent AI rules from loading chunks
-+        if (block == null) return null; // Paper - Prevent AI rules from loading chunks
-+        if (block.is(this.blockToRemove)) { // Paper - Prevent AI rules from loading chunks
-             return pos;
-         } else {
-             BlockPos[] ablockposition = new BlockPos[]{pos.below(), pos.west(), pos.east(), pos.north(), pos.south(), pos.below().below()};
-@@ -128,7 +139,8 @@
-             for (int j = 0; j < i; ++j) {
-                 BlockPos blockposition1 = ablockposition1[j];
- 
--                if (world.getBlockState(blockposition1).is(this.blockToRemove)) {
-+                net.minecraft.world.level.block.state.BlockState block2 = world.getBlockStateIfLoaded(blockposition1); // Paper - Prevent AI rules from loading chunks
-+                if (block2 != null && block2.is(this.blockToRemove)) { // Paper - Prevent AI rules from loading chunks
-                     return blockposition1;
-                 }
-             }
-@@ -139,7 +151,7 @@
- 
-     @Override
-     protected boolean isValidTarget(LevelReader world, BlockPos pos) {
--        ChunkAccess ichunkaccess = world.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ()), ChunkStatus.FULL, false);
-+        ChunkAccess ichunkaccess = world.getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4); // Paper - Prevent AI rules from loading chunks
- 
-         return ichunkaccess == null ? false : ichunkaccess.getBlockState(pos).is(this.blockToRemove) && ichunkaccess.getBlockState(pos.above()).isAir() && ichunkaccess.getBlockState(pos.above(2)).isAir();
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/RunAroundLikeCrazyGoal.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/RunAroundLikeCrazyGoal.java.patch
deleted file mode 100644
index 4f41b508ea..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/RunAroundLikeCrazyGoal.java.patch
+++ /dev/null
@@ -1,22 +0,0 @@
---- a/net/minecraft/world/entity/ai/goal/RunAroundLikeCrazyGoal.java
-+++ b/net/minecraft/world/entity/ai/goal/RunAroundLikeCrazyGoal.java
-@@ -6,6 +6,10 @@
- import net.minecraft.world.entity.animal.horse.AbstractHorse;
- import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+// CraftBukkit end
- 
- public class RunAroundLikeCrazyGoal extends Goal {
- 
-@@ -63,7 +67,7 @@
-                 int i = this.horse.getTemper();
-                 int j = this.horse.getMaxTemper();
- 
--                if (j > 0 && this.horse.getRandom().nextInt(j) < i) {
-+                if (j > 0 && this.horse.getRandom().nextInt(j) < i && !CraftEventFactory.callEntityTameEvent(this.horse, ((CraftHumanEntity) this.horse.getBukkitEntity().getPassenger()).getHandle()).isCancelled()) { // CraftBukkit - fire EntityTameEvent
-                     this.horse.tameWithName(entityhuman);
-                     return;
-                 }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/TemptGoal.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/TemptGoal.java.patch
deleted file mode 100644
index 8d33e61ef6..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/TemptGoal.java.patch
+++ /dev/null
@@ -1,44 +0,0 @@
---- a/net/minecraft/world/entity/ai/goal/TemptGoal.java
-+++ b/net/minecraft/world/entity/ai/goal/TemptGoal.java
-@@ -8,9 +8,15 @@
- import net.minecraft.world.entity.PathfinderMob;
- import net.minecraft.world.entity.ai.attributes.Attributes;
- import net.minecraft.world.entity.ai.targeting.TargetingConditions;
--import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.item.ItemStack;
- 
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.entity.CraftLivingEntity;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.EntityTargetEvent;
-+import org.bukkit.event.entity.EntityTargetLivingEntityEvent;
-+// CraftBukkit end
-+
- public class TemptGoal extends Goal {
- 
-     private static final TargetingConditions TEMPT_TARGETING = TargetingConditions.forNonCombat().ignoreLineOfSight();
-@@ -23,7 +29,7 @@
-     private double pRotX;
-     private double pRotY;
-     @Nullable
--    protected Player player;
-+    protected LivingEntity player; // CraftBukkit
-     private int calmDown;
-     private boolean isRunning;
-     private final Predicate<ItemStack> items;
-@@ -47,6 +53,15 @@
-             return false;
-         } else {
-             this.player = getServerLevel((Entity) this.mob).getNearestPlayer(this.targetingConditions.range(this.mob.getAttributeValue(Attributes.TEMPT_RANGE)), this.mob);
-+            // CraftBukkit start
-+            if (this.player != null) {
-+                EntityTargetLivingEntityEvent event = CraftEventFactory.callEntityTargetLivingEvent(this.mob, this.player, EntityTargetEvent.TargetReason.TEMPT);
-+                if (event.isCancelled()) {
-+                    return false;
-+                }
-+                this.player = (event.getTarget() == null) ? null : ((CraftLivingEntity) event.getTarget()).getHandle();
-+            }
-+            // CraftBukkit end
-             return this.player != null;
-         }
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/TargetGoal.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/TargetGoal.java.patch
deleted file mode 100644
index ab51c6b369..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/TargetGoal.java.patch
+++ /dev/null
@@ -1,30 +0,0 @@
---- a/net/minecraft/world/entity/ai/goal/target/TargetGoal.java
-+++ b/net/minecraft/world/entity/ai/goal/target/TargetGoal.java
-@@ -10,6 +10,9 @@
- import net.minecraft.world.level.pathfinder.Node;
- import net.minecraft.world.level.pathfinder.Path;
- import net.minecraft.world.scores.PlayerTeam;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityTargetEvent;
-+// CraftBukkit end
- 
- public abstract class TargetGoal extends Goal {
- 
-@@ -69,7 +72,7 @@
-                         }
-                     }
- 
--                    this.mob.setTarget(entityliving);
-+                    this.mob.setTarget(entityliving, EntityTargetEvent.TargetReason.CLOSEST_ENTITY, true); // CraftBukkit
-                     return true;
-                 }
-             }
-@@ -89,7 +92,7 @@
- 
-     @Override
-     public void stop() {
--        this.mob.setTarget((LivingEntity) null);
-+        this.mob.setTarget((LivingEntity) null, EntityTargetEvent.TargetReason.FORGOT_TARGET, true); // CraftBukkit
-         this.targetMob = null;
-     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java.patch
deleted file mode 100644
index 36cc381dc3..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java.patch
+++ /dev/null
@@ -1,46 +0,0 @@
---- a/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java
-+++ b/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java
-@@ -41,7 +41,7 @@
-     }
- 
-     @Override
--    public Path createPath(BlockPos target, int distance) {
-+    public Path createPath(BlockPos target, @javax.annotation.Nullable Entity entity, int distance) { // Paper - EntityPathfindEvent
-         LevelChunk levelChunk = this.level
-             .getChunkSource()
-             .getChunkNow(SectionPos.blockToSectionCoord(target.getX()), SectionPos.blockToSectionCoord(target.getZ()));
-@@ -56,7 +56,7 @@
-                 }
- 
-                 if (mutableBlockPos.getY() > this.level.getMinY()) {
--                    return super.createPath(mutableBlockPos.above(), distance);
-+                    return super.createPath(mutableBlockPos.above(), entity, distance); // Paper - EntityPathfindEvent
-                 }
- 
-                 mutableBlockPos.setY(target.getY() + 1);
-@@ -69,7 +69,7 @@
-             }
- 
-             if (!levelChunk.getBlockState(target).isSolid()) {
--                return super.createPath(target, distance);
-+                return super.createPath(target, entity, distance); // Paper - EntityPathfindEvent
-             } else {
-                 BlockPos.MutableBlockPos mutableBlockPos2 = target.mutable().move(Direction.UP);
- 
-@@ -77,14 +77,14 @@
-                     mutableBlockPos2.move(Direction.UP);
-                 }
- 
--                return super.createPath(mutableBlockPos2.immutable(), distance);
-+                return super.createPath(mutableBlockPos2.immutable(), entity, distance);  // Paper - EntityPathfindEvent
-             }
-         }
-     }
- 
-     @Override
-     public Path createPath(Entity entity, int distance) {
--        return this.createPath(entity.blockPosition(), distance);
-+        return this.createPath(entity.blockPosition(), entity, distance); // Paper - EntityPathfindEvent
-     }
- 
-     private int getSurfaceY() {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/navigation/WallClimberNavigation.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/navigation/WallClimberNavigation.java.patch
deleted file mode 100644
index b2662d7401..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/navigation/WallClimberNavigation.java.patch
+++ /dev/null
@@ -1,14 +0,0 @@
---- a/net/minecraft/world/entity/ai/navigation/WallClimberNavigation.java
-+++ b/net/minecraft/world/entity/ai/navigation/WallClimberNavigation.java
-@@ -16,9 +16,9 @@
-     }
- 
-     @Override
--    public Path createPath(BlockPos target, int distance) {
-+    public Path createPath(BlockPos target, @Nullable Entity entity, int distance) { // Paper - EntityPathfindEvent
-         this.pathToPosition = target;
--        return super.createPath(target, distance);
-+        return super.createPath(target, entity, distance); // Paper - EntityPathfindEvent
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/sensing/TemptingSensor.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/sensing/TemptingSensor.java.patch
deleted file mode 100644
index 2bd53d40cd..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/sensing/TemptingSensor.java.patch
+++ /dev/null
@@ -1,45 +0,0 @@
---- a/net/minecraft/world/entity/ai/sensing/TemptingSensor.java
-+++ b/net/minecraft/world/entity/ai/sensing/TemptingSensor.java
-@@ -19,6 +19,14 @@
- import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.item.ItemStack;
- 
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.entity.HumanEntity;
-+import org.bukkit.event.entity.EntityTargetEvent;
-+import org.bukkit.event.entity.EntityTargetLivingEntityEvent;
-+// CraftBukkit end
-+
- public class TemptingSensor extends Sensor<PathfinderMob> {
- 
-     private static final TargetingConditions TEMPT_TARGETING = TargetingConditions.forNonCombat().ignoreLineOfSight();
-@@ -31,7 +39,7 @@
-     protected void doTick(ServerLevel world, PathfinderMob entity) {
-         Brain<?> behaviorcontroller = entity.getBrain();
-         TargetingConditions pathfindertargetcondition = TemptingSensor.TEMPT_TARGETING.copy().range((double) ((float) entity.getAttributeValue(Attributes.TEMPT_RANGE)));
--        Stream stream = world.players().stream().filter(EntitySelector.NO_SPECTATORS).filter((entityplayer) -> {
-+        Stream<net.minecraft.server.level.ServerPlayer> stream = world.players().stream().filter(EntitySelector.NO_SPECTATORS).filter((entityplayer) -> { // CraftBukkit - decompile error
-             return pathfindertargetcondition.test(world, entity, entityplayer);
-         }).filter(this::playerHoldingTemptation).filter((entityplayer) -> {
-             return !entity.hasPassenger((Entity) entityplayer);
-@@ -43,7 +51,17 @@
-         if (!list.isEmpty()) {
-             Player entityhuman = (Player) list.get(0);
- 
--            behaviorcontroller.setMemory(MemoryModuleType.TEMPTING_PLAYER, (Object) entityhuman);
-+            // CraftBukkit start
-+            EntityTargetLivingEntityEvent event = CraftEventFactory.callEntityTargetLivingEvent(entity, entityhuman, EntityTargetEvent.TargetReason.TEMPT);
-+            if (event.isCancelled()) {
-+                return;
-+            }
-+            if (event.getTarget() instanceof HumanEntity) {
-+                behaviorcontroller.setMemory(MemoryModuleType.TEMPTING_PLAYER, ((CraftHumanEntity) event.getTarget()).getHandle());
-+            } else {
-+                behaviorcontroller.eraseMemory(MemoryModuleType.TEMPTING_PLAYER);
-+            }
-+            // CraftBukkit end
-         } else {
-             behaviorcontroller.eraseMemory(MemoryModuleType.TEMPTING_PLAYER);
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/village/VillageSiege.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/village/VillageSiege.java.patch
deleted file mode 100644
index 4b4a523928..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/village/VillageSiege.java.patch
+++ /dev/null
@@ -1,16 +0,0 @@
---- a/net/minecraft/world/entity/ai/village/VillageSiege.java
-+++ b/net/minecraft/world/entity/ai/village/VillageSiege.java
-@@ -117,11 +117,12 @@
-                 entityzombie.finalizeSpawn(world, world.getCurrentDifficultyAt(entityzombie.blockPosition()), EntitySpawnReason.EVENT, (SpawnGroupData) null);
-             } catch (Exception exception) {
-                 VillageSiege.LOGGER.warn("Failed to create zombie for village siege at {}", vec3d, exception);
-+                com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(exception); // Paper - ServerExceptionEvent
-                 return;
-             }
- 
-             entityzombie.moveTo(vec3d.x, vec3d.y, vec3d.z, world.random.nextFloat() * 360.0F, 0.0F);
--            world.addFreshEntityWithPassengers(entityzombie);
-+            world.addFreshEntityWithPassengers(entityzombie, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.VILLAGE_INVASION); // CraftBukkit
-         }
-     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ambient/Bat.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ambient/Bat.java.patch
deleted file mode 100644
index acfaa378fe..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/ambient/Bat.java.patch
+++ /dev/null
@@ -1,55 +0,0 @@
---- a/net/minecraft/world/entity/ambient/Bat.java
-+++ b/net/minecraft/world/entity/ambient/Bat.java
-@@ -29,6 +29,9 @@
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.levelgen.Heightmap;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+// CraftBukkit end
- 
- public class Bat extends AmbientCreature {
- 
-@@ -88,7 +91,7 @@
-     }
- 
-     @Override
--    public boolean isPushable() {
-+    public boolean isCollidable(boolean ignoreClimbing) { // Paper - Climbing should not bypass cramming gamerule
-         return false;
-     }
- 
-@@ -144,13 +147,13 @@
-                     this.yHeadRot = (float) this.random.nextInt(360);
-                 }
- 
--                if (world.getNearestPlayer(Bat.BAT_RESTING_TARGETING, this) != null) {
-+                if (world.getNearestPlayer(Bat.BAT_RESTING_TARGETING, this) != null && CraftEventFactory.handleBatToggleSleepEvent(this, true)) { // CraftBukkit - Call BatToggleSleepEvent
-                     this.setResting(false);
-                     if (!flag) {
-                         world.levelEvent((Player) null, 1025, blockposition, 0);
-                     }
-                 }
--            } else {
-+            } else if (CraftEventFactory.handleBatToggleSleepEvent(this, true)) { // CraftBukkit - Call BatToggleSleepEvent
-                 this.setResting(false);
-                 if (!flag) {
-                     world.levelEvent((Player) null, 1025, blockposition, 0);
-@@ -177,7 +180,7 @@
- 
-             this.zza = 0.5F;
-             this.setYRot(this.getYRot() + f1);
--            if (this.random.nextInt(100) == 0 && world.getBlockState(blockposition1).isRedstoneConductor(world, blockposition1)) {
-+            if (this.random.nextInt(100) == 0 && world.getBlockState(blockposition1).isRedstoneConductor(world, blockposition1) && CraftEventFactory.handleBatToggleSleepEvent(this, false)) { // CraftBukkit - Call BatToggleSleepEvent
-                 this.setResting(true);
-             }
-         }
-@@ -202,7 +205,7 @@
-         if (this.isInvulnerableTo(world, source)) {
-             return false;
-         } else {
--            if (this.isResting()) {
-+            if (this.isResting() && CraftEventFactory.handleBatToggleSleepEvent(this, true)) { // CraftBukkit - Call BatToggleSleepEvent
-                 this.setResting(false);
-             }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Animal.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Animal.java.patch
deleted file mode 100644
index f8c533216c..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Animal.java.patch
+++ /dev/null
@@ -1,141 +0,0 @@
---- a/net/minecraft/world/entity/animal/Animal.java
-+++ b/net/minecraft/world/entity/animal/Animal.java
-@@ -35,12 +35,20 @@
- import net.minecraft.world.level.block.Blocks;
- import net.minecraft.world.level.pathfinder.PathType;
- 
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.EntityBreedEvent;
-+import org.bukkit.event.entity.EntityDamageEvent;
-+import org.bukkit.event.entity.EntityEnterLoveModeEvent;
-+// CraftBukkit end
-+
- public abstract class Animal extends AgeableMob {
- 
-     protected static final int PARENT_AGE_AFTER_BREEDING = 6000;
-     public int inLove;
-     @Nullable
-     public UUID loveCause;
-+    public ItemStack breedItem; // CraftBukkit - Add breedItem variable
- 
-     protected Animal(EntityType<? extends Animal> type, Level world) {
-         super(type, world);
-@@ -82,9 +90,15 @@
-     }
- 
-     @Override
--    protected void actuallyHurt(ServerLevel world, DamageSource source, float amount) {
-+    // CraftBukkit start - void -> boolean
-+    public boolean actuallyHurt(ServerLevel worldserver, DamageSource damagesource, float f, EntityDamageEvent event) {
-+        boolean damageResult = super.actuallyHurt(worldserver, damagesource, f, event);
-+        if (!damageResult) {
-+            return false;
-+        }
-         this.resetLove();
--        super.actuallyHurt(world, source, amount);
-+        return true;
-+        // CraftBukkit end
-     }
- 
-     @Override
-@@ -144,8 +158,9 @@
-             int i = this.getAge();
- 
-             if (!this.level().isClientSide && i == 0 && this.canFallInLove()) {
-+                final ItemStack breedCopy = itemstack.copy(); // Paper - Fix EntityBreedEvent copying
-                 this.usePlayerItem(player, hand, itemstack);
--                this.setInLove(player);
-+                this.setInLove(player, breedCopy); // Paper - Fix EntityBreedEvent copying
-                 this.playEatingSound();
-                 return InteractionResult.SUCCESS_SERVER;
-             }
-@@ -187,11 +202,26 @@
-         return this.inLove <= 0;
-     }
- 
-+    @Deprecated @io.papermc.paper.annotation.DoNotUse // Paper - Fix EntityBreedEvent copying
-     public void setInLove(@Nullable Player player) {
--        this.inLove = 600;
-+        // Paper start - Fix EntityBreedEvent copying
-+        this.setInLove(player, null);
-+    }
-+    public void setInLove(@Nullable Player player, @Nullable ItemStack breedItemCopy) {
-+        if (breedItemCopy != null) this.breedItem = breedItemCopy;
-+        // Paper end - Fix EntityBreedEvent copying
-+        // CraftBukkit start
-+        EntityEnterLoveModeEvent entityEnterLoveModeEvent = CraftEventFactory.callEntityEnterLoveModeEvent(player, this, 600);
-+        if (entityEnterLoveModeEvent.isCancelled()) {
-+            this.breedItem = null; // Paper - Fix EntityBreedEvent copying; clear if cancelled
-+            return;
-+        }
-+        this.inLove = entityEnterLoveModeEvent.getTicksInLove();
-+        // CraftBukkit end
-         if (player != null) {
-             this.loveCause = player.getUUID();
-         }
-+        // Paper - Fix EntityBreedEvent copying; set breed item in better place
- 
-         this.level().broadcastEntityEvent(this, (byte) 18);
-     }
-@@ -233,25 +263,48 @@
-         if (entityageable != null) {
-             entityageable.setBaby(true);
-             entityageable.moveTo(this.getX(), this.getY(), this.getZ(), 0.0F, 0.0F);
--            this.finalizeSpawnChildFromBreeding(world, other, entityageable);
--            world.addFreshEntityWithPassengers(entityageable);
-+            // CraftBukkit start - call EntityBreedEvent
-+            ServerPlayer breeder = Optional.ofNullable(this.getLoveCause()).or(() -> {
-+                return Optional.ofNullable(other.getLoveCause());
-+            }).orElse(null);
-+            int experience = this.getRandom().nextInt(7) + 1;
-+            EntityBreedEvent entityBreedEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityBreedEvent(entityageable, this, other, breeder, this.breedItem, experience);
-+            if (entityBreedEvent.isCancelled()) {
-+                return;
-+            }
-+            experience = entityBreedEvent.getExperience();
-+            this.finalizeSpawnChildFromBreeding(world, other, entityageable, experience);
-+            world.addFreshEntityWithPassengers(entityageable, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.BREEDING);
-+            // CraftBukkit end
-         }
-     }
- 
-     public void finalizeSpawnChildFromBreeding(ServerLevel world, Animal other, @Nullable AgeableMob baby) {
--        Optional.ofNullable(this.getLoveCause()).or(() -> {
--            return Optional.ofNullable(other.getLoveCause());
--        }).ifPresent((entityplayer) -> {
-+        // CraftBukkit start
-+        this.finalizeSpawnChildFromBreeding(world, other, baby, this.getRandom().nextInt(7) + 1);
-+    }
-+
-+    public void finalizeSpawnChildFromBreeding(ServerLevel worldserver, Animal entityanimal, @Nullable AgeableMob entityageable, int experience) {
-+        // CraftBukkit end
-+        // Paper start
-+        ServerPlayer entityplayer = this.getLoveCause();
-+        if (entityplayer == null) entityplayer = entityanimal.getLoveCause();
-+        if (entityplayer != null) {
-+            // Paper end
-             entityplayer.awardStat(Stats.ANIMALS_BRED);
--            CriteriaTriggers.BRED_ANIMALS.trigger(entityplayer, this, other, baby);
--        });
-+            CriteriaTriggers.BRED_ANIMALS.trigger(entityplayer, this, entityanimal, entityageable);
-+        } // Paper
-         this.setAge(6000);
--        other.setAge(6000);
-+        entityanimal.setAge(6000);
-         this.resetLove();
--        other.resetLove();
--        world.broadcastEntityEvent(this, (byte) 18);
--        if (world.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) {
--            world.addFreshEntity(new ExperienceOrb(world, this.getX(), this.getY(), this.getZ(), this.getRandom().nextInt(7) + 1));
-+        entityanimal.resetLove();
-+        worldserver.broadcastEntityEvent(this, (byte) 18);
-+        if (worldserver.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) {
-+            // CraftBukkit start - use event experience
-+            if (experience > 0) {
-+                worldserver.addFreshEntity(new ExperienceOrb(worldserver, this.getX(), this.getY(), this.getZ(), experience, org.bukkit.entity.ExperienceOrb.SpawnReason.BREED, entityplayer, entityageable)); // Paper
-+            }
-+            // CraftBukkit end
-         }
- 
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Bee.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Bee.java.patch
deleted file mode 100644
index 748c94de8e..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Bee.java.patch
+++ /dev/null
@@ -1,205 +0,0 @@
---- a/net/minecraft/world/entity/animal/Bee.java
-+++ b/net/minecraft/world/entity/animal/Bee.java
-@@ -92,6 +92,11 @@
- import net.minecraft.world.level.pathfinder.Path;
- import net.minecraft.world.level.pathfinder.PathType;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.EntityPotionEffectEvent;
-+import org.bukkit.event.entity.EntityTargetEvent;
-+// CraftBukkit end
- 
- public class Bee extends Animal implements NeutralMob, FlyingAnimal {
- 
-@@ -149,7 +154,22 @@
-     public Bee(EntityType<? extends Bee> type, Level world) {
-         super(type, world);
-         this.remainingCooldownBeforeLocatingNewFlower = Mth.nextInt(this.random, 20, 60);
--        this.moveControl = new FlyingMoveControl(this, 20, true);
-+        // Paper start - Fix MC-167279
-+        class BeeFlyingMoveControl extends FlyingMoveControl {
-+            public BeeFlyingMoveControl(final Mob entity, final int maxPitchChange, final boolean noGravity) {
-+                super(entity, maxPitchChange, noGravity);
-+            }
-+
-+            @Override
-+            public void tick() {
-+                if (this.mob.getY() <= Bee.this.level().getMinY()) {
-+                    this.mob.setNoGravity(false);
-+                }
-+                super.tick();
-+            }
-+        }
-+        this.moveControl = new BeeFlyingMoveControl(this, 20, true);
-+        // Paper end - Fix MC-167279
-         this.lookControl = new Bee.BeeLookControl(this);
-         this.setPathfindingMalus(PathType.DANGER_FIRE, -1.0F);
-         this.setPathfindingMalus(PathType.WATER, -1.0F);
-@@ -198,21 +218,28 @@
- 
-     @Override
-     public void addAdditionalSaveData(CompoundTag nbt) {
--        super.addAdditionalSaveData(nbt);
--        if (this.hasHive()) {
--            nbt.put("hive_pos", NbtUtils.writeBlockPos(this.getHivePos()));
-+        // CraftBukkit start - selectively save data
-+        this.addAdditionalSaveData(nbt, true);
-+    }
-+
-+    @Override
-+    public void addAdditionalSaveData(CompoundTag nbttagcompound, boolean includeAll) {
-+        // CraftBukkit end
-+        super.addAdditionalSaveData(nbttagcompound);
-+        if (includeAll && this.hasHive()) { // CraftBukkit - selectively save hive
-+            nbttagcompound.put("hive_pos", NbtUtils.writeBlockPos(this.getHivePos()));
-         }
- 
--        if (this.hasSavedFlowerPos()) {
--            nbt.put("flower_pos", NbtUtils.writeBlockPos(this.getSavedFlowerPos()));
-+        if (includeAll && this.hasSavedFlowerPos()) { // CraftBukkit - selectively save flower
-+            nbttagcompound.put("flower_pos", NbtUtils.writeBlockPos(this.getSavedFlowerPos()));
-         }
- 
--        nbt.putBoolean("HasNectar", this.hasNectar());
--        nbt.putBoolean("HasStung", this.hasStung());
--        nbt.putInt("TicksSincePollination", this.ticksWithoutNectarSinceExitingHive);
--        nbt.putInt("CannotEnterHiveTicks", this.stayOutOfHiveCountdown);
--        nbt.putInt("CropsGrownSincePollination", this.numCropsGrownSincePollination);
--        this.addPersistentAngerSaveData(nbt);
-+        nbttagcompound.putBoolean("HasNectar", this.hasNectar());
-+        nbttagcompound.putBoolean("HasStung", this.hasStung());
-+        nbttagcompound.putInt("TicksSincePollination", this.ticksWithoutNectarSinceExitingHive);
-+        nbttagcompound.putInt("CannotEnterHiveTicks", this.stayOutOfHiveCountdown);
-+        nbttagcompound.putInt("CropsGrownSincePollination", this.numCropsGrownSincePollination);
-+        this.addPersistentAngerSaveData(nbttagcompound);
-     }
- 
-     @Override
-@@ -223,8 +250,8 @@
-         this.ticksWithoutNectarSinceExitingHive = nbt.getInt("TicksSincePollination");
-         this.stayOutOfHiveCountdown = nbt.getInt("CannotEnterHiveTicks");
-         this.numCropsGrownSincePollination = nbt.getInt("CropsGrownSincePollination");
--        this.hivePos = (BlockPos) NbtUtils.readBlockPos(nbt, "hive_pos").orElse((Object) null);
--        this.savedFlowerPos = (BlockPos) NbtUtils.readBlockPos(nbt, "flower_pos").orElse((Object) null);
-+        this.hivePos = (BlockPos) NbtUtils.readBlockPos(nbt, "hive_pos").orElse(null); // CraftBukkit - decompile error
-+        this.savedFlowerPos = (BlockPos) NbtUtils.readBlockPos(nbt, "flower_pos").orElse(null); // CraftBukkit - decompile error
-         this.readPersistentAngerSaveData(this.level(), nbt);
-     }
- 
-@@ -248,7 +275,7 @@
-                 }
- 
-                 if (b0 > 0) {
--                    entityliving.addEffect(new MobEffectInstance(MobEffects.POISON, b0 * 20, 0), this);
-+                    entityliving.addEffect(new MobEffectInstance(MobEffects.POISON, b0 * 20, 0), this, EntityPotionEffectEvent.Cause.ATTACK); // CraftBukkit
-                 }
-             }
- 
-@@ -506,7 +533,12 @@
- 
-     @Nullable
-     BeehiveBlockEntity getBeehiveBlockEntity() {
--        return this.hivePos == null ? null : (this.isTooFarAway(this.hivePos) ? null : (BeehiveBlockEntity) this.level().getBlockEntity(this.hivePos, BlockEntityType.BEEHIVE).orElse((Object) null));
-+        // Paper start - move over logic to accommodate isTooFarAway with chunk load check
-+        if (this.hivePos != null && !this.isTooFarAway(this.hivePos) && this.level().getChunkIfLoadedImmediately(this.hivePos.getX() >> 4, this.hivePos.getZ() >> 4) != null) {
-+            return (BeehiveBlockEntity) this.level().getBlockEntity(this.hivePos, BlockEntityType.BEEHIVE).orElse(null);
-+        }
-+        return null;
-+        // Paper end
-     }
- 
-     boolean isHiveValid() {
-@@ -533,11 +565,13 @@
-         this.setFlag(4, hasStung);
-     }
- 
-+    public net.kyori.adventure.util.TriState rollingOverride = net.kyori.adventure.util.TriState.NOT_SET; // Paper - Rolling override
-     public boolean isRolling() {
-         return this.getFlag(2);
-     }
- 
-     public void setRolling(boolean nearTarget) {
-+        nearTarget = rollingOverride.toBooleanOrElse(nearTarget); // Paper - Rolling override
-         this.setFlag(2, nearTarget);
-     }
- 
-@@ -602,7 +636,7 @@
-                     if (mobeffect != null) {
-                         this.usePlayerItem(player, hand, itemstack);
-                         if (!this.level().isClientSide) {
--                            this.addEffect(mobeffect);
-+                            this.addEffect(mobeffect, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.FOOD); // Paper - Add missing effect cause
-                         }
- 
-                         return InteractionResult.SUCCESS;
-@@ -671,8 +705,14 @@
-         if (this.isInvulnerableTo(world, source)) {
-             return false;
-         } else {
-+            // CraftBukkit start - Only stop pollinating if entity was damaged
-+            boolean result = super.hurtServer(world, source, amount);
-+            if (!result) {
-+                return result;
-+            }
-+            // CraftBukkit end
-             this.beePollinateGoal.stopPollinating();
--            return super.hurtServer(world, source, amount);
-+            return result; // CraftBukkit
-         }
-     }
- 
-@@ -934,7 +974,7 @@
-                     Bee.this.dropFlower();
-                     this.pollinating = false;
-                     Bee.this.remainingCooldownBeforeLocatingNewFlower = 200;
--                } else {
-+                } else if (Bee.this.savedFlowerPos != null) { // Paper - add null check since API can manipulate this
-                     Vec3 vec3d = Vec3.atBottomCenterOf(Bee.this.savedFlowerPos).add(0.0D, 0.6000000238418579D, 0.0D);
- 
-                     if (vec3d.distanceTo(Bee.this.position()) > 1.0D) {
-@@ -1082,7 +1122,7 @@
- 
-         BeeGoToHiveGoal() {
-             super();
--            this.travellingTicks = Bee.this.level().random.nextInt(10);
-+            this.travellingTicks = Bee.this.random.nextInt(10); // CraftBukkit - SPIGOT-7495: Give Bees another chance and let them use their own random, avoid concurrency issues
-             this.blacklistedTargets = Lists.newArrayList();
-             this.setFlags(EnumSet.of(Goal.Flag.MOVE));
-         }
-@@ -1196,7 +1236,7 @@
- 
-         BeeGoToKnownFlowerGoal() {
-             super();
--            this.travellingTicks = Bee.this.level().random.nextInt(10);
-+            this.travellingTicks = Bee.this.random.nextInt(10); // CraftBukkit - SPIGOT-7495: Give Bees another chance and let them use their own random, avoid concurrency issues
-             this.setFlags(EnumSet.of(Goal.Flag.MOVE));
-         }
- 
-@@ -1301,7 +1341,7 @@
-                             }
-                         }
- 
--                        if (iblockdata1 != null) {
-+                        if (iblockdata1 != null && CraftEventFactory.callEntityChangeBlockEvent(Bee.this, blockposition, iblockdata1)) { // CraftBukkit
-                             Bee.this.level().levelEvent(2011, blockposition, 15);
-                             Bee.this.level().setBlockAndUpdate(blockposition, iblockdata1);
-                             Bee.this.incrementNumCropsGrownSincePollination();
-@@ -1378,7 +1418,7 @@
-         @Override
-         protected void alertOther(Mob mob, LivingEntity target) {
-             if (mob instanceof Bee && this.mob.hasLineOfSight(target)) {
--                mob.setTarget(target);
-+                mob.setTarget(target, EntityTargetEvent.TargetReason.TARGET_ATTACKED_ENTITY, true); // CraftBukkit - reason
-             }
- 
-         }
-@@ -1387,7 +1427,7 @@
-     private static class BeeBecomeAngryTargetGoal extends NearestAttackableTargetGoal<Player> {
- 
-         BeeBecomeAngryTargetGoal(Bee bee) {
--            Objects.requireNonNull(bee);
-+            // Objects.requireNonNull(entitybee); // CraftBukkit - decompile error
-             super(bee, Player.class, 10, true, false, bee::isAngryAt);
-         }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Bucketable.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Bucketable.java.patch
deleted file mode 100644
index 804bc3e693..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Bucketable.java.patch
+++ /dev/null
@@ -1,46 +0,0 @@
---- a/net/minecraft/world/entity/animal/Bucketable.java
-+++ b/net/minecraft/world/entity/animal/Bucketable.java
-@@ -16,6 +16,11 @@
- import net.minecraft.world.item.Items;
- import net.minecraft.world.item.component.CustomData;
- import net.minecraft.world.level.Level;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.event.player.PlayerBucketEntityEvent;
-+// CraftBukkit end
- 
- public interface Bucketable {
- 
-@@ -93,10 +98,21 @@
-         ItemStack itemstack = player.getItemInHand(hand);
- 
-         if (itemstack.getItem() == Items.WATER_BUCKET && entity.isAlive()) {
--            entity.playSound(((Bucketable) entity).getPickupSound(), 1.0F, 1.0F);
-+            // CraftBukkit start
-+            // t0.playSound(((Bucketable) t0).getPickupSound(), 1.0F, 1.0F); // CraftBukkit - moved down
-             ItemStack itemstack1 = ((Bucketable) entity).getBucketItemStack();
- 
-             ((Bucketable) entity).saveToBucketTag(itemstack1);
-+
-+            PlayerBucketEntityEvent playerBucketFishEvent = CraftEventFactory.callPlayerFishBucketEvent(entity, player, itemstack, itemstack1, hand);
-+            itemstack1 = CraftItemStack.asNMSCopy(playerBucketFishEvent.getEntityBucket());
-+            if (playerBucketFishEvent.isCancelled()) {
-+                ((ServerPlayer) player).containerMenu.sendAllDataToRemote(); // We need to update inventory to resync client's bucket
-+                entity.resendPossiblyDesyncedEntityData((ServerPlayer) player); // Paper
-+                return Optional.of(InteractionResult.FAIL);
-+            }
-+            entity.playSound(((Bucketable) entity).getPickupSound(), 1.0F, 1.0F);
-+            // CraftBukkit end
-             ItemStack itemstack2 = ItemUtils.createFilledResult(itemstack, player, itemstack1, false);
- 
-             player.setItemInHand(hand, itemstack2);
-@@ -106,7 +122,7 @@
-                 CriteriaTriggers.FILLED_BUCKET.trigger((ServerPlayer) player, itemstack1);
-             }
- 
--            entity.discard();
-+            entity.discard(EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
-             return Optional.of(InteractionResult.SUCCESS);
-         } else {
-             return Optional.empty();
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Cat.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Cat.java.patch
deleted file mode 100644
index d829874cb7..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Cat.java.patch
+++ /dev/null
@@ -1,96 +0,0 @@
---- a/net/minecraft/world/entity/animal/Cat.java
-+++ b/net/minecraft/world/entity/animal/Cat.java
-@@ -174,10 +174,10 @@
-     @Override
-     public void readAdditionalSaveData(CompoundTag nbt) {
-         super.readAdditionalSaveData(nbt);
--        Optional optional = Optional.ofNullable(ResourceLocation.tryParse(nbt.getString("variant"))).map((minecraftkey) -> {
-+        Optional<ResourceKey<CatVariant>> optional = Optional.ofNullable(ResourceLocation.tryParse(nbt.getString("variant"))).map((minecraftkey) -> { // CraftBukkit - decompile error
-             return ResourceKey.create(Registries.CAT_VARIANT, minecraftkey);
-         });
--        Registry iregistry = BuiltInRegistries.CAT_VARIANT;
-+        Registry<CatVariant> iregistry = BuiltInRegistries.CAT_VARIANT; // CraftBukkit - decompile error
- 
-         Objects.requireNonNull(iregistry);
-         optional.flatMap(iregistry::get).ifPresent(this::setVariant);
-@@ -365,7 +365,7 @@
-         BuiltInRegistries.CAT_VARIANT.getRandomElementOf(tagkey, world.getRandom()).ifPresent(this::setVariant);
-         ServerLevel worldserver = world.getLevel();
- 
--        if (worldserver.structureManager().getStructureWithPieceAt(this.blockPosition(), StructureTags.CATS_SPAWN_AS_BLACK).isValid()) {
-+        if (worldserver.structureManager().getStructureWithPieceAt(this.blockPosition(), StructureTags.CATS_SPAWN_AS_BLACK, world).isValid()) { // Paper - Fix swamp hut cat generation deadlock
-             this.setVariant((Holder) BuiltInRegistries.CAT_VARIANT.getOrThrow(CatVariant.ALL_BLACK));
-             this.setPersistenceRequired();
-         }
-@@ -386,6 +386,13 @@
-                     DyeColor enumcolor = itemdye.getDyeColor();
- 
-                     if (enumcolor != this.getCollarColor()) {
-+                        // Paper start - Add EntityDyeEvent and CollarColorable interface
-+                        final io.papermc.paper.event.entity.EntityDyeEvent event = new io.papermc.paper.event.entity.EntityDyeEvent(this.getBukkitEntity(), org.bukkit.DyeColor.getByWoolData((byte) enumcolor.getId()), ((net.minecraft.server.level.ServerPlayer) player).getBukkitEntity());
-+                        if (!event.callEvent()) {
-+                            return InteractionResult.FAIL;
-+                        }
-+                        enumcolor = DyeColor.byId(event.getColor().getWoolData());
-+                        // Paper end - Add EntityDyeEvent and CollarColorable interface
-                         if (!this.level().isClientSide()) {
-                             this.setCollarColor(enumcolor);
-                             itemstack.consume(1, player);
-@@ -399,7 +406,7 @@
-                         this.usePlayerItem(player, hand, itemstack);
-                         FoodProperties foodinfo = (FoodProperties) itemstack.get(DataComponents.FOOD);
- 
--                        this.heal(foodinfo != null ? (float) foodinfo.nutrition() : 1.0F);
-+                        this.heal(foodinfo != null ? (float) foodinfo.nutrition() : 1.0F, org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.EATING); // Paper - Add missing regain reason
-                         this.playEatingSound();
-                     }
- 
-@@ -462,7 +469,7 @@
-     }
- 
-     private void tryToTame(Player player) {
--        if (this.random.nextInt(3) == 0) {
-+        if (this.random.nextInt(3) == 0 && !org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTameEvent(this, player).isCancelled()) { // CraftBukkit
-             this.tame(player);
-             this.setOrderedToSit(true);
-             this.level().broadcastEntityEvent(this, (byte) 7);
-@@ -480,7 +487,7 @@
-     private static class CatTemptGoal extends TemptGoal {
- 
-         @Nullable
--        private Player selectedPlayer;
-+        private LivingEntity selectedPlayer; // CraftBukkit
-         private final Cat cat;
- 
-         public CatTemptGoal(Cat cat, double speed, Predicate<ItemStack> foodPredicate, boolean canBeScared) {
-@@ -614,7 +621,15 @@
-             this.cat.randomTeleport((double) (blockposition_mutableblockposition.getX() + randomsource.nextInt(11) - 5), (double) (blockposition_mutableblockposition.getY() + randomsource.nextInt(5) - 2), (double) (blockposition_mutableblockposition.getZ() + randomsource.nextInt(11) - 5), false);
-             blockposition_mutableblockposition.set(this.cat.blockPosition());
-             this.cat.dropFromGiftLootTable(getServerLevel((Entity) this.cat), BuiltInLootTables.CAT_MORNING_GIFT, (worldserver, itemstack) -> {
--                worldserver.addFreshEntity(new ItemEntity(worldserver, (double) blockposition_mutableblockposition.getX() - (double) Mth.sin(this.cat.yBodyRot * 0.017453292F), (double) blockposition_mutableblockposition.getY(), (double) blockposition_mutableblockposition.getZ() + (double) Mth.cos(this.cat.yBodyRot * 0.017453292F), itemstack));
-+                // CraftBukkit start
-+                ItemEntity entityitem = new ItemEntity(worldserver, (double) blockposition_mutableblockposition.getX() - (double) Mth.sin(this.cat.yBodyRot * 0.017453292F), (double) blockposition_mutableblockposition.getY(), (double) blockposition_mutableblockposition.getZ() + (double) Mth.cos(this.cat.yBodyRot * 0.017453292F), itemstack);
-+                org.bukkit.event.entity.EntityDropItemEvent event = new org.bukkit.event.entity.EntityDropItemEvent(this.cat.getBukkitEntity(), (org.bukkit.entity.Item) entityitem.getBukkitEntity());
-+                entityitem.level().getCraftServer().getPluginManager().callEvent(event);
-+                if (event.isCancelled()) {
-+                    return;
-+                }
-+                worldserver.addFreshEntity(entityitem);
-+                // CraftBukkit end
-             });
-         }
- 
-@@ -645,10 +660,10 @@
-         private final Cat cat;
- 
-         public CatAvoidEntityGoal(Cat cat, Class<T> fleeFromType, float distance, double slowSpeed, double fastSpeed) {
--            Predicate predicate = EntitySelector.NO_CREATIVE_OR_SPECTATOR;
-+            // Predicate predicate = IEntitySelector.NO_CREATIVE_OR_SPECTATOR; // CraftBukkit - decompile error
- 
--            Objects.requireNonNull(predicate);
--            super(cat, fleeFromType, distance, slowSpeed, fastSpeed, predicate::test);
-+            // Objects.requireNonNull(predicate); // CraftBukkit - decompile error
-+            super(cat, fleeFromType, distance, slowSpeed, fastSpeed, EntitySelector.NO_CREATIVE_OR_SPECTATOR::test); // CraftBukkit - decompile error
-             this.cat = cat;
-         }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Chicken.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Chicken.java.patch
deleted file mode 100644
index 78c5696f2a..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Chicken.java.patch
+++ /dev/null
@@ -1,15 +0,0 @@
---- a/net/minecraft/world/entity/animal/Chicken.java
-+++ b/net/minecraft/world/entity/animal/Chicken.java
-@@ -99,10 +99,12 @@
- 
-         if (world instanceof ServerLevel worldserver) {
-             if (this.isAlive() && !this.isBaby() && !this.isChickenJockey() && --this.eggTime <= 0) {
-+                this.forceDrops = true; // CraftBukkit
-                 if (this.dropFromGiftLootTable(worldserver, BuiltInLootTables.CHICKEN_LAY, this::spawnAtLocation)) {
-                     this.playSound(SoundEvents.CHICKEN_EGG, 1.0F, (this.random.nextFloat() - this.random.nextFloat()) * 0.2F + 1.0F);
-                     this.gameEvent(GameEvent.ENTITY_PLACE);
-                 }
-+                this.forceDrops = false; // CraftBukkit
- 
-                 this.eggTime = this.random.nextInt(6000) + 6000;
-             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Cow.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Cow.java.patch
deleted file mode 100644
index 4e23f7179b..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Cow.java.patch
+++ /dev/null
@@ -1,33 +0,0 @@
---- a/net/minecraft/world/entity/animal/Cow.java
-+++ b/net/minecraft/world/entity/animal/Cow.java
-@@ -30,6 +30,11 @@
- import net.minecraft.world.item.Items;
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.block.state.BlockState;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.event.player.PlayerBucketFillEvent;
-+// CraftBukkit end
- 
- public class Cow extends Animal {
- 
-@@ -92,8 +97,17 @@
-         ItemStack itemstack = player.getItemInHand(hand);
- 
-         if (itemstack.is(Items.BUCKET) && !this.isBaby()) {
-+            // CraftBukkit start - Got milk?
-+            PlayerBucketFillEvent event = CraftEventFactory.callPlayerBucketFillEvent((ServerLevel) player.level(), player, this.blockPosition(), this.blockPosition(), null, itemstack, Items.MILK_BUCKET, hand);
-+
-+            if (event.isCancelled()) {
-+                player.containerMenu.sendAllDataToRemote(); // Paper - Fix inventory desync
-+                return InteractionResult.PASS;
-+            }
-+            // CraftBukkit end
-+
-             player.playSound(SoundEvents.COW_MILK, 1.0F, 1.0F);
--            ItemStack itemstack1 = ItemUtils.createFilledResult(itemstack, player, Items.MILK_BUCKET.getDefaultInstance());
-+            ItemStack itemstack1 = ItemUtils.createFilledResult(itemstack, player, CraftItemStack.asNMSCopy(event.getItemStack())); // CraftBukkit
- 
-             player.setItemInHand(hand, itemstack1);
-             return InteractionResult.SUCCESS;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Dolphin.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Dolphin.java.patch
deleted file mode 100644
index c16913343e..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Dolphin.java.patch
+++ /dev/null
@@ -1,87 +0,0 @@
---- a/net/minecraft/world/entity/animal/Dolphin.java
-+++ b/net/minecraft/world/entity/animal/Dolphin.java
-@@ -61,9 +61,20 @@
- import net.minecraft.world.level.ServerLevelAccessor;
- import net.minecraft.world.level.pathfinder.PathComputationType;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.EntityPotionEffectEvent;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public class Dolphin extends AgeableWaterCreature {
- 
-+    // CraftBukkit start - SPIGOT-6907: re-implement LivingEntity#setMaximumAir()
-+    @Override
-+    public int getDefaultMaxAirSupply() {
-+        return Dolphin.TOTAL_AIR_SUPPLY;
-+    }
-+    // CraftBukkit end
-     private static final EntityDataAccessor<BlockPos> TREASURE_POS = SynchedEntityData.defineId(Dolphin.class, EntityDataSerializers.BLOCK_POS);
-     private static final EntityDataAccessor<Boolean> GOT_FISH = SynchedEntityData.defineId(Dolphin.class, EntityDataSerializers.BOOLEAN);
-     private static final EntityDataAccessor<Integer> MOISTNESS_LEVEL = SynchedEntityData.defineId(Dolphin.class, EntityDataSerializers.INT);
-@@ -200,7 +211,7 @@
- 
-     @Override
-     public int getMaxAirSupply() {
--        return 4800;
-+        return this.maxAirTicks; // CraftBukkit - SPIGOT-6907: re-implement LivingEntity#setMaximumAir()
-     }
- 
-     @Override
-@@ -234,11 +245,17 @@
-             ItemStack itemstack = itemEntity.getItem();
- 
-             if (this.canHoldItem(itemstack)) {
-+                // CraftBukkit start - call EntityPickupItemEvent
-+                if (CraftEventFactory.callEntityPickupItemEvent(this, itemEntity, 0, false).isCancelled()) {
-+                    return;
-+                }
-+                itemstack = itemEntity.getItem(); // CraftBukkit- update ItemStack from event
-+                // CraftBukkit start
-                 this.onItemPickup(itemEntity);
-                 this.setItemSlot(EquipmentSlot.MAINHAND, itemstack);
-                 this.setGuaranteedDrop(EquipmentSlot.MAINHAND);
-                 this.take(itemEntity, itemstack.getCount());
--                itemEntity.discard();
-+                itemEntity.discard(EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
-             }
-         }
- 
-@@ -332,7 +349,7 @@
- 
-     @Nullable
-     @Override
--    protected SoundEvent getDeathSound() {
-+    public SoundEvent getDeathSound() { // Paper - decompile error
-         return SoundEvents.DOLPHIN_DEATH;
-     }
- 
-@@ -495,7 +512,7 @@
- 
-         @Override
-         public void start() {
--            this.player.addEffect(new MobEffectInstance(MobEffects.DOLPHINS_GRACE, 100), this.dolphin);
-+            this.player.addEffect(new MobEffectInstance(MobEffects.DOLPHINS_GRACE, 100), this.dolphin, EntityPotionEffectEvent.Cause.DOLPHIN); // CraftBukkit
-         }
- 
-         @Override
-@@ -514,7 +531,7 @@
-             }
- 
-             if (this.player.isSwimming() && this.player.level().random.nextInt(6) == 0) {
--                this.player.addEffect(new MobEffectInstance(MobEffects.DOLPHINS_GRACE, 100), this.dolphin);
-+                this.player.addEffect(new MobEffectInstance(MobEffects.DOLPHINS_GRACE, 100), this.dolphin, EntityPotionEffectEvent.Cause.DOLPHIN); // CraftBukkit
-             }
- 
-         }
-@@ -587,7 +604,7 @@
-                 float f2 = 0.02F * Dolphin.this.random.nextFloat();
- 
-                 entityitem.setDeltaMovement((double) (0.3F * -Mth.sin(Dolphin.this.getYRot() * 0.017453292F) * Mth.cos(Dolphin.this.getXRot() * 0.017453292F) + Mth.cos(f1) * f2), (double) (0.3F * Mth.sin(Dolphin.this.getXRot() * 0.017453292F) * 1.5F), (double) (0.3F * Mth.cos(Dolphin.this.getYRot() * 0.017453292F) * Mth.cos(Dolphin.this.getXRot() * 0.017453292F) + Mth.sin(f1) * f2));
--                Dolphin.this.level().addFreshEntity(entityitem);
-+                Dolphin.this.spawnAtLocation(getServerLevel(Dolphin.this), entityitem); // Paper - Call EntityDropItemEvent
-             }
-         }
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Fox.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Fox.java.patch
deleted file mode 100644
index 7052ecb52a..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Fox.java.patch
+++ /dev/null
@@ -1,168 +0,0 @@
---- a/net/minecraft/world/entity/animal/Fox.java
-+++ b/net/minecraft/world/entity/animal/Fox.java
-@@ -90,6 +90,9 @@
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.level.pathfinder.PathType;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public class Fox extends Animal implements VariantHolder<Fox.Variant> {
- 
-@@ -416,7 +419,7 @@
- 
-         this.setSleeping(nbt.getBoolean("Sleeping"));
-         this.setVariant(Fox.Variant.byName(nbt.getString("Type")));
--        this.setSitting(nbt.getBoolean("Sitting"));
-+        this.setSitting(nbt.getBoolean("Sitting"), false); // Paper - Add EntityToggleSitEvent
-         this.setIsCrouching(nbt.getBoolean("Crouching"));
-         if (this.level() instanceof ServerLevel) {
-             this.setTargetGoals();
-@@ -429,6 +432,12 @@
-     }
- 
-     public void setSitting(boolean sitting) {
-+        // Paper start - Add EntityToggleSitEvent
-+        this.setSitting(sitting, true);
-+    }
-+    public void setSitting(boolean sitting, boolean fireEvent) {
-+        if (fireEvent && !new io.papermc.paper.event.entity.EntityToggleSitEvent(this.getBukkitEntity(), sitting).callEvent()) return;
-+        // Paper end - Add EntityToggleSitEvent
-         this.setFlag(1, sitting);
-     }
- 
-@@ -489,21 +498,22 @@
-             entityitem.setPickUpDelay(40);
-             entityitem.setThrower(this);
-             this.playSound(SoundEvents.FOX_SPIT, 1.0F, 1.0F);
--            this.level().addFreshEntity(entityitem);
-+            this.spawnAtLocation((net.minecraft.server.level.ServerLevel) this.level(), entityitem); // Paper - Call EntityDropItemEvent
-         }
-     }
- 
-     private void dropItemStack(ItemStack stack) {
-         ItemEntity entityitem = new ItemEntity(this.level(), this.getX(), this.getY(), this.getZ(), stack);
- 
--        this.level().addFreshEntity(entityitem);
-+        this.spawnAtLocation((net.minecraft.server.level.ServerLevel) this.level(), entityitem); // Paper - Call EntityDropItemEvent
-     }
- 
-     @Override
-     protected void pickUpItem(ServerLevel world, ItemEntity itemEntity) {
-         ItemStack itemstack = itemEntity.getItem();
- 
--        if (this.canHoldItem(itemstack)) {
-+        if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPickupItemEvent(this, itemEntity, itemstack.getCount() - 1, !this.canHoldItem(itemstack)).isCancelled()) { // CraftBukkit - call EntityPickupItemEvent
-+            itemstack = itemEntity.getItem(); // CraftBukkit - update ItemStack from event
-             int i = itemstack.getCount();
- 
-             if (i > 1) {
-@@ -515,7 +525,7 @@
-             this.setItemSlot(EquipmentSlot.MAINHAND, itemstack.split(1));
-             this.setGuaranteedDrop(EquipmentSlot.MAINHAND);
-             this.take(itemEntity, itemstack.getCount());
--            itemEntity.discard();
-+            itemEntity.discard(EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
-             this.ticksSinceEaten = 0;
-         }
- 
-@@ -685,16 +695,38 @@
-         return this.getTrustedUUIDs().contains(uuid);
-     }
- 
-+    // Paper start - handle the bitten item separately like vanilla
-     @Override
--    protected void dropAllDeathLoot(ServerLevel world, DamageSource damageSource) {
-+    protected boolean shouldSkipLoot(EquipmentSlot slot) {
-+        return slot == EquipmentSlot.MAINHAND;
-+    }
-+    // Paper end
-+
-+    @Override
-+    // Paper start - Cancellable death event
-+    protected org.bukkit.event.entity.EntityDeathEvent dropAllDeathLoot(ServerLevel world, DamageSource damageSource) {
-         ItemStack itemstack = this.getItemBySlot(EquipmentSlot.MAINHAND);
- 
--        if (!itemstack.isEmpty()) {
-+        boolean releaseMouth = false;
-+        if (!itemstack.isEmpty() && world.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) { // Fix MC-153010
-             this.spawnAtLocation(world, itemstack);
-+            releaseMouth = true;
-+        }
-+
-+        org.bukkit.event.entity.EntityDeathEvent deathEvent = super.dropAllDeathLoot(world, damageSource);
-+
-+        // Below is code to drop
-+
-+        if (deathEvent == null || deathEvent.isCancelled()) {
-+            return deathEvent;
-+        }
-+
-+        if (releaseMouth) {
-+            // Paper end - Cancellable death event
-             this.setItemSlot(EquipmentSlot.MAINHAND, ItemStack.EMPTY);
-         }
- 
--        super.dropAllDeathLoot(world, damageSource);
-+        return deathEvent; // Paper - Cancellable death event
-     }
- 
-     public static boolean isPathClear(Fox fox, LivingEntity chasedEntity) {
-@@ -853,6 +885,16 @@
-                 if (entityplayer1 != null && entityplayer != entityplayer1) {
-                     entityfox.addTrustedUUID(entityplayer1.getUUID());
-                 }
-+                // CraftBukkit start - call EntityBreedEvent
-+                entityfox.setAge(-24000);
-+                entityfox.moveTo(this.animal.getX(), this.animal.getY(), this.animal.getZ(), 0.0F, 0.0F);
-+                int experience = this.animal.getRandom().nextInt(7) + 1;
-+                org.bukkit.event.entity.EntityBreedEvent entityBreedEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityBreedEvent(entityfox, this.animal, this.partner, entityplayer, this.animal.breedItem, experience);
-+                if (entityBreedEvent.isCancelled()) {
-+                    return;
-+                }
-+                experience = entityBreedEvent.getExperience();
-+                // CraftBukkit end
- 
-                 if (entityplayer2 != null) {
-                     entityplayer2.awardStat(Stats.ANIMALS_BRED);
-@@ -863,12 +905,14 @@
-                 this.partner.setAge(6000);
-                 this.animal.resetLove();
-                 this.partner.resetLove();
--                entityfox.setAge(-24000);
--                entityfox.moveTo(this.animal.getX(), this.animal.getY(), this.animal.getZ(), 0.0F, 0.0F);
--                worldserver.addFreshEntityWithPassengers(entityfox);
-+                worldserver.addFreshEntityWithPassengers(entityfox, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.BREEDING); // CraftBukkit - added SpawnReason
-                 this.level.broadcastEntityEvent(this.animal, (byte) 18);
-                 if (worldserver.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) {
--                    this.level.addFreshEntity(new ExperienceOrb(this.level, this.animal.getX(), this.animal.getY(), this.animal.getZ(), this.animal.getRandom().nextInt(7) + 1));
-+                    // CraftBukkit start - use event experience
-+                    if (experience > 0) {
-+                        this.level.addFreshEntity(new ExperienceOrb(this.level, this.animal.getX(), this.animal.getY(), this.animal.getZ(), experience, org.bukkit.entity.ExperienceOrb.SpawnReason.BREED, entityplayer, entityfox)); // Paper
-+                    }
-+                    // CraftBukkit end
-                 }
- 
-             }
-@@ -1264,6 +1308,11 @@
-             int i = (Integer) state.getValue(SweetBerryBushBlock.AGE);
- 
-             state.setValue(SweetBerryBushBlock.AGE, 1);
-+            // CraftBukkit start - call EntityChangeBlockEvent
-+            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(Fox.this, this.blockPos, state.setValue(SweetBerryBushBlock.AGE, 1))) {
-+                return;
-+            }
-+            // CraftBukkit end
-             int j = 1 + Fox.this.level().random.nextInt(2) + (i == 3 ? 1 : 0);
-             ItemStack itemstack = Fox.this.getItemBySlot(EquipmentSlot.MAINHAND);
- 
-@@ -1494,7 +1543,7 @@
-         }
- 
-         public static Fox.Variant byName(String name) {
--            return (Fox.Variant) Fox.Variant.CODEC.byName(name, (Enum) Fox.Variant.RED);
-+            return (Fox.Variant) Fox.Variant.CODEC.byName(name, Fox.Variant.RED); // CraftBukkit - decompile error
-         }
- 
-         public static Fox.Variant byId(int id) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/IronGolem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/IronGolem.java.patch
deleted file mode 100644
index 4562593cc0..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/IronGolem.java.patch
+++ /dev/null
@@ -1,20 +0,0 @@
---- a/net/minecraft/world/entity/animal/IronGolem.java
-+++ b/net/minecraft/world/entity/animal/IronGolem.java
-@@ -98,7 +98,7 @@
-     @Override
-     protected void doPush(Entity entity) {
-         if (entity instanceof Enemy && !(entity instanceof Creeper) && this.getRandom().nextInt(20) == 0) {
--            this.setTarget((LivingEntity) entity);
-+            this.setTarget((LivingEntity) entity, org.bukkit.event.entity.EntityTargetLivingEntityEvent.TargetReason.COLLISION, true); // CraftBukkit - set reason
-         }
- 
-         super.doPush(entity);
-@@ -319,7 +319,7 @@
-         BlockPos blockposition1 = blockposition.below();
-         BlockState iblockdata = world.getBlockState(blockposition1);
- 
--        if (!iblockdata.entityCanStandOn(world, blockposition1, this)) {
-+        if (!iblockdata.entityCanStandOn(world, blockposition1, this) && !this.level().paperConfig().entities.spawning.ironGolemsCanSpawnInAir) { // Paper - Add option to allow iron golems to spawn in air
-             return false;
-         } else {
-             for (int i = 1; i < 3; ++i) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/MushroomCow.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/MushroomCow.java.patch
deleted file mode 100644
index a7cff6ad18..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/MushroomCow.java.patch
+++ /dev/null
@@ -1,85 +0,0 @@
---- a/net/minecraft/world/entity/animal/MushroomCow.java
-+++ b/net/minecraft/world/entity/animal/MushroomCow.java
-@@ -42,6 +42,13 @@
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.level.storage.loot.BuiltInLootTables;
-+// CraftBukkit start
-+import org.bukkit.Bukkit;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.event.entity.EntityDropItemEvent;
-+import org.bukkit.event.entity.EntityTransformEvent;
-+// CraftBukkit end
- 
- public class MushroomCow extends Cow implements Shearable, VariantHolder<MushroomCow.Variant> {
- 
-@@ -120,7 +127,19 @@
-             if (world instanceof ServerLevel) {
-                 ServerLevel worldserver = (ServerLevel) world;
- 
--                this.shear(worldserver, SoundSource.PLAYERS, itemstack);
-+                // CraftBukkit start
-+                // Paper start - custom shear drops
-+                java.util.List<ItemStack> drops = this.generateDefaultDrops(worldserver, itemstack);
-+                org.bukkit.event.player.PlayerShearEntityEvent event = CraftEventFactory.handlePlayerShearEntityEvent(player, this, itemstack, hand, drops);
-+                if (event != null) {
-+                    if (event.isCancelled()) {
-+                        return InteractionResult.PASS;
-+                    }
-+                    drops = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getDrops());
-+                // Paper end - custom shear drops
-+                }
-+                // CraftBukkit end
-+                this.shear(worldserver, SoundSource.PLAYERS, itemstack, drops); // Paper - custom shear drops
-                 this.gameEvent(GameEvent.SHEAR, player);
-                 itemstack.hurtAndBreak(1, player, getSlotForHand(hand));
-             }
-@@ -158,16 +177,32 @@
- 
-     @Override
-     public void shear(ServerLevel world, SoundSource shearedSoundCategory, ItemStack shears) {
-+    // Paper start - custom shear drops
-+        this.shear(world, shearedSoundCategory, shears, this.generateDefaultDrops(world, shears));
-+    }
-+
-+    @Override
-+    public java.util.List<ItemStack> generateDefaultDrops(final ServerLevel serverLevel, final ItemStack shears) {
-+        final java.util.List<ItemStack> drops = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>();
-+        this.dropFromShearingLootTable(serverLevel, BuiltInLootTables.SHEAR_MOOSHROOM, shears, (ignored, stack) -> {
-+            for (int i = 0; i < stack.getCount(); ++i) drops.add(stack.copyWithCount(1));
-+        });
-+        return drops;
-+    }
-+
-+    @Override
-+    public void shear(ServerLevel world, SoundSource shearedSoundCategory, ItemStack shears, java.util.List<ItemStack> drops) {
-+    // Paper end - custom shear drops
-         world.playSound((Player) null, (Entity) this, SoundEvents.MOOSHROOM_SHEAR, shearedSoundCategory, 1.0F, 1.0F);
-         this.convertTo(EntityType.COW, ConversionParams.single(this, false, false), (entitycow) -> {
-             world.sendParticles(ParticleTypes.EXPLOSION, this.getX(), this.getY(0.5D), this.getZ(), 1, 0.0D, 0.0D, 0.0D, 0.0D);
--            this.dropFromShearingLootTable(world, BuiltInLootTables.SHEAR_MOOSHROOM, shears, (worldserver1, itemstack1) -> {
--                for (int i = 0; i < itemstack1.getCount(); ++i) {
--                    worldserver1.addFreshEntity(new ItemEntity(this.level(), this.getX(), this.getY(1.0D), this.getZ(), itemstack1.copyWithCount(1)));
--                }
--
-+            // Paper start - custom shear drops; moved drop generation to separate method
-+            drops.forEach(drop -> {
-+                ItemEntity entityitem = new ItemEntity(this.level(), this.getX(), this.getY(1.0D), this.getZ(), drop);
-+                this.spawnAtLocation(world, entityitem);
-+            // Paper end - custom shear drops; moved drop generation to separate method
-             });
--        });
-+        }, EntityTransformEvent.TransformReason.SHEARED, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.SHEARED); // CraftBukkit
-     }
- 
-     @Override
-@@ -263,7 +298,7 @@
-         }
- 
-         static MushroomCow.Variant byName(String name) {
--            return (MushroomCow.Variant) MushroomCow.Variant.CODEC.byName(name, (Enum) MushroomCow.Variant.RED);
-+            return (MushroomCow.Variant) MushroomCow.Variant.CODEC.byName(name, MushroomCow.Variant.RED); // CraftBukkit - decompile error
-         }
-     }
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Ocelot.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Ocelot.java.patch
deleted file mode 100644
index e217641cf1..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Ocelot.java.patch
+++ /dev/null
@@ -1,34 +0,0 @@
---- a/net/minecraft/world/entity/animal/Ocelot.java
-+++ b/net/minecraft/world/entity/animal/Ocelot.java
-@@ -132,7 +132,7 @@
- 
-     @Override
-     public boolean removeWhenFarAway(double distanceSquared) {
--        return !this.isTrusting() && this.tickCount > 2400;
-+        return !this.isTrusting() && this.tickCount > 2400 && !this.hasCustomName() && !this.isLeashed(); // Paper - honor name and leash
-     }
- 
-     public static AttributeSupplier.Builder createAttributes() {
-@@ -167,7 +167,7 @@
-         if ((this.temptGoal == null || this.temptGoal.isRunning()) && !this.isTrusting() && this.isFood(itemstack) && player.distanceToSqr((Entity) this) < 9.0D) {
-             this.usePlayerItem(player, hand, itemstack);
-             if (!this.level().isClientSide) {
--                if (this.random.nextInt(3) == 0) {
-+                if (this.random.nextInt(3) == 0 && !org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTameEvent(this, player).isCancelled()) { // CraftBukkit - added event call and isCancelled check
-                     this.setTrusting(true);
-                     this.spawnTrustingParticles(true);
-                     this.level().broadcastEntityEvent(this, (byte) 41);
-@@ -298,10 +298,10 @@
-         private final Ocelot ocelot;
- 
-         public OcelotAvoidEntityGoal(Ocelot ocelot, Class<T> fleeFromType, float distance, double slowSpeed, double fastSpeed) {
--            Predicate predicate = EntitySelector.NO_CREATIVE_OR_SPECTATOR;
-+            // Predicate predicate = IEntitySelector.NO_CREATIVE_OR_SPECTATOR; // CraftBukkit - decompile error
- 
--            Objects.requireNonNull(predicate);
--            super(ocelot, fleeFromType, distance, slowSpeed, fastSpeed, predicate::test);
-+            // Objects.requireNonNull(predicate); // CraftBukkit - decompile error
-+            super(ocelot, fleeFromType, distance, slowSpeed, fastSpeed, EntitySelector.NO_CREATIVE_OR_SPECTATOR::test); // CraftBukkit - decompile error
-             this.ocelot = ocelot;
-         }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Panda.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Panda.java.patch
deleted file mode 100644
index 911c02e2c0..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Panda.java.patch
+++ /dev/null
@@ -1,122 +0,0 @@
---- a/net/minecraft/world/entity/animal/Panda.java
-+++ b/net/minecraft/world/entity/animal/Panda.java
-@@ -68,6 +68,11 @@
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.level.storage.loot.BuiltInLootTables;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.event.entity.EntityTargetEvent;
-+// CraftBukkit end
- 
- public class Panda extends Animal {
- 
-@@ -129,6 +134,7 @@
-     }
- 
-     public void sit(boolean sitting) {
-+        if (!new io.papermc.paper.event.entity.EntityToggleSitEvent(this.getBukkitEntity(), sitting).callEvent()) return; // Paper - Add EntityToggleSitEvent
-         this.setFlag(8, sitting);
-     }
- 
-@@ -525,7 +531,9 @@
-             Panda entitypanda = (Panda) iterator.next();
- 
-             if (!entitypanda.isBaby() && entitypanda.onGround() && !entitypanda.isInWater() && entitypanda.canPerformAction()) {
-+                if (new com.destroystokyo.paper.event.entity.EntityJumpEvent(getBukkitLivingEntity()).callEvent()) { // Paper - Entity Jump API
-                 entitypanda.jumpFromGround();
-+                } else { this.setJumping(false); } // Paper - Entity Jump API; setJumping(false) stops a potential loop
-             }
-         }
- 
-@@ -533,7 +541,9 @@
- 
-         if (world1 instanceof ServerLevel worldserver) {
-             if (worldserver.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) {
-+                this.forceDrops = true; // Paper - Add missing forceDrop toggles
-                 this.dropFromGiftLootTable(worldserver, BuiltInLootTables.PANDA_SNEEZE, this::spawnAtLocation);
-+                this.forceDrops = false; // Paper - Add missing forceDrop toggles
-             }
-         }
- 
-@@ -541,14 +551,14 @@
- 
-     @Override
-     protected void pickUpItem(ServerLevel world, ItemEntity itemEntity) {
--        if (this.getItemBySlot(EquipmentSlot.MAINHAND).isEmpty() && Panda.canPickUpAndEat(itemEntity)) {
-+        if (!CraftEventFactory.callEntityPickupItemEvent(this, itemEntity, 0, !(this.getItemBySlot(EquipmentSlot.MAINHAND).isEmpty() && Panda.canPickUpAndEat(itemEntity))).isCancelled()) { // CraftBukkit
-             this.onItemPickup(itemEntity);
-             ItemStack itemstack = itemEntity.getItem();
- 
-             this.setItemSlot(EquipmentSlot.MAINHAND, itemstack);
-             this.setGuaranteedDrop(EquipmentSlot.MAINHAND);
-             this.take(itemEntity, itemstack.getCount());
--            itemEntity.discard();
-+            itemEntity.discard(EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
-         }
- 
-     }
-@@ -643,8 +653,9 @@
-                 this.usePlayerItem(player, hand, itemstack);
-                 this.ageUp((int) ((float) (-this.getAge() / 20) * 0.1F), true);
-             } else if (!this.level().isClientSide && this.getAge() == 0 && this.canFallInLove()) {
-+                final ItemStack breedCopy = itemstack.copy(); // Paper - Fix EntityBreedEvent copying
-                 this.usePlayerItem(player, hand, itemstack);
--                this.setInLove(player);
-+                this.setInLove(player, breedCopy); // Paper - Fix EntityBreedEvent copying
-             } else {
-                 Level world = this.level();
- 
-@@ -657,7 +668,9 @@
-                         ItemStack itemstack1 = this.getItemBySlot(EquipmentSlot.MAINHAND);
- 
-                         if (!itemstack1.isEmpty() && !player.hasInfiniteMaterials()) {
-+                            this.forceDrops = true; // Paper - Add missing forceDrop toggles
-                             this.spawnAtLocation(worldserver, itemstack1);
-+                            this.forceDrops = false; // Paper - Add missing forceDrop toggles
-                         }
- 
-                         this.setItemSlot(EquipmentSlot.MAINHAND, new ItemStack(itemstack.getItem(), 1));
-@@ -772,7 +785,7 @@
-         }
- 
-         public static Panda.Gene byName(String name) {
--            return (Panda.Gene) Panda.Gene.CODEC.byName(name, (Enum) Panda.Gene.NORMAL);
-+            return (Panda.Gene) Panda.Gene.CODEC.byName(name, Panda.Gene.NORMAL); // CraftBukkit - decompile error
-         }
- 
-         public static Panda.Gene getRandom(RandomSource random) {
-@@ -876,10 +889,10 @@
-         private final Panda panda;
- 
-         public PandaAvoidGoal(Panda panda, Class<T> fleeFromType, float distance, double slowSpeed, double fastSpeed) {
--            Predicate predicate = EntitySelector.NO_SPECTATORS;
-+            // Predicate predicate = IEntitySelector.NO_SPECTATORS;
- 
--            Objects.requireNonNull(predicate);
--            super(panda, fleeFromType, distance, slowSpeed, fastSpeed, predicate::test);
-+            // Objects.requireNonNull(predicate);
-+            super(panda, fleeFromType, distance, slowSpeed, fastSpeed, EntitySelector.NO_SPECTATORS::test);
-             this.panda = panda;
-         }
- 
-@@ -935,7 +948,9 @@
-             ItemStack itemstack = Panda.this.getItemBySlot(EquipmentSlot.MAINHAND);
- 
-             if (!itemstack.isEmpty()) {
-+                Panda.this.forceDrops = true; // Paper - Add missing forceDrop toggles
-                 Panda.this.spawnAtLocation(getServerLevel(Panda.this.level()), itemstack);
-+                Panda.this.forceDrops = false; // Paper - Add missing forceDrop toggles
-                 Panda.this.setItemSlot(EquipmentSlot.MAINHAND, ItemStack.EMPTY);
-                 int i = Panda.this.isLazy() ? Panda.this.random.nextInt(50) + 10 : Panda.this.random.nextInt(150) + 10;
- 
-@@ -1116,7 +1131,7 @@
-         @Override
-         protected void alertOther(Mob mob, LivingEntity target) {
-             if (mob instanceof Panda && mob.isAggressive()) {
--                mob.setTarget(target);
-+                mob.setTarget(target, EntityTargetEvent.TargetReason.TARGET_ATTACKED_ENTITY, true); // CraftBukkit
-             }
- 
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Pig.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Pig.java.patch
deleted file mode 100644
index a5cdaa0866..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Pig.java.patch
+++ /dev/null
@@ -1,29 +0,0 @@
---- a/net/minecraft/world/entity/animal/Pig.java
-+++ b/net/minecraft/world/entity/animal/Pig.java
-@@ -49,6 +49,10 @@
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.phys.AABB;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public class Pig extends Animal implements ItemSteerable, Saddleable {
- 
-@@ -247,7 +251,14 @@
-                 }
- 
-                 entitypigzombie1.setPersistenceRequired();
--            });
-+            // CraftBukkit start
-+            }, null, null);
-+            if (CraftEventFactory.callPigZapEvent(this, lightning, entitypigzombie).isCancelled()) {
-+                return;
-+            }
-+            world.addFreshEntity(entitypigzombie, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.LIGHTNING);
-+            this.discard(EntityRemoveEvent.Cause.TRANSFORMATION); // CraftBukkit - add Bukkit remove cause
-+            // CraftBukkit end
- 
-             if (entitypigzombie == null) {
-                 super.thunderHit(world, lightning);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Rabbit.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Rabbit.java.patch
deleted file mode 100644
index 1f8f84be30..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Rabbit.java.patch
+++ /dev/null
@@ -1,40 +0,0 @@
---- a/net/minecraft/world/entity/animal/Rabbit.java
-+++ b/net/minecraft/world/entity/animal/Rabbit.java
-@@ -65,6 +65,9 @@
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.level.pathfinder.Path;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+// CraftBukkit end
- 
- public class Rabbit extends Animal implements VariantHolder<Rabbit.Variant> {
- 
-@@ -90,7 +93,6 @@
-         super(type, world);
-         this.jumpControl = new Rabbit.RabbitJumpControl(this);
-         this.moveControl = new Rabbit.RabbitMoveControl(this);
--        this.setSpeedModifier(0.0D);
-     }
- 
-     @Override
-@@ -577,9 +579,19 @@
-                     int i = (Integer) iblockdata.getValue(CarrotBlock.AGE);
- 
-                     if (i == 0) {
-+                        // CraftBukkit start
-+                        if (!CraftEventFactory.callEntityChangeBlockEvent(this.rabbit, blockposition, iblockdata.getFluidState().createLegacyBlock())) { // Paper - fix wrong block state
-+                            return;
-+                        }
-+                        // CraftBukkit end
-                         world.setBlock(blockposition, Blocks.AIR.defaultBlockState(), 2);
-                         world.destroyBlock(blockposition, true, this.rabbit);
-                     } else {
-+                        // CraftBukkit start
-+                        if (!CraftEventFactory.callEntityChangeBlockEvent(this.rabbit, blockposition, iblockdata.setValue(CarrotBlock.AGE, i - 1))) {
-+                            return;
-+                        }
-+                        // CraftBukkit end
-                         world.setBlock(blockposition, (BlockState) iblockdata.setValue(CarrotBlock.AGE, i - 1), 2);
-                         world.gameEvent((Holder) GameEvent.BLOCK_CHANGE, blockposition, GameEvent.Context.of((Entity) this.rabbit));
-                         world.levelEvent(2001, blockposition, Block.getId(iblockdata));
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Sheep.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Sheep.java.patch
deleted file mode 100644
index ce52e0ef57..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Sheep.java.patch
+++ /dev/null
@@ -1,90 +0,0 @@
---- a/net/minecraft/world/entity/animal/Sheep.java
-+++ b/net/minecraft/world/entity/animal/Sheep.java
-@@ -41,7 +41,6 @@
- import net.minecraft.world.entity.ai.goal.WaterAvoidingRandomStrollGoal;
- import net.minecraft.world.entity.item.ItemEntity;
- import net.minecraft.world.entity.player.Player;
--import net.minecraft.world.item.DyeColor;
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.item.Items;
- import net.minecraft.world.level.Level;
-@@ -49,6 +48,12 @@
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.level.storage.loot.BuiltInLootTables;
-+import net.minecraft.world.item.DyeColor;
-+// CraftBukkit start
-+import net.minecraft.world.item.Item;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.SheepRegrowWoolEvent;
-+// CraftBukkit end
- 
- public class Sheep extends Animal implements Shearable {
- 
-@@ -160,7 +165,19 @@
-                 ServerLevel worldserver = (ServerLevel) world;
- 
-                 if (this.readyForShearing()) {
--                    this.shear(worldserver, SoundSource.PLAYERS, itemstack);
-+                    // CraftBukkit start
-+                    // Paper start - custom shear drops
-+                    java.util.List<ItemStack> drops = this.generateDefaultDrops(worldserver, itemstack);
-+                    org.bukkit.event.player.PlayerShearEntityEvent event = CraftEventFactory.handlePlayerShearEntityEvent(player, this, itemstack, hand, drops);
-+                    if (event != null) {
-+                        if (event.isCancelled()) {
-+                            return InteractionResult.PASS;
-+                        }
-+                        drops = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getDrops());
-+                    // Paper end - custom shear drops
-+                    }
-+                    // CraftBukkit end
-+                    this.shear(worldserver, SoundSource.PLAYERS, itemstack, drops); // Paper - custom shear drops
-                     this.gameEvent(GameEvent.SHEAR, player);
-                     itemstack.hurtAndBreak(1, player, getSlotForHand(hand));
-                     return InteractionResult.SUCCESS_SERVER;
-@@ -175,10 +192,29 @@
- 
-     @Override
-     public void shear(ServerLevel world, SoundSource shearedSoundCategory, ItemStack shears) {
-+        // Paper start - custom shear drops
-+        this.shear(world, shearedSoundCategory, shears, this.generateDefaultDrops(world, shears));
-+    }
-+
-+    @Override
-+    public java.util.List<ItemStack> generateDefaultDrops(final ServerLevel serverLevel, final ItemStack shears) {
-+        final java.util.List<ItemStack> drops = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>();
-+        this.dropFromShearingLootTable(serverLevel, BuiltInLootTables.SHEAR_SHEEP, shears, (ignored, stack) -> {
-+            for (int i = 0; i < stack.getCount(); ++i) drops.add(stack.copyWithCount(1));
-+        });
-+        return drops;
-+    }
-+
-+    @Override
-+    public void shear(ServerLevel world, SoundSource shearedSoundCategory, ItemStack shears, java.util.List<ItemStack> drops) {
-+        final ServerLevel worldserver1 = world; // Named for lambda consumption
-+        // Paper end - custom shear drops
-         world.playSound((Player) null, (Entity) this, SoundEvents.SHEEP_SHEAR, shearedSoundCategory, 1.0F, 1.0F);
--        this.dropFromShearingLootTable(world, BuiltInLootTables.SHEAR_SHEEP, shears, (worldserver1, itemstack1) -> {
--            for (int i = 0; i < itemstack1.getCount(); ++i) {
--                ItemEntity entityitem = this.spawnAtLocation(worldserver1, itemstack1.copyWithCount(1), 1.0F);
-+        drops.forEach(itemstack1 -> { // Paper - custom drops - loop in generated default drops
-+            if (true) { // Paper - custom drops - loop in generated default drops
-+                this.forceDrops = true; // CraftBukkit
-+                ItemEntity entityitem = this.spawnAtLocation(worldserver1, itemstack1, 1.0F); // Paper - custom drops - copy already done above
-+                this.forceDrops = false; // CraftBukkit
- 
-                 if (entityitem != null) {
-                     entityitem.setDeltaMovement(entityitem.getDeltaMovement().add((double) ((this.random.nextFloat() - this.random.nextFloat()) * 0.1F), (double) (this.random.nextFloat() * 0.05F), (double) ((this.random.nextFloat() - this.random.nextFloat()) * 0.1F)));
-@@ -276,6 +312,12 @@
- 
-     @Override
-     public void ate() {
-+        // CraftBukkit start
-+        SheepRegrowWoolEvent event = new SheepRegrowWoolEvent((org.bukkit.entity.Sheep) this.getBukkitEntity());
-+        this.level().getCraftServer().getPluginManager().callEvent(event);
-+
-+        if (event.isCancelled()) return;
-+        // CraftBukkit end
-         super.ate();
-         this.setSheared(false);
-         if (this.isBaby()) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/ShoulderRidingEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/ShoulderRidingEntity.java.patch
deleted file mode 100644
index 40307984a0..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/ShoulderRidingEntity.java.patch
+++ /dev/null
@@ -1,21 +0,0 @@
---- a/net/minecraft/world/entity/animal/ShoulderRidingEntity.java
-+++ b/net/minecraft/world/entity/animal/ShoulderRidingEntity.java
-@@ -5,6 +5,9 @@
- import net.minecraft.world.entity.EntityType;
- import net.minecraft.world.entity.TamableAnimal;
- import net.minecraft.world.level.Level;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public abstract class ShoulderRidingEntity extends TamableAnimal {
- 
-@@ -21,7 +24,7 @@
-         nbttagcompound.putString("id", this.getEncodeId());
-         this.saveWithoutId(nbttagcompound);
-         if (player.setEntityOnShoulder(nbttagcompound)) {
--            this.discard();
-+            this.discard(EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
-             return true;
-         } else {
-             return false;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/SnowGolem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/SnowGolem.java.patch
deleted file mode 100644
index c07f86335e..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/SnowGolem.java.patch
+++ /dev/null
@@ -1,86 +0,0 @@
---- a/net/minecraft/world/entity/animal/SnowGolem.java
-+++ b/net/minecraft/world/entity/animal/SnowGolem.java
-@@ -42,6 +42,9 @@
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.level.storage.loot.BuiltInLootTables;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+// CraftBukkit end
- 
- public class SnowGolem extends AbstractGolem implements Shearable, RangedAttackMob {
- 
-@@ -100,7 +103,7 @@
- 
-         if (world instanceof ServerLevel worldserver) {
-             if (this.level().getBiome(this.blockPosition()).is(BiomeTags.SNOW_GOLEM_MELTS)) {
--                this.hurtServer(worldserver, this.damageSources().onFire(), 1.0F);
-+                this.hurtServer(worldserver, this.damageSources().melting(), 1.0F); // CraftBukkit - DamageSources.ON_FIRE -> CraftEventFactory.MELTING
-             }
- 
-             if (!worldserver.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) {
-@@ -116,7 +119,11 @@
-                 BlockPos blockposition = new BlockPos(j, k, l);
- 
-                 if (this.level().getBlockState(blockposition).isAir() && iblockdata.canSurvive(this.level(), blockposition)) {
--                    this.level().setBlockAndUpdate(blockposition, iblockdata);
-+                    // CraftBukkit start
-+                    if (!CraftEventFactory.handleBlockFormEvent(this.level(), blockposition, iblockdata, this)) {
-+                        continue;
-+                    }
-+                    // CraftBukkit end
-                     this.level().gameEvent((Holder) GameEvent.BLOCK_PLACE, blockposition, GameEvent.Context.of(this, iblockdata));
-                 }
-             }
-@@ -153,7 +160,19 @@
-             if (world instanceof ServerLevel) {
-                 ServerLevel worldserver = (ServerLevel) world;
- 
--                this.shear(worldserver, SoundSource.PLAYERS, itemstack);
-+                // CraftBukkit start
-+                // Paper start - custom shear drops
-+                java.util.List<ItemStack> drops = this.generateDefaultDrops(worldserver, itemstack);
-+                org.bukkit.event.player.PlayerShearEntityEvent event = CraftEventFactory.handlePlayerShearEntityEvent(player, this, itemstack, hand, drops);
-+                if (event != null) {
-+                    if (event.isCancelled()) {
-+                        return InteractionResult.PASS;
-+                    }
-+                    drops = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getDrops());
-+                // Paper end - custom shear drops
-+                }
-+                // CraftBukkit end
-+                this.shear(worldserver, SoundSource.PLAYERS, itemstack, drops); // Paper - custom shear drops
-                 this.gameEvent(GameEvent.SHEAR, player);
-                 itemstack.hurtAndBreak(1, player, getSlotForHand(hand));
-             }
-@@ -166,10 +185,29 @@
- 
-     @Override
-     public void shear(ServerLevel world, SoundSource shearedSoundCategory, ItemStack shears) {
-+    // Paper start - custom shear drops
-+        this.shear(world, shearedSoundCategory, shears, this.generateDefaultDrops(world, shears));
-+    }
-+
-+    @Override
-+    public java.util.List<ItemStack> generateDefaultDrops(final ServerLevel serverLevel, final ItemStack shears) {
-+        final java.util.List<ItemStack> drops = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>();
-+        this.dropFromShearingLootTable(serverLevel, BuiltInLootTables.SHEAR_SNOW_GOLEM, shears, (ignored, stack) -> {
-+            drops.add(stack);
-+        });
-+        return drops;
-+    }
-+
-+    @Override
-+    public void shear(ServerLevel world, SoundSource shearedSoundCategory, ItemStack shears, java.util.List<ItemStack> drops) {
-+        final ServerLevel worldserver1 = world; // Named for lambda consumption
-+    // Paper end - custom shear drops
-         world.playSound((Player) null, (Entity) this, SoundEvents.SNOW_GOLEM_SHEAR, shearedSoundCategory, 1.0F, 1.0F);
-         this.setPumpkin(false);
--        this.dropFromShearingLootTable(world, BuiltInLootTables.SHEAR_SNOW_GOLEM, shears, (worldserver1, itemstack1) -> {
-+        drops.forEach(itemstack1 -> { // Paper - custom shear drops
-+            this.forceDrops = true; // CraftBukkit
-             this.spawnAtLocation(worldserver1, itemstack1, this.getEyeHeight());
-+            this.forceDrops = false; // CraftBukkit
-         });
-     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Squid.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Squid.java.patch
deleted file mode 100644
index d67bfc9654..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Squid.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/entity/animal/Squid.java
-+++ b/net/minecraft/world/entity/animal/Squid.java
-@@ -46,7 +46,7 @@
- 
-     public Squid(EntityType<? extends Squid> type, Level world) {
-         super(type, world);
--        this.random.setSeed((long)this.getId());
-+        //this.random.setSeed((long)this.getId()); // Paper - Share random for entities to make them more random
-         this.tentacleSpeed = 1.0F / (this.random.nextFloat() + 1.0F) * 0.2F;
-     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Turtle.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Turtle.java.patch
deleted file mode 100644
index 49bba2e107..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Turtle.java.patch
+++ /dev/null
@@ -1,74 +0,0 @@
---- a/net/minecraft/world/entity/animal/Turtle.java
-+++ b/net/minecraft/world/entity/animal/Turtle.java
-@@ -310,7 +310,9 @@
-                 ServerLevel worldserver = (ServerLevel) world;
- 
-                 if (worldserver.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) {
-+                    this.forceDrops = true; // CraftBukkit
-                     this.spawnAtLocation(worldserver, Items.TURTLE_SCUTE, 1);
-+                    this.forceDrops = false; // CraftBukkit
-                 }
-             }
-         }
-@@ -339,7 +341,7 @@
- 
-     @Override
-     public void thunderHit(ServerLevel world, LightningBolt lightning) {
--        this.hurtServer(world, this.damageSources().lightningBolt(), Float.MAX_VALUE);
-+        this.hurtServer(world, this.damageSources().lightningBolt().customEventDamager(lightning), Float.MAX_VALUE); // CraftBukkit // Paper - fix DamageSource API
-     }
- 
-     @Override
-@@ -446,6 +448,10 @@
-             if (entityplayer == null && this.partner.getLoveCause() != null) {
-                 entityplayer = this.partner.getLoveCause();
-             }
-+            // Paper start - Add EntityFertilizeEggEvent event
-+            io.papermc.paper.event.entity.EntityFertilizeEggEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityFertilizeEggEvent(this.animal, this.partner);
-+            if (event.isCancelled()) return;
-+            // Paper end - Add EntityFertilizeEggEvent event
- 
-             if (entityplayer != null) {
-                 entityplayer.awardStat(Stats.ANIMALS_BRED);
-@@ -460,7 +466,7 @@
-             RandomSource randomsource = this.animal.getRandom();
- 
-             if (getServerLevel((Level) this.level).getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) {
--                this.level.addFreshEntity(new ExperienceOrb(this.level, this.animal.getX(), this.animal.getY(), this.animal.getZ(), randomsource.nextInt(7) + 1));
-+                if (event.getExperience() > 0) this.level.addFreshEntity(new ExperienceOrb(this.level, this.animal.getX(), this.animal.getY(), this.animal.getZ(), event.getExperience(), org.bukkit.entity.ExperienceOrb.SpawnReason.BREED, entityplayer)); // Paper - Add EntityFertilizeEggEvent event
-             }
- 
-         }
-@@ -492,16 +498,21 @@
- 
-             if (!this.turtle.isInWater() && this.isReachedTarget()) {
-                 if (this.turtle.layEggCounter < 1) {
--                    this.turtle.setLayingEgg(true);
-+                    this.turtle.setLayingEgg(new com.destroystokyo.paper.event.entity.TurtleStartDiggingEvent((org.bukkit.entity.Turtle) this.turtle.getBukkitEntity(), io.papermc.paper.util.MCUtil.toLocation(this.turtle.level(), this.blockPos)).callEvent()); // Paper - Turtle API
-                 } else if (this.turtle.layEggCounter > this.adjustedTickDelay(200)) {
-                     Level world = this.turtle.level();
- 
-+                    // Paper start - Turtle API
-+                    int eggCount = this.turtle.random.nextInt(4) + 1;
-+                    com.destroystokyo.paper.event.entity.TurtleLayEggEvent layEggEvent = new com.destroystokyo.paper.event.entity.TurtleLayEggEvent((org.bukkit.entity.Turtle) this.turtle.getBukkitEntity(), io.papermc.paper.util.MCUtil.toLocation(this.turtle.level(), this.blockPos.above()), eggCount);
-+                    if (layEggEvent.callEvent() && org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this.turtle, this.blockPos.above(), Blocks.TURTLE_EGG.defaultBlockState().setValue(TurtleEggBlock.EGGS, layEggEvent.getEggCount()))) {
-                     world.playSound((Player) null, blockposition, SoundEvents.TURTLE_LAY_EGG, SoundSource.BLOCKS, 0.3F, 0.9F + world.random.nextFloat() * 0.2F);
-                     BlockPos blockposition1 = this.blockPos.above();
--                    BlockState iblockdata = (BlockState) Blocks.TURTLE_EGG.defaultBlockState().setValue(TurtleEggBlock.EGGS, this.turtle.random.nextInt(4) + 1);
-+                    BlockState iblockdata = (BlockState) Blocks.TURTLE_EGG.defaultBlockState().setValue(TurtleEggBlock.EGGS, layEggEvent.getEggCount()); // Paper
- 
-                     world.setBlock(blockposition1, iblockdata, 3);
-                     world.gameEvent((Holder) GameEvent.BLOCK_PLACE, blockposition1, GameEvent.Context.of(this.turtle, iblockdata));
-+                    } // CraftBukkit
-                     this.turtle.setHasEgg(false);
-                     this.turtle.setLayingEgg(false);
-                     this.turtle.setInLoveTime(600);
-@@ -567,7 +578,7 @@
- 
-         @Override
-         public boolean canUse() {
--            return this.turtle.isBaby() ? false : (this.turtle.hasEgg() ? true : (this.turtle.getRandom().nextInt(reducedTickDelay(700)) != 0 ? false : !this.turtle.getHomePos().closerToCenterThan(this.turtle.position(), 64.0D)));
-+            return this.turtle.isBaby() ? false : (this.turtle.hasEgg() ? true : (this.turtle.getRandom().nextInt(reducedTickDelay(700)) != 0 ? false : !this.turtle.getHomePos().closerToCenterThan(this.turtle.position(), 64.0D))) && new com.destroystokyo.paper.event.entity.TurtleGoHomeEvent((org.bukkit.entity.Turtle) this.turtle.getBukkitEntity()).callEvent(); // Paper - Turtle API
-         }
- 
-         @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/WaterAnimal.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/WaterAnimal.java.patch
deleted file mode 100644
index 5089fd98c0..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/WaterAnimal.java.patch
+++ /dev/null
@@ -1,13 +0,0 @@
---- a/net/minecraft/world/entity/animal/WaterAnimal.java
-+++ b/net/minecraft/world/entity/animal/WaterAnimal.java
-@@ -70,6 +70,10 @@
-     ) {
-         int i = world.getSeaLevel();
-         int j = i - 13;
-+        // Paper start - Make water animal spawn height configurable
-+        i = world.getMinecraftWorld().paperConfig().entities.spawning.wateranimalSpawnHeight.maximum.or(i);
-+        j = world.getMinecraftWorld().paperConfig().entities.spawning.wateranimalSpawnHeight.minimum.or(j);
-+        // Paper end - Make water animal spawn height configurable
-         return pos.getY() >= j && pos.getY() <= i && world.getFluidState(pos.below()).is(FluidTags.WATER) && world.getBlockState(pos.above()).is(Blocks.WATER);
-     }
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Wolf.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Wolf.java.patch
deleted file mode 100644
index 50375e02de..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Wolf.java.patch
+++ /dev/null
@@ -1,126 +0,0 @@
---- a/net/minecraft/world/entity/animal/Wolf.java
-+++ b/net/minecraft/world/entity/animal/Wolf.java
-@@ -85,6 +85,12 @@
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.level.pathfinder.PathType;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityDamageEvent;
-+import org.bukkit.event.entity.EntityRegainHealthEvent;
-+import org.bukkit.event.entity.EntityTargetEvent;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+// CraftBukkit end
- 
- public class Wolf extends TamableAnimal implements NeutralMob, VariantHolder<Holder<WolfVariant>> {
- 
-@@ -345,8 +351,14 @@
-         if (this.isInvulnerableTo(world, source)) {
-             return false;
-         } else {
-+            // CraftBukkit start
-+            boolean result = super.hurtServer(world, source, amount);
-+            if (!result) {
-+                return result;
-+            }
-+            // CraftBukkit end
-             this.setOrderedToSit(false);
--            return super.hurtServer(world, source, amount);
-+            return result; // CraftBukkit
-         }
-     }
- 
-@@ -356,21 +368,27 @@
-     }
- 
-     @Override
--    protected void actuallyHurt(ServerLevel world, DamageSource source, float amount) {
--        if (!this.canArmorAbsorb(source)) {
--            super.actuallyHurt(world, source, amount);
-+    public boolean actuallyHurt(ServerLevel worldserver, DamageSource damagesource, float f, EntityDamageEvent event) { // CraftBukkit - void -> boolean
-+        if (!this.canArmorAbsorb(damagesource)) {
-+            return super.actuallyHurt(worldserver, damagesource, f, event); // CraftBukkit
-         } else {
-+            // CraftBukkit start - SPIGOT-7815: if the damage was cancelled, no need to run the wolf armor behaviour
-+            if (event.isCancelled()) {
-+                return false;
-+            }
-+            // CraftBukkit end
-             ItemStack itemstack = this.getBodyArmorItem();
-             int i = itemstack.getDamageValue();
-             int j = itemstack.getMaxDamage();
- 
--            itemstack.hurtAndBreak(Mth.ceil(amount), this, EquipmentSlot.BODY);
-+            itemstack.hurtAndBreak(Mth.ceil(f), this, EquipmentSlot.BODY);
-             if (Crackiness.WOLF_ARMOR.byDamage(i, j) != Crackiness.WOLF_ARMOR.byDamage(this.getBodyArmorItem())) {
-                 this.playSound(SoundEvents.WOLF_ARMOR_CRACK);
--                world.sendParticles(new ItemParticleOption(ParticleTypes.ITEM, Items.ARMADILLO_SCUTE.getDefaultInstance()), this.getX(), this.getY() + 1.0D, this.getZ(), 20, 0.2D, 0.1D, 0.2D, 0.1D);
-+                worldserver.sendParticles(new ItemParticleOption(ParticleTypes.ITEM, Items.ARMADILLO_SCUTE.getDefaultInstance()), this.getX(), this.getY() + 1.0D, this.getZ(), 20, 0.2D, 0.1D, 0.2D, 0.1D);
-             }
- 
-         }
-+        return true; // CraftBukkit // Paper - return false ONLY if event was cancelled
-     }
- 
-     private boolean canArmorAbsorb(DamageSource source) {
-@@ -381,7 +399,7 @@
-     protected void applyTamingSideEffects() {
-         if (this.isTame()) {
-             this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(40.0D);
--            this.setHealth(40.0F);
-+            this.setHealth(this.getMaxHealth()); // CraftBukkit - 40.0 -> getMaxHealth()
-         } else {
-             this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(8.0D);
-         }
-@@ -404,7 +422,7 @@
-                 FoodProperties foodinfo = (FoodProperties) itemstack.get(DataComponents.FOOD);
-                 float f = foodinfo != null ? (float) foodinfo.nutrition() : 1.0F;
- 
--                this.heal(2.0F * f);
-+                this.heal(2.0F * f, EntityRegainHealthEvent.RegainReason.EATING); // CraftBukkit
-                 return InteractionResult.SUCCESS;
-             } else {
-                 if (item instanceof DyeItem) {
-@@ -414,6 +432,14 @@
-                         DyeColor enumcolor = itemdye.getDyeColor();
- 
-                         if (enumcolor != this.getCollarColor()) {
-+                            // Paper start - Add EntityDyeEvent and CollarColorable interface
-+                            final io.papermc.paper.event.entity.EntityDyeEvent event = new io.papermc.paper.event.entity.EntityDyeEvent(this.getBukkitEntity(), org.bukkit.DyeColor.getByWoolData((byte) enumcolor.getId()), ((net.minecraft.server.level.ServerPlayer) player).getBukkitEntity());
-+                            if (!event.callEvent()) {
-+                                return InteractionResult.FAIL;
-+                            }
-+                            enumcolor = DyeColor.byId(event.getColor().getWoolData());
-+                            // Paper end - Add EntityDyeEvent and CollarColorable interface
-+
-                             this.setCollarColor(enumcolor);
-                             itemstack.consume(1, player);
-                             return InteractionResult.SUCCESS;
-@@ -440,7 +466,9 @@
-                         if (world instanceof ServerLevel) {
-                             ServerLevel worldserver = (ServerLevel) world;
- 
-+                            this.forceDrops = true; // CraftBukkit
-                             this.spawnAtLocation(worldserver, itemstack1);
-+                            this.forceDrops = false; // CraftBukkit
-                         }
- 
-                         return InteractionResult.SUCCESS;
-@@ -459,7 +487,7 @@
-                             this.setOrderedToSit(!this.isOrderedToSit());
-                             this.jumping = false;
-                             this.navigation.stop();
--                            this.setTarget((LivingEntity) null);
-+                            this.setTarget((LivingEntity) null, EntityTargetEvent.TargetReason.FORGOT_TARGET, true); // CraftBukkit - reason
-                             return InteractionResult.SUCCESS.withoutItem();
-                         } else {
-                             return enuminteractionresult;
-@@ -477,7 +505,8 @@
-     }
- 
-     private void tryToTame(Player player) {
--        if (this.random.nextInt(3) == 0) {
-+        // CraftBukkit - added event call and isCancelled check.
-+        if (this.random.nextInt(3) == 0 && !CraftEventFactory.callEntityTameEvent(this, player).isCancelled()) {
-             this.tame(player);
-             this.navigation.stop();
-             this.setTarget((LivingEntity) null);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/allay/Allay.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/allay/Allay.java.patch
deleted file mode 100644
index 8c853dc764..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/allay/Allay.java.patch
+++ /dev/null
@@ -1,102 +0,0 @@
---- a/net/minecraft/world/entity/animal/allay/Allay.java
-+++ b/net/minecraft/world/entity/animal/allay/Allay.java
-@@ -103,6 +103,7 @@
-     private float dancingAnimationTicks;
-     private float spinningAnimationTicks;
-     private float spinningAnimationTicks0;
-+    public boolean forceDancing = false; // CraftBukkit
- 
-     public Allay(EntityType<? extends Allay> type, Level world) {
-         super(type, world);
-@@ -114,6 +115,12 @@
-         this.dynamicJukeboxListener = new DynamicGameEventListener<>(new Allay.JukeboxListener(this.vibrationUser.getPositionSource(), ((GameEvent) GameEvent.JUKEBOX_PLAY.value()).notificationRadius()));
-     }
- 
-+    // CraftBukkit start
-+    public void setCanDuplicate(boolean canDuplicate) {
-+        this.entityData.set(Allay.DATA_CAN_DUPLICATE, canDuplicate);
-+    }
-+    // CraftBukkit end
-+
-     @Override
-     protected Brain.Provider<Allay> brainProvider() {
-         return Brain.provider(Allay.MEMORY_TYPES, Allay.SENSOR_TYPES);
-@@ -126,7 +133,7 @@
- 
-     @Override
-     public Brain<Allay> getBrain() {
--        return super.getBrain();
-+        return (Brain<Allay>) super.getBrain(); // CraftBukkit - decompile error
-     }
- 
-     public static AttributeSupplier.Builder createAttributes() {
-@@ -233,7 +240,7 @@
-     public void aiStep() {
-         super.aiStep();
-         if (!this.level().isClientSide && this.isAlive() && this.tickCount % 10 == 0) {
--            this.heal(1.0F);
-+            this.heal(1.0F, org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.REGEN); // CraftBukkit
-         }
- 
-         if (this.isDancing() && this.shouldStopDancing() && this.tickCount % 20 == 0) {
-@@ -303,7 +310,12 @@
-         ItemStack itemstack1 = this.getItemInHand(InteractionHand.MAIN_HAND);
- 
-         if (this.isDancing() && itemstack.is(ItemTags.DUPLICATES_ALLAYS) && this.canDuplicate()) {
--            this.duplicateAllay();
-+            // CraftBukkit start - handle cancel duplication
-+            Allay allay = this.duplicateAllay();
-+            if (allay == null) {
-+                return InteractionResult.SUCCESS;
-+            }
-+            // CraftBukkit end
-             this.level().broadcastEntityEvent(this, (byte) 18);
-             this.level().playSound(player, (Entity) this, SoundEvents.AMETHYST_BLOCK_CHIME, SoundSource.NEUTRAL, 2.0F, 1.0F);
-             this.removeInteractionItem(player, itemstack);
-@@ -314,7 +326,7 @@
-             this.setItemInHand(InteractionHand.MAIN_HAND, itemstack2);
-             this.removeInteractionItem(player, itemstack);
-             this.level().playSound(player, (Entity) this, SoundEvents.ALLAY_ITEM_GIVEN, SoundSource.NEUTRAL, 2.0F, 1.0F);
--            this.getBrain().setMemory(MemoryModuleType.LIKED_PLAYER, (Object) player.getUUID());
-+            this.getBrain().setMemory(MemoryModuleType.LIKED_PLAYER, player.getUUID()); // CraftBukkit - decompile error
-             return InteractionResult.SUCCESS;
-         } else if (!itemstack1.isEmpty() && hand == InteractionHand.MAIN_HAND && itemstack.isEmpty()) {
-             this.setItemSlot(EquipmentSlot.MAINHAND, ItemStack.EMPTY);
-@@ -415,6 +427,7 @@
-     }
- 
-     private boolean shouldStopDancing() {
-+        if (this.forceDancing) {return false;} // CraftBukkit
-         return this.jukeboxPos == null || !this.jukeboxPos.closerToCenterThan(this.position(), (double) ((GameEvent) GameEvent.JUKEBOX_PLAY.value()).notificationRadius()) || !this.level().getBlockState(this.jukeboxPos).is(Blocks.JUKEBOX);
-     }
- 
-@@ -486,7 +499,7 @@
-             });
-         }
- 
--        this.duplicationCooldown = (long) nbt.getInt("DuplicationCooldown");
-+        this.duplicationCooldown = nbt.getLong("DuplicationCooldown"); // Paper - Load as long
-         this.entityData.set(Allay.DATA_CAN_DUPLICATE, nbt.getBoolean("CanDuplicate"));
-     }
- 
-@@ -506,7 +519,7 @@
- 
-     }
- 
--    public void duplicateAllay() {
-+    public Allay duplicateAllay() { // CraftBukkit - return allay
-         Allay allay = (Allay) EntityType.ALLAY.create(this.level(), EntitySpawnReason.BREEDING);
- 
-         if (allay != null) {
-@@ -514,9 +527,9 @@
-             allay.setPersistenceRequired();
-             allay.resetDuplicationCooldown();
-             this.resetDuplicationCooldown();
--            this.level().addFreshEntity(allay);
-+            this.level().addFreshEntity(allay, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DUPLICATION); // CraftBukkit - reason for duplicated allay
-         }
--
-+        return allay; // CraftBukkit
-     }
- 
-     public void resetDuplicationCooldown() {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/armadillo/Armadillo.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/armadillo/Armadillo.java.patch
deleted file mode 100644
index 2e72adb5ea..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/armadillo/Armadillo.java.patch
+++ /dev/null
@@ -1,81 +0,0 @@
---- a/net/minecraft/world/entity/animal/armadillo/Armadillo.java
-+++ b/net/minecraft/world/entity/animal/armadillo/Armadillo.java
-@@ -47,6 +47,9 @@
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.level.storage.loot.BuiltInLootTables;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityDamageEvent;
-+// CraftBukkit end
- 
- public class Armadillo extends Animal {
- 
-@@ -135,16 +138,18 @@
-         ProfilerFiller gameprofilerfiller = Profiler.get();
- 
-         gameprofilerfiller.push("armadilloBrain");
--        this.brain.tick(world, this);
-+        ((Brain<Armadillo>) this.brain).tick(world, this); // CraftBukkit - decompile error
-         gameprofilerfiller.pop();
-         gameprofilerfiller.push("armadilloActivityUpdate");
-         ArmadilloAi.updateActivity(this);
-         gameprofilerfiller.pop();
-         if (this.isAlive() && !this.isBaby() && --this.scuteTime <= 0) {
-+            this.forceDrops = true; // CraftBukkit
-             if (this.dropFromGiftLootTable(world, BuiltInLootTables.ARMADILLO_SHED, this::spawnAtLocation)) {
-                 this.playSound(SoundEvents.ARMADILLO_SCUTE_DROP, 1.0F, (this.random.nextFloat() - this.random.nextFloat()) * 0.2F + 1.0F);
-                 this.gameEvent(GameEvent.ENTITY_PLACE);
-             }
-+            this.forceDrops = false; // CraftBukkit
- 
-             this.scuteTime = this.pickNextScuteDropTime();
-         }
-@@ -291,19 +296,25 @@
-     }
- 
-     @Override
--    protected void actuallyHurt(ServerLevel world, DamageSource source, float amount) {
--        super.actuallyHurt(world, source, amount);
-+    // CraftBukkit start - void -> boolean
-+    public boolean actuallyHurt(ServerLevel worldserver, DamageSource damagesource, float f, EntityDamageEvent event) {
-+        boolean damageResult = super.actuallyHurt(worldserver, damagesource, f, event);
-+        if (!damageResult) {
-+            return false;
-+        }
-+        // CraftBukkit end
-         if (!this.isNoAi() && !this.isDeadOrDying()) {
--            if (source.getEntity() instanceof LivingEntity) {
-+            if (damagesource.getEntity() instanceof LivingEntity) {
-                 this.getBrain().setMemoryWithExpiry(MemoryModuleType.DANGER_DETECTED_RECENTLY, true, 80L);
-                 if (this.canStayRolledUp()) {
-                     this.rollUp();
-                 }
--            } else if (source.is(DamageTypeTags.PANIC_ENVIRONMENTAL_CAUSES)) {
-+            } else if (damagesource.is(DamageTypeTags.PANIC_ENVIRONMENTAL_CAUSES)) {
-                 this.rollOut();
-             }
- 
-         }
-+        return true; // CraftBukkit
-     }
- 
-     @Override
-@@ -327,7 +338,9 @@
-             if (world instanceof ServerLevel) {
-                 ServerLevel worldserver = (ServerLevel) world;
- 
-+                this.forceDrops = true; // CraftBukkit
-                 this.spawnAtLocation(worldserver, new ItemStack(Items.ARMADILLO_SCUTE));
-+                this.forceDrops = false; // CraftBukkit
-                 this.gameEvent(GameEvent.ENTITY_INTERACT);
-                 this.playSound(SoundEvents.ARMADILLO_BRUSH);
-             }
-@@ -431,7 +444,7 @@
-         }
- 
-         public static Armadillo.ArmadilloState fromName(String name) {
--            return (Armadillo.ArmadilloState) Armadillo.ArmadilloState.CODEC.byName(name, (Enum) Armadillo.ArmadilloState.IDLE);
-+            return (Armadillo.ArmadilloState) Armadillo.ArmadilloState.CODEC.byName(name, Armadillo.ArmadilloState.IDLE); // CraftBukkit - decompile error
-         }
- 
-         @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/axolotl/Axolotl.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/axolotl/Axolotl.java.patch
deleted file mode 100644
index 3923bd7b5c..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/axolotl/Axolotl.java.patch
+++ /dev/null
@@ -1,52 +0,0 @@
---- a/net/minecraft/world/entity/animal/axolotl/Axolotl.java
-+++ b/net/minecraft/world/entity/animal/axolotl/Axolotl.java
-@@ -67,10 +67,17 @@
- 
- public class Axolotl extends Animal implements VariantHolder<Axolotl.Variant>, Bucketable {
- 
-+    // CraftBukkit start - SPIGOT-6907: re-implement LivingEntity#setMaximumAir()
-+    @Override
-+    public int getDefaultMaxAirSupply() {
-+        return Axolotl.AXOLOTL_TOTAL_AIR_SUPPLY;
-+    }
-+    // CraftBukkit end
-     public static final int TOTAL_PLAYDEAD_TIME = 200;
-     private static final int POSE_ANIMATION_TICKS = 10;
-     protected static final ImmutableList<? extends SensorType<? extends Sensor<? super Axolotl>>> SENSOR_TYPES = ImmutableList.of(SensorType.NEAREST_LIVING_ENTITIES, SensorType.NEAREST_ADULT, SensorType.HURT_BY, SensorType.AXOLOTL_ATTACKABLES, SensorType.AXOLOTL_TEMPTATIONS);
--    protected static final ImmutableList<? extends MemoryModuleType<?>> MEMORY_TYPES = ImmutableList.of(MemoryModuleType.BREED_TARGET, MemoryModuleType.NEAREST_LIVING_ENTITIES, MemoryModuleType.NEAREST_VISIBLE_LIVING_ENTITIES, MemoryModuleType.NEAREST_VISIBLE_PLAYER, MemoryModuleType.NEAREST_VISIBLE_ATTACKABLE_PLAYER, MemoryModuleType.LOOK_TARGET, MemoryModuleType.WALK_TARGET, MemoryModuleType.CANT_REACH_WALK_TARGET_SINCE, MemoryModuleType.PATH, MemoryModuleType.ATTACK_TARGET, MemoryModuleType.ATTACK_COOLING_DOWN, MemoryModuleType.NEAREST_VISIBLE_ADULT, new MemoryModuleType[]{MemoryModuleType.HURT_BY_ENTITY, MemoryModuleType.PLAY_DEAD_TICKS, MemoryModuleType.NEAREST_ATTACKABLE, MemoryModuleType.TEMPTING_PLAYER, MemoryModuleType.TEMPTATION_COOLDOWN_TICKS, MemoryModuleType.IS_TEMPTED, MemoryModuleType.HAS_HUNTING_COOLDOWN, MemoryModuleType.IS_PANICKING});
-+    // CraftBukkit - decompile error
-+    protected static final ImmutableList<? extends MemoryModuleType<?>> MEMORY_TYPES = ImmutableList.<MemoryModuleType<?>>of(MemoryModuleType.BREED_TARGET, MemoryModuleType.NEAREST_LIVING_ENTITIES, MemoryModuleType.NEAREST_VISIBLE_LIVING_ENTITIES, MemoryModuleType.NEAREST_VISIBLE_PLAYER, MemoryModuleType.NEAREST_VISIBLE_ATTACKABLE_PLAYER, MemoryModuleType.LOOK_TARGET, MemoryModuleType.WALK_TARGET, MemoryModuleType.CANT_REACH_WALK_TARGET_SINCE, MemoryModuleType.PATH, MemoryModuleType.ATTACK_TARGET, MemoryModuleType.ATTACK_COOLING_DOWN, MemoryModuleType.NEAREST_VISIBLE_ADULT, new MemoryModuleType[]{MemoryModuleType.HURT_BY_ENTITY, MemoryModuleType.PLAY_DEAD_TICKS, MemoryModuleType.NEAREST_ATTACKABLE, MemoryModuleType.TEMPTING_PLAYER, MemoryModuleType.TEMPTATION_COOLDOWN_TICKS, MemoryModuleType.IS_TEMPTED, MemoryModuleType.HAS_HUNTING_COOLDOWN, MemoryModuleType.IS_PANICKING});
-     private static final EntityDataAccessor<Integer> DATA_VARIANT = SynchedEntityData.defineId(Axolotl.class, EntityDataSerializers.INT);
-     private static final EntityDataAccessor<Boolean> DATA_PLAYING_DEAD = SynchedEntityData.defineId(Axolotl.class, EntityDataSerializers.BOOLEAN);
-     private static final EntityDataAccessor<Boolean> FROM_BUCKET = SynchedEntityData.defineId(Axolotl.class, EntityDataSerializers.BOOLEAN);
-@@ -210,7 +217,7 @@
- 
-     @Override
-     public int getMaxAirSupply() {
--        return 6000;
-+        return this.maxAirTicks; // CraftBukkit - SPIGOT-6907: re-implement LivingEntity#setMaximumAir()
-     }
- 
-     @Override
-@@ -414,10 +421,10 @@
-             int i = mobeffect != null ? mobeffect.getDuration() : 0;
-             int j = Math.min(2400, 100 + i);
- 
--            player.addEffect(new MobEffectInstance(MobEffects.REGENERATION, j, 0), this);
-+            player.addEffect(new MobEffectInstance(MobEffects.REGENERATION, j, 0), this, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.AXOLOTL); // CraftBukkit
-         }
- 
--        player.removeEffect(MobEffects.DIG_SLOWDOWN);
-+        player.removeEffect(MobEffects.DIG_SLOWDOWN, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.AXOLOTL); // Paper - Add missing effect cause
-     }
- 
-     @Override
-@@ -464,7 +471,7 @@
- 
-     @Override
-     public Brain<Axolotl> getBrain() {
--        return super.getBrain();
-+        return (Brain<Axolotl>) super.getBrain(); // CraftBukkit - decompile error
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/frog/ShootTongue.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/frog/ShootTongue.java.patch
deleted file mode 100644
index 354d8a45c5..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/frog/ShootTongue.java.patch
+++ /dev/null
@@ -1,39 +0,0 @@
---- a/net/minecraft/world/entity/animal/frog/ShootTongue.java
-+++ b/net/minecraft/world/entity/animal/frog/ShootTongue.java
-@@ -19,6 +19,9 @@
- import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.level.pathfinder.Path;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public class ShootTongue extends Behavior<Frog> {
- 
-@@ -64,7 +67,7 @@
- 
-         BehaviorUtils.lookAtEntity(frog, entityliving);
-         frog.setTongueTarget(entityliving);
--        frog.getBrain().setMemory(MemoryModuleType.WALK_TARGET, (Object) (new WalkTarget(entityliving.position(), 2.0F, 0)));
-+        frog.getBrain().setMemory(MemoryModuleType.WALK_TARGET, (new WalkTarget(entityliving.position(), 2.0F, 0))); // CraftBukkit - decompile error
-         this.calculatePathCounter = 10;
-         this.state = ShootTongue.State.MOVE_TO_TARGET;
-     }
-@@ -85,7 +88,7 @@
-             if (entity.isAlive()) {
-                 frog.doHurtTarget(world, entity);
-                 if (!entity.isAlive()) {
--                    entity.remove(Entity.RemovalReason.KILLED);
-+                    entity.remove(Entity.RemovalReason.KILLED, EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause
-                 }
-             }
-         }
-@@ -106,7 +109,7 @@
-                     this.eatAnimationTimer = 0;
-                     this.state = ShootTongue.State.CATCH_ANIMATION;
-                 } else if (this.calculatePathCounter <= 0) {
--                    frog.getBrain().setMemory(MemoryModuleType.WALK_TARGET, (Object) (new WalkTarget(entityliving.position(), 2.0F, 0)));
-+                    frog.getBrain().setMemory(MemoryModuleType.WALK_TARGET, (new WalkTarget(entityliving.position(), 2.0F, 0))); // CraftBukkit - decompile error
-                     this.calculatePathCounter = 10;
-                 } else {
-                     --this.calculatePathCounter;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/frog/Tadpole.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/frog/Tadpole.java.patch
deleted file mode 100644
index 636405ea80..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/frog/Tadpole.java.patch
+++ /dev/null
@@ -1,87 +0,0 @@
---- a/net/minecraft/world/entity/animal/frog/Tadpole.java
-+++ b/net/minecraft/world/entity/animal/frog/Tadpole.java
-@@ -50,6 +50,7 @@
-     public int age;
-     protected static final ImmutableList<SensorType<? extends Sensor<? super Tadpole>>> SENSOR_TYPES = ImmutableList.of(SensorType.NEAREST_LIVING_ENTITIES, SensorType.NEAREST_PLAYERS, SensorType.HURT_BY, SensorType.FROG_TEMPTATIONS);
-     protected static final ImmutableList<MemoryModuleType<?>> MEMORY_TYPES = ImmutableList.of(MemoryModuleType.LOOK_TARGET, MemoryModuleType.NEAREST_VISIBLE_LIVING_ENTITIES, MemoryModuleType.WALK_TARGET, MemoryModuleType.CANT_REACH_WALK_TARGET_SINCE, MemoryModuleType.PATH, MemoryModuleType.NEAREST_VISIBLE_ADULT, MemoryModuleType.TEMPTATION_COOLDOWN_TICKS, MemoryModuleType.IS_TEMPTED, MemoryModuleType.TEMPTING_PLAYER, MemoryModuleType.BREED_TARGET, MemoryModuleType.IS_PANICKING);
-+    public boolean ageLocked; // Paper
- 
-     public Tadpole(EntityType<? extends AbstractFish> type, Level world) {
-         super(type, world);
-@@ -74,7 +75,7 @@
- 
-     @Override
-     public Brain<Tadpole> getBrain() {
--        return super.getBrain();
-+        return (Brain<Tadpole>) super.getBrain(); // CraftBukkit - decompile error
-     }
- 
-     @Override
-@@ -102,7 +103,7 @@
-     @Override
-     public void aiStep() {
-         super.aiStep();
--        if (!this.level().isClientSide) {
-+        if (!this.level().isClientSide && !this.ageLocked) { // Paper
-             this.setAge(this.age + 1);
-         }
- 
-@@ -112,12 +113,14 @@
-     public void addAdditionalSaveData(CompoundTag nbt) {
-         super.addAdditionalSaveData(nbt);
-         nbt.putInt("Age", this.age);
-+        nbt.putBoolean("AgeLocked", this.ageLocked); // Paper
-     }
- 
-     @Override
-     public void readAdditionalSaveData(CompoundTag nbt) {
-         super.readAdditionalSaveData(nbt);
-         this.setAge(nbt.getInt("Age"));
-+        this.ageLocked = nbt.getBoolean("AgeLocked"); // Paper
-     }
- 
-     @Nullable
-@@ -169,6 +172,7 @@
-         Bucketable.saveDefaultDataToBucketTag(this, stack);
-         CustomData.update(DataComponents.BUCKET_ENTITY_DATA, stack, (nbttagcompound) -> {
-             nbttagcompound.putInt("Age", this.getAge());
-+            nbttagcompound.putBoolean("AgeLocked", this.ageLocked); // Paper
-         });
-     }
- 
-@@ -179,6 +183,7 @@
-             this.setAge(nbt.getInt("Age"));
-         }
- 
-+        this.ageLocked = nbt.getBoolean("AgeLocked"); // Paper
-     }
- 
-     @Override
-@@ -210,6 +215,7 @@
-     }
- 
-     private void ageUp(int seconds) {
-+        if (this.ageLocked) return; // Paper
-         this.setAge(this.age + seconds * 20);
-     }
- 
-@@ -225,12 +231,17 @@
-         Level world = this.level();
- 
-         if (world instanceof ServerLevel worldserver) {
--            this.convertTo(EntityType.FROG, ConversionParams.single(this, false, false), (frog) -> {
-+            Frog converted = this.convertTo(EntityType.FROG, ConversionParams.single(this, false, false), (frog) -> { // CraftBukkit
-                 frog.finalizeSpawn(worldserver, this.level().getCurrentDifficultyAt(frog.blockPosition()), EntitySpawnReason.CONVERSION, (SpawnGroupData) null);
-                 frog.setPersistenceRequired();
-                 frog.fudgePositionAfterSizeChange(this.getDimensions(this.getPose()));
-                 this.playSound(SoundEvents.TADPOLE_GROW_UP, 0.15F, 1.0F);
--            });
-+            // CraftBukkit start
-+            }, org.bukkit.event.entity.EntityTransformEvent.TransformReason.METAMORPHOSIS, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.METAMORPHOSIS);
-+            if (converted == null) {
-+                this.setAge(0); // Sets the age to 0 for avoid a loop if the event is canceled
-+            }
-+            // CraftBukkit end
-         }
- 
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/goat/Goat.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/goat/Goat.java.patch
deleted file mode 100644
index 44290161de..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/goat/Goat.java.patch
+++ /dev/null
@@ -1,76 +0,0 @@
---- a/net/minecraft/world/entity/animal/goat/Goat.java
-+++ b/net/minecraft/world/entity/animal/goat/Goat.java
-@@ -53,6 +53,11 @@
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.pathfinder.PathType;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.event.player.PlayerBucketFillEvent;
-+// CraftBukkit end
- 
- public class Goat extends Animal {
- 
-@@ -184,7 +189,7 @@
- 
-     @Override
-     public Brain<Goat> getBrain() {
--        return super.getBrain();
-+        return (Brain<Goat>) super.getBrain(); // CraftBukkit - decompile error
-     }
- 
-     @Override
-@@ -229,15 +234,24 @@
-         ItemStack itemstack = player.getItemInHand(hand);
- 
-         if (itemstack.is(Items.BUCKET) && !this.isBaby()) {
-+            // CraftBukkit start - Got milk?
-+            PlayerBucketFillEvent event = CraftEventFactory.callPlayerBucketFillEvent((ServerLevel) player.level(), player, this.blockPosition(), this.blockPosition(), null, itemstack, Items.MILK_BUCKET, hand);
-+
-+            if (event.isCancelled()) {
-+                player.containerMenu.sendAllDataToRemote(); // Paper - Fix inventory desync
-+                return InteractionResult.PASS;
-+            }
-+            // CraftBukkit end
-             player.playSound(this.getMilkingSound(), 1.0F, 1.0F);
--            ItemStack itemstack1 = ItemUtils.createFilledResult(itemstack, player, Items.MILK_BUCKET.getDefaultInstance());
-+            ItemStack itemstack1 = ItemUtils.createFilledResult(itemstack, player, CraftItemStack.asNMSCopy(event.getItemStack())); // CraftBukkit
- 
-             player.setItemInHand(hand, itemstack1);
-             return InteractionResult.SUCCESS;
-         } else {
-+            boolean isFood = this.isFood(itemstack); // Paper - track before stack is possibly decreased to 0 (Fixes MC-244739)
-             InteractionResult enuminteractionresult = super.mobInteract(player, hand);
- 
--            if (enuminteractionresult.consumesAction() && this.isFood(itemstack)) {
-+            if (enuminteractionresult.consumesAction() && isFood) { // Paper
-                 this.playEatingSound();
-             }
- 
-@@ -353,8 +367,7 @@
-             double d2 = (double) Mth.randomBetween(this.random, -0.2F, 0.2F);
-             ItemEntity entityitem = new ItemEntity(this.level(), vec3d.x(), vec3d.y(), vec3d.z(), itemstack, d0, d1, d2);
- 
--            this.level().addFreshEntity(entityitem);
--            return true;
-+            return this.spawnAtLocation((net.minecraft.server.level.ServerLevel) this.level(), entityitem) != null; // Paper - Call EntityDropItemEvent
-         }
-     }
- 
-@@ -383,4 +396,15 @@
-     public static boolean checkGoatSpawnRules(EntityType<? extends Animal> entityType, LevelAccessor world, EntitySpawnReason spawnReason, BlockPos pos, RandomSource random) {
-         return world.getBlockState(pos.below()).is(BlockTags.GOATS_SPAWNABLE_ON) && isBrightEnoughToSpawn(world, pos);
-     }
-+
-+    // Paper start - Goat ram API
-+    public void ram(net.minecraft.world.entity.LivingEntity entity) {
-+        Brain<Goat> brain = this.getBrain();
-+        brain.setMemory(MemoryModuleType.RAM_TARGET, entity.position());
-+        brain.eraseMemory(MemoryModuleType.RAM_COOLDOWN_TICKS);
-+        brain.eraseMemory(MemoryModuleType.BREED_TARGET);
-+        brain.eraseMemory(MemoryModuleType.TEMPTING_PLAYER);
-+        brain.setActiveActivityIfPossible(net.minecraft.world.entity.schedule.Activity.RAM);
-+    }
-+    // Paper end - Goat ram API
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/AbstractChestedHorse.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/AbstractChestedHorse.java.patch
deleted file mode 100644
index 78095235f3..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/AbstractChestedHorse.java.patch
+++ /dev/null
@@ -1,19 +0,0 @@
---- a/net/minecraft/world/entity/animal/horse/AbstractChestedHorse.java
-+++ b/net/minecraft/world/entity/animal/horse/AbstractChestedHorse.java
-@@ -69,9 +69,16 @@
-         super.dropEquipment(world);
-         if (this.hasChest()) {
-             this.spawnAtLocation(world, Blocks.CHEST);
-+            //this.setChest(false); // Paper - moved to post death logic
-+        }
-+    }
-+    // Paper start
-+    protected void postDeathDropItems(org.bukkit.event.entity.EntityDeathEvent event) {
-+        if (this.hasChest() && (event == null || !event.isCancelled())) {
-             this.setChest(false);
-         }
-     }
-+    // Paper end
- 
-     @Override
-     public void addAdditionalSaveData(CompoundTag nbt) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/SkeletonHorse.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/SkeletonHorse.java.patch
deleted file mode 100644
index a0ae868b36..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/SkeletonHorse.java.patch
+++ /dev/null
@@ -1,21 +0,0 @@
---- a/net/minecraft/world/entity/animal/horse/SkeletonHorse.java
-+++ b/net/minecraft/world/entity/animal/horse/SkeletonHorse.java
-@@ -26,6 +26,9 @@
- import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.LevelAccessor;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public class SkeletonHorse extends AbstractHorse {
- 
-@@ -122,7 +125,7 @@
-     public void aiStep() {
-         super.aiStep();
-         if (this.isTrap() && this.trapTime++ >= 18000) {
--            this.discard();
-+            this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-         }
- 
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/SkeletonTrapGoal.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/SkeletonTrapGoal.java.patch
deleted file mode 100644
index 8f22bd7fd2..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/SkeletonTrapGoal.java.patch
+++ /dev/null
@@ -1,49 +0,0 @@
---- a/net/minecraft/world/entity/animal/horse/SkeletonTrapGoal.java
-+++ b/net/minecraft/world/entity/animal/horse/SkeletonTrapGoal.java
-@@ -20,6 +20,7 @@
- public class SkeletonTrapGoal extends Goal {
- 
-     private final SkeletonHorse horse;
-+    private java.util.List<org.bukkit.entity.HumanEntity> eligiblePlayers; // Paper
- 
-     public SkeletonTrapGoal(SkeletonHorse skeletonHorse) {
-         this.horse = skeletonHorse;
-@@ -27,12 +28,13 @@
- 
-     @Override
-     public boolean canUse() {
--        return this.horse.level().hasNearbyAlivePlayer(this.horse.getX(), this.horse.getY(), this.horse.getZ(), 10.0D);
-+        return !(eligiblePlayers = this.horse.level().findNearbyBukkitPlayers(this.horse.getX(), this.horse.getY(), this.horse.getZ(), 10.0D, net.minecraft.world.entity.EntitySelector.PLAYER_AFFECTS_SPAWNING)).isEmpty(); // Paper - Affects Spawning API & SkeletonHorseTrapEvent
-     }
- 
-     @Override
-     public void tick() {
-         ServerLevel worldserver = (ServerLevel) this.horse.level();
-+        if (!new com.destroystokyo.paper.event.entity.SkeletonHorseTrapEvent((org.bukkit.entity.SkeletonHorse) this.horse.getBukkitEntity(), eligiblePlayers).callEvent()) return; // Paper
-         DifficultyInstance difficultydamagescaler = worldserver.getCurrentDifficultyAt(this.horse.blockPosition());
- 
-         this.horse.setTrap(false);
-@@ -43,12 +45,12 @@
-         if (entitylightning != null) {
-             entitylightning.moveTo(this.horse.getX(), this.horse.getY(), this.horse.getZ());
-             entitylightning.setVisualOnly(true);
--            worldserver.addFreshEntity(entitylightning);
-+            worldserver.strikeLightning(entitylightning, org.bukkit.event.weather.LightningStrikeEvent.Cause.TRAP); // CraftBukkit
-             Skeleton entityskeleton = this.createSkeleton(difficultydamagescaler, this.horse);
- 
-             if (entityskeleton != null) {
-                 entityskeleton.startRiding(this.horse);
--                worldserver.addFreshEntityWithPassengers(entityskeleton);
-+                worldserver.addFreshEntityWithPassengers(entityskeleton, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.TRAP); // CraftBukkit
- 
-                 for (int i = 0; i < 3; ++i) {
-                     AbstractHorse entityhorseabstract = this.createHorse(difficultydamagescaler);
-@@ -59,7 +61,7 @@
-                         if (entityskeleton1 != null) {
-                             entityskeleton1.startRiding(entityhorseabstract);
-                             entityhorseabstract.push(this.horse.getRandom().triangle(0.0D, 1.1485D), 0.0D, this.horse.getRandom().triangle(0.0D, 1.1485D));
--                            worldserver.addFreshEntityWithPassengers(entityhorseabstract);
-+                            worldserver.addFreshEntityWithPassengers(entityhorseabstract, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.JOCKEY); // CraftBukkit
-                         }
-                     }
-                 }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/TraderLlama.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/TraderLlama.java.patch
deleted file mode 100644
index 1edf3cd1bd..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/TraderLlama.java.patch
+++ /dev/null
@@ -1,30 +0,0 @@
---- a/net/minecraft/world/entity/animal/horse/TraderLlama.java
-+++ b/net/minecraft/world/entity/animal/horse/TraderLlama.java
-@@ -21,6 +21,9 @@
- import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.ServerLevelAccessor;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public class TraderLlama extends Llama {
- 
-@@ -94,7 +97,7 @@
-             this.despawnDelay = this.isLeashedToWanderingTrader() ? ((WanderingTrader) this.getLeashHolder()).getDespawnDelay() - 1 : this.despawnDelay - 1;
-             if (this.despawnDelay <= 0) {
-                 this.removeLeash();
--                this.discard();
-+                this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-             }
- 
-         }
-@@ -160,7 +163,7 @@
- 
-         @Override
-         public void start() {
--            this.mob.setTarget(this.ownerLastHurtBy);
-+            this.mob.setTarget(this.ownerLastHurtBy, org.bukkit.event.entity.EntityTargetEvent.TargetReason.TARGET_ATTACKED_OWNER, true); // CraftBukkit
-             Entity entity = this.llama.getLeashHolder();
- 
-             if (entity instanceof WanderingTrader) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/sniffer/Sniffer.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/sniffer/Sniffer.java.patch
deleted file mode 100644
index a6567df470..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/sniffer/Sniffer.java.patch
+++ /dev/null
@@ -1,56 +0,0 @@
---- a/net/minecraft/world/entity/animal/sniffer/Sniffer.java
-+++ b/net/minecraft/world/entity/animal/sniffer/Sniffer.java
-@@ -267,6 +267,13 @@
-                 this.dropFromGiftLootTable(worldserver, BuiltInLootTables.SNIFFER_DIGGING, (worldserver1, itemstack) -> {
-                     ItemEntity entityitem = new ItemEntity(this.level(), (double) blockposition.getX(), (double) blockposition.getY(), (double) blockposition.getZ(), itemstack);
- 
-+                    // CraftBukkit start - handle EntityDropItemEvent
-+                    org.bukkit.event.entity.EntityDropItemEvent event = new org.bukkit.event.entity.EntityDropItemEvent(this.getBukkitEntity(), (org.bukkit.entity.Item) entityitem.getBukkitEntity());
-+                    org.bukkit.Bukkit.getPluginManager().callEvent(event);
-+                    if (event.isCancelled()) {
-+                        return;
-+                    }
-+                    // CraftBukkit end
-                     entityitem.setDefaultPickUpDelay();
-                     worldserver1.addFreshEntity(entityitem);
-                 });
-@@ -308,7 +315,7 @@
-         List<GlobalPos> list = (List) this.getExploredPositions().limit(20L).collect(Collectors.toList());
- 
-         list.add(0, GlobalPos.of(this.level().dimension(), pos));
--        this.getBrain().setMemory(MemoryModuleType.SNIFFER_EXPLORED_POSITIONS, (Object) list);
-+        this.getBrain().setMemory(MemoryModuleType.SNIFFER_EXPLORED_POSITIONS, list); // CraftBukkit - decompile error
-         return this;
-     }
- 
-@@ -333,13 +340,19 @@
- 
-     @Override
-     public void spawnChildFromBreeding(ServerLevel world, Animal other) {
-+        // Paper start - Add EntityFertilizeEggEvent event
-+        final io.papermc.paper.event.entity.EntityFertilizeEggEvent result = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityFertilizeEggEvent(this, other);
-+        if (result.isCancelled()) return;
-+        // Paper end - Add EntityFertilizeEggEvent event
-+
-         ItemStack itemstack = new ItemStack(Items.SNIFFER_EGG);
-         ItemEntity entityitem = new ItemEntity(world, this.position().x(), this.position().y(), this.position().z(), itemstack);
- 
-         entityitem.setDefaultPickUpDelay();
--        this.finalizeSpawnChildFromBreeding(world, other, (AgeableMob) null);
-+        this.finalizeSpawnChildFromBreeding(world, other, (AgeableMob) null, result.getExperience()); // Paper - Add EntityFertilizeEggEvent event
-+        if (this.spawnAtLocation(world, entityitem) != null) { // Paper - Call EntityDropItemEvent
-         this.playSound(SoundEvents.SNIFFER_EGG_PLOP, 1.0F, (this.random.nextFloat() - this.random.nextFloat()) * 0.2F + 0.5F);
--        world.addFreshEntity(entityitem);
-+        } // Paper - Call EntityDropItemEvent
-     }
- 
-     @Override
-@@ -444,7 +457,7 @@
- 
-     @Override
-     public Brain<Sniffer> getBrain() {
--        return super.getBrain();
-+        return (Brain<Sniffer>) super.getBrain(); // CraftBukkit - decompile error
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java.patch
deleted file mode 100644
index 9f6803d4ee..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java.patch
+++ /dev/null
@@ -1,91 +0,0 @@
---- a/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java
-+++ b/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java
-@@ -19,12 +19,18 @@
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.block.BaseFireBlock;
- import net.minecraft.world.level.dimension.end.EndDragonFight;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.event.entity.ExplosionPrimeEvent;
-+// CraftBukkit end
- 
- public class EndCrystal extends Entity {
- 
-     private static final EntityDataAccessor<Optional<BlockPos>> DATA_BEAM_TARGET = SynchedEntityData.defineId(EndCrystal.class, EntityDataSerializers.OPTIONAL_BLOCK_POS);
-     private static final EntityDataAccessor<Boolean> DATA_SHOW_BOTTOM = SynchedEntityData.defineId(EndCrystal.class, EntityDataSerializers.BOOLEAN);
-     public int time;
-+    public boolean generatedByDragonFight = false; // Paper - Fix invulnerable end crystals
- 
-     public EndCrystal(EntityType<? extends EndCrystal> type, Level world) {
-         super(type, world);
-@@ -57,8 +63,23 @@
-             BlockPos blockposition = this.blockPosition();
- 
-             if (((ServerLevel) this.level()).getDragonFight() != null && this.level().getBlockState(blockposition).isAir()) {
--                this.level().setBlockAndUpdate(blockposition, BaseFireBlock.getState(this.level(), blockposition));
-+                // CraftBukkit start
-+                if (!CraftEventFactory.callBlockIgniteEvent(this.level(), blockposition, this).isCancelled()) {
-+                    this.level().setBlockAndUpdate(blockposition, BaseFireBlock.getState(this.level(), blockposition));
-+                }
-+                // CraftBukkit end
-             }
-+            // Paper start - Fix invulnerable end crystals
-+            if (this.level().paperConfig().unsupportedSettings.fixInvulnerableEndCrystalExploit && this.generatedByDragonFight && this.isInvulnerable()) {
-+                if (!java.util.Objects.equals(((ServerLevel) this.level()).uuid, this.getOriginWorld())
-+                    || ((ServerLevel) this.level()).getDragonFight() == null
-+                    || ((ServerLevel) this.level()).getDragonFight().respawnStage == null
-+                    || ((ServerLevel) this.level()).getDragonFight().respawnStage.ordinal() > net.minecraft.world.level.dimension.end.DragonRespawnAnimation.SUMMONING_DRAGON.ordinal()) {
-+                    this.setInvulnerable(false);
-+                    this.setBeamTarget(null);
-+                }
-+            }
-+            // Paper end - Fix invulnerable end crystals
-         }
- 
-     }
-@@ -70,6 +91,7 @@
-         }
- 
-         nbt.putBoolean("ShowBottom", this.showsBottom());
-+        if (this.generatedByDragonFight) nbt.putBoolean("Paper.GeneratedByDragonFight", this.generatedByDragonFight); // Paper - Fix invulnerable end crystals
-     }
- 
-     @Override
-@@ -78,6 +100,7 @@
-         if (nbt.contains("ShowBottom", 1)) {
-             this.setShowBottom(nbt.getBoolean("ShowBottom"));
-         }
-+        if (nbt.contains("Paper.GeneratedByDragonFight", 1)) this.generatedByDragonFight = nbt.getBoolean("Paper.GeneratedByDragonFight"); // Paper - Fix invulnerable end crystals
- 
-     }
- 
-@@ -99,12 +122,26 @@
-             return false;
-         } else {
-             if (!this.isRemoved()) {
--                this.remove(Entity.RemovalReason.KILLED);
-+                // CraftBukkit start - All non-living entities need this
-+                if (CraftEventFactory.handleNonLivingEntityDamageEvent(this, source, amount, false)) {
-+                    return false;
-+                }
-+                // CraftBukkit end
-                 if (!source.is(DamageTypeTags.IS_EXPLOSION)) {
-                     DamageSource damagesource1 = source.getEntity() != null ? this.damageSources().explosion(this, source.getEntity()) : null;
- 
--                    world.explode(this, damagesource1, (ExplosionDamageCalculator) null, this.getX(), this.getY(), this.getZ(), 6.0F, false, Level.ExplosionInteraction.BLOCK);
-+                    // CraftBukkit start
-+                    ExplosionPrimeEvent event = CraftEventFactory.callExplosionPrimeEvent(this, 6.0F, false);
-+                    if (event.isCancelled()) {
-+                        return false;
-+                    }
-+
-+                    this.remove(Entity.RemovalReason.KILLED, EntityRemoveEvent.Cause.EXPLODE); // CraftBukkit - add Bukkit remove cause
-+                    world.explode(this, damagesource1, (ExplosionDamageCalculator) null, this.getX(), this.getY(), this.getZ(), event.getRadius(), event.getFire(), Level.ExplosionInteraction.BLOCK);
-+                } else {
-+                    this.remove(Entity.RemovalReason.KILLED, EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause
-                 }
-+                // CraftBukkit end
- 
-                 this.onDestroyedBy(world, source);
-             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java.patch
deleted file mode 100644
index 01ecc5bc3d..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java.patch
+++ /dev/null
@@ -1,331 +0,0 @@
---- a/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java
-+++ b/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java
-@@ -37,20 +37,35 @@
- import net.minecraft.world.entity.boss.enderdragon.phases.EnderDragonPhaseManager;
- import net.minecraft.world.entity.monster.Enemy;
- import net.minecraft.world.entity.player.Player;
--import net.minecraft.world.item.enchantment.EnchantmentHelper;
- import net.minecraft.world.level.GameRules;
- import net.minecraft.world.level.Level;
--import net.minecraft.world.level.block.state.BlockState;
--import net.minecraft.world.level.dimension.end.EndDragonFight;
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.level.levelgen.Heightmap;
- import net.minecraft.world.level.levelgen.feature.EndPodiumFeature;
- import net.minecraft.world.level.pathfinder.BinaryHeap;
- import net.minecraft.world.level.pathfinder.Node;
- import net.minecraft.world.level.pathfinder.Path;
-+import org.slf4j.Logger;
-+
-+// CraftBukkit start
-+import net.minecraft.world.item.ItemStack;
-+import net.minecraft.world.item.enchantment.EnchantmentHelper;
-+import net.minecraft.world.level.Explosion;
-+import net.minecraft.world.level.ServerExplosion;
-+import net.minecraft.world.level.block.Block;
-+import net.minecraft.world.level.block.entity.BlockEntity;
-+import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.world.level.dimension.end.EndDragonFight;
-+import net.minecraft.world.level.storage.loot.LootParams;
-+import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
- import net.minecraft.world.phys.AABB;
- import net.minecraft.world.phys.Vec3;
--import org.slf4j.Logger;
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.event.entity.EntityExplodeEvent;
-+import org.bukkit.event.entity.EntityRegainHealthEvent;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+// CraftBukkit end
- 
- public class EnderDragon extends Mob implements Enemy {
- 
-@@ -88,6 +103,11 @@
-     private final Node[] nodes;
-     private final int[] nodeAdjacency;
-     private final BinaryHeap openSet;
-+    private final Explosion explosionSource; // CraftBukkit - reusable source for CraftTNTPrimed.getSource()
-+    // Paper start - Allow changing the EnderDragon podium
-+    @Nullable
-+    private BlockPos podium;
-+    // Paper end - Allow changing the EnderDragon podium
- 
-     public EnderDragon(EntityType<? extends EnderDragon> entitytypes, Level world) {
-         super(EntityType.ENDER_DRAGON, world);
-@@ -108,6 +128,7 @@
-         this.setHealth(this.getMaxHealth());
-         this.noPhysics = true;
-         this.phaseManager = new EnderDragonPhaseManager(this);
-+        this.explosionSource = new ServerExplosion(world.getMinecraftWorld(), this, null, null, new Vec3(Double.NaN, Double.NaN, Double.NaN), Float.NaN, true, Explosion.BlockInteraction.DESTROY); // CraftBukkit
-     }
- 
-     public void setDragonFight(EndDragonFight fight) {
-@@ -124,7 +145,20 @@
- 
-     public static AttributeSupplier.Builder createAttributes() {
-         return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 200.0D);
-+    }
-+
-+    // Paper start - Allow changing the EnderDragon podium
-+    public BlockPos getPodium() {
-+        if (this.podium == null) {
-+            return EndPodiumFeature.getLocation(this.getFightOrigin());
-+        }
-+        return this.podium;
-+    }
-+
-+    public void setPodium(@Nullable BlockPos blockPos) {
-+        this.podium = blockPos;
-     }
-+    // Paper end - Allow changing the EnderDragon podium
- 
-     @Override
-     public boolean isFlapping() {
-@@ -218,7 +252,7 @@
- 
-                     Vec3 vec3d1 = idragoncontroller.getFlyTargetLocation();
- 
--                    if (vec3d1 != null) {
-+                    if (vec3d1 != null && idragoncontroller.getPhase() != EnderDragonPhase.HOVERING) { // CraftBukkit - Don't move when hovering
-                         double d0 = vec3d1.x - this.getX();
-                         double d1 = vec3d1.y - this.getY();
-                         double d2 = vec3d1.z - this.getZ();
-@@ -379,7 +413,14 @@
-             if (this.nearestCrystal.isRemoved()) {
-                 this.nearestCrystal = null;
-             } else if (this.tickCount % 10 == 0 && this.getHealth() < this.getMaxHealth()) {
--                this.setHealth(this.getHealth() + 1.0F);
-+                // CraftBukkit start
-+                EntityRegainHealthEvent event = new EntityRegainHealthEvent(this.getBukkitEntity(), 1.0F, EntityRegainHealthEvent.RegainReason.ENDER_CRYSTAL);
-+                this.level().getCraftServer().getPluginManager().callEvent(event);
-+
-+                if (!event.isCancelled()) {
-+                    this.setHealth((float) (this.getHealth() + event.getAmount()));
-+                }
-+                // CraftBukkit end
-             }
-         }
- 
-@@ -417,7 +458,7 @@
-                 double d3 = entity.getZ() - d1;
-                 double d4 = Math.max(d2 * d2 + d3 * d3, 0.1D);
- 
--                entity.push(d2 / d4 * 4.0D, 0.20000000298023224D, d3 / d4 * 4.0D);
-+                entity.push(d2 / d4 * 4.0D, 0.20000000298023224D, d3 / d4 * 4.0D, this); // Paper - Add EntityKnockbackByEntityEvent and EntityPushedByEntityAttackEvent
-                 if (!this.phaseManager.getCurrentPhase().isSitting() && entityliving.getLastHurtByMobTimestamp() < entity.tickCount - 2) {
-                     DamageSource damagesource = this.damageSources().mobAttack(this);
- 
-@@ -458,6 +499,9 @@
-         int j1 = Mth.floor(box.maxZ);
-         boolean flag = false;
-         boolean flag1 = false;
-+        // CraftBukkit start - Create a list to hold all the destroyed blocks
-+        List<org.bukkit.block.Block> destroyedBlocks = new java.util.ArrayList<org.bukkit.block.Block>();
-+        // CraftBukkit end
- 
-         for (int k1 = i; k1 <= l; ++k1) {
-             for (int l1 = j; l1 <= i1; ++l1) {
-@@ -467,14 +511,66 @@
- 
-                     if (!iblockdata.isAir() && !iblockdata.is(BlockTags.DRAGON_TRANSPARENT)) {
-                         if (world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) && !iblockdata.is(BlockTags.DRAGON_IMMUNE)) {
--                            flag1 = world.removeBlock(blockposition, false) || flag1;
-+                            // CraftBukkit start - Add blocks to list rather than destroying them
-+                            // flag1 = worldserver.removeBlock(blockposition, false) || flag1;
-+                            flag1 = true;
-+                            destroyedBlocks.add(CraftBlock.at(world, blockposition));
-+                            // CraftBukkit end
-                         } else {
-                             flag = true;
-                         }
-                     }
-+                }
-+            }
-+        }
-+
-+        // CraftBukkit start - Set off an EntityExplodeEvent for the dragon exploding all these blocks
-+        // SPIGOT-4882: don't fire event if nothing hit
-+        if (!flag1) {
-+            return flag;
-+        }
-+
-+        EntityExplodeEvent event = CraftEventFactory.callEntityExplodeEvent(this, destroyedBlocks, 0F, this.explosionSource.getBlockInteraction());
-+        if (event.isCancelled()) {
-+            // This flag literally means 'Dragon hit something hard' (Obsidian, White Stone or Bedrock) and will cause the dragon to slow down.
-+            // We should consider adding an event extension for it, or perhaps returning true if the event is cancelled.
-+            return flag;
-+        } else if (event.getYield() == 0F) {
-+            // Yield zero ==> no drops
-+            for (org.bukkit.block.Block block : event.blockList()) {
-+                this.level().removeBlock(new BlockPos(block.getX(), block.getY(), block.getZ()), false);
-+            }
-+        } else {
-+            for (org.bukkit.block.Block block : event.blockList()) {
-+                org.bukkit.Material blockId = block.getType();
-+                if (blockId.isAir()) {
-+                    continue;
-+                }
-+
-+                CraftBlock craftBlock = ((CraftBlock) block);
-+                BlockPos blockposition = craftBlock.getPosition();
-+
-+                Block nmsBlock = craftBlock.getNMS().getBlock();
-+                if (nmsBlock.dropFromExplosion(this.explosionSource)) {
-+                    BlockEntity tileentity = craftBlock.getNMS().hasBlockEntity() ? this.level().getBlockEntity(blockposition) : null;
-+                    LootParams.Builder loottableinfo_builder = (new LootParams.Builder((ServerLevel) this.level())).withParameter(LootContextParams.ORIGIN, Vec3.atCenterOf(blockposition)).withParameter(LootContextParams.TOOL, ItemStack.EMPTY).withParameter(LootContextParams.EXPLOSION_RADIUS, 1.0F / event.getYield()).withOptionalParameter(LootContextParams.BLOCK_ENTITY, tileentity);
-+
-+                    craftBlock.getNMS().getDrops(loottableinfo_builder).forEach((itemstack) -> {
-+                        Block.popResource(this.level(), blockposition, itemstack);
-+                    });
-+                    craftBlock.getNMS().spawnAfterBreak((ServerLevel) this.level(), blockposition, ItemStack.EMPTY, false);
-                 }
-+                // Paper start - TNTPrimeEvent
-+                org.bukkit.block.Block tntBlock = CraftBlock.at(this.level(), blockposition);
-+                if (!new com.destroystokyo.paper.event.block.TNTPrimeEvent(tntBlock, com.destroystokyo.paper.event.block.TNTPrimeEvent.PrimeReason.EXPLOSION, explosionSource.getIndirectSourceEntity().getBukkitEntity()).callEvent())
-+                    continue;
-+                // Paper end - TNTPrimeEvent
-+                nmsBlock.wasExploded((ServerLevel) this.level(), blockposition, this.explosionSource);
-+
-+                this.level().removeBlock(blockposition, false);
-             }
-         }
-+        // CraftBukkit end
- 
-         if (flag1) {
-             BlockPos blockposition1 = new BlockPos(i + this.random.nextInt(l - i + 1), j + this.random.nextInt(i1 - j + 1), k + this.random.nextInt(j1 - k + 1));
-@@ -531,7 +627,15 @@
- 
-     @Override
-     public void kill(ServerLevel world) {
--        this.remove(Entity.RemovalReason.KILLED);
-+        // Paper start - Fire entity death event
-+        this.silentDeath = true;
-+        org.bukkit.event.entity.EntityDeathEvent deathEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityDeathEvent(this, this.damageSources().genericKill());
-+        if (deathEvent.isCancelled()) {
-+            this.silentDeath = false; // Reset to default if event was cancelled
-+            return;
-+        }
-+        // Paper end - Fire entity death event
-+        this.remove(Entity.RemovalReason.KILLED, EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause
-         this.gameEvent(GameEvent.ENTITY_DIE);
-         if (this.dragonFight != null) {
-             this.dragonFight.updateDragon(this);
-@@ -540,7 +644,22 @@
- 
-     }
- 
-+    // CraftBukkit start - SPIGOT-2420: Special case, the ender dragon drops 12000 xp for the first kill and 500 xp for every other kill and this over time.
-     @Override
-+    public int getExpReward(ServerLevel worldserver, Entity entity) {
-+        // CraftBukkit - Moved from #tickDeath method
-+        boolean flag = worldserver.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT);
-+        short short0 = 500;
-+
-+        if (this.dragonFight != null && !this.dragonFight.hasPreviouslyKilledDragon()) {
-+            short0 = 12000;
-+        }
-+
-+        return flag ? short0 : 0;
-+    }
-+    // CraftBukkit end
-+
-+    @Override
-     protected void tickDeath() {
-         if (this.dragonFight != null) {
-             this.dragonFight.updateDragon(this);
-@@ -555,21 +674,44 @@
-             this.level().addParticle(ParticleTypes.EXPLOSION_EMITTER, this.getX() + (double) f, this.getY() + 2.0D + (double) f1, this.getZ() + (double) f2, 0.0D, 0.0D, 0.0D);
-         }
- 
-+        // CraftBukkit start - SPIGOT-2420: Moved up to #getExpReward method
-+        /*
-         short short0 = 500;
- 
-         if (this.dragonFight != null && !this.dragonFight.hasPreviouslyKilledDragon()) {
-             short0 = 12000;
-         }
-+        */
-+        int short0 = this.expToDrop;
-+        // CraftBukkit end
- 
-         Level world = this.level();
- 
-         if (world instanceof ServerLevel worldserver) {
--            if (this.dragonDeathTime > 150 && this.dragonDeathTime % 5 == 0 && worldserver.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) {
--                ExperienceOrb.award(worldserver, this.position(), Mth.floor((float) short0 * 0.08F));
-+            if (this.dragonDeathTime > 150 && this.dragonDeathTime % 5 == 0 && true) {  // CraftBukkit - SPIGOT-2420: Already checked for the game rule when calculating the xp
-+                ExperienceOrb.award(worldserver, this.position(), Mth.floor((float) short0 * 0.08F), org.bukkit.entity.ExperienceOrb.SpawnReason.ENTITY_DEATH, this.lastHurtByPlayer, this); // Paper
-             }
- 
-             if (this.dragonDeathTime == 1 && !this.isSilent()) {
--                worldserver.globalLevelEvent(1028, this.blockPosition(), 0);
-+                // CraftBukkit start - Use relative location for far away sounds
-+                // worldserver.globalLevelEvent(1028, this.blockPosition(), 0);
-+                int viewDistance = worldserver.getCraftServer().getViewDistance() * 16;
-+                for (net.minecraft.server.level.ServerPlayer player : worldserver.getPlayersForGlobalSoundGamerule()) { // Paper - respect global sound events gamerule
-+                    double deltaX = this.getX() - player.getX();
-+                    double deltaZ = this.getZ() - player.getZ();
-+                    double distanceSquared = deltaX * deltaX + deltaZ * deltaZ;
-+                    final double soundRadiusSquared = worldserver.getGlobalSoundRangeSquared(config -> config.dragonDeathSoundRadius); // Paper - respect global sound events gamerule
-+                    if ( !worldserver.getGameRules().getBoolean(GameRules.RULE_GLOBAL_SOUND_EVENTS) && distanceSquared > soundRadiusSquared ) continue; // Spigot // Paper - respect global sound events gamerule
-+                    if (distanceSquared > viewDistance * viewDistance) {
-+                        double deltaLength = Math.sqrt(distanceSquared);
-+                        double relativeX = player.getX() + (deltaX / deltaLength) * viewDistance;
-+                        double relativeZ = player.getZ() + (deltaZ / deltaLength) * viewDistance;
-+                        player.connection.send(new net.minecraft.network.protocol.game.ClientboundLevelEventPacket(1028, new BlockPos((int) relativeX, (int) this.getY(), (int) relativeZ), 0, true));
-+                    } else {
-+                        player.connection.send(new net.minecraft.network.protocol.game.ClientboundLevelEventPacket(1028, new BlockPos((int) this.getX(), (int) this.getY(), (int) this.getZ()), 0, true));
-+                    }
-+                }
-+                // CraftBukkit end
-             }
-         }
- 
-@@ -592,15 +734,15 @@
-             if (world1 instanceof ServerLevel) {
-                 ServerLevel worldserver1 = (ServerLevel) world1;
- 
--                if (worldserver1.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) {
--                    ExperienceOrb.award(worldserver1, this.position(), Mth.floor((float) short0 * 0.2F));
-+                if (true) { // CraftBukkit - SPIGOT-2420: Already checked for the game rule when calculating the xp
-+                    ExperienceOrb.award(worldserver1, this.position(), Mth.floor((float) short0 * 0.2F), org.bukkit.entity.ExperienceOrb.SpawnReason.ENTITY_DEATH, this.lastHurtByPlayer, this); // Paper
-                 }
- 
-                 if (this.dragonFight != null) {
-                     this.dragonFight.setDragonKilled(this);
-                 }
- 
--                this.remove(Entity.RemovalReason.KILLED);
-+                this.remove(Entity.RemovalReason.KILLED, EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause
-                 this.gameEvent(GameEvent.ENTITY_DIE);
-             }
-         }
-@@ -814,6 +956,7 @@
-         super.addAdditionalSaveData(nbt);
-         nbt.putInt("DragonPhase", this.phaseManager.getCurrentPhase().getPhase().getId());
-         nbt.putInt("DragonDeathTime", this.dragonDeathTime);
-+        nbt.putInt("Bukkit.expToDrop", this.expToDrop); // CraftBukkit - SPIGOT-2420: The ender dragon drops xp over time which can also happen between server starts
-     }
- 
-     @Override
-@@ -827,6 +970,11 @@
-             this.dragonDeathTime = nbt.getInt("DragonDeathTime");
-         }
- 
-+        // CraftBukkit start - SPIGOT-2420: The ender dragon drops xp over time which can also happen between server starts
-+        if (nbt.contains("Bukkit.expToDrop")) {
-+            this.expToDrop = nbt.getInt("Bukkit.expToDrop");
-+        }
-+        // CraftBukkit end
-     }
- 
-     @Override
-@@ -879,7 +1027,7 @@
-                 vec3d = this.getViewVector(tickDelta);
-             }
-         } else {
--            BlockPos blockposition = this.level().getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, EndPodiumFeature.getLocation(this.fightOrigin));
-+            BlockPos blockposition = this.level().getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, this.getPodium()); // Paper - Allow changing the EnderDragon podium
- 
-             f1 = Math.max((float) Math.sqrt(blockposition.distToCenterSqr(this.position())) / 4.0F, 1.0F);
-             float f3 = 6.0F / f1;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonDeathPhase.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonDeathPhase.java.patch
deleted file mode 100644
index 2dcf3ac15b..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonDeathPhase.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/entity/boss/enderdragon/phases/DragonDeathPhase.java
-+++ b/net/minecraft/world/entity/boss/enderdragon/phases/DragonDeathPhase.java
-@@ -42,7 +42,7 @@
-     public void doServerTick(ServerLevel world) {
-         this.time++;
-         if (this.targetLocation == null) {
--            BlockPos blockPos = world.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, EndPodiumFeature.getLocation(this.dragon.getFightOrigin()));
-+            BlockPos blockPos = world.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, this.dragon.getPodium()); // Paper - Allow changing the EnderDragon podium
-             this.targetLocation = Vec3.atBottomCenterOf(blockPos);
-         }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonLandingApproachPhase.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonLandingApproachPhase.java.patch
deleted file mode 100644
index ce24273215..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonLandingApproachPhase.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/entity/boss/enderdragon/phases/DragonLandingApproachPhase.java
-+++ b/net/minecraft/world/entity/boss/enderdragon/phases/DragonLandingApproachPhase.java
-@@ -52,7 +52,7 @@
-     private void findNewTarget(ServerLevel world) {
-         if (this.currentPath == null || this.currentPath.isDone()) {
-             int i = this.dragon.findClosestNode();
--            BlockPos blockPos = world.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, EndPodiumFeature.getLocation(this.dragon.getFightOrigin()));
-+            BlockPos blockPos = world.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, this.dragon.getPodium()); // Paper - Allow changing the EnderDragon podium
-             Player player = world.getNearestPlayer(NEAR_EGG_TARGETING, this.dragon, (double)blockPos.getX(), (double)blockPos.getY(), (double)blockPos.getZ());
-             int j;
-             if (player != null) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonTakeoffPhase.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonTakeoffPhase.java.patch
deleted file mode 100644
index 54fe962df5..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonTakeoffPhase.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/entity/boss/enderdragon/phases/DragonTakeoffPhase.java
-+++ b/net/minecraft/world/entity/boss/enderdragon/phases/DragonTakeoffPhase.java
-@@ -24,7 +24,7 @@
-     @Override
-     public void doServerTick(ServerLevel world) {
-         if (!this.firstTick && this.currentPath != null) {
--            BlockPos blockPos = world.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, EndPodiumFeature.getLocation(this.dragon.getFightOrigin()));
-+            BlockPos blockPos = world.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, this.dragon.getPodium()); // Paper - Allow changing the EnderDragon podium
-             if (!blockPos.closerToCenterThan(this.dragon.position(), 10.0)) {
-                 this.dragon.getPhaseManager().setPhase(EnderDragonPhase.HOLDING_PATTERN);
-             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/EnderDragonPhaseManager.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/EnderDragonPhaseManager.java.patch
deleted file mode 100644
index d39507da58..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/EnderDragonPhaseManager.java.patch
+++ /dev/null
@@ -1,42 +0,0 @@
---- a/net/minecraft/world/entity/boss/enderdragon/phases/EnderDragonPhaseManager.java
-+++ b/net/minecraft/world/entity/boss/enderdragon/phases/EnderDragonPhaseManager.java
-@@ -5,6 +5,11 @@
- import net.minecraft.world.entity.boss.enderdragon.EnderDragon;
- import org.slf4j.Logger;
- 
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.entity.CraftEnderDragon;
-+import org.bukkit.event.entity.EnderDragonChangePhaseEvent;
-+// CraftBukkit end
-+
- public class EnderDragonPhaseManager {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
-@@ -24,6 +29,19 @@
-                 this.currentPhase.end();
-             }
- 
-+            // CraftBukkit start - Call EnderDragonChangePhaseEvent
-+            EnderDragonChangePhaseEvent event = new EnderDragonChangePhaseEvent(
-+                    (CraftEnderDragon) this.dragon.getBukkitEntity(),
-+                    (this.currentPhase == null) ? null : CraftEnderDragon.getBukkitPhase(this.currentPhase.getPhase()),
-+                    CraftEnderDragon.getBukkitPhase(type)
-+            );
-+            this.dragon.level().getCraftServer().getPluginManager().callEvent(event);
-+            if (event.isCancelled()) {
-+                return;
-+            }
-+            type = CraftEnderDragon.getMinecraftPhase(event.getNewPhase());
-+            // CraftBukkit end
-+
-             this.currentPhase = this.getPhase(type);
-             if (!this.dragon.level().isClientSide) {
-                 this.dragon.getEntityData().set(EnderDragon.DATA_PHASE, type.getId());
-@@ -45,6 +63,6 @@
-             this.phases[i] = type.createInstance(this.dragon);
-         }
- 
--        return this.phases[i];
-+        return (T) this.phases[i]; // CraftBukkit - decompile error
-     }
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/boss/wither/WitherBoss.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/boss/wither/WitherBoss.java.patch
deleted file mode 100644
index 67144bc046..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/boss/wither/WitherBoss.java.patch
+++ /dev/null
@@ -1,165 +0,0 @@
---- a/net/minecraft/world/entity/boss/wither/WitherBoss.java
-+++ b/net/minecraft/world/entity/boss/wither/WitherBoss.java
-@@ -10,14 +10,10 @@
- import net.minecraft.core.particles.ParticleTypes;
- import net.minecraft.nbt.CompoundTag;
- import net.minecraft.network.chat.Component;
-+import net.minecraft.network.protocol.game.ClientboundLevelEventPacket;
- import net.minecraft.network.syncher.EntityDataAccessor;
- import net.minecraft.network.syncher.EntityDataSerializers;
- import net.minecraft.network.syncher.SynchedEntityData;
--import net.minecraft.server.level.ServerBossEvent;
--import net.minecraft.server.level.ServerLevel;
--import net.minecraft.server.level.ServerPlayer;
--import net.minecraft.sounds.SoundEvent;
--import net.minecraft.sounds.SoundEvents;
- import net.minecraft.tags.BlockTags;
- import net.minecraft.tags.DamageTypeTags;
- import net.minecraft.tags.EntityTypeTags;
-@@ -54,8 +50,21 @@
- import net.minecraft.world.level.GameRules;
- import net.minecraft.world.level.ItemLike;
- import net.minecraft.world.level.Level;
-+import net.minecraft.server.MinecraftServer;
-+import net.minecraft.server.level.ServerBossEvent;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.sounds.SoundEvent;
-+import net.minecraft.sounds.SoundEvents;
-+import net.minecraft.world.level.block.Blocks;
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.phys.Vec3;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.EntityRegainHealthEvent;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.event.entity.EntityTargetEvent;
-+import org.bukkit.event.entity.ExplosionPrimeEvent;
-+// CraftBukkit end
- 
- public class WitherBoss extends Monster implements RangedAttackMob {
- 
-@@ -77,7 +86,12 @@
-         return !entityliving.getType().is(EntityTypeTags.WITHER_FRIENDS) && entityliving.attackable();
-     };
-     private static final TargetingConditions TARGETING_CONDITIONS = TargetingConditions.forCombat().range(20.0D).selector(WitherBoss.LIVING_ENTITY_SELECTOR);
-+    // Paper start
-+    private boolean canPortal = false;
- 
-+    public void setCanTravelThroughPortals(boolean canPortal) { this.canPortal = canPortal; }
-+    // Paper end
-+
-     public WitherBoss(EntityType<? extends WitherBoss> type, Level world) {
-         super(type, world);
-         this.bossEvent = (ServerBossEvent) (new ServerBossEvent(this.getDisplayName(), BossEvent.BossBarColor.PURPLE, BossEvent.BossBarOverlay.PROGRESS)).setDarkenScreen(true);
-@@ -252,15 +266,42 @@
-             i = this.getInvulnerableTicks() - 1;
-             this.bossEvent.setProgress(1.0F - (float) i / 220.0F);
-             if (i <= 0) {
--                world.explode(this, this.getX(), this.getEyeY(), this.getZ(), 7.0F, false, Level.ExplosionInteraction.MOB);
-+                // CraftBukkit start
-+                // worldserver.explode(this, this.getX(), this.getEyeY(), this.getZ(), 7.0F, false, World.a.MOB);
-+                ExplosionPrimeEvent event = new ExplosionPrimeEvent(this.getBukkitEntity(), 7.0F, false);
-+                world.getCraftServer().getPluginManager().callEvent(event);
-+
-+                if (!event.isCancelled()) {
-+                    world.explode(this, this.getX(), this.getEyeY(), this.getZ(), event.getRadius(), event.getFire(), Level.ExplosionInteraction.MOB);
-+                }
-+                // CraftBukkit end
-+
-                 if (!this.isSilent()) {
--                    world.globalLevelEvent(1023, this.blockPosition(), 0);
-+                    // CraftBukkit start - Use relative location for far away sounds
-+                    // worldserver.globalLevelEvent(1023, new BlockPosition(this), 0);
-+                    int viewDistance = world.getCraftServer().getViewDistance() * 16;
-+                    for (ServerPlayer player : world.getPlayersForGlobalSoundGamerule()) { // Paper - respect global sound events gamerule
-+                        double deltaX = this.getX() - player.getX();
-+                        double deltaZ = this.getZ() - player.getZ();
-+                        double distanceSquared = deltaX * deltaX + deltaZ * deltaZ;
-+                        final double soundRadiusSquared = world.getGlobalSoundRangeSquared(config -> config.witherSpawnSoundRadius); // Paper - respect global sound events gamerule
-+                        if ( !world.getGameRules().getBoolean(GameRules.RULE_GLOBAL_SOUND_EVENTS) && distanceSquared > soundRadiusSquared ) continue; // Spigot // Paper - respect global sound events gamerule
-+                        if (distanceSquared > viewDistance * viewDistance) {
-+                            double deltaLength = Math.sqrt(distanceSquared);
-+                            double relativeX = player.getX() + (deltaX / deltaLength) * viewDistance;
-+                            double relativeZ = player.getZ() + (deltaZ / deltaLength) * viewDistance;
-+                            player.connection.send(new ClientboundLevelEventPacket(1023, new BlockPos((int) relativeX, (int) this.getY(), (int) relativeZ), 0, true));
-+                        } else {
-+                            player.connection.send(new ClientboundLevelEventPacket(1023, this.blockPosition(), 0, true));
-+                        }
-+                    }
-+                    // CraftBukkit end
-                 }
-             }
- 
-             this.setInvulnerableTicks(i);
-             if (this.tickCount % 10 == 0) {
--                this.heal(10.0F);
-+                this.heal(10.0F, EntityRegainHealthEvent.RegainReason.WITHER_SPAWN); // CraftBukkit
-             }
- 
-         } else {
-@@ -305,6 +346,7 @@
-                         if (!list.isEmpty()) {
-                             LivingEntity entityliving1 = (LivingEntity) list.get(this.random.nextInt(list.size()));
- 
-+                            if (CraftEventFactory.callEntityTargetLivingEvent(this, entityliving1, EntityTargetEvent.TargetReason.CLOSEST_ENTITY).isCancelled()) continue; // CraftBukkit
-                             this.setAlternativeTarget(i, entityliving1.getId());
-                         }
-                     }
-@@ -331,6 +373,11 @@
-                         BlockState iblockdata = world.getBlockState(blockposition);
- 
-                         if (WitherBoss.canDestroy(iblockdata)) {
-+                            // CraftBukkit start
-+                            if (!CraftEventFactory.callEntityChangeBlockEvent(this, blockposition, iblockdata.getFluidState().createLegacyBlock())) { // Paper - fix wrong block state
-+                                continue;
-+                            }
-+                            // CraftBukkit end
-                             flag = world.destroyBlock(blockposition, true, this) || flag;
-                         }
-                     }
-@@ -342,7 +389,7 @@
-             }
- 
-             if (this.tickCount % 20 == 0) {
--                this.heal(1.0F);
-+                this.heal(1.0F, EntityRegainHealthEvent.RegainReason.REGEN); // CraftBukkit
-             }
- 
-             this.bossEvent.setProgress(this.getHealth() / this.getMaxHealth());
-@@ -488,10 +535,10 @@
-     @Override
-     protected void dropCustomDeathLoot(ServerLevel world, DamageSource source, boolean causedByPlayer) {
-         super.dropCustomDeathLoot(world, source, causedByPlayer);
--        ItemEntity entityitem = this.spawnAtLocation(world, (ItemLike) Items.NETHER_STAR);
-+        ItemEntity entityitem = this.spawnAtLocation(world, new net.minecraft.world.item.ItemStack(Items.NETHER_STAR), 0, ItemEntity::setExtendedLifetime); // Paper - Restore vanilla drops behavior; spawnAtLocation returns null so modify the item entity with a consumer
- 
-         if (entityitem != null) {
--            entityitem.setExtendedLifetime();
-+            entityitem.setExtendedLifetime(); // Paper - diff on change
-         }
- 
-     }
-@@ -499,7 +546,7 @@
-     @Override
-     public void checkDespawn() {
-         if (this.level().getDifficulty() == Difficulty.PEACEFUL && this.shouldDespawnInPeaceful()) {
--            this.discard();
-+            this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-         } else {
-             this.noActionTime = 0;
-         }
-@@ -549,12 +596,12 @@
- 
-     @Override
-     public boolean canUsePortal(boolean allowVehicles) {
--        return false;
-+        return this.canPortal; // Paper
-     }
- 
-     @Override
-     public boolean canBeAffected(MobEffectInstance effect) {
--        return effect.is(MobEffects.WITHER) ? false : super.canBeAffected(effect);
-+        return effect.is(MobEffects.WITHER) && this.level().paperConfig().entities.mobEffects.immuneToWitherEffect.wither ? false : super.canBeAffected(effect); // Paper - Add config for mobs immune to default effects
-     }
- 
-     private class WitherDoNothingGoal extends Goal {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/decoration/ArmorStand.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/decoration/ArmorStand.java.patch
deleted file mode 100644
index edb69ea66e..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/decoration/ArmorStand.java.patch
+++ /dev/null
@@ -1,513 +0,0 @@
---- a/net/minecraft/world/entity/decoration/ArmorStand.java
-+++ b/net/minecraft/world/entity/decoration/ArmorStand.java
-@@ -25,7 +25,6 @@
- import net.minecraft.world.entity.Entity;
- import net.minecraft.world.entity.EntityDimensions;
- import net.minecraft.world.entity.EntityType;
--import net.minecraft.world.entity.EquipmentSlot;
- import net.minecraft.world.entity.HumanoidArm;
- import net.minecraft.world.entity.LightningBolt;
- import net.minecraft.world.entity.LivingEntity;
-@@ -33,7 +32,6 @@
- import net.minecraft.world.entity.Pose;
- import net.minecraft.world.entity.ai.attributes.AttributeSupplier;
- import net.minecraft.world.entity.ai.attributes.Attributes;
--import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.entity.vehicle.AbstractMinecart;
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.item.Items;
-@@ -47,6 +45,14 @@
- import net.minecraft.world.level.material.PushReaction;
- import net.minecraft.world.phys.AABB;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.inventory.EquipmentSlot;
-+import org.bukkit.craftbukkit.CraftEquipmentSlot;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.entity.Player;
-+import org.bukkit.event.player.PlayerArmorStandManipulateEvent;
-+// CraftBukkit end
- 
- public class ArmorStand extends LivingEntity {
- 
-@@ -101,9 +107,17 @@
-     public Rotations rightArmPose;
-     public Rotations leftLegPose;
-     public Rotations rightLegPose;
-+    public boolean canMove = true; // Paper
-+    // Paper start - Allow ArmorStands not to tick
-+    public boolean canTick = true;
-+    public boolean canTickSetByAPI = false;
-+    private boolean noTickPoseDirty = false;
-+    private boolean noTickEquipmentDirty = false;
-+    // Paper end - Allow ArmorStands not to tick
- 
-     public ArmorStand(EntityType<? extends ArmorStand> type, Level world) {
-         super(type, world);
-+        if (world != null) this.canTick = world.paperConfig().entities.armorStands.tick; // Paper - Allow ArmorStands not to tick
-         this.handItems = NonNullList.withSize(2, ItemStack.EMPTY);
-         this.armorItems = NonNullList.withSize(4, ItemStack.EMPTY);
-         this.headPose = ArmorStand.DEFAULT_HEAD_POSE;
-@@ -123,7 +137,14 @@
-         return createLivingAttributes().add(Attributes.STEP_HEIGHT, 0.0D);
-     }
- 
-+    // CraftBukkit start - SPIGOT-3607, SPIGOT-3637
-     @Override
-+    public float getBukkitYaw() {
-+        return this.getYRot();
-+    }
-+    // CraftBukkit end
-+
-+    @Override
-     public void refreshDimensions() {
-         double d0 = this.getX();
-         double d1 = this.getY();
-@@ -165,7 +186,7 @@
-     }
- 
-     @Override
--    public ItemStack getItemBySlot(EquipmentSlot slot) {
-+    public ItemStack getItemBySlot(net.minecraft.world.entity.EquipmentSlot slot) {
-         switch (slot.getType()) {
-             case HAND:
-                 return (ItemStack) this.handItems.get(slot.getIndex());
-@@ -177,21 +198,29 @@
-     }
- 
-     @Override
--    public boolean canUseSlot(EquipmentSlot slot) {
--        return slot != EquipmentSlot.BODY && !this.isDisabled(slot);
-+    public boolean canUseSlot(net.minecraft.world.entity.EquipmentSlot slot) {
-+        return slot != net.minecraft.world.entity.EquipmentSlot.BODY && !this.isDisabled(slot);
-+    }
-+
-+    @Override
-+    public void setItemSlot(net.minecraft.world.entity.EquipmentSlot slot, ItemStack stack) {
-+        // CraftBukkit start
-+        this.setItemSlot(slot, stack, false);
-     }
- 
-     @Override
--    public void setItemSlot(EquipmentSlot slot, ItemStack stack) {
--        this.verifyEquippedItem(stack);
--        switch (slot.getType()) {
-+    public void setItemSlot(net.minecraft.world.entity.EquipmentSlot enumitemslot, ItemStack itemstack, boolean silent) {
-+        // CraftBukkit end
-+        this.verifyEquippedItem(itemstack);
-+        switch (enumitemslot.getType()) {
-             case HAND:
--                this.onEquipItem(slot, (ItemStack) this.handItems.set(slot.getIndex(), stack), stack);
-+                this.onEquipItem(enumitemslot, (ItemStack) this.handItems.set(enumitemslot.getIndex(), itemstack), itemstack, silent); // CraftBukkit
-                 break;
-             case HUMANOID_ARMOR:
--                this.onEquipItem(slot, (ItemStack) this.armorItems.set(slot.getIndex(), stack), stack);
-+                this.onEquipItem(enumitemslot, (ItemStack) this.armorItems.set(enumitemslot.getIndex(), itemstack), itemstack, silent); // CraftBukkit
-         }
- 
-+        this.noTickEquipmentDirty = true; // Paper - Allow ArmorStands not to tick; Still update equipment
-     }
- 
-     @Override
-@@ -227,6 +256,7 @@
-         }
- 
-         nbt.put("Pose", this.writePose());
-+        if (this.canTickSetByAPI) nbt.putBoolean("Paper.CanTickOverride", this.canTick); // Paper - Allow ArmorStands not to tick
-     }
- 
-     @Override
-@@ -261,6 +291,12 @@
-         this.setNoBasePlate(nbt.getBoolean("NoBasePlate"));
-         this.setMarker(nbt.getBoolean("Marker"));
-         this.noPhysics = !this.hasPhysics();
-+        // Paper start - Allow ArmorStands not to tick
-+        if (nbt.contains("Paper.CanTickOverride")) {
-+            this.canTick = nbt.getBoolean("Paper.CanTickOverride");
-+            this.canTickSetByAPI = true;
-+        }
-+        // Paper end - Allow ArmorStands not to tick
-         CompoundTag nbttagcompound2 = nbt.getCompound("Pose");
- 
-         this.readPose(nbttagcompound2);
-@@ -318,7 +354,7 @@
-     }
- 
-     @Override
--    public boolean isPushable() {
-+    public boolean isCollidable(boolean ignoreClimbing) { // Paper - Climbing should not bypass cramming gamerule
-         return false;
-     }
- 
-@@ -327,6 +363,7 @@
- 
-     @Override
-     protected void pushEntities() {
-+        if (!this.level().paperConfig().entities.armorStands.doCollisionEntityLookups) return; // Paper - Option to prevent armor stands from doing entity lookups
-         List<Entity> list = this.level().getEntities((Entity) this, this.getBoundingBox(), ArmorStand.RIDABLE_MINECARTS);
-         Iterator iterator = list.iterator();
- 
-@@ -341,7 +378,7 @@
-     }
- 
-     @Override
--    public InteractionResult interactAt(Player player, Vec3 hitPos, InteractionHand hand) {
-+    public InteractionResult interactAt(net.minecraft.world.entity.player.Player player, Vec3 hitPos, InteractionHand hand) {
-         ItemStack itemstack = player.getItemInHand(hand);
- 
-         if (!this.isMarker() && !itemstack.is(Items.NAME_TAG)) {
-@@ -350,11 +387,11 @@
-             } else if (player.level().isClientSide) {
-                 return InteractionResult.SUCCESS_SERVER;
-             } else {
--                EquipmentSlot enumitemslot = this.getEquipmentSlotForItem(itemstack);
-+                net.minecraft.world.entity.EquipmentSlot enumitemslot = this.getEquipmentSlotForItem(itemstack);
- 
-                 if (itemstack.isEmpty()) {
--                    EquipmentSlot enumitemslot1 = this.getClickedSlot(hitPos);
--                    EquipmentSlot enumitemslot2 = this.isDisabled(enumitemslot1) ? enumitemslot : enumitemslot1;
-+                    net.minecraft.world.entity.EquipmentSlot enumitemslot1 = this.getClickedSlot(hitPos);
-+                    net.minecraft.world.entity.EquipmentSlot enumitemslot2 = this.isDisabled(enumitemslot1) ? enumitemslot : enumitemslot1;
- 
-                     if (this.hasItemInSlot(enumitemslot2) && this.swapItem(player, enumitemslot2, itemstack, hand)) {
-                         return InteractionResult.SUCCESS_SERVER;
-@@ -364,7 +401,7 @@
-                         return InteractionResult.FAIL;
-                     }
- 
--                    if (enumitemslot.getType() == EquipmentSlot.Type.HAND && !this.showArms()) {
-+                    if (enumitemslot.getType() == net.minecraft.world.entity.EquipmentSlot.Type.HAND && !this.showArms()) {
-                         return InteractionResult.FAIL;
-                     }
- 
-@@ -380,39 +417,57 @@
-         }
-     }
- 
--    private EquipmentSlot getClickedSlot(Vec3 hitPos) {
--        EquipmentSlot enumitemslot = EquipmentSlot.MAINHAND;
-+    private net.minecraft.world.entity.EquipmentSlot getClickedSlot(Vec3 hitPos) {
-+        net.minecraft.world.entity.EquipmentSlot enumitemslot = net.minecraft.world.entity.EquipmentSlot.MAINHAND;
-         boolean flag = this.isSmall();
-         double d0 = hitPos.y / (double) (this.getScale() * this.getAgeScale());
--        EquipmentSlot enumitemslot1 = EquipmentSlot.FEET;
-+        net.minecraft.world.entity.EquipmentSlot enumitemslot1 = net.minecraft.world.entity.EquipmentSlot.FEET;
- 
-         if (d0 >= 0.1D && d0 < 0.1D + (flag ? 0.8D : 0.45D) && this.hasItemInSlot(enumitemslot1)) {
--            enumitemslot = EquipmentSlot.FEET;
--        } else if (d0 >= 0.9D + (flag ? 0.3D : 0.0D) && d0 < 0.9D + (flag ? 1.0D : 0.7D) && this.hasItemInSlot(EquipmentSlot.CHEST)) {
--            enumitemslot = EquipmentSlot.CHEST;
--        } else if (d0 >= 0.4D && d0 < 0.4D + (flag ? 1.0D : 0.8D) && this.hasItemInSlot(EquipmentSlot.LEGS)) {
--            enumitemslot = EquipmentSlot.LEGS;
--        } else if (d0 >= 1.6D && this.hasItemInSlot(EquipmentSlot.HEAD)) {
--            enumitemslot = EquipmentSlot.HEAD;
--        } else if (!this.hasItemInSlot(EquipmentSlot.MAINHAND) && this.hasItemInSlot(EquipmentSlot.OFFHAND)) {
--            enumitemslot = EquipmentSlot.OFFHAND;
-+            enumitemslot = net.minecraft.world.entity.EquipmentSlot.FEET;
-+        } else if (d0 >= 0.9D + (flag ? 0.3D : 0.0D) && d0 < 0.9D + (flag ? 1.0D : 0.7D) && this.hasItemInSlot(net.minecraft.world.entity.EquipmentSlot.CHEST)) {
-+            enumitemslot = net.minecraft.world.entity.EquipmentSlot.CHEST;
-+        } else if (d0 >= 0.4D && d0 < 0.4D + (flag ? 1.0D : 0.8D) && this.hasItemInSlot(net.minecraft.world.entity.EquipmentSlot.LEGS)) {
-+            enumitemslot = net.minecraft.world.entity.EquipmentSlot.LEGS;
-+        } else if (d0 >= 1.6D && this.hasItemInSlot(net.minecraft.world.entity.EquipmentSlot.HEAD)) {
-+            enumitemslot = net.minecraft.world.entity.EquipmentSlot.HEAD;
-+        } else if (!this.hasItemInSlot(net.minecraft.world.entity.EquipmentSlot.MAINHAND) && this.hasItemInSlot(net.minecraft.world.entity.EquipmentSlot.OFFHAND)) {
-+            enumitemslot = net.minecraft.world.entity.EquipmentSlot.OFFHAND;
-         }
- 
-         return enumitemslot;
-     }
- 
--    public boolean isDisabled(EquipmentSlot slot) {
--        return (this.disabledSlots & 1 << slot.getFilterBit(0)) != 0 || slot.getType() == EquipmentSlot.Type.HAND && !this.showArms();
-+    public boolean isDisabled(net.minecraft.world.entity.EquipmentSlot slot) {
-+        return (this.disabledSlots & 1 << slot.getFilterBit(0)) != 0 || slot.getType() == net.minecraft.world.entity.EquipmentSlot.Type.HAND && !this.showArms();
-     }
- 
--    private boolean swapItem(Player player, EquipmentSlot slot, ItemStack stack, InteractionHand hand) {
-+    private boolean swapItem(net.minecraft.world.entity.player.Player player, net.minecraft.world.entity.EquipmentSlot slot, ItemStack stack, InteractionHand hand) {
-         ItemStack itemstack1 = this.getItemBySlot(slot);
- 
-         if (!itemstack1.isEmpty() && (this.disabledSlots & 1 << slot.getFilterBit(8)) != 0) {
-             return false;
-         } else if (itemstack1.isEmpty() && (this.disabledSlots & 1 << slot.getFilterBit(16)) != 0) {
-             return false;
--        } else if (player.hasInfiniteMaterials() && itemstack1.isEmpty() && !stack.isEmpty()) {
-+            // CraftBukkit start
-+        } else {
-+            org.bukkit.inventory.ItemStack armorStandItem = CraftItemStack.asCraftMirror(itemstack1);
-+            org.bukkit.inventory.ItemStack playerHeldItem = CraftItemStack.asCraftMirror(stack);
-+
-+            Player player1 = (Player) player.getBukkitEntity();
-+            org.bukkit.entity.ArmorStand self = (org.bukkit.entity.ArmorStand) this.getBukkitEntity();
-+
-+            EquipmentSlot slot1 = CraftEquipmentSlot.getSlot(slot);
-+            EquipmentSlot hand1 = CraftEquipmentSlot.getHand(hand);
-+            PlayerArmorStandManipulateEvent armorStandManipulateEvent = new PlayerArmorStandManipulateEvent(player1, self, playerHeldItem, armorStandItem, slot1, hand1);
-+            this.level().getCraftServer().getPluginManager().callEvent(armorStandManipulateEvent);
-+
-+            if (armorStandManipulateEvent.isCancelled()) {
-+                return true;
-+            }
-+
-+        if (player.hasInfiniteMaterials() && itemstack1.isEmpty() && !stack.isEmpty()) {
-+            // CraftBukkit end
-             this.setItemSlot(slot, stack.copyWithCount(1));
-             return true;
-         } else if (!stack.isEmpty() && stack.getCount() > 1) {
-@@ -427,6 +482,7 @@
-             player.setItemInHand(hand, itemstack1);
-             return true;
-         }
-+        } // CraftBukkit
-     }
- 
-     @Override
-@@ -436,12 +492,24 @@
-         } else if (!world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) && source.getEntity() instanceof Mob) {
-             return false;
-         } else if (source.is(DamageTypeTags.BYPASSES_INVULNERABILITY)) {
--            this.kill(world);
-+            // CraftBukkit start
-+            if (org.bukkit.craftbukkit.event.CraftEventFactory.handleNonLivingEntityDamageEvent(this, source, amount)) {
-+                return false;
-+            }
-+            this.kill(world, source); // CraftBukkit
-+            // CraftBukkit end
-             return false;
--        } else if (!this.isInvulnerableTo(world, source) && !this.invisible && !this.isMarker()) {
-+        } else if (!this.isInvulnerableTo(world, source) && (true || !this.invisible) && !this.isMarker()) { // CraftBukkit
-+            // CraftBukkit start
-+            if (org.bukkit.craftbukkit.event.CraftEventFactory.handleNonLivingEntityDamageEvent(this, source, amount, true, this.invisible)) {
-+                return false;
-+            }
-+            // CraftBukkit end
-             if (source.is(DamageTypeTags.IS_EXPLOSION)) {
--                this.brokenByAnything(world, source);
--                this.kill(world);
-+                // Paper start - avoid duplicate event call
-+                org.bukkit.event.entity.EntityDeathEvent event = this.brokenByAnything(world, source);
-+                if (!event.isCancelled()) this.kill(source, false); // CraftBukkit
-+                // Paper end
-                 return false;
-             } else if (source.is(DamageTypeTags.IGNITES_ARMOR_STANDS)) {
-                 if (this.isOnFire()) {
-@@ -463,8 +531,8 @@
-                 } else {
-                     Entity entity = source.getEntity();
- 
--                    if (entity instanceof Player) {
--                        Player entityhuman = (Player) entity;
-+                    if (entity instanceof net.minecraft.world.entity.player.Player) {
-+                        net.minecraft.world.entity.player.Player entityhuman = (net.minecraft.world.entity.player.Player) entity;
- 
-                         if (!entityhuman.getAbilities().mayBuild) {
-                             return false;
-@@ -474,7 +542,7 @@
-                     if (source.isCreativePlayer()) {
-                         this.playBrokenSound();
-                         this.showBreakingParticles();
--                        this.kill(world);
-+                        this.kill(world, source); // CraftBukkit
-                         return true;
-                     } else {
-                         long i = world.getGameTime();
-@@ -484,9 +552,9 @@
-                             this.gameEvent(GameEvent.ENTITY_DAMAGE, source.getEntity());
-                             this.lastHit = i;
-                         } else {
--                            this.brokenByPlayer(world, source);
-+                            org.bukkit.event.entity.EntityDeathEvent event = this.brokenByPlayer(world, source); // Paper
-                             this.showBreakingParticles();
--                            this.kill(world);
-+                            if (!event.isCancelled()) this.kill(source, false); // Paper - we still need to kill to follow vanilla logic (emit the game event etc...)
-                         }
- 
-                         return true;
-@@ -536,7 +604,10 @@
-         f1 -= amount;
-         if (f1 <= 0.5F) {
-             this.brokenByAnything(world, damageSource);
--            this.kill(world);
-+            // Paper start - avoid duplicate event call
-+            org.bukkit.event.entity.EntityDeathEvent event = this.brokenByAnything(world, damageSource);
-+            if (!event.isCancelled()) this.kill(damageSource, false); // CraftBukkit
-+            // Paper end
-         } else {
-             this.setHealth(f1);
-             this.gameEvent(GameEvent.ENTITY_DAMAGE, damageSource.getEntity());
-@@ -544,17 +615,17 @@
- 
-     }
- 
--    private void brokenByPlayer(ServerLevel world, DamageSource damageSource) {
-+    private org.bukkit.event.entity.EntityDeathEvent brokenByPlayer(ServerLevel world, DamageSource damageSource) { // Paper
-         ItemStack itemstack = new ItemStack(Items.ARMOR_STAND);
- 
-         itemstack.set(DataComponents.CUSTOM_NAME, this.getCustomName());
--        Block.popResource(this.level(), this.blockPosition(), itemstack);
--        this.brokenByAnything(world, damageSource);
-+        this.drops.add(new DefaultDrop(itemstack, stack -> Block.popResource(this.level(), this.blockPosition(), stack))); // CraftBukkit - add to drops // Paper - Restore vanilla drops behavior
-+        return this.brokenByAnything(world, damageSource); // Paper
-     }
- 
--    private void brokenByAnything(ServerLevel world, DamageSource damageSource) {
-+    private org.bukkit.event.entity.EntityDeathEvent brokenByAnything(ServerLevel world, DamageSource damageSource) { // Paper
-         this.playBrokenSound();
--        this.dropAllDeathLoot(world, damageSource);
-+        // this.dropAllDeathLoot(worldserver, damagesource); // CraftBukkit - moved down
- 
-         ItemStack itemstack;
-         int i;
-@@ -562,7 +633,7 @@
-         for (i = 0; i < this.handItems.size(); ++i) {
-             itemstack = (ItemStack) this.handItems.get(i);
-             if (!itemstack.isEmpty()) {
--                Block.popResource(this.level(), this.blockPosition().above(), itemstack);
-+                this.drops.add(new DefaultDrop(itemstack, stack -> Block.popResource(this.level(), this.blockPosition().above(), stack))); // CraftBukkit - add to drops // Paper - Restore vanilla drops behavior; mirror so we can destroy it later - though this call site was safe & spawn drops correctly
-                 this.handItems.set(i, ItemStack.EMPTY);
-             }
-         }
-@@ -570,15 +641,16 @@
-         for (i = 0; i < this.armorItems.size(); ++i) {
-             itemstack = (ItemStack) this.armorItems.get(i);
-             if (!itemstack.isEmpty()) {
--                Block.popResource(this.level(), this.blockPosition().above(), itemstack);
-+                this.drops.add(new DefaultDrop(itemstack, stack -> Block.popResource(this.level(), this.blockPosition().above(), stack))); // CraftBukkit - add to drops // Paper - Restore vanilla drops behavior; mirror so we can destroy it later - though this call site was safe & spawn drops correctly
-                 this.armorItems.set(i, ItemStack.EMPTY);
-             }
-         }
-+        return this.dropAllDeathLoot(world, damageSource); // CraftBukkit - moved from above // Paper
- 
-     }
- 
-     private void playBrokenSound() {
--        this.level().playSound((Player) null, this.getX(), this.getY(), this.getZ(), SoundEvents.ARMOR_STAND_BREAK, this.getSoundSource(), 1.0F, 1.0F);
-+        this.level().playSound((net.minecraft.world.entity.player.Player) null, this.getX(), this.getY(), this.getZ(), SoundEvents.ARMOR_STAND_BREAK, this.getSoundSource(), 1.0F, 1.0F);
-     }
- 
-     @Override
-@@ -609,7 +681,29 @@
- 
-     @Override
-     public void tick() {
-+        // Paper start - Allow ArmorStands not to tick
-+        if (!this.canTick) {
-+            if (this.noTickPoseDirty) {
-+                this.noTickPoseDirty = false;
-+                this.updatePose();
-+            }
-+
-+            if (this.noTickEquipmentDirty) {
-+                this.noTickEquipmentDirty = false;
-+                this.detectEquipmentUpdatesPublic();
-+            }
-+
-+            return;
-+        }
-+        // Paper end - Allow ArmorStands not to tick
-+
-         super.tick();
-+        // Paper start - Allow ArmorStands not to tick
-+        updatePose();
-+    }
-+
-+    public void updatePose() {
-+        // Paper end - Allow ArmorStands not to tick
-         Rotations vector3f = (Rotations) this.entityData.get(ArmorStand.DATA_HEAD_POSE);
- 
-         if (!this.headPose.equals(vector3f)) {
-@@ -664,9 +758,31 @@
-         return this.isSmall();
-     }
- 
-+    // CraftBukkit start
-     @Override
-+    public boolean shouldDropExperience() {
-+        return true; // MC-157395, SPIGOT-5193 even baby (small) armor stands should drop
-+    }
-+    // CraftBukkit end
-+
-+    @Override
-     public void kill(ServerLevel world) {
--        this.remove(Entity.RemovalReason.KILLED);
-+        // CraftBukkit start - pass DamageSource for kill
-+        this.kill(world, null);
-+    }
-+
-+    public void kill(ServerLevel worldserver, DamageSource damageSource) {
-+        // Paper start - make cancellable
-+        this.kill(damageSource, true);
-+    }
-+    public void kill(DamageSource damageSource, boolean callEvent) {
-+        if (callEvent) {
-+            org.bukkit.event.entity.EntityDeathEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityDeathEvent(this, (damageSource == null ? this.damageSources().genericKill() : damageSource), this.drops); // CraftBukkit - call event
-+            if (event.isCancelled()) return;
-+        }
-+        // Paper end
-+        this.remove(Entity.RemovalReason.KILLED, EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause
-+        // CraftBukkit end
-         this.gameEvent(GameEvent.ENTITY_DIE);
-     }
- 
-@@ -730,31 +846,37 @@
-     public void setHeadPose(Rotations angle) {
-         this.headPose = angle;
-         this.entityData.set(ArmorStand.DATA_HEAD_POSE, angle);
-+        this.noTickPoseDirty = true; // Paper - Allow updates when not ticking
-     }
- 
-     public void setBodyPose(Rotations angle) {
-         this.bodyPose = angle;
-         this.entityData.set(ArmorStand.DATA_BODY_POSE, angle);
-+        this.noTickPoseDirty = true; // Paper - Allow updates when not ticking
-     }
- 
-     public void setLeftArmPose(Rotations angle) {
-         this.leftArmPose = angle;
-         this.entityData.set(ArmorStand.DATA_LEFT_ARM_POSE, angle);
-+        this.noTickPoseDirty = true; // Paper - Allow updates when not ticking
-     }
- 
-     public void setRightArmPose(Rotations angle) {
-         this.rightArmPose = angle;
-         this.entityData.set(ArmorStand.DATA_RIGHT_ARM_POSE, angle);
-+        this.noTickPoseDirty = true; // Paper - Allow updates when not ticking
-     }
- 
-     public void setLeftLegPose(Rotations angle) {
-         this.leftLegPose = angle;
-         this.entityData.set(ArmorStand.DATA_LEFT_LEG_POSE, angle);
-+        this.noTickPoseDirty = true; // Paper - Allow updates when not ticking
-     }
- 
-     public void setRightLegPose(Rotations angle) {
-         this.rightLegPose = angle;
-         this.entityData.set(ArmorStand.DATA_RIGHT_LEG_POSE, angle);
-+        this.noTickPoseDirty = true; // Paper - Allow updates when not ticking
-     }
- 
-     public Rotations getHeadPose() {
-@@ -788,7 +910,7 @@
- 
-     @Override
-     public boolean skipAttackInteraction(Entity attacker) {
--        return attacker instanceof Player && !this.level().mayInteract((Player) attacker, this.blockPosition());
-+        return attacker instanceof net.minecraft.world.entity.player.Player && !this.level().mayInteract((net.minecraft.world.entity.player.Player) attacker, this.blockPosition());
-     }
- 
-     @Override
-@@ -882,4 +1004,13 @@
-     public boolean canBeSeenByAnyone() {
-         return !this.isInvisible() && !this.isMarker();
-     }
-+
-+    // Paper start
-+    @Override
-+    public void move(net.minecraft.world.entity.MoverType type, Vec3 movement) {
-+        if (this.canMove) {
-+            super.move(type, movement);
-+        }
-+    }
-+    // Paper end
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/decoration/BlockAttachedEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/decoration/BlockAttachedEntity.java.patch
deleted file mode 100644
index 2f102c86b9..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/decoration/BlockAttachedEntity.java.patch
+++ /dev/null
@@ -1,140 +0,0 @@
---- a/net/minecraft/world/entity/decoration/BlockAttachedEntity.java
-+++ b/net/minecraft/world/entity/decoration/BlockAttachedEntity.java
-@@ -2,9 +2,6 @@
- 
- import com.mojang.logging.LogUtils;
- import javax.annotation.Nullable;
--import net.minecraft.core.BlockPos;
--import net.minecraft.nbt.CompoundTag;
--import net.minecraft.server.level.ServerLevel;
- import net.minecraft.world.damagesource.DamageSource;
- import net.minecraft.world.entity.Entity;
- import net.minecraft.world.entity.EntityType;
-@@ -15,13 +12,24 @@
- import net.minecraft.world.level.Explosion;
- import net.minecraft.world.level.GameRules;
- import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.phys.Vec3;
- import org.slf4j.Logger;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.server.level.ServerLevel;
-+// CraftBukkit start
-+import net.minecraft.tags.DamageTypeTags;
-+import org.bukkit.entity.Hanging;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.event.hanging.HangingBreakByEntityEvent;
-+import org.bukkit.event.hanging.HangingBreakEvent;
-+// CraftBukkit end
- 
- public abstract class BlockAttachedEntity extends Entity {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
--    private int checkInterval;
-+    private int checkInterval; { this.checkInterval = this.getId() % this.level().spigotConfig.hangingTickFrequency; } // Paper - Perf: offset item frame ticking
-     protected BlockPos pos;
- 
-     protected BlockAttachedEntity(EntityType<? extends BlockAttachedEntity> type, Level world) {
-@@ -41,10 +49,28 @@
- 
-         if (world instanceof ServerLevel worldserver) {
-             this.checkBelowWorld();
--            if (this.checkInterval++ == 100) {
-+            if (this.checkInterval++ == this.level().spigotConfig.hangingTickFrequency) { // Spigot
-                 this.checkInterval = 0;
-                 if (!this.isRemoved() && !this.survives()) {
--                    this.discard();
-+                    // CraftBukkit start - fire break events
-+                    BlockState material = this.level().getBlockState(this.blockPosition());
-+                    HangingBreakEvent.RemoveCause cause;
-+
-+                    if (!material.isAir()) {
-+                        // TODO: This feels insufficient to catch 100% of suffocation cases
-+                        cause = HangingBreakEvent.RemoveCause.OBSTRUCTION;
-+                    } else {
-+                        cause = HangingBreakEvent.RemoveCause.PHYSICS;
-+                    }
-+
-+                    HangingBreakEvent event = new HangingBreakEvent((Hanging) this.getBukkitEntity(), cause);
-+                    this.level().getCraftServer().getPluginManager().callEvent(event);
-+
-+                    if (this.isRemoved() || event.isCancelled()) {
-+                        return;
-+                    }
-+                    // CraftBukkit end
-+                    this.discard(EntityRemoveEvent.Cause.DROP); // CraftBukkit - add Bukkit remove cause
-                     this.dropItem(worldserver, (Entity) null);
-                 }
-             }
-@@ -81,6 +107,22 @@
-             return false;
-         } else {
-             if (!this.isRemoved()) {
-+                // CraftBukkit start - fire break events
-+                Entity damager = (!source.isDirect() && source.getEntity() != null) ? source.getEntity() : source.getDirectEntity(); // Paper - fix DamageSource API
-+                HangingBreakEvent event;
-+                if (damager != null) {
-+                    event = new HangingBreakByEntityEvent((Hanging) this.getBukkitEntity(), damager.getBukkitEntity(), source.is(DamageTypeTags.IS_EXPLOSION) ? HangingBreakEvent.RemoveCause.EXPLOSION : HangingBreakEvent.RemoveCause.ENTITY);
-+                } else {
-+                    event = new HangingBreakEvent((Hanging) this.getBukkitEntity(), source.is(DamageTypeTags.IS_EXPLOSION) ? HangingBreakEvent.RemoveCause.EXPLOSION : HangingBreakEvent.RemoveCause.DEFAULT);
-+                }
-+
-+                this.level().getCraftServer().getPluginManager().callEvent(event);
-+
-+                if (this.isRemoved() || event.isCancelled()) {
-+                    return true;
-+                }
-+                // CraftBukkit end
-+
-                 this.kill(world);
-                 this.markHurt();
-                 this.dropItem(world, source.getEntity());
-@@ -101,6 +143,16 @@
- 
-         if (world instanceof ServerLevel worldserver) {
-             if (!this.isRemoved() && movement.lengthSqr() > 0.0D) {
-+                // CraftBukkit start - fire break events
-+                // TODO - Does this need its own cause? Seems to only be triggered by pistons
-+                HangingBreakEvent event = new HangingBreakEvent((Hanging) this.getBukkitEntity(), HangingBreakEvent.RemoveCause.PHYSICS);
-+                this.level().getCraftServer().getPluginManager().callEvent(event);
-+
-+                if (this.isRemoved() || event.isCancelled()) {
-+                    return;
-+                }
-+                // CraftBukkit end
-+
-                 this.kill(worldserver);
-                 this.dropItem(worldserver, (Entity) null);
-             }
-@@ -109,11 +161,11 @@
-     }
- 
-     @Override
--    public void push(double deltaX, double deltaY, double deltaZ) {
-+    public void push(double deltaX, double deltaY, double deltaZ, @Nullable Entity pushingEntity) { // Paper - override correct overload
-         Level world = this.level();
- 
-         if (world instanceof ServerLevel worldserver) {
--            if (!this.isRemoved() && deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ > 0.0D) {
-+            if (false && !this.isRemoved() && deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ > 0.0D) { // CraftBukkit - not needed
-                 this.kill(worldserver);
-                 this.dropItem(worldserver, (Entity) null);
-             }
-@@ -121,7 +173,16 @@
- 
-     }
- 
-+    // CraftBukkit start - selectively save tile position
-     @Override
-+    public void addAdditionalSaveData(CompoundTag nbttagcompound, boolean includeAll) {
-+        if (includeAll) {
-+            this.addAdditionalSaveData(nbttagcompound);
-+        }
-+    }
-+    // CraftBukkit end
-+
-+    @Override
-     public void addAdditionalSaveData(CompoundTag nbt) {
-         BlockPos blockposition = this.getPos();
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/decoration/ItemFrame.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/decoration/ItemFrame.java.patch
deleted file mode 100644
index f6d7c47bb4..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/decoration/ItemFrame.java.patch
+++ /dev/null
@@ -1,150 +0,0 @@
---- a/net/minecraft/world/entity/decoration/ItemFrame.java
-+++ b/net/minecraft/world/entity/decoration/ItemFrame.java
-@@ -1,6 +1,7 @@
- package net.minecraft.world.entity.decoration;
- 
- import javax.annotation.Nullable;
-+import io.papermc.paper.event.player.PlayerItemFrameChangeEvent; // Paper - Add PlayerItemFrameChangeEvent
- import net.minecraft.core.BlockPos;
- import net.minecraft.core.Direction;
- import net.minecraft.core.component.DataComponents;
-@@ -50,6 +51,7 @@
-     private static final float HEIGHT = 0.75F;
-     public float dropChance;
-     public boolean fixed;
-+    public @Nullable MapId cachedMapId; // Paper - Perf: Cache map ids on item frames
- 
-     public ItemFrame(EntityType<? extends ItemFrame> type, Level world) {
-         super(type, world);
-@@ -91,9 +93,15 @@
- 
-     @Override
-     protected AABB calculateBoundingBox(BlockPos pos, Direction side) {
-+        // CraftBukkit start - break out BB calc into own method
-+        return ItemFrame.calculateBoundingBoxStatic(pos, side);
-+    }
-+
-+    public static AABB calculateBoundingBoxStatic(BlockPos blockposition, Direction enumdirection) {
-+        // CraftBukkit end
-         float f = 0.46875F;
--        Vec3 vec3d = Vec3.atCenterOf(pos).relative(side, -0.46875D);
--        Direction.Axis enumdirection_enumaxis = side.getAxis();
-+        Vec3 vec3d = Vec3.atCenterOf(blockposition).relative(enumdirection, -0.46875D);
-+        Direction.Axis enumdirection_enumaxis = enumdirection.getAxis();
-         double d0 = enumdirection_enumaxis == Direction.Axis.X ? 0.0625D : 0.75D;
-         double d1 = enumdirection_enumaxis == Direction.Axis.Y ? 0.0625D : 0.75D;
-         double d2 = enumdirection_enumaxis == Direction.Axis.Z ? 0.0625D : 0.75D;
-@@ -123,9 +131,9 @@
-     }
- 
-     @Override
--    public void push(double deltaX, double deltaY, double deltaZ) {
-+    public void push(double deltaX, double deltaY, double deltaZ, @Nullable Entity pushingEntity) { // Paper - add push source entity param
-         if (!this.fixed) {
--            super.push(deltaX, deltaY, deltaZ);
-+            super.push(deltaX, deltaY, deltaZ, pushingEntity); // Paper - add push source entity param
-         }
- 
-     }
-@@ -155,6 +163,18 @@
-             if (this.isInvulnerableToBase(source)) {
-                 return false;
-             } else if (this.shouldDamageDropItem(source)) {
-+                // CraftBukkit start - fire EntityDamageEvent
-+                if (org.bukkit.craftbukkit.event.CraftEventFactory.handleNonLivingEntityDamageEvent(this, source, amount, false) || this.isRemoved()) {
-+                    return true;
-+                }
-+                // CraftBukkit end
-+                // Paper start - Add PlayerItemFrameChangeEvent
-+                if (source.getEntity() instanceof Player player) {
-+                    var event = new PlayerItemFrameChangeEvent((org.bukkit.entity.Player) player.getBukkitEntity(), (org.bukkit.entity.ItemFrame) this.getBukkitEntity(), this.getItem().asBukkitCopy(), PlayerItemFrameChangeEvent.ItemFrameChangeAction.REMOVE);
-+                    if (!event.callEvent()) return true; // return true here because you aren't cancelling the damage, just the change
-+                    this.setItem(ItemStack.fromBukkitCopy(event.getItemStack()), false);
-+                }
-+                // Paper end - Add PlayerItemFrameChangeEvent
-                 this.dropItem(world, source.getEntity(), false);
-                 this.gameEvent(GameEvent.BLOCK_CHANGE, source.getEntity());
-                 this.playSound(this.getRemoveItemSound(), 1.0F, 1.0F);
-@@ -251,7 +271,15 @@
- 
-     public ItemStack getItem() {
-         return (ItemStack) this.getEntityData().get(ItemFrame.DATA_ITEM);
-+    }
-+
-+    // Paper start - Fix MC-123848 (spawn item frame drops above block)
-+    @Nullable
-+    @Override
-+    public net.minecraft.world.entity.item.ItemEntity spawnAtLocation(ServerLevel serverLevel, ItemStack stack) {
-+        return this.spawnAtLocation(serverLevel, stack, this.getDirection() == Direction.DOWN ? -0.6F : 0.0F);
-     }
-+    // Paper end
- 
-     @Nullable
-     public MapId getFramedMapId(ItemStack stack) {
-@@ -267,17 +295,23 @@
-     }
- 
-     public void setItem(ItemStack value, boolean update) {
--        if (!value.isEmpty()) {
--            value = value.copyWithCount(1);
-+        // CraftBukkit start
-+        this.setItem(value, update, true);
-+    }
-+
-+    public void setItem(ItemStack itemstack, boolean flag, boolean playSound) {
-+        // CraftBukkit end
-+        if (!itemstack.isEmpty()) {
-+            itemstack = itemstack.copyWithCount(1);
-         }
- 
--        this.onItemChanged(value);
--        this.getEntityData().set(ItemFrame.DATA_ITEM, value);
--        if (!value.isEmpty()) {
-+        this.onItemChanged(itemstack);
-+        this.getEntityData().set(ItemFrame.DATA_ITEM, itemstack);
-+        if (!itemstack.isEmpty() && flag && playSound) { // CraftBukkit // Paper - only play sound when update flag is set
-             this.playSound(this.getAddItemSound(), 1.0F, 1.0F);
-         }
- 
--        if (update && this.pos != null) {
-+        if (flag && this.pos != null) {
-             this.level().updateNeighbourForOutputSignal(this.pos, Blocks.AIR);
-         }
- 
-@@ -301,6 +335,7 @@
-     }
- 
-     private void onItemChanged(ItemStack stack) {
-+        this.cachedMapId = stack.getComponents().get(net.minecraft.core.component.DataComponents.MAP_ID); // Paper - Perf: Cache map ids on item frames
-         if (!stack.isEmpty() && stack.getFrame() != this) {
-             stack.setEntityRepresentation(this);
-         }
-@@ -386,7 +421,13 @@
-                     if (worldmap != null && worldmap.isTrackedCountOverLimit(256)) {
-                         return InteractionResult.FAIL;
-                     } else {
--                        this.setItem(itemstack);
-+                        // Paper start - Add PlayerItemFrameChangeEvent
-+                        PlayerItemFrameChangeEvent event = new PlayerItemFrameChangeEvent((org.bukkit.entity.Player) player.getBukkitEntity(), (org.bukkit.entity.ItemFrame) this.getBukkitEntity(), itemstack.asBukkitCopy(), PlayerItemFrameChangeEvent.ItemFrameChangeAction.PLACE);
-+                        if (!event.callEvent()) {
-+                            return InteractionResult.FAIL;
-+                        }
-+                        this.setItem(ItemStack.fromBukkitCopy(event.getItemStack()));
-+                        // Paper end - Add PlayerItemFrameChangeEvent
-                         this.gameEvent(GameEvent.BLOCK_CHANGE, player);
-                         itemstack.consume(1, player);
-                         return InteractionResult.SUCCESS;
-@@ -395,6 +436,13 @@
-                     return InteractionResult.PASS;
-                 }
-             } else {
-+                // Paper start - Add PlayerItemFrameChangeEvent
-+                PlayerItemFrameChangeEvent event = new PlayerItemFrameChangeEvent((org.bukkit.entity.Player) player.getBukkitEntity(), (org.bukkit.entity.ItemFrame) this.getBukkitEntity(), this.getItem().asBukkitCopy(), PlayerItemFrameChangeEvent.ItemFrameChangeAction.ROTATE);
-+                if (!event.callEvent()) {
-+                    return InteractionResult.FAIL;
-+                }
-+                setItem(ItemStack.fromBukkitCopy(event.getItemStack()), false, false);
-+                // Paper end - Add PlayerItemFrameChangeEvent
-                 this.playSound(this.getRotateItemSound(), 1.0F, 1.0F);
-                 this.setRotation(this.getRotation() + 1);
-                 this.gameEvent(GameEvent.BLOCK_CHANGE, player);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/decoration/Painting.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/decoration/Painting.java.patch
deleted file mode 100644
index 122466b765..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/decoration/Painting.java.patch
+++ /dev/null
@@ -1,54 +0,0 @@
---- a/net/minecraft/world/entity/decoration/Painting.java
-+++ b/net/minecraft/world/entity/decoration/Painting.java
-@@ -72,7 +72,7 @@
-     public static Optional<Painting> create(Level world, BlockPos pos, Direction facing) {
-         Painting entitypainting = new Painting(world, pos);
-         List<Holder<PaintingVariant>> list = new ArrayList();
--        Iterable iterable = world.registryAccess().lookupOrThrow(Registries.PAINTING_VARIANT).getTagOrEmpty(PaintingVariantTags.PLACEABLE);
-+        Iterable<Holder<PaintingVariant>> iterable = world.registryAccess().lookupOrThrow(Registries.PAINTING_VARIANT).getTagOrEmpty(PaintingVariantTags.PLACEABLE); // CraftBukkit - decompile error
- 
-         Objects.requireNonNull(list);
-         iterable.forEach(list::add);
-@@ -138,22 +138,32 @@
- 
-     @Override
-     protected AABB calculateBoundingBox(BlockPos pos, Direction side) {
--        float f = 0.46875F;
--        Vec3 vec3d = Vec3.atCenterOf(pos).relative(side, -0.46875D);
-+        // CraftBukkit start
-         PaintingVariant paintingvariant = (PaintingVariant) this.getVariant().value();
--        double d0 = this.offsetForPaintingSize(paintingvariant.width());
--        double d1 = this.offsetForPaintingSize(paintingvariant.height());
--        Direction enumdirection1 = side.getCounterClockWise();
-+        return Painting.calculateBoundingBoxStatic(pos, side, paintingvariant.width(), paintingvariant.height());
-+    }
-+
-+    public static AABB calculateBoundingBoxStatic(BlockPos blockposition, Direction enumdirection, int width, int height) {
-+        // CraftBukkit end
-+        float f = 0.46875F;
-+        Vec3 vec3d = Vec3.atCenterOf(blockposition).relative(enumdirection, -0.46875D);
-+        // CraftBukkit start
-+        double d0 = Painting.offsetForPaintingSize(width);
-+        double d1 = Painting.offsetForPaintingSize(height);
-+        // CraftBukkit end
-+        Direction enumdirection1 = enumdirection.getCounterClockWise();
-         Vec3 vec3d1 = vec3d.relative(enumdirection1, d0).relative(Direction.UP, d1);
--        Direction.Axis enumdirection_enumaxis = side.getAxis();
--        double d2 = enumdirection_enumaxis == Direction.Axis.X ? 0.0625D : (double) paintingvariant.width();
--        double d3 = (double) paintingvariant.height();
--        double d4 = enumdirection_enumaxis == Direction.Axis.Z ? 0.0625D : (double) paintingvariant.width();
-+        Direction.Axis enumdirection_enumaxis = enumdirection.getAxis();
-+        // CraftBukkit start
-+        double d2 = enumdirection_enumaxis == Direction.Axis.X ? 0.0625D : (double) width;
-+        double d3 = (double) height;
-+        double d4 = enumdirection_enumaxis == Direction.Axis.Z ? 0.0625D : (double) width;
-+        // CraftBukkit end
- 
-         return AABB.ofSize(vec3d1, d2, d3, d4);
-     }
- 
--    private double offsetForPaintingSize(int length) {
-+    private static double offsetForPaintingSize(int length) { // CraftBukkit - static
-         return length % 2 == 0 ? 0.5D : 0.0D;
-     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/item/FallingBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/item/FallingBlockEntity.java.patch
deleted file mode 100644
index 8f36430b05..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/item/FallingBlockEntity.java.patch
+++ /dev/null
@@ -1,162 +0,0 @@
---- a/net/minecraft/world/entity/item/FallingBlockEntity.java
-+++ b/net/minecraft/world/entity/item/FallingBlockEntity.java
-@@ -52,6 +52,11 @@
- import net.minecraft.world.phys.Vec3;
- import org.slf4j.Logger;
- 
-+// CraftBukkit start;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
-+
- public class FallingBlockEntity extends Entity {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
-@@ -66,6 +71,7 @@
-     public CompoundTag blockData;
-     public boolean forceTickAfterTeleportToDuplicate;
-     protected static final EntityDataAccessor<BlockPos> DATA_START_POS = SynchedEntityData.defineId(FallingBlockEntity.class, EntityDataSerializers.BLOCK_POS);
-+    public boolean autoExpire = true; // Paper - Expand FallingBlock API
- 
-     public FallingBlockEntity(EntityType<? extends FallingBlockEntity> type, Level world) {
-         super(type, world);
-@@ -87,10 +93,17 @@
-     }
- 
-     public static FallingBlockEntity fall(Level world, BlockPos pos, BlockState state) {
--        FallingBlockEntity entityfallingblock = new FallingBlockEntity(world, (double) pos.getX() + 0.5D, (double) pos.getY(), (double) pos.getZ() + 0.5D, state.hasProperty(BlockStateProperties.WATERLOGGED) ? (BlockState) state.setValue(BlockStateProperties.WATERLOGGED, false) : state);
-+        // CraftBukkit start
-+        return FallingBlockEntity.fall(world, pos, state, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT);
-+    }
- 
--        world.setBlock(pos, state.getFluidState().createLegacyBlock(), 3);
--        world.addFreshEntity(entityfallingblock);
-+    public static FallingBlockEntity fall(Level world, BlockPos blockposition, BlockState iblockdata, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason spawnReason) {
-+        // CraftBukkit end
-+        FallingBlockEntity entityfallingblock = new FallingBlockEntity(world, (double) blockposition.getX() + 0.5D, (double) blockposition.getY(), (double) blockposition.getZ() + 0.5D, iblockdata.hasProperty(BlockStateProperties.WATERLOGGED) ? (BlockState) iblockdata.setValue(BlockStateProperties.WATERLOGGED, false) : iblockdata);
-+        if (!CraftEventFactory.callEntityChangeBlockEvent(entityfallingblock, blockposition, iblockdata.getFluidState().createLegacyBlock())) return entityfallingblock; // CraftBukkit
-+
-+        world.setBlock(blockposition, iblockdata.getFluidState().createLegacyBlock(), 3);
-+        world.addFreshEntity(entityfallingblock, spawnReason); // CraftBukkit
-         return entityfallingblock;
-     }
- 
-@@ -139,7 +152,7 @@
-     @Override
-     public void tick() {
-         if (this.blockState.isAir()) {
--            this.discard();
-+            this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-         } else {
-             Block block = this.blockState.getBlock();
- 
-@@ -147,6 +160,16 @@
-             this.applyGravity();
-             this.move(MoverType.SELF, this.getDeltaMovement());
-             this.applyEffectsFromBlocks();
-+            // Paper start - Configurable falling blocks height nerf
-+            if (this.level().paperConfig().fixes.fallingBlockHeightNerf.test(v -> this.getY() > v)) {
-+                if (this.dropItem && this.level() instanceof final ServerLevel serverLevel && serverLevel.getGameRules().getBoolean(GameRules.RULE_DOENTITYDROPS)) {
-+                    this.spawnAtLocation(serverLevel, block);
-+                }
-+
-+                this.discard(EntityRemoveEvent.Cause.OUT_OF_WORLD);
-+                return;
-+            }
-+            // Paper end - Configurable falling blocks height nerf
-             this.handlePortal();
-             Level world = this.level();
- 
-@@ -169,12 +192,12 @@
-                     }
- 
-                     if (!this.onGround() && !flag1) {
--                        if (this.time > 100 && (blockposition.getY() <= this.level().getMinY() || blockposition.getY() > this.level().getMaxY()) || this.time > 600) {
-+                        if ((this.time > 100 && autoExpire) && (blockposition.getY() <= this.level().getMinY() || blockposition.getY() > this.level().getMaxY()) || (this.time > 600 && autoExpire)) { // Paper - Expand FallingBlock API
-                             if (this.dropItem && worldserver.getGameRules().getBoolean(GameRules.RULE_DOENTITYDROPS)) {
-                                 this.spawnAtLocation(worldserver, (ItemLike) block);
-                             }
- 
--                            this.discard();
-+                            this.discard(EntityRemoveEvent.Cause.DROP); // CraftBukkit - add Bukkit remove cause
-                         }
-                     } else {
-                         BlockState iblockdata = this.level().getBlockState(blockposition);
-@@ -191,9 +214,15 @@
-                                         this.blockState = (BlockState) this.blockState.setValue(BlockStateProperties.WATERLOGGED, true);
-                                     }
- 
-+                                    // CraftBukkit start
-+                                    if (!CraftEventFactory.callEntityChangeBlockEvent(this, blockposition, this.blockState)) {
-+                                        this.discard(EntityRemoveEvent.Cause.DESPAWN); // SPIGOT-6586 called before the event in previous versions
-+                                        return;
-+                                    }
-+                                    // CraftBukkit end
-                                     if (this.level().setBlock(blockposition, this.blockState, 3)) {
-                                         ((ServerLevel) this.level()).getChunkSource().chunkMap.broadcast(this, new ClientboundBlockUpdatePacket(blockposition, this.level().getBlockState(blockposition)));
--                                        this.discard();
-+                                        this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-                                         if (block instanceof Fallable) {
-                                             ((Fallable) block).onLand(this.level(), blockposition, this.blockState, iblockdata, this);
-                                         }
-@@ -221,19 +250,19 @@
-                                             }
-                                         }
-                                     } else if (this.dropItem && worldserver.getGameRules().getBoolean(GameRules.RULE_DOENTITYDROPS)) {
--                                        this.discard();
-+                                        this.discard(EntityRemoveEvent.Cause.DROP); // CraftBukkit - add Bukkit remove cause
-                                         this.callOnBrokenAfterFall(block, blockposition);
-                                         this.spawnAtLocation(worldserver, (ItemLike) block);
-                                     }
-                                 } else {
--                                    this.discard();
-+                                    this.discard(EntityRemoveEvent.Cause.DROP); // CraftBukkit - add Bukkit remove cause
-                                     if (this.dropItem && worldserver.getGameRules().getBoolean(GameRules.RULE_DOENTITYDROPS)) {
-                                         this.callOnBrokenAfterFall(block, blockposition);
-                                         this.spawnAtLocation(worldserver, (ItemLike) block);
-                                     }
-                                 }
-                             } else {
--                                this.discard();
-+                                this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-                                 this.callOnBrokenAfterFall(block, blockposition);
-                             }
-                         }
-@@ -310,6 +339,7 @@
-         }
- 
-         nbt.putBoolean("CancelDrop", this.cancelDrop);
-+        if (!autoExpire) {nbt.putBoolean("Paper.AutoExpire", false);} // Paper - Expand FallingBlock API
-     }
- 
-     @Override
-@@ -328,7 +358,7 @@
-             this.dropItem = nbt.getBoolean("DropItem");
-         }
- 
--        if (nbt.contains("TileEntityData", 10)) {
-+        if (nbt.contains("TileEntityData", 10) && !(this.level().paperConfig().entities.spawning.filterBadTileEntityNbtFromFallingBlocks && this.blockState.getBlock() instanceof net.minecraft.world.level.block.GameMasterBlock)) { // Paper - Filter bad block entity nbt data from falling blocks
-             this.blockData = nbt.getCompound("TileEntityData").copy();
-         }
- 
-@@ -337,6 +367,11 @@
-             this.blockState = Blocks.SAND.defaultBlockState();
-         }
- 
-+        // Paper start - Expand FallingBlock API
-+         if (nbt.contains("Paper.AutoExpire")) {
-+            this.autoExpire = nbt.getBoolean("Paper.AutoExpire");
-+         }
-+        // Paper end - Expand FallingBlock API
-     }
- 
-     public void setHurtsEntities(float fallHurtAmount, int fallHurtMax) {
-@@ -395,7 +430,7 @@
-         boolean flag = (resourcekey1 == Level.END || resourcekey == Level.END) && resourcekey1 != resourcekey;
-         Entity entity = super.teleport(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/paper-server/patches/unapplied/net/minecraft/world/entity/item/ItemEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/item/ItemEntity.java.patch
deleted file mode 100644
index 0bc136847c..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/item/ItemEntity.java.patch
+++ /dev/null
@@ -1,389 +0,0 @@
---- a/net/minecraft/world/entity/item/ItemEntity.java
-+++ b/net/minecraft/world/entity/item/ItemEntity.java
-@@ -5,18 +5,6 @@
- import java.util.Objects;
- import java.util.UUID;
- import javax.annotation.Nullable;
--import net.minecraft.core.BlockPos;
--import net.minecraft.nbt.CompoundTag;
--import net.minecraft.network.chat.Component;
--import net.minecraft.network.syncher.EntityDataAccessor;
--import net.minecraft.network.syncher.EntityDataSerializers;
--import net.minecraft.network.syncher.SynchedEntityData;
--import net.minecraft.server.level.ServerLevel;
--import net.minecraft.sounds.SoundSource;
--import net.minecraft.stats.Stats;
--import net.minecraft.tags.FluidTags;
--import net.minecraft.tags.ItemTags;
--import net.minecraft.util.Mth;
- import net.minecraft.world.damagesource.DamageSource;
- import net.minecraft.world.entity.Entity;
- import net.minecraft.world.entity.EntityType;
-@@ -24,7 +12,6 @@
- import net.minecraft.world.entity.MoverType;
- import net.minecraft.world.entity.SlotAccess;
- import net.minecraft.world.entity.TraceableEntity;
--import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.item.Item;
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.level.Explosion;
-@@ -33,6 +20,27 @@
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.level.portal.TeleportTransition;
- import net.minecraft.world.phys.Vec3;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.network.chat.Component;
-+import net.minecraft.network.syncher.EntityDataAccessor;
-+import net.minecraft.network.syncher.EntityDataSerializers;
-+import net.minecraft.network.syncher.SynchedEntityData;
-+// CraftBukkit start
-+import net.minecraft.server.MinecraftServer;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.sounds.SoundSource;
-+import net.minecraft.stats.Stats;
-+import net.minecraft.tags.FluidTags;
-+import net.minecraft.tags.ItemTags;
-+import net.minecraft.util.Mth;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.entity.Player;
-+import org.bukkit.event.entity.EntityPickupItemEvent;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.event.player.PlayerPickupItemEvent;
-+// CraftBukkit end
-+import org.bukkit.event.player.PlayerAttemptPickupItemEvent; // Paper
- 
- public class ItemEntity extends Entity implements TraceableEntity {
- 
-@@ -52,6 +60,10 @@
-     @Nullable
-     public UUID target;
-     public final float bobOffs;
-+    // private int lastTick = MinecraftServer.currentTick - 1; // CraftBukkit // Paper - remove anti tick skipping measures / wall time
-+    public boolean canMobPickup = true; // Paper - Item#canEntityPickup
-+    private int despawnRate = -1; // Paper - Alternative item-despawn-rate
-+    public net.kyori.adventure.util.TriState frictionState = net.kyori.adventure.util.TriState.NOT_SET; // Paper - Friction API
- 
-     public ItemEntity(EntityType<? extends ItemEntity> type, Level world) {
-         super(type, world);
-@@ -61,7 +73,12 @@
-     }
- 
-     public ItemEntity(Level world, double x, double y, double z, ItemStack stack) {
--        this(world, x, y, z, stack, world.random.nextDouble() * 0.2D - 0.1D, 0.2D, world.random.nextDouble() * 0.2D - 0.1D);
-+        // Paper start - Don't use level random in entity constructors (to make them thread-safe)
-+        this(EntityType.ITEM, world);
-+        this.setPos(x, y, z);
-+        this.setDeltaMovement(this.random.nextDouble() * 0.2D - 0.1D, 0.2D, this.random.nextDouble() * 0.2D - 0.1D);
-+        this.setItem(stack);
-+        // Paper end - Don't use level random in entity constructors
-     }
- 
-     public ItemEntity(Level world, double x, double y, double z, ItemStack stack, double velocityX, double velocityY, double velocityZ) {
-@@ -133,12 +150,14 @@
-     @Override
-     public void tick() {
-         if (this.getItem().isEmpty()) {
--            this.discard();
-+            this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-         } else {
-             super.tick();
-+            // Paper start - remove anti tick skipping measures / wall time - revert to vanilla
-             if (this.pickupDelay > 0 && this.pickupDelay != 32767) {
-                 --this.pickupDelay;
-             }
-+            // Paper end - remove anti tick skipping measures / wall time - revert to vanilla
- 
-             this.xo = this.getX();
-             this.yo = this.getY();
-@@ -162,12 +181,16 @@
-                 }
-             }
- 
--            if (!this.onGround() || this.getDeltaMovement().horizontalDistanceSqr() > 9.999999747378752E-6D || (this.tickCount + this.getId()) % 4 == 0) {
-+            if (!this.onGround() || this.getDeltaMovement().horizontalDistanceSqr() > 9.999999747378752E-6D || (this.tickCount + this.getId()) % 4 == 0) { // Paper - Diff on change; ActivationRange immunity
-                 this.move(MoverType.SELF, this.getDeltaMovement());
-                 this.applyEffectsFromBlocks();
-                 float f = 0.98F;
- 
--                if (this.onGround()) {
-+                // Paper start - Friction API
-+                if (frictionState == net.kyori.adventure.util.TriState.FALSE) {
-+                    f = 1F;
-+                } else if (this.onGround()) {
-+                    // Paper end - Friction API
-                     f = this.level().getBlockState(this.getBlockPosBelowThatAffectsMyMovement()).getBlock().getFriction() * 0.98F;
-                 }
- 
-@@ -188,9 +211,11 @@
-                 this.mergeWithNeighbours();
-             }
- 
-+            // Paper - remove anti tick skipping measures / wall time - revert to vanilla /* CraftBukkit start - moved up
-             if (this.age != -32768) {
-                 ++this.age;
-             }
-+            // CraftBukkit end */
- 
-             this.hasImpulse |= this.updateInWaterStateAndDoFluidPushing();
-             if (!this.level().isClientSide) {
-@@ -201,14 +226,44 @@
-                 }
-             }
- 
--            if (!this.level().isClientSide && this.age >= 6000) {
--                this.discard();
-+            if (!this.level().isClientSide && this.age >= this.despawnRate) { // Spigot // Paper - Alternative item-despawn-rate
-+                // CraftBukkit start - fire ItemDespawnEvent
-+                if (CraftEventFactory.callItemDespawnEvent(this).isCancelled()) {
-+                    this.age = 0;
-+                    return;
-+                }
-+                // CraftBukkit end
-+                this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-             }
- 
-         }
-     }
- 
-+    // Spigot start - copied from above
-     @Override
-+    public void inactiveTick() {
-+        // Paper start - remove anti tick skipping measures / wall time - copied from above
-+        if (this.pickupDelay > 0 && this.pickupDelay != 32767) {
-+            --this.pickupDelay;
-+        }
-+        if (this.age != -32768) {
-+            ++this.age;
-+        }
-+        // Paper end - remove anti tick skipping measures / wall time - copied from above
-+
-+        if (!this.level().isClientSide && this.age >= this.despawnRate) { // Spigot // Paper - Alternative item-despawn-rate
-+            // CraftBukkit start - fire ItemDespawnEvent
-+            if (org.bukkit.craftbukkit.event.CraftEventFactory.callItemDespawnEvent(this).isCancelled()) {
-+                this.age = 0;
-+                return;
-+            }
-+            // CraftBukkit end
-+            this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-+        }
-+    }
-+    // Spigot end
-+
-+    @Override
-     public BlockPos getBlockPosBelowThatAffectsMyMovement() {
-         return this.getOnPos(0.999999F);
-     }
-@@ -229,7 +284,10 @@
- 
-     private void mergeWithNeighbours() {
-         if (this.isMergable()) {
--            List<ItemEntity> list = this.level().getEntitiesOfClass(ItemEntity.class, this.getBoundingBox().inflate(0.5D, 0.0D, 0.5D), (entityitem) -> {
-+            // Spigot start
-+            double radius = this.level().spigotConfig.itemMerge;
-+            List<ItemEntity> list = this.level().getEntitiesOfClass(ItemEntity.class, this.getBoundingBox().inflate(radius, this.level().paperConfig().entities.behavior.onlyMergeItemsHorizontally ? 0 : radius - 0.5D, radius), (entityitem) -> { // Paper - configuration to only merge items horizontally
-+                // Spigot end
-                 return entityitem != this && entityitem.isMergable();
-             });
-             Iterator iterator = list.iterator();
-@@ -238,6 +296,14 @@
-                 ItemEntity entityitem = (ItemEntity) iterator.next();
- 
-                 if (entityitem.isMergable()) {
-+                    // Paper start - Fix items merging through walls
-+                    if (this.level().paperConfig().fixes.fixItemsMergingThroughWalls) {
-+                        if (this.level().clipDirect(this.position(), entityitem.position(),
-+                            net.minecraft.world.phys.shapes.CollisionContext.of(this)) == net.minecraft.world.phys.HitResult.Type.BLOCK) {
-+                            continue;
-+                        }
-+                    }
-+                    // Paper end - Fix items merging through walls
-                     this.tryToMerge(entityitem);
-                     if (this.isRemoved()) {
-                         break;
-@@ -251,7 +317,7 @@
-     private boolean isMergable() {
-         ItemStack itemstack = this.getItem();
- 
--        return this.isAlive() && this.pickupDelay != 32767 && this.age != -32768 && this.age < 6000 && itemstack.getCount() < itemstack.getMaxStackSize();
-+        return this.isAlive() && this.pickupDelay != 32767 && this.age != -32768 && this.age < this.despawnRate && itemstack.getCount() < itemstack.getMaxStackSize(); // Paper - Alternative item-despawn-rate
-     }
- 
-     private void tryToMerge(ItemEntity other) {
-@@ -259,7 +325,7 @@
-         ItemStack itemstack1 = other.getItem();
- 
-         if (Objects.equals(this.target, other.target) && ItemEntity.areMergable(itemstack, itemstack1)) {
--            if (itemstack1.getCount() < itemstack.getCount()) {
-+            if (true || itemstack1.getCount() < itemstack.getCount()) { // Spigot
-                 ItemEntity.merge(this, itemstack, other, itemstack1);
-             } else {
-                 ItemEntity.merge(other, itemstack1, this, itemstack);
-@@ -287,11 +353,16 @@
-     }
- 
-     private static void merge(ItemEntity targetEntity, ItemStack targetStack, ItemEntity sourceEntity, ItemStack sourceStack) {
-+        // CraftBukkit start
-+        if (!CraftEventFactory.callItemMergeEvent(sourceEntity, targetEntity)) {
-+            return;
-+        }
-+        // CraftBukkit end
-         ItemEntity.merge(targetEntity, targetStack, sourceStack);
-         targetEntity.pickupDelay = Math.max(targetEntity.pickupDelay, sourceEntity.pickupDelay);
-         targetEntity.age = Math.min(targetEntity.age, sourceEntity.age);
-         if (sourceStack.isEmpty()) {
--            sourceEntity.discard();
-+            sourceEntity.discard(EntityRemoveEvent.Cause.MERGE); // CraftBukkit - add Bukkit remove cause);
-         }
- 
-     }
-@@ -320,12 +391,17 @@
-         } else if (!this.getItem().canBeHurtBy(source)) {
-             return false;
-         } else {
-+            // CraftBukkit start
-+            if (CraftEventFactory.handleNonLivingEntityDamageEvent(this, source, amount)) {
-+                return false;
-+            }
-+            // CraftBukkit end
-             this.markHurt();
-             this.health = (int) ((float) this.health - amount);
-             this.gameEvent(GameEvent.ENTITY_DAMAGE, source.getEntity());
-             if (this.health <= 0) {
-                 this.getItem().onDestroyed(this);
--                this.discard();
-+                this.discard(EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause
-             }
- 
-             return true;
-@@ -339,6 +415,11 @@
- 
-     @Override
-     public void addAdditionalSaveData(CompoundTag nbt) {
-+        // Paper start - Friction API
-+        if (this.frictionState != net.kyori.adventure.util.TriState.NOT_SET) {
-+            nbt.putString("Paper.FrictionState", this.frictionState.toString());
-+        }
-+        // Paper end - Friction API
-         nbt.putShort("Health", (short) this.health);
-         nbt.putShort("Age", (short) this.age);
-         nbt.putShort("PickupDelay", (short) this.pickupDelay);
-@@ -381,23 +462,98 @@
-             this.setItem(ItemStack.EMPTY);
-         }
- 
-+        // Paper start - Friction API
-+        if (nbt.contains("Paper.FrictionState")) {
-+            String fs = nbt.getString("Paper.FrictionState");
-+            try {
-+                frictionState = net.kyori.adventure.util.TriState.valueOf(fs);
-+            } catch (Exception ignored) {
-+                com.mojang.logging.LogUtils.getLogger().error("Unknown friction state " + fs + " for " + this);
-+            }
-+        }
-+        // Paper end - Friction API
-+
-         if (this.getItem().isEmpty()) {
--            this.discard();
-+            this.discard(null); // CraftBukkit - add Bukkit remove cause
-         }
- 
-     }
- 
-     @Override
--    public void playerTouch(Player player) {
-+    public void playerTouch(net.minecraft.world.entity.player.Player player) {
-         if (!this.level().isClientSide) {
-             ItemStack itemstack = this.getItem();
-             Item item = itemstack.getItem();
-             int i = itemstack.getCount();
-+
-+            // CraftBukkit start - fire PlayerPickupItemEvent
-+            int canHold = player.getInventory().canHold(itemstack);
-+            int remaining = i - canHold;
-+            boolean flyAtPlayer = false; // Paper
-+
-+            // Paper start - PlayerAttemptPickupItemEvent
-+            if (this.pickupDelay <= 0) {
-+                PlayerAttemptPickupItemEvent attemptEvent = new PlayerAttemptPickupItemEvent((org.bukkit.entity.Player) player.getBukkitEntity(), (org.bukkit.entity.Item) this.getBukkitEntity(), remaining);
-+                this.level().getCraftServer().getPluginManager().callEvent(attemptEvent);
-+
-+                flyAtPlayer = attemptEvent.getFlyAtPlayer();
-+                if (attemptEvent.isCancelled()) {
-+                    if (flyAtPlayer) {
-+                        player.take(this, i);
-+                    }
-+
-+                    return;
-+                }
-+            }
-+            // Paper end - PlayerAttemptPickupItemEvent
- 
-+            if (this.pickupDelay <= 0 && canHold > 0) {
-+                itemstack.setCount(canHold);
-+                // Call legacy event
-+                PlayerPickupItemEvent playerEvent = new PlayerPickupItemEvent((Player) player.getBukkitEntity(), (org.bukkit.entity.Item) this.getBukkitEntity(), remaining);
-+                playerEvent.setCancelled(!playerEvent.getPlayer().getCanPickupItems());
-+                this.level().getCraftServer().getPluginManager().callEvent(playerEvent);
-+                flyAtPlayer = playerEvent.getFlyAtPlayer(); // Paper
-+                if (playerEvent.isCancelled()) {
-+                    itemstack.setCount(i); // SPIGOT-5294 - restore count
-+                    // Paper start
-+                    if (flyAtPlayer) {
-+                        player.take(this, i);
-+                    }
-+                    // Paper end
-+                    return;
-+                }
-+
-+                // Call newer event afterwards
-+                EntityPickupItemEvent entityEvent = new EntityPickupItemEvent((Player) player.getBukkitEntity(), (org.bukkit.entity.Item) this.getBukkitEntity(), remaining);
-+                entityEvent.setCancelled(!entityEvent.getEntity().getCanPickupItems());
-+                this.level().getCraftServer().getPluginManager().callEvent(entityEvent);
-+                if (entityEvent.isCancelled()) {
-+                    itemstack.setCount(i); // SPIGOT-5294 - restore count
-+                    return;
-+                }
-+
-+                // Update the ItemStack if it was changed in the event
-+                ItemStack current = this.getItem();
-+                if (!itemstack.equals(current)) {
-+                    itemstack = current;
-+                } else {
-+                    itemstack.setCount(canHold + remaining); // = i
-+                }
-+
-+                // Possibly < 0; fix here so we do not have to modify code below
-+                this.pickupDelay = 0;
-+            } else if (this.pickupDelay == 0) {
-+                // ensure that the code below isn't triggered if canHold says we can't pick the items up
-+                this.pickupDelay = -1;
-+            }
-+            // CraftBukkit end
-+
-             if (this.pickupDelay == 0 && (this.target == null || this.target.equals(player.getUUID())) && player.getInventory().add(itemstack)) {
-+                if (flyAtPlayer) // Paper - PlayerPickupItemEvent
-                 player.take(this, i);
-                 if (itemstack.isEmpty()) {
--                    this.discard();
-+                    this.discard(EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
-                     itemstack.setCount(i);
-                 }
- 
-@@ -438,6 +594,7 @@
- 
-     public void setItem(ItemStack stack) {
-         this.getEntityData().set(ItemEntity.DATA_ITEM, stack);
-+        this.despawnRate = this.level().paperConfig().entities.spawning.altItemDespawnRate.enabled ? this.level().paperConfig().entities.spawning.altItemDespawnRate.items.getOrDefault(stack.getItem(), this.level().spigotConfig.itemDespawnRate) : this.level().spigotConfig.itemDespawnRate; // Paper - Alternative item-despawn-rate
-     }
- 
-     @Override
-@@ -492,7 +649,7 @@
- 
-     public void makeFakeItem() {
-         this.setNeverPickUp();
--        this.age = 5999;
-+        this.age = this.despawnRate - 1; // Spigot // Paper - Alternative item-despawn-rate
-     }
- 
-     public static float getSpin(float f, float f1) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/AbstractSkeleton.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/AbstractSkeleton.java.patch
deleted file mode 100644
index d726409faf..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/AbstractSkeleton.java.patch
+++ /dev/null
@@ -1,74 +0,0 @@
---- a/net/minecraft/world/entity/monster/AbstractSkeleton.java
-+++ b/net/minecraft/world/entity/monster/AbstractSkeleton.java
-@@ -97,9 +97,15 @@
- 
-     abstract SoundEvent getStepSound();
- 
-+    // Paper start - shouldBurnInDay API
-+    private boolean shouldBurnInDay = true;
-+    public boolean shouldBurnInDay() { return shouldBurnInDay; }
-+    public void setShouldBurnInDay(boolean shouldBurnInDay) { this.shouldBurnInDay = shouldBurnInDay; }
-+    // Paper end - shouldBurnInDay API
-+
-     @Override
-     public void aiStep() {
--        boolean flag = this.isSunBurnTick();
-+        boolean flag = shouldBurnInDay && this.isSunBurnTick(); // Paper - shouldBurnInDay API
- 
-         if (flag) {
-             ItemStack itemstack = this.getItemBySlot(EquipmentSlot.HEAD);
-@@ -152,7 +158,7 @@
-         this.populateDefaultEquipmentSlots(randomsource, difficulty);
-         this.populateDefaultEquipmentEnchantments(world, randomsource, difficulty);
-         this.reassessWeaponGoal();
--        this.setCanPickUpLoot(randomsource.nextFloat() < 0.55F * difficulty.getSpecialMultiplier());
-+        this.setCanPickUpLoot(this.level().paperConfig().entities.behavior.mobsCanAlwaysPickUpLoot.skeletons || randomsource.nextFloat() < 0.55F * difficulty.getSpecialMultiplier()); // Paper - Add world settings for mobs picking up loot
-         if (this.getItemBySlot(EquipmentSlot.HEAD).isEmpty()) {
-             LocalDate localdate = LocalDate.now();
-             int i = localdate.get(ChronoField.DAY_OF_MONTH);
-@@ -209,7 +215,17 @@
-         Level world = this.level();
- 
-         if (world instanceof ServerLevel worldserver) {
--            Projectile.spawnProjectileUsingShoot(entityarrow, worldserver, itemstack1, d0, d1 + d3 * 0.20000000298023224D, d2, 1.6F, (float) (14 - worldserver.getDifficulty().getId() * 4));
-+            // CraftBukkit start
-+            org.bukkit.event.entity.EntityShootBowEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityShootBowEvent(this, this.getMainHandItem(), entityarrow.getPickupItem(), entityarrow, net.minecraft.world.InteractionHand.MAIN_HAND, 0.8F, true); // Paper - improve entity shhot bow event - add arrow stack to event
-+            if (event.isCancelled()) {
-+                event.getProjectile().remove();
-+                return;
-+            }
-+
-+            if (event.getProjectile() == entityarrow.getBukkitEntity()) {
-+                Projectile.spawnProjectileUsingShoot(entityarrow, worldserver, itemstack1, d0, d1 + d3 * 0.20000000298023224D, d2, 1.6F, (float) (14 - worldserver.getDifficulty().getId() * 4));
-+            }
-+            // CraftBukkit end
-         }
- 
-         this.playSound(SoundEvents.SKELETON_SHOOT, 1.0F, 1.0F / (this.getRandom().nextFloat() * 0.4F + 0.8F));
-@@ -233,11 +249,24 @@
-     public void readAdditionalSaveData(CompoundTag nbt) {
-         super.readAdditionalSaveData(nbt);
-         this.reassessWeaponGoal();
-+        // Paper start - shouldBurnInDay API
-+        if (nbt.contains("Paper.ShouldBurnInDay")) {
-+            this.shouldBurnInDay = nbt.getBoolean("Paper.ShouldBurnInDay");
-+        }
-+        // Paper end - shouldBurnInDay API
-     }
- 
-+    // Paper start - shouldBurnInDay API
-     @Override
--    public void setItemSlot(EquipmentSlot slot, ItemStack stack) {
--        super.setItemSlot(slot, stack);
-+    public void addAdditionalSaveData(CompoundTag nbt) {
-+        super.addAdditionalSaveData(nbt);
-+        nbt.putBoolean("Paper.ShouldBurnInDay", this.shouldBurnInDay);
-+    }
-+    // Paper end - shouldBurnInDay API
-+
-+    @Override
-+    public void setItemSlot(EquipmentSlot slot, ItemStack stack, boolean silent) { // Paper - Fix silent equipment change
-+        super.setItemSlot(slot, stack, silent); // Paper - Fix silent equipment change
-         if (!this.level().isClientSide) {
-             this.reassessWeaponGoal();
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Bogged.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Bogged.java.patch
deleted file mode 100644
index 1cd68614bd..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Bogged.java.patch
+++ /dev/null
@@ -1,72 +0,0 @@
---- a/net/minecraft/world/entity/monster/Bogged.java
-+++ b/net/minecraft/world/entity/monster/Bogged.java
-@@ -27,6 +27,7 @@
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.level.storage.loot.BuiltInLootTables;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
- 
- public class Bogged extends AbstractSkeleton implements Shearable {
- 
-@@ -79,7 +80,20 @@
-             if (world instanceof ServerLevel) {
-                 ServerLevel worldserver = (ServerLevel) world;
- 
--                this.shear(worldserver, SoundSource.PLAYERS, itemstack);
-+                // CraftBukkit start
-+                // Paper start - custom shear drops
-+                java.util.List<ItemStack> drops = this.generateDefaultDrops(worldserver, itemstack);
-+                org.bukkit.event.player.PlayerShearEntityEvent event = CraftEventFactory.handlePlayerShearEntityEvent(player, this, itemstack, hand, drops);
-+                if (event != null) {
-+                    if (event.isCancelled()) {
-+                        // this.getEntityData().markDirty(Bogged.DATA_SHEARED); // CraftBukkit - mark dirty to restore sheared state to clients // Paper - no longer needed
-+                        return InteractionResult.PASS;
-+                    }
-+                    drops = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getDrops());
-+                // Paper end - custom shear drops
-+                }
-+                // CraftBukkit end
-+                this.shear(worldserver, SoundSource.PLAYERS, itemstack, drops); // Paper - custom shear drops
-                 this.gameEvent(GameEvent.SHEAR, player);
-                 itemstack.hurtAndBreak(1, player, getSlotForHand(hand));
-             }
-@@ -133,15 +147,36 @@
- 
-     @Override
-     public void shear(ServerLevel world, SoundSource shearedSoundCategory, ItemStack shears) {
-+    // Paper start - custom shear drops
-+        this.shear(world, shearedSoundCategory, shears, this.generateDefaultDrops(world, shears));
-+    }
-+
-+    @Override
-+    public java.util.List<ItemStack> generateDefaultDrops(final ServerLevel serverLevel, final ItemStack shears) {
-+        final java.util.List<ItemStack> drops = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>();
-+        this.dropFromShearingLootTable(serverLevel, BuiltInLootTables.BOGGED_SHEAR, shears, (ignored, stack) -> {
-+            drops.add(stack);
-+        });
-+        return drops;
-+    }
-+
-+    @Override
-+    public void shear(ServerLevel world, SoundSource shearedSoundCategory, ItemStack shears, java.util.List<ItemStack> drops) {
-+    // Paper end - custom shear drops
-         world.playSound((Player) null, (Entity) this, SoundEvents.BOGGED_SHEAR, shearedSoundCategory, 1.0F, 1.0F);
--        this.spawnShearedMushrooms(world, shears);
-+        this.spawnShearedMushrooms(world, shears, drops); // Paper - custom shear drops
-         this.setSheared(true);
-     }
- 
--    private void spawnShearedMushrooms(ServerLevel world, ItemStack shears) {
--        this.dropFromShearingLootTable(world, BuiltInLootTables.BOGGED_SHEAR, shears, (worldserver1, itemstack1) -> {
-+    // Paper start - custom shear drops
-+    private void spawnShearedMushrooms(ServerLevel world, ItemStack shears, java.util.List<ItemStack> drops) {
-+        final ServerLevel worldserver1 = world; // Named for lambda consumption
-+        this.forceDrops = true; // Paper - Add missing forceDrop toggles
-+        drops.forEach(itemstack1 -> {
-+    // Paper end - custom shear drops
-             this.spawnAtLocation(worldserver1, itemstack1, this.getBbHeight());
-         });
-+        this.forceDrops = false; // Paper - Add missing forceDrop toggles
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/CaveSpider.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/CaveSpider.java.patch
deleted file mode 100644
index c2449e2257..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/CaveSpider.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/entity/monster/CaveSpider.java
-+++ b/net/minecraft/world/entity/monster/CaveSpider.java
-@@ -40,7 +40,7 @@
-                 }
- 
-                 if (b0 > 0) {
--                    ((LivingEntity) target).addEffect(new MobEffectInstance(MobEffects.POISON, b0 * 20, 0), this);
-+                    ((LivingEntity) target).addEffect(new MobEffectInstance(MobEffects.POISON, b0 * 20, 0), this, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ATTACK); // CraftBukkit
-                 }
-             }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Creeper.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Creeper.java.patch
deleted file mode 100644
index ea4e74957f..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Creeper.java.patch
+++ /dev/null
@@ -1,132 +0,0 @@
---- a/net/minecraft/world/entity/monster/Creeper.java
-+++ b/net/minecraft/world/entity/monster/Creeper.java
-@@ -42,6 +42,13 @@
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.gameevent.GameEvent;
- 
-+// CraftBukkit start;
-+import org.bukkit.event.entity.CreatureSpawnEvent;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.event.entity.ExplosionPrimeEvent;
-+// CraftBukkit end
-+
- public class Creeper extends Monster {
- 
-     private static final EntityDataAccessor<Integer> DATA_SWELL_DIR = SynchedEntityData.defineId(Creeper.class, EntityDataSerializers.INT);
-@@ -52,6 +59,7 @@
-     public int maxSwell = 30;
-     public int explosionRadius = 3;
-     private int droppedSkulls;
-+    public Entity entityIgniter; // CraftBukkit
- 
-     public Creeper(EntityType<? extends Creeper> type, Level world) {
-         super(type, world);
-@@ -125,7 +133,7 @@
-         }
- 
-         if (nbt.getBoolean("ignited")) {
--            this.ignite();
-+            this.entityData.set(Creeper.DATA_IS_IGNITED, true); // Paper - set directly to avoid firing event
-         }
- 
-     }
-@@ -214,9 +222,20 @@
-     @Override
-     public void thunderHit(ServerLevel world, LightningBolt lightning) {
-         super.thunderHit(world, lightning);
-+        // CraftBukkit start
-+        if (CraftEventFactory.callCreeperPowerEvent(this, lightning, org.bukkit.event.entity.CreeperPowerEvent.PowerCause.LIGHTNING).isCancelled()) {
-+            return;
-+        }
-+        // CraftBukkit end
-         this.entityData.set(Creeper.DATA_IS_POWERED, true);
-     }
- 
-+    // CraftBukkit start
-+    public void setPowered(boolean powered) {
-+        this.entityData.set(Creeper.DATA_IS_POWERED, powered);
-+    }
-+    // CraftBukkit end
-+
-     @Override
-     protected InteractionResult mobInteract(Player player, InteractionHand hand) {
-         ItemStack itemstack = player.getItemInHand(hand);
-@@ -226,8 +245,9 @@
- 
-             this.level().playSound(player, this.getX(), this.getY(), this.getZ(), soundeffect, this.getSoundSource(), 1.0F, this.random.nextFloat() * 0.4F + 0.8F);
-             if (!this.level().isClientSide) {
-+                this.entityIgniter = player; // CraftBukkit
-                 this.ignite();
--                if (!itemstack.isDamageableItem()) {
-+                if (itemstack.getMaxDamage() == 0) { // CraftBukkit - fix MC-264285: unbreakable flint and steels are completely consumed when igniting a creeper
-                     itemstack.shrink(1);
-                 } else {
-                     itemstack.hurtAndBreak(1, player, getSlotForHand(hand));
-@@ -246,11 +266,21 @@
-         if (world instanceof ServerLevel worldserver) {
-             float f = this.isPowered() ? 2.0F : 1.0F;
- 
-+            // CraftBukkit start
-+            ExplosionPrimeEvent event = CraftEventFactory.callExplosionPrimeEvent(this, this.explosionRadius * f, false);
-+            if (!event.isCancelled()) {
-+            // CraftBukkit end
-             this.dead = true;
--            worldserver.explode(this, this.getX(), this.getY(), this.getZ(), (float) this.explosionRadius * f, Level.ExplosionInteraction.MOB);
-+            worldserver.explode(this, this.getX(), this.getY(), this.getZ(), event.getRadius(), event.getFire(), Level.ExplosionInteraction.MOB); // CraftBukkit // Paper - fix DamageSource API (revert to vanilla, no, just no, don't change this)
-             this.spawnLingeringCloud();
-             this.triggerOnDeathMobEffects(worldserver, Entity.RemovalReason.KILLED);
--            this.discard();
-+            this.discard(EntityRemoveEvent.Cause.EXPLODE); // CraftBukkit - add Bukkit remove cause
-+            // CraftBukkit start
-+            } else {
-+                this.swell = 0;
-+                this.entityData.set(DATA_IS_IGNITED, Boolean.valueOf(false)); // Paper
-+            }
-+            // CraftBukkit end
-         }
- 
-     }
-@@ -258,9 +288,10 @@
-     private void spawnLingeringCloud() {
-         Collection<MobEffectInstance> collection = this.getActiveEffects();
- 
--        if (!collection.isEmpty()) {
-+        if (!collection.isEmpty() && !this.level().paperConfig().entities.behavior.disableCreeperLingeringEffect) { // Paper - Option to disable creeper lingering effect
-             AreaEffectCloud entityareaeffectcloud = new AreaEffectCloud(this.level(), this.getX(), this.getY(), this.getZ());
- 
-+            entityareaeffectcloud.setOwner(this); // CraftBukkit
-             entityareaeffectcloud.setRadius(2.5F);
-             entityareaeffectcloud.setRadiusOnUse(-0.5F);
-             entityareaeffectcloud.setWaitTime(10);
-@@ -274,7 +305,7 @@
-                 entityareaeffectcloud.addEffect(new MobEffectInstance(mobeffect));
-             }
- 
--            this.level().addFreshEntity(entityareaeffectcloud);
-+            this.level().addFreshEntity(entityareaeffectcloud, CreatureSpawnEvent.SpawnReason.EXPLOSION); // CraftBukkit
-         }
- 
-     }
-@@ -284,9 +315,20 @@
-     }
- 
-     public void ignite() {
--        this.entityData.set(Creeper.DATA_IS_IGNITED, true);
-+        // Paper start - CreeperIgniteEvent
-+        setIgnited(true);
-     }
- 
-+    public void setIgnited(boolean ignited) {
-+        if (isIgnited() != ignited) {
-+            com.destroystokyo.paper.event.entity.CreeperIgniteEvent event = new com.destroystokyo.paper.event.entity.CreeperIgniteEvent((org.bukkit.entity.Creeper) getBukkitEntity(), ignited);
-+            if (event.callEvent()) {
-+                this.entityData.set(Creeper.DATA_IS_IGNITED, event.isIgnited());
-+            }
-+        }
-+        // Paper end - CreeperIgniteEvent
-+    }
-+
-     public boolean canDropMobsSkull() {
-         return this.isPowered() && this.droppedSkulls < 1;
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/ElderGuardian.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/ElderGuardian.java.patch
deleted file mode 100644
index b270c246e4..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/ElderGuardian.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/entity/monster/ElderGuardian.java
-+++ b/net/minecraft/world/entity/monster/ElderGuardian.java
-@@ -67,7 +67,7 @@
-         super.customServerAiStep(world);
-         if ((this.tickCount + this.getId()) % 1200 == 0) {
-             MobEffectInstance mobeffect = new MobEffectInstance(MobEffects.DIG_SLOWDOWN, 6000, 2);
--            List<ServerPlayer> list = MobEffectUtil.addEffectToPlayersAround(world, this, this.position(), 50.0D, mobeffect, 1200);
-+            List<ServerPlayer> list = MobEffectUtil.addEffectToPlayersAround(world, this, this.position(), 50.0D, mobeffect, 1200, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ATTACK, (player) -> new io.papermc.paper.event.entity.ElderGuardianAppearanceEvent((org.bukkit.entity.ElderGuardian) this.getBukkitEntity(), player.getBukkitEntity()).callEvent()); // CraftBukkit // Paper - Add ElderGuardianAppearanceEvent
- 
-             list.forEach((entityplayer) -> {
-                 entityplayer.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.GUARDIAN_ELDER_EFFECT, this.isSilent() ? 0.0F : 1.0F));
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/EnderMan.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/EnderMan.java.patch
deleted file mode 100644
index 512e3e45cf..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/EnderMan.java.patch
+++ /dev/null
@@ -1,157 +0,0 @@
---- a/net/minecraft/world/entity/monster/EnderMan.java
-+++ b/net/minecraft/world/entity/monster/EnderMan.java
-@@ -68,6 +68,10 @@
- import net.minecraft.world.phys.AABB;
- import net.minecraft.world.phys.BlockHitResult;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.EntityTargetEvent;
-+// CraftBukkit end
- 
- public class EnderMan extends Monster implements NeutralMob {
- 
-@@ -112,10 +116,26 @@
- 
-     @Override
-     public void setTarget(@Nullable LivingEntity target) {
--        super.setTarget(target);
-+        // CraftBukkit start - fire event
-+        this.setTarget(target, EntityTargetEvent.TargetReason.UNKNOWN, true);
-+    }
-+
-+    // Paper start - EndermanEscapeEvent
-+    private boolean tryEscape(com.destroystokyo.paper.event.entity.EndermanEscapeEvent.Reason reason) {
-+        return new com.destroystokyo.paper.event.entity.EndermanEscapeEvent((org.bukkit.craftbukkit.entity.CraftEnderman) this.getBukkitEntity(), reason).callEvent();
-+    }
-+    // Paper end - EndermanEscapeEvent
-+
-+    @Override
-+    public boolean setTarget(LivingEntity entityliving, EntityTargetEvent.TargetReason reason, boolean fireEvent) {
-+        if (!super.setTarget(entityliving, reason, fireEvent)) {
-+            return false;
-+        }
-+        entityliving = this.getTarget();
-+        // CraftBukkit end
-         AttributeInstance attributemodifiable = this.getAttribute(Attributes.MOVEMENT_SPEED);
- 
--        if (target == null) {
-+        if (entityliving == null) {
-             this.targetChangeTime = 0;
-             this.entityData.set(EnderMan.DATA_CREEPY, false);
-             this.entityData.set(EnderMan.DATA_STARED_AT, false);
-@@ -127,6 +147,7 @@
-                 attributemodifiable.addTransientModifier(EnderMan.SPEED_MODIFIER_ATTACKING);
-             }
-         }
-+        return true;
- 
-     }
- 
-@@ -212,6 +233,14 @@
-     }
- 
-     boolean isBeingStaredBy(Player player) {
-+        // Paper start - EndermanAttackPlayerEvent
-+        final boolean shouldAttack = isBeingStaredBy0(player);
-+        final com.destroystokyo.paper.event.entity.EndermanAttackPlayerEvent event = new com.destroystokyo.paper.event.entity.EndermanAttackPlayerEvent((org.bukkit.entity.Enderman) getBukkitEntity(), (org.bukkit.entity.Player) player.getBukkitEntity());
-+        event.setCancelled(!shouldAttack);
-+        return event.callEvent();
-+    }
-+    private boolean isBeingStaredBy0(Player player) {
-+        // Paper end - EndermanAttackPlayerEvent
-         return !LivingEntity.PLAYER_NOT_WEARING_DISGUISE_ITEM.test(player) ? false : this.isLookingAtMe(player, 0.025D, true, false, new double[]{this.getEyeY()});
-     }
- 
-@@ -241,7 +270,7 @@
-         if (world.isDay() && this.tickCount >= this.targetChangeTime + 600) {
-             float f = this.getLightLevelDependentMagicValue();
- 
--            if (f > 0.5F && world.canSeeSky(this.blockPosition()) && this.random.nextFloat() * 30.0F < (f - 0.4F) * 2.0F) {
-+            if (f > 0.5F && world.canSeeSky(this.blockPosition()) && this.random.nextFloat() * 30.0F < (f - 0.4F) * 2.0F && this.tryEscape(com.destroystokyo.paper.event.entity.EndermanEscapeEvent.Reason.RUNAWAY)) { // Paper - EndermanEscapeEvent
-                 this.setTarget((LivingEntity) null);
-                 this.teleport();
-             }
-@@ -367,11 +396,13 @@
-             } else {
-                 flag1 = flag && this.hurtWithCleanWater(world, source, (ThrownPotion) source.getDirectEntity(), amount);
- 
-+                if (this.tryEscape(com.destroystokyo.paper.event.entity.EndermanEscapeEvent.Reason.INDIRECT)) { // Paper - EndermanEscapeEvent
-                 for (int i = 0; i < 64; ++i) {
-                     if (this.teleport()) {
-                         return true;
-                     }
-                 }
-+                } // Paper - EndermanEscapeEvent
- 
-                 return flag1;
-             }
-@@ -397,6 +428,16 @@
-         this.entityData.set(EnderMan.DATA_STARED_AT, true);
-     }
- 
-+    // Paper start
-+    public void setCreepy(boolean creepy) {
-+        this.entityData.set(EnderMan.DATA_CREEPY, creepy);
-+    }
-+
-+    public void setHasBeenStaredAt(boolean hasBeenStaredAt) {
-+        this.entityData.set(EnderMan.DATA_STARED_AT, hasBeenStaredAt);
-+    }
-+    // Paper end
-+
-     @Override
-     public boolean requiresCustomPersistence() {
-         return super.requiresCustomPersistence() || this.getCarriedBlock() != null;
-@@ -457,7 +498,8 @@
-             int j = Mth.floor(this.enderman.getY() + randomsource.nextDouble() * 2.0D);
-             int k = Mth.floor(this.enderman.getZ() - 1.0D + randomsource.nextDouble() * 2.0D);
-             BlockPos blockposition = new BlockPos(i, j, k);
--            BlockState iblockdata = world.getBlockState(blockposition);
-+            BlockState iblockdata = world.getBlockStateIfLoaded(blockposition); // Paper - Prevent endermen from loading chunks
-+            if (iblockdata == null) return; // Paper - Prevent endermen from loading chunks
-             BlockPos blockposition1 = blockposition.below();
-             BlockState iblockdata1 = world.getBlockState(blockposition1);
-             BlockState iblockdata2 = this.enderman.getCarriedBlock();
-@@ -465,9 +507,11 @@
-             if (iblockdata2 != null) {
-                 iblockdata2 = Block.updateFromNeighbourShapes(iblockdata2, this.enderman.level(), blockposition);
-                 if (this.canPlaceBlock(world, blockposition, iblockdata2, iblockdata, iblockdata1, blockposition1)) {
-+                    if (CraftEventFactory.callEntityChangeBlockEvent(this.enderman, blockposition, iblockdata2)) { // CraftBukkit - Place event
-                     world.setBlock(blockposition, iblockdata2, 3);
-                     world.gameEvent((Holder) GameEvent.BLOCK_PLACE, blockposition, GameEvent.Context.of(this.enderman, iblockdata2));
-                     this.enderman.setCarriedBlock((BlockState) null);
-+                    } // CraftBukkit
-                 }
- 
-             }
-@@ -499,16 +543,19 @@
-             int j = Mth.floor(this.enderman.getY() + randomsource.nextDouble() * 3.0D);
-             int k = Mth.floor(this.enderman.getZ() - 2.0D + randomsource.nextDouble() * 4.0D);
-             BlockPos blockposition = new BlockPos(i, j, k);
--            BlockState iblockdata = world.getBlockState(blockposition);
-+            BlockState iblockdata = world.getBlockStateIfLoaded(blockposition); // Paper - Prevent endermen from loading chunks
-+            if (iblockdata == null) return; // Paper - Prevent endermen from loading chunks
-             Vec3 vec3d = new Vec3((double) this.enderman.getBlockX() + 0.5D, (double) j + 0.5D, (double) this.enderman.getBlockZ() + 0.5D);
-             Vec3 vec3d1 = new Vec3((double) i + 0.5D, (double) j + 0.5D, (double) k + 0.5D);
-             BlockHitResult movingobjectpositionblock = world.clip(new ClipContext(vec3d, vec3d1, ClipContext.Block.OUTLINE, ClipContext.Fluid.NONE, this.enderman));
-             boolean flag = movingobjectpositionblock.getBlockPos().equals(blockposition);
- 
-             if (iblockdata.is(BlockTags.ENDERMAN_HOLDABLE) && flag) {
-+                if (CraftEventFactory.callEntityChangeBlockEvent(this.enderman, blockposition, iblockdata.getFluidState().createLegacyBlock())) { // CraftBukkit - Place event // Paper - fix wrong block state
-                 world.removeBlock(blockposition, false);
-                 world.gameEvent((Holder) GameEvent.BLOCK_DESTROY, blockposition, GameEvent.Context.of(this.enderman, iblockdata));
-                 this.enderman.setCarriedBlock(iblockdata.getBlock().defaultBlockState());
-+                } // CraftBukkit
-             }
- 
-         }
-@@ -592,7 +639,7 @@
-             } else {
-                 if (this.target != null && !this.enderman.isPassenger()) {
-                     if (this.enderman.isBeingStaredBy((Player) this.target)) {
--                        if (this.target.distanceToSqr((Entity) this.enderman) < 16.0D) {
-+                        if (this.target.distanceToSqr((Entity) this.enderman) < 16.0D && this.enderman.tryEscape(com.destroystokyo.paper.event.entity.EndermanEscapeEvent.Reason.STARE)) { // Paper - EndermanEscapeEvent
-                             this.enderman.teleport();
-                         }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Endermite.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Endermite.java.patch
deleted file mode 100644
index 9a32ade2bd..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Endermite.java.patch
+++ /dev/null
@@ -1,21 +0,0 @@
---- a/net/minecraft/world/entity/monster/Endermite.java
-+++ b/net/minecraft/world/entity/monster/Endermite.java
-@@ -24,6 +24,9 @@
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.LevelAccessor;
- import net.minecraft.world.level.block.state.BlockState;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public class Endermite extends Monster {
- 
-@@ -113,7 +116,7 @@
-             }
- 
-             if (this.life >= 2400) {
--                this.discard();
-+                this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-             }
-         }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Evoker.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Evoker.java.patch
deleted file mode 100644
index d8921518a6..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Evoker.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/entity/monster/Evoker.java
-+++ b/net/minecraft/world/entity/monster/Evoker.java
-@@ -212,7 +212,7 @@
-                         worldserver.getScoreboard().addPlayerToTeam(entityvex.getScoreboardName(), scoreboardteam);
-                     }
- 
--                    worldserver.addFreshEntityWithPassengers(entityvex);
-+                    worldserver.addFreshEntityWithPassengers(entityvex, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.SPELL); // CraftBukkit - Add SpawnReason
-                     worldserver.gameEvent((Holder) GameEvent.ENTITY_PLACE, blockposition, GameEvent.Context.of((Entity) Evoker.this));
-                 }
-             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Ghast.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Ghast.java.patch
deleted file mode 100644
index e91f483915..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Ghast.java.patch
+++ /dev/null
@@ -1,25 +0,0 @@
---- a/net/minecraft/world/entity/monster/Ghast.java
-+++ b/net/minecraft/world/entity/monster/Ghast.java
-@@ -64,7 +64,13 @@
- 
-     public int getExplosionPower() {
-         return this.explosionPower;
-+    }
-+
-+    // Paper start
-+    public void setExplosionPower(int explosionPower) {
-+        this.explosionPower = explosionPower;
-     }
-+    // Paper end
- 
-     @Override
-     protected boolean shouldDespawnInPeaceful() {
-@@ -333,6 +339,8 @@
- 
-                         LargeFireball entitylargefireball = new LargeFireball(world, this.ghast, vec3d1.normalize(), this.ghast.getExplosionPower());
- 
-+                        // CraftBukkit - set bukkitYield when setting explosionpower
-+                        entitylargefireball.bukkitYield = entitylargefireball.explosionPower = this.ghast.getExplosionPower();
-                         entitylargefireball.setPos(this.ghast.getX() + vec3d.x * 4.0D, this.ghast.getY(0.5D) + 0.5D, entitylargefireball.getZ() + vec3d.z * 4.0D);
-                         world.addFreshEntity(entitylargefireball);
-                         this.chargeTime = -40;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Husk.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Husk.java.patch
deleted file mode 100644
index 0df55d8f37..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Husk.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/entity/monster/Husk.java
-+++ b/net/minecraft/world/entity/monster/Husk.java
-@@ -59,7 +59,7 @@
-         if (flag && this.getMainHandItem().isEmpty() && target instanceof LivingEntity) {
-             float f = this.level().getCurrentDifficultyAt(this.blockPosition()).getEffectiveDifficulty();
- 
--            ((LivingEntity) target).addEffect(new MobEffectInstance(MobEffects.HUNGER, 140 * (int) f), this);
-+            ((LivingEntity) target).addEffect(new MobEffectInstance(MobEffects.HUNGER, 140 * (int) f), this, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ATTACK); // CraftBukkit
-         }
- 
-         return flag;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Phantom.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Phantom.java.patch
deleted file mode 100644
index ca42f84c2e..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Phantom.java.patch
+++ /dev/null
@@ -1,78 +0,0 @@
---- a/net/minecraft/world/entity/monster/Phantom.java
-+++ b/net/minecraft/world/entity/monster/Phantom.java
-@@ -139,7 +139,7 @@
- 
-     @Override
-     public void aiStep() {
--        if (this.isAlive() && this.isSunBurnTick()) {
-+        if (this.isAlive() && this.shouldBurnInDay && this.isSunBurnTick()) { // Paper - shouldBurnInDay API
-             this.igniteForSeconds(8.0F);
-         }
- 
-@@ -161,6 +161,14 @@
-         }
- 
-         this.setPhantomSize(nbt.getInt("Size"));
-+        // Paper start
-+        if (nbt.hasUUID("Paper.SpawningEntity")) {
-+            this.spawningEntity = nbt.getUUID("Paper.SpawningEntity");
-+        }
-+        if (nbt.contains("Paper.ShouldBurnInDay")) {
-+            this.shouldBurnInDay = nbt.getBoolean("Paper.ShouldBurnInDay");
-+        }
-+        // Paper end
-     }
- 
-     @Override
-@@ -170,6 +178,12 @@
-         nbt.putInt("AY", this.anchorPoint.getY());
-         nbt.putInt("AZ", this.anchorPoint.getZ());
-         nbt.putInt("Size", this.getPhantomSize());
-+        // Paper start
-+        if (this.spawningEntity != null) {
-+            nbt.putUUID("Paper.SpawningEntity", this.spawningEntity);
-+        }
-+        nbt.putBoolean("Paper.ShouldBurnInDay", shouldBurnInDay);
-+        // Paper end
-     }
- 
-     @Override
-@@ -219,6 +233,20 @@
-         return predicate.test(world, this, target);
-     }
- 
-+    // Paper start
-+    @Nullable
-+    java.util.UUID spawningEntity;
-+
-+    @Nullable
-+    public java.util.UUID getSpawningEntity() {
-+        return this.spawningEntity;
-+    }
-+    public void setSpawningEntity(java.util.UUID entity) { this.spawningEntity = entity; }
-+    private boolean shouldBurnInDay = true;
-+    public boolean shouldBurnInDay() { return shouldBurnInDay; }
-+    public void setShouldBurnInDay(boolean shouldBurnInDay) { this.shouldBurnInDay = shouldBurnInDay; }
-+    // Paper end
-+
-     private static enum AttackPhase {
- 
-         CIRCLE, SWOOP;
-@@ -522,14 +550,15 @@
-                 List<Player> list = worldserver.getNearbyPlayers(this.attackTargeting, Phantom.this, Phantom.this.getBoundingBox().inflate(16.0D, 64.0D, 16.0D));
- 
-                 if (!list.isEmpty()) {
--                    list.sort(Comparator.comparing(Entity::getY).reversed());
-+                    list.sort(Comparator.comparing((Entity e) -> { return e.getY(); }).reversed()); // CraftBukkit - decompile error
-                     Iterator iterator = list.iterator();
- 
-                     while (iterator.hasNext()) {
-                         Player entityhuman = (Player) iterator.next();
- 
-                         if (Phantom.this.canAttack(worldserver, entityhuman, TargetingConditions.DEFAULT)) {
--                            Phantom.this.setTarget(entityhuman);
-+                            if (!level().paperConfig().entities.behavior.phantomsOnlyAttackInsomniacs || EntitySelector.IS_INSOMNIAC.test(entityhuman)) // Paper - Add phantom creative and insomniac controls
-+                            Phantom.this.setTarget(entityhuman, org.bukkit.event.entity.EntityTargetEvent.TargetReason.CLOSEST_PLAYER, true); // CraftBukkit - reason
-                             return true;
-                         }
-                     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Pillager.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Pillager.java.patch
deleted file mode 100644
index 43bd8b72e6..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Pillager.java.patch
+++ /dev/null
@@ -1,21 +0,0 @@
---- a/net/minecraft/world/entity/monster/Pillager.java
-+++ b/net/minecraft/world/entity/monster/Pillager.java
-@@ -52,6 +52,9 @@
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.LevelReader;
- import net.minecraft.world.level.ServerLevelAccessor;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public class Pillager extends AbstractIllager implements CrossbowAttackMob, InventoryCarrier {
- 
-@@ -206,7 +209,7 @@
-             ItemStack itemstack1 = this.inventory.addItem(itemstack);
- 
-             if (itemstack1.isEmpty()) {
--                itemEntity.discard();
-+                itemEntity.discard(EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
-             } else {
-                 itemstack.setCount(itemstack1.getCount());
-             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Ravager.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Ravager.java.patch
deleted file mode 100644
index 1cbab8cd90..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Ravager.java.patch
+++ /dev/null
@@ -1,41 +0,0 @@
---- a/net/minecraft/world/entity/monster/Ravager.java
-+++ b/net/minecraft/world/entity/monster/Ravager.java
-@@ -42,6 +42,9 @@
- import net.minecraft.world.level.pathfinder.PathType;
- import net.minecraft.world.phys.AABB;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+// CraftBukkit end
- 
- public class Ravager extends Raider {
- 
-@@ -158,12 +161,19 @@
-                         Block block = iblockdata.getBlock();
- 
-                         if (block instanceof LeavesBlock) {
-+                            // CraftBukkit start
-+                            if (!CraftEventFactory.callEntityChangeBlockEvent(this, blockposition, iblockdata.getFluidState().createLegacyBlock())) { // Paper - fix wrong block state
-+                                continue;
-+                            }
-+                            // CraftBukkit end
-                             flag = worldserver.destroyBlock(blockposition, true, this) || flag;
-                         }
-                     }
- 
-                     if (!flag && this.onGround()) {
-+                        if (new com.destroystokyo.paper.event.entity.EntityJumpEvent(getBukkitLivingEntity()).callEvent()) { // Paper - Entity Jump API
-                         this.jumpFromGround();
-+                        } else { this.setJumping(false); } // Paper - Entity Jump API; setJumping(false) stops a potential loop
-                     }
-                 }
-             }
-@@ -281,7 +291,7 @@
-         double d1 = entity.getZ() - this.getZ();
-         double d2 = Math.max(d0 * d0 + d1 * d1, 0.001D);
- 
--        entity.push(d0 / d2 * 4.0D, 0.2D, d1 / d2 * 4.0D);
-+        entity.push(d0 / d2 * 4.0D, 0.2D, d1 / d2 * 4.0D, this); // Paper - Add EntityKnockbackByEntityEvent and EntityPushedByEntityAttackEvent
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Shulker.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Shulker.java.patch
deleted file mode 100644
index fb73125321..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Shulker.java.patch
+++ /dev/null
@@ -1,59 +0,0 @@
---- a/net/minecraft/world/entity/monster/Shulker.java
-+++ b/net/minecraft/world/entity/monster/Shulker.java
-@@ -59,6 +59,12 @@
- import net.minecraft.world.phys.Vec3;
- import org.joml.Vector3f;
- 
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.util.CraftLocation;
-+import org.bukkit.event.entity.EntityTeleportEvent;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+// CraftBukkit end
-+
- public class Shulker extends AbstractGolem implements VariantHolder<Optional<DyeColor>>, Enemy {
- 
-     private static final ResourceLocation COVERED_ARMOR_MODIFIER_ID = ResourceLocation.withDefaultNamespace("covered");
-@@ -283,7 +289,13 @@
- 
-     @Override
-     public void stopRiding() {
--        super.stopRiding();
-+        // Paper start - Force entity dismount during teleportation
-+        this.stopRiding(false);
-+    }
-+    @Override
-+    public void stopRiding(boolean suppressCancellation) {
-+        super.stopRiding(suppressCancellation);
-+        // Paper end - Force entity dismount during teleportation
-         if (this.level().isClientSide) {
-             this.clientOldAttachPosition = this.blockPosition();
-         }
-@@ -402,6 +414,14 @@
-                     Direction enumdirection = this.findAttachableSurface(blockposition1);
- 
-                     if (enumdirection != null) {
-+                        // CraftBukkit start
-+                        EntityTeleportEvent teleportEvent = CraftEventFactory.callEntityTeleportEvent(this, blockposition1.getX(), blockposition1.getY(), blockposition1.getZ());
-+                        if (teleportEvent.isCancelled() || teleportEvent.getTo() == null) { // Paper
-+                            return false;
-+                        } else {
-+                            blockposition1 = CraftLocation.toBlockPosition(teleportEvent.getTo());
-+                        }
-+                        // CraftBukkit end
-                         this.unRide();
-                         this.setAttachFace(enumdirection);
-                         this.playSound(SoundEvents.SHULKER_TELEPORT, 1.0F, 1.0F);
-@@ -472,7 +492,12 @@
-                 if (entityshulker != null) {
-                     entityshulker.setVariant(this.getVariant());
-                     entityshulker.moveTo(vec3d);
--                    this.level().addFreshEntity(entityshulker);
-+                    // Paper start - Shulker duplicate event
-+                    if (!new io.papermc.paper.event.entity.ShulkerDuplicateEvent((org.bukkit.entity.Shulker) entityshulker.getBukkitEntity(), (org.bukkit.entity.Shulker) this.getBukkitEntity()).callEvent()) {
-+                        return;
-+                    }
-+                    // Paper end - Shulker duplicate event
-+                    this.level().addFreshEntity(entityshulker, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.BREEDING); // CraftBukkit - the mysteries of life
-                 }
- 
-             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Silverfish.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Silverfish.java.patch
deleted file mode 100644
index b031571b09..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Silverfish.java.patch
+++ /dev/null
@@ -1,51 +0,0 @@
---- a/net/minecraft/world/entity/monster/Silverfish.java
-+++ b/net/minecraft/world/entity/monster/Silverfish.java
-@@ -30,6 +30,10 @@
- import net.minecraft.world.level.block.Block;
- import net.minecraft.world.level.block.InfestedBlock;
- import net.minecraft.world.level.block.state.BlockState;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public class Silverfish extends Monster {
- 
-@@ -119,7 +123,7 @@
-         } else {
-             Player entityhuman = world.getNearestPlayer((double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D, 5.0D, true);
- 
--            return entityhuman == null;
-+            return !(entityhuman != null && !entityhuman.affectsSpawning) && entityhuman == null; // Paper - Affects Spawning API
-         }
-     }
- 
-@@ -160,6 +164,12 @@
-                             Block block = iblockdata.getBlock();
- 
-                             if (block instanceof InfestedBlock) {
-+                                // CraftBukkit start
-+                                BlockState afterState = getServerLevel(world).getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) ? iblockdata.getFluidState().createLegacyBlock() : ((InfestedBlock) block).hostStateByInfested(world.getBlockState(blockposition1)); // Paper - fix wrong block state
-+                                if (!CraftEventFactory.callEntityChangeBlockEvent(this.silverfish, blockposition1, afterState)) { // Paper - fix wrong block state
-+                                    continue;
-+                                }
-+                                // CraftBukkit end
-                                 if (getServerLevel(world).getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) {
-                                     world.destroyBlock(blockposition1, true, this.silverfish);
-                                 } else {
-@@ -229,9 +239,14 @@
-                 BlockState iblockdata = world.getBlockState(blockposition);
- 
-                 if (InfestedBlock.isCompatibleHostBlock(iblockdata)) {
-+                    // CraftBukkit start
-+                    if (!CraftEventFactory.callEntityChangeBlockEvent(this.mob, blockposition, InfestedBlock.infestedStateByHost(iblockdata))) {
-+                        return;
-+                    }
-+                    // CraftBukkit end
-                     world.setBlock(blockposition, InfestedBlock.infestedStateByHost(iblockdata), 3);
-                     this.mob.spawnAnim();
--                    this.mob.discard();
-+                    this.mob.discard(EntityRemoveEvent.Cause.ENTER_BLOCK); // CraftBukkit - add Bukkit remove cause
-                 }
- 
-             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Slime.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Slime.java.patch
deleted file mode 100644
index 4ee5279ca5..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Slime.java.patch
+++ /dev/null
@@ -1,239 +0,0 @@
---- a/net/minecraft/world/entity/monster/Slime.java
-+++ b/net/minecraft/world/entity/monster/Slime.java
-@@ -46,6 +46,14 @@
- import net.minecraft.world.level.levelgen.WorldgenRandom;
- import net.minecraft.world.phys.Vec3;
- import net.minecraft.world.scores.PlayerTeam;
-+// CraftBukkit start
-+import java.util.ArrayList;
-+import java.util.List;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.event.entity.EntityTransformEvent;
-+import org.bukkit.event.entity.SlimeSplitEvent;
-+// CraftBukkit end
- 
- public class Slime extends Mob implements Enemy {
- 
-@@ -111,6 +119,7 @@
-     @Override
-     public void addAdditionalSaveData(CompoundTag nbt) {
-         super.addAdditionalSaveData(nbt);
-+        nbt.putBoolean("Paper.canWander", this.canWander); // Paper
-         nbt.putInt("Size", this.getSize() - 1);
-         nbt.putBoolean("wasOnGround", this.wasOnGround);
-     }
-@@ -119,6 +128,11 @@
-     public void readAdditionalSaveData(CompoundTag nbt) {
-         this.setSize(nbt.getInt("Size") + 1, false);
-         super.readAdditionalSaveData(nbt);
-+        // Paper start
-+        if (nbt.contains("Paper.canWander")) {
-+            this.canWander = nbt.getBoolean("Paper.canWander");
-+        }
-+        // Paper end
-         this.wasOnGround = nbt.getBoolean("wasOnGround");
-     }
- 
-@@ -197,11 +211,18 @@
- 
-     @Override
-     public EntityType<? extends Slime> getType() {
--        return super.getType();
-+        return (EntityType<? extends Slime>) super.getType(); // CraftBukkit - decompile error
-     }
- 
-     @Override
-     public void remove(Entity.RemovalReason reason) {
-+        // CraftBukkit start - add Bukkit remove cause
-+        this.remove(reason, null);
-+    }
-+
-+    @Override
-+    public void remove(Entity.RemovalReason entity_removalreason, EntityRemoveEvent.Cause cause) {
-+        // CraftBukkit end
-         int i = this.getSize();
- 
-         if (!this.level().isClientSide && i > 1 && this.isDeadOrDying()) {
-@@ -210,19 +231,47 @@
-             int j = i / 2;
-             int k = 2 + this.random.nextInt(3);
-             PlayerTeam scoreboardteam = this.getTeam();
-+
-+            // CraftBukkit start
-+            SlimeSplitEvent event = new SlimeSplitEvent((org.bukkit.entity.Slime) this.getBukkitEntity(), k);
-+            this.level().getCraftServer().getPluginManager().callEvent(event);
-+
-+            if (!event.isCancelled() && event.getCount() > 0) {
-+                k = event.getCount();
-+            } else {
-+                super.remove(entity_removalreason, cause); // CraftBukkit - add Bukkit remove cause
-+                return;
-+            }
-+            List<LivingEntity> slimes = new ArrayList<>(j);
-+            // CraftBukkit end
- 
-             for (int l = 0; l < k; ++l) {
-                 float f2 = ((float) (l % 2) - 0.5F) * f1;
-                 float f3 = ((float) (l / 2) - 0.5F) * f1;
- 
--                this.convertTo(this.getType(), new ConversionParams(ConversionType.SPLIT_ON_DEATH, false, false, scoreboardteam), EntitySpawnReason.TRIGGERED, (entityslime) -> {
-+                Slime converted = this.convertTo(this.getType(), new ConversionParams(ConversionType.SPLIT_ON_DEATH, false, false, scoreboardteam), EntitySpawnReason.TRIGGERED, (entityslime) -> { // CraftBukkit
-+                    entityslime.aware = this.aware; // Paper - Fix nerfed slime when splitting
-                     entityslime.setSize(j, true);
-                     entityslime.moveTo(this.getX() + (double) f2, this.getY() + 0.5D, this.getZ() + (double) f3, this.random.nextFloat() * 360.0F, 0.0F);
--                });
-+                // CraftBukkit start
-+                }, null, null);
-+                if (converted != null) {
-+                    slimes.add(converted);
-+                }
-+                // CraftBukkit end
-             }
-+            // CraftBukkit start
-+            if (CraftEventFactory.callEntityTransformEvent(this, slimes, EntityTransformEvent.TransformReason.SPLIT).isCancelled()) {
-+                super.remove(entity_removalreason, cause); // CraftBukkit - add Bukkit remove cause
-+                return;
-+            }
-+            for (LivingEntity living : slimes) {
-+                this.level().addFreshEntity(living, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.SLIME_SPLIT); // CraftBukkit - SpawnReason
-+            }
-+            // CraftBukkit end
-         }
- 
--        super.remove(reason);
-+        super.remove(entity_removalreason, cause); // CraftBukkit - add Bukkit remove cause
-     }
- 
-     @Override
-@@ -291,7 +340,11 @@
-                 return checkMobSpawnRules(type, world, spawnReason, pos, random);
-             }
- 
--            if (world.getBiome(pos).is(BiomeTags.ALLOWS_SURFACE_SLIME_SPAWNS) && pos.getY() > 50 && pos.getY() < 70 && random.nextFloat() < 0.5F && random.nextFloat() < world.getMoonBrightness() && world.getMaxLocalRawBrightness(pos) <= random.nextInt(8)) {
-+                // Paper start - Replace rules for Height in Swamp Biome
-+                final double maxHeightSwamp = world.getMinecraftWorld().paperConfig().entities.spawning.slimeSpawnHeight.surfaceBiome.maximum;
-+                final double minHeightSwamp = world.getMinecraftWorld().paperConfig().entities.spawning.slimeSpawnHeight.surfaceBiome.minimum;
-+                if (world.getBiome(pos).is(BiomeTags.ALLOWS_SURFACE_SLIME_SPAWNS) && pos.getY() > minHeightSwamp && pos.getY() < maxHeightSwamp && random.nextFloat() < 0.5F && random.nextFloat() < world.getMoonBrightness() && world.getMaxLocalRawBrightness(pos) <= random.nextInt(8)) {
-+                // Paper end - Replace rules for Height in Swamp Biome
-                 return checkMobSpawnRules(type, world, spawnReason, pos, random);
-             }
- 
-@@ -300,9 +353,12 @@
-             }
- 
-             ChunkPos chunkcoordintpair = new ChunkPos(pos);
--            boolean flag = WorldgenRandom.seedSlimeChunk(chunkcoordintpair.x, chunkcoordintpair.z, ((WorldGenLevel) world).getSeed(), 987234911L).nextInt(10) == 0;
-+                boolean flag = world.getMinecraftWorld().paperConfig().entities.spawning.allChunksAreSlimeChunks || WorldgenRandom.seedSlimeChunk(chunkcoordintpair.x, chunkcoordintpair.z, ((WorldGenLevel) world).getSeed(), world.getMinecraftWorld().spigotConfig.slimeSeed).nextInt(10) == 0; // Spigot // Paper
- 
--            if (random.nextInt(10) == 0 && flag && pos.getY() < 40) {
-+                // Paper start - Replace rules for Height in Slime Chunks
-+                final double maxHeightSlimeChunk = world.getMinecraftWorld().paperConfig().entities.spawning.slimeSpawnHeight.slimeChunk.maximum;
-+                if (random.nextInt(10) == 0 && flag && pos.getY() < maxHeightSlimeChunk) {
-+                // Paper end - Replace rules for Height in Slime Chunks
-                 return checkMobSpawnRules(type, world, spawnReason, pos, random);
-             }
-         }
-@@ -432,7 +488,7 @@
- 
-         @Override
-         public boolean canUse() {
--            return (this.slime.isInWater() || this.slime.isInLava()) && this.slime.getMoveControl() instanceof Slime.SlimeMoveControl;
-+            return (this.slime.isInWater() || this.slime.isInLava()) && this.slime.getMoveControl() instanceof Slime.SlimeMoveControl && this.slime.canWander && new com.destroystokyo.paper.event.entity.SlimeSwimEvent((org.bukkit.entity.Slime) this.slime.getBukkitEntity()).callEvent(); // Paper - Slime pathfinder events
-         }
- 
-         @Override
-@@ -469,7 +525,15 @@
-         public boolean canUse() {
-             LivingEntity entityliving = this.slime.getTarget();
- 
--            return entityliving == null ? false : (!this.slime.canAttack(entityliving) ? false : this.slime.getMoveControl() instanceof Slime.SlimeMoveControl);
-+            // Paper start - Slime pathfinder events
-+            if (entityliving == null || !entityliving.isAlive()) {
-+                return false;
-+            }
-+            if (!this.slime.canAttack(entityliving)) {
-+                return false;
-+            }
-+            return this.slime.getMoveControl() instanceof Slime.SlimeMoveControl && this.slime.canWander && new com.destroystokyo.paper.event.entity.SlimeTargetLivingEntityEvent((org.bukkit.entity.Slime) this.slime.getBukkitEntity(), (org.bukkit.entity.LivingEntity) entityliving.getBukkitEntity()).callEvent();
-+            // Paper end - Slime pathfinder events
-         }
- 
-         @Override
-@@ -482,7 +546,15 @@
-         public boolean canContinueToUse() {
-             LivingEntity entityliving = this.slime.getTarget();
- 
--            return entityliving == null ? false : (!this.slime.canAttack(entityliving) ? false : --this.growTiredTimer > 0);
-+            // Paper start - Slime pathfinder events
-+            if (entityliving == null || !entityliving.isAlive()) {
-+                return false;
-+            }
-+            if (!this.slime.canAttack(entityliving)) {
-+                return false;
-+            }
-+            return --this.growTiredTimer > 0 && this.slime.canWander && new com.destroystokyo.paper.event.entity.SlimeTargetLivingEntityEvent((org.bukkit.entity.Slime) this.slime.getBukkitEntity(), (org.bukkit.entity.LivingEntity) entityliving.getBukkitEntity()).callEvent();
-+            // Paper end - Slime pathfinder events
-         }
- 
-         @Override
-@@ -505,6 +577,13 @@
-             }
- 
-         }
-+
-+        // Paper start - Slime pathfinder events; clear timer and target when goal resets
-+        public void stop() {
-+            this.growTiredTimer = 0;
-+            this.slime.setTarget(null);
-+        }
-+        // Paper end - Slime pathfinder events
-     }
- 
-     private static class SlimeRandomDirectionGoal extends Goal {
-@@ -520,7 +599,7 @@
- 
-         @Override
-         public boolean canUse() {
--            return this.slime.getTarget() == null && (this.slime.onGround() || this.slime.isInWater() || this.slime.isInLava() || this.slime.hasEffect(MobEffects.LEVITATION)) && this.slime.getMoveControl() instanceof Slime.SlimeMoveControl;
-+            return this.slime.getTarget() == null && (this.slime.onGround() || this.slime.isInWater() || this.slime.isInLava() || this.slime.hasEffect(MobEffects.LEVITATION)) && this.slime.getMoveControl() instanceof Slime.SlimeMoveControl && this.slime.canWander; // Paper - Slime pathfinder events
-         }
- 
-         @Override
-@@ -528,6 +607,11 @@
-             if (--this.nextRandomizeTime <= 0) {
-                 this.nextRandomizeTime = this.adjustedTickDelay(40 + this.slime.getRandom().nextInt(60));
-                 this.chosenDegrees = (float) this.slime.getRandom().nextInt(360);
-+                // Paper start - Slime pathfinder events
-+                com.destroystokyo.paper.event.entity.SlimeChangeDirectionEvent event = new com.destroystokyo.paper.event.entity.SlimeChangeDirectionEvent((org.bukkit.entity.Slime) this.slime.getBukkitEntity(), this.chosenDegrees);
-+                if (!this.slime.canWander || !event.callEvent()) return;
-+                this.chosenDegrees = event.getNewYaw();
-+                // Paper end - Slime pathfinder events
-             }
- 
-             MoveControl controllermove = this.slime.getMoveControl();
-@@ -550,7 +634,7 @@
- 
-         @Override
-         public boolean canUse() {
--            return !this.slime.isPassenger();
-+            return !this.slime.isPassenger() && this.slime.canWander && new com.destroystokyo.paper.event.entity.SlimeWanderEvent((org.bukkit.entity.Slime) this.slime.getBukkitEntity()).callEvent(); // Paper - Slime pathfinder events
-         }
- 
-         @Override
-@@ -563,4 +647,15 @@
- 
-         }
-     }
-+
-+    // Paper start - Slime pathfinder events
-+    private boolean canWander = true;
-+    public boolean canWander() {
-+        return canWander;
-+    }
-+
-+    public void setWander(boolean canWander) {
-+        this.canWander = canWander;
-+    }
-+    // Paper end - Slime pathfinder events
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Spider.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Spider.java.patch
deleted file mode 100644
index 1dae9b0f57..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Spider.java.patch
+++ /dev/null
@@ -1,29 +0,0 @@
---- a/net/minecraft/world/entity/monster/Spider.java
-+++ b/net/minecraft/world/entity/monster/Spider.java
-@@ -82,7 +82,7 @@
-     public void tick() {
-         super.tick();
-         if (!this.level().isClientSide) {
--            this.setClimbing(this.horizontalCollision);
-+            this.setClimbing(this.horizontalCollision && (this.level().paperConfig().entities.behavior.allowSpiderWorldBorderClimbing || !(ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isCollidingWithBorder(this.level().getWorldBorder(), this.getBoundingBox().inflate(ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) && this.level().getWorldBorder().isInsideCloseToBorder(this, this.getBoundingBox())))); // Paper - Add config option for spider worldborder climbing (Inflate by +EPSILON as collision will just barely place us outside border)
-         }
- 
-     }
-@@ -126,7 +126,7 @@
- 
-     @Override
-     public boolean canBeAffected(MobEffectInstance effect) {
--        return effect.is(MobEffects.POISON) ? false : super.canBeAffected(effect);
-+        return effect.is(MobEffects.POISON) && this.level().paperConfig().entities.mobEffects.spidersImmuneToPoisonEffect ? false : super.canBeAffected(effect); // Paper - Add config for mobs immune to default effects
-     }
- 
-     public boolean isClimbing() {
-@@ -172,7 +172,7 @@
-             Holder<MobEffect> holder = entityspider_groupdataspider.effect;
- 
-             if (holder != null) {
--                this.addEffect(new MobEffectInstance(holder, -1));
-+                this.addEffect(new MobEffectInstance(holder, -1), null, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.SPIDER_SPAWN, world instanceof net.minecraft.server.level.ServerLevel); // CraftBukkit
-             }
-         }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/WitherSkeleton.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/WitherSkeleton.java.patch
deleted file mode 100644
index 61b7e05667..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/WitherSkeleton.java.patch
+++ /dev/null
@@ -1,19 +0,0 @@
---- a/net/minecraft/world/entity/monster/WitherSkeleton.java
-+++ b/net/minecraft/world/entity/monster/WitherSkeleton.java
-@@ -110,7 +110,7 @@
-             return false;
-         } else {
-             if (target instanceof LivingEntity) {
--                ((LivingEntity) target).addEffect(new MobEffectInstance(MobEffects.WITHER, 200), this);
-+                ((LivingEntity) target).addEffect(new MobEffectInstance(MobEffects.WITHER, 200), this, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ATTACK); // CraftBukkit
-             }
- 
-             return true;
-@@ -127,6 +127,6 @@
- 
-     @Override
-     public boolean canBeAffected(MobEffectInstance effect) {
--        return effect.is(MobEffects.WITHER) ? false : super.canBeAffected(effect);
-+        return effect.is(MobEffects.WITHER) && this.level().paperConfig().entities.mobEffects.immuneToWitherEffect.witherSkeleton ? false : super.canBeAffected(effect); // Paper - Add config for mobs immune to default effects
-     }
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Zombie.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Zombie.java.patch
deleted file mode 100644
index 4b95368547..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Zombie.java.patch
+++ /dev/null
@@ -1,304 +0,0 @@
---- a/net/minecraft/world/entity/monster/Zombie.java
-+++ b/net/minecraft/world/entity/monster/Zombie.java
-@@ -6,19 +6,6 @@
- import java.util.List;
- import java.util.function.Predicate;
- import javax.annotation.Nullable;
--import net.minecraft.core.BlockPos;
--import net.minecraft.nbt.CompoundTag;
--import net.minecraft.nbt.NbtOps;
--import net.minecraft.nbt.Tag;
--import net.minecraft.network.syncher.EntityDataAccessor;
--import net.minecraft.network.syncher.EntityDataSerializers;
--import net.minecraft.network.syncher.SynchedEntityData;
--import net.minecraft.resources.ResourceLocation;
--import net.minecraft.server.level.ServerLevel;
--import net.minecraft.sounds.SoundEvent;
--import net.minecraft.sounds.SoundEvents;
--import net.minecraft.sounds.SoundSource;
--import net.minecraft.tags.FluidTags;
- import net.minecraft.util.Mth;
- import net.minecraft.util.RandomSource;
- import net.minecraft.world.Difficulty;
-@@ -66,11 +53,31 @@
- import net.minecraft.world.level.ServerLevelAccessor;
- import net.minecraft.world.level.block.Blocks;
- import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.nbt.NbtOps;
-+import net.minecraft.nbt.Tag;
-+import net.minecraft.network.syncher.EntityDataAccessor;
-+import net.minecraft.network.syncher.EntityDataSerializers;
-+import net.minecraft.network.syncher.SynchedEntityData;
-+import net.minecraft.resources.ResourceLocation;
-+// CraftBukkit start
-+import net.minecraft.server.MinecraftServer;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.sounds.SoundEvent;
-+import net.minecraft.sounds.SoundEvents;
-+import net.minecraft.sounds.SoundSource;
-+import net.minecraft.tags.FluidTags;
-+import org.bukkit.event.entity.CreatureSpawnEvent;
-+import org.bukkit.event.entity.EntityCombustByEntityEvent;
-+import org.bukkit.event.entity.EntityTargetEvent;
-+import org.bukkit.event.entity.EntityTransformEvent;
-+// CraftBukkit end
- 
- public class Zombie extends Monster {
- 
-     private static final ResourceLocation SPEED_MODIFIER_BABY_ID = ResourceLocation.withDefaultNamespace("baby");
--    private static final AttributeModifier SPEED_MODIFIER_BABY = new AttributeModifier(Zombie.SPEED_MODIFIER_BABY_ID, 0.5D, AttributeModifier.Operation.ADD_MULTIPLIED_BASE);
-+    private final AttributeModifier babyModifier = new AttributeModifier(Zombie.SPEED_MODIFIER_BABY_ID, this.level().paperConfig().entities.behavior.babyZombieMovementModifier, AttributeModifier.Operation.ADD_MULTIPLIED_BASE); // Paper - Make baby speed configurable
-     private static final ResourceLocation REINFORCEMENT_CALLER_CHARGE_ID = ResourceLocation.withDefaultNamespace("reinforcement_caller_charge");
-     private static final AttributeModifier ZOMBIE_REINFORCEMENT_CALLEE_CHARGE = new AttributeModifier(ResourceLocation.withDefaultNamespace("reinforcement_callee_charge"), -0.05000000074505806D, AttributeModifier.Operation.ADD_VALUE);
-     private static final ResourceLocation LEADER_ZOMBIE_BONUS_ID = ResourceLocation.withDefaultNamespace("leader_zombie_bonus");
-@@ -91,10 +98,12 @@
-     private boolean canBreakDoors;
-     private int inWaterTime;
-     public int conversionTime;
-+    // private int lastTick = MinecraftServer.currentTick; // CraftBukkit - add field // Paper - remove anti tick skipping measures / wall time
-+    private boolean shouldBurnInDay = true; // Paper - Add more Zombie API
- 
-     public Zombie(EntityType<? extends Zombie> type, Level world) {
-         super(type, world);
--        this.breakDoorGoal = new BreakDoorGoal(this, Zombie.DOOR_BREAKING_PREDICATE);
-+        this.breakDoorGoal = new BreakDoorGoal(this, com.google.common.base.Predicates.in(world.paperConfig().entities.behavior.doorBreakingDifficulty.getOrDefault(type, world.paperConfig().entities.behavior.doorBreakingDifficulty.get(EntityType.ZOMBIE)))); // Paper - Configurable door breaking difficulty
-     }
- 
-     public Zombie(Level world) {
-@@ -103,7 +112,7 @@
- 
-     @Override
-     protected void registerGoals() {
--        this.goalSelector.addGoal(4, new Zombie.ZombieAttackTurtleEggGoal(this, 1.0D, 3));
-+        if (this.level().paperConfig().entities.behavior.zombiesTargetTurtleEggs) this.goalSelector.addGoal(4, new Zombie.ZombieAttackTurtleEggGoal(this, 1.0D, 3)); // Paper - Add zombie targets turtle egg config
-         this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F));
-         this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));
-         this.addBehaviourGoals();
-@@ -115,7 +124,7 @@
-         this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0D));
-         this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[0])).setAlertOthers(ZombifiedPiglin.class));
-         this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true));
--        this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false));
-+        if ( this.level().spigotConfig.zombieAggressiveTowardsVillager ) this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false)); // Spigot
-         this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, IronGolem.class, true));
-         this.targetSelector.addGoal(5, new NearestAttackableTargetGoal<>(this, Turtle.class, 10, true, false, Turtle.BABY_ON_LAND_SELECTOR));
-     }
-@@ -165,11 +174,16 @@
- 
-     @Override
-     protected int getBaseExperienceReward(ServerLevel world) {
-+        final int previousReward = this.xpReward; // Paper - store previous value to reset after calculating XP reward
-         if (this.isBaby()) {
-             this.xpReward = (int) ((double) this.xpReward * 2.5D);
-         }
- 
--        return super.getBaseExperienceReward(world);
-+        // Paper start - store previous value to reset after calculating XP reward
-+        int reward = super.getBaseExperienceReward(world);
-+        this.xpReward = previousReward;
-+        return reward;
-+        // Paper end - store previous value to reset after calculating XP reward
-     }
- 
-     @Override
-@@ -178,9 +192,9 @@
-         if (this.level() != null && !this.level().isClientSide) {
-             AttributeInstance attributemodifiable = this.getAttribute(Attributes.MOVEMENT_SPEED);
- 
--            attributemodifiable.removeModifier(Zombie.SPEED_MODIFIER_BABY_ID);
-+            attributemodifiable.removeModifier(this.babyModifier.id()); // Paper - Make baby speed configurable
-             if (baby) {
--                attributemodifiable.addTransientModifier(Zombie.SPEED_MODIFIER_BABY);
-+                attributemodifiable.addTransientModifier(this.babyModifier); // Paper - Make baby speed configurable
-             }
-         }
- 
-@@ -203,7 +217,7 @@
-     public void tick() {
-         if (!this.level().isClientSide && this.isAlive() && !this.isNoAi()) {
-             if (this.isUnderWaterConverting()) {
--                --this.conversionTime;
-+                --this.conversionTime; // Paper - remove anti tick skipping measures / wall time
-                 if (this.conversionTime < 0) {
-                     this.doUnderWaterConversion();
-                 }
-@@ -220,6 +234,7 @@
-         }
- 
-         super.tick();
-+        // this.lastTick = MinecraftServer.currentTick; // CraftBukkit // Paper - remove anti tick skipping measures / wall time
-     }
- 
-     @Override
-@@ -253,7 +268,14 @@
-         super.aiStep();
-     }
- 
-+    // Paper start - Add more Zombie API
-+    public void stopDrowning() {
-+        this.conversionTime = -1;
-+        this.getEntityData().set(Zombie.DATA_DROWNED_CONVERSION_ID, false);
-+    }
-+    // Paper end - Add more Zombie API
-     public void startUnderWaterConversion(int ticksUntilWaterConversion) {
-+        // this.lastTick = MinecraftServer.currentTick; // CraftBukkit // Paper - remove anti tick skipping measures / wall time
-         this.conversionTime = ticksUntilWaterConversion;
-         this.getEntityData().set(Zombie.DATA_DROWNED_CONVERSION_ID, true);
-     }
-@@ -267,32 +289,51 @@
-     }
- 
-     protected void convertToZombieType(EntityType<? extends Zombie> entityType) {
--        this.convertTo(entityType, ConversionParams.single(this, true, true), (entityzombie) -> {
-+        Zombie converted = this.convertTo(entityType, ConversionParams.single(this, true, true), (entityzombie) -> { // CraftBukkit
-             entityzombie.handleAttributes(entityzombie.level().getCurrentDifficultyAt(entityzombie.blockPosition()).getSpecialMultiplier());
--        });
-+        // CraftBukkit start
-+        }, EntityTransformEvent.TransformReason.DROWNED, CreatureSpawnEvent.SpawnReason.DROWNED);
-+        if (converted == null) {
-+            ((org.bukkit.entity.Zombie) this.getBukkitEntity()).setConversionTime(-1); // CraftBukkit - SPIGOT-5208: End conversion to stop event spam
-+        }
-+        // CraftBukkit end
-     }
- 
-     @VisibleForTesting
-     public boolean convertVillagerToZombieVillager(ServerLevel world, Villager villager) {
--        ZombieVillager entityzombievillager = (ZombieVillager) villager.convertTo(EntityType.ZOMBIE_VILLAGER, ConversionParams.single(villager, true, true), (entityzombievillager1) -> {
--            entityzombievillager1.finalizeSpawn(world, world.getCurrentDifficultyAt(entityzombievillager1.blockPosition()), EntitySpawnReason.CONVERSION, new Zombie.ZombieGroupData(false, true));
--            entityzombievillager1.setVillagerData(villager.getVillagerData());
--            entityzombievillager1.setGossips((Tag) villager.getGossips().store(NbtOps.INSTANCE));
--            entityzombievillager1.setTradeOffers(villager.getOffers().copy());
--            entityzombievillager1.setVillagerXp(villager.getVillagerXp());
--            if (!this.isSilent()) {
--                world.levelEvent((Player) null, 1026, this.blockPosition(), 0);
-+        // CraftBukkit start
-+        return Zombie.convertVillagerToZombieVillager(world, villager, this.blockPosition(), this.isSilent(), EntityTransformEvent.TransformReason.INFECTION, CreatureSpawnEvent.SpawnReason.INFECTION) != null;
-+    }
-+
-+    public static ZombieVillager convertVillagerToZombieVillager(ServerLevel worldserver, Villager entityvillager, net.minecraft.core.BlockPos blockPosition, boolean silent, EntityTransformEvent.TransformReason transformReason, CreatureSpawnEvent.SpawnReason spawnReason) {
-+        // CraftBukkit end
-+        ZombieVillager entityzombievillager = (ZombieVillager) entityvillager.convertTo(EntityType.ZOMBIE_VILLAGER, ConversionParams.single(entityvillager, true, true), (entityzombievillager1) -> {
-+            entityzombievillager1.finalizeSpawn(worldserver, worldserver.getCurrentDifficultyAt(entityzombievillager1.blockPosition()), EntitySpawnReason.CONVERSION, new Zombie.ZombieGroupData(false, true));
-+            entityzombievillager1.setVillagerData(entityvillager.getVillagerData());
-+            entityzombievillager1.setGossips((Tag) entityvillager.getGossips().store(NbtOps.INSTANCE));
-+            entityzombievillager1.setTradeOffers(entityvillager.getOffers().copy());
-+            entityzombievillager1.setVillagerXp(entityvillager.getVillagerXp());
-+            // CraftBukkit start
-+            if (!silent) {
-+                worldserver.levelEvent((Player) null, 1026, blockPosition, 0);
-             }
- 
--        });
-+        }, transformReason, spawnReason);
- 
--        return entityzombievillager != null;
-+        return entityzombievillager;
-+        // CraftBukkit end
-     }
- 
-     public boolean isSunSensitive() {
--        return true;
-+        return this.shouldBurnInDay; // Paper - Add more Zombie API
-     }
- 
-+    // Paper start - Add more Zombie API
-+    public void setShouldBurnInDay(boolean shouldBurnInDay) {
-+        this.shouldBurnInDay = shouldBurnInDay;
-+    }
-+    // Paper end - Add more Zombie API
-+
-     @Override
-     public boolean hurtServer(ServerLevel world, DamageSource source, float amount) {
-         if (!super.hurtServer(world, source, amount)) {
-@@ -323,10 +364,10 @@
- 
-                     if (SpawnPlacements.isSpawnPositionOk(entitytypes, world, blockposition) && SpawnPlacements.checkSpawnRules(entitytypes, world, EntitySpawnReason.REINFORCEMENT, blockposition, world.random)) {
-                         entityzombie.setPos((double) i1, (double) j1, (double) k1);
--                        if (!world.hasNearbyAlivePlayer((double) i1, (double) j1, (double) k1, 7.0D) && world.isUnobstructed(entityzombie) && world.noCollision((Entity) entityzombie) && (entityzombie.canSpawnInLiquids() || !world.containsAnyLiquid(entityzombie.getBoundingBox()))) {
--                            entityzombie.setTarget(entityliving);
-+                        if (!world.hasNearbyAlivePlayerThatAffectsSpawning((double) i1, (double) j1, (double) k1, 7.0D) && world.isUnobstructed(entityzombie) && world.noCollision((Entity) entityzombie) && (entityzombie.canSpawnInLiquids() || !world.containsAnyLiquid(entityzombie.getBoundingBox()))) { // Paper - affects spawning api
-+                            entityzombie.setTarget(entityliving, EntityTargetEvent.TargetReason.REINFORCEMENT_TARGET, true); // CraftBukkit
-                             entityzombie.finalizeSpawn(world, world.getCurrentDifficultyAt(entityzombie.blockPosition()), EntitySpawnReason.REINFORCEMENT, (SpawnGroupData) null);
--                            world.addFreshEntityWithPassengers(entityzombie);
-+                            world.addFreshEntityWithPassengers(entityzombie, CreatureSpawnEvent.SpawnReason.REINFORCEMENTS); // CraftBukkit
-                             AttributeInstance attributemodifiable = this.getAttribute(Attributes.SPAWN_REINFORCEMENTS_CHANCE);
-                             AttributeModifier attributemodifier = attributemodifiable.getModifier(Zombie.REINFORCEMENT_CALLER_CHARGE_ID);
-                             double d0 = attributemodifier != null ? attributemodifier.amount() : 0.0D;
-@@ -352,7 +393,14 @@
-             float f = this.level().getCurrentDifficultyAt(this.blockPosition()).getEffectiveDifficulty();
- 
-             if (this.getMainHandItem().isEmpty() && this.isOnFire() && this.random.nextFloat() < f * 0.3F) {
--                target.igniteForSeconds((float) (2 * (int) f));
-+                // CraftBukkit start
-+                EntityCombustByEntityEvent event = new EntityCombustByEntityEvent(this.getBukkitEntity(), target.getBukkitEntity(), (float) (2 * (int) f)); // PAIL: fixme
-+                this.level().getCraftServer().getPluginManager().callEvent(event);
-+
-+                if (!event.isCancelled()) {
-+                    target.igniteForSeconds(event.getDuration(), false);
-+                }
-+                // CraftBukkit end
-             }
-         }
- 
-@@ -385,7 +433,7 @@
- 
-     @Override
-     public EntityType<? extends Zombie> getType() {
--        return super.getType();
-+        return (EntityType<? extends Zombie>) super.getType(); // CraftBukkit - decompile error
-     }
- 
-     protected boolean canSpawnInLiquids() {
-@@ -414,6 +462,7 @@
-         nbt.putBoolean("CanBreakDoors", this.canBreakDoors());
-         nbt.putInt("InWaterTime", this.isInWater() ? this.inWaterTime : -1);
-         nbt.putInt("DrownedConversionTime", this.isUnderWaterConverting() ? this.conversionTime : -1);
-+        nbt.putBoolean("Paper.ShouldBurnInDay", this.shouldBurnInDay); // Paper - Add more Zombie API
-     }
- 
-     @Override
-@@ -425,6 +474,11 @@
-         if (nbt.contains("DrownedConversionTime", 99) && nbt.getInt("DrownedConversionTime") > -1) {
-             this.startUnderWaterConversion(nbt.getInt("DrownedConversionTime"));
-         }
-+        // Paper start - Add more Zombie API
-+        if (nbt.contains("Paper.ShouldBurnInDay")) {
-+            this.shouldBurnInDay = nbt.getBoolean("Paper.ShouldBurnInDay");
-+        }
-+        // Paper end - Add more Zombie API
- 
-     }
- 
-@@ -432,10 +486,8 @@
-     public boolean killedEntity(ServerLevel world, LivingEntity other) {
-         boolean flag = super.killedEntity(world, other);
- 
--        if ((world.getDifficulty() == Difficulty.NORMAL || world.getDifficulty() == Difficulty.HARD) && other instanceof Villager entityvillager) {
--            if (world.getDifficulty() != Difficulty.HARD && this.random.nextBoolean()) {
--                return flag;
--            }
-+        final double fallbackChance = world.getDifficulty() == Difficulty.HARD ? 100d : world.getDifficulty() == Difficulty.NORMAL ? 50d : 0d; // Paper - Configurable chance of villager zombie infection
-+        if (this.random.nextDouble() * 100 < world.paperConfig().entities.behavior.zombieVillagerInfectionChance.or(fallbackChance) && other instanceof Villager entityvillager) { // Paper - Configurable chance of villager zombie infection
- 
-             if (this.convertVillagerToZombieVillager(world, entityvillager)) {
-                 flag = false;
-@@ -468,7 +520,7 @@
-         float f = difficulty.getSpecialMultiplier();
- 
-         if (spawnReason != EntitySpawnReason.CONVERSION) {
--            this.setCanPickUpLoot(randomsource.nextFloat() < 0.55F * f);
-+            this.setCanPickUpLoot(this.level().paperConfig().entities.behavior.mobsCanAlwaysPickUpLoot.zombies || randomsource.nextFloat() < 0.55F * f); // Paper - Add world settings for mobs picking up loot
-         }
- 
-         if (object == null) {
-@@ -496,7 +548,7 @@
-                             entitychicken1.finalizeSpawn(world, difficulty, EntitySpawnReason.JOCKEY, (SpawnGroupData) null);
-                             entitychicken1.setChickenJockey(true);
-                             this.startRiding(entitychicken1);
--                            world.addFreshEntity(entitychicken1);
-+                            world.addFreshEntity(entitychicken1, CreatureSpawnEvent.SpawnReason.MOUNT); // CraftBukkit
-                         }
-                     }
-                 }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/ZombieVillager.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/ZombieVillager.java.patch
deleted file mode 100644
index d025644b62..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/ZombieVillager.java.patch
+++ /dev/null
@@ -1,149 +0,0 @@
---- a/net/minecraft/world/entity/monster/ZombieVillager.java
-+++ b/net/minecraft/world/entity/monster/ZombieVillager.java
-@@ -18,10 +18,6 @@
- import net.minecraft.network.syncher.EntityDataAccessor;
- import net.minecraft.network.syncher.EntityDataSerializers;
- import net.minecraft.network.syncher.SynchedEntityData;
--import net.minecraft.server.level.ServerLevel;
--import net.minecraft.server.level.ServerPlayer;
--import net.minecraft.sounds.SoundEvent;
--import net.minecraft.sounds.SoundEvents;
- import net.minecraft.world.DifficultyInstance;
- import net.minecraft.world.InteractionHand;
- import net.minecraft.world.InteractionResult;
-@@ -35,6 +31,7 @@
- import net.minecraft.world.entity.SlotAccess;
- import net.minecraft.world.entity.SpawnGroupData;
- import net.minecraft.world.entity.ai.village.ReputationEventType;
-+import net.minecraft.world.entity.npc.Villager;
- import net.minecraft.world.entity.npc.VillagerData;
- import net.minecraft.world.entity.npc.VillagerDataHolder;
- import net.minecraft.world.entity.npc.VillagerProfession;
-@@ -52,6 +49,16 @@
- import net.minecraft.world.level.block.state.BlockState;
- import org.slf4j.Logger;
- 
-+// CraftBukkit start
-+import net.minecraft.server.MinecraftServer;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.sounds.SoundEvent;
-+import net.minecraft.sounds.SoundEvents;
-+import org.bukkit.event.entity.CreatureSpawnEvent;
-+import org.bukkit.event.entity.EntityTransformEvent;
-+// CraftBukkit end
-+
- public class ZombieVillager extends Zombie implements VillagerDataHolder {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
-@@ -69,6 +76,7 @@
-     @Nullable
-     private MerchantOffers tradeOffers;
-     private int villagerXp;
-+    private int lastTick = MinecraftServer.currentTick; // CraftBukkit - add field
- 
-     public ZombieVillager(EntityType<? extends ZombieVillager> type, Level world) {
-         super(type, world);
-@@ -87,7 +95,7 @@
-     @Override
-     public void addAdditionalSaveData(CompoundTag nbt) {
-         super.addAdditionalSaveData(nbt);
--        DataResult dataresult = VillagerData.CODEC.encodeStart(NbtOps.INSTANCE, this.getVillagerData());
-+        DataResult<Tag> dataresult = VillagerData.CODEC.encodeStart(NbtOps.INSTANCE, this.getVillagerData()); // CraftBukkit - decompile error
-         Logger logger = ZombieVillager.LOGGER;
- 
-         Objects.requireNonNull(logger);
-@@ -122,7 +130,7 @@
-         }
- 
-         if (nbt.contains("Offers")) {
--            DataResult dataresult1 = MerchantOffers.CODEC.parse(this.registryAccess().createSerializationContext(NbtOps.INSTANCE), nbt.get("Offers"));
-+            DataResult<MerchantOffers> dataresult1 = MerchantOffers.CODEC.parse(this.registryAccess().createSerializationContext(NbtOps.INSTANCE), nbt.get("Offers")); // CraftBukkit - decompile error
-             Logger logger1 = ZombieVillager.LOGGER;
- 
-             Objects.requireNonNull(logger1);
-@@ -149,6 +157,10 @@
-     public void tick() {
-         if (!this.level().isClientSide && this.isAlive() && this.isConverting()) {
-             int i = this.getConversionProgress();
-+            // CraftBukkit start - Use wall time instead of ticks for villager conversion
-+            int elapsedTicks = MinecraftServer.currentTick - this.lastTick;
-+            i *= elapsedTicks;
-+            // CraftBukkit end
- 
-             this.villagerConversionTime -= i;
-             if (this.villagerConversionTime <= 0) {
-@@ -157,6 +169,7 @@
-         }
- 
-         super.tick();
-+        this.lastTick = MinecraftServer.currentTick; // CraftBukkit
-     }
- 
-     @Override
-@@ -194,12 +207,20 @@
-     }
- 
-     public void startConverting(@Nullable UUID uuid, int delay) {
-+    // Paper start - missing entity behaviour api - converting without entity event
-+        this.startConverting(uuid, delay, true);
-+    }
-+
-+    public void startConverting(@Nullable UUID uuid, int delay, boolean broadcastEntityEvent) {
-+    // Paper end - missing entity behaviour api - converting without entity event
-         this.conversionStarter = uuid;
-         this.villagerConversionTime = delay;
-         this.getEntityData().set(ZombieVillager.DATA_CONVERTING_ID, true);
--        this.removeEffect(MobEffects.WEAKNESS);
--        this.addEffect(new MobEffectInstance(MobEffects.DAMAGE_BOOST, delay, Math.min(this.level().getDifficulty().getId() - 1, 0)));
--        this.level().broadcastEntityEvent(this, (byte) 16);
-+        // CraftBukkit start
-+        this.removeEffect(MobEffects.WEAKNESS, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.CONVERSION);
-+        this.addEffect(new MobEffectInstance(MobEffects.DAMAGE_BOOST, delay, Math.min(this.level().getDifficulty().getId() - 1, 0)), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.CONVERSION);
-+        // CraftBukkit end
-+        if (broadcastEntityEvent) this.level().broadcastEntityEvent(this, (byte) 16); // Paper - missing entity behaviour api - converting without entity event
-     }
- 
-     @Override
-@@ -215,10 +236,11 @@
-     }
- 
-     private void finishConversion(ServerLevel world) {
--        this.convertTo(EntityType.VILLAGER, ConversionParams.single(this, false, false), (entityvillager) -> {
-+        Villager converted = this.convertTo(EntityType.VILLAGER, ConversionParams.single(this, false, false), (entityvillager) -> { // CraftBukkit
-             Iterator iterator = this.dropPreservedEquipment(world, (itemstack) -> {
-                 return !EnchantmentHelper.has(itemstack, EnchantmentEffectComponents.PREVENT_ARMOR_CHANGE);
-             }).iterator();
-+            this.forceDrops = false; // CraftBukkit
- 
-             while (iterator.hasNext()) {
-                 EquipmentSlot enumitemslot = (EquipmentSlot) iterator.next();
-@@ -240,7 +262,7 @@
-             entityvillager.finalizeSpawn(world, world.getCurrentDifficultyAt(entityvillager.blockPosition()), EntitySpawnReason.CONVERSION, (SpawnGroupData) null);
-             entityvillager.refreshBrain(world);
-             if (this.conversionStarter != null) {
--                Player entityhuman = world.getPlayerByUUID(this.conversionStarter);
-+            Player entityhuman = world.getGlobalPlayerByUUID(this.conversionStarter); // Paper - check global player list where appropriate
- 
-                 if (entityhuman instanceof ServerPlayer) {
-                     CriteriaTriggers.CURED_ZOMBIE_VILLAGER.trigger((ServerPlayer) entityhuman, this, entityvillager);
-@@ -248,12 +270,16 @@
-                 }
-             }
- 
--            entityvillager.addEffect(new MobEffectInstance(MobEffects.CONFUSION, 200, 0));
-+            entityvillager.addEffect(new MobEffectInstance(MobEffects.CONFUSION, 200, 0), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.CONVERSION); // CraftBukkit
-             if (!this.isSilent()) {
-                 world.levelEvent((Player) null, 1027, this.blockPosition(), 0);
-             }
--
--        });
-+        // CraftBukkit start
-+        }, EntityTransformEvent.TransformReason.CURED, CreatureSpawnEvent.SpawnReason.CURED);
-+        if (converted == null) {
-+            ((org.bukkit.entity.ZombieVillager) this.getBukkitEntity()).setConversionTime(-1); // SPIGOT-5208: End conversion to stop event spam
-+        }
-+        // CraftBukkit end
-     }
- 
-     @VisibleForTesting
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/ZombifiedPiglin.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/ZombifiedPiglin.java.patch
deleted file mode 100644
index 0b3d7e043c..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/ZombifiedPiglin.java.patch
+++ /dev/null
@@ -1,67 +0,0 @@
---- a/net/minecraft/world/entity/monster/ZombifiedPiglin.java
-+++ b/net/minecraft/world/entity/monster/ZombifiedPiglin.java
-@@ -56,6 +56,7 @@
-     private static final int ALERT_RANGE_Y = 10;
-     private static final UniformInt ALERT_INTERVAL = TimeUtil.rangeOfSeconds(4, 6);
-     private int ticksUntilNextAlert;
-+    private HurtByTargetGoal pathfinderGoalHurtByTarget; // Paper - fix PigZombieAngerEvent cancellation
- 
-     public ZombifiedPiglin(EntityType<? extends ZombifiedPiglin> type, Level world) {
-         super(type, world);
-@@ -71,7 +72,7 @@
-     protected void addBehaviourGoals() {
-         this.goalSelector.addGoal(2, new ZombieAttackGoal(this, 1.0D, false));
-         this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0D));
--        this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[0])).setAlertOthers());
-+        this.targetSelector.addGoal(1, pathfinderGoalHurtByTarget = (new HurtByTargetGoal(this, new Class[0])).setAlertOthers()); // Paper - fix PigZombieAngerEvent cancellation
-         this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, this::isAngryAt));
-         this.targetSelector.addGoal(3, new ResetUniversalAngerTargetGoal<>(this, true));
-     }
-@@ -149,7 +150,7 @@
-         }).filter((entitypigzombie) -> {
-             return !entitypigzombie.isAlliedTo((Entity) this.getTarget());
-         }).forEach((entitypigzombie) -> {
--            entitypigzombie.setTarget(this.getTarget());
-+            entitypigzombie.setTarget(this.getTarget(), org.bukkit.event.entity.EntityTargetEvent.TargetReason.TARGET_ATTACKED_NEARBY_ENTITY, true); // CraftBukkit
-         });
-     }
- 
-@@ -158,22 +159,32 @@
-     }
- 
-     @Override
--    public void setTarget(@Nullable LivingEntity target) {
--        if (this.getTarget() == null && target != null) {
-+    public boolean setTarget(@Nullable LivingEntity entityliving, org.bukkit.event.entity.EntityTargetEvent.TargetReason reason, boolean fireEvent) { // CraftBukkit - signature
-+        if (this.getTarget() == null && entityliving != null) {
-             this.playFirstAngerSoundIn = ZombifiedPiglin.FIRST_ANGER_SOUND_DELAY.sample(this.random);
-             this.ticksUntilNextAlert = ZombifiedPiglin.ALERT_INTERVAL.sample(this.random);
-         }
- 
--        if (target instanceof Player) {
--            this.setLastHurtByPlayer((Player) target);
-+        if (entityliving instanceof Player) {
-+            this.setLastHurtByPlayer((Player) entityliving);
-         }
- 
--        super.setTarget(target);
-+        return super.setTarget(entityliving, reason, fireEvent); // CraftBukkit
-     }
- 
-     @Override
-     public void startPersistentAngerTimer() {
--        this.setRemainingPersistentAngerTime(ZombifiedPiglin.PERSISTENT_ANGER_TIME.sample(this.random));
-+        // CraftBukkit start
-+        Entity entity = ((ServerLevel) this.level()).getEntity(this.getPersistentAngerTarget());
-+        org.bukkit.event.entity.PigZombieAngerEvent event = new org.bukkit.event.entity.PigZombieAngerEvent((org.bukkit.entity.PigZombie) this.getBukkitEntity(), (entity == null) ? null : entity.getBukkitEntity(), ZombifiedPiglin.PERSISTENT_ANGER_TIME.sample(this.random));
-+        this.level().getCraftServer().getPluginManager().callEvent(event);
-+        if (event.isCancelled()) {
-+            this.setPersistentAngerTarget(null);
-+            pathfinderGoalHurtByTarget.stop(); // Paper - fix PigZombieAngerEvent cancellation
-+            return;
-+        }
-+        this.setRemainingPersistentAngerTime(event.getNewAnger());
-+        // CraftBukkit end
-     }
- 
-     public static boolean checkZombifiedPiglinSpawnRules(EntityType<ZombifiedPiglin> type, LevelAccessor world, EntitySpawnReason spawnReason, BlockPos pos, RandomSource random) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/creaking/Creaking.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/creaking/Creaking.java.patch
deleted file mode 100644
index d3d16a6b28..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/creaking/Creaking.java.patch
+++ /dev/null
@@ -1,60 +0,0 @@
---- a/net/minecraft/world/entity/monster/creaking/Creaking.java
-+++ b/net/minecraft/world/entity/monster/creaking/Creaking.java
-@@ -198,15 +198,15 @@
-     }
- 
-     @Override
--    public void push(double deltaX, double deltaY, double deltaZ) {
-+    public void push(double deltaX, double deltaY, double deltaZ, @Nullable Entity pushingEntity) { // Paper - add push source entity param
-         if (this.canMove()) {
--            super.push(deltaX, deltaY, deltaZ);
-+            super.push(deltaX, deltaY, deltaZ, pushingEntity); // Paper - add push source entity param
-         }
-     }
- 
-     @Override
-     public Brain<Creaking> getBrain() {
--        return super.getBrain();
-+        return (Brain<Creaking>) super.getBrain(); // CraftBukkit - decompile error
-     }
- 
-     @Override
-@@ -329,7 +329,7 @@
-         }
- 
-         this.makeSound(this.getDeathSound());
--        this.remove(Entity.RemovalReason.DISCARDED);
-+        this.remove(Entity.RemovalReason.DISCARDED, null); // CraftBukkit - add Bukkit remove cause
-     }
- 
-     public void creakingDeathEffects(DamageSource damageSource) {
-@@ -476,7 +476,7 @@
- 
-     @Override
-     protected SoundEvent getHurtSound(DamageSource source) {
--        return this.isHeartBound() ? SoundEvents.CREAKING_SWAY : super.getHurtSound(source);
-+        return SoundEvents.CREAKING_SWAY;
-     }
- 
-     @Override
-@@ -502,9 +502,9 @@
-     }
- 
-     @Override
--    public void knockback(double strength, double x, double z) {
-+    public void knockback(double strength, double x, double z, @Nullable Entity attacker, io.papermc.paper.event.entity.EntityKnockbackEvent.Cause cause) { // Paper - knockback events
-         if (this.canMove()) {
--            super.knockback(strength, x, z);
-+            super.knockback(strength, x, z, attacker, cause); // Paper - knockback events
-         }
-     }
- 
-@@ -549,7 +549,7 @@
-     }
- 
-     public void activate(Player player) {
--        this.getBrain().setMemory(MemoryModuleType.ATTACK_TARGET, (Object) player);
-+        this.getBrain().setMemory(MemoryModuleType.ATTACK_TARGET, player); // CraftBukkit - decompile error
-         this.gameEvent(GameEvent.ENTITY_ACTION);
-         this.makeSound(SoundEvents.CREAKING_ACTIVATE);
-         this.setIsActive(true);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/hoglin/Hoglin.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/hoglin/Hoglin.java.patch
deleted file mode 100644
index 345420cb17..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/hoglin/Hoglin.java.patch
+++ /dev/null
@@ -1,48 +0,0 @@
---- a/net/minecraft/world/entity/monster/hoglin/Hoglin.java
-+++ b/net/minecraft/world/entity/monster/hoglin/Hoglin.java
-@@ -63,7 +63,8 @@
-     public int timeInOverworld;
-     public boolean cannotBeHunted;
-     protected static final ImmutableList<? extends SensorType<? extends Sensor<? super Hoglin>>> SENSOR_TYPES = ImmutableList.of(SensorType.NEAREST_LIVING_ENTITIES, SensorType.NEAREST_PLAYERS, SensorType.NEAREST_ADULT, SensorType.HOGLIN_SPECIFIC_SENSOR);
--    protected static final ImmutableList<? extends MemoryModuleType<?>> MEMORY_TYPES = ImmutableList.of(MemoryModuleType.BREED_TARGET, MemoryModuleType.NEAREST_LIVING_ENTITIES, MemoryModuleType.NEAREST_VISIBLE_LIVING_ENTITIES, MemoryModuleType.NEAREST_VISIBLE_PLAYER, MemoryModuleType.NEAREST_VISIBLE_ATTACKABLE_PLAYER, MemoryModuleType.LOOK_TARGET, MemoryModuleType.WALK_TARGET, MemoryModuleType.CANT_REACH_WALK_TARGET_SINCE, MemoryModuleType.PATH, MemoryModuleType.ATTACK_TARGET, MemoryModuleType.ATTACK_COOLING_DOWN, MemoryModuleType.NEAREST_VISIBLE_ADULT_PIGLIN, new MemoryModuleType[]{MemoryModuleType.AVOID_TARGET, MemoryModuleType.VISIBLE_ADULT_PIGLIN_COUNT, MemoryModuleType.VISIBLE_ADULT_HOGLIN_COUNT, MemoryModuleType.NEAREST_VISIBLE_ADULT_HOGLINS, MemoryModuleType.NEAREST_VISIBLE_ADULT, MemoryModuleType.NEAREST_REPELLENT, MemoryModuleType.PACIFIED, MemoryModuleType.IS_PANICKING});
-+    // CraftBukkit - decompile error
-+    protected static final ImmutableList<? extends MemoryModuleType<?>> MEMORY_TYPES = ImmutableList.<MemoryModuleType<?>>of(MemoryModuleType.BREED_TARGET, MemoryModuleType.NEAREST_LIVING_ENTITIES, MemoryModuleType.NEAREST_VISIBLE_LIVING_ENTITIES, MemoryModuleType.NEAREST_VISIBLE_PLAYER, MemoryModuleType.NEAREST_VISIBLE_ATTACKABLE_PLAYER, MemoryModuleType.LOOK_TARGET, MemoryModuleType.WALK_TARGET, MemoryModuleType.CANT_REACH_WALK_TARGET_SINCE, MemoryModuleType.PATH, MemoryModuleType.ATTACK_TARGET, MemoryModuleType.ATTACK_COOLING_DOWN, MemoryModuleType.NEAREST_VISIBLE_ADULT_PIGLIN, new MemoryModuleType[]{MemoryModuleType.AVOID_TARGET, MemoryModuleType.VISIBLE_ADULT_PIGLIN_COUNT, MemoryModuleType.VISIBLE_ADULT_HOGLIN_COUNT, MemoryModuleType.NEAREST_VISIBLE_ADULT_HOGLINS, MemoryModuleType.NEAREST_VISIBLE_ADULT, MemoryModuleType.NEAREST_REPELLENT, MemoryModuleType.PACIFIED, MemoryModuleType.IS_PANICKING});
- 
-     public Hoglin(EntityType<? extends Hoglin> type, Level world) {
-         super(type, world);
-@@ -134,7 +135,7 @@
- 
-     @Override
-     public Brain<Hoglin> getBrain() {
--        return super.getBrain();
-+        return (Brain<Hoglin>) super.getBrain(); // CraftBukkit - decompile error
-     }
- 
-     @Override
-@@ -240,9 +241,15 @@
-     }
- 
-     private void finishConversion() {
--        this.convertTo(EntityType.ZOGLIN, ConversionParams.single(this, true, false), (entityzoglin) -> {
-+        net.minecraft.world.entity.Entity converted = this.convertTo(EntityType.ZOGLIN, ConversionParams.single(this, true, false), (entityzoglin) -> {
-             entityzoglin.addEffect(new MobEffectInstance(MobEffects.CONFUSION, 200, 0));
--        });
-+        }, org.bukkit.event.entity.EntityTransformEvent.TransformReason.PIGLIN_ZOMBIFIED, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.PIGLIN_ZOMBIFIED); // CraftBukkit - add spawn and transform reasons
-+
-+        // Paper start - Fix issues with mob conversion; reset to prevent event spam
-+        if (converted == null) {
-+            this.timeInOverworld = 0;
-+        }
-+        // Paper end - Fix issues with mob conversion
-     }
- 
-     @Override
-@@ -326,7 +333,7 @@
- 
-     @Override
-     protected SoundEvent getAmbientSound() {
--        return this.level().isClientSide ? null : (SoundEvent) HoglinAi.getSoundForCurrentActivity(this).orElse((Object) null);
-+        return this.level().isClientSide ? null : (SoundEvent) HoglinAi.getSoundForCurrentActivity(this).orElse(null); // CraftBukkit - decompile error
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/hoglin/HoglinBase.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/hoglin/HoglinBase.java.patch
deleted file mode 100644
index 634a383e72..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/hoglin/HoglinBase.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/entity/monster/hoglin/HoglinBase.java
-+++ b/net/minecraft/world/entity/monster/hoglin/HoglinBase.java
-@@ -45,7 +45,7 @@
-             double j = f * (double)(attacker.level().random.nextFloat() * 0.5F + 0.2F);
-             Vec3 vec3 = new Vec3(g, 0.0, h).normalize().scale(j).yRot(i);
-             double k = f * (double)attacker.level().random.nextFloat() * 0.5;
--            target.push(vec3.x, k, vec3.z);
-+            target.push(vec3.x, k, vec3.z, attacker); // Paper - Add EntityKnockbackByEntityEvent and EntityPushedByEntityAttackEvent
-             target.hurtMarked = true;
-         }
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/piglin/AbstractPiglin.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/piglin/AbstractPiglin.java.patch
deleted file mode 100644
index 68e189739c..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/piglin/AbstractPiglin.java.patch
+++ /dev/null
@@ -1,20 +0,0 @@
---- a/net/minecraft/world/entity/monster/piglin/AbstractPiglin.java
-+++ b/net/minecraft/world/entity/monster/piglin/AbstractPiglin.java
-@@ -100,9 +100,15 @@
-     }
- 
-     protected void finishConversion(ServerLevel world) {
--        this.convertTo(EntityType.ZOMBIFIED_PIGLIN, ConversionParams.single(this, true, true), (entitypigzombie) -> {
-+        net.minecraft.world.entity.Entity converted = this.convertTo(EntityType.ZOMBIFIED_PIGLIN, ConversionParams.single(this, true, true), (entitypigzombie) -> { // Paper - Fix issues with mob conversion; reset to prevent event spam
-             entitypigzombie.addEffect(new MobEffectInstance(MobEffects.CONFUSION, 200, 0));
--        });
-+        }, org.bukkit.event.entity.EntityTransformEvent.TransformReason.PIGLIN_ZOMBIFIED, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.PIGLIN_ZOMBIFIED); // CraftBukkit - add spawn and transform reasons
-+
-+        // Paper start - Fix issues with mob conversion; reset to prevent event spam
-+        if (converted == null) {
-+            this.timeInOverworld = 0;
-+        }
-+        // Paper end - Fix issues with mob conversion
-     }
- 
-     public boolean isAdult() {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/piglin/Piglin.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/piglin/Piglin.java.patch
deleted file mode 100644
index d238fb349f..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/piglin/Piglin.java.patch
+++ /dev/null
@@ -1,140 +0,0 @@
---- a/net/minecraft/world/entity/monster/piglin/Piglin.java
-+++ b/net/minecraft/world/entity/monster/piglin/Piglin.java
-@@ -4,15 +4,6 @@
- import com.mojang.serialization.Dynamic;
- import java.util.List;
- import javax.annotation.Nullable;
--import net.minecraft.core.BlockPos;
--import net.minecraft.nbt.CompoundTag;
--import net.minecraft.network.syncher.EntityDataAccessor;
--import net.minecraft.network.syncher.EntityDataSerializers;
--import net.minecraft.network.syncher.SynchedEntityData;
--import net.minecraft.resources.ResourceLocation;
--import net.minecraft.server.level.ServerLevel;
--import net.minecraft.sounds.SoundEvent;
--import net.minecraft.sounds.SoundEvents;
- import net.minecraft.tags.ItemTags;
- import net.minecraft.tags.TagKey;
- import net.minecraft.util.RandomSource;
-@@ -59,6 +50,25 @@
- import net.minecraft.world.level.ServerLevelAccessor;
- import net.minecraft.world.level.block.Blocks;
- import net.minecraft.world.level.block.state.BlockState;
-+// CraftBukkit start
-+import java.util.stream.Collectors;
-+import java.util.HashSet;
-+import java.util.Set;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.core.registries.BuiltInRegistries;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.nbt.ListTag;
-+import net.minecraft.nbt.StringTag;
-+import net.minecraft.nbt.Tag;
-+import net.minecraft.network.syncher.EntityDataAccessor;
-+import net.minecraft.network.syncher.EntityDataSerializers;
-+import net.minecraft.network.syncher.SynchedEntityData;
-+import net.minecraft.resources.ResourceLocation;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.sounds.SoundEvent;
-+import net.minecraft.sounds.SoundEvents;
-+import net.minecraft.world.item.Item;
-+// CraftBukkit end
- 
- public class Piglin extends AbstractPiglin implements CrossbowAttackMob, InventoryCarrier {
- 
-@@ -79,6 +89,10 @@
-     public boolean cannotHunt;
-     protected static final ImmutableList<SensorType<? extends Sensor<? super Piglin>>> SENSOR_TYPES = ImmutableList.of(SensorType.NEAREST_LIVING_ENTITIES, SensorType.NEAREST_PLAYERS, SensorType.NEAREST_ITEMS, SensorType.HURT_BY, SensorType.PIGLIN_SPECIFIC_SENSOR);
-     protected static final ImmutableList<MemoryModuleType<?>> MEMORY_TYPES = ImmutableList.of(MemoryModuleType.LOOK_TARGET, MemoryModuleType.DOORS_TO_CLOSE, MemoryModuleType.NEAREST_LIVING_ENTITIES, MemoryModuleType.NEAREST_VISIBLE_LIVING_ENTITIES, MemoryModuleType.NEAREST_VISIBLE_PLAYER, MemoryModuleType.NEAREST_VISIBLE_ATTACKABLE_PLAYER, MemoryModuleType.NEAREST_VISIBLE_ADULT_PIGLINS, MemoryModuleType.NEARBY_ADULT_PIGLINS, MemoryModuleType.NEAREST_VISIBLE_WANTED_ITEM, MemoryModuleType.ITEM_PICKUP_COOLDOWN_TICKS, MemoryModuleType.HURT_BY, MemoryModuleType.HURT_BY_ENTITY, new MemoryModuleType[]{MemoryModuleType.WALK_TARGET, MemoryModuleType.CANT_REACH_WALK_TARGET_SINCE, MemoryModuleType.ATTACK_TARGET, MemoryModuleType.ATTACK_COOLING_DOWN, MemoryModuleType.INTERACTION_TARGET, MemoryModuleType.PATH, MemoryModuleType.ANGRY_AT, MemoryModuleType.UNIVERSAL_ANGER, MemoryModuleType.AVOID_TARGET, MemoryModuleType.ADMIRING_ITEM, MemoryModuleType.TIME_TRYING_TO_REACH_ADMIRE_ITEM, MemoryModuleType.ADMIRING_DISABLED, MemoryModuleType.DISABLE_WALK_TO_ADMIRE_ITEM, MemoryModuleType.CELEBRATE_LOCATION, MemoryModuleType.DANCING, MemoryModuleType.HUNTED_RECENTLY, MemoryModuleType.NEAREST_VISIBLE_BABY_HOGLIN, MemoryModuleType.NEAREST_VISIBLE_NEMESIS, MemoryModuleType.NEAREST_VISIBLE_ZOMBIFIED, MemoryModuleType.RIDE_TARGET, MemoryModuleType.VISIBLE_ADULT_PIGLIN_COUNT, MemoryModuleType.VISIBLE_ADULT_HOGLIN_COUNT, MemoryModuleType.NEAREST_VISIBLE_HUNTABLE_HOGLIN, MemoryModuleType.NEAREST_TARGETABLE_PLAYER_NOT_WEARING_GOLD, MemoryModuleType.NEAREST_PLAYER_HOLDING_WANTED_ITEM, MemoryModuleType.ATE_RECENTLY, MemoryModuleType.NEAREST_REPELLENT});
-+    // CraftBukkit start - Custom bartering and interest list
-+    public Set<Item> allowedBarterItems = new HashSet<>();
-+    public Set<Item> interestItems = new HashSet<>();
-+    // CraftBukkit end
- 
-     public Piglin(EntityType<? extends AbstractPiglin> type, Level world) {
-         super(type, world);
-@@ -97,6 +111,14 @@
-         }
- 
-         this.writeInventoryToTag(nbt, this.registryAccess());
-+        // CraftBukkit start
-+        ListTag barterList = new ListTag();
-+        this.allowedBarterItems.stream().map(BuiltInRegistries.ITEM::getKey).map(ResourceLocation::toString).map(StringTag::valueOf).forEach(barterList::add);
-+        nbt.put("Bukkit.BarterList", barterList);
-+        ListTag interestList = new ListTag();
-+        this.interestItems.stream().map(BuiltInRegistries.ITEM::getKey).map(ResourceLocation::toString).map(StringTag::valueOf).forEach(interestList::add);
-+        nbt.put("Bukkit.InterestList", interestList);
-+        // CraftBukkit end
-     }
- 
-     @Override
-@@ -105,6 +127,10 @@
-         this.setBaby(nbt.getBoolean("IsBaby"));
-         this.setCannotHunt(nbt.getBoolean("CannotHunt"));
-         this.readInventoryFromTag(nbt, this.registryAccess());
-+        // CraftBukkit start
-+        this.allowedBarterItems = nbt.getList("Bukkit.BarterList", 8).stream().map(Tag::getAsString).map(ResourceLocation::tryParse).map(BuiltInRegistries.ITEM::getValue).collect(Collectors.toCollection(HashSet::new));
-+        this.interestItems = nbt.getList("Bukkit.InterestList", 8).stream().map(Tag::getAsString).map(ResourceLocation::tryParse).map(BuiltInRegistries.ITEM::getValue).collect(Collectors.toCollection(HashSet::new));
-+        // CraftBukkit end
-     }
- 
-     @VisibleForDebug
-@@ -224,7 +250,7 @@
- 
-     @Override
-     public Brain<Piglin> getBrain() {
--        return super.getBrain();
-+        return (Brain<Piglin>) super.getBrain(); // CraftBukkit - Decompile error
-     }
- 
-     @Override
-@@ -300,9 +326,11 @@
-     @Override
-     protected void finishConversion(ServerLevel world) {
-         PiglinAi.cancelAdmiring(world, this);
-+        this.forceDrops = true; // Paper - Add missing forceDrop toggles
-         this.inventory.removeAllItems().forEach((itemstack) -> {
-             this.spawnAtLocation(world, itemstack);
-         });
-+        this.forceDrops = false; // Paper - Add missing forceDrop toggles
-         super.finishConversion(world);
-     }
- 
-@@ -374,7 +402,7 @@
-     }
- 
-     protected void holdInOffHand(ItemStack stack) {
--        if (stack.is(PiglinAi.BARTERING_ITEM)) {
-+        if (stack.is(PiglinAi.BARTERING_ITEM) || this.allowedBarterItems.contains(stack.getItem())) { // CraftBukkit - Changes to accept custom payment items
-             this.setItemSlot(EquipmentSlot.OFFHAND, stack);
-             this.setGuaranteedDrop(EquipmentSlot.OFFHAND);
-         } else {
-@@ -401,8 +429,8 @@
-             return false;
-         } else {
-             TagKey<Item> tagkey = this.getPreferredWeaponType();
--            boolean flag = PiglinAi.isLovedItem(newStack) || tagkey != null && newStack.is(tagkey);
--            boolean flag1 = PiglinAi.isLovedItem(currentStack) || tagkey != null && currentStack.is(tagkey);
-+            boolean flag = PiglinAi.isLovedItem(newStack, this) || tagkey != null && newStack.is(tagkey); // CraftBukkit
-+            boolean flag1 = PiglinAi.isLovedItem(currentStack, this) || tagkey != null && currentStack.is(tagkey); // CraftBukkit
- 
-             return flag && !flag1 ? true : (!flag && flag1 ? false : super.canReplaceCurrentItem(newStack, currentStack, slot));
-         }
-@@ -410,7 +438,7 @@
- 
-     @Override
-     protected void pickUpItem(ServerLevel world, ItemEntity itemEntity) {
--        this.onItemPickup(itemEntity);
-+        // this.onItemPickup(itemEntity); // Paper - EntityPickupItemEvent fixes; call in PiglinAi#pickUpItem after EntityPickupItemEvent is fired
-         PiglinAi.pickUpItem(world, this, itemEntity);
-     }
- 
-@@ -431,7 +459,7 @@
- 
-     @Override
-     protected SoundEvent getAmbientSound() {
--        return this.level().isClientSide ? null : (SoundEvent) PiglinAi.getSoundForCurrentActivity(this).orElse((Object) null);
-+        return this.level().isClientSide ? null : (SoundEvent) PiglinAi.getSoundForCurrentActivity(this).orElse(null); // CraftBukkit - Decompile error
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/piglin/PiglinAi.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/piglin/PiglinAi.java.patch
deleted file mode 100644
index 66919128bd..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/piglin/PiglinAi.java.patch
+++ /dev/null
@@ -1,210 +0,0 @@
---- a/net/minecraft/world/entity/monster/piglin/PiglinAi.java
-+++ b/net/minecraft/world/entity/monster/piglin/PiglinAi.java
-@@ -71,6 +71,13 @@
- import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets;
- import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import java.util.stream.Collectors;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.event.entity.PiglinBarterEvent;
-+// CraftBukkit end
- 
- public class PiglinAi {
- 
-@@ -166,7 +173,8 @@
-     }
- 
-     private static void initRideHoglinActivity(Brain<Piglin> brain) {
--        brain.addActivityAndRemoveMemoryWhenStopped(Activity.RIDE, 10, ImmutableList.of(Mount.create(0.8F), SetEntityLookTarget.create(PiglinAi::isPlayerHoldingLovedItem, 8.0F), BehaviorBuilder.sequence(BehaviorBuilder.triggerIf(Entity::isPassenger), TriggerGate.triggerOneShuffled(ImmutableList.builder().addAll(PiglinAi.createLookBehaviors()).add(Pair.of(BehaviorBuilder.triggerIf((entitypiglin) -> {
-+        // CraftBukkit - decompile error
-+        brain.addActivityAndRemoveMemoryWhenStopped(Activity.RIDE, 10, ImmutableList.of(Mount.create(0.8F), SetEntityLookTarget.create(PiglinAi::isPlayerHoldingLovedItem, 8.0F), BehaviorBuilder.sequence(BehaviorBuilder.triggerIf(Entity::isPassenger), TriggerGate.triggerOneShuffled(ImmutableList.<Pair<? extends net.minecraft.world.entity.ai.behavior.declarative.Trigger<? super LivingEntity>, Integer>>builder().addAll(PiglinAi.createLookBehaviors()).add(Pair.of(BehaviorBuilder.triggerIf((entitypiglin) -> {
-             return true;
-         }), 1)).build())), DismountOrSkipMounting.create(8, PiglinAi::wantsToStopRiding)), MemoryModuleType.RIDE_TARGET);
-     }
-@@ -176,7 +184,7 @@
-     }
- 
-     private static RunOne<LivingEntity> createIdleLookBehaviors() {
--        return new RunOne<>(ImmutableList.builder().addAll(PiglinAi.createLookBehaviors()).add(Pair.of(new DoNothing(30, 60), 1)).build());
-+        return new RunOne<>(ImmutableList.<Pair<? extends BehaviorControl<? super LivingEntity>, Integer>>builder().addAll(PiglinAi.createLookBehaviors()).add(Pair.of(new DoNothing(30, 60), 1)).build()); // CraftBukkit - decompile error
-     }
- 
-     private static RunOne<Piglin> createIdleMovementBehaviors() {
-@@ -197,13 +205,13 @@
- 
-     protected static void updateActivity(Piglin piglin) {
-         Brain<Piglin> behaviorcontroller = piglin.getBrain();
--        Activity activity = (Activity) behaviorcontroller.getActiveNonCoreActivity().orElse((Object) null);
-+        Activity activity = (Activity) behaviorcontroller.getActiveNonCoreActivity().orElse(null); // CraftBukkit - decompile error
- 
-         behaviorcontroller.setActiveActivityToFirstValid(ImmutableList.of(Activity.ADMIRE_ITEM, Activity.FIGHT, Activity.AVOID, Activity.CELEBRATE, Activity.RIDE, Activity.IDLE));
--        Activity activity1 = (Activity) behaviorcontroller.getActiveNonCoreActivity().orElse((Object) null);
-+        Activity activity1 = (Activity) behaviorcontroller.getActiveNonCoreActivity().orElse(null); // CraftBukkit - decompile error
- 
-         if (activity != activity1) {
--            Optional optional = PiglinAi.getSoundForCurrentActivity(piglin);
-+            Optional<SoundEvent> optional = PiglinAi.getSoundForCurrentActivity(piglin); // CraftBukkit - decompile error
- 
-             Objects.requireNonNull(piglin);
-             optional.ifPresent(piglin::makeSound);
-@@ -235,23 +243,32 @@
-         PiglinAi.stopWalking(piglin);
-         ItemStack itemstack;
- 
--        if (itemEntity.getItem().is(Items.GOLD_NUGGET)) {
--            piglin.take(itemEntity, itemEntity.getItem().getCount());
--            itemstack = itemEntity.getItem();
--            itemEntity.discard();
--        } else {
-+        // CraftBukkit start
-+        // Paper start - EntityPickupItemEvent fixes; fix event firing twice
-+        if (itemEntity.getItem().is(Items.GOLD_NUGGET)/* && !org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPickupItemEvent(piglin, itemEntity, 0, false).isCancelled()*/) { // Paper
-+            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPickupItemEvent(piglin, itemEntity, 0, false).isCancelled()) return;
-+            piglin.onItemPickup(itemEntity); // Paper - moved from Piglin#pickUpItem - call prior to item entity modification
-+            // Paper end
-+            piglin.take(itemEntity, itemEntity.getItem().getCount());
-+            itemstack = itemEntity.getItem();
-+            itemEntity.discard(EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
-+        } else if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPickupItemEvent(piglin, itemEntity, itemEntity.getItem().getCount() - 1, false).isCancelled()) {
-+            piglin.onItemPickup(itemEntity); // Paper - EntityPickupItemEvent fixes; moved from Piglin#pickUpItem - call prior to item entity modification
-             piglin.take(itemEntity, 1);
-             itemstack = PiglinAi.removeOneItemFromItemEntity(itemEntity);
-+        } else {
-+            return;
-         }
-+        // CraftBukkit end
- 
--        if (PiglinAi.isLovedItem(itemstack)) {
-+        if (PiglinAi.isLovedItem(itemstack, piglin)) { // CraftBukkit - Changes to allow for custom payment in bartering
-             piglin.getBrain().eraseMemory(MemoryModuleType.TIME_TRYING_TO_REACH_ADMIRE_ITEM);
-             PiglinAi.holdInOffhand(world, piglin, itemstack);
-             PiglinAi.admireGoldItem(piglin);
-         } else if (PiglinAi.isFood(itemstack) && !PiglinAi.hasEatenRecently(piglin)) {
-             PiglinAi.eat(piglin);
-         } else {
--            boolean flag = !piglin.equipItemIfPossible(world, itemstack).equals(ItemStack.EMPTY);
-+            boolean flag = !piglin.equipItemIfPossible(world, itemstack, null).equals(ItemStack.EMPTY); // CraftBukkit // Paper - pass null item entity to prevent duplicate pickup item event call - called above.
- 
-             if (!flag) {
-                 PiglinAi.putInInventory(piglin, itemstack);
-@@ -261,7 +278,9 @@
- 
-     private static void holdInOffhand(ServerLevel world, Piglin piglin, ItemStack stack) {
-         if (PiglinAi.isHoldingItemInOffHand(piglin)) {
-+            piglin.forceDrops = true; // Paper - Add missing forceDrop toggles
-             piglin.spawnAtLocation(world, piglin.getItemInHand(InteractionHand.OFF_HAND));
-+            piglin.forceDrops = false; // Paper - Add missing forceDrop toggles
-         }
- 
-         piglin.holdInOffHand(stack);
-@@ -272,7 +291,7 @@
-         ItemStack itemstack1 = itemstack.split(1);
- 
-         if (itemstack.isEmpty()) {
--            stack.discard();
-+            stack.discard(EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
-         } else {
-             stack.setItem(itemstack);
-         }
-@@ -287,9 +306,14 @@
-         boolean flag1;
- 
-         if (piglin.isAdult()) {
--            flag1 = PiglinAi.isBarterCurrency(itemstack);
-+            flag1 = PiglinAi.isBarterCurrency(itemstack, piglin); // CraftBukkit - Changes to allow custom payment for bartering
-             if (barter && flag1) {
--                PiglinAi.throwItems(piglin, PiglinAi.getBarterResponseItems(piglin));
-+                // CraftBukkit start
-+                PiglinBarterEvent event = CraftEventFactory.callPiglinBarterEvent(piglin, PiglinAi.getBarterResponseItems(piglin), itemstack);
-+                if (!event.isCancelled()) {
-+                    PiglinAi.throwItems(piglin, event.getOutcome().stream().map(CraftItemStack::asNMSCopy).collect(Collectors.toList()));
-+                }
-+                // CraftBukkit end
-             } else if (!flag1) {
-                 boolean flag2 = !piglin.equipItemIfPossible(world, itemstack).isEmpty();
- 
-@@ -302,7 +326,7 @@
-             if (!flag1) {
-                 ItemStack itemstack1 = piglin.getMainHandItem();
- 
--                if (PiglinAi.isLovedItem(itemstack1)) {
-+                if (PiglinAi.isLovedItem(itemstack1, piglin)) { // CraftBukkit - Changes to allow for custom payment in bartering
-                     PiglinAi.putInInventory(piglin, itemstack1);
-                 } else {
-                     PiglinAi.throwItems(piglin, Collections.singletonList(itemstack1));
-@@ -316,7 +340,9 @@
- 
-     protected static void cancelAdmiring(ServerLevel world, Piglin piglin) {
-         if (PiglinAi.isAdmiringItem(piglin) && !piglin.getOffhandItem().isEmpty()) {
-+            piglin.forceDrops = true; // Paper - Add missing forceDrop toggles
-             piglin.spawnAtLocation(world, piglin.getOffhandItem());
-+            piglin.forceDrops = false; // Paper - Add missing forceDrop toggles
-             piglin.setItemInHand(InteractionHand.OFF_HAND, ItemStack.EMPTY);
-         }
- 
-@@ -379,15 +405,21 @@
-             return false;
-         } else if (PiglinAi.isAdmiringDisabled(piglin) && piglin.getBrain().hasMemoryValue(MemoryModuleType.ATTACK_TARGET)) {
-             return false;
--        } else if (PiglinAi.isBarterCurrency(stack)) {
-+        } else if (PiglinAi.isBarterCurrency(stack, piglin)) { // CraftBukkit
-             return PiglinAi.isNotHoldingLovedItemInOffHand(piglin);
-         } else {
-             boolean flag = piglin.canAddToInventory(stack);
- 
--            return stack.is(Items.GOLD_NUGGET) ? flag : (PiglinAi.isFood(stack) ? !PiglinAi.hasEatenRecently(piglin) && flag : (!PiglinAi.isLovedItem(stack) ? piglin.canReplaceCurrentItem(stack) : PiglinAi.isNotHoldingLovedItemInOffHand(piglin) && flag));
-+            return stack.is(Items.GOLD_NUGGET) ? flag : (PiglinAi.isFood(stack) ? !PiglinAi.hasEatenRecently(piglin) && flag : (!PiglinAi.isLovedItem(stack, piglin) ? piglin.canReplaceCurrentItem(stack) : PiglinAi.isNotHoldingLovedItemInOffHand(piglin) && flag)); // Paper - upstream missed isLovedItem check
-         }
-     }
- 
-+    // CraftBukkit start - Added method to allow checking for custom payment items
-+    protected static boolean isLovedItem(ItemStack itemstack, Piglin piglin) {
-+        return PiglinAi.isLovedItem(itemstack) || (piglin.interestItems.contains(itemstack.getItem()) || piglin.allowedBarterItems.contains(itemstack.getItem()));
-+    }
-+    // CraftBukkit end
-+
-     protected static boolean isLovedItem(ItemStack stack) {
-         return stack.is(ItemTags.PIGLIN_LOVED);
-     }
-@@ -451,6 +483,7 @@
-     }
- 
-     public static void angerNearbyPiglins(ServerLevel world, Player player, boolean blockOpen) {
-+        if (!player.level().paperConfig().entities.behavior.piglinsGuardChests) return; // Paper - Config option for Piglins guarding chests
-         List<Piglin> list = player.level().getEntitiesOfClass(Piglin.class, player.getBoundingBox().inflate(16.0D));
- 
-         list.stream().filter(PiglinAi::isIdle).filter((entitypiglin) -> {
-@@ -481,7 +514,7 @@
-     }
- 
-     protected static boolean canAdmire(Piglin piglin, ItemStack nearbyItems) {
--        return !PiglinAi.isAdmiringDisabled(piglin) && !PiglinAi.isAdmiringItem(piglin) && piglin.isAdult() && PiglinAi.isBarterCurrency(nearbyItems);
-+        return !PiglinAi.isAdmiringDisabled(piglin) && !PiglinAi.isAdmiringItem(piglin) && piglin.isAdult() && PiglinAi.isBarterCurrency(nearbyItems, piglin); // CraftBukkit
-     }
- 
-     protected static void wasHurtBy(ServerLevel world, Piglin piglin, LivingEntity attacker) {
-@@ -735,6 +768,12 @@
-         return entity.getBrain().hasMemoryValue(MemoryModuleType.ADMIRING_ITEM);
-     }
- 
-+    // CraftBukkit start - Changes to allow custom payment for bartering
-+    private static boolean isBarterCurrency(ItemStack itemstack, Piglin piglin) {
-+        return PiglinAi.isBarterCurrency(itemstack) || piglin.allowedBarterItems.contains(itemstack.getItem());
-+    }
-+    // CraftBukkit end
-+
-     private static boolean isBarterCurrency(ItemStack stack) {
-         return stack.is(PiglinAi.BARTERING_ITEM);
-     }
-@@ -772,7 +811,7 @@
-     }
- 
-     private static boolean isNotHoldingLovedItemInOffHand(Piglin piglin) {
--        return piglin.getOffhandItem().isEmpty() || !PiglinAi.isLovedItem(piglin.getOffhandItem());
-+        return piglin.getOffhandItem().isEmpty() || !PiglinAi.isLovedItem(piglin.getOffhandItem(), piglin); // CraftBukkit - Changes to allow custom payment for bartering
-     }
- 
-     public static boolean isZombified(EntityType<?> entityType) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/warden/AngerManagement.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/warden/AngerManagement.java.patch
deleted file mode 100644
index f65a699a17..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/warden/AngerManagement.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/entity/monster/warden/AngerManagement.java
-+++ b/net/minecraft/world/entity/monster/warden/AngerManagement.java
-@@ -146,7 +146,7 @@
- 
-     public int increaseAnger(Entity entity, int amount) {
-         boolean bl = !this.angerBySuspect.containsKey(entity);
--        int i = this.angerBySuspect.computeInt(entity, (suspect, anger) -> Math.min(150, (anger == null ? 0 : anger) + amount));
-+        int i = this.angerBySuspect.computeInt(entity, (suspect, anger) -> Math.min(150, (anger == null ? 0 : anger) + amount)); // Paper - diff on change (Warden#increaseAngerAt WardenAngerChangeEvent)
-         if (bl) {
-             int j = this.angerByUuid.removeInt(entity.getUUID());
-             i += j;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/warden/Warden.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/warden/Warden.java.patch
deleted file mode 100644
index c6378460b2..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/warden/Warden.java.patch
+++ /dev/null
@@ -1,59 +0,0 @@
---- a/net/minecraft/world/entity/monster/warden/Warden.java
-+++ b/net/minecraft/world/entity/monster/warden/Warden.java
-@@ -375,7 +375,7 @@
- 
-     @Override
-     public Brain<Warden> getBrain() {
--        return super.getBrain();
-+        return (Brain<Warden>) super.getBrain(); // CraftBukkit - decompile error
-     }
- 
-     @Override
-@@ -412,7 +412,7 @@
-     public static void applyDarknessAround(ServerLevel world, Vec3 pos, @Nullable Entity entity, int range) {
-         MobEffectInstance mobeffect = new MobEffectInstance(MobEffects.DARKNESS, 260, 0, false, false);
- 
--        MobEffectUtil.addEffectToPlayersAround(world, entity, pos, (double) range, mobeffect, 200);
-+        MobEffectUtil.addEffectToPlayersAround(world, entity, pos, range, mobeffect, 200, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.WARDEN); // CraftBukkit - Add EntityPotionEffectEvent.Cause
-     }
- 
-     @Override
-@@ -482,6 +482,15 @@
-     @VisibleForTesting
-     public void increaseAngerAt(@Nullable Entity entity, int amount, boolean listening) {
-         if (!this.isNoAi() && this.canTargetEntity(entity)) {
-+            // Paper start - Add WardenAngerChangeEvent
-+            int activeAnger = this.angerManagement.getActiveAnger(entity);
-+            io.papermc.paper.event.entity.WardenAngerChangeEvent event = new io.papermc.paper.event.entity.WardenAngerChangeEvent((org.bukkit.entity.Warden) this.getBukkitEntity(), entity.getBukkitEntity(), activeAnger, Math.min(150, activeAnger + amount));
-+            this.level().getCraftServer().getPluginManager().callEvent(event);
-+            if (event.isCancelled()) {
-+                return;
-+            }
-+            amount = event.getNewAnger() - activeAnger;
-+            // Paper end - Add WardenAngerChangeEvent
-             WardenAi.setDigCooldown(this);
-             boolean flag1 = !(this.getTarget() instanceof Player);
-             int j = this.angerManagement.increaseAnger(entity, amount);
-@@ -547,7 +556,7 @@
- 
-     public void setAttackTarget(LivingEntity target) {
-         this.getBrain().eraseMemory(MemoryModuleType.ROAR_TARGET);
--        this.getBrain().setMemory(MemoryModuleType.ATTACK_TARGET, (Object) target);
-+        this.getBrain().setMemory(MemoryModuleType.ATTACK_TARGET, target); // CraftBukkit - decompile error
-         this.getBrain().eraseMemory(MemoryModuleType.CANT_REACH_WALK_TARGET_SINCE);
-         SonicBoom.setCooldown(this, 200);
-     }
-@@ -582,11 +591,11 @@
- 
-     @Override
-     protected PathNavigation createNavigation(Level world) {
--        return new GroundPathNavigation(this, this, world) {
-+        return new GroundPathNavigation(this, world) { // CraftBukkit - decompile error
-             @Override
-             protected PathFinder createPathFinder(int range) {
-                 this.nodeEvaluator = new WalkNodeEvaluator();
--                return new PathFinder(this, this.nodeEvaluator, range) {
-+                return new PathFinder(this.nodeEvaluator, range) { // CraftBukkit - decompile error
-                     @Override
-                     protected float distance(Node a, Node b) {
-                         return a.distanceToXZ(b);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/npc/Villager.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/npc/Villager.java.patch
deleted file mode 100644
index cb4983a642..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/npc/Villager.java.patch
+++ /dev/null
@@ -1,211 +0,0 @@
---- a/net/minecraft/world/entity/npc/Villager.java
-+++ b/net/minecraft/world/entity/npc/Villager.java
-@@ -93,6 +93,14 @@
- import net.minecraft.world.phys.AABB;
- import org.slf4j.Logger;
- 
-+// CraftBukkit start
-+import org.bukkit.Bukkit;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.event.entity.EntityTransformEvent;
-+import org.bukkit.event.entity.VillagerReplenishTradeEvent;
-+// CraftBukkit end
-+
- public class Villager extends AbstractVillager implements ReputationEventHandler, VillagerDataHolder {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
-@@ -150,7 +158,7 @@
- 
-     @Override
-     public Brain<Villager> getBrain() {
--        return super.getBrain();
-+        return (Brain<Villager>) super.getBrain(); // CraftBukkit - decompile error
-     }
- 
-     @Override
-@@ -216,7 +224,18 @@
-         return this.assignProfessionWhenSpawned;
-     }
- 
-+    // Spigot Start
-     @Override
-+    public void inactiveTick() {
-+        // SPIGOT-3874, SPIGOT-3894, SPIGOT-3846, SPIGOT-5286 :(
-+        if (this.level().spigotConfig.tickInactiveVillagers && this.isEffectiveAi()) {
-+            this.customServerAiStep((ServerLevel) this.level());
-+        }
-+        super.inactiveTick();
-+    }
-+    // Spigot End
-+
-+    @Override
-     protected void customServerAiStep(ServerLevel world) {
-         ProfilerFiller gameprofilerfiller = Profiler.get();
- 
-@@ -235,7 +254,7 @@
-                     this.increaseProfessionLevelOnUpdate = false;
-                 }
- 
--                this.addEffect(new MobEffectInstance(MobEffects.REGENERATION, 200, 0));
-+                this.addEffect(new MobEffectInstance(MobEffects.REGENERATION, 200, 0), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.VILLAGER_TRADE); // CraftBukkit
-             }
-         }
- 
-@@ -360,7 +379,13 @@
-         while (iterator.hasNext()) {
-             MerchantOffer merchantrecipe = (MerchantOffer) iterator.next();
- 
--            merchantrecipe.resetUses();
-+            // CraftBukkit start
-+            VillagerReplenishTradeEvent event = new VillagerReplenishTradeEvent((org.bukkit.entity.Villager) this.getBukkitEntity(), merchantrecipe.asBukkit());
-+            Bukkit.getPluginManager().callEvent(event);
-+            if (!event.isCancelled()) {
-+                merchantrecipe.resetUses();
-+            }
-+            // CraftBukkit end
-         }
- 
-         this.resendOffersToTradingPlayer();
-@@ -429,7 +454,13 @@
-             while (iterator.hasNext()) {
-                 MerchantOffer merchantrecipe = (MerchantOffer) iterator.next();
- 
--                merchantrecipe.resetUses();
-+                // CraftBukkit start
-+                VillagerReplenishTradeEvent event = new VillagerReplenishTradeEvent((org.bukkit.entity.Villager) this.getBukkitEntity(), merchantrecipe.asBukkit());
-+                Bukkit.getPluginManager().callEvent(event);
-+                if (!event.isCancelled()) {
-+                    merchantrecipe.resetUses();
-+                }
-+                // CraftBukkit end
-             }
-         }
- 
-@@ -459,6 +490,7 @@
- 
-             while (iterator.hasNext()) {
-                 MerchantOffer merchantrecipe = (MerchantOffer) iterator.next();
-+                if (merchantrecipe.ignoreDiscounts) continue; // Paper - Add ignore discounts API
- 
-                 merchantrecipe.addToSpecialPriceDiff(-Mth.floor((float) i * merchantrecipe.getPriceMultiplier()));
-             }
-@@ -471,6 +503,7 @@
- 
-             while (iterator1.hasNext()) {
-                 MerchantOffer merchantrecipe1 = (MerchantOffer) iterator1.next();
-+                if (merchantrecipe1.ignoreDiscounts) continue; // Paper - Add ignore discounts API
-                 double d0 = 0.3D + 0.0625D * (double) j;
-                 int k = (int) Math.floor(d0 * (double) merchantrecipe1.getBaseCostA().getCount());
- 
-@@ -489,7 +522,7 @@
-     @Override
-     public void addAdditionalSaveData(CompoundTag nbt) {
-         super.addAdditionalSaveData(nbt);
--        DataResult dataresult = VillagerData.CODEC.encodeStart(NbtOps.INSTANCE, this.getVillagerData());
-+        DataResult<Tag> dataresult = VillagerData.CODEC.encodeStart(NbtOps.INSTANCE, this.getVillagerData()); // CraftBukkit - decompile error
-         Logger logger = Villager.LOGGER;
- 
-         Objects.requireNonNull(logger);
-@@ -512,7 +545,7 @@
-     public void readAdditionalSaveData(CompoundTag nbt) {
-         super.readAdditionalSaveData(nbt);
-         if (nbt.contains("VillagerData", 10)) {
--            DataResult dataresult = VillagerData.CODEC.parse(NbtOps.INSTANCE, nbt.get("VillagerData"));
-+            DataResult<VillagerData> dataresult = VillagerData.CODEC.parse(new Dynamic(NbtOps.INSTANCE, nbt.get("VillagerData")));
-             Logger logger = Villager.LOGGER;
- 
-             Objects.requireNonNull(logger);
-@@ -599,7 +632,7 @@
-         }
- 
-         if (offer.shouldRewardExp()) {
--            this.level().addFreshEntity(new ExperienceOrb(this.level(), this.getX(), this.getY() + 0.5D, this.getZ(), i));
-+            this.level().addFreshEntity(new ExperienceOrb(this.level(), this.getX(), this.getY() + 0.5D, this.getZ(), i, org.bukkit.entity.ExperienceOrb.SpawnReason.VILLAGER_TRADE, this.getTradingPlayer(), this)); // Paper
-         }
- 
-     }
-@@ -618,7 +651,7 @@
- 
-     @Override
-     public void die(DamageSource damageSource) {
--        Villager.LOGGER.info("Villager {} died, message: '{}'", this, damageSource.getLocalizedDeathMessage(this).getString());
-+        if (org.spigotmc.SpigotConfig.logVillagerDeaths) Villager.LOGGER.info("Villager {} died, message: '{}'", this, damageSource.getLocalizedDeathMessage(this).getString()); // Spigot
-         Entity entity = damageSource.getEntity();
- 
-         if (entity != null) {
-@@ -803,12 +836,19 @@
-     @Override
-     public void thunderHit(ServerLevel world, LightningBolt lightning) {
-         if (world.getDifficulty() != Difficulty.PEACEFUL) {
--            Villager.LOGGER.info("Villager {} was struck by lightning {}.", this, lightning);
-+            // Paper - Add EntityZapEvent; move log down, event can cancel
-             Witch entitywitch = (Witch) this.convertTo(EntityType.WITCH, ConversionParams.single(this, false, false), (entitywitch1) -> {
-+               // Paper start - Add EntityZapEvent
-+               if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityZapEvent(this, lightning, entitywitch1).isCancelled()) {
-+                   return false;
-+               }
-+               if (org.spigotmc.SpigotConfig.logVillagerDeaths) Villager.LOGGER.info("Villager {} was struck by lightning {}.", this, lightning); // Move down
-+               // Paper end - Add EntityZapEvent
-                 entitywitch1.finalizeSpawn(world, world.getCurrentDifficultyAt(entitywitch1.blockPosition()), EntitySpawnReason.CONVERSION, (SpawnGroupData) null);
-                 entitywitch1.setPersistenceRequired();
-                 this.releaseAllPois();
--            });
-+                return true; // Paper start - Add EntityZapEvent
-+            }, EntityTransformEvent.TransformReason.LIGHTNING, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.LIGHTNING); // CraftBukkit
- 
-             if (entitywitch == null) {
-                 super.thunderHit(world, lightning);
-@@ -855,6 +895,12 @@
- 
-     @Override
-     protected void updateTrades() {
-+        // Paper start - More vanilla friendly methods to update trades
-+        updateTrades(TRADES_PER_LEVEL);
-+    }
-+
-+    public boolean updateTrades(int amount) {
-+        // Paper end - More vanilla friendly methods to update trades
-         VillagerData villagerdata = this.getVillagerData();
-         Int2ObjectMap int2objectmap;
- 
-@@ -872,9 +918,11 @@
-             if (avillagertrades_imerchantrecipeoption != null) {
-                 MerchantOffers merchantrecipelist = this.getOffers();
- 
--                this.addOffersFromItemListings(merchantrecipelist, avillagertrades_imerchantrecipeoption, 2);
-+                this.addOffersFromItemListings(merchantrecipelist, avillagertrades_imerchantrecipeoption, amount); // Paper - More vanilla friendly methods to update trades
-+                return true; // Paper - More vanilla friendly methods to update trades
-             }
-         }
-+        return false; // Paper - More vanilla friendly methods to update trades
-     }
- 
-     public void gossip(ServerLevel world, Villager villager, long time) {
-@@ -906,7 +954,7 @@
-             }).limit(5L).toList();
- 
-             if (list1.size() >= requiredCount) {
--                if (!SpawnUtil.trySpawnMob(EntityType.IRON_GOLEM, EntitySpawnReason.MOB_SUMMONED, world, this.blockPosition(), 10, 8, 6, SpawnUtil.Strategy.LEGACY_IRON_GOLEM, false).isEmpty()) {
-+                if (SpawnUtil.trySpawnMob(EntityType.IRON_GOLEM, EntitySpawnReason.MOB_SUMMONED, world, this.blockPosition(), 10, 8, 6, SpawnUtil.Strategy.LEGACY_IRON_GOLEM, false, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.VILLAGE_DEFENSE, () -> {GolemSensor.golemDetected(this);}).isPresent()) { // CraftBukkit // Paper - Set Golem Last Seen to stop it from spawning another one
-                     list.forEach(GolemSensor::golemDetected);
-                 }
-             }
-@@ -963,7 +1011,7 @@
-     @Override
-     public void startSleeping(BlockPos pos) {
-         super.startSleeping(pos);
--        this.brain.setMemory(MemoryModuleType.LAST_SLEPT, (Object) this.level().getGameTime());
-+        this.brain.setMemory(MemoryModuleType.LAST_SLEPT, this.level().getGameTime()); // CraftBukkit - decompile error
-         this.brain.eraseMemory(MemoryModuleType.WALK_TARGET);
-         this.brain.eraseMemory(MemoryModuleType.CANT_REACH_WALK_TARGET_SINCE);
-     }
-@@ -971,7 +1019,7 @@
-     @Override
-     public void stopSleeping() {
-         super.stopSleeping();
--        this.brain.setMemory(MemoryModuleType.LAST_WOKEN, (Object) this.level().getGameTime());
-+        this.brain.setMemory(MemoryModuleType.LAST_WOKEN, this.level().getGameTime()); // CraftBukkit - decompile error
-     }
- 
-     private boolean golemSpawnConditionsMet(long worldTime) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/npc/WanderingTraderSpawner.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/npc/WanderingTraderSpawner.java.patch
deleted file mode 100644
index 5b4f2d10a7..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/npc/WanderingTraderSpawner.java.patch
+++ /dev/null
@@ -1,100 +0,0 @@
---- a/net/minecraft/world/entity/npc/WanderingTraderSpawner.java
-+++ b/net/minecraft/world/entity/npc/WanderingTraderSpawner.java
-@@ -40,43 +40,53 @@
- 
-     public WanderingTraderSpawner(ServerLevelData properties) {
-         this.serverLevelData = properties;
--        this.tickDelay = 1200;
--        this.spawnDelay = properties.getWanderingTraderSpawnDelay();
--        this.spawnChance = properties.getWanderingTraderSpawnChance();
--        if (this.spawnDelay == 0 && this.spawnChance == 0) {
--            this.spawnDelay = 24000;
--            properties.setWanderingTraderSpawnDelay(this.spawnDelay);
--            this.spawnChance = 25;
--            properties.setWanderingTraderSpawnChance(this.spawnChance);
--        }
-+        // Paper start - Add Wandering Trader spawn rate config options
-+        this.tickDelay = Integer.MIN_VALUE;
-+        //this.spawnDelay = properties.getWanderingTraderSpawnDelay(); // Paper - This value is read from the world file only for the first spawn, after which vanilla uses a hardcoded value
-+        //this.spawnChance = properties.getWanderingTraderSpawnChance(); // Paper - This value is read from the world file only for the first spawn, after which vanilla uses a hardcoded value
-+        //if (this.spawnDelay == 0 && this.spawnChance == 0) {
-+        //    this.spawnDelay = 24000;
-+        //    properties.setWanderingTraderSpawnDelay(this.spawnDelay);
-+        //    this.spawnChance = 25;
-+        //    properties.setWanderingTraderSpawnChance(this.spawnChance);
-+        //}
-+        // Paper end - Add Wandering Trader spawn rate config options
- 
-     }
- 
-     @Override
-     public int tick(ServerLevel world, boolean spawnMonsters, boolean spawnAnimals) {
-+        // Paper start - Add Wandering Trader spawn rate config options
-+        if (this.tickDelay == Integer.MIN_VALUE) {
-+            this.tickDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength;
-+            this.spawnDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnDayLength;
-+            this.spawnChance = world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin;
-+        }
-         if (!world.getGameRules().getBoolean(GameRules.RULE_DO_TRADER_SPAWNING)) {
-             return 0;
--        } else if (--this.tickDelay > 0) {
-+        } else if (this.tickDelay - 1 > 0) {
-+            this.tickDelay = this.tickDelay - 1;
-             return 0;
-         } else {
--            this.tickDelay = 1200;
--            this.spawnDelay -= 1200;
--            this.serverLevelData.setWanderingTraderSpawnDelay(this.spawnDelay);
-+            this.tickDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength;
-+            this.spawnDelay = this.spawnDelay - world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength;
-+            //this.serverLevelData.setWanderingTraderSpawnDelay(this.spawnDelay); // Paper - We don't need to save this value to disk if it gets set back to a hardcoded value anyways
-             if (this.spawnDelay > 0) {
-                 return 0;
-             } else {
--                this.spawnDelay = 24000;
-+                this.spawnDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnDayLength;
-                 if (!world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) {
-                     return 0;
-                 } else {
-                     int i = this.spawnChance;
- 
--                    this.spawnChance = Mth.clamp(this.spawnChance + 25, 25, 75);
--                    this.serverLevelData.setWanderingTraderSpawnChance(this.spawnChance);
-+                    // this.serverLevelData.setWanderingTraderSpawnChance(this.spawnChance); // Paper - We don't need to save this value to disk if it gets set back to a hardcoded value anyways
-+                    this.spawnChance = Mth.clamp(i + world.paperConfig().entities.spawning.wanderingTrader.spawnChanceFailureIncrement, world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin, world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMax);
-                     if (this.random.nextInt(100) > i) {
-                         return 0;
-                     } else if (this.spawn(world)) {
--                        this.spawnChance = 25;
-+                        this.spawnChance = world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin;
-+                        // Paper end - Add Wandering Trader spawn rate config options
-                         return 1;
-                     } else {
-                         return 0;
-@@ -110,7 +120,7 @@
-                     return false;
-                 }
- 
--                WanderingTrader entityvillagertrader = (WanderingTrader) EntityType.WANDERING_TRADER.spawn(world, blockposition2, EntitySpawnReason.EVENT);
-+                WanderingTrader entityvillagertrader = (WanderingTrader) EntityType.WANDERING_TRADER.spawn(world, trader -> trader.setDespawnDelay(48000), blockposition2, EntitySpawnReason.EVENT, false, false, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.NATURAL); // CraftBukkit // Paper - set despawnTimer before spawn events called
- 
-                 if (entityvillagertrader != null) {
-                     for (int i = 0; i < 2; ++i) {
-@@ -118,7 +128,7 @@
-                     }
- 
-                     this.serverLevelData.setWanderingTraderId(entityvillagertrader.getUUID());
--                    entityvillagertrader.setDespawnDelay(48000);
-+                    // entityvillagertrader.setDespawnDelay(48000); // CraftBukkit - moved to EntityVillagerTrader constructor. This lets the value be modified by plugins on CreatureSpawnEvent
-                     entityvillagertrader.setWanderTarget(blockposition1);
-                     entityvillagertrader.restrictTo(blockposition1, 16);
-                     return true;
-@@ -133,7 +143,7 @@
-         BlockPos blockposition = this.findSpawnPositionNear(world, wanderingTrader.blockPosition(), range);
- 
-         if (blockposition != null) {
--            TraderLlama entityllamatrader = (TraderLlama) EntityType.TRADER_LLAMA.spawn(world, blockposition, EntitySpawnReason.EVENT);
-+            TraderLlama entityllamatrader = (TraderLlama) EntityType.TRADER_LLAMA.spawn(world, blockposition, EntitySpawnReason.EVENT, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.NATURAL); // CraftBukkit
- 
-             if (entityllamatrader != null) {
-                 entityllamatrader.setLeashedTo(wanderingTrader, true);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/player/Inventory.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/player/Inventory.java.patch
deleted file mode 100644
index be3c5cccdb..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/player/Inventory.java.patch
+++ /dev/null
@@ -1,130 +0,0 @@
---- a/net/minecraft/world/entity/player/Inventory.java
-+++ b/net/minecraft/world/entity/player/Inventory.java
-@@ -23,6 +23,12 @@
- import net.minecraft.world.item.Item;
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.level.block.state.BlockState;
-+// CraftBukkit start
-+import java.util.ArrayList;
-+import org.bukkit.Location;
-+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
-+import org.bukkit.entity.HumanEntity;
-+// CraftBukkit end
- 
- public class Inventory implements Container, Nameable {
- 
-@@ -38,7 +44,55 @@
-     public int selected;
-     public final Player player;
-     private int timesChanged;
-+
-+    // CraftBukkit start - add fields and methods
-+    public List<HumanEntity> transaction = new java.util.ArrayList<HumanEntity>();
-+    private int maxStack = MAX_STACK;
-+
-+    public List<ItemStack> getContents() {
-+        List<ItemStack> combined = new ArrayList<ItemStack>(this.items.size() + this.armor.size() + this.offhand.size());
-+        for (List<net.minecraft.world.item.ItemStack> sub : this.compartments) {
-+            combined.addAll(sub);
-+        }
-+
-+        return combined;
-+    }
-+
-+    public List<ItemStack> getArmorContents() {
-+        return this.armor;
-+    }
-+
-+    public void onOpen(CraftHumanEntity who) {
-+        this.transaction.add(who);
-+    }
-+
-+    public void onClose(CraftHumanEntity who) {
-+        this.transaction.remove(who);
-+    }
-+
-+    public List<HumanEntity> getViewers() {
-+        return this.transaction;
-+    }
-+
-+    public org.bukkit.inventory.InventoryHolder getOwner() {
-+        return this.player.getBukkitEntity();
-+    }
-+
-+    @Override
-+    public int getMaxStackSize() {
-+        return this.maxStack;
-+    }
-+
-+    public void setMaxStackSize(int size) {
-+        this.maxStack = size;
-+    }
- 
-+    @Override
-+    public Location getLocation() {
-+        return this.player.getBukkitEntity().getLocation();
-+    }
-+    // CraftBukkit end
-+
-     public Inventory(Player player) {
-         this.items = NonNullList.withSize(36, ItemStack.EMPTY);
-         this.armor = NonNullList.withSize(4, ItemStack.EMPTY);
-@@ -56,9 +110,31 @@
-     }
- 
-     private boolean hasRemainingSpaceForItem(ItemStack existingStack, ItemStack stack) {
--        return !existingStack.isEmpty() && ItemStack.isSameItemSameComponents(existingStack, stack) && existingStack.isStackable() && existingStack.getCount() < this.getMaxStackSize(existingStack);
-+        return !existingStack.isEmpty() && existingStack.isStackable() && existingStack.getCount() < this.getMaxStackSize(existingStack) && ItemStack.isSameItemSameComponents(existingStack, stack); // Paper - check if itemstack is stackable first
-     }
- 
-+    // CraftBukkit start - Watch method above! :D
-+    public int canHold(ItemStack itemstack) {
-+        int remains = itemstack.getCount();
-+        for (int i = 0; i < this.items.size(); ++i) {
-+            ItemStack itemstack1 = this.getItem(i);
-+            if (itemstack1.isEmpty()) return itemstack.getCount();
-+
-+            if (this.hasRemainingSpaceForItem(itemstack1, itemstack)) {
-+                remains -= (itemstack1.getMaxStackSize() < this.getMaxStackSize() ? itemstack1.getMaxStackSize() : this.getMaxStackSize()) - itemstack1.getCount();
-+            }
-+            if (remains <= 0) return itemstack.getCount();
-+        }
-+        ItemStack offhandItemStack = this.getItem(this.items.size() + this.armor.size());
-+        if (this.hasRemainingSpaceForItem(offhandItemStack, itemstack)) {
-+            remains -= (offhandItemStack.getMaxStackSize() < this.getMaxStackSize() ? offhandItemStack.getMaxStackSize() : this.getMaxStackSize()) - offhandItemStack.getCount();
-+        }
-+        if (remains <= 0) return itemstack.getCount();
-+
-+        return itemstack.getCount() - remains;
-+    }
-+    // CraftBukkit end
-+
-     public int getFreeSlot() {
-         for (int i = 0; i < this.items.size(); ++i) {
-             if (((ItemStack) this.items.get(i)).isEmpty()) {
-@@ -69,8 +145,10 @@
-         return -1;
-     }
- 
--    public void addAndPickItem(ItemStack stack) {
--        this.selected = this.getSuitableHotbarSlot();
-+    // Paper start - Add PlayerPickItemEvent
-+    public void addAndPickItem(ItemStack stack, final int targetSlot) {
-+        this.selected = targetSlot;
-+        // Paper end - Add PlayerPickItemEvent
-         if (!((ItemStack) this.items.get(this.selected)).isEmpty()) {
-             int i = this.getFreeSlot();
- 
-@@ -82,8 +160,10 @@
-         this.items.set(this.selected, stack);
-     }
- 
--    public void pickSlot(int slot) {
--        this.selected = this.getSuitableHotbarSlot();
-+    // Paper start - Add PlayerPickItemEvent
-+    public void pickSlot(int slot, final int targetSlot) {
-+        this.selected = targetSlot;
-+        // Paper end - Add PlayerPickItemEvent
-         ItemStack itemstack = (ItemStack) this.items.get(this.selected);
- 
-         this.items.set(this.selected, (ItemStack) this.items.get(slot));
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/player/Player.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/player/Player.java.patch
deleted file mode 100644
index 3f8a17b363..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/player/Player.java.patch
+++ /dev/null
@@ -1,735 +0,0 @@
---- a/net/minecraft/world/entity/player/Player.java
-+++ b/net/minecraft/world/entity/player/Player.java
-@@ -118,6 +118,15 @@
- import net.minecraft.world.scores.PlayerTeam;
- import net.minecraft.world.scores.Scoreboard;
- import org.slf4j.Logger;
-+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.craftbukkit.util.CraftVector;
-+import org.bukkit.event.entity.CreatureSpawnEvent;
-+import org.bukkit.event.entity.EntityDamageEvent;
-+import org.bukkit.event.entity.EntityExhaustionEvent;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.event.player.PlayerVelocityEvent;
-+// CraftBukkit end
- 
- public abstract class Player extends LivingEntity {
- 
-@@ -139,7 +148,8 @@
-     private static final int CURRENT_IMPULSE_CONTEXT_RESET_GRACE_TIME_TICKS = 40;
-     public static final Vec3 DEFAULT_VEHICLE_ATTACHMENT = new Vec3(0.0D, 0.6D, 0.0D);
-     public static final EntityDimensions STANDING_DIMENSIONS = EntityDimensions.scalable(0.6F, 1.8F).withEyeHeight(1.62F).withAttachments(EntityAttachments.builder().attach(EntityAttachment.VEHICLE, Player.DEFAULT_VEHICLE_ATTACHMENT));
--    private static final Map<Pose, EntityDimensions> POSES = ImmutableMap.builder().put(Pose.STANDING, Player.STANDING_DIMENSIONS).put(Pose.SLEEPING, Player.SLEEPING_DIMENSIONS).put(Pose.FALL_FLYING, EntityDimensions.scalable(0.6F, 0.6F).withEyeHeight(0.4F)).put(Pose.SWIMMING, EntityDimensions.scalable(0.6F, 0.6F).withEyeHeight(0.4F)).put(Pose.SPIN_ATTACK, EntityDimensions.scalable(0.6F, 0.6F).withEyeHeight(0.4F)).put(Pose.CROUCHING, EntityDimensions.scalable(0.6F, 1.5F).withEyeHeight(1.27F).withAttachments(EntityAttachments.builder().attach(EntityAttachment.VEHICLE, Player.DEFAULT_VEHICLE_ATTACHMENT))).put(Pose.DYING, EntityDimensions.fixed(0.2F, 0.2F).withEyeHeight(1.62F)).build();
-+    // CraftBukkit - decompile error
-+    private static final Map<Pose, EntityDimensions> POSES = ImmutableMap.<Pose, EntityDimensions>builder().put(Pose.STANDING, Player.STANDING_DIMENSIONS).put(Pose.SLEEPING, Player.SLEEPING_DIMENSIONS).put(Pose.FALL_FLYING, EntityDimensions.scalable(0.6F, 0.6F).withEyeHeight(0.4F)).put(Pose.SWIMMING, EntityDimensions.scalable(0.6F, 0.6F).withEyeHeight(0.4F)).put(Pose.SPIN_ATTACK, EntityDimensions.scalable(0.6F, 0.6F).withEyeHeight(0.4F)).put(Pose.CROUCHING, EntityDimensions.scalable(0.6F, 1.5F).withEyeHeight(1.27F).withAttachments(EntityAttachments.builder().attach(EntityAttachment.VEHICLE, Player.DEFAULT_VEHICLE_ATTACHMENT))).put(Pose.DYING, EntityDimensions.fixed(0.2F, 0.2F).withEyeHeight(1.62F)).build();
-     private static final EntityDataAccessor<Float> DATA_PLAYER_ABSORPTION_ID = SynchedEntityData.defineId(Player.class, EntityDataSerializers.FLOAT);
-     private static final EntityDataAccessor<Integer> DATA_SCORE_ID = SynchedEntityData.defineId(Player.class, EntityDataSerializers.INT);
-     public static final EntityDataAccessor<Byte> DATA_PLAYER_MODE_CUSTOMISATION = SynchedEntityData.defineId(Player.class, EntityDataSerializers.BYTE);
-@@ -149,7 +159,7 @@
-     public static final int CLIENT_LOADED_TIMEOUT_TIME = 60;
-     private long timeEntitySatOnShoulder;
-     final Inventory inventory = new Inventory(this);
--    protected PlayerEnderChestContainer enderChestInventory = new PlayerEnderChestContainer();
-+    protected PlayerEnderChestContainer enderChestInventory = new PlayerEnderChestContainer(this); // CraftBukkit - add "this" to constructor
-     public final InventoryMenu inventoryMenu;
-     public AbstractContainerMenu containerMenu;
-     protected FoodData foodData = new FoodData();
-@@ -181,13 +191,25 @@
-     private Optional<GlobalPos> lastDeathLocation;
-     @Nullable
-     public FishingHook fishing;
--    protected float hurtDir;
-+    public float hurtDir; // Paper - protected -> public
-     @Nullable
-     public Vec3 currentImpulseImpactPos;
-     @Nullable
-     public Entity currentExplosionCause;
-     private boolean ignoreFallDamageFromCurrentImpulse;
-     private int currentImpulseContextResetGraceTime;
-+    public boolean affectsSpawning = true; // Paper - Affects Spawning API
-+    public net.kyori.adventure.util.TriState flyingFallDamage = net.kyori.adventure.util.TriState.NOT_SET; // Paper - flying fall damage
-+
-+    // CraftBukkit start
-+    public boolean fauxSleeping;
-+    public int oldLevel = -1;
-+
-+    @Override
-+    public CraftHumanEntity getBukkitEntity() {
-+        return (CraftHumanEntity) super.getBukkitEntity();
-+    }
-+    // CraftBukkit end
- 
-     public Player(Level world, BlockPos pos, float yaw, GameProfile gameProfile) {
-         super(EntityType.PLAYER, world);
-@@ -244,6 +266,13 @@
- 
-         if (this.isSleeping()) {
-             ++this.sleepCounter;
-+            // Paper start - Add PlayerDeepSleepEvent
-+            if (this.sleepCounter == SLEEP_DURATION) {
-+                if (!new io.papermc.paper.event.player.PlayerDeepSleepEvent((org.bukkit.entity.Player) getBukkitEntity()).callEvent()) {
-+                    this.sleepCounter = Integer.MIN_VALUE;
-+                }
-+            }
-+            // Paper end - Add PlayerDeepSleepEvent
-             if (this.sleepCounter > 100) {
-                 this.sleepCounter = 100;
-             }
-@@ -261,7 +290,7 @@
-         this.updateIsUnderwater();
-         super.tick();
-         if (!this.level().isClientSide && this.containerMenu != null && !this.containerMenu.stillValid(this)) {
--            this.closeContainer();
-+            this.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.CANT_USE); // Paper - Inventory close reason
-             this.containerMenu = this.inventoryMenu;
-         }
- 
-@@ -353,7 +382,7 @@
-     }
- 
-     private void turtleHelmetTick() {
--        this.addEffect(new MobEffectInstance(MobEffects.WATER_BREATHING, 200, 0, false, false, true));
-+        this.addEffect(new MobEffectInstance(MobEffects.WATER_BREATHING, 200, 0, false, false, true), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.TURTLE_HELMET); // CraftBukkit
-     }
- 
-     private boolean isEquipped(Item item) {
-@@ -511,7 +540,19 @@
-             super.handleEntityEvent(status);
-         }
- 
-+    }
-+
-+    // Paper start - Inventory close reason; unused code, but to keep signatures aligned
-+    public void closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason reason) {
-+        closeContainer();
-+        this.containerMenu = this.inventoryMenu;
-+    }
-+    // Paper end - Inventory close reason
-+    // Paper start - special close for unloaded inventory
-+    public void closeUnloadedInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason reason) {
-+        this.containerMenu = this.inventoryMenu;
-     }
-+    // Paper end - special close for unloaded inventory
- 
-     public void closeContainer() {
-         this.containerMenu = this.inventoryMenu;
-@@ -523,8 +564,14 @@
-     public void rideTick() {
-         if (!this.level().isClientSide && this.wantsToStopRiding() && this.isPassenger()) {
-             this.stopRiding();
--            this.setShiftKeyDown(false);
--        } else {
-+            // CraftBukkit start - SPIGOT-7316: no longer passenger, dismount and return
-+            if (!this.isPassenger()) {
-+                this.setShiftKeyDown(false);
-+                return;
-+            }
-+        }
-+        {
-+            // CraftBukkit end
-             super.rideTick();
-             this.oBob = this.bob;
-             this.bob = 0.0F;
-@@ -593,6 +640,7 @@
-         this.playShoulderEntityAmbientSound(this.getShoulderEntityLeft());
-         this.playShoulderEntityAmbientSound(this.getShoulderEntityRight());
-         if (!this.level().isClientSide && (this.fallDistance > 0.5F || this.isInWater()) || this.abilities.flying || this.isSleeping() || this.isInPowderSnow) {
-+            if (!this.level().paperConfig().entities.behavior.parrotsAreUnaffectedByPlayerMovement) // Paper - Add option to make parrots stay
-             this.removeEntitiesOnShoulder();
-         }
- 
-@@ -719,7 +767,14 @@
- 
-     @Nullable
-     public ItemEntity drop(ItemStack stack, boolean throwRandomly, boolean retainOwnership) {
--        if (!stack.isEmpty() && this.level().isClientSide) {
-+        // CraftBukkit start - SPIGOT-2942: Add boolean to call event
-+        return this.drop(stack, throwRandomly, retainOwnership, true);
-+    }
-+
-+    @Nullable
-+    public ItemEntity drop(ItemStack itemstack, boolean flag, boolean flag1, boolean callEvent) {
-+        // CraftBukkit end
-+        if (!itemstack.isEmpty() && this.level().isClientSide) {
-             this.swing(InteractionHand.MAIN_HAND);
-         }
- 
-@@ -809,7 +864,7 @@
-         }
- 
-         if (nbt.contains("LastDeathLocation", 10)) {
--            DataResult dataresult = GlobalPos.CODEC.parse(NbtOps.INSTANCE, nbt.get("LastDeathLocation"));
-+            DataResult<GlobalPos> dataresult = GlobalPos.CODEC.parse(NbtOps.INSTANCE, nbt.get("LastDeathLocation")); // CraftBukkit - decompile error
-             Logger logger = Player.LOGGER;
- 
-             Objects.requireNonNull(logger);
-@@ -817,7 +872,7 @@
-         }
- 
-         if (nbt.contains("current_explosion_impact_pos", 9)) {
--            DataResult dataresult1 = Vec3.CODEC.parse(NbtOps.INSTANCE, nbt.get("current_explosion_impact_pos"));
-+            DataResult<Vec3> dataresult1 = Vec3.CODEC.parse(NbtOps.INSTANCE, nbt.get("current_explosion_impact_pos")); // CraftBukkit - decompile error
-             Logger logger1 = Player.LOGGER;
- 
-             Objects.requireNonNull(logger1);
-@@ -854,7 +909,7 @@
-         }
- 
-         this.getLastDeathLocation().flatMap((globalpos) -> {
--            DataResult dataresult = GlobalPos.CODEC.encodeStart(NbtOps.INSTANCE, globalpos);
-+            DataResult<Tag> dataresult = GlobalPos.CODEC.encodeStart(NbtOps.INSTANCE, globalpos); // CraftBukkit - decompile error
-             Logger logger = Player.LOGGER;
- 
-             Objects.requireNonNull(logger);
-@@ -886,10 +941,10 @@
-             if (this.isDeadOrDying()) {
-                 return false;
-             } else {
--                this.removeEntitiesOnShoulder();
-+                // this.removeEntitiesOnShoulder(); // CraftBukkit - moved down
-                 if (source.scalesWithDifficulty()) {
-                     if (world.getDifficulty() == Difficulty.PEACEFUL) {
--                        amount = 0.0F;
-+                        return false; // CraftBukkit - f = 0.0f -> return false
-                     }
- 
-                     if (world.getDifficulty() == Difficulty.EASY) {
-@@ -901,7 +956,13 @@
-                     }
-                 }
- 
--                return amount == 0.0F ? false : super.hurtServer(world, source, amount);
-+                // CraftBukkit start - Don't filter out 0 damage
-+                boolean damaged = super.hurtServer(world, source, amount);
-+                if (damaged) {
-+                    this.removeEntitiesOnShoulder();
-+                }
-+                return damaged;
-+                // CraftBukkit end
-             }
-         }
-     }
-@@ -912,7 +973,7 @@
-         ItemStack itemstack = this.getItemBlockingWith();
- 
-         if (attacker.canDisableShield() && itemstack != null) {
--            this.disableShield(itemstack);
-+            this.disableShield(itemstack, attacker); // Paper - Add PlayerShieldDisableEvent
-         }
- 
-     }
-@@ -923,10 +984,29 @@
-     }
- 
-     public boolean canHarmPlayer(Player player) {
--        PlayerTeam scoreboardteam = this.getTeam();
--        PlayerTeam scoreboardteam1 = player.getTeam();
-+        // CraftBukkit start - Change to check OTHER player's scoreboard team according to API
-+        // To summarize this method's logic, it's "Can parameter hurt this"
-+        org.bukkit.scoreboard.Team team;
-+        if (player instanceof ServerPlayer) {
-+            ServerPlayer thatPlayer = (ServerPlayer) player;
-+            team = thatPlayer.getBukkitEntity().getScoreboard().getPlayerTeam(thatPlayer.getBukkitEntity());
-+            if (team == null || team.allowFriendlyFire()) {
-+                return true;
-+            }
-+        } else {
-+            // This should never be called, but is implemented anyway
-+            org.bukkit.OfflinePlayer thisPlayer = player.level().getCraftServer().getOfflinePlayer(player.getScoreboardName());
-+            team = player.level().getCraftServer().getScoreboardManager().getMainScoreboard().getPlayerTeam(thisPlayer);
-+            if (team == null || team.allowFriendlyFire()) {
-+                return true;
-+            }
-+        }
- 
--        return scoreboardteam == null ? true : (!scoreboardteam.isAlliedTo(scoreboardteam1) ? true : scoreboardteam.isAllowFriendlyFire());
-+        if (this instanceof ServerPlayer) {
-+            return !team.hasPlayer(((ServerPlayer) this).getBukkitEntity());
-+        }
-+        return !team.hasPlayer(this.level().getCraftServer().getOfflinePlayer(this.getScoreboardName()));
-+        // CraftBukkit end
-     }
- 
-     @Override
-@@ -966,32 +1046,38 @@
-         }
-     }
- 
-+    // CraftBukkit start
-     @Override
--    protected void actuallyHurt(ServerLevel world, DamageSource source, float amount) {
--        if (!this.isInvulnerableTo(world, source)) {
--            amount = this.getDamageAfterArmorAbsorb(source, amount);
--            amount = this.getDamageAfterMagicAbsorb(source, amount);
--            float f1 = amount;
-+    protected boolean actuallyHurt(ServerLevel worldserver, DamageSource damagesource, float f, EntityDamageEvent event) { // void -> boolean
-+        if (true) {
-+            return super.actuallyHurt(worldserver, damagesource, f, event);
-+        }
-+        // CraftBukkit end
-+        if (!this.isInvulnerableTo(worldserver, damagesource)) {
-+            f = this.getDamageAfterArmorAbsorb(damagesource, f);
-+            f = this.getDamageAfterMagicAbsorb(damagesource, f);
-+            float f1 = f;
- 
--            amount = Math.max(amount - this.getAbsorptionAmount(), 0.0F);
--            this.setAbsorptionAmount(this.getAbsorptionAmount() - (f1 - amount));
--            float f2 = f1 - amount;
-+            f = Math.max(f - this.getAbsorptionAmount(), 0.0F);
-+            this.setAbsorptionAmount(this.getAbsorptionAmount() - (f1 - f));
-+            float f2 = f1 - f;
- 
-             if (f2 > 0.0F && f2 < 3.4028235E37F) {
-                 this.awardStat(Stats.DAMAGE_ABSORBED, Math.round(f2 * 10.0F));
-             }
- 
--            if (amount != 0.0F) {
--                this.causeFoodExhaustion(source.getFoodExhaustion());
--                this.getCombatTracker().recordDamage(source, amount);
--                this.setHealth(this.getHealth() - amount);
--                if (amount < 3.4028235E37F) {
--                    this.awardStat(Stats.DAMAGE_TAKEN, Math.round(amount * 10.0F));
-+            if (f != 0.0F) {
-+                this.causeFoodExhaustion(damagesource.getFoodExhaustion(), EntityExhaustionEvent.ExhaustionReason.DAMAGED); // CraftBukkit - EntityExhaustionEvent
-+                this.getCombatTracker().recordDamage(damagesource, f);
-+                this.setHealth(this.getHealth() - f);
-+                if (f < 3.4028235E37F) {
-+                    this.awardStat(Stats.DAMAGE_TAKEN, Math.round(f * 10.0F));
-                 }
- 
-                 this.gameEvent(GameEvent.ENTITY_DAMAGE);
-             }
-         }
-+        return false; // CraftBukkit
-     }
- 
-     public boolean isTextFilteringEnabled() {
-@@ -1061,13 +1147,19 @@
- 
-     @Override
-     public void removeVehicle() {
--        super.removeVehicle();
-+        // Paper start - Force entity dismount during teleportation
-+        this.removeVehicle(false);
-+    }
-+    @Override
-+    public void removeVehicle(boolean suppressCancellation) {
-+        super.removeVehicle(suppressCancellation);
-+        // Paper end - Force entity dismount during teleportation
-         this.boardingCooldown = 0;
-     }
- 
-     @Override
-     protected boolean isImmobile() {
--        return super.isImmobile() || this.isSleeping();
-+        return super.isImmobile() || this.isSleeping() || this.isRemoved() || !valid; // Paper - player's who are dead or not in a world shouldn't move...
-     }
- 
-     @Override
-@@ -1134,8 +1226,17 @@
-     }
- 
-     public void attack(Entity target) {
--        if (target.isAttackable()) {
--            if (!target.skipAttackInteraction(this)) {
-+        // Paper start - PlayerAttackEntityEvent
-+        boolean willAttack = target.isAttackable() && !target.skipAttackInteraction(this); // Vanilla logic
-+        io.papermc.paper.event.player.PrePlayerAttackEntityEvent playerAttackEntityEvent = new io.papermc.paper.event.player.PrePlayerAttackEntityEvent(
-+            (org.bukkit.entity.Player) this.getBukkitEntity(),
-+            target.getBukkitEntity(),
-+            willAttack
-+        );
-+
-+        if (playerAttackEntityEvent.callEvent() && willAttack) { // Logic moved to willAttack local variable.
-+            {
-+        // Paper end - PlayerAttackEntityEvent
-                 float f = this.isAutoSpinAttack() ? this.autoSpinAttackDmg : (float) this.getAttributeValue(Attributes.ATTACK_DAMAGE);
-                 ItemStack itemstack = this.getWeaponItem();
-                 DamageSource damagesource = (DamageSource) Optional.ofNullable(itemstack.getItem().getDamageSource(this)).orElse(this.damageSources().playerAttack(this));
-@@ -1144,10 +1245,15 @@
- 
-                 f *= 0.2F + f2 * f2 * 0.8F;
-                 f1 *= f2;
--                this.resetAttackStrengthTicker();
-+                // this.resetAttackStrengthTicker(); // CraftBukkit - Moved to EntityLiving to reset the cooldown after the damage is dealt
-                 if (target.getType().is(EntityTypeTags.REDIRECTABLE_PROJECTILE) && target instanceof Projectile) {
-                     Projectile iprojectile = (Projectile) target;
- 
-+                    // CraftBukkit start
-+                    if (CraftEventFactory.handleNonLivingEntityDamageEvent(target, damagesource, f1, false)) {
-+                        return;
-+                    }
-+                    // CraftBukkit end
-                     if (iprojectile.deflect(ProjectileDeflection.AIM_DEFLECT, this, this, true)) {
-                         this.level().playSound((Player) null, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_ATTACK_NODAMAGE, this.getSoundSource());
-                         return;
-@@ -1159,7 +1265,7 @@
-                     boolean flag1;
- 
-                     if (this.isSprinting() && flag) {
--                        this.level().playSound((Player) null, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_ATTACK_KNOCKBACK, this.getSoundSource(), 1.0F, 1.0F);
-+                        sendSoundEffect(this, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_ATTACK_KNOCKBACK, this.getSoundSource(), 1.0F, 1.0F); // Paper - send while respecting visibility
-                         flag1 = true;
-                     } else {
-                         flag1 = false;
-@@ -1168,7 +1274,9 @@
-                     f += itemstack.getItem().getAttackDamageBonus(target, f, damagesource);
-                     boolean flag2 = flag && this.fallDistance > 0.0F && !this.onGround() && !this.onClimbable() && !this.isInWater() && !this.hasEffect(MobEffects.BLINDNESS) && !this.isPassenger() && target instanceof LivingEntity && !this.isSprinting();
- 
-+                    flag2 = flag2 && !this.level().paperConfig().entities.behavior.disablePlayerCrits; // Paper - Toggleable player crits
-                     if (flag2) {
-+                        damagesource = damagesource.critical(true); // Paper start - critical damage API
-                         f *= 1.5F;
-                     }
- 
-@@ -1202,13 +1310,17 @@
-                             if (target instanceof LivingEntity) {
-                                 LivingEntity entityliving1 = (LivingEntity) target;
- 
--                                entityliving1.knockback((double) (f5 * 0.5F), (double) Mth.sin(this.getYRot() * 0.017453292F), (double) (-Mth.cos(this.getYRot() * 0.017453292F)));
-+                                entityliving1.knockback((double) (f5 * 0.5F), (double) Mth.sin(this.getYRot() * 0.017453292F), (double) (-Mth.cos(this.getYRot() * 0.017453292F)), this, io.papermc.paper.event.entity.EntityKnockbackEvent.Cause.ENTITY_ATTACK); // Paper - knockback events
-                             } else {
--                                target.push((double) (-Mth.sin(this.getYRot() * 0.017453292F) * f5 * 0.5F), 0.1D, (double) (Mth.cos(this.getYRot() * 0.017453292F) * f5 * 0.5F));
-+                                target.push((double) (-Mth.sin(this.getYRot() * 0.017453292F) * f5 * 0.5F), 0.1D, (double) (Mth.cos(this.getYRot() * 0.017453292F) * f5 * 0.5F), this); // Paper - Add EntityKnockbackByEntityEvent and EntityPushedByEntityAttackEvent
-                             }
- 
-                             this.setDeltaMovement(this.getDeltaMovement().multiply(0.6D, 1.0D, 0.6D));
-+                            // Paper start - Configurable sprint interruption on attack
-+                            if (!this.level().paperConfig().misc.disableSprintInterruptionOnAttack) {
-                             this.setSprinting(false);
-+                            }
-+                            // Paper end - Configurable sprint interruption on attack
-                         }
- 
-                         LivingEntity entityliving2;
-@@ -1223,8 +1335,13 @@
-                                 if (entityliving2 != this && entityliving2 != target && !this.isAlliedTo((Entity) entityliving2) && (!(entityliving2 instanceof ArmorStand) || !((ArmorStand) entityliving2).isMarker()) && this.distanceToSqr((Entity) entityliving2) < 9.0D) {
-                                     float f7 = this.getEnchantedDamage(entityliving2, f6, damagesource) * f2;
- 
--                                    entityliving2.knockback(0.4000000059604645D, (double) Mth.sin(this.getYRot() * 0.017453292F), (double) (-Mth.cos(this.getYRot() * 0.017453292F)));
--                                    entityliving2.hurt(damagesource, f7);
-+                                    // CraftBukkit start - Only apply knockback if the damage hits
-+                                    if (!entityliving2.hurtServer((ServerLevel) this.level(), this.damageSources().playerAttack(this).sweep().critical(flag2), f7)) { // Paper - add critical damage API
-+                                        continue;
-+                                    }
-+                                    // CraftBukkit end
-+                                    entityliving2.knockback(0.4000000059604645D, (double) Mth.sin(this.getYRot() * 0.017453292F), (double) (-Mth.cos(this.getYRot() * 0.017453292F)), this, io.papermc.paper.event.entity.EntityKnockbackEvent.Cause.SWEEP_ATTACK); // CraftBukkit // Paper - knockback events
-+                                    // entityliving2.hurt(damagesource, f7); // CraftBukkit - moved up
-                                     Level world = this.level();
- 
-                                     if (world instanceof ServerLevel) {
-@@ -1235,26 +1352,43 @@
-                                 }
-                             }
- 
--                            this.level().playSound((Player) null, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_ATTACK_SWEEP, this.getSoundSource(), 1.0F, 1.0F);
-+                            sendSoundEffect(this, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_ATTACK_SWEEP, this.getSoundSource(), 1.0F, 1.0F); // Paper - send while respecting visibility
-                             this.sweepAttack();
-                         }
- 
-                         if (target instanceof ServerPlayer && target.hurtMarked) {
-+                            // CraftBukkit start - Add Velocity Event
-+                            boolean cancelled = false;
-+                            org.bukkit.entity.Player player = (org.bukkit.entity.Player) target.getBukkitEntity();
-+                            org.bukkit.util.Vector velocity = CraftVector.toBukkit(vec3d);
-+
-+                            PlayerVelocityEvent event = new PlayerVelocityEvent(player, velocity.clone());
-+                            this.level().getCraftServer().getPluginManager().callEvent(event);
-+
-+                            if (event.isCancelled()) {
-+                                cancelled = true;
-+                            } else if (!velocity.equals(event.getVelocity())) {
-+                                player.setVelocity(event.getVelocity());
-+                            }
-+
-+                            if (!cancelled) {
-                             ((ServerPlayer) target).connection.send(new ClientboundSetEntityMotionPacket(target));
-                             target.hurtMarked = false;
-                             target.setDeltaMovement(vec3d);
-+                            }
-+                            // CraftBukkit end
-                         }
- 
-                         if (flag2) {
--                            this.level().playSound((Player) null, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_ATTACK_CRIT, this.getSoundSource(), 1.0F, 1.0F);
-+                            sendSoundEffect(this, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_ATTACK_CRIT, this.getSoundSource(), 1.0F, 1.0F); // Paper - send while respecting visibility
-                             this.crit(target);
-                         }
- 
-                         if (!flag2 && !flag3) {
-                             if (flag) {
--                                this.level().playSound((Player) null, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_ATTACK_STRONG, this.getSoundSource(), 1.0F, 1.0F);
-+                                sendSoundEffect(this, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_ATTACK_STRONG, this.getSoundSource(), 1.0F, 1.0F); // Paper - send while respecting visibility
-                             } else {
--                                this.level().playSound((Player) null, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_ATTACK_WEAK, this.getSoundSource(), 1.0F, 1.0F);
-+                                sendSoundEffect(this, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_ATTACK_WEAK, this.getSoundSource(), 1.0F, 1.0F); // Paper - send while respecting visibility
-                             }
-                         }
- 
-@@ -1308,9 +1442,14 @@
-                             }
-                         }
- 
--                        this.causeFoodExhaustion(0.1F);
-+                        this.causeFoodExhaustion(this.level().spigotConfig.combatExhaustion, EntityExhaustionEvent.ExhaustionReason.ATTACK); // CraftBukkit - EntityExhaustionEvent // Spigot - Change to use configurable value
-                     } else {
--                        this.level().playSound((Player) null, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_ATTACK_NODAMAGE, this.getSoundSource(), 1.0F, 1.0F);
-+                        sendSoundEffect(this, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_ATTACK_NODAMAGE, this.getSoundSource(), 1.0F, 1.0F); // Paper - send while respecting visibility
-+                        // CraftBukkit start - resync on cancelled event
-+                        if (this instanceof ServerPlayer) {
-+                            ((ServerPlayer) this).getBukkitEntity().updateInventory();
-+                        }
-+                        // CraftBukkit end
-                     }
-                 }
- 
-@@ -1327,8 +1466,21 @@
-         this.attack(target);
-     }
- 
-+    @io.papermc.paper.annotation.DoNotUse @Deprecated // Paper - Add PlayerShieldDisableEvent
-     public void disableShield(ItemStack shield) {
--        this.getCooldowns().addCooldown(shield, 100);
-+        // Paper start - Add PlayerShieldDisableEvent
-+        this.disableShield(shield, null);
-+    }
-+    public void disableShield(ItemStack shield, @Nullable LivingEntity attacker) {
-+        final org.bukkit.entity.Entity finalAttacker = attacker != null ? attacker.getBukkitEntity() : null;
-+        if (finalAttacker != null) {
-+            final io.papermc.paper.event.player.PlayerShieldDisableEvent shieldDisableEvent = new io.papermc.paper.event.player.PlayerShieldDisableEvent((org.bukkit.entity.Player) getBukkitEntity(), finalAttacker, 100);
-+            if (!shieldDisableEvent.callEvent()) return;
-+            this.getCooldowns().addCooldown(shield, shieldDisableEvent.getCooldown());
-+        } else {
-+            this.getCooldowns().addCooldown(shield, 100);
-+        }
-+        // Paper end - Add PlayerShieldDisableEvent
-         this.stopUsingItem();
-         this.level().broadcastEntityEvent(this, (byte) 30);
-     }
-@@ -1351,7 +1503,14 @@
- 
-     @Override
-     public void remove(Entity.RemovalReason reason) {
--        super.remove(reason);
-+        // CraftBukkit start - add Bukkit remove cause
-+        this.remove(reason, null);
-+    }
-+
-+    @Override
-+    public void remove(Entity.RemovalReason entity_removalreason, EntityRemoveEvent.Cause cause) {
-+        super.remove(entity_removalreason, cause);
-+        // CraftBukkit end
-         this.inventoryMenu.removed(this);
-         if (this.containerMenu != null && this.hasContainerOpen()) {
-             this.doCloseContainer();
-@@ -1391,7 +1550,13 @@
-     }
- 
-     public Either<Player.BedSleepingProblem, Unit> startSleepInBed(BlockPos pos) {
--        this.startSleeping(pos);
-+        // CraftBukkit start
-+        return this.startSleepInBed(pos, false);
-+    }
-+
-+    public Either<Player.BedSleepingProblem, Unit> startSleepInBed(BlockPos blockposition, boolean force) {
-+        // CraftBukkit end
-+        this.startSleeping(blockposition);
-         this.sleepCounter = 0;
-         return Either.right(Unit.INSTANCE);
-     }
-@@ -1503,7 +1668,7 @@
- 
-     @Override
-     public boolean causeFallDamage(float fallDistance, float damageMultiplier, DamageSource damageSource) {
--        if (this.abilities.mayfly) {
-+        if (this.abilities.mayfly && !this.flyingFallDamage.toBooleanOrElse(false)) { // Paper - flying fall damage
-             return false;
-         } else {
-             if (fallDistance >= 2.0F) {
-@@ -1545,12 +1710,24 @@
-     }
- 
-     public void startFallFlying() {
--        this.setSharedFlag(7, true);
-+        // CraftBukkit start
-+        if (!org.bukkit.craftbukkit.event.CraftEventFactory.callToggleGlideEvent(this, true).isCancelled()) {
-+            this.setSharedFlag(7, true);
-+        } else {
-+            // SPIGOT-5542: must toggle like below
-+            this.setSharedFlag(7, true);
-+            this.setSharedFlag(7, false);
-+        }
-+        // CraftBukkit end
-     }
- 
-     public void stopFallFlying() {
-+        // CraftBukkit start
-+        if (!org.bukkit.craftbukkit.event.CraftEventFactory.callToggleGlideEvent(this, false).isCancelled()) {
-         this.setSharedFlag(7, true);
-         this.setSharedFlag(7, false);
-+        }
-+        // CraftBukkit end
-     }
- 
-     @Override
-@@ -1662,13 +1839,32 @@
-     }
- 
-     public int getXpNeededForNextLevel() {
--        return this.experienceLevel >= 30 ? 112 + (this.experienceLevel - 30) * 9 : (this.experienceLevel >= 15 ? 37 + (this.experienceLevel - 15) * 5 : 7 + this.experienceLevel * 2);
-+        return this.experienceLevel >= 30 ? 112 + (this.experienceLevel - 30) * 9 : (this.experienceLevel >= 15 ? 37 + (this.experienceLevel - 15) * 5 : 7 + this.experienceLevel * 2); // Paper - diff on change; calculateTotalExperiencePoints
-+    }
-+    // Paper start - send while respecting visibility
-+    private static void sendSoundEffect(Player fromEntity, double x, double y, double z, SoundEvent soundEffect, SoundSource soundCategory, float volume, float pitch) {
-+        fromEntity.level().playSound(fromEntity, x, y, z, soundEffect, soundCategory, volume, pitch); // This will not send the effect to the entity itself
-+        if (fromEntity instanceof ServerPlayer serverPlayer) {
-+            serverPlayer.connection.send(new net.minecraft.network.protocol.game.ClientboundSoundPacket(net.minecraft.core.registries.BuiltInRegistries.SOUND_EVENT.wrapAsHolder(soundEffect), soundCategory, x, y, z, volume, pitch, fromEntity.random.nextLong()));
-+        }
-     }
-+    // Paper end - send while respecting visibility
- 
-+    // CraftBukkit start
-     public void causeFoodExhaustion(float exhaustion) {
-+        this.causeFoodExhaustion(exhaustion, EntityExhaustionEvent.ExhaustionReason.UNKNOWN);
-+    }
-+
-+    public void causeFoodExhaustion(float f, EntityExhaustionEvent.ExhaustionReason reason) {
-+        // CraftBukkit end
-         if (!this.abilities.invulnerable) {
-             if (!this.level().isClientSide) {
--                this.foodData.addExhaustion(exhaustion);
-+                // CraftBukkit start
-+                EntityExhaustionEvent event = CraftEventFactory.callPlayerExhaustionEvent(this, reason, f);
-+                if (!event.isCancelled()) {
-+                    this.foodData.addExhaustion(event.getExhaustion());
-+                }
-+                // CraftBukkit end
-             }
- 
-         }
-@@ -1748,13 +1944,20 @@
- 
-     @Override
-     public void setItemSlot(EquipmentSlot slot, ItemStack stack) {
--        this.verifyEquippedItem(stack);
--        if (slot == EquipmentSlot.MAINHAND) {
--            this.onEquipItem(slot, (ItemStack) this.inventory.items.set(this.inventory.selected, stack), stack);
--        } else if (slot == EquipmentSlot.OFFHAND) {
--            this.onEquipItem(slot, (ItemStack) this.inventory.offhand.set(0, stack), stack);
--        } else if (slot.getType() == EquipmentSlot.Type.HUMANOID_ARMOR) {
--            this.onEquipItem(slot, (ItemStack) this.inventory.armor.set(slot.getIndex(), stack), stack);
-+        // CraftBukkit start
-+        this.setItemSlot(slot, stack, false);
-+    }
-+
-+    @Override
-+    public void setItemSlot(EquipmentSlot enumitemslot, ItemStack itemstack, boolean silent) {
-+        // CraftBukkit end
-+        this.verifyEquippedItem(itemstack);
-+        if (enumitemslot == EquipmentSlot.MAINHAND) {
-+            this.onEquipItem(enumitemslot, (ItemStack) this.inventory.items.set(this.inventory.selected, itemstack), itemstack, silent); // CraftBukkit
-+        } else if (enumitemslot == EquipmentSlot.OFFHAND) {
-+            this.onEquipItem(enumitemslot, (ItemStack) this.inventory.offhand.set(0, itemstack), itemstack, silent); // CraftBukkit
-+        } else if (enumitemslot.getType() == EquipmentSlot.Type.HUMANOID_ARMOR) {
-+            this.onEquipItem(enumitemslot, (ItemStack) this.inventory.armor.set(enumitemslot.getIndex(), itemstack), itemstack, silent); // CraftBukkit
-         }
- 
-     }
-@@ -1798,26 +2001,55 @@
- 
-     public void removeEntitiesOnShoulder() {
-         if (this.timeEntitySatOnShoulder + 20L < this.level().getGameTime()) {
--            this.respawnEntityOnShoulder(this.getShoulderEntityLeft());
-+            // CraftBukkit start
-+            if (this.respawnEntityOnShoulder(this.getShoulderEntityLeft())) {
-+                this.setShoulderEntityLeft(new CompoundTag());
-+            }
-+            if (this.respawnEntityOnShoulder(this.getShoulderEntityRight())) {
-+                this.setShoulderEntityRight(new CompoundTag());
-+            }
-+            // CraftBukkit end
-+        }
-+
-+    }
-+
-+    // Paper start - release entity api
-+    public Entity releaseLeftShoulderEntity() {
-+        Entity entity = this.respawnEntityOnShoulder0(this.getShoulderEntityLeft());
-+        if (entity != null) {
-             this.setShoulderEntityLeft(new CompoundTag());
--            this.respawnEntityOnShoulder(this.getShoulderEntityRight());
-+        }
-+        return entity;
-+    }
-+
-+    public Entity releaseRightShoulderEntity() {
-+        Entity entity = this.respawnEntityOnShoulder0(this.getShoulderEntityRight());
-+        if (entity != null) {
-             this.setShoulderEntityRight(new CompoundTag());
-         }
-+        return entity;
-+    }
-+    // Paper end - release entity api
- 
-+    private boolean respawnEntityOnShoulder(CompoundTag nbttagcompound) { // CraftBukkit void->boolean
-+    // Paper start - release entity api - return entity - overload
-+        return this.respawnEntityOnShoulder0(nbttagcompound) != null;
-     }
- 
--    private void respawnEntityOnShoulder(CompoundTag entityNbt) {
--        if (!this.level().isClientSide && !entityNbt.isEmpty()) {
--            EntityType.create(entityNbt, this.level(), EntitySpawnReason.LOAD).ifPresent((entity) -> {
-+    private Entity respawnEntityOnShoulder0(CompoundTag nbttagcompound) { // CraftBukkit void->boolean
-+    // Paper end - release entity api - return entity - overload
-+        if (!this.level().isClientSide && !nbttagcompound.isEmpty()) {
-+            return EntityType.create(nbttagcompound, this.level(), EntitySpawnReason.LOAD).map((entity) -> { // CraftBukkit
-                 if (entity instanceof TamableAnimal) {
-                     ((TamableAnimal) entity).setOwnerUUID(this.uuid);
-                 }
- 
-                 entity.setPos(this.getX(), this.getY() + 0.699999988079071D, this.getZ());
--                ((ServerLevel) this.level()).addWithUUID(entity);
--            });
-+                return ((ServerLevel) this.level()).addWithUUID(entity, CreatureSpawnEvent.SpawnReason.SHOULDER_ENTITY) ? entity : null; // CraftBukkit // Paper start - release entity api - return entity
-+            }).orElse(null); // CraftBukkit // Paper end - release entity api - return entity
-         }
- 
-+        return null; // Paper - return null
-     }
- 
-     @Override
-@@ -2003,20 +2235,31 @@
-     @Override
-     public ImmutableList<Pose> getDismountPoses() {
-         return ImmutableList.of(Pose.STANDING, Pose.CROUCHING, Pose.SWIMMING);
-+    }
-+
-+    // Paper start - PlayerReadyArrowEvent
-+    protected boolean tryReadyArrow(ItemStack bow, ItemStack itemstack) {
-+        return !(this instanceof ServerPlayer) ||
-+                new com.destroystokyo.paper.event.player.PlayerReadyArrowEvent(
-+                    ((ServerPlayer) this).getBukkitEntity(),
-+                    org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(bow),
-+                    org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack)
-+                ).callEvent();
-     }
-+    // Paper end - PlayerReadyArrowEvent
- 
-     @Override
-     public ItemStack getProjectile(ItemStack stack) {
-         if (!(stack.getItem() instanceof ProjectileWeaponItem)) {
-             return ItemStack.EMPTY;
-         } else {
--            Predicate<ItemStack> predicate = ((ProjectileWeaponItem) stack.getItem()).getSupportedHeldProjectiles();
-+            Predicate<ItemStack> predicate = ((ProjectileWeaponItem) stack.getItem()).getSupportedHeldProjectiles().and(item -> tryReadyArrow(stack, item)); // Paper - PlayerReadyArrowEvent
-             ItemStack itemstack1 = ProjectileWeaponItem.getHeldProjectile(this, predicate);
- 
-             if (!itemstack1.isEmpty()) {
-                 return itemstack1;
-             } else {
--                predicate = ((ProjectileWeaponItem) stack.getItem()).getAllSupportedProjectiles();
-+                predicate = ((ProjectileWeaponItem) stack.getItem()).getAllSupportedProjectiles().and(item -> tryReadyArrow(stack, item)); // Paper - PlayerReadyArrowEvent
- 
-                 for (int i = 0; i < this.inventory.getContainerSize(); ++i) {
-                     ItemStack itemstack2 = this.inventory.getItem(i);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/AbstractArrow.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/AbstractArrow.java.patch
deleted file mode 100644
index d2cc2fa1a9..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/AbstractArrow.java.patch
+++ /dev/null
@@ -1,320 +0,0 @@
---- a/net/minecraft/world/entity/projectile/AbstractArrow.java
-+++ b/net/minecraft/world/entity/projectile/AbstractArrow.java
-@@ -36,6 +36,7 @@
- import net.minecraft.world.entity.OminousItemSpawner;
- import net.minecraft.world.entity.SlotAccess;
- import net.minecraft.world.entity.ai.attributes.Attributes;
-+import net.minecraft.world.entity.item.ItemEntity;
- import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.item.Item;
- import net.minecraft.world.item.ItemStack;
-@@ -50,6 +51,10 @@
- import net.minecraft.world.phys.HitResult;
- import net.minecraft.world.phys.Vec3;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+import org.bukkit.event.entity.EntityCombustByEntityEvent;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.event.player.PlayerPickupArrowEvent;
-+// CraftBukkit end
- 
- public abstract class AbstractArrow extends Projectile {
- 
-@@ -78,6 +83,18 @@
-     @Nullable
-     public ItemStack firedFromWeapon;
- 
-+    // Spigot Start
-+    @Override
-+    public void inactiveTick()
-+    {
-+        if ( this.isInGround() )
-+        {
-+            this.life += 1;
-+        }
-+        super.inactiveTick();
-+    }
-+    // Spigot End
-+
-     protected AbstractArrow(EntityType<? extends AbstractArrow> type, Level world) {
-         super(type, world);
-         this.pickup = AbstractArrow.Pickup.DISALLOWED;
-@@ -88,23 +105,30 @@
-     }
- 
-     protected AbstractArrow(EntityType<? extends AbstractArrow> type, double x, double y, double z, Level world, ItemStack stack, @Nullable ItemStack weapon) {
--        this(type, world);
--        this.pickupItemStack = stack.copy();
--        this.setCustomName((Component) stack.get(DataComponents.CUSTOM_NAME));
--        Unit unit = (Unit) stack.remove(DataComponents.INTANGIBLE_PROJECTILE);
-+        // CraftBukkit start - handle the owner before the rest of things
-+        this(type, x, y, z, world, stack, weapon, null);
-+    }
- 
-+    protected AbstractArrow(EntityType<? extends AbstractArrow> entitytypes, double d0, double d1, double d2, Level world, ItemStack itemstack, @Nullable ItemStack itemstack1, @Nullable LivingEntity ownerEntity) {
-+        this(entitytypes, world);
-+        this.setOwner(ownerEntity);
-+        // CraftBukkit end
-+        this.pickupItemStack = itemstack.copy();
-+        this.setCustomName((Component) itemstack.get(DataComponents.CUSTOM_NAME));
-+        Unit unit = (Unit) itemstack.remove(DataComponents.INTANGIBLE_PROJECTILE);
-+
-         if (unit != null) {
-             this.pickup = AbstractArrow.Pickup.CREATIVE_ONLY;
-         }
- 
--        this.setPos(x, y, z);
--        if (weapon != null && world instanceof ServerLevel worldserver) {
--            if (weapon.isEmpty()) {
-+        this.setPos(d0, d1, d2);
-+        if (itemstack1 != null && world instanceof ServerLevel worldserver) {
-+            if (itemstack1.isEmpty()) {
-                 throw new IllegalArgumentException("Invalid weapon firing an arrow");
-             }
- 
--            this.firedFromWeapon = weapon.copy();
--            int i = EnchantmentHelper.getPiercingCount(worldserver, weapon, this.pickupItemStack);
-+            this.firedFromWeapon = itemstack1.copy();
-+            int i = EnchantmentHelper.getPiercingCount(worldserver, itemstack1, this.pickupItemStack);
- 
-             if (i > 0) {
-                 this.setPierceLevel((byte) i);
-@@ -114,8 +138,8 @@
-     }
- 
-     protected AbstractArrow(EntityType<? extends AbstractArrow> type, LivingEntity owner, Level world, ItemStack stack, @Nullable ItemStack shotFrom) {
--        this(type, owner.getX(), owner.getEyeY() - 0.10000000149011612D, owner.getZ(), world, stack, shotFrom);
--        this.setOwner(owner);
-+        this(type, owner.getX(), owner.getEyeY() - 0.10000000149011612D, owner.getZ(), world, stack, shotFrom, owner); // CraftBukkit
-+        // this.setOwner(entityliving); // SPIGOT-7744 - Moved to the above constructor
-     }
- 
-     public void setSoundEvent(SoundEvent sound) {
-@@ -220,6 +244,7 @@
-             }
- 
-         } else {
-+            if (tickCount > 200) this.tickDespawn(); // Paper - tick despawnCounter regardless after 10 seconds
-             this.inGroundTime = 0;
-             Vec3 vec3d2 = this.position();
- 
-@@ -282,7 +307,7 @@
- 
-                 if (movingobjectpositionentity == null) {
-                     if (this.isAlive() && blockHitResult.getType() != HitResult.Type.MISS) {
--                        this.hitTargetOrDeflectSelf(blockHitResult);
-+                        this.preHitTargetOrDeflectSelf(blockHitResult); // CraftBukkit - projectile hit event
-                         this.hasImpulse = true;
-                     }
-                 } else {
-@@ -290,7 +315,7 @@
-                         continue;
-                     }
- 
--                    ProjectileDeflection projectiledeflection = this.hitTargetOrDeflectSelf(movingobjectpositionentity);
-+                    ProjectileDeflection projectiledeflection = this.preHitTargetOrDeflectSelf(movingobjectpositionentity); // CraftBukkit - projectile hit event
- 
-                     this.hasImpulse = true;
-                     if (this.getPierceLevel() > 0 && projectiledeflection == ProjectileDeflection.NONE) {
-@@ -318,7 +343,20 @@
-             this.level().addParticle(ParticleTypes.BUBBLE, pos.x - vec3d1.x * 0.25D, pos.y - vec3d1.y * 0.25D, pos.z - vec3d1.z * 0.25D, vec3d1.x, vec3d1.y, vec3d1.z);
-         }
- 
-+    }
-+
-+    // Paper start - Fix cancelling ProjectileHitEvent for piercing arrows
-+    @Override
-+    public ProjectileDeflection preHitTargetOrDeflectSelf(HitResult hitResult) {
-+        if (hitResult instanceof EntityHitResult entityHitResult && this.hitCancelled && this.getPierceLevel() > 0) {
-+            if (this.piercingIgnoreEntityIds == null) {
-+                this.piercingIgnoreEntityIds = new IntOpenHashSet(5);
-+            }
-+            this.piercingIgnoreEntityIds.add(entityHitResult.getEntity().getId());
-+        }
-+        return super.preHitTargetOrDeflectSelf(hitResult);
-     }
-+    // Paper end - Fix cancelling ProjectileHitEvent for piercing arrows
- 
-     @Override
-     protected double getDefaultGravity() {
-@@ -356,8 +394,8 @@
- 
-     protected void tickDespawn() {
-         ++this.life;
--        if (this.life >= 1200) {
--            this.discard();
-+        if (this.life >= (pickup == Pickup.CREATIVE_ONLY ? this.level().paperConfig().entities.spawning.creativeArrowDespawnRate.value() : (pickup == Pickup.DISALLOWED ? this.level().paperConfig().entities.spawning.nonPlayerArrowDespawnRate.value() : ((this instanceof ThrownTrident) ? this.level().spigotConfig.tridentDespawnRate : this.level().spigotConfig.arrowDespawnRate)))) { // Spigot // Paper - Configurable non-player arrow despawn rate; TODO: Extract this to init?
-+            this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-         }
- 
-     }
-@@ -386,9 +424,9 @@
-     }
- 
-     @Override
--    public void push(double deltaX, double deltaY, double deltaZ) {
-+    public void push(double deltaX, double deltaY, double deltaZ, @Nullable Entity pushingEntity) { // Paper - add push source entity param
-         if (!this.isInGround()) {
--            super.push(deltaX, deltaY, deltaZ);
-+            super.push(deltaX, deltaY, deltaZ, pushingEntity); // Paper - add push source entity param
-         }
-     }
- 
-@@ -423,7 +461,7 @@
-             }
- 
-             if (this.piercingIgnoreEntityIds.size() >= this.getPierceLevel() + 1) {
--                this.discard();
-+                this.discard(EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
-                 return;
-             }
- 
-@@ -440,11 +478,18 @@
-             entityliving.setLastHurtMob(entity);
-         }
- 
-+        if (this.isCritArrow()) damagesource = damagesource.critical(); // Paper - add critical damage API
-         boolean flag = entity.getType() == EntityType.ENDERMAN;
-         int k = entity.getRemainingFireTicks();
- 
-         if (this.isOnFire() && !flag) {
--            entity.igniteForSeconds(5.0F);
-+            // CraftBukkit start
-+            EntityCombustByEntityEvent combustEvent = new EntityCombustByEntityEvent(this.getBukkitEntity(), entity.getBukkitEntity(), 5.0F);
-+            org.bukkit.Bukkit.getPluginManager().callEvent(combustEvent);
-+            if (!combustEvent.isCancelled()) {
-+                entity.igniteForSeconds(combustEvent.getDuration(), false);
-+            }
-+            // CraftBukkit end
-         }
- 
-         if (entity.hurtOrSimulate(damagesource, (float) i)) {
-@@ -490,7 +535,7 @@
- 
-             this.playSound(this.soundEvent, 1.0F, 1.2F / (this.random.nextFloat() * 0.2F + 0.9F));
-             if (this.getPierceLevel() <= 0) {
--                this.discard();
-+                this.discard(EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
-             }
-         } else {
-             entity.setRemainingFireTicks(k);
-@@ -506,7 +551,7 @@
-                         this.spawnAtLocation(worldserver2, this.getPickupItem(), 0.1F);
-                     }
- 
--                    this.discard();
-+                    this.discard(EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
-                 }
-             }
-         }
-@@ -538,7 +583,7 @@
-             Vec3 vec3d = this.getDeltaMovement().multiply(1.0D, 0.0D, 1.0D).normalize().scale(d0 * 0.6D * d1);
- 
-             if (vec3d.lengthSqr() > 0.0D) {
--                target.push(vec3d.x, 0.1D, vec3d.z);
-+                target.push(vec3d.x, 0.1D, vec3d.z, this); // Paper - Add EntityKnockbackByEntityEvent and EntityPushedByEntityAttackEvent
-             }
-         }
- 
-@@ -665,7 +710,7 @@
-         this.setCritArrow(nbt.getBoolean("crit"));
-         this.setPierceLevel(nbt.getByte("PierceLevel"));
-         if (nbt.contains("SoundEvent", 8)) {
--            this.soundEvent = (SoundEvent) BuiltInRegistries.SOUND_EVENT.getOptional(ResourceLocation.parse(nbt.getString("SoundEvent"))).orElse(this.getDefaultHitGroundSoundEvent());
-+            this.soundEvent = (SoundEvent) BuiltInRegistries.SOUND_EVENT.getOptional(ResourceLocation.tryParse(nbt.getString("SoundEvent"))).orElse(this.getDefaultHitGroundSoundEvent()); // Paper - Validate resource location
-         }
- 
-         if (nbt.contains("item", 10)) {
-@@ -675,7 +720,7 @@
-         }
- 
-         if (nbt.contains("weapon", 10)) {
--            this.firedFromWeapon = (ItemStack) ItemStack.parse(this.registryAccess(), nbt.getCompound("weapon")).orElse((Object) null);
-+            this.firedFromWeapon = (ItemStack) ItemStack.parse(this.registryAccess(), nbt.getCompound("weapon")).orElse(null); // CraftBukkit - decompile error
-         } else {
-             this.firedFromWeapon = null;
-         }
-@@ -684,38 +729,42 @@
- 
-     @Override
-     public void setOwner(@Nullable Entity entity) {
-+        // Paper start - Fix PickupStatus getting reset
-+        this.setOwner(entity, true);
-+    }
-+
-+    public void setOwner(@Nullable Entity entity, boolean resetPickup) {
-+        // Paper end - Fix PickupStatus getting reset
-         super.setOwner(entity);
-+        if (!resetPickup) return; // Paper - Fix PickupStatus getting reset
-         Entity entity1 = entity;
-         byte b0 = 0;
- 
--        EntityArrow.PickupStatus entityarrow_pickupstatus;
-+        EntityArrow.PickupStatus entityarrow_pickupstatus = this.pickup; // CraftBukkit - decompile error
- 
-         label16:
--        while(true) {
--            //$FF: b0->value
--            //0->net/minecraft/world/entity/player/EntityHuman
--            //1->net/minecraft/world/entity/OminousItemSpawner
--            switch (entity1.typeSwitch<invokedynamic>(entity1, b0)) {
--                case -1:
--                default:
--                    entityarrow_pickupstatus = this.pickup;
--                    break label16;
--                case 0:
--                    EntityHuman entityhuman = (EntityHuman)entity1;
-+        // CraftBukkit start - decompile error
-+        while (true) {
-+            switch (entity1) {
-+                case EntityHuman entityhuman:
- 
-                     if (this.pickup != EntityArrow.PickupStatus.DISALLOWED) {
-                         b0 = 1;
--                        break;
-+                        break label16;
-                     }
- 
-                     entityarrow_pickupstatus = EntityArrow.PickupStatus.ALLOWED;
-                     break label16;
--                case 1:
--                    OminousItemSpawner ominousitemspawner = (OminousItemSpawner)entity1;
-+                case OminousItemSpawner ominousitemspawner:
- 
-                     entityarrow_pickupstatus = EntityArrow.PickupStatus.DISALLOWED;
-                     break label16;
-+                case null: // SPIGOT-7751: Fix crash caused by null owner
-+                default:
-+                    entityarrow_pickupstatus = this.pickup;
-+                    break label16;
-             }
-+            // CraftBukkit end
-         }
- 
-         this.pickup = entityarrow_pickupstatus;
-@@ -724,9 +773,24 @@
-     @Override
-     public void playerTouch(Player player) {
-         if (!this.level().isClientSide && (this.isInGround() || this.isNoPhysics()) && this.shakeTime <= 0) {
--            if (this.tryPickup(player)) {
-+            // CraftBukkit start
-+            ItemStack itemstack = this.getPickupItem();
-+            if (this.pickup == Pickup.ALLOWED && !itemstack.isEmpty() && player.getInventory().canHold(itemstack) > 0) {
-+                ItemEntity item = new ItemEntity(this.level(), this.getX(), this.getY(), this.getZ(), itemstack);
-+                PlayerPickupArrowEvent event = new PlayerPickupArrowEvent((org.bukkit.entity.Player) player.getBukkitEntity(), new org.bukkit.craftbukkit.entity.CraftItem(this.level().getCraftServer(), item), (org.bukkit.entity.AbstractArrow) this.getBukkitEntity());
-+                // event.setCancelled(!entityhuman.canPickUpLoot); TODO
-+                this.level().getCraftServer().getPluginManager().callEvent(event);
-+
-+                if (event.isCancelled()) {
-+                    return;
-+                }
-+                itemstack = item.getItem();
-+            }
-+
-+            if ((this.pickup == AbstractArrow.Pickup.ALLOWED && player.getInventory().add(itemstack)) || (this.pickup == AbstractArrow.Pickup.CREATIVE_ONLY && player.getAbilities().instabuild)) {
-+                // CraftBukkit end
-                 player.take(this, 1);
--                this.discard();
-+                this.discard(EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
-             }
- 
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java.patch
deleted file mode 100644
index b53dd9f069..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java.patch
+++ /dev/null
@@ -1,38 +0,0 @@
---- a/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java
-+++ b/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java
-@@ -14,12 +14,17 @@
- import net.minecraft.world.level.Level;
- import net.minecraft.world.phys.HitResult;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public abstract class AbstractHurtingProjectile extends Projectile {
- 
-     public static final double INITAL_ACCELERATION_POWER = 0.1D;
-     public static final double DEFLECTION_SCALE = 0.5D;
-     public double accelerationPower;
-+    public float bukkitYield = 1; // CraftBukkit
-+    public boolean isIncendiary = true; // CraftBukkit
- 
-     protected AbstractHurtingProjectile(EntityType<? extends AbstractHurtingProjectile> type, Level world) {
-         super(type, world);
-@@ -69,7 +74,7 @@
- 
-         this.applyInertia();
-         if (!this.level().isClientSide && (entity != null && entity.isRemoved() || !this.level().hasChunkAt(this.blockPosition()))) {
--            this.discard();
-+            this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-         } else {
-             HitResult movingobjectposition = ProjectileUtil.getHitResultOnMoveVector(this, this::canHitEntity, this.getClipType());
-             Vec3 vec3d;
-@@ -89,7 +94,7 @@
-             }
- 
-             if (movingobjectposition.getType() != HitResult.Type.MISS && this.isAlive()) {
--                this.hitTargetOrDeflectSelf(movingobjectposition);
-+                this.preHitTargetOrDeflectSelf(movingobjectposition); // CraftBukkit - projectile hit event
-             }
- 
-             this.createParticleTrail();
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/Arrow.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/Arrow.java.patch
deleted file mode 100644
index c19bed3b4d..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/Arrow.java.patch
+++ /dev/null
@@ -1,20 +0,0 @@
---- a/net/minecraft/world/entity/projectile/Arrow.java
-+++ b/net/minecraft/world/entity/projectile/Arrow.java
-@@ -119,7 +119,7 @@
-                 mobeffect = (MobEffectInstance) iterator.next();
-                 target.addEffect(new MobEffectInstance(mobeffect.getEffect(), Math.max(mobeffect.mapDuration((i) -> {
-                     return i / 8;
--                }), 1), mobeffect.getAmplifier(), mobeffect.isAmbient(), mobeffect.isVisible()), entity);
-+                }), 1), mobeffect.getAmplifier(), mobeffect.isAmbient(), mobeffect.isVisible()), entity, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ARROW); // CraftBukkit
-             }
-         }
- 
-@@ -127,7 +127,7 @@
- 
-         while (iterator.hasNext()) {
-             mobeffect = (MobEffectInstance) iterator.next();
--            target.addEffect(mobeffect, entity);
-+            target.addEffect(mobeffect, entity, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ARROW); // CraftBukkit
-         }
- 
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/DragonFireball.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/DragonFireball.java.patch
deleted file mode 100644
index 6278d9b3bb..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/DragonFireball.java.patch
+++ /dev/null
@@ -1,26 +0,0 @@
---- a/net/minecraft/world/entity/projectile/DragonFireball.java
-+++ b/net/minecraft/world/entity/projectile/DragonFireball.java
-@@ -14,6 +14,9 @@
- import net.minecraft.world.phys.EntityHitResult;
- import net.minecraft.world.phys.HitResult;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public class DragonFireball extends AbstractHurtingProjectile {
- 
-@@ -59,9 +62,11 @@
-                     }
-                 }
- 
-+                if (new com.destroystokyo.paper.event.entity.EnderDragonFireballHitEvent((org.bukkit.entity.DragonFireball) this.getBukkitEntity(), list.stream().map(LivingEntity::getBukkitLivingEntity).collect(java.util.stream.Collectors.toList()), (org.bukkit.entity.AreaEffectCloud) entityareaeffectcloud.getBukkitEntity()).callEvent()) { // Paper - EnderDragon Events
-                 this.level().levelEvent(2006, this.blockPosition(), this.isSilent() ? -1 : 1);
--                this.level().addFreshEntity(entityareaeffectcloud);
--                this.discard();
-+                this.level().addFreshEntity(entityareaeffectcloud, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.EXPLOSION); // Paper - use correct spawn reason
-+                } else entityareaeffectcloud.discard(null); // Paper - EnderDragon Events
-+                this.discard(EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
-             }
- 
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/EvokerFangs.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/EvokerFangs.java.patch
deleted file mode 100644
index fc93ad07be..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/EvokerFangs.java.patch
+++ /dev/null
@@ -1,30 +0,0 @@
---- a/net/minecraft/world/entity/projectile/EvokerFangs.java
-+++ b/net/minecraft/world/entity/projectile/EvokerFangs.java
-@@ -16,6 +16,9 @@
- import net.minecraft.world.entity.TraceableEntity;
- import net.minecraft.world.item.enchantment.EnchantmentHelper;
- import net.minecraft.world.level.Level;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public class EvokerFangs extends Entity implements TraceableEntity {
- 
-@@ -121,7 +124,7 @@
-             }
- 
-             if (--this.lifeTicks < 0) {
--                this.discard();
-+                this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-             }
-         }
- 
-@@ -132,7 +135,7 @@
- 
-         if (target.isAlive() && !target.isInvulnerable() && target != entityliving1) {
-             if (entityliving1 == null) {
--                target.hurt(this.damageSources().magic(), 6.0F);
-+                target.hurt(this.damageSources().magic().customEventDamager(this), 6.0F); // CraftBukkit // Paper - fix DamageSource API
-             } else {
-                 if (entityliving1.isAlliedTo((Entity) target)) {
-                     return;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/Fireball.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/Fireball.java.patch
deleted file mode 100644
index 6cc358f744..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/Fireball.java.patch
+++ /dev/null
@@ -1,16 +0,0 @@
---- a/net/minecraft/world/entity/projectile/Fireball.java
-+++ b/net/minecraft/world/entity/projectile/Fireball.java
-@@ -61,7 +61,12 @@
-     public void readAdditionalSaveData(CompoundTag nbt) {
-         super.readAdditionalSaveData(nbt);
-         if (nbt.contains("Item", 10)) {
--            this.setItem((ItemStack) ItemStack.parse(this.registryAccess(), nbt.getCompound("Item")).orElse(this.getDefaultItem()));
-+            // CraftBukkit start - SPIGOT-5474 probably came from bugged earlier versions
-+            ItemStack itemstack = (ItemStack) ItemStack.parse(this.registryAccess(), nbt.getCompound("Item")).orElse(this.getDefaultItem());
-+            if (!itemstack.isEmpty()) {
-+                this.setItem(itemstack);
-+            }
-+            // CraftBukkit end
-         } else {
-             this.setItem(this.getDefaultItem());
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/FireworkRocketEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/FireworkRocketEntity.java.patch
deleted file mode 100644
index 91715aeb75..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/FireworkRocketEntity.java.patch
+++ /dev/null
@@ -1,132 +0,0 @@
---- a/net/minecraft/world/entity/projectile/FireworkRocketEntity.java
-+++ b/net/minecraft/world/entity/projectile/FireworkRocketEntity.java
-@@ -32,6 +32,9 @@
- import net.minecraft.world.phys.EntityHitResult;
- import net.minecraft.world.phys.HitResult;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public class FireworkRocketEntity extends Projectile implements ItemSupplier {
- 
-@@ -42,6 +45,7 @@
-     public int lifetime;
-     @Nullable
-     public LivingEntity attachedToEntity;
-+    @Nullable public java.util.UUID spawningEntity; // Paper
- 
-     public FireworkRocketEntity(EntityType<? extends FireworkRocketEntity> type, Level world) {
-         super(type, world);
-@@ -84,7 +88,29 @@
-         this.setOwner(entity);
-     }
- 
-+    // Spigot Start - copied from tick
-     @Override
-+    public void inactiveTick() {
-+        this.life += 1;
-+
-+        if (this.life > this.lifetime) {
-+            Level world = this.level();
-+
-+            if (world instanceof ServerLevel) {
-+                ServerLevel worldserver = (ServerLevel) world;
-+
-+                // CraftBukkit start
-+                if (!org.bukkit.craftbukkit.event.CraftEventFactory.callFireworkExplodeEvent(this).isCancelled()) {
-+                    this.explode(worldserver);
-+                }
-+                // CraftBukkit end
-+            }
-+        }
-+        super.inactiveTick();
-+    }
-+    // Spigot End
-+
-+    @Override
-     protected void defineSynchedData(SynchedEntityData.Builder builder) {
-         builder.define(FireworkRocketEntity.DATA_ID_FIREWORKS_ITEM, FireworkRocketEntity.getDefaultItem());
-         builder.define(FireworkRocketEntity.DATA_ATTACHED_TO_TARGET, OptionalInt.empty());
-@@ -152,7 +178,7 @@
-         }
- 
-         if (!this.noPhysics && this.isAlive() && movingobjectposition.getType() != HitResult.Type.MISS) {
--            this.hitTargetOrDeflectSelf(movingobjectposition);
-+            this.preHitTargetOrDeflectSelf(movingobjectposition); // CraftBukkit - projectile hit event
-             this.hasImpulse = true;
-         }
- 
-@@ -172,7 +198,11 @@
-             if (world instanceof ServerLevel) {
-                 ServerLevel worldserver = (ServerLevel) world;
- 
--                this.explode(worldserver);
-+                // CraftBukkit start
-+                if (!org.bukkit.craftbukkit.event.CraftEventFactory.callFireworkExplodeEvent(this).isCancelled()) {
-+                    this.explode(worldserver);
-+                }
-+                // CraftBukkit end
-             }
-         }
- 
-@@ -182,7 +212,7 @@
-         world.broadcastEntityEvent(this, (byte) 17);
-         this.gameEvent(GameEvent.EXPLODE, this.getOwner());
-         this.dealExplosionDamage(world);
--        this.discard();
-+        this.discard(EntityRemoveEvent.Cause.EXPLODE); // CraftBukkit - add Bukkit remove cause
-     }
- 
-     @Override
-@@ -191,7 +221,11 @@
-         Level world = this.level();
- 
-         if (world instanceof ServerLevel worldserver) {
--            this.explode(worldserver);
-+            // CraftBukkit start
-+            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callFireworkExplodeEvent(this).isCancelled()) {
-+                this.explode(worldserver);
-+            }
-+            // CraftBukkit end
-         }
- 
-     }
-@@ -205,7 +239,11 @@
- 
-         if (world instanceof ServerLevel worldserver) {
-             if (this.hasExplosion()) {
--                this.explode(worldserver);
-+                // CraftBukkit start
-+                if (!org.bukkit.craftbukkit.event.CraftEventFactory.callFireworkExplodeEvent(this).isCancelled()) {
-+                    this.explode(worldserver);
-+                }
-+                // CraftBukkit end
-             }
-         }
- 
-@@ -287,6 +325,11 @@
-         nbt.putInt("LifeTime", this.lifetime);
-         nbt.put("FireworksItem", this.getItem().save(this.registryAccess()));
-         nbt.putBoolean("ShotAtAngle", (Boolean) this.entityData.get(FireworkRocketEntity.DATA_SHOT_AT_ANGLE));
-+        // Paper start
-+        if (this.spawningEntity != null) {
-+            nbt.putUUID("SpawningEntity", this.spawningEntity);
-+        }
-+        // Paper end
-     }
- 
-     @Override
-@@ -303,7 +346,11 @@
-         if (nbt.contains("ShotAtAngle")) {
-             this.entityData.set(FireworkRocketEntity.DATA_SHOT_AT_ANGLE, nbt.getBoolean("ShotAtAngle"));
-         }
--
-+        // Paper start
-+        if (nbt.hasUUID("SpawningEntity")) {
-+            this.spawningEntity = nbt.getUUID("SpawningEntity");
-+        }
-+        // Paper end
-     }
- 
-     private List<FireworkExplosion> getExplosions() {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/FishingHook.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/FishingHook.java.patch
deleted file mode 100644
index 27f0995455..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/FishingHook.java.patch
+++ /dev/null
@@ -1,352 +0,0 @@
---- a/net/minecraft/world/entity/projectile/FishingHook.java
-+++ b/net/minecraft/world/entity/projectile/FishingHook.java
-@@ -29,7 +29,6 @@
- import net.minecraft.world.entity.ExperienceOrb;
- import net.minecraft.world.entity.MoverType;
- import net.minecraft.world.entity.item.ItemEntity;
--import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.item.Items;
- import net.minecraft.world.level.Level;
-@@ -47,6 +46,13 @@
- import net.minecraft.world.phys.Vec3;
- import org.slf4j.Logger;
- 
-+// CraftBukkit start
-+import org.bukkit.entity.Player;
-+import org.bukkit.entity.FishHook;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.event.player.PlayerFishEvent;
-+// CraftBukkit end
-+
- public class FishingHook extends Projectile {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
-@@ -68,6 +74,18 @@
-     private final int luck;
-     private final int lureSpeed;
- 
-+    // CraftBukkit start - Extra variables to enable modification of fishing wait time, values are minecraft defaults
-+    public int minWaitTime = 100;
-+    public int maxWaitTime = 600;
-+    public int minLureTime = 20;
-+    public int maxLureTime = 80;
-+    public float minLureAngle = 0.0F;
-+    public float maxLureAngle = 360.0F;
-+    public boolean applyLure = true;
-+    public boolean rainInfluenced = true;
-+    public boolean skyInfluenced = true;
-+    // CraftBukkit end
-+
-     private FishingHook(EntityType<? extends FishingHook> type, Level world, int luckBonus, int waitTimeReductionTicks) {
-         super(type, world);
-         this.syncronizedRandom = RandomSource.create();
-@@ -75,13 +93,17 @@
-         this.currentState = FishingHook.FishHookState.FLYING;
-         this.luck = Math.max(0, luckBonus);
-         this.lureSpeed = Math.max(0, waitTimeReductionTicks);
-+        // Paper start - Configurable fishing time ranges
-+        minWaitTime = world.paperConfig().fishingTimeRange.minimum;
-+        maxWaitTime = world.paperConfig().fishingTimeRange.maximum;
-+        // Paper end - Configurable fishing time ranges
-     }
- 
-     public FishingHook(EntityType<? extends FishingHook> type, Level world) {
-         this(type, world, 0, 0);
-     }
- 
--    public FishingHook(Player thrower, Level world, int luckBonus, int waitTimeReductionTicks) {
-+    public FishingHook(net.minecraft.world.entity.player.Player thrower, Level world, int luckBonus, int waitTimeReductionTicks) {
-         this(EntityType.FISHING_BOBBER, world, luckBonus, waitTimeReductionTicks);
-         this.setOwner(thrower);
-         float f = thrower.getXRot();
-@@ -149,15 +171,15 @@
-     public void tick() {
-         this.syncronizedRandom.setSeed(this.getUUID().getLeastSignificantBits() ^ this.level().getGameTime());
-         super.tick();
--        Player entityhuman = this.getPlayerOwner();
-+        net.minecraft.world.entity.player.Player entityhuman = this.getPlayerOwner();
- 
-         if (entityhuman == null) {
--            this.discard();
-+            this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-         } else if (this.level().isClientSide || !this.shouldStopFishing(entityhuman)) {
-             if (this.onGround()) {
-                 ++this.life;
-                 if (this.life >= 1200) {
--                    this.discard();
-+                    this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-                     return;
-                 }
-             } else {
-@@ -250,7 +272,7 @@
-         }
-     }
- 
--    private boolean shouldStopFishing(Player player) {
-+    private boolean shouldStopFishing(net.minecraft.world.entity.player.Player player) {
-         ItemStack itemstack = player.getMainHandItem();
-         ItemStack itemstack1 = player.getOffhandItem();
-         boolean flag = itemstack.is(Items.FISHING_ROD);
-@@ -259,7 +281,7 @@
-         if (!player.isRemoved() && player.isAlive() && (flag || flag1) && this.distanceToSqr((Entity) player) <= 1024.0D) {
-             return false;
-         } else {
--            this.discard();
-+            this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-             return true;
-         }
-     }
-@@ -267,7 +289,7 @@
-     private void checkCollision() {
-         HitResult movingobjectposition = ProjectileUtil.getHitResultOnMoveVector(this, this::canHitEntity);
- 
--        this.hitTargetOrDeflectSelf(movingobjectposition);
-+        this.preHitTargetOrDeflectSelf(movingobjectposition); // CraftBukkit - projectile hit event
-     }
- 
-     @Override
-@@ -300,11 +322,11 @@
-         int i = 1;
-         BlockPos blockposition1 = pos.above();
- 
--        if (this.random.nextFloat() < 0.25F && this.level().isRainingAt(blockposition1)) {
-+        if (this.rainInfluenced && this.random.nextFloat() < 0.25F && this.level().isRainingAt(blockposition1)) { // CraftBukkit
-             ++i;
-         }
- 
--        if (this.random.nextFloat() < 0.5F && !this.level().canSeeSky(blockposition1)) {
-+        if (this.skyInfluenced && this.random.nextFloat() < 0.5F && !this.level().canSeeSky(blockposition1)) { // CraftBukkit
-             --i;
-         }
- 
-@@ -314,6 +336,10 @@
-                 this.timeUntilLured = 0;
-                 this.timeUntilHooked = 0;
-                 this.getEntityData().set(FishingHook.DATA_BITING, false);
-+                // CraftBukkit start
-+                PlayerFishEvent playerFishEvent = new PlayerFishEvent((Player) this.getPlayerOwner().getBukkitEntity(), null, (FishHook) this.getBukkitEntity(), PlayerFishEvent.State.FAILED_ATTEMPT);
-+                this.level().getCraftServer().getPluginManager().callEvent(playerFishEvent);
-+                // CraftBukkit end
-             }
-         } else {
-             float f;
-@@ -347,6 +373,13 @@
-                         worldserver.sendParticles(ParticleTypes.FISHING, d0, d1, d2, 0, (double) (-f4), 0.01D, (double) f3, 1.0D);
-                     }
-                 } else {
-+                    // CraftBukkit start
-+                    PlayerFishEvent playerFishEvent = new PlayerFishEvent((Player) this.getPlayerOwner().getBukkitEntity(), null, (FishHook) this.getBukkitEntity(), PlayerFishEvent.State.BITE);
-+                    this.level().getCraftServer().getPluginManager().callEvent(playerFishEvent);
-+                    if (playerFishEvent.isCancelled()) {
-+                        return;
-+                    }
-+                    // CraftBukkit end
-                     this.playSound(SoundEvents.FISHING_BOBBER_SPLASH, 0.25F, 1.0F + (this.random.nextFloat() - this.random.nextFloat()) * 0.4F);
-                     double d3 = this.getY() + 0.5D;
- 
-@@ -379,16 +412,34 @@
-                 }
- 
-                 if (this.timeUntilLured <= 0) {
--                    this.fishAngle = Mth.nextFloat(this.random, 0.0F, 360.0F);
--                    this.timeUntilHooked = Mth.nextInt(this.random, 20, 80);
-+                    // CraftBukkit start - logic to modify fishing wait time, lure time, and lure angle
-+                    this.fishAngle = Mth.nextFloat(this.random, this.minLureAngle, this.maxLureAngle);
-+                    this.timeUntilHooked = Mth.nextInt(this.random, this.minLureTime, this.maxLureTime);
-+                    // CraftBukkit end
-+                    // Paper start - Add missing fishing event state
-+                    if (this.getPlayerOwner() != null) {
-+                        PlayerFishEvent playerFishEvent = new PlayerFishEvent((Player) this.getPlayerOwner().getBukkitEntity(), null, (FishHook) this.getBukkitEntity(), PlayerFishEvent.State.LURED);
-+                        if (!playerFishEvent.callEvent()) {
-+                            this.timeUntilHooked = 0;
-+                            return;
-+                        }
-+                    }
-+                    // Paper end - Add missing fishing event state
-                 }
-             } else {
--                this.timeUntilLured = Mth.nextInt(this.random, 100, 600);
--                this.timeUntilLured -= this.lureSpeed;
-+                // CraftBukkit start - logic to modify fishing wait time
-+                this.resetTimeUntilLured(); // Paper - more projectile api - extract time until lured reset logic
-+                // CraftBukkit end
-             }
-         }
- 
-     }
-+    // Paper start - more projectile api - extract time until lured reset logic
-+    public void resetTimeUntilLured() {
-+        this.timeUntilLured = Mth.nextInt(this.random, this.minWaitTime, this.maxWaitTime);
-+        this.timeUntilLured -= (this.applyLure) ? (this.lureSpeed >= this.maxWaitTime ? this.timeUntilLured - 1 : this.lureSpeed ) : 0; // Paper - Fix Lure infinite loop
-+    }
-+    // Paper end - more projectile api - extract time until lured reset logic
- 
-     public boolean calculateOpenWater(BlockPos pos) {
-         FishingHook.OpenWaterType entityfishinghook_waterposition = FishingHook.OpenWaterType.INVALID;
-@@ -445,17 +496,35 @@
-     @Override
-     public void readAdditionalSaveData(CompoundTag nbt) {}
- 
-+    // Paper start - Add hand parameter to PlayerFishEvent
-+    @Deprecated
-+    @io.papermc.paper.annotation.DoNotUse
-     public int retrieve(ItemStack usedItem) {
--        Player entityhuman = this.getPlayerOwner();
-+        return this.retrieve(net.minecraft.world.InteractionHand.MAIN_HAND, usedItem);
-+    }
- 
-+    public int retrieve(net.minecraft.world.InteractionHand hand, ItemStack usedItem) {
-+        // Paper end - Add hand parameter to PlayerFishEvent
-+        net.minecraft.world.entity.player.Player entityhuman = this.getPlayerOwner();
-+
-         if (!this.level().isClientSide && entityhuman != null && !this.shouldStopFishing(entityhuman)) {
-             int i = 0;
- 
-             if (this.hookedIn != null) {
-+                // CraftBukkit start
-+                PlayerFishEvent playerFishEvent = new PlayerFishEvent((Player) entityhuman.getBukkitEntity(), this.hookedIn.getBukkitEntity(), (FishHook) this.getBukkitEntity(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand), PlayerFishEvent.State.CAUGHT_ENTITY); // Paper - Add hand parameter to PlayerFishEvent
-+                this.level().getCraftServer().getPluginManager().callEvent(playerFishEvent);
-+
-+                if (playerFishEvent.isCancelled()) {
-+                    return 0;
-+                }
-+                if (this.hookedIn != null) { // Paper - re-check to see if there is a hooked entity
-+                // CraftBukkit end
-                 this.pullEntity(this.hookedIn);
-                 CriteriaTriggers.FISHING_ROD_HOOKED.trigger((ServerPlayer) entityhuman, usedItem, this, Collections.emptyList());
-                 this.level().broadcastEntityEvent(this, (byte) 31);
-                 i = this.hookedIn instanceof ItemEntity ? 3 : 5;
-+                } // Paper - re-check to see if there is a hooked entity
-             } else if (this.nibble > 0) {
-                 LootParams lootparams = (new LootParams.Builder((ServerLevel) this.level())).withParameter(LootContextParams.ORIGIN, this.position()).withParameter(LootContextParams.TOOL, usedItem).withParameter(LootContextParams.THIS_ENTITY, this).withLuck((float) this.luck + entityhuman.getLuck()).create(LootContextParamSets.FISHING);
-                 LootTable loottable = this.level().getServer().reloadableRegistries().getLootTable(BuiltInLootTables.FISHING);
-@@ -466,15 +535,38 @@
- 
-                 while (iterator.hasNext()) {
-                     ItemStack itemstack1 = (ItemStack) iterator.next();
--                    ItemEntity entityitem = new ItemEntity(this.level(), this.getX(), this.getY(), this.getZ(), itemstack1);
-+                    // Paper start - new ItemEntity would throw if for whatever reason (mostly shitty datapacks) the itemstack1 turns out to be empty
-+                    // if the item stack is empty we instead just have our entityitem as null
-+                    ItemEntity entityitem = null;
-+                    if (!itemstack1.isEmpty()) {
-+                        entityitem = new ItemEntity(this.level(), this.getX(), this.getY(), this.getZ(), itemstack1);
-+                    }
-+                    // Paper end
-+                    // CraftBukkit start
-+                    PlayerFishEvent playerFishEvent = new PlayerFishEvent((Player) entityhuman.getBukkitEntity(), entityitem != null ? entityitem.getBukkitEntity() : null, (FishHook) this.getBukkitEntity(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand), PlayerFishEvent.State.CAUGHT_FISH); // Paper - entityitem may be null // Paper - Add hand parameter to PlayerFishEvent
-+                    playerFishEvent.setExpToDrop(this.random.nextInt(6) + 1);
-+                    this.level().getCraftServer().getPluginManager().callEvent(playerFishEvent);
-+
-+                    if (playerFishEvent.isCancelled()) {
-+                        return 0;
-+                    }
-+                    // CraftBukkit end
-                     double d0 = entityhuman.getX() - this.getX();
-                     double d1 = entityhuman.getY() - this.getY();
-                     double d2 = entityhuman.getZ() - this.getZ();
-                     double d3 = 0.1D;
- 
--                    entityitem.setDeltaMovement(d0 * 0.1D, d1 * 0.1D + Math.sqrt(Math.sqrt(d0 * d0 + d1 * d1 + d2 * d2)) * 0.08D, d2 * 0.1D);
--                    this.level().addFreshEntity(entityitem);
--                    entityhuman.level().addFreshEntity(new ExperienceOrb(entityhuman.level(), entityhuman.getX(), entityhuman.getY() + 0.5D, entityhuman.getZ() + 0.5D, this.random.nextInt(6) + 1));
-+                    // Paper start - entity item can be null, so we need to check against this
-+                    if (entityitem != null) {
-+                        entityitem.setDeltaMovement(d0 * 0.1D, d1 * 0.1D + Math.sqrt(Math.sqrt(d0 * d0 + d1 * d1 + d2 * d2)) * 0.08D, d2 * 0.1D);
-+                        this.level().addFreshEntity(entityitem);
-+                    }
-+                    // Paper end
-+                    // CraftBukkit start - this.random.nextInt(6) + 1 -> playerFishEvent.getExpToDrop()
-+                    if (playerFishEvent.getExpToDrop() > 0) {
-+                        entityhuman.level().addFreshEntity(new ExperienceOrb(entityhuman.level(), entityhuman.getX(), entityhuman.getY() + 0.5D, entityhuman.getZ() + 0.5D, playerFishEvent.getExpToDrop(), org.bukkit.entity.ExperienceOrb.SpawnReason.FISHING, this.getPlayerOwner(), this)); // Paper
-+                    }
-+                    // CraftBukkit end
-                     if (itemstack1.is(ItemTags.FISHES)) {
-                         entityhuman.awardStat(Stats.FISH_CAUGHT, 1);
-                     }
-@@ -484,10 +576,27 @@
-             }
- 
-             if (this.onGround()) {
-+                // CraftBukkit start
-+                PlayerFishEvent playerFishEvent = new PlayerFishEvent((Player) entityhuman.getBukkitEntity(), null, (FishHook) this.getBukkitEntity(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand), PlayerFishEvent.State.IN_GROUND); // Paper - Add hand parameter to PlayerFishEvent
-+                this.level().getCraftServer().getPluginManager().callEvent(playerFishEvent);
-+
-+                if (playerFishEvent.isCancelled()) {
-+                    return 0;
-+                }
-+                // CraftBukkit end
-                 i = 2;
-             }
-+            // CraftBukkit start
-+            if (i == 0) {
-+                PlayerFishEvent playerFishEvent = new PlayerFishEvent((Player) entityhuman.getBukkitEntity(), null, (FishHook) this.getBukkitEntity(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand), PlayerFishEvent.State.REEL_IN); // Paper - Add hand parameter to PlayerFishEvent
-+                this.level().getCraftServer().getPluginManager().callEvent(playerFishEvent);
-+                if (playerFishEvent.isCancelled()) {
-+                    return 0;
-+                }
-+            }
-+            // CraftBukkit end
- 
--            this.discard();
-+            this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-             return i;
-         } else {
-             return 0;
-@@ -496,7 +605,7 @@
- 
-     @Override
-     public void handleEntityEvent(byte status) {
--        if (status == 31 && this.level().isClientSide && this.hookedIn instanceof Player && ((Player) this.hookedIn).isLocalPlayer()) {
-+        if (status == 31 && this.level().isClientSide && this.hookedIn instanceof net.minecraft.world.entity.player.Player && ((net.minecraft.world.entity.player.Player) this.hookedIn).isLocalPlayer()) {
-             this.pullEntity(this.hookedIn);
-         }
- 
-@@ -520,8 +629,15 @@
- 
-     @Override
-     public void remove(Entity.RemovalReason reason) {
-+        // CraftBukkit start - add Bukkit remove cause
-+        this.remove(reason, null);
-+    }
-+
-+    @Override
-+    public void remove(Entity.RemovalReason entity_removalreason, EntityRemoveEvent.Cause cause) {
-+        // CraftBukkit end
-         this.updateOwnerInfo((FishingHook) null);
--        super.remove(reason);
-+        super.remove(entity_removalreason, cause); // CraftBukkit - add Bukkit remove cause
-     }
- 
-     @Override
-@@ -536,7 +652,7 @@
-     }
- 
-     private void updateOwnerInfo(@Nullable FishingHook fishingBobber) {
--        Player entityhuman = this.getPlayerOwner();
-+        net.minecraft.world.entity.player.Player entityhuman = this.getPlayerOwner();
- 
-         if (entityhuman != null) {
-             entityhuman.fishing = fishingBobber;
-@@ -545,10 +661,10 @@
-     }
- 
-     @Nullable
--    public Player getPlayerOwner() {
-+    public net.minecraft.world.entity.player.Player getPlayerOwner() {
-         Entity entity = this.getOwner();
- 
--        return entity instanceof Player ? (Player) entity : null;
-+        return entity instanceof net.minecraft.world.entity.player.Player ? (net.minecraft.world.entity.player.Player) entity : null;
-     }
- 
-     @Nullable
-@@ -575,7 +691,7 @@
-             int i = packet.getData();
- 
-             FishingHook.LOGGER.error("Failed to recreate fishing hook on client. {} (id: {}) is not a valid owner.", this.level().getEntity(i), i);
--            this.discard();
-+            this.discard(null); // CraftBukkit - add Bukkit remove cause
-         }
- 
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/LargeFireball.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/LargeFireball.java.patch
deleted file mode 100644
index 494d25292d..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/LargeFireball.java.patch
+++ /dev/null
@@ -1,56 +0,0 @@
---- a/net/minecraft/world/entity/projectile/LargeFireball.java
-+++ b/net/minecraft/world/entity/projectile/LargeFireball.java
-@@ -12,6 +12,10 @@
- import net.minecraft.world.phys.EntityHitResult;
- import net.minecraft.world.phys.HitResult;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.event.entity.ExplosionPrimeEvent;
-+// CraftBukkit end
- 
- public class LargeFireball extends Fireball {
- 
-@@ -19,11 +23,13 @@
- 
-     public LargeFireball(EntityType<? extends LargeFireball> type, Level world) {
-         super(type, world);
-+        this.isIncendiary = (world instanceof ServerLevel worldserver) && worldserver.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // CraftBukkit
-     }
- 
-     public LargeFireball(Level world, LivingEntity owner, Vec3 velocity, int explosionPower) {
-         super(EntityType.FIREBALL, owner, velocity, world);
-         this.explosionPower = explosionPower;
-+        this.isIncendiary = (world instanceof ServerLevel worldserver) && worldserver.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // CraftBukkit
-     }
- 
-     @Override
-@@ -34,8 +40,16 @@
-         if (world instanceof ServerLevel worldserver) {
-             boolean flag = worldserver.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING);
- 
--            this.level().explode(this, this.getX(), this.getY(), this.getZ(), (float) this.explosionPower, flag, Level.ExplosionInteraction.MOB);
--            this.discard();
-+            // CraftBukkit start - fire ExplosionPrimeEvent
-+            ExplosionPrimeEvent event = new ExplosionPrimeEvent((org.bukkit.entity.Explosive) this.getBukkitEntity());
-+            this.level().getCraftServer().getPluginManager().callEvent(event);
-+
-+            if (!event.isCancelled()) {
-+                // give 'this' instead of (Entity) null so we know what causes the damage
-+                this.level().explode(this, this.getX(), this.getY(), this.getZ(), event.getRadius(), event.getFire(), Level.ExplosionInteraction.MOB);
-+            }
-+            // CraftBukkit end
-+            this.discard(EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
-         }
- 
-     }
-@@ -65,7 +79,8 @@
-     public void readAdditionalSaveData(CompoundTag nbt) {
-         super.readAdditionalSaveData(nbt);
-         if (nbt.contains("ExplosionPower", 99)) {
--            this.explosionPower = nbt.getByte("ExplosionPower");
-+            // CraftBukkit - set bukkitYield when setting explosionpower
-+            this.bukkitYield = this.explosionPower = nbt.getByte("ExplosionPower");
-         }
- 
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/LlamaSpit.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/LlamaSpit.java.patch
deleted file mode 100644
index 370c83f5da..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/LlamaSpit.java.patch
+++ /dev/null
@@ -1,42 +0,0 @@
---- a/net/minecraft/world/entity/projectile/LlamaSpit.java
-+++ b/net/minecraft/world/entity/projectile/LlamaSpit.java
-@@ -17,6 +17,9 @@
- import net.minecraft.world.phys.EntityHitResult;
- import net.minecraft.world.phys.HitResult;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public class LlamaSpit extends Projectile {
- 
-@@ -41,7 +44,7 @@
-         Vec3 vec3d = this.getDeltaMovement();
-         HitResult movingobjectposition = ProjectileUtil.getHitResultOnMoveVector(this, this::canHitEntity);
- 
--        this.hitTargetOrDeflectSelf(movingobjectposition);
-+        this.preHitTargetOrDeflectSelf(movingobjectposition); // CraftBukkit - projectile hit event
-         double d0 = this.getX() + vec3d.x;
-         double d1 = this.getY() + vec3d.y;
-         double d2 = this.getZ() + vec3d.z;
-@@ -50,9 +53,9 @@
-         float f = 0.99F;
- 
-         if (this.level().getBlockStates(this.getBoundingBox()).noneMatch(BlockBehaviour.BlockStateBase::isAir)) {
--            this.discard();
-+            this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-         } else if (this.isInWaterOrBubble()) {
--            this.discard();
-+            this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-         } else {
-             this.setDeltaMovement(vec3d.scale(0.9900000095367432D));
-             this.applyGravity();
-@@ -83,7 +86,7 @@
-     protected void onHitBlock(BlockHitResult blockHitResult) {
-         super.onHitBlock(blockHitResult);
-         if (!this.level().isClientSide) {
--            this.discard();
-+            this.discard(EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
-         }
- 
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/Projectile.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/Projectile.java.patch
deleted file mode 100644
index 4e0aeff12d..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/Projectile.java.patch
+++ /dev/null
@@ -1,231 +0,0 @@
---- a/net/minecraft/world/entity/projectile/Projectile.java
-+++ b/net/minecraft/world/entity/projectile/Projectile.java
-@@ -35,6 +35,9 @@
- import net.minecraft.world.phys.EntityHitResult;
- import net.minecraft.world.phys.HitResult;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.projectiles.ProjectileSource;
-+// CraftBukkit end
- 
- public abstract class Projectile extends Entity implements TraceableEntity {
- 
-@@ -47,6 +50,10 @@
-     @Nullable
-     private Entity lastDeflectedBy;
- 
-+    // CraftBukkit start
-+    protected boolean hitCancelled = false;
-+    // CraftBukkit end
-+
-     Projectile(EntityType<? extends Projectile> type, Level world) {
-         super(type, world);
-     }
-@@ -56,16 +63,35 @@
-             this.ownerUUID = entity.getUUID();
-             this.cachedOwner = entity;
-         }
--
-+        // Paper start - Refresh ProjectileSource for projectiles
-+        else {
-+            this.ownerUUID = null;
-+            this.cachedOwner = null;
-+            this.projectileSource = null;
-+        }
-+        // Paper end - Refresh ProjectileSource for projectiles
-+        this.refreshProjectileSource(false); // Paper
-     }
-+    // Paper start - Refresh ProjectileSource for projectiles
-+    public void refreshProjectileSource(boolean fillCache) {
-+        if (fillCache) {
-+            this.getOwner();
-+        }
-+        if (this.cachedOwner != null && !this.cachedOwner.isRemoved() && this.projectileSource == null && this.cachedOwner.getBukkitEntity() instanceof ProjectileSource projSource) {
-+            this.projectileSource = projSource;
-+        }
-+    }
-+    // Paper end - Refresh ProjectileSource for projectiles
- 
-     @Nullable
-     @Override
-     public Entity getOwner() {
-         if (this.cachedOwner != null && !this.cachedOwner.isRemoved()) {
-+            this.refreshProjectileSource(false); // Paper - Refresh ProjectileSource for projectiles
-             return this.cachedOwner;
-         } else if (this.ownerUUID != null) {
-             this.cachedOwner = this.findOwner(this.ownerUUID);
-+            this.refreshProjectileSource(false); // Paper - Refresh ProjectileSource for projectiles
-             return this.cachedOwner;
-         } else {
-             return null;
-@@ -108,6 +134,7 @@
-     protected void readAdditionalSaveData(CompoundTag nbt) {
-         if (nbt.hasUUID("Owner")) {
-             this.setOwnerThroughUUID(nbt.getUUID("Owner"));
-+            if (this instanceof ThrownEnderpearl && this.level() != null && this.level().paperConfig().fixes.disableUnloadedChunkEnderpearlExploit && this.level().paperConfig().misc.legacyEnderPearlBehavior) { this.ownerUUID = null; } // Paper - Reset pearls when they stop being ticked; Don't store shooter name for pearls to block enderpearl travel exploit
-         }
- 
-         this.leftOwner = nbt.getBoolean("LeftOwner");
-@@ -184,12 +211,20 @@
- 
-         this.shoot((double) f5, (double) f6, (double) f7, speed, divergence);
-         Vec3 vec3d = shooter.getKnownMovement();
--
-+        // Paper start - allow disabling relative velocity
-+        if (!shooter.level().paperConfig().misc.disableRelativeProjectileVelocity) {
-         this.setDeltaMovement(this.getDeltaMovement().add(vec3d.x, shooter.onGround() ? 0.0D : vec3d.y, vec3d.z));
-+        }
-+        // Paper end - allow disabling relative velocity
-     }
- 
-     public static <T extends Projectile> T spawnProjectileFromRotation(Projectile.ProjectileFactory<T> creator, ServerLevel world, ItemStack projectileStack, LivingEntity shooter, float roll, float power, float divergence) {
--        return Projectile.spawnProjectile(creator.create(world, shooter, projectileStack), world, projectileStack, (iprojectile) -> {
-+    // Paper start - PlayerLaunchProjectileEvent
-+        return spawnProjectileFromRotationDelayed(creator, world, projectileStack, shooter, roll, power, divergence).spawn();
-+    }
-+    public static <T extends Projectile> Delayed<T> spawnProjectileFromRotationDelayed(Projectile.ProjectileFactory<T> creator, ServerLevel world, ItemStack projectileStack, LivingEntity shooter, float roll, float power, float divergence) {
-+        return Projectile.spawnProjectileDelayed(creator.create(world, shooter, projectileStack), world, projectileStack, (iprojectile) -> {
-+    // Paper end - PlayerLaunchProjectileEvent
-             iprojectile.shootFromRotation(shooter, shooter.getXRot(), shooter.getYRot(), roll, power, divergence);
-         });
-     }
-@@ -201,7 +236,12 @@
-     }
- 
-     public static <T extends Projectile> T spawnProjectileUsingShoot(T projectile, ServerLevel world, ItemStack projectileStack, double velocityX, double velocityY, double velocityZ, float power, float divergence) {
--        return Projectile.spawnProjectile(projectile, world, projectileStack, (iprojectile) -> {
-+    // Paper start - fixes and addition to spawn reason API
-+        return spawnProjectileUsingShootDelayed(projectile, world, projectileStack, velocityX, velocityY, velocityZ, power, divergence).spawn();
-+    }
-+    public static <T extends Projectile> Delayed<T> spawnProjectileUsingShootDelayed(T projectile, ServerLevel world, ItemStack projectileStack, double velocityX, double velocityY, double velocityZ, float power, float divergence) {
-+        return Projectile.spawnProjectileDelayed(projectile, world, projectileStack, (iprojectile) -> {
-+    // Paper end - fixes and addition to spawn reason API
-             projectile.shoot(velocityX, velocityY, velocityZ, power, divergence);
-         });
-     }
-@@ -211,11 +251,45 @@
-         });
-     }
- 
-+    // Paper start - delayed projectile spawning
-+    public record Delayed<T extends Projectile>(
-+        T projectile,
-+        ServerLevel world,
-+        ItemStack projectileStack
-+    ) {
-+        // Taken from net.minecraft.world.entity.projectile.Projectile.spawnProjectile(T, net.minecraft.server.level.ServerLevel, net.minecraft.world.item.ItemStack, java.util.function.Consumer<T>)
-+        public boolean attemptSpawn() {
-+            if (!world.addFreshEntity(projectile)) return false;
-+            projectile.applyOnProjectileSpawned(this.world, this.projectileStack);
-+            return true;
-+        }
-+
-+        public T spawn() {
-+            this.attemptSpawn();
-+            return projectile();
-+        }
-+
-+        public boolean attemptSpawn(final org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) {
-+            if (!world.addFreshEntity(projectile, reason)) return false;
-+            projectile.applyOnProjectileSpawned(this.world, this.projectileStack);
-+            return true;
-+        }
-+
-+        public T spawn(final org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) {
-+            this.attemptSpawn(reason);
-+            return projectile();
-+        }
-+    }
-+    // Paper end - delayed projectile spawning
-+
-     public static <T extends Projectile> T spawnProjectile(T projectile, ServerLevel world, ItemStack projectileStack, Consumer<T> beforeSpawn) {
-+    // Paper start - delayed projectile spawning
-+        return spawnProjectileDelayed(projectile, world, projectileStack, beforeSpawn).spawn();
-+    }
-+    public static <T extends Projectile> Delayed<T> spawnProjectileDelayed(T projectile, ServerLevel world, ItemStack projectileStack, Consumer<T> beforeSpawn) {
-+    // Paper end - delayed projectile spawning
-         beforeSpawn.accept(projectile);
--        world.addFreshEntity(projectile);
--        projectile.applyOnProjectileSpawned(world, projectileStack);
--        return projectile;
-+        return new Delayed<>(projectile, world, projectileStack); // Paper - delayed projectile spawning
-     }
- 
-     public void applyOnProjectileSpawned(ServerLevel world, ItemStack projectileStack) {
-@@ -232,6 +306,17 @@
- 
-     }
- 
-+    // CraftBukkit start - call projectile hit event
-+    public ProjectileDeflection preHitTargetOrDeflectSelf(HitResult movingobjectposition) { // Paper - protected -> public
-+        org.bukkit.event.entity.ProjectileHitEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callProjectileHitEvent(this, movingobjectposition);
-+        this.hitCancelled = event != null && event.isCancelled();
-+        if (movingobjectposition.getType() == HitResult.Type.BLOCK || !this.hitCancelled) {
-+            return this.hitTargetOrDeflectSelf(movingobjectposition);
-+        }
-+        return ProjectileDeflection.NONE;
-+    }
-+    // CraftBukkit end
-+
-     protected ProjectileDeflection hitTargetOrDeflectSelf(HitResult hitResult) {
-         if (hitResult.getType() == HitResult.Type.ENTITY) {
-             EntityHitResult movingobjectpositionentity = (EntityHitResult) hitResult;
-@@ -269,7 +354,13 @@
-     public boolean deflect(ProjectileDeflection deflection, @Nullable Entity deflector, @Nullable Entity owner, boolean fromAttack) {
-         deflection.deflect(this, deflector, this.random);
-         if (!this.level().isClientSide) {
--            this.setOwner(owner);
-+            // Paper start - Fix PickupStatus getting reset
-+            if (this instanceof AbstractArrow arrow) {
-+                arrow.setOwner(owner, false);
-+            } else {
-+                this.setOwner(owner);
-+            }
-+            // Paper end - Fix PickupStatus getting reset
-             this.onDeflection(deflector, fromAttack);
-         }
- 
-@@ -309,6 +400,11 @@
-     protected void onHitEntity(EntityHitResult entityHitResult) {}
- 
-     protected void onHitBlock(BlockHitResult blockHitResult) {
-+        // CraftBukkit start - cancellable hit event
-+        if (this.hitCancelled) {
-+            return;
-+        }
-+        // CraftBukkit end
-         BlockState iblockdata = this.level().getBlockState(blockHitResult.getBlockPos());
- 
-         iblockdata.onProjectileHit(this.level(), iblockdata, blockHitResult, this);
-@@ -320,6 +416,15 @@
-         } else {
-             Entity entity1 = this.getOwner();
- 
-+            // Paper start - Cancel hit for vanished players
-+            if (entity1 instanceof net.minecraft.server.level.ServerPlayer && entity instanceof net.minecraft.server.level.ServerPlayer) {
-+                org.bukkit.entity.Player collided = (org.bukkit.entity.Player) entity.getBukkitEntity();
-+                org.bukkit.entity.Player shooter = (org.bukkit.entity.Player) entity1.getBukkitEntity();
-+                if (!shooter.canSee(collided)) {
-+                    return false;
-+                }
-+            }
-+            // Paper end - Cancel hit for vanished players
-             return entity1 == null || this.leftOwner || !entity1.isPassengerOfSameVehicle(entity);
-         }
-     }
-@@ -333,14 +438,8 @@
-     }
- 
-     protected static float lerpRotation(float prevRot, float newRot) {
--        while (newRot - prevRot < -180.0F) {
--            prevRot -= 360.0F;
--        }
-+        prevRot += Math.round((newRot - prevRot) / 360.0F) * 360.0F; // Paper - stop large look changes from crashing the server
- 
--        while (newRot - prevRot >= 180.0F) {
--            prevRot += 360.0F;
--        }
--
-         return Mth.lerp(0.2F, prevRot, newRot);
-     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ShulkerBullet.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ShulkerBullet.java.patch
deleted file mode 100644
index bac5f4b417..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ShulkerBullet.java.patch
+++ /dev/null
@@ -1,100 +0,0 @@
---- a/net/minecraft/world/entity/projectile/ShulkerBullet.java
-+++ b/net/minecraft/world/entity/projectile/ShulkerBullet.java
-@@ -31,6 +31,9 @@
- import net.minecraft.world.phys.EntityHitResult;
- import net.minecraft.world.phys.HitResult;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public class ShulkerBullet extends Projectile {
- 
-@@ -60,8 +63,21 @@
-         this.finalTarget = target;
-         this.currentMoveDirection = Direction.UP;
-         this.selectNextMoveDirection(axis);
-+        this.projectileSource = (org.bukkit.entity.LivingEntity) owner.getBukkitEntity(); // CraftBukkit
-     }
- 
-+    // CraftBukkit start
-+    public Entity getTarget() {
-+        return this.finalTarget;
-+    }
-+
-+    public void setTarget(Entity e) {
-+        this.finalTarget = e;
-+        this.currentMoveDirection = Direction.UP;
-+        this.selectNextMoveDirection(Direction.Axis.X);
-+    }
-+    // CraftBukkit end
-+
-     @Override
-     public SoundSource getSoundSource() {
-         return SoundSource.HOSTILE;
-@@ -194,7 +210,7 @@
-     @Override
-     public void checkDespawn() {
-         if (this.level().getDifficulty() == Difficulty.PEACEFUL) {
--            this.discard();
-+            this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-         }
- 
-     }
-@@ -239,7 +255,7 @@
-         }
- 
-         if (movingobjectposition != null && this.isAlive() && movingobjectposition.getType() != HitResult.Type.MISS) {
--            this.hitTargetOrDeflectSelf(movingobjectposition);
-+            this.preHitTargetOrDeflectSelf(movingobjectposition); // CraftBukkit - projectile hit event
-         }
- 
-         ProjectileUtil.rotateTowardsMovement(this, 0.5F);
-@@ -312,7 +328,7 @@
-             if (entity instanceof LivingEntity) {
-                 LivingEntity entityliving1 = (LivingEntity) entity;
- 
--                entityliving1.addEffect(new MobEffectInstance(MobEffects.LEVITATION, 200), (Entity) MoreObjects.firstNonNull(entity1, this));
-+                entityliving1.addEffect(new MobEffectInstance(MobEffects.LEVITATION, 200), (Entity) MoreObjects.firstNonNull(entity1, this), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ATTACK); // CraftBukkit
-             }
-         }
- 
-@@ -326,14 +342,20 @@
-     }
- 
-     private void destroy() {
--        this.discard();
-+        // CraftBukkit start - add Bukkit remove cause
-+        this.destroy(null);
-+    }
-+
-+    private void destroy(EntityRemoveEvent.Cause cause) {
-+        this.discard(cause);
-+        // CraftBukkit end
-         this.level().gameEvent((Holder) GameEvent.ENTITY_DAMAGE, this.position(), GameEvent.Context.of((Entity) this));
-     }
- 
-     @Override
-     protected void onHit(HitResult hitResult) {
-         super.onHit(hitResult);
--        this.destroy();
-+        this.destroy(EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
-     }
- 
-     @Override
-@@ -348,9 +370,14 @@
- 
-     @Override
-     public boolean hurtServer(ServerLevel world, DamageSource source, float amount) {
-+        // CraftBukkit start
-+        if (org.bukkit.craftbukkit.event.CraftEventFactory.handleNonLivingEntityDamageEvent(this, source, amount, false)) {
-+            return false;
-+        }
-+        // CraftBukkit end
-         this.playSound(SoundEvents.SHULKER_BULLET_HURT, 1.0F, 1.0F);
-         world.sendParticles(ParticleTypes.CRIT, this.getX(), this.getY(), this.getZ(), 15, 0.2D, 0.2D, 0.2D, 0.0D);
--        this.destroy();
-+        this.destroy(EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause
-         return true;
-     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/SmallFireball.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/SmallFireball.java.patch
deleted file mode 100644
index 65522ba256..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/SmallFireball.java.patch
+++ /dev/null
@@ -1,63 +0,0 @@
---- a/net/minecraft/world/entity/projectile/SmallFireball.java
-+++ b/net/minecraft/world/entity/projectile/SmallFireball.java
-@@ -15,6 +15,10 @@
- import net.minecraft.world.phys.EntityHitResult;
- import net.minecraft.world.phys.HitResult;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityCombustByEntityEvent;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public class SmallFireball extends Fireball {
- 
-@@ -24,6 +28,11 @@
- 
-     public SmallFireball(Level world, LivingEntity owner, Vec3 velocity) {
-         super(EntityType.SMALL_FIREBALL, owner, velocity, world);
-+        // CraftBukkit start
-+        if (this.getOwner() != null && this.getOwner() instanceof Mob) {
-+            this.isIncendiary = (world instanceof ServerLevel worldserver) && worldserver.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING);
-+        }
-+        // CraftBukkit end
-     }
- 
-     public SmallFireball(Level world, double x, double y, double z, Vec3 velocity) {
-@@ -40,7 +49,14 @@
-             Entity entity1 = this.getOwner();
-             int i = entity.getRemainingFireTicks();
- 
--            entity.igniteForSeconds(5.0F);
-+            // CraftBukkit start - Entity damage by entity event + combust event
-+            EntityCombustByEntityEvent event = new EntityCombustByEntityEvent((org.bukkit.entity.Projectile) this.getBukkitEntity(), entity.getBukkitEntity(), 5.0F);
-+            entity.level().getCraftServer().getPluginManager().callEvent(event);
-+
-+            if (!event.isCancelled()) {
-+                entity.igniteForSeconds(event.getDuration(), false);
-+            }
-+            // CraftBukkit end
-             DamageSource damagesource = this.damageSources().fireball(this, entity1);
- 
-             if (!entity.hurtServer(worldserver, damagesource, 5.0F)) {
-@@ -60,10 +76,10 @@
-         if (world instanceof ServerLevel worldserver) {
-             Entity entity = this.getOwner();
- 
--            if (!(entity instanceof Mob) || worldserver.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) {
-+            if (this.isIncendiary) { // CraftBukkit
-                 BlockPos blockposition = blockHitResult.getBlockPos().relative(blockHitResult.getDirection());
- 
--                if (this.level().isEmptyBlock(blockposition)) {
-+                if (this.level().isEmptyBlock(blockposition) && !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(this.level(), blockposition, this).isCancelled()) { // CraftBukkit
-                     this.level().setBlockAndUpdate(blockposition, BaseFireBlock.getState(this.level(), blockposition));
-                 }
-             }
-@@ -75,7 +91,7 @@
-     protected void onHit(HitResult hitResult) {
-         super.onHit(hitResult);
-         if (!this.level().isClientSide) {
--            this.discard();
-+            this.discard(EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
-         }
- 
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/Snowball.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/Snowball.java.patch
deleted file mode 100644
index 8d2ce7e8d8..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/Snowball.java.patch
+++ /dev/null
@@ -1,21 +0,0 @@
---- a/net/minecraft/world/entity/projectile/Snowball.java
-+++ b/net/minecraft/world/entity/projectile/Snowball.java
-@@ -13,6 +13,9 @@
- import net.minecraft.world.level.Level;
- import net.minecraft.world.phys.EntityHitResult;
- import net.minecraft.world.phys.HitResult;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public class Snowball extends ThrowableItemProjectile {
- 
-@@ -65,7 +68,7 @@
-         super.onHit(hitResult);
-         if (!this.level().isClientSide) {
-             this.level().broadcastEntityEvent(this, (byte) 3);
--            this.discard();
-+            this.discard(EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
-         }
- 
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/SpectralArrow.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/SpectralArrow.java.patch
deleted file mode 100644
index 629d60da70..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/SpectralArrow.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/entity/projectile/SpectralArrow.java
-+++ b/net/minecraft/world/entity/projectile/SpectralArrow.java
-@@ -41,7 +41,7 @@
-         super.doPostHurtEffects(target);
-         MobEffectInstance mobeffect = new MobEffectInstance(MobEffects.GLOWING, this.duration, 0);
- 
--        target.addEffect(mobeffect, this.getEffectSource());
-+        target.addEffect(mobeffect, this.getEffectSource(), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ARROW); // CraftBukkit
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrowableProjectile.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrowableProjectile.java.patch
deleted file mode 100644
index db8a43fd04..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrowableProjectile.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/entity/projectile/ThrowableProjectile.java
-+++ b/net/minecraft/world/entity/projectile/ThrowableProjectile.java
-@@ -63,7 +63,7 @@
-         this.applyEffectsFromBlocks();
-         super.tick();
-         if (movingobjectposition.getType() != HitResult.Type.MISS && this.isAlive()) {
--            this.hitTargetOrDeflectSelf(movingobjectposition);
-+            this.preHitTargetOrDeflectSelf(movingobjectposition); // CraftBukkit - projectile hit event
-         }
- 
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrownEgg.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrownEgg.java.patch
deleted file mode 100644
index 5df84b160a..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrownEgg.java.patch
+++ /dev/null
@@ -1,121 +0,0 @@
---- a/net/minecraft/world/entity/projectile/ThrownEgg.java
-+++ b/net/minecraft/world/entity/projectile/ThrownEgg.java
-@@ -1,33 +1,39 @@
- package net.minecraft.world.entity.projectile;
- 
--import net.minecraft.core.particles.ItemParticleOption;
--import net.minecraft.core.particles.ParticleTypes;
--import net.minecraft.world.entity.EntityDimensions;
- import net.minecraft.world.entity.EntitySpawnReason;
--import net.minecraft.world.entity.EntityType;
- import net.minecraft.world.entity.LivingEntity;
--import net.minecraft.world.entity.animal.Chicken;
- import net.minecraft.world.item.Item;
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.item.Items;
- import net.minecraft.world.level.Level;
- import net.minecraft.world.phys.EntityHitResult;
- import net.minecraft.world.phys.HitResult;
-+import net.minecraft.core.particles.ItemParticleOption;
-+import net.minecraft.core.particles.ParticleTypes;
-+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.entity.EntityDimensions;
-+import org.bukkit.entity.Ageable;
-+import org.bukkit.entity.EntityType;
-+import org.bukkit.entity.Player;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.event.player.PlayerEggThrowEvent;
-+// CraftBukkit end
- 
- public class ThrownEgg extends ThrowableItemProjectile {
- 
-     private static final EntityDimensions ZERO_SIZED_DIMENSIONS = EntityDimensions.fixed(0.0F, 0.0F);
- 
--    public ThrownEgg(EntityType<? extends ThrownEgg> type, Level world) {
-+    public ThrownEgg(net.minecraft.world.entity.EntityType<? extends ThrownEgg> type, Level world) {
-         super(type, world);
-     }
- 
-     public ThrownEgg(Level world, LivingEntity owner, ItemStack stack) {
--        super(EntityType.EGG, owner, world, stack);
-+        super(net.minecraft.world.entity.EntityType.EGG, owner, world, stack);
-     }
- 
-     public ThrownEgg(Level world, double x, double y, double z, ItemStack stack) {
--        super(EntityType.EGG, x, y, z, world, stack);
-+        super(net.minecraft.world.entity.EntityType.EGG, x, y, z, world, stack);
-     }
- 
-     @Override
-@@ -52,30 +58,65 @@
-     protected void onHit(HitResult hitResult) {
-         super.onHit(hitResult);
-         if (!this.level().isClientSide) {
--            if (this.random.nextInt(8) == 0) {
-+            // CraftBukkit start
-+            boolean hatching = this.random.nextInt(8) == 0;
-+            if (true) {
-+            // CraftBukkit end
-                 byte b0 = 1;
- 
-                 if (this.random.nextInt(32) == 0) {
-                     b0 = 4;
-                 }
- 
-+                // CraftBukkit start
-+                EntityType hatchingType = EntityType.CHICKEN;
-+
-+                Entity shooter = this.getOwner();
-+                if (!hatching) {
-+                    b0 = 0;
-+                }
-+                if (shooter instanceof ServerPlayer) {
-+                    PlayerEggThrowEvent event = new PlayerEggThrowEvent((Player) shooter.getBukkitEntity(), (org.bukkit.entity.Egg) this.getBukkitEntity(), hatching, b0, hatchingType);
-+                    this.level().getCraftServer().getPluginManager().callEvent(event);
-+
-+                    b0 = event.getNumHatches();
-+                    hatching = event.isHatching();
-+                    hatchingType = event.getHatchingType();
-+                    // If hatching is set to false, ensure child count is 0
-+                    if (!hatching) {
-+                        b0 = 0;
-+                    }
-+                }
-+                // CraftBukkit end
-+                // Paper start - Add ThrownEggHatchEvent
-+                com.destroystokyo.paper.event.entity.ThrownEggHatchEvent event = new com.destroystokyo.paper.event.entity.ThrownEggHatchEvent((org.bukkit.entity.Egg) getBukkitEntity(), hatching, b0, hatchingType);
-+                event.callEvent();
-+                hatching = event.isHatching();
-+                b0 = hatching ? event.getNumHatches() : 0; // If hatching is set to false, ensure child count is 0
-+                hatchingType = event.getHatchingType();
-+                // Paper end - Add ThrownEggHatchEvent
-+
-                 for (int i = 0; i < b0; ++i) {
--                    Chicken entitychicken = (Chicken) EntityType.CHICKEN.create(this.level(), EntitySpawnReason.TRIGGERED);
-+                    Entity entitychicken = this.level().getWorld().makeEntity(new org.bukkit.Location(this.level().getWorld(), this.getX(), this.getY(), this.getZ(), this.getYRot(), 0.0F), hatchingType.getEntityClass()); // CraftBukkit
- 
-                     if (entitychicken != null) {
--                        entitychicken.setAge(-24000);
-+                        // CraftBukkit start
-+                        if (entitychicken.getBukkitEntity() instanceof Ageable) {
-+                            ((Ageable) entitychicken.getBukkitEntity()).setBaby();
-+                        }
-+                        // CraftBukkit end
-                         entitychicken.moveTo(this.getX(), this.getY(), this.getZ(), this.getYRot(), 0.0F);
-                         if (!entitychicken.fudgePositionAfterSizeChange(ThrownEgg.ZERO_SIZED_DIMENSIONS)) {
-                             break;
-                         }
- 
--                        this.level().addFreshEntity(entitychicken);
-+                        this.level().addFreshEntity(entitychicken, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.EGG); // CraftBukkit
-                     }
-                 }
-             }
- 
-             this.level().broadcastEntityEvent(this, (byte) 3);
--            this.discard();
-+            this.discard(EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
-         }
- 
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrownEnderpearl.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrownEnderpearl.java.patch
deleted file mode 100644
index f24cfeb088..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrownEnderpearl.java.patch
+++ /dev/null
@@ -1,95 +0,0 @@
---- a/net/minecraft/world/entity/projectile/ThrownEnderpearl.java
-+++ b/net/minecraft/world/entity/projectile/ThrownEnderpearl.java
-@@ -24,10 +24,15 @@
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.block.Blocks;
- import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.world.level.dimension.LevelStem;
- import net.minecraft.world.level.portal.TeleportTransition;
- import net.minecraft.world.phys.EntityHitResult;
- import net.minecraft.world.phys.HitResult;
- import net.minecraft.world.phys.Vec3;
-+import org.bukkit.event.entity.CreatureSpawnEvent;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.event.player.PlayerTeleportEvent;
-+// CraftBukkit end
- 
- public class ThrownEnderpearl extends ThrowableItemProjectile {
- 
-@@ -140,12 +145,19 @@
-                         ServerPlayer entityplayer = (ServerPlayer) entity;
- 
-                         if (entityplayer.connection.isAcceptingMessages()) {
-+                            // CraftBukkit start
-+                            ServerPlayer entityplayer1 = entityplayer.teleport(new TeleportTransition(worldserver, vec3d, Vec3.ZERO, 0.0F, 0.0F, Relative.union(Relative.ROTATION, Relative.DELTA), TeleportTransition.DO_NOTHING, PlayerTeleportEvent.TeleportCause.ENDER_PEARL));
-+                            if (entityplayer1 == null) {
-+                                this.discard(EntityRemoveEvent.Cause.HIT);
-+                                return;
-+                            }
-+                            // CraftBukkit end
-                             if (this.random.nextFloat() < 0.05F && worldserver.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) {
-                                 Endermite entityendermite = (Endermite) EntityType.ENDERMITE.create(worldserver, EntitySpawnReason.TRIGGERED);
- 
-                                 if (entityendermite != null) {
-                                     entityendermite.moveTo(entity.getX(), entity.getY(), entity.getZ(), entity.getYRot(), entity.getXRot());
--                                    worldserver.addFreshEntity(entityendermite);
-+                                    worldserver.addFreshEntity(entityendermite, CreatureSpawnEvent.SpawnReason.ENDER_PEARL);
-                                 }
-                             }
- 
-@@ -153,12 +165,12 @@
-                                 entity.setPortalCooldown();
-                             }
- 
--                            ServerPlayer entityplayer1 = entityplayer.teleport(new TeleportTransition(worldserver, vec3d, Vec3.ZERO, 0.0F, 0.0F, Relative.union(Relative.ROTATION, Relative.DELTA), TeleportTransition.DO_NOTHING));
-+                            // EntityPlayer entityplayer1 = entityplayer.teleport(new TeleportTransition(worldserver, vec3d, Vec3D.ZERO, 0.0F, 0.0F, Relative.union(Relative.ROTATION, Relative.DELTA), TeleportTransition.DO_NOTHING)); // CraftBukkit - moved up
- 
-                             if (entityplayer1 != null) {
-                                 entityplayer1.resetFallDistance();
-                                 entityplayer1.resetCurrentImpulseContext();
--                                entityplayer1.hurtServer(entityplayer.serverLevel(), this.damageSources().enderPearl(), 5.0F);
-+                                entityplayer1.hurtServer(entityplayer.serverLevel(), this.damageSources().enderPearl().customEventDamager(this), 5.0F); // CraftBukkit // Paper - fix DamageSource API
-                             }
- 
-                             this.playSound(worldserver, vec3d);
-@@ -173,11 +185,11 @@
-                         this.playSound(worldserver, vec3d);
-                     }
- 
--                    this.discard();
-+                    this.discard(EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
-                     return;
-                 }
- 
--                this.discard();
-+                this.discard(EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
-                 return;
-             }
-         }
-@@ -210,7 +222,7 @@
-             entity = this.getOwner();
-             if (entity instanceof ServerPlayer entityplayer) {
-                 if (!entity.isAlive() && entityplayer.serverLevel().getGameRules().getBoolean(GameRules.RULE_ENDER_PEARLS_VANISH_ON_DEATH)) {
--                    this.discard();
-+                    this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-                     break label30;
-                 }
-             }
-@@ -240,7 +252,7 @@
-         Entity entity = super.teleport(teleportTarget);
- 
-         if (entity != null) {
--            entity.placePortalTicket(BlockPos.containing(entity.position()));
-+            if (!this.level().paperConfig().misc.legacyEnderPearlBehavior) entity.placePortalTicket(BlockPos.containing(entity.position())); // Paper - Allow using old ender pearl behavior
-         }
- 
-         return entity;
-@@ -248,7 +260,7 @@
- 
-     @Override
-     public boolean canTeleport(Level from, Level to) {
--        if (from.dimension() == Level.END && to.dimension() == Level.OVERWORLD) {
-+        if (from.getTypeKey() == LevelStem.END && to.getTypeKey() == LevelStem.OVERWORLD) { // CraftBukkit
-             Entity entity = this.getOwner();
- 
-             if (entity instanceof ServerPlayer) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrownExperienceBottle.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrownExperienceBottle.java.patch
deleted file mode 100644
index 40cd201036..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrownExperienceBottle.java.patch
+++ /dev/null
@@ -1,36 +0,0 @@
---- a/net/minecraft/world/entity/projectile/ThrownExperienceBottle.java
-+++ b/net/minecraft/world/entity/projectile/ThrownExperienceBottle.java
-@@ -9,6 +9,9 @@
- import net.minecraft.world.item.Items;
- import net.minecraft.world.level.Level;
- import net.minecraft.world.phys.HitResult;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public class ThrownExperienceBottle extends ThrowableItemProjectile {
- 
-@@ -38,11 +41,20 @@
-     protected void onHit(HitResult hitResult) {
-         super.onHit(hitResult);
-         if (this.level() instanceof ServerLevel) {
--            this.level().levelEvent(2002, this.blockPosition(), -13083194);
-+            // CraftBukkit - moved to after event
-+            // this.level().levelEvent(2002, this.blockPosition(), -13083194);
-             int i = 3 + this.level().random.nextInt(5) + this.level().random.nextInt(5);
- 
--            ExperienceOrb.award((ServerLevel) this.level(), this.position(), i);
--            this.discard();
-+            // CraftBukkit start
-+            org.bukkit.event.entity.ExpBottleEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callExpBottleEvent(this, hitResult, i);
-+            i = event.getExperience();
-+            if (event.getShowEffect()) {
-+                this.level().levelEvent(2002, this.blockPosition(), -13083194);
-+            }
-+            // CraftBukkit end
-+
-+            ExperienceOrb.award((ServerLevel) this.level(), this.position(), i, org.bukkit.entity.ExperienceOrb.SpawnReason.EXP_BOTTLE, this.getOwner(), this); // Paper
-+            this.discard(EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
-         }
- 
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrownPotion.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrownPotion.java.patch
deleted file mode 100644
index babee054f3..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrownPotion.java.patch
+++ /dev/null
@@ -1,322 +0,0 @@
---- a/net/minecraft/world/entity/projectile/ThrownPotion.java
-+++ b/net/minecraft/world/entity/projectile/ThrownPotion.java
-@@ -10,6 +10,7 @@
- import net.minecraft.core.Holder;
- import net.minecraft.core.component.DataComponents;
- import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
- import net.minecraft.tags.BlockTags;
- import net.minecraft.world.damagesource.DamageSource;
- import net.minecraft.world.effect.MobEffect;
-@@ -17,7 +18,6 @@
- import net.minecraft.world.entity.AreaEffectCloud;
- import net.minecraft.world.entity.Entity;
- import net.minecraft.world.entity.EntityType;
--import net.minecraft.world.entity.LivingEntity;
- import net.minecraft.world.entity.animal.axolotl.Axolotl;
- import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.item.Item;
-@@ -28,18 +28,28 @@
- import net.minecraft.world.item.alchemy.Potions;
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.block.AbstractCandleBlock;
-+// CraftBukkit start
-+import java.util.HashMap;
-+import java.util.Map;
-+import net.minecraft.world.effect.MobEffects;
-+import net.minecraft.world.level.block.Blocks;
- import net.minecraft.world.level.block.CampfireBlock;
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.phys.AABB;
- import net.minecraft.world.phys.BlockHitResult;
- import net.minecraft.world.phys.EntityHitResult;
- import net.minecraft.world.phys.HitResult;
-+import org.bukkit.craftbukkit.entity.CraftLivingEntity;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.entity.LivingEntity;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public class ThrownPotion extends ThrowableItemProjectile {
- 
-     public static final double SPLASH_RANGE = 4.0D;
-     private static final double SPLASH_RANGE_SQ = 16.0D;
--    public static final Predicate<LivingEntity> WATER_SENSITIVE_OR_ON_FIRE = (entityliving) -> {
-+    public static final Predicate<net.minecraft.world.entity.LivingEntity> WATER_SENSITIVE_OR_ON_FIRE = (entityliving) -> {
-         return entityliving.isSensitiveToWater() || entityliving.isOnFire();
-     };
- 
-@@ -47,7 +57,7 @@
-         super(type, world);
-     }
- 
--    public ThrownPotion(Level world, LivingEntity owner, ItemStack stack) {
-+    public ThrownPotion(Level world, net.minecraft.world.entity.LivingEntity owner, ItemStack stack) {
-         super(EntityType.POTION, owner, world, stack);
-     }
- 
-@@ -93,70 +103,96 @@
-     @Override
-     protected void onHit(HitResult hitResult) {
-         super.onHit(hitResult);
-+        // Paper start - More projectile API
-+        this.splash(hitResult);
-+    }
-+    public void splash(@Nullable HitResult hitResult) {
-+        // Paper end - More projectile API
-         Level world = this.level();
--
-         if (world instanceof ServerLevel worldserver) {
-             ItemStack itemstack = this.getItem();
-             PotionContents potioncontents = (PotionContents) itemstack.getOrDefault(DataComponents.POTION_CONTENTS, PotionContents.EMPTY);
- 
-+            boolean showParticles = true; // Paper - Fix potions splash events
-             if (potioncontents.is(Potions.WATER)) {
--                this.applyWater(worldserver);
--            } else if (potioncontents.hasEffects()) {
-+                showParticles = this.applyWater(worldserver, hitResult); // Paper - Fix potions splash events
-+            } else if (true || potioncontents.hasEffects()) { // CraftBukkit - Call event even if no effects to apply
-                 if (this.isLingering()) {
--                    this.makeAreaOfEffectCloud(potioncontents);
-+                    showParticles = this.makeAreaOfEffectCloud(potioncontents, hitResult); // CraftBukkit - Pass MovingObjectPosition // Paper
-                 } else {
--                    this.applySplash(worldserver, potioncontents.getAllEffects(), hitResult.getType() == HitResult.Type.ENTITY ? ((EntityHitResult) hitResult).getEntity() : null);
-+                    showParticles = this.applySplash(worldserver, potioncontents.getAllEffects(), hitResult != null && hitResult.getType() == HitResult.Type.ENTITY ? ((EntityHitResult) hitResult).getEntity() : null, hitResult); // CraftBukkit - Pass MovingObjectPosition // Paper - More projectile API
-                 }
-             }
- 
-+            if (showParticles) { // Paper - Fix potions splash events
-             int i = potioncontents.potion().isPresent() && ((Potion) ((Holder) potioncontents.potion().get()).value()).hasInstantEffects() ? 2007 : 2002;
- 
-             worldserver.levelEvent(i, this.blockPosition(), potioncontents.getColor());
--            this.discard();
-+            } // Paper - Fix potions splash events
-+            this.discard(EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
-         }
-     }
- 
--    private void applyWater(ServerLevel world) {
-+    private static final Predicate<net.minecraft.world.entity.LivingEntity> APPLY_WATER_GET_ENTITIES_PREDICATE = ThrownPotion.WATER_SENSITIVE_OR_ON_FIRE.or(Axolotl.class::isInstance); // Paper - Fix potions splash events
-+    private boolean applyWater(ServerLevel world, @Nullable HitResult hitResult) { // Paper - Fix potions splash events
-         AABB axisalignedbb = this.getBoundingBox().inflate(4.0D, 2.0D, 4.0D);
--        List<LivingEntity> list = this.level().getEntitiesOfClass(LivingEntity.class, axisalignedbb, ThrownPotion.WATER_SENSITIVE_OR_ON_FIRE);
-+        // Paper start - Fix potions splash events
-+        List<net.minecraft.world.entity.LivingEntity> list = this.level().getEntitiesOfClass(net.minecraft.world.entity.LivingEntity.class, axisalignedbb, ThrownPotion.APPLY_WATER_GET_ENTITIES_PREDICATE);
-+        Map<LivingEntity, Double> affected = new HashMap<>();
-+        java.util.Set<LivingEntity> rehydrate = new java.util.HashSet<>();
-+        java.util.Set<LivingEntity> extinguish = new java.util.HashSet<>();
-         Iterator iterator = list.iterator();
- 
-         while (iterator.hasNext()) {
--            LivingEntity entityliving = (LivingEntity) iterator.next();
-+            net.minecraft.world.entity.LivingEntity entityliving = (net.minecraft.world.entity.LivingEntity) iterator.next();
-+            if (entityliving instanceof Axolotl axolotl) {
-+                rehydrate.add(((org.bukkit.entity.Axolotl) axolotl.getBukkitEntity()));
-+            }
-             double d0 = this.distanceToSqr((Entity) entityliving);
- 
-             if (d0 < 16.0D) {
-                 if (entityliving.isSensitiveToWater()) {
--                    entityliving.hurtServer(world, this.damageSources().indirectMagic(this, this.getOwner()), 1.0F);
-+                    affected.put(entityliving.getBukkitLivingEntity(), 1.0);
-                 }
- 
-                 if (entityliving.isOnFire() && entityliving.isAlive()) {
--                    entityliving.extinguishFire();
-+                    extinguish.add(entityliving.getBukkitLivingEntity());
-                 }
-             }
-         }
- 
--        List<Axolotl> list1 = this.level().getEntitiesOfClass(Axolotl.class, axisalignedbb);
--        Iterator iterator1 = list1.iterator();
--
--        while (iterator1.hasNext()) {
--            Axolotl axolotl = (Axolotl) iterator1.next();
--
--            axolotl.rehydrate();
-+        io.papermc.paper.event.entity.WaterBottleSplashEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callWaterBottleSplashEvent(
-+            this, hitResult, affected, rehydrate, extinguish
-+        );
-+        if (!event.isCancelled()) {
-+            for (LivingEntity affectedEntity : event.getToDamage()) {
-+                ((CraftLivingEntity) affectedEntity).getHandle().hurtServer(world, this.damageSources().indirectMagic(this, this.getOwner()), 1.0F);
-+            }
-+            for (LivingEntity toExtinguish : event.getToExtinguish()) {
-+                ((CraftLivingEntity) toExtinguish).getHandle().extinguishFire();
-+            }
-+            for (LivingEntity toRehydrate : event.getToRehydrate()) {
-+                if (((CraftLivingEntity) toRehydrate).getHandle() instanceof Axolotl axolotl) {
-+                    axolotl.rehydrate();
-+                }
-+            }
-+            // Paper end - Fix potions splash events
-         }
-+        return !event.isCancelled(); // Paper - Fix potions splash events
- 
-     }
- 
--    private void applySplash(ServerLevel world, Iterable<MobEffectInstance> effects, @Nullable Entity entity) {
-+    private boolean applySplash(ServerLevel worldserver, Iterable<MobEffectInstance> iterable, @Nullable Entity entity, @Nullable HitResult position) { // CraftBukkit - Pass MovingObjectPosition // Paper - Fix potions splash events & More projectile API
-         AABB axisalignedbb = this.getBoundingBox().inflate(4.0D, 2.0D, 4.0D);
--        List<LivingEntity> list = world.getEntitiesOfClass(LivingEntity.class, axisalignedbb);
-+        List<net.minecraft.world.entity.LivingEntity> list = worldserver.getEntitiesOfClass(net.minecraft.world.entity.LivingEntity.class, axisalignedbb);
-+        Map<LivingEntity, Double> affected = new HashMap<LivingEntity, Double>(); // CraftBukkit
- 
-         if (!list.isEmpty()) {
-             Entity entity1 = this.getEffectSource();
-             Iterator iterator = list.iterator();
- 
-             while (iterator.hasNext()) {
--                LivingEntity entityliving = (LivingEntity) iterator.next();
-+                net.minecraft.world.entity.LivingEntity entityliving = (net.minecraft.world.entity.LivingEntity) iterator.next();
- 
-                 if (entityliving.isAffectedByPotions()) {
-                     double d0 = this.distanceToSqr((Entity) entityliving);
-@@ -164,43 +200,71 @@
-                     if (d0 < 16.0D) {
-                         double d1;
- 
-+                        // Paper - diff on change, used when calling the splash event for water splash potions
-                         if (entityliving == entity) {
-                             d1 = 1.0D;
-                         } else {
-                             d1 = 1.0D - Math.sqrt(d0) / 4.0D;
-                         }
- 
--                        Iterator iterator1 = effects.iterator();
-+                        // CraftBukkit start
-+                        affected.put((LivingEntity) entityliving.getBukkitEntity(), d1);
-+                    }
-+                }
-+            }
-+        }
- 
--                        while (iterator1.hasNext()) {
--                            MobEffectInstance mobeffect = (MobEffectInstance) iterator1.next();
--                            Holder<MobEffect> holder = mobeffect.getEffect();
-+        org.bukkit.event.entity.PotionSplashEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPotionSplashEvent(this, position, affected);
-+        if (!event.isCancelled() && list != null && !list.isEmpty()) { // do not process effects if there are no effects to process
-+            Entity entity1 = this.getEffectSource();
-+            for (LivingEntity victim : event.getAffectedEntities()) {
-+                if (!(victim instanceof CraftLivingEntity)) {
-+                    continue;
-+                }
- 
--                            if (((MobEffect) holder.value()).isInstantenous()) {
--                                ((MobEffect) holder.value()).applyInstantenousEffect(world, this, this.getOwner(), entityliving, mobeffect.getAmplifier(), d1);
--                            } else {
--                                int i = mobeffect.mapDuration((j) -> {
--                                    return (int) (d1 * (double) j + 0.5D);
--                                });
--                                MobEffectInstance mobeffect1 = new MobEffectInstance(holder, i, mobeffect.getAmplifier(), mobeffect.isAmbient(), mobeffect.isVisible());
-+                net.minecraft.world.entity.LivingEntity entityliving = ((CraftLivingEntity) victim).getHandle();
-+                double d1 = event.getIntensity(victim);
-+                // CraftBukkit end
- 
--                                if (!mobeffect1.endsWithin(20)) {
--                                    entityliving.addEffect(mobeffect1, entity1);
--                                }
--                            }
-+                Iterator iterator1 = iterable.iterator();
-+
-+                while (iterator1.hasNext()) {
-+                    MobEffectInstance mobeffect = (MobEffectInstance) iterator1.next();
-+                    Holder<MobEffect> holder = mobeffect.getEffect();
-+                    // CraftBukkit start - Abide by PVP settings - for players only!
-+                    if (!this.level().pvpMode && this.getOwner() instanceof ServerPlayer && entityliving instanceof ServerPlayer && entityliving != this.getOwner()) {
-+                        MobEffect mobeffectlist = (MobEffect) holder.value();
-+                        if (mobeffectlist == MobEffects.MOVEMENT_SLOWDOWN || mobeffectlist == MobEffects.DIG_SLOWDOWN || mobeffectlist == MobEffects.HARM || mobeffectlist == MobEffects.BLINDNESS
-+                                || mobeffectlist == MobEffects.HUNGER || mobeffectlist == MobEffects.WEAKNESS || mobeffectlist == MobEffects.POISON) {
-+                            continue;
-                         }
-                     }
-+                    // CraftBukkit end
-+
-+                    if (((MobEffect) holder.value()).isInstantenous()) {
-+                        ((MobEffect) holder.value()).applyInstantenousEffect(worldserver, this, this.getOwner(), entityliving, mobeffect.getAmplifier(), d1);
-+                    } else {
-+                        int i = mobeffect.mapDuration((j) -> {
-+                            return (int) (d1 * (double) j + 0.5D);
-+                        });
-+                        MobEffectInstance mobeffect1 = new MobEffectInstance(holder, i, mobeffect.getAmplifier(), mobeffect.isAmbient(), mobeffect.isVisible());
-+
-+                        if (!mobeffect1.endsWithin(20)) {
-+                            entityliving.addEffect(mobeffect1, entity1, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.POTION_SPLASH); // CraftBukkit
-+                        }
-+                    }
-                 }
-             }
-         }
-+        return !event.isCancelled(); // Paper - Fix potions splash events
- 
-     }
- 
--    private void makeAreaOfEffectCloud(PotionContents potion) {
-+    private boolean makeAreaOfEffectCloud(PotionContents potioncontents, @Nullable HitResult position) { // CraftBukkit - Pass MovingObjectPosition // Paper - More projectile API
-         AreaEffectCloud entityareaeffectcloud = new AreaEffectCloud(this.level(), this.getX(), this.getY(), this.getZ());
-         Entity entity = this.getOwner();
- 
--        if (entity instanceof LivingEntity entityliving) {
-+        if (entity instanceof net.minecraft.world.entity.LivingEntity entityliving) {
-             entityareaeffectcloud.setOwner(entityliving);
-         }
- 
-@@ -208,8 +272,17 @@
-         entityareaeffectcloud.setRadiusOnUse(-0.5F);
-         entityareaeffectcloud.setWaitTime(10);
-         entityareaeffectcloud.setRadiusPerTick(-entityareaeffectcloud.getRadius() / (float) entityareaeffectcloud.getDuration());
--        entityareaeffectcloud.setPotionContents(potion);
--        this.level().addFreshEntity(entityareaeffectcloud);
-+        entityareaeffectcloud.setPotionContents(potioncontents);
-+        boolean noEffects = potioncontents.hasEffects(); // Paper - Fix potions splash events
-+        // CraftBukkit start
-+        org.bukkit.event.entity.LingeringPotionSplashEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callLingeringPotionSplashEvent(this, position, entityareaeffectcloud);
-+        if (!(event.isCancelled() || entityareaeffectcloud.isRemoved() || (!event.allowsEmptyCreation() && (noEffects && !entityareaeffectcloud.potionContents.hasEffects())))) { // Paper - don't spawn area effect cloud if the effects were empty and not changed during the event handling
-+            this.level().addFreshEntity(entityareaeffectcloud);
-+        } else {
-+            entityareaeffectcloud.discard(null); // CraftBukkit - add Bukkit remove cause
-+        }
-+        // CraftBukkit end
-+        return !event.isCancelled(); // Paper - Fix potions splash events
-     }
- 
-     public boolean isLingering() {
-@@ -220,19 +293,31 @@
-         BlockState iblockdata = this.level().getBlockState(pos);
- 
-         if (iblockdata.is(BlockTags.FIRE)) {
--            this.level().destroyBlock(pos, false, this);
-+            // CraftBukkit start
-+            if (CraftEventFactory.callEntityChangeBlockEvent(this, pos, iblockdata.getFluidState().createLegacyBlock())) { // Paper - fix wrong block state
-+                this.level().destroyBlock(pos, false, this);
-+            }
-+            // CraftBukkit end
-         } else if (AbstractCandleBlock.isLit(iblockdata)) {
--            AbstractCandleBlock.extinguish((Player) null, iblockdata, this.level(), pos);
-+            // CraftBukkit start
-+            if (CraftEventFactory.callEntityChangeBlockEvent(this, pos, iblockdata.setValue(AbstractCandleBlock.LIT, false))) {
-+                AbstractCandleBlock.extinguish((Player) null, iblockdata, this.level(), pos);
-+            }
-+            // CraftBukkit end
-         } else if (CampfireBlock.isLitCampfire(iblockdata)) {
--            this.level().levelEvent((Player) null, 1009, pos, 0);
--            CampfireBlock.dowse(this.getOwner(), this.level(), pos, iblockdata);
--            this.level().setBlockAndUpdate(pos, (BlockState) iblockdata.setValue(CampfireBlock.LIT, false));
-+            // CraftBukkit start
-+            if (CraftEventFactory.callEntityChangeBlockEvent(this, pos, iblockdata.setValue(CampfireBlock.LIT, false))) {
-+                this.level().levelEvent((Player) null, 1009, pos, 0);
-+                CampfireBlock.dowse(this.getOwner(), this.level(), pos, iblockdata);
-+                this.level().setBlockAndUpdate(pos, (BlockState) iblockdata.setValue(CampfireBlock.LIT, false));
-+            }
-+            // CraftBukkit end
-         }
- 
-     }
- 
-     @Override
--    public DoubleDoubleImmutablePair calculateHorizontalHurtKnockbackDirection(LivingEntity target, DamageSource source) {
-+    public DoubleDoubleImmutablePair calculateHorizontalHurtKnockbackDirection(net.minecraft.world.entity.LivingEntity target, DamageSource source) {
-         double d0 = target.position().x - this.position().x;
-         double d1 = target.position().z - this.position().z;
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrownTrident.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrownTrident.java.patch
deleted file mode 100644
index fff7300b9e..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrownTrident.java.patch
+++ /dev/null
@@ -1,86 +0,0 @@
---- a/net/minecraft/world/entity/projectile/ThrownTrident.java
-+++ b/net/minecraft/world/entity/projectile/ThrownTrident.java
-@@ -23,6 +23,9 @@
- import net.minecraft.world.phys.BlockHitResult;
- import net.minecraft.world.phys.EntityHitResult;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public class ThrownTrident extends AbstractArrow {
- 
-@@ -34,16 +37,19 @@
- 
-     public ThrownTrident(EntityType<? extends ThrownTrident> type, Level world) {
-         super(type, world);
-+        this.setBaseDamage(net.minecraft.world.item.TridentItem.BASE_DAMAGE); // Paper - Allow trident custom damage
-     }
- 
-     public ThrownTrident(Level world, LivingEntity owner, ItemStack stack) {
-         super(EntityType.TRIDENT, owner, world, stack, (ItemStack) null);
-+        this.setBaseDamage(net.minecraft.world.item.TridentItem.BASE_DAMAGE); // Paper - Allow trident custom damage
-         this.entityData.set(ThrownTrident.ID_LOYALTY, this.getLoyaltyFromItem(stack));
-         this.entityData.set(ThrownTrident.ID_FOIL, stack.hasFoil());
-     }
- 
-     public ThrownTrident(Level world, double x, double y, double z, ItemStack stack) {
-         super(EntityType.TRIDENT, x, y, z, world, stack, stack);
-+        this.setBaseDamage(net.minecraft.world.item.TridentItem.BASE_DAMAGE); // Paper - Allow trident custom damage
-         this.entityData.set(ThrownTrident.ID_LOYALTY, this.getLoyaltyFromItem(stack));
-         this.entityData.set(ThrownTrident.ID_FOIL, stack.hasFoil());
-     }
-@@ -76,10 +82,10 @@
-                     }
-                 }
- 
--                this.discard();
-+                this.discard(EntityRemoveEvent.Cause.DROP); // CraftBukkit - add Bukkit remove cause
-             } else {
-                 if (!(entity instanceof Player) && this.position().distanceTo(entity.getEyePosition()) < (double) entity.getBbWidth() + 1.0D) {
--                    this.discard();
-+                    this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
-                     return;
-                 }
- 
-@@ -109,8 +115,22 @@
- 
-     public boolean isFoil() {
-         return (Boolean) this.entityData.get(ThrownTrident.ID_FOIL);
-+    }
-+
-+    // Paper start
-+    public void setFoil(boolean foil) {
-+        this.entityData.set(ThrownTrident.ID_FOIL, foil);
-     }
- 
-+    public int getLoyalty() {
-+        return this.entityData.get(ThrownTrident.ID_LOYALTY);
-+    }
-+
-+    public void setLoyalty(byte loyalty) {
-+        this.entityData.set(ThrownTrident.ID_LOYALTY, loyalty);
-+    }
-+    // Paper end
-+
-     @Nullable
-     @Override
-     protected EntityHitResult findHitEntity(Vec3 currentPosition, Vec3 nextPosition) {
-@@ -120,7 +140,7 @@
-     @Override
-     protected void onHitEntity(EntityHitResult entityHitResult) {
-         Entity entity = entityHitResult.getEntity();
--        float f = 8.0F;
-+        float f = (float) this.getBaseDamage(); // Paper - Allow trident custom damage
-         Entity entity1 = this.getOwner();
-         DamageSource damagesource = this.damageSources().trident(this, (Entity) (entity1 == null ? this : entity1));
-         Level world = this.level();
-@@ -137,7 +157,7 @@
- 
-             world = this.level();
-             if (world instanceof ServerLevel) {
--                worldserver = (ServerLevel) world;
-+                ServerLevel worldserver = (ServerLevel) world; // CraftBukkit - decompile error
-                 EnchantmentHelper.doPostAttackEffectsWithItemSourceOnBreak(worldserver, entity, damagesource, this.getWeaponItem(), (item) -> {
-                     this.kill(worldserver);
-                 });
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/WitherSkull.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/WitherSkull.java.patch
deleted file mode 100644
index 96234163f1..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/WitherSkull.java.patch
+++ /dev/null
@@ -1,55 +0,0 @@
---- a/net/minecraft/world/entity/projectile/WitherSkull.java
-+++ b/net/minecraft/world/entity/projectile/WitherSkull.java
-@@ -23,6 +23,10 @@
- import net.minecraft.world.phys.EntityHitResult;
- import net.minecraft.world.phys.HitResult;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.event.entity.ExplosionPrimeEvent;
-+// CraftBukkit end
- 
- public class WitherSkull extends AbstractHurtingProjectile {
- 
-@@ -69,11 +73,11 @@
-                     if (entity.isAlive()) {
-                         EnchantmentHelper.doPostAttackEffects(worldserver, entity, damagesource);
-                     } else {
--                        entityliving.heal(5.0F);
-+                        entityliving.heal(5.0F, org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.WITHER); // CraftBukkit
-                     }
-                 }
-             } else {
--                flag = entity.hurtServer(worldserver, this.damageSources().magic(), 5.0F);
-+                flag = entity.hurtServer(worldserver, this.damageSources().magic().customEventDamager(this), 5.0F); // Paper - Fire EntityDamageByEntityEvent for unowned wither skulls // Paper - fix DamageSource API
-             }
- 
-             if (flag && entity instanceof LivingEntity entityliving) {
-@@ -86,7 +90,7 @@
-                 }
- 
-                 if (b0 > 0) {
--                    entityliving.addEffect(new MobEffectInstance(MobEffects.WITHER, 20 * b0, 1), this.getEffectSource());
-+                    entityliving.addEffect(new MobEffectInstance(MobEffects.WITHER, 20 * b0, 1), this.getEffectSource(), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ATTACK); // CraftBukkit
-                 }
-             }
- 
-@@ -97,8 +101,16 @@
-     protected void onHit(HitResult hitResult) {
-         super.onHit(hitResult);
-         if (!this.level().isClientSide) {
--            this.level().explode(this, this.getX(), this.getY(), this.getZ(), 1.0F, false, Level.ExplosionInteraction.MOB);
--            this.discard();
-+            // CraftBukkit start
-+            // this.level().explode(this, this.getX(), this.getY(), this.getZ(), 1.0F, false, World.a.MOB);
-+            ExplosionPrimeEvent event = new ExplosionPrimeEvent(this.getBukkitEntity(), 1.0F, false);
-+            this.level().getCraftServer().getPluginManager().callEvent(event);
-+
-+            if (!event.isCancelled()) {
-+                this.level().explode(this, this.getX(), this.getY(), this.getZ(), event.getRadius(), event.getFire(), Level.ExplosionInteraction.MOB);
-+            }
-+            // CraftBukkit end
-+            this.discard(EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
-         }
- 
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/windcharge/AbstractWindCharge.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/windcharge/AbstractWindCharge.java.patch
deleted file mode 100644
index 01844591e6..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/windcharge/AbstractWindCharge.java.patch
+++ /dev/null
@@ -1,48 +0,0 @@
---- a/net/minecraft/world/entity/projectile/windcharge/AbstractWindCharge.java
-+++ b/net/minecraft/world/entity/projectile/windcharge/AbstractWindCharge.java
-@@ -24,6 +24,9 @@
- import net.minecraft.world.phys.EntityHitResult;
- import net.minecraft.world.phys.HitResult;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public abstract class AbstractWindCharge extends AbstractHurtingProjectile implements ItemSupplier {
- 
-@@ -98,7 +101,7 @@
-     }
- 
-     @Override
--    public void push(double deltaX, double deltaY, double deltaZ) {}
-+    public void push(double deltaX, double deltaY, double deltaZ, @Nullable Entity pushingEntity) {} // Paper - Add EntityKnockbackByEntityEvent and EntityPushedByEntityAttackEvent
- 
-     public abstract void explode(Vec3 pos);
- 
-@@ -111,7 +114,7 @@
-             Vec3 vec3d1 = blockHitResult.getLocation().add(vec3d);
- 
-             this.explode(vec3d1);
--            this.discard();
-+            this.discard(EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
-         }
- 
-     }
-@@ -120,7 +123,7 @@
-     protected void onHit(HitResult hitResult) {
-         super.onHit(hitResult);
-         if (!this.level().isClientSide) {
--            this.discard();
-+            this.discard(EntityRemoveEvent.Cause.HIT); // CraftBukkit - add Bukkit remove cause
-         }
- 
-     }
-@@ -155,7 +158,7 @@
-     public void tick() {
-         if (!this.level().isClientSide && this.getBlockY() > this.level().getMaxY() + 30) {
-             this.explode(this.position());
--            this.discard();
-+            this.discard(EntityRemoveEvent.Cause.OUT_OF_WORLD); // CraftBukkit - add Bukkit remove cause
-         } else {
-             super.tick();
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/raid/Raider.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/raid/Raider.java.patch
deleted file mode 100644
index 7f33d0dc93..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/raid/Raider.java.patch
+++ /dev/null
@@ -1,83 +0,0 @@
---- a/net/minecraft/world/entity/raid/Raider.java
-+++ b/net/minecraft/world/entity/raid/Raider.java
-@@ -40,6 +40,9 @@
- import net.minecraft.world.level.ServerLevelAccessor;
- import net.minecraft.world.level.pathfinder.Path;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public abstract class Raider extends PatrollingMonster {
- 
-@@ -225,18 +228,25 @@
-         boolean flag = this.hasActiveRaid() && this.getCurrentRaid().getLeader(this.getWave()) != null;
- 
-         if (this.hasActiveRaid() && !flag && ItemStack.matches(itemstack, Raid.getOminousBannerInstance(this.registryAccess().lookupOrThrow(Registries.BANNER_PATTERN)))) {
-+            // Paper start - EntityPickupItemEvent fixes
-+            if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPickupItemEvent(this, itemEntity, 0, false).isCancelled()) {
-+                return;
-+            }
-+            // Paper end - EntityPickupItemEvent fixes
-             EquipmentSlot enumitemslot = EquipmentSlot.HEAD;
-             ItemStack itemstack1 = this.getItemBySlot(enumitemslot);
-             double d0 = (double) this.getEquipmentDropChance(enumitemslot);
- 
-             if (!itemstack1.isEmpty() && (double) Math.max(this.random.nextFloat() - 0.1F, 0.0F) < d0) {
-+                this.forceDrops = true; // Paper - Add missing forceDrop toggles
-                 this.spawnAtLocation(world, itemstack1);
-+                this.forceDrops = false; // Paper - Add missing forceDrop toggles
-             }
- 
-             this.onItemPickup(itemEntity);
-             this.setItemSlot(enumitemslot, itemstack);
-             this.take(itemEntity, itemstack.getCount());
--            itemEntity.discard();
-+            itemEntity.discard(EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
-             this.getCurrentRaid().setLeader(this.getWave(), this);
-             this.setPatrolLeader(true);
-         } else {
-@@ -290,7 +300,7 @@
-         @Nullable
-         private ItemEntity pursuedBannerItemEntity;
- 
--        public ObtainRaidLeaderBannerGoal(final Raider entityraider) {
-+        public ObtainRaidLeaderBannerGoal(final T entityraider) { // CraftBukkit - decompile error
-             this.mob = entityraider;
-             this.setFlags(EnumSet.of(Goal.Flag.MOVE));
-         }
-@@ -335,6 +345,7 @@
-         }
- 
-         private boolean cannotPickUpBanner() {
-+            if (!getServerLevel(this.mob).getGameRules().getBoolean(net.minecraft.world.level.GameRules.RULE_MOBGRIEFING) || !this.mob.canPickUpLoot()) return false; // Paper - respect game and entity rules for picking up items
-             if (!this.mob.hasActiveRaid()) {
-                 return true;
-             } else if (this.mob.getCurrentRaid().isOver()) {
-@@ -518,7 +529,7 @@
-         }
-     }
- 
--    protected static class HoldGroundAttackGoal extends Goal {
-+    public static class HoldGroundAttackGoal extends Goal {
- 
-         private final Raider mob;
-         private final float hostileRadiusSqr;
-@@ -547,7 +558,7 @@
-             while (iterator.hasNext()) {
-                 Raider entityraider = (Raider) iterator.next();
- 
--                entityraider.setTarget(this.mob.getTarget());
-+                entityraider.setTarget(this.mob.getTarget(), org.bukkit.event.entity.EntityTargetEvent.TargetReason.FOLLOW_LEADER, true); // CraftBukkit
-             }
- 
-         }
-@@ -564,7 +575,7 @@
-                 while (iterator.hasNext()) {
-                     Raider entityraider = (Raider) iterator.next();
- 
--                    entityraider.setTarget(entityliving);
-+                    entityraider.setTarget(this.mob.getTarget(), org.bukkit.event.entity.EntityTargetEvent.TargetReason.FOLLOW_LEADER, true); // CraftBukkit
-                     entityraider.setAggressive(true);
-                 }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/AbstractBoat.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/AbstractBoat.java.patch
deleted file mode 100644
index 28b20b3791..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/AbstractBoat.java.patch
+++ /dev/null
@@ -1,125 +0,0 @@
---- a/net/minecraft/world/entity/vehicle/AbstractBoat.java
-+++ b/net/minecraft/world/entity/vehicle/AbstractBoat.java
-@@ -47,6 +47,14 @@
- import net.minecraft.world.phys.shapes.BooleanOp;
- import net.minecraft.world.phys.shapes.Shapes;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+// CraftBukkit start
-+import org.bukkit.Location;
-+import org.bukkit.craftbukkit.util.CraftLocation;
-+import org.bukkit.entity.Vehicle;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.event.vehicle.VehicleEntityCollisionEvent;
-+import org.bukkit.event.vehicle.VehicleMoveEvent;
-+// CraftBukkit end
- 
- public abstract class AbstractBoat extends VehicleEntity implements Leashable {
- 
-@@ -87,6 +95,14 @@
-     private Leashable.LeashData leashData;
-     private final Supplier<Item> dropItem;
- 
-+    // CraftBukkit start
-+    // PAIL: Some of these haven't worked since a few updates, and since 1.9 they are less and less applicable.
-+    public double maxSpeed = 0.4D;
-+    public double occupiedDeceleration = 0.2D;
-+    public double unoccupiedDeceleration = -1;
-+    public boolean landBoats = false;
-+    // CraftBukkit end
-+
-     public AbstractBoat(EntityType<? extends AbstractBoat> type, Level world, Supplier<Item> itemSupplier) {
-         super(type, world);
-         this.dropItem = itemSupplier;
-@@ -128,7 +144,7 @@
-     }
- 
-     @Override
--    public boolean isPushable() {
-+    public boolean isCollidable(boolean ignoreClimbing) { // Paper - Climbing should not bypass cramming gamerule
-         return true;
-     }
- 
-@@ -180,11 +196,32 @@
- 
-     @Override
-     public void push(Entity entity) {
-+        if (!this.level().paperConfig().collisions.allowVehicleCollisions && this.level().paperConfig().collisions.onlyPlayersCollide && !(entity instanceof Player)) return; // Paper - Collision option for requiring a player participant
-         if (entity instanceof AbstractBoat) {
-             if (entity.getBoundingBox().minY < this.getBoundingBox().maxY) {
-+                // CraftBukkit start
-+                if (!this.isPassengerOfSameVehicle(entity)) {
-+                    VehicleEntityCollisionEvent event = new VehicleEntityCollisionEvent((Vehicle) this.getBukkitEntity(), entity.getBukkitEntity());
-+                    this.level().getCraftServer().getPluginManager().callEvent(event);
-+
-+                    if (event.isCancelled()) {
-+                        return;
-+                    }
-+                }
-+                // CraftBukkit end
-                 super.push(entity);
-             }
-         } else if (entity.getBoundingBox().minY <= this.getBoundingBox().minY) {
-+            // CraftBukkit start
-+            if (!this.isPassengerOfSameVehicle(entity)) {
-+                VehicleEntityCollisionEvent event = new VehicleEntityCollisionEvent((Vehicle) this.getBukkitEntity(), entity.getBukkitEntity());
-+                this.level().getCraftServer().getPluginManager().callEvent(event);
-+
-+                if (event.isCancelled()) {
-+                    return;
-+                }
-+            }
-+            // CraftBukkit end
-             super.push(entity);
-         }
- 
-@@ -247,6 +284,7 @@
-         return this.getDirection().getClockWise();
-     }
- 
-+    private Location lastLocation; // CraftBukkit
-     @Override
-     public void tick() {
-         this.oldStatus = this.status;
-@@ -287,6 +325,21 @@
-             this.setDeltaMovement(Vec3.ZERO);
-         }
- 
-+        // CraftBukkit start
-+        org.bukkit.Server server = this.level().getCraftServer();
-+        org.bukkit.World bworld = this.level().getWorld();
-+
-+        Location to = CraftLocation.toBukkit(this.position(), bworld, this.getYRot(), this.getXRot());
-+        Vehicle vehicle = (Vehicle) this.getBukkitEntity();
-+
-+        server.getPluginManager().callEvent(new org.bukkit.event.vehicle.VehicleUpdateEvent(vehicle));
-+
-+        if (this.lastLocation != null && !this.lastLocation.equals(to)) {
-+            VehicleMoveEvent event = new VehicleMoveEvent(vehicle, this.lastLocation, to);
-+            server.getPluginManager().callEvent(event);
-+        }
-+        this.lastLocation = vehicle.getLocation();
-+        // CraftBukkit end
-         this.applyEffectsFromBlocks();
-         this.applyEffectsFromBlocks();
-         this.tickBubbleColumn();
-@@ -790,11 +843,18 @@
- 
-     @Override
-     public void remove(Entity.RemovalReason reason) {
--        if (!this.level().isClientSide && reason.shouldDestroy() && this.isLeashed()) {
-+        // CraftBukkit start - add Bukkit remove cause
-+        this.remove(reason, null);
-+    }
-+
-+    @Override
-+    public void remove(Entity.RemovalReason entity_removalreason, EntityRemoveEvent.Cause cause) {
-+        // CraftBukkit end
-+        if (!this.level().isClientSide && entity_removalreason.shouldDestroy() && this.isLeashed()) {
-             this.dropLeash();
-         }
- 
--        super.remove(reason);
-+        super.remove(entity_removalreason, cause); // CraftBukkit - add Bukkit remove cause
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/AbstractChestBoat.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/AbstractChestBoat.java.patch
deleted file mode 100644
index f23f862b79..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/AbstractChestBoat.java.patch
+++ /dev/null
@@ -1,121 +0,0 @@
---- a/net/minecraft/world/entity/vehicle/AbstractChestBoat.java
-+++ b/net/minecraft/world/entity/vehicle/AbstractChestBoat.java
-@@ -27,6 +27,15 @@
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.level.storage.loot.LootTable;
- 
-+// CraftBukkit start
-+import java.util.List;
-+import org.bukkit.Location;
-+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
-+import org.bukkit.entity.HumanEntity;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.inventory.InventoryHolder;
-+// CraftBukkit end
-+
- public abstract class AbstractChestBoat extends AbstractBoat implements HasCustomInventoryScreen, ContainerEntity {
- 
-     private static final int CONTAINER_SIZE = 27;
-@@ -70,11 +79,18 @@
- 
-     @Override
-     public void remove(Entity.RemovalReason reason) {
--        if (!this.level().isClientSide && reason.shouldDestroy()) {
-+        // CraftBukkit start - add Bukkit remove cause
-+        this.remove(reason, null);
-+    }
-+
-+    @Override
-+    public void remove(Entity.RemovalReason entity_removalreason, EntityRemoveEvent.Cause cause) {
-+        // CraftBukkit end
-+        if (!this.level().isClientSide && entity_removalreason.shouldDestroy()) {
-             Containers.dropContents(this.level(), (Entity) this, (Container) this);
-         }
- 
--        super.remove(reason);
-+        super.remove(entity_removalreason, cause); // CraftBukkit - add Bukkit remove cause
-     }
- 
-     @Override
-@@ -109,10 +125,10 @@
- 
-     @Override
-     public void openCustomInventoryScreen(Player player) {
--        player.openMenu(this);
-+        // Paper - fix inventory open cancel - moved into below if
-         Level world = player.level();
- 
--        if (world instanceof ServerLevel worldserver) {
-+        if (world instanceof ServerLevel worldserver && player.openMenu(this).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
-             this.gameEvent(GameEvent.CONTAINER_OPEN, player);
-             PiglinAi.angerNearbyPiglins(worldserver, player, true);
-         }
-@@ -165,7 +181,7 @@
-     @Nullable
-     @Override
-     public AbstractContainerMenu createMenu(int syncId, Inventory playerInventory, Player player) {
--        if (this.lootTable != null && player.isSpectator()) {
-+        if (this.lootTable != null && player.isSpectator()) { // Paper - LootTable API (TODO spectators can open chests that aren't ready to be re-generated but this doesn't support that)
-             return null;
-         } else {
-             this.unpackLootTable(playerInventory.player);
-@@ -212,4 +228,59 @@
-     public void stopOpen(Player player) {
-         this.level().gameEvent((Holder) GameEvent.CONTAINER_CLOSE, this.position(), GameEvent.Context.of((Entity) player));
-     }
-+
-+    // Paper start - LootTable API
-+    final com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData = new com.destroystokyo.paper.loottable.PaperLootableInventoryData();
-+
-+    @Override
-+    public com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData() {
-+        return this.lootableData;
-+    }
-+    // Paper end - LootTable API
-+    // CraftBukkit start
-+    public List<HumanEntity> transaction = new java.util.ArrayList<HumanEntity>();
-+    private int maxStack = MAX_STACK;
-+
-+    @Override
-+    public List<ItemStack> getContents() {
-+        return this.itemStacks;
-+    }
-+
-+    @Override
-+    public void onOpen(CraftHumanEntity who) {
-+        this.transaction.add(who);
-+    }
-+
-+    @Override
-+    public void onClose(CraftHumanEntity who) {
-+        this.transaction.remove(who);
-+    }
-+
-+    @Override
-+    public List<HumanEntity> getViewers() {
-+        return this.transaction;
-+    }
-+
-+    @Override
-+    public InventoryHolder getOwner() {
-+        org.bukkit.entity.Entity entity = this.getBukkitEntity();
-+        if (entity instanceof InventoryHolder) return (InventoryHolder) entity;
-+        return null;
-+    }
-+
-+    @Override
-+    public int getMaxStackSize() {
-+        return this.maxStack;
-+    }
-+
-+    @Override
-+    public void setMaxStackSize(int size) {
-+        this.maxStack = size;
-+    }
-+
-+    @Override
-+    public Location getLocation() {
-+        return this.getBukkitEntity().getLocation();
-+    }
-+    // CraftBukkit end
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java.patch
deleted file mode 100644
index b33136b346..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java.patch
+++ /dev/null
@@ -1,98 +0,0 @@
---- a/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java
-+++ b/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java
-@@ -20,6 +20,14 @@
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.storage.loot.LootTable;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import java.util.List;
-+import org.bukkit.Location;
-+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
-+import org.bukkit.entity.HumanEntity;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.inventory.InventoryHolder;
-+// CraftBukkit end
- 
- public abstract class AbstractMinecartContainer extends AbstractMinecart implements ContainerEntity {
- 
-@@ -28,9 +36,58 @@
-     public ResourceKey<LootTable> lootTable;
-     public long lootTableSeed;
- 
-+    // Paper start - LootTable API
-+    final com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData = new com.destroystokyo.paper.loottable.PaperLootableInventoryData();
-+
-+    @Override
-+    public com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData() {
-+        return this.lootableData;
-+    }
-+    // Paper end - LootTable API
-+    // CraftBukkit start
-+    public List<HumanEntity> transaction = new java.util.ArrayList<HumanEntity>();
-+    private int maxStack = MAX_STACK;
-+
-+    public List<ItemStack> getContents() {
-+        return this.itemStacks;
-+    }
-+
-+    public void onOpen(CraftHumanEntity who) {
-+        this.transaction.add(who);
-+    }
-+
-+    public void onClose(CraftHumanEntity who) {
-+        this.transaction.remove(who);
-+    }
-+
-+    public List<HumanEntity> getViewers() {
-+        return this.transaction;
-+    }
-+
-+    public InventoryHolder getOwner() {
-+        org.bukkit.entity.Entity cart = this.getBukkitEntity();
-+        if(cart instanceof InventoryHolder) return (InventoryHolder) cart;
-+        return null;
-+    }
-+
-+    @Override
-+    public int getMaxStackSize() {
-+        return this.maxStack;
-+    }
-+
-+    public void setMaxStackSize(int size) {
-+        this.maxStack = size;
-+    }
-+
-+    @Override
-+    public Location getLocation() {
-+        return this.getBukkitEntity().getLocation();
-+    }
-+    // CraftBukkit end
-+
-     protected AbstractMinecartContainer(EntityType<?> type, Level world) {
-         super(type, world);
--        this.itemStacks = NonNullList.withSize(36, ItemStack.EMPTY);
-+        this.itemStacks = NonNullList.withSize(this.getContainerSize(), ItemStack.EMPTY); // CraftBukkit - SPIGOT-3513
-     }
- 
-     @Override
-@@ -74,11 +131,18 @@
- 
-     @Override
-     public void remove(Entity.RemovalReason reason) {
--        if (!this.level().isClientSide && reason.shouldDestroy()) {
-+        // CraftBukkit start - add Bukkit remove cause
-+        this.remove(reason, null);
-+    }
-+
-+    @Override
-+    public void remove(Entity.RemovalReason entity_removalreason, EntityRemoveEvent.Cause cause) {
-+        // CraftBukkit end
-+        if (!this.level().isClientSide && entity_removalreason.shouldDestroy()) {
-             Containers.dropContents(this.level(), (Entity) this, (Container) this);
-         }
- 
--        super.remove(reason);
-+        super.remove(entity_removalreason, cause); // CraftBukkit - add Bukkit remove cause
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java.patch
deleted file mode 100644
index 7c789e80ad..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java.patch
+++ /dev/null
@@ -1,23 +0,0 @@
---- a/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java
-+++ b/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java
-@@ -128,12 +128,19 @@
- 
-         @Override
-         public CommandSourceStack createCommandSourceStack() {
--            return new CommandSourceStack(this, MinecartCommandBlock.this.position(), MinecartCommandBlock.this.getRotationVector(), this.getLevel(), 2, this.getName().getString(), MinecartCommandBlock.this.getDisplayName(), this.getLevel().getServer(), MinecartCommandBlock.this);
-+            return new CommandSourceStack(this, MinecartCommandBlock.this.position(), MinecartCommandBlock.this.getRotationVector(), this.getLevel(), this.getLevel().paperConfig().commandBlocks.permissionsLevel, this.getName().getString(), MinecartCommandBlock.this.getDisplayName(), this.getLevel().getServer(), MinecartCommandBlock.this); // Paper - configurable command block perm level
-         }
- 
-         @Override
-         public boolean isValid() {
-             return !MinecartCommandBlock.this.isRemoved();
-         }
-+
-+        // CraftBukkit start
-+        @Override
-+        public org.bukkit.command.CommandSender getBukkitSender(CommandSourceStack wrapper) {
-+            return (org.bukkit.craftbukkit.entity.CraftMinecartCommand) MinecartCommandBlock.this.getBukkitEntity();
-+        }
-+        // CraftBukkit end
-     }
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/MinecartTNT.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/MinecartTNT.java.patch
deleted file mode 100644
index 7fdf216d8a..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/MinecartTNT.java.patch
+++ /dev/null
@@ -1,53 +0,0 @@
---- a/net/minecraft/world/entity/vehicle/MinecartTNT.java
-+++ b/net/minecraft/world/entity/vehicle/MinecartTNT.java
-@@ -25,6 +25,10 @@
- import net.minecraft.world.level.block.Blocks;
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.material.FluidState;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.event.entity.ExplosionPrimeEvent;
-+// CraftBukkit end
- 
- public class MinecartTNT extends AbstractMinecart {
- 
-@@ -37,6 +41,7 @@
-     public int fuse = -1;
-     public float explosionPowerBase = 4.0F;
-     public float explosionSpeedFactor = 1.0F;
-+    public boolean isIncendiary = false; // CraftBukkit - add field
- 
-     public MinecartTNT(EntityType<? extends MinecartTNT> type, Level world) {
-         super(type, world);
-@@ -51,6 +56,12 @@
-     public void tick() {
-         super.tick();
-         if (this.fuse > 0) {
-+            // Paper start - Configurable TNT height nerf
-+            if (this.level().paperConfig().fixes.tntEntityHeightNerf.test(v -> this.getY() > v)) {
-+                this.discard(EntityRemoveEvent.Cause.OUT_OF_WORLD);
-+                return;
-+            }
-+            // Paper end - Configurable TNT height nerf
-             --this.fuse;
-             this.level().addParticle(ParticleTypes.SMOKE, this.getX(), this.getY() + 0.5D, this.getZ(), 0.0D, 0.0D, 0.0D);
-         } else if (this.fuse == 0) {
-@@ -117,8 +128,16 @@
-         if (world instanceof ServerLevel worldserver) {
-             double d1 = Math.min(Math.sqrt(power), 5.0D);
- 
--            worldserver.explode(this, damageSource, (ExplosionDamageCalculator) null, this.getX(), this.getY(), this.getZ(), (float) ((double) this.explosionPowerBase + (double) this.explosionSpeedFactor * this.random.nextDouble() * 1.5D * d1), false, Level.ExplosionInteraction.TNT);
--            this.discard();
-+            // CraftBukkit start
-+            ExplosionPrimeEvent event = new ExplosionPrimeEvent(this.getBukkitEntity(), (float) ((double) this.explosionPowerBase + (double) this.explosionSpeedFactor * this.random.nextDouble() * 1.5D * d1), this.isIncendiary);
-+            worldserver.getCraftServer().getPluginManager().callEvent(event);
-+            if (event.isCancelled()) {
-+                this.fuse = -1;
-+                return;
-+            }
-+            worldserver.explode(this, damageSource, (ExplosionDamageCalculator) null, this.getX(), this.getY(), this.getZ(), event.getRadius(), event.getFire(), Level.ExplosionInteraction.TNT);
-+            // CraftBukkit end
-+            this.discard(EntityRemoveEvent.Cause.EXPLODE); // CraftBukkit - add Bukkit remove cause
-         }
- 
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/NewMinecartBehavior.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/NewMinecartBehavior.java.patch
deleted file mode 100644
index 8bed619b55..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/NewMinecartBehavior.java.patch
+++ /dev/null
@@ -1,83 +0,0 @@
---- a/net/minecraft/world/entity/vehicle/NewMinecartBehavior.java
-+++ b/net/minecraft/world/entity/vehicle/NewMinecartBehavior.java
-@@ -27,6 +27,10 @@
- import net.minecraft.world.level.block.state.properties.RailShape;
- import net.minecraft.world.phys.AABB;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.entity.Vehicle;
-+import org.bukkit.event.vehicle.VehicleEntityCollisionEvent;
-+// CraftBukkit end
- 
- public class NewMinecartBehavior extends MinecartBehavior {
- 
-@@ -516,6 +520,12 @@
- 
-     @Override
-     public double getMaxSpeed(ServerLevel world) {
-+        // CraftBukkit start
-+        Double maxSpeed = this.minecart.maxSpeed;
-+        if (maxSpeed != null) {
-+            return (this.minecart.isInWater() ? maxSpeed / 2.0D : maxSpeed);
-+        }
-+        // CraftBukkit end
-         return (double) world.getGameRules().getInt(GameRules.RULE_MINECART_MAX_SPEED) * (this.minecart.isInWater() ? 0.5D : 1.0D) / 20.0D;
-     }
- 
-@@ -544,7 +554,8 @@
- 
-     @Override
-     public double getSlowdownFactor() {
--        return this.minecart.isVehicle() ? 0.997D : 0.975D;
-+        if (this.minecart.frictionState == net.kyori.adventure.util.TriState.FALSE) return 1; // Paper
-+        return this.minecart.isVehicle() || !this.minecart.slowWhenEmpty ? 0.997D : 0.975D; // CraftBukkit - add !this.slowWhenEmpty
-     }
- 
-     @Override
-@@ -571,6 +582,14 @@
-                     Entity entity = (Entity) iterator.next();
- 
-                     if (!(entity instanceof Player) && !(entity instanceof IronGolem) && !(entity instanceof AbstractMinecart) && !this.minecart.isVehicle() && !entity.isPassenger()) {
-+                        // CraftBukkit start
-+                        VehicleEntityCollisionEvent collisionEvent = new VehicleEntityCollisionEvent((Vehicle) this.minecart.getBukkitEntity(), entity.getBukkitEntity());
-+                        this.level().getCraftServer().getPluginManager().callEvent(collisionEvent);
-+
-+                        if (collisionEvent.isCancelled()) {
-+                            continue;
-+                        }
-+                        // CraftBukkit end
-                         boolean flag = entity.startRiding(this.minecart);
- 
-                         if (flag) {
-@@ -597,6 +616,16 @@
-                     Entity entity = (Entity) iterator.next();
- 
-                     if (entity instanceof Player || entity instanceof IronGolem || entity instanceof AbstractMinecart || this.minecart.isVehicle() || entity.isPassenger()) {
-+                        // CraftBukkit start
-+                        if (!this.minecart.isPassengerOfSameVehicle(entity)) {
-+                            VehicleEntityCollisionEvent collisionEvent = new VehicleEntityCollisionEvent((Vehicle) this.minecart.getBukkitEntity(), entity.getBukkitEntity());
-+                            this.level().getCraftServer().getPluginManager().callEvent(collisionEvent);
-+
-+                            if (collisionEvent.isCancelled()) {
-+                                continue;
-+                            }
-+                        }
-+                        // CraftBukkit end
-                         entity.push((Entity) this.minecart);
-                         flag = true;
-                     }
-@@ -609,6 +638,14 @@
-                 Entity entity1 = (Entity) iterator1.next();
- 
-                 if (!this.minecart.hasPassenger(entity1) && entity1.isPushable() && entity1 instanceof AbstractMinecart) {
-+                    // CraftBukkit start
-+                    VehicleEntityCollisionEvent collisionEvent = new VehicleEntityCollisionEvent((Vehicle) this.minecart.getBukkitEntity(), entity1.getBukkitEntity());
-+                    this.level().getCraftServer().getPluginManager().callEvent(collisionEvent);
-+
-+                    if (collisionEvent.isCancelled()) {
-+                        continue;
-+                    }
-+                    // CraftBukkit end
-                     entity1.push((Entity) this.minecart);
-                     flag = true;
-                 }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/OldMinecartBehavior.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/OldMinecartBehavior.java.patch
deleted file mode 100644
index 5f791f5901..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/OldMinecartBehavior.java.patch
+++ /dev/null
@@ -1,75 +0,0 @@
---- a/net/minecraft/world/entity/vehicle/OldMinecartBehavior.java
-+++ b/net/minecraft/world/entity/vehicle/OldMinecartBehavior.java
-@@ -24,6 +24,10 @@
- import net.minecraft.world.level.block.state.properties.RailShape;
- import net.minecraft.world.phys.AABB;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.entity.Vehicle;
-+import org.bukkit.event.vehicle.VehicleEntityCollisionEvent;
-+// CraftBukkit end
- 
- public class OldMinecartBehavior extends MinecartBehavior {
- 
-@@ -454,8 +458,26 @@
-                     Entity entity = (Entity) iterator.next();
- 
-                     if (!(entity instanceof Player) && !(entity instanceof IronGolem) && !(entity instanceof AbstractMinecart) && !this.minecart.isVehicle() && !entity.isPassenger()) {
-+                        // CraftBukkit start
-+                        VehicleEntityCollisionEvent collisionEvent = new VehicleEntityCollisionEvent((Vehicle) this.minecart.getBukkitEntity(), entity.getBukkitEntity());
-+                        this.level().getCraftServer().getPluginManager().callEvent(collisionEvent);
-+
-+                        if (collisionEvent.isCancelled()) {
-+                            continue;
-+                        }
-+                        // CraftBukkit end
-                         entity.startRiding(this.minecart);
-                     } else {
-+                        // CraftBukkit start
-+                        if (!this.minecart.isPassengerOfSameVehicle(entity)) {
-+                            VehicleEntityCollisionEvent collisionEvent = new VehicleEntityCollisionEvent((Vehicle) this.minecart.getBukkitEntity(), entity.getBukkitEntity());
-+                            this.level().getCraftServer().getPluginManager().callEvent(collisionEvent);
-+
-+                            if (collisionEvent.isCancelled()) {
-+                                continue;
-+                            }
-+                        }
-+                        // CraftBukkit end
-                         entity.push((Entity) this.minecart);
-                     }
-                 }
-@@ -467,6 +489,14 @@
-                 Entity entity1 = (Entity) iterator1.next();
- 
-                 if (!this.minecart.hasPassenger(entity1) && entity1.isPushable() && entity1 instanceof AbstractMinecart) {
-+                    // CraftBukkit start
-+                    VehicleEntityCollisionEvent collisionEvent = new VehicleEntityCollisionEvent((Vehicle) this.minecart.getBukkitEntity(), entity1.getBukkitEntity());
-+                    this.level().getCraftServer().getPluginManager().callEvent(collisionEvent);
-+
-+                    if (collisionEvent.isCancelled()) {
-+                        continue;
-+                    }
-+                    // CraftBukkit end
-                     entity1.push((Entity) this.minecart);
-                 }
-             }
-@@ -487,11 +517,18 @@
- 
-     @Override
-     public double getMaxSpeed(ServerLevel world) {
-+        // CraftBukkit start
-+        Double maxSpeed = this.minecart.maxSpeed;
-+        if (maxSpeed != null) {
-+            return (this.minecart.isInWater() ? maxSpeed / 2.0D : maxSpeed);
-+        }
-+        // CraftBukkit end
-         return this.minecart.isInWater() ? 0.2D : 0.4D;
-     }
- 
-     @Override
-     public double getSlowdownFactor() {
--        return this.minecart.isVehicle() ? 0.997D : 0.96D;
-+        if (this.minecart.frictionState == net.kyori.adventure.util.TriState.FALSE) return 1; // Paper
-+        return this.minecart.isVehicle() || !this.minecart.slowWhenEmpty ? 0.997D : 0.96D; // CraftBukkit - add !this.slowWhenEmpty
-     }
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/VehicleEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/VehicleEntity.java.patch
deleted file mode 100644
index 10fce15f79..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/VehicleEntity.java.patch
+++ /dev/null
@@ -1,64 +0,0 @@
---- a/net/minecraft/world/entity/vehicle/VehicleEntity.java
-+++ b/net/minecraft/world/entity/vehicle/VehicleEntity.java
-@@ -17,6 +17,13 @@
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.gameevent.GameEvent;
- 
-+// CraftBukkit start
-+import org.bukkit.entity.Vehicle;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.event.vehicle.VehicleDamageEvent;
-+import org.bukkit.event.vehicle.VehicleDestroyEvent;
-+// CraftBukkit end
-+
- public abstract class VehicleEntity extends Entity {
- 
-     protected static final EntityDataAccessor<Integer> DATA_ID_HURT = SynchedEntityData.defineId(VehicleEntity.class, EntityDataSerializers.INT);
-@@ -40,6 +47,18 @@
-             return false;
-         } else {
-             boolean flag;
-+            // CraftBukkit start
-+            Vehicle vehicle = (Vehicle) this.getBukkitEntity();
-+            org.bukkit.entity.Entity attacker = (source.getEntity() == null) ? null : source.getEntity().getBukkitEntity();
-+
-+            VehicleDamageEvent event = new VehicleDamageEvent(vehicle, attacker, (double) amount);
-+            this.level().getCraftServer().getPluginManager().callEvent(event);
-+
-+            if (event.isCancelled()) {
-+                return false;
-+            }
-+            amount = (float) event.getDamage();
-+            // CraftBukkit end
-             label32:
-             {
-                 this.setHurtDir(-this.getHurtDir());
-@@ -65,9 +84,27 @@
- 
-             if ((flag1 || this.getDamage() <= 40.0F) && !this.shouldSourceDestroy(source)) {
-                 if (flag1) {
--                    this.discard();
-+                    // CraftBukkit start
-+                    VehicleDestroyEvent destroyEvent = new VehicleDestroyEvent(vehicle, attacker);
-+                    this.level().getCraftServer().getPluginManager().callEvent(destroyEvent);
-+
-+                    if (destroyEvent.isCancelled()) {
-+                        this.setDamage(40.0F); // Maximize damage so this doesn't get triggered again right away
-+                        return true;
-+                    }
-+                    // CraftBukkit end
-+                    this.discard(EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause
-                 }
-             } else {
-+                // CraftBukkit start
-+                VehicleDestroyEvent destroyEvent = new VehicleDestroyEvent(vehicle, attacker);
-+                this.level().getCraftServer().getPluginManager().callEvent(destroyEvent);
-+
-+                if (destroyEvent.isCancelled()) {
-+                    this.setDamage(40.0F); // Maximize damage so this doesn't get triggered again right away
-+                    return true;
-+                }
-+                // CraftBukkit end
-                 this.destroy(world, source);
-             }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/food/FoodProperties.java.patch b/paper-server/patches/unapplied/net/minecraft/world/food/FoodProperties.java.patch
deleted file mode 100644
index a8c85757de..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/food/FoodProperties.java.patch
+++ /dev/null
@@ -1,19 +0,0 @@
---- a/net/minecraft/world/food/FoodProperties.java
-+++ b/net/minecraft/world/food/FoodProperties.java
-@@ -5,6 +5,7 @@
- import net.minecraft.network.RegistryFriendlyByteBuf;
- import net.minecraft.network.codec.ByteBufCodecs;
- import net.minecraft.network.codec.StreamCodec;
-+import net.minecraft.server.level.ServerPlayer;
- import net.minecraft.sounds.SoundEvent;
- import net.minecraft.sounds.SoundEvents;
- import net.minecraft.sounds.SoundSource;
-@@ -31,7 +32,7 @@
- 
-         world.playSound((Player) null, user.getX(), user.getY(), user.getZ(), (SoundEvent) consumable.sound().value(), SoundSource.NEUTRAL, 1.0F, randomsource.triangle(1.0F, 0.4F));
-         if (user instanceof Player entityhuman) {
--            entityhuman.getFoodData().eat(this);
-+            entityhuman.getFoodData().eat(this, stack, (ServerPlayer) entityhuman); // CraftBukkit
-             world.playSound((Player) null, entityhuman.getX(), entityhuman.getY(), entityhuman.getZ(), SoundEvents.PLAYER_BURP, SoundSource.PLAYERS, 0.5F, Mth.randomBetween(randomsource, 0.9F, 1.0F));
-         }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/AbstractContainerMenu.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/AbstractContainerMenu.java.patch
deleted file mode 100644
index a0c2f5bf6b..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/AbstractContainerMenu.java.patch
+++ /dev/null
@@ -1,310 +0,0 @@
---- a/net/minecraft/world/inventory/AbstractContainerMenu.java
-+++ b/net/minecraft/world/inventory/AbstractContainerMenu.java
-@@ -21,6 +21,8 @@
- import net.minecraft.ReportedException;
- import net.minecraft.core.NonNullList;
- import net.minecraft.core.registries.BuiltInRegistries;
-+import net.minecraft.network.chat.Component;
-+import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket;
- import net.minecraft.server.level.ServerPlayer;
- import net.minecraft.util.Mth;
- import net.minecraft.world.Container;
-@@ -35,6 +37,18 @@
- import net.minecraft.world.level.block.entity.BlockEntity;
- import org.slf4j.Logger;
- 
-+// CraftBukkit start
-+import com.google.common.base.Preconditions;
-+import java.util.HashMap;
-+import java.util.Map;
-+import org.bukkit.craftbukkit.inventory.CraftInventory;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.event.Event.Result;
-+import org.bukkit.event.inventory.InventoryDragEvent;
-+import org.bukkit.event.inventory.InventoryType;
-+import org.bukkit.inventory.InventoryView;
-+// CraftBukkit end
-+
- public abstract class AbstractContainerMenu {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
-@@ -67,6 +81,32 @@
-     private ContainerSynchronizer synchronizer;
-     private boolean suppressRemoteUpdates;
- 
-+    // CraftBukkit start
-+    public boolean checkReachable = true;
-+    public abstract InventoryView getBukkitView();
-+    public void transferTo(AbstractContainerMenu other, org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
-+        InventoryView source = this.getBukkitView(), destination = other.getBukkitView();
-+        ((CraftInventory) source.getTopInventory()).getInventory().onClose(player);
-+        ((CraftInventory) source.getBottomInventory()).getInventory().onClose(player);
-+        ((CraftInventory) destination.getTopInventory()).getInventory().onOpen(player);
-+        ((CraftInventory) destination.getBottomInventory()).getInventory().onOpen(player);
-+    }
-+    private Component title;
-+    public final Component getTitle() {
-+        // Paper start - return chat component with empty text instead of throwing error
-+        // Preconditions.checkState(this.title != null, "Title not set");
-+        if (this.title == null){
-+            return Component.literal("");
-+        }
-+        // Paper end - return chat component with empty text instead of throwing error
-+        return this.title;
-+    }
-+    public final void setTitle(Component title) {
-+        Preconditions.checkState(this.title == null, "Title already set");
-+        this.title = title;
-+    }
-+    // CraftBukkit end
-+
-     protected AbstractContainerMenu(@Nullable MenuType<?> type, int syncId) {
-         this.carried = ItemStack.EMPTY;
-         this.remoteSlots = NonNullList.create();
-@@ -188,10 +228,20 @@
- 
-         if (this.synchronizer != null) {
-             this.synchronizer.sendInitialData(this, this.remoteSlots, this.remoteCarried, this.remoteDataSlots.toIntArray());
-+            this.synchronizer.sendOffHandSlotChange(); // Paper - Sync offhand slot in menus; update player's offhand since the offhand slot is not added to the slots for menus but can be changed by swapping from a menu slot
-         }
- 
-     }
- 
-+    // CraftBukkit start
-+    public void broadcastCarriedItem() {
-+        this.remoteCarried = this.getCarried().copy();
-+        if (this.synchronizer != null) {
-+            this.synchronizer.sendCarriedChange(this, this.remoteCarried);
-+        }
-+    }
-+    // CraftBukkit end
-+
-     public void removeSlotListener(ContainerListener listener) {
-         this.containerListeners.remove(listener);
-     }
-@@ -281,7 +331,7 @@
-             while (iterator.hasNext()) {
-                 ContainerListener icrafting = (ContainerListener) iterator.next();
- 
--                icrafting.slotChanged(this, slot, itemstack2);
-+                icrafting.slotChanged(this, slot, itemstack1, itemstack2); // Paper - Add PlayerInventorySlotChangeEvent
-             }
-         }
- 
-@@ -410,6 +460,7 @@
-                     this.resetQuickCraft();
-                 }
-             } else if (this.quickcraftStatus == 1) {
-+                if (slotIndex < 0) return; // Paper - Add slot sanity checks to container clicks
-                 slot = (Slot) this.slots.get(slotIndex);
-                 itemstack = this.getCarried();
-                 if (AbstractContainerMenu.canItemQuickReplace(slot, itemstack, true) && slot.mayPlace(itemstack) && (this.quickcraftType == 2 || itemstack.getCount() > this.quickcraftSlots.size()) && this.canDragTo(slot)) {
-@@ -417,7 +468,7 @@
-                 }
-             } else if (this.quickcraftStatus == 2) {
-                 if (!this.quickcraftSlots.isEmpty()) {
--                    if (this.quickcraftSlots.size() == 1) {
-+                    if (this.quickcraftSlots.size() == 1) { // Paper - Fix CraftBukkit drag system
-                         k = ((Slot) this.quickcraftSlots.iterator().next()).index;
-                         this.resetQuickCraft();
-                         this.doClick(k, this.quickcraftType, ClickType.PICKUP, player);
-@@ -433,6 +484,7 @@
-                     l = this.getCarried().getCount();
-                     Iterator iterator = this.quickcraftSlots.iterator();
- 
-+                    Map<Integer, ItemStack> draggedSlots = new HashMap<Integer, ItemStack>(); // CraftBukkit - Store slots from drag in map (raw slot id -> new stack)
-                     while (iterator.hasNext()) {
-                         Slot slot1 = (Slot) iterator.next();
-                         ItemStack itemstack2 = this.getCarried();
-@@ -443,12 +495,48 @@
-                             int l1 = Math.min(AbstractContainerMenu.getQuickCraftPlaceCount(this.quickcraftSlots, this.quickcraftType, itemstack1) + j1, k1);
- 
-                             l -= l1 - j1;
--                            slot1.setByPlayer(itemstack1.copyWithCount(l1));
-+                            // slot1.setByPlayer(itemstack1.copyWithCount(l1));
-+                            draggedSlots.put(slot1.index, itemstack1.copyWithCount(l1)); // CraftBukkit - Put in map instead of setting
-                         }
-                     }
- 
--                    itemstack1.setCount(l);
--                    this.setCarried(itemstack1);
-+                    // CraftBukkit start - InventoryDragEvent
-+                    InventoryView view = this.getBukkitView();
-+                    org.bukkit.inventory.ItemStack newcursor = CraftItemStack.asCraftMirror(itemstack1);
-+                    newcursor.setAmount(l);
-+                    Map<Integer, org.bukkit.inventory.ItemStack> eventmap = new HashMap<Integer, org.bukkit.inventory.ItemStack>();
-+                    for (Map.Entry<Integer, ItemStack> ditem : draggedSlots.entrySet()) {
-+                        eventmap.put(ditem.getKey(), CraftItemStack.asBukkitCopy(ditem.getValue()));
-+                    }
-+
-+                    // It's essential that we set the cursor to the new value here to prevent item duplication if a plugin closes the inventory.
-+                    ItemStack oldCursor = this.getCarried();
-+                    this.setCarried(CraftItemStack.asNMSCopy(newcursor));
-+
-+                    InventoryDragEvent event = new InventoryDragEvent(view, (newcursor.getType() != org.bukkit.Material.AIR ? newcursor : null), CraftItemStack.asBukkitCopy(oldCursor), this.quickcraftType == 1, eventmap);
-+                    player.level().getCraftServer().getPluginManager().callEvent(event);
-+
-+                    // Whether or not a change was made to the inventory that requires an update.
-+                    boolean needsUpdate = event.getResult() != Result.DEFAULT;
-+
-+                    if (event.getResult() != Result.DENY) {
-+                        for (Map.Entry<Integer, ItemStack> dslot : draggedSlots.entrySet()) {
-+                            view.setItem(dslot.getKey(), CraftItemStack.asBukkitCopy(dslot.getValue()));
-+                        }
-+                        // The only time the carried item will be set to null is if the inventory is closed by the server.
-+                        // If the inventory is closed by the server, then the cursor items are dropped.  This is why we change the cursor early.
-+                        if (this.getCarried() != null) {
-+                            this.setCarried(CraftItemStack.asNMSCopy(event.getCursor()));
-+                            needsUpdate = true;
-+                        }
-+                    } else {
-+                        this.setCarried(oldCursor);
-+                    }
-+
-+                    if (needsUpdate && player instanceof ServerPlayer) {
-+                        this.sendAllDataToRemote();
-+                    }
-+                    // CraftBukkit end
-                 }
- 
-                 this.resetQuickCraft();
-@@ -466,8 +554,11 @@
-                 if (slotIndex == -999) {
-                     if (!this.getCarried().isEmpty()) {
-                         if (clickaction == ClickAction.PRIMARY) {
--                            player.drop(this.getCarried(), true);
-+                            // CraftBukkit start
-+                            ItemStack carried = this.getCarried();
-                             this.setCarried(ItemStack.EMPTY);
-+                            player.drop(carried, true);
-+                            // CraftBukkit start
-                         } else {
-                             player.drop(this.getCarried().split(1), true);
-                         }
-@@ -530,11 +621,21 @@
-                     }
- 
-                     slot.setChanged();
-+                    // CraftBukkit start - Make sure the client has the right slot contents
-+                    if (player instanceof ServerPlayer && slot.getMaxStackSize() != 64) {
-+                        ((ServerPlayer) player).connection.send(new ClientboundContainerSetSlotPacket(this.containerId, this.incrementStateId(), slot.index, slot.getItem()));
-+                        // Updating a crafting inventory makes the client reset the result slot, have to send it again
-+                        if (this.getBukkitView().getType() == InventoryType.WORKBENCH || this.getBukkitView().getType() == InventoryType.CRAFTING) {
-+                            ((ServerPlayer) player).connection.send(new ClientboundContainerSetSlotPacket(this.containerId, this.incrementStateId(), 0, this.getSlot(0).getItem()));
-+                        }
-+                    }
-+                    // CraftBukkit end
-                 }
-             } else {
-                 int j2;
- 
-                 if (actionType == ClickType.SWAP && (button >= 0 && button < 9 || button == 40)) {
-+                    if (slotIndex < 0) return; // Paper - Add slot sanity checks to container clicks
-                     ItemStack itemstack4 = playerinventory.getItem(button);
- 
-                     slot = (Slot) this.slots.get(slotIndex);
-@@ -662,8 +763,9 @@
-             ItemStack itemstack = this.getCarried();
- 
-             if (!itemstack.isEmpty()) {
-+                this.setCarried(ItemStack.EMPTY); // CraftBukkit - SPIGOT-4556 - from below
-                 AbstractContainerMenu.dropOrPlaceInInventory(player, itemstack);
--                this.setCarried(ItemStack.EMPTY);
-+                // this.setCarried(ItemStack.EMPTY); // CraftBukkit - moved up
-             }
- 
-         }
-@@ -729,6 +831,14 @@
-     public abstract boolean stillValid(Player player);
- 
-     protected boolean moveItemStackTo(ItemStack stack, int startIndex, int endIndex, boolean fromLast) {
-+        // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent
-+        return this.moveItemStackTo(stack, startIndex, endIndex, fromLast, false);
-+    }
-+    protected boolean moveItemStackTo(ItemStack stack, int startIndex, int endIndex, boolean fromLast, boolean isCheck) {
-+        if (isCheck) {
-+            stack = stack.copy();
-+        }
-+        // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent
-         boolean flag1 = false;
-         int k = startIndex;
- 
-@@ -752,6 +862,11 @@
- 
-                 slot = (Slot) this.slots.get(k);
-                 itemstack1 = slot.getItem();
-+                // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent; clone if only a check
-+                if (isCheck) {
-+                    itemstack1 = itemstack1.copy();
-+                }
-+                // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent
-                 if (!itemstack1.isEmpty() && ItemStack.isSameItemSameComponents(stack, itemstack1)) {
-                     l = itemstack1.getCount() + stack.getCount();
-                     int i1 = slot.getMaxStackSize(itemstack1);
-@@ -759,12 +874,16 @@
-                     if (l <= i1) {
-                         stack.setCount(0);
-                         itemstack1.setCount(l);
-+                        if (!isCheck) { // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
-                         slot.setChanged();
-+                        } // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
-                         flag1 = true;
-                     } else if (itemstack1.getCount() < i1) {
-                         stack.shrink(i1 - itemstack1.getCount());
-                         itemstack1.setCount(i1);
-+                        if (!isCheck) { // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
-                         slot.setChanged();
-+                        } // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
-                         flag1 = true;
-                     }
-                 }
-@@ -795,10 +914,21 @@
- 
-                 slot = (Slot) this.slots.get(k);
-                 itemstack1 = slot.getItem();
-+                // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent
-+                if (isCheck) {
-+                    itemstack1 = itemstack1.copy();
-+                }
-+                // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent
-                 if (itemstack1.isEmpty() && slot.mayPlace(stack)) {
-                     l = slot.getMaxStackSize(stack);
-+                    // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent
-+                    if (isCheck) {
-+                        stack.shrink(Math.min(stack.getCount(), l));
-+                    } else {
-+                    // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent
-                     slot.setByPlayer(stack.split(Math.min(stack.getCount(), l)));
-                     slot.setChanged();
-+                    } // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
-                     flag1 = true;
-                     break;
-                 }
-@@ -893,6 +1023,11 @@
-     }
- 
-     public ItemStack getCarried() {
-+        // CraftBukkit start
-+        if (this.carried.isEmpty()) {
-+            this.setCarried(ItemStack.EMPTY);
-+        }
-+        // CraftBukkit end
-         return this.carried;
-     }
- 
-@@ -947,4 +1082,15 @@
-         this.stateId = this.stateId + 1 & 32767;
-         return this.stateId;
-     }
-+
-+    // Paper start - Add missing InventoryHolders
-+    // The reason this is a supplier, is that the createHolder method uses the bukkit InventoryView#getTopInventory to get the inventory in question
-+    // and that can't be obtained safely until the AbstractContainerMenu has been fully constructed. Using a supplier lazily
-+    // initializes the InventoryHolder safely.
-+    protected final Supplier<org.bukkit.inventory.BlockInventoryHolder> createBlockHolder(final ContainerLevelAccess context) {
-+        //noinspection ConstantValue
-+        Preconditions.checkArgument(context != null, "context was null");
-+        return () -> context.createBlockHolder(this);
-+    }
-+    // Paper end - Add missing InventoryHolders
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/AbstractCraftingMenu.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/AbstractCraftingMenu.java.patch
deleted file mode 100644
index bda213246a..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/AbstractCraftingMenu.java.patch
+++ /dev/null
@@ -1,44 +0,0 @@
---- a/net/minecraft/world/inventory/AbstractCraftingMenu.java
-+++ b/net/minecraft/world/inventory/AbstractCraftingMenu.java
-@@ -13,14 +13,17 @@
- 
-     private final int width;
-     private final int height;
--    public final CraftingContainer craftSlots;
-+    public final TransientCraftingContainer craftSlots; // CraftBukkit
-     public final ResultContainer resultSlots = new ResultContainer();
- 
--    public AbstractCraftingMenu(MenuType<?> type, int syncId, int width, int height) {
--        super(type, syncId);
--        this.width = width;
--        this.height = height;
--        this.craftSlots = new TransientCraftingContainer(this, width, height);
-+    public AbstractCraftingMenu(MenuType<?> containers, int i, int j, int k, Inventory playerInventory) { // CraftBukkit
-+        super(containers, i);
-+        this.width = j;
-+        this.height = k;
-+        // CraftBukkit start
-+        this.craftSlots = new TransientCraftingContainer(this, j, k, playerInventory.player); // CraftBukkit - pass player
-+        this.craftSlots.resultInventory = this.resultSlots; // CraftBukkit - let InventoryCrafting know about its result slot
-+        // CraftBukkit end
-     }
- 
-     protected Slot addResultSlot(Player player, int x, int y) {
-@@ -38,7 +41,7 @@
- 
-     @Override
-     public RecipeBookMenu.PostPlaceAction handlePlacement(boolean craftAll, boolean creative, RecipeHolder<?> recipe, ServerLevel world, Inventory inventory) {
--        RecipeHolder<CraftingRecipe> recipeholder1 = recipe;
-+        RecipeHolder<CraftingRecipe> recipeholder1 = (RecipeHolder<CraftingRecipe>) recipe; // CraftBukkit - decompile error
- 
-         this.beginPlacingRecipe();
- 
-@@ -65,7 +68,7 @@
-                 }
-             }, this.width, this.height, list, list, inventory, recipeholder1, craftAll, creative);
-         } finally {
--            this.finishPlacingRecipe(world, recipe);
-+            this.finishPlacingRecipe(world, recipeholder1); // CraftBukkit - decompile error
-         }
- 
-         return containerrecipebook_a;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/AbstractFurnaceMenu.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/AbstractFurnaceMenu.java.patch
deleted file mode 100644
index 42a568418f..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/AbstractFurnaceMenu.java.patch
+++ /dev/null
@@ -1,60 +0,0 @@
---- a/net/minecraft/world/inventory/AbstractFurnaceMenu.java
-+++ b/net/minecraft/world/inventory/AbstractFurnaceMenu.java
-@@ -17,6 +17,10 @@
- import net.minecraft.world.item.crafting.RecipeType;
- import net.minecraft.world.item.crafting.SingleRecipeInput;
- import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity;
-+import org.bukkit.craftbukkit.inventory.CraftInventoryFurnace;
-+import org.bukkit.craftbukkit.inventory.view.CraftFurnaceView;
-+// CraftBukkit end
- 
- public abstract class AbstractFurnaceMenu extends RecipeBookMenu {
- 
-@@ -36,6 +40,22 @@
-     private final RecipePropertySet acceptedInputs;
-     private final RecipeBookType recipeBookType;
- 
-+    // CraftBukkit start
-+    private CraftFurnaceView bukkitEntity = null;
-+    private Inventory player;
-+
-+    @Override
-+    public CraftFurnaceView getBukkitView() {
-+        if (this.bukkitEntity != null) {
-+            return this.bukkitEntity;
-+        }
-+
-+        CraftInventoryFurnace inventory = new CraftInventoryFurnace((AbstractFurnaceBlockEntity) this.container);
-+        this.bukkitEntity = new CraftFurnaceView(this.player.player.getBukkitEntity(), inventory, this);
-+        return this.bukkitEntity;
-+    }
-+    // CraftBukkit end
-+
-     protected AbstractFurnaceMenu(MenuType<?> type, RecipeType<? extends AbstractCookingRecipe> recipeType, ResourceKey<RecipePropertySet> recipePropertySetKey, RecipeBookType category, int syncId, Inventory platerInventory) {
-         this(type, recipeType, recipePropertySetKey, category, syncId, platerInventory, new SimpleContainer(3), new SimpleContainerData(4));
-     }
-@@ -53,6 +73,7 @@
-         this.addSlot(new Slot(inventory, 0, 56, 17));
-         this.addSlot(new FurnaceFuelSlot(this, inventory, 1, 56, 53));
-         this.addSlot(new FurnaceResultSlot(platerInventory.player, inventory, 2, 116, 35));
-+        this.player = platerInventory; // CraftBukkit - save player
-         this.addStandardInventorySlots(platerInventory, 8, 84);
-         this.addDataSlots(propertyDelegate);
-     }
-@@ -71,6 +92,7 @@
- 
-     @Override
-     public boolean stillValid(Player player) {
-+        if (!this.checkReachable) return true; // CraftBukkit
-         return this.container.stillValid(player);
-     }
- 
-@@ -180,6 +202,6 @@
-             public boolean recipeMatches(RecipeHolder<AbstractCookingRecipe> entry) {
-                 return ((AbstractCookingRecipe) entry.value()).matches(new SingleRecipeInput(AbstractFurnaceMenu.this.container.getItem(0)), world);
-             }
--        }, 1, 1, List.of(this.getSlot(0)), list, inventory, recipe, craftAll, creative);
-+        }, 1, 1, List.of(this.getSlot(0)), list, inventory, (RecipeHolder<AbstractCookingRecipe>) recipe, craftAll, creative); // CraftBukkit - decompile error
-     }
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/BrewingStandMenu.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/BrewingStandMenu.java.patch
deleted file mode 100644
index 36dcdb7277..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/BrewingStandMenu.java.patch
+++ /dev/null
@@ -1,127 +0,0 @@
---- a/net/minecraft/world/inventory/BrewingStandMenu.java
-+++ b/net/minecraft/world/inventory/BrewingStandMenu.java
-@@ -16,6 +16,10 @@
- import net.minecraft.world.item.alchemy.Potion;
- import net.minecraft.world.item.alchemy.PotionBrewing;
- import net.minecraft.world.item.alchemy.PotionContents;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.inventory.CraftInventoryBrewer;
-+import org.bukkit.craftbukkit.inventory.view.CraftBrewingStandView;
-+// CraftBukkit end
- 
- public class BrewingStandMenu extends AbstractContainerMenu {
- 
-@@ -35,29 +39,51 @@
-     public final ContainerData brewingStandData;
-     private final Slot ingredientSlot;
- 
-+    // CraftBukkit start
-+    private CraftBrewingStandView bukkitEntity = null;
-+    private Inventory player;
-+    // CraftBukkit end
-+
-     public BrewingStandMenu(int syncId, Inventory playerInventory) {
--        this(syncId, playerInventory, new SimpleContainer(5), new SimpleContainerData(2));
-+        this(syncId, playerInventory, new SimpleContainer(5), new io.papermc.paper.inventory.BrewingSimpleContainerData()); // Paper - Add totalBrewTime
-     }
- 
-     public BrewingStandMenu(int syncId, Inventory playerInventory, Container inventory, ContainerData propertyDelegate) {
-         super(MenuType.BREWING_STAND, syncId);
-+        this.player = playerInventory; // CraftBukkit
-         checkContainerSize(inventory, 5);
--        checkContainerDataCount(propertyDelegate, 2);
-+        checkContainerDataCount(propertyDelegate, 3); // Paper - Add recipeBrewTime
-         this.brewingStand = inventory;
-         this.brewingStandData = propertyDelegate;
-         PotionBrewing potionbrewer = playerInventory.player.level().potionBrewing();
- 
--        this.addSlot(new BrewingStandMenu.PotionSlot(inventory, 0, 56, 51));
--        this.addSlot(new BrewingStandMenu.PotionSlot(inventory, 1, 79, 58));
--        this.addSlot(new BrewingStandMenu.PotionSlot(inventory, 2, 102, 51));
-+        // Paper start - custom potion mixes
-+        this.addSlot(new BrewingStandMenu.PotionSlot(inventory, 0, 56, 51, potionbrewer));
-+        this.addSlot(new BrewingStandMenu.PotionSlot(inventory, 1, 79, 58, potionbrewer));
-+        this.addSlot(new BrewingStandMenu.PotionSlot(inventory, 2, 102, 51, potionbrewer));
-+        // Paper end - custom potion mixes
-         this.ingredientSlot = this.addSlot(new BrewingStandMenu.IngredientsSlot(potionbrewer, inventory, 3, 79, 17));
-         this.addSlot(new BrewingStandMenu.FuelSlot(inventory, 4, 17, 17));
--        this.addDataSlots(propertyDelegate);
-+        // Paper start - Add recipeBrewTime
-+        this.addDataSlots(new SimpleContainerData(2) {
-+            @Override
-+            public int get(final int index) {
-+                if (index == 0) return 400 * propertyDelegate.get(index) / propertyDelegate.get(2);
-+                return propertyDelegate.get(index);
-+            }
-+
-+            @Override
-+            public void set(final int index, final int value) {
-+                propertyDelegate.set(index, value);
-+            }
-+        });
-+        // Paper end - Add recipeBrewTime
-         this.addStandardInventorySlots(playerInventory, 8, 84);
-     }
- 
-     @Override
-     public boolean stillValid(Player player) {
-+        if (!this.checkReachable) return true; // CraftBukkit
-         return this.brewingStand.stillValid(player);
-     }
- 
-@@ -79,7 +105,7 @@
-                     if (!this.moveItemStackTo(itemstack1, 3, 4, false)) {
-                         return ItemStack.EMPTY;
-                     }
--                } else if (BrewingStandMenu.PotionSlot.mayPlaceItem(itemstack)) {
-+                } else if (BrewingStandMenu.PotionSlot.mayPlaceItem(itemstack, this.player.player.level().potionBrewing())) { // Paper - custom potion mixes
-                     if (!this.moveItemStackTo(itemstack1, 0, 3, false)) {
-                         return ItemStack.EMPTY;
-                     }
-@@ -128,13 +154,15 @@
- 
-     private static class PotionSlot extends Slot {
- 
--        public PotionSlot(Container inventory, int index, int x, int y) {
-+        private final PotionBrewing potionBrewing; // Paper - custom potion mixes
-+        public PotionSlot(Container inventory, int index, int x, int y, PotionBrewing potionBrewing) { // Paper - custom potion mixes
-             super(inventory, index, x, y);
-+            this.potionBrewing = potionBrewing; // Paper - custom potion mixes
-         }
- 
-         @Override
-         public boolean mayPlace(ItemStack stack) {
--            return PotionSlot.mayPlaceItem(stack);
-+            return PotionSlot.mayPlaceItem(stack, this.potionBrewing); // Paper - custom potion mixes
-         }
- 
-         @Override
-@@ -153,8 +181,8 @@
-             super.onTake(player, stack);
-         }
- 
--        public static boolean mayPlaceItem(ItemStack stack) {
--            return stack.is(Items.POTION) || stack.is(Items.SPLASH_POTION) || stack.is(Items.LINGERING_POTION) || stack.is(Items.GLASS_BOTTLE);
-+        public static boolean mayPlaceItem(ItemStack stack, PotionBrewing potionBrewing) { // Paper - custom potion mixes
-+            return stack.is(Items.POTION) || stack.is(Items.SPLASH_POTION) || stack.is(Items.LINGERING_POTION) || stack.is(Items.GLASS_BOTTLE) || potionBrewing.isCustomInput(stack); // Paper - Custom Potion Mixes
-         }
- 
-         @Override
-@@ -198,4 +226,17 @@
-             return BrewingStandMenu.EMPTY_SLOT_FUEL;
-         }
-     }
-+
-+    // CraftBukkit start
-+    @Override
-+    public CraftBrewingStandView getBukkitView() {
-+        if (this.bukkitEntity != null) {
-+            return this.bukkitEntity;
-+        }
-+
-+        CraftInventoryBrewer inventory = new CraftInventoryBrewer(this.brewingStand);
-+        this.bukkitEntity = new CraftBrewingStandView(this.player.player.getBukkitEntity(), inventory, this);
-+        return this.bukkitEntity;
-+    }
-+    // CraftBukkit end
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/CartographyTableMenu.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/CartographyTableMenu.java.patch
deleted file mode 100644
index b46cd6b42c..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/CartographyTableMenu.java.patch
+++ /dev/null
@@ -1,146 +0,0 @@
---- a/net/minecraft/world/inventory/CartographyTableMenu.java
-+++ b/net/minecraft/world/inventory/CartographyTableMenu.java
-@@ -6,16 +6,36 @@
- import net.minecraft.world.Container;
- import net.minecraft.world.SimpleContainer;
- import net.minecraft.world.entity.player.Inventory;
--import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.item.Items;
- import net.minecraft.world.item.MapItem;
- import net.minecraft.world.item.component.MapPostProcessing;
- import net.minecraft.world.level.block.Blocks;
- import net.minecraft.world.level.saveddata.maps.MapItemSavedData;
-+// CraftBukkit start
-+import org.bukkit.Location;
-+import org.bukkit.craftbukkit.inventory.CraftInventoryCartography;
-+import org.bukkit.craftbukkit.inventory.CraftInventoryView;
-+import org.bukkit.entity.Player;
-+// CraftBukkit end
- 
- public class CartographyTableMenu extends AbstractContainerMenu {
- 
-+    // CraftBukkit start
-+    private CraftInventoryView bukkitEntity = null;
-+    private Player player;
-+
-+    @Override
-+    public CraftInventoryView getBukkitView() {
-+        if (this.bukkitEntity != null) {
-+            return this.bukkitEntity;
-+        }
-+
-+        CraftInventoryCartography inventory = new CraftInventoryCartography(this.container, this.resultContainer);
-+        this.bukkitEntity = new CraftInventoryView(this.player, inventory, this);
-+        return this.bukkitEntity;
-+    }
-+    // CraftBukkit end
-     public static final int MAP_SLOT = 0;
-     public static final int ADDITIONAL_SLOT = 1;
-     public static final int RESULT_SLOT = 2;
-@@ -34,28 +54,42 @@
- 
-     public CartographyTableMenu(int syncId, Inventory inventory, final ContainerLevelAccess context) {
-         super(MenuType.CARTOGRAPHY_TABLE, syncId);
--        this.container = new SimpleContainer(2) {
-+        this.container = new SimpleContainer(this.createBlockHolder(context), 2) { // Paper - Add missing InventoryHolders
-             @Override
-             public void setChanged() {
-                 CartographyTableMenu.this.slotsChanged(this);
-                 super.setChanged();
-             }
-+
-+            // CraftBukkit start
-+            @Override
-+            public Location getLocation() {
-+                return context.getLocation();
-+            }
-+            // CraftBukkit end
-         };
--        this.resultContainer = new ResultContainer() {
-+        this.resultContainer = new ResultContainer(this.createBlockHolder(context)) { // Paper - Add missing InventoryHolders
-             @Override
-             public void setChanged() {
--                CartographyTableMenu.this.slotsChanged(this);
-+                // CartographyTableMenu.this.slotsChanged(this); // Paper - Add CatographyItemEvent - do not recompute results if the result slot changes - allows to set the result slot via api
-                 super.setChanged();
-             }
-+
-+            // CraftBukkit start
-+            @Override
-+            public Location getLocation() {
-+                return context.getLocation();
-+            }
-+            // CraftBukkit end
-         };
-         this.access = context;
--        this.addSlot(new Slot(this, this.container, 0, 15, 15) {
-+        this.addSlot(new Slot(this.container, 0, 15, 15) { // CraftBukkit - decompile error
-             @Override
-             public boolean mayPlace(ItemStack stack) {
-                 return stack.has(DataComponents.MAP_ID);
-             }
-         });
--        this.addSlot(new Slot(this, this.container, 1, 15, 52) {
-+        this.addSlot(new Slot(this.container, 1, 15, 52) { // CraftBukkit - decompile error
-             @Override
-             public boolean mayPlace(ItemStack stack) {
-                 return stack.is(Items.PAPER) || stack.is(Items.MAP) || stack.is(Items.GLASS_PANE);
-@@ -68,7 +102,7 @@
-             }
- 
-             @Override
--            public void onTake(Player player, ItemStack stack) {
-+            public void onTake(net.minecraft.world.entity.player.Player player, ItemStack stack) {
-                 ((Slot) CartographyTableMenu.this.slots.get(0)).remove(1);
-                 ((Slot) CartographyTableMenu.this.slots.get(1)).remove(1);
-                 stack.getItem().onCraftedBy(stack, player.level(), player);
-@@ -76,7 +110,7 @@
-                     long j = world.getGameTime();
- 
-                     if (CartographyTableMenu.this.lastSoundTime != j) {
--                        world.playSound((Player) null, blockposition, SoundEvents.UI_CARTOGRAPHY_TABLE_TAKE_RESULT, SoundSource.BLOCKS, 1.0F, 1.0F);
-+                        world.playSound((net.minecraft.world.entity.player.Player) null, blockposition, SoundEvents.UI_CARTOGRAPHY_TABLE_TAKE_RESULT, SoundSource.BLOCKS, 1.0F, 1.0F);
-                         CartographyTableMenu.this.lastSoundTime = j;
-                     }
- 
-@@ -85,10 +119,12 @@
-             }
-         });
-         this.addStandardInventorySlots(inventory, 8, 84);
-+        this.player = (Player) inventory.player.getBukkitEntity(); // CraftBukkit
-     }
- 
-     @Override
--    public boolean stillValid(Player player) {
-+    public boolean stillValid(net.minecraft.world.entity.player.Player player) {
-+        if (!this.checkReachable) return true; // CraftBukkit
-         return stillValid(this.access, player, Blocks.CARTOGRAPHY_TABLE);
-     }
- 
-@@ -104,6 +140,7 @@
-             this.setupResultSlot(itemstack, itemstack1, itemstack2);
-         }
- 
-+        org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareResultEvent(this, RESULT_SLOT); // Paper - Add PrepareResultEvent
-     }
- 
-     private void setupResultSlot(ItemStack map, ItemStack item, ItemStack oldResult) {
-@@ -147,7 +184,7 @@
-     }
- 
-     @Override
--    public ItemStack quickMoveStack(Player player, int slot) {
-+    public ItemStack quickMoveStack(net.minecraft.world.entity.player.Player player, int slot) {
-         ItemStack itemstack = ItemStack.EMPTY;
-         Slot slot1 = (Slot) this.slots.get(slot);
- 
-@@ -199,7 +236,7 @@
-     }
- 
-     @Override
--    public void removed(Player player) {
-+    public void removed(net.minecraft.world.entity.player.Player player) {
-         super.removed(player);
-         this.resultContainer.removeItemNoUpdate(2);
-         this.access.execute((world, blockposition) -> {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/ChestMenu.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/ChestMenu.java.patch
deleted file mode 100644
index 8ba0e1a6a1..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/ChestMenu.java.patch
+++ /dev/null
@@ -1,64 +0,0 @@
---- a/net/minecraft/world/inventory/ChestMenu.java
-+++ b/net/minecraft/world/inventory/ChestMenu.java
-@@ -1,16 +1,43 @@
- package net.minecraft.world.inventory;
- 
-+import net.minecraft.world.CompoundContainer;
- import net.minecraft.world.Container;
- import net.minecraft.world.SimpleContainer;
- import net.minecraft.world.entity.player.Inventory;
- import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.item.ItemStack;
-+import org.bukkit.craftbukkit.inventory.CraftInventory;
-+import org.bukkit.craftbukkit.inventory.CraftInventoryView;
-+// CraftBukkit end
- 
- public class ChestMenu extends AbstractContainerMenu {
- 
-     private final Container container;
-     private final int containerRows;
-+    // CraftBukkit start
-+    private CraftInventoryView bukkitEntity = null;
-+    private Inventory player;
- 
-+    @Override
-+    public CraftInventoryView getBukkitView() {
-+        if (this.bukkitEntity != null) {
-+            return this.bukkitEntity;
-+        }
-+
-+        CraftInventory inventory;
-+        if (this.container instanceof Inventory) {
-+            inventory = new org.bukkit.craftbukkit.inventory.CraftInventoryPlayer((Inventory) this.container);
-+        } else if (this.container instanceof CompoundContainer) {
-+            inventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest((CompoundContainer) this.container);
-+        } else {
-+            inventory = new CraftInventory(this.container);
-+        }
-+
-+        this.bukkitEntity = new CraftInventoryView(this.player.player.getBukkitEntity(), inventory, this);
-+        return this.bukkitEntity;
-+    }
-+    // CraftBukkit end
-+
-     private ChestMenu(MenuType<?> type, int syncId, Inventory playerInventory, int rows) {
-         this(type, syncId, playerInventory, new SimpleContainer(9 * rows), rows);
-     }
-@@ -53,6 +80,9 @@
-         this.container = inventory;
-         this.containerRows = rows;
-         inventory.startOpen(playerInventory.player);
-+        // CraftBukkit start - Save player
-+        this.player = playerInventory;
-+        // CraftBukkit end
-         boolean flag = true;
- 
-         this.addChestGrid(inventory, 8, 18);
-@@ -72,6 +102,7 @@
- 
-     @Override
-     public boolean stillValid(Player player) {
-+        if (!this.checkReachable) return true; // CraftBukkit
-         return this.container.stillValid(player);
-     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/ContainerListener.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/ContainerListener.java.patch
deleted file mode 100644
index 7d9e714dfe..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/ContainerListener.java.patch
+++ /dev/null
@@ -1,14 +0,0 @@
---- a/net/minecraft/world/inventory/ContainerListener.java
-+++ b/net/minecraft/world/inventory/ContainerListener.java
-@@ -5,5 +5,11 @@
- public interface ContainerListener {
-     void slotChanged(AbstractContainerMenu handler, int slotId, ItemStack stack);
- 
-+    // Paper start - Add PlayerInventorySlotChangeEvent
-+    default void slotChanged(AbstractContainerMenu handler, int slotId, ItemStack oldStack, ItemStack stack) {
-+        slotChanged(handler, slotId, stack);
-+    }
-+    // Paper end - Add PlayerInventorySlotChangeEvent
-+
-     void dataChanged(AbstractContainerMenu handler, int property, int value);
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/ContainerSynchronizer.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/ContainerSynchronizer.java.patch
deleted file mode 100644
index a76ea56e99..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/ContainerSynchronizer.java.patch
+++ /dev/null
@@ -1,10 +0,0 @@
---- a/net/minecraft/world/inventory/ContainerSynchronizer.java
-+++ b/net/minecraft/world/inventory/ContainerSynchronizer.java
-@@ -6,6 +6,7 @@
- public interface ContainerSynchronizer {
-     void sendInitialData(AbstractContainerMenu handler, NonNullList<ItemStack> stacks, ItemStack cursorStack, int[] properties);
- 
-+    default void sendOffHandSlotChange() {} // Paper - Sync offhand slot in menus
-     void sendSlotChange(AbstractContainerMenu handler, int slot, ItemStack stack);
- 
-     void sendCarriedChange(AbstractContainerMenu handler, ItemStack stack);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/CrafterMenu.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/CrafterMenu.java.patch
deleted file mode 100644
index e998f9ae45..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/CrafterMenu.java.patch
+++ /dev/null
@@ -1,38 +0,0 @@
---- a/net/minecraft/world/inventory/CrafterMenu.java
-+++ b/net/minecraft/world/inventory/CrafterMenu.java
-@@ -10,8 +10,27 @@
- import net.minecraft.world.item.crafting.CraftingRecipe;
- import net.minecraft.world.level.block.CrafterBlock;
- 
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.inventory.CraftInventoryCrafter;
-+import org.bukkit.craftbukkit.inventory.view.CraftCrafterView;
-+// CraftBukkit end
-+
- public class CrafterMenu extends AbstractContainerMenu implements ContainerListener {
- 
-+    // CraftBukkit start
-+    private CraftCrafterView bukkitEntity = null;
-+
-+    @Override
-+    public CraftCrafterView getBukkitView() {
-+        if (this.bukkitEntity != null) {
-+            return this.bukkitEntity;
-+        }
-+
-+        CraftInventoryCrafter inventory = new CraftInventoryCrafter(this.container, this.resultContainer);
-+        this.bukkitEntity = new CraftCrafterView(this.player.getBukkitEntity(), inventory, this);
-+        return this.bukkitEntity;
-+    }
-+    // CraftBukkit end
-     protected static final int SLOT_COUNT = 9;
-     private static final int INV_SLOT_START = 9;
-     private static final int INV_SLOT_END = 36;
-@@ -106,6 +125,7 @@
- 
-     @Override
-     public boolean stillValid(Player player) {
-+        if (!this.checkReachable) return true; // CraftBukkit
-         return this.container.stillValid(player);
-     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/CraftingContainer.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/CraftingContainer.java.patch
deleted file mode 100644
index 0c5babe735..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/CraftingContainer.java.patch
+++ /dev/null
@@ -1,29 +0,0 @@
---- a/net/minecraft/world/inventory/CraftingContainer.java
-+++ b/net/minecraft/world/inventory/CraftingContainer.java
-@@ -5,6 +5,10 @@
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.item.crafting.CraftingInput;
- 
-+// CraftBukkit start
-+import net.minecraft.world.item.crafting.RecipeHolder;
-+// CraftBukkit end
-+
- public interface CraftingContainer extends Container, StackedContentsCompatible {
- 
-     int getWidth();
-@@ -13,6 +17,15 @@
- 
-     List<ItemStack> getItems();
- 
-+    // CraftBukkit start
-+    default RecipeHolder<?> getCurrentRecipe() {
-+        return null;
-+    }
-+
-+    default void setCurrentRecipe(RecipeHolder<?> recipe) {
-+    }
-+    // CraftBukkit end
-+
-     default CraftingInput asCraftInput() {
-         return this.asPositionedCraftInput().input();
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/CraftingMenu.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/CraftingMenu.java.patch
deleted file mode 100644
index 9f0941d40b..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/CraftingMenu.java.patch
+++ /dev/null
@@ -1,74 +0,0 @@
---- a/net/minecraft/world/inventory/CraftingMenu.java
-+++ b/net/minecraft/world/inventory/CraftingMenu.java
-@@ -14,7 +14,11 @@
- import net.minecraft.world.item.crafting.CraftingRecipe;
- import net.minecraft.world.item.crafting.RecipeHolder;
- import net.minecraft.world.item.crafting.RecipeType;
-+import net.minecraft.world.item.crafting.RepairItemRecipe;
- import net.minecraft.world.level.block.Blocks;
-+import org.bukkit.craftbukkit.inventory.CraftInventoryCrafting;
-+import org.bukkit.craftbukkit.inventory.CraftInventoryView;
-+// CraftBukkit end
- 
- public class CraftingMenu extends AbstractCraftingMenu {
- 
-@@ -31,13 +35,16 @@
-     public final ContainerLevelAccess access;
-     private final Player player;
-     private boolean placingRecipe;
-+    // CraftBukkit start
-+    private CraftInventoryView bukkitEntity = null;
-+    // CraftBukkit end
- 
-     public CraftingMenu(int syncId, Inventory playerInventory) {
-         this(syncId, playerInventory, ContainerLevelAccess.NULL);
-     }
- 
-     public CraftingMenu(int syncId, Inventory playerInventory, ContainerLevelAccess context) {
--        super(MenuType.CRAFTING, syncId, 3, 3);
-+        super(MenuType.CRAFTING, syncId, 3, 3, playerInventory); // CraftBukkit - pass player
-         this.access = context;
-         this.player = playerInventory.player;
-         this.addResultSlot(this.player, 124, 35);
-@@ -50,6 +57,7 @@
-         ServerPlayer entityplayer = (ServerPlayer) player;
-         ItemStack itemstack = ItemStack.EMPTY;
-         Optional<RecipeHolder<CraftingRecipe>> optional = world.getServer().getRecipeManager().getRecipeFor(RecipeType.CRAFTING, craftinginput, world, recipe);
-+        craftingInventory.setCurrentRecipe(optional.orElse(null)); // CraftBukkit
- 
-         if (optional.isPresent()) {
-             RecipeHolder<CraftingRecipe> recipeholder1 = (RecipeHolder) optional.get();
-@@ -63,6 +71,7 @@
-                 }
-             }
-         }
-+        itemstack = org.bukkit.craftbukkit.event.CraftEventFactory.callPreCraftEvent(craftingInventory, resultInventory, itemstack, handler.getBukkitView(), optional.map(RecipeHolder::value).orElse(null) instanceof RepairItemRecipe); // CraftBukkit
- 
-         resultInventory.setItem(0, itemstack);
-         handler.setRemoteSlot(0, itemstack);
-@@ -103,6 +112,7 @@
- 
-     @Override
-     public boolean stillValid(Player player) {
-+        if (!this.checkReachable) return true; // CraftBukkit
-         return stillValid(this.access, player, Blocks.CRAFTING_TABLE);
-     }
- 
-@@ -181,4 +191,17 @@
-     protected Player owner() {
-         return this.player;
-     }
-+
-+    // CraftBukkit start
-+    @Override
-+    public CraftInventoryView getBukkitView() {
-+        if (this.bukkitEntity != null) {
-+            return this.bukkitEntity;
-+        }
-+
-+        CraftInventoryCrafting inventory = new CraftInventoryCrafting(this.craftSlots, this.resultSlots);
-+        this.bukkitEntity = new CraftInventoryView(this.player.getBukkitEntity(), inventory, this);
-+        return this.bukkitEntity;
-+    }
-+    // CraftBukkit end
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/DispenserMenu.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/DispenserMenu.java.patch
deleted file mode 100644
index 18ed7c2049..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/DispenserMenu.java.patch
+++ /dev/null
@@ -1,62 +0,0 @@
---- a/net/minecraft/world/inventory/DispenserMenu.java
-+++ b/net/minecraft/world/inventory/DispenserMenu.java
-@@ -6,6 +6,11 @@
- import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.item.ItemStack;
- 
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.inventory.CraftInventory;
-+import org.bukkit.craftbukkit.inventory.CraftInventoryView;
-+// CraftBukkit end
-+
- public class DispenserMenu extends AbstractContainerMenu {
- 
-     private static final int SLOT_COUNT = 9;
-@@ -14,6 +19,10 @@
-     private static final int USE_ROW_SLOT_START = 36;
-     private static final int USE_ROW_SLOT_END = 45;
-     public final Container dispenser;
-+    // CraftBukkit start
-+    private CraftInventoryView bukkitEntity = null;
-+    private Inventory player;
-+    // CraftBukkit end
- 
-     public DispenserMenu(int syncId, Inventory playerInventory) {
-         this(syncId, playerInventory, new SimpleContainer(9));
-@@ -21,6 +30,10 @@
- 
-     public DispenserMenu(int syncId, Inventory playerInventory, Container inventory) {
-         super(MenuType.GENERIC_3x3, syncId);
-+        // CraftBukkit start - Save player
-+        this.player = playerInventory;
-+        // CraftBukkit end
-+
-         checkContainerSize(inventory, 9);
-         this.dispenser = inventory;
-         inventory.startOpen(playerInventory.player);
-@@ -41,6 +54,7 @@
- 
-     @Override
-     public boolean stillValid(Player player) {
-+        if (!this.checkReachable) return true; // CraftBukkit
-         return this.dispenser.stillValid(player);
-     }
- 
-@@ -82,4 +96,17 @@
-         super.removed(player);
-         this.dispenser.stopOpen(player);
-     }
-+
-+    // CraftBukkit start
-+    @Override
-+    public CraftInventoryView getBukkitView() {
-+        if (this.bukkitEntity != null) {
-+            return this.bukkitEntity;
-+        }
-+
-+        CraftInventory inventory = new CraftInventory(this.dispenser);
-+        this.bukkitEntity = new CraftInventoryView(this.player.player.getBukkitEntity(), inventory, this);
-+        return this.bukkitEntity;
-+    }
-+    // CraftBukkit end
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/EnchantmentMenu.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/EnchantmentMenu.java.patch
deleted file mode 100644
index 531e6792cc..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/EnchantmentMenu.java.patch
+++ /dev/null
@@ -1,270 +0,0 @@
---- a/net/minecraft/world/inventory/EnchantmentMenu.java
-+++ b/net/minecraft/world/inventory/EnchantmentMenu.java
-@@ -21,7 +21,6 @@
- import net.minecraft.world.Container;
- import net.minecraft.world.SimpleContainer;
- import net.minecraft.world.entity.player.Inventory;
--import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.item.Items;
- import net.minecraft.world.item.enchantment.Enchantment;
-@@ -29,6 +28,18 @@
- import net.minecraft.world.item.enchantment.EnchantmentInstance;
- import net.minecraft.world.level.block.Blocks;
- import net.minecraft.world.level.block.EnchantingTableBlock;
-+// CraftBukkit start
-+import java.util.Map;
-+import org.bukkit.Location;
-+import org.bukkit.craftbukkit.enchantments.CraftEnchantment;
-+import org.bukkit.craftbukkit.inventory.CraftInventoryEnchanting;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.craftbukkit.inventory.view.CraftEnchantmentView;
-+import org.bukkit.enchantments.EnchantmentOffer;
-+import org.bukkit.event.enchantment.EnchantItemEvent;
-+import org.bukkit.event.enchantment.PrepareItemEnchantEvent;
-+import org.bukkit.entity.Player;
-+// CraftBukkit end
- 
- public class EnchantmentMenu extends AbstractContainerMenu {
- 
-@@ -40,6 +51,10 @@
-     public final int[] costs;
-     public final int[] enchantClue;
-     public final int[] levelClue;
-+    // CraftBukkit start
-+    private CraftEnchantmentView bukkitEntity = null;
-+    private Player player;
-+    // CraftBukkit end
- 
-     public EnchantmentMenu(int syncId, Inventory playerInventory) {
-         this(syncId, playerInventory, ContainerLevelAccess.NULL);
-@@ -47,12 +62,19 @@
- 
-     public EnchantmentMenu(int syncId, Inventory playerInventory, ContainerLevelAccess context) {
-         super(MenuType.ENCHANTMENT, syncId);
--        this.enchantSlots = new SimpleContainer(2) {
-+        this.enchantSlots = new SimpleContainer(this.createBlockHolder(context), 2) { // Paper - Add missing InventoryHolders
-             @Override
-             public void setChanged() {
-                 super.setChanged();
-                 EnchantmentMenu.this.slotsChanged(this);
-             }
-+
-+            // CraftBukkit start
-+            @Override
-+            public Location getLocation() {
-+                return context.getLocation();
-+            }
-+            // CraftBukkit end
-         };
-         this.random = RandomSource.create();
-         this.enchantmentSeed = DataSlot.standalone();
-@@ -60,13 +82,13 @@
-         this.enchantClue = new int[]{-1, -1, -1};
-         this.levelClue = new int[]{-1, -1, -1};
-         this.access = context;
--        this.addSlot(new Slot(this, this.enchantSlots, 0, 15, 47) {
-+        this.addSlot(new Slot(this.enchantSlots, 0, 15, 47) { // CraftBukkit - decompile error
-             @Override
-             public int getMaxStackSize() {
-                 return 1;
-             }
-         });
--        this.addSlot(new Slot(this, this.enchantSlots, 1, 35, 47) {
-+        this.addSlot(new Slot(this.enchantSlots, 1, 35, 47) { // CraftBukkit - decompile error
-             @Override
-             public boolean mayPlace(ItemStack stack) {
-                 return stack.is(Items.LAPIS_LAZULI);
-@@ -88,6 +110,9 @@
-         this.addDataSlot(DataSlot.shared(this.levelClue, 0));
-         this.addDataSlot(DataSlot.shared(this.levelClue, 1));
-         this.addDataSlot(DataSlot.shared(this.levelClue, 2));
-+        // CraftBukkit start
-+        this.player = (Player) playerInventory.player.getBukkitEntity();
-+        // CraftBukkit end
-     }
- 
-     @Override
-@@ -95,7 +120,7 @@
-         if (inventory == this.enchantSlots) {
-             ItemStack itemstack = inventory.getItem(0);
- 
--            if (!itemstack.isEmpty() && itemstack.isEnchantable()) {
-+            if (!itemstack.isEmpty()) { // CraftBukkit - relax condition
-                 this.access.execute((world, blockposition) -> {
-                     IdMap<Holder<Enchantment>> registry = world.registryAccess().lookupOrThrow(Registries.ENCHANTMENT).asHolderIdMap();
-                     int i = 0;
-@@ -135,6 +160,41 @@
-                         }
-                     }
- 
-+                    // CraftBukkit start
-+                    CraftItemStack item = CraftItemStack.asCraftMirror(itemstack);
-+                    org.bukkit.enchantments.EnchantmentOffer[] offers = new EnchantmentOffer[3];
-+                    for (j = 0; j < 3; ++j) {
-+                        org.bukkit.enchantments.Enchantment enchantment = (this.enchantClue[j] >= 0) ? CraftEnchantment.minecraftHolderToBukkit(registry.byId(this.enchantClue[j])) : null;
-+                        offers[j] = (enchantment != null) ? new EnchantmentOffer(enchantment, this.levelClue[j], this.costs[j]) : null;
-+                    }
-+
-+                    PrepareItemEnchantEvent event = new PrepareItemEnchantEvent(this.player, this.getBukkitView(), this.access.getLocation().getBlock(), item, offers, i);
-+                    event.setCancelled(!itemstack.isEnchantable());
-+                    world.getCraftServer().getPluginManager().callEvent(event);
-+
-+                    if (event.isCancelled()) {
-+                        for (j = 0; j < 3; ++j) {
-+                            this.costs[j] = 0;
-+                            this.enchantClue[j] = -1;
-+                            this.levelClue[j] = -1;
-+                        }
-+                        return;
-+                    }
-+
-+                    for (j = 0; j < 3; j++) {
-+                        EnchantmentOffer offer = event.getOffers()[j];
-+                        if (offer != null) {
-+                            this.costs[j] = offer.getCost();
-+                            this.enchantClue[j] = registry.getId(CraftEnchantment.bukkitToMinecraftHolder(offer.getEnchantment()));
-+                            this.levelClue[j] = offer.getEnchantmentLevel();
-+                        } else {
-+                            this.costs[j] = 0;
-+                            this.enchantClue[j] = -1;
-+                            this.levelClue[j] = -1;
-+                        }
-+                    }
-+                    // CraftBukkit end
-+
-                     this.broadcastChanges();
-                 });
-             } else {
-@@ -149,7 +209,7 @@
-     }
- 
-     @Override
--    public boolean clickMenuButton(Player player, int id) {
-+    public boolean clickMenuButton(net.minecraft.world.entity.player.Player player, int id) {
-         if (id >= 0 && id < this.costs.length) {
-             ItemStack itemstack = this.enchantSlots.getItem(0);
-             ItemStack itemstack1 = this.enchantSlots.getItem(1);
-@@ -159,24 +219,55 @@
-                 return false;
-             } else if (this.costs[id] > 0 && !itemstack.isEmpty() && (player.experienceLevel >= j && player.experienceLevel >= this.costs[id] || player.getAbilities().instabuild)) {
-                 this.access.execute((world, blockposition) -> {
--                    ItemStack itemstack2 = itemstack;
-+                    ItemStack itemstack2 = itemstack; // Paper - diff on change
-                     List<EnchantmentInstance> list = this.getEnchantmentList(world.registryAccess(), itemstack, id, this.costs[id]);
- 
--                    if (!list.isEmpty()) {
--                        player.onEnchantmentPerformed(itemstack, j);
--                        if (itemstack.is(Items.BOOK)) {
--                            itemstack2 = itemstack.transmuteCopy(Items.ENCHANTED_BOOK);
--                            this.enchantSlots.setItem(0, itemstack2);
-+                    // CraftBukkit start
-+                    IdMap<Holder<Enchantment>> registry = world.registryAccess().lookupOrThrow(Registries.ENCHANTMENT).asHolderIdMap();
-+                    if (true || !list.isEmpty()) {
-+                        // entityhuman.onEnchantmentPerformed(itemstack, j); // Moved down
-+                        Map<org.bukkit.enchantments.Enchantment, Integer> enchants = new java.util.HashMap<org.bukkit.enchantments.Enchantment, Integer>();
-+                        for (EnchantmentInstance instance : list) {
-+                            enchants.put(CraftEnchantment.minecraftHolderToBukkit(instance.enchantment), instance.level);
-                         }
-+                        CraftItemStack item = CraftItemStack.asCraftMirror(itemstack2);
- 
--                        Iterator iterator = list.iterator();
-+                        org.bukkit.enchantments.Enchantment hintedEnchantment = CraftEnchantment.minecraftHolderToBukkit(registry.byId(this.enchantClue[id]));
-+                        int hintedEnchantmentLevel = this.levelClue[id];
-+                        EnchantItemEvent event = new EnchantItemEvent((Player) player.getBukkitEntity(), this.getBukkitView(), this.access.getLocation().getBlock(), item, this.costs[id], enchants, hintedEnchantment, hintedEnchantmentLevel, id);
-+                        world.getCraftServer().getPluginManager().callEvent(event);
- 
--                        while (iterator.hasNext()) {
--                            EnchantmentInstance weightedrandomenchant = (EnchantmentInstance) iterator.next();
-+                        int level = event.getExpLevelCost();
-+                        if (event.isCancelled() || (level > player.experienceLevel && !player.getAbilities().instabuild) || event.getEnchantsToAdd().isEmpty()) {
-+                            return;
-+                        }
-+                        // CraftBukkit end
-+                        // Paper start
-+                        itemstack2 = org.bukkit.craftbukkit.inventory.CraftItemStack.getOrCloneOnMutation(item, event.getItem());
-+                        if (itemstack2 != itemstack) {
-+                            this.enchantSlots.setItem(0, itemstack2);
-+                        }
-+                        if (itemstack2.is(Items.BOOK)) {
-+                            itemstack2 = itemstack2.transmuteCopy(Items.ENCHANTED_BOOK);
-+                            this.enchantSlots.setItem(0, itemstack2);
-+                        }
-+                        // Paper end
- 
-+                        // CraftBukkit start
-+                        for (Map.Entry<org.bukkit.enchantments.Enchantment, Integer> entry : event.getEnchantsToAdd().entrySet()) {
-+                            Holder<Enchantment> nms = CraftEnchantment.bukkitToMinecraftHolder(entry.getKey());
-+                            if (nms == null) {
-+                                continue;
-+                            }
-+
-+                            EnchantmentInstance weightedrandomenchant = new EnchantmentInstance(nms, entry.getValue());
-                             itemstack2.enchant(weightedrandomenchant.enchantment, weightedrandomenchant.level);
-                         }
- 
-+                        player.onEnchantmentPerformed(itemstack, j);
-+                        // CraftBukkit end
-+
-+                        // CraftBukkit - TODO: let plugins change this
-                         itemstack1.consume(j, player);
-                         if (itemstack1.isEmpty()) {
-                             this.enchantSlots.setItem(1, ItemStack.EMPTY);
-@@ -190,7 +281,7 @@
-                         this.enchantSlots.setChanged();
-                         this.enchantmentSeed.set(player.getEnchantmentSeed());
-                         this.slotsChanged(this.enchantSlots);
--                        world.playSound((Player) null, blockposition, SoundEvents.ENCHANTMENT_TABLE_USE, SoundSource.BLOCKS, 1.0F, world.random.nextFloat() * 0.1F + 0.9F);
-+                        world.playSound((net.minecraft.world.entity.player.Player) null, blockposition, SoundEvents.ENCHANTMENT_TABLE_USE, SoundSource.BLOCKS, 1.0F, world.random.nextFloat() * 0.1F + 0.9F);
-                     }
- 
-                 });
-@@ -234,7 +325,7 @@
-     }
- 
-     @Override
--    public void removed(Player player) {
-+    public void removed(net.minecraft.world.entity.player.Player player) {
-         super.removed(player);
-         this.access.execute((world, blockposition) -> {
-             this.clearContainer(player, this.enchantSlots);
-@@ -242,12 +333,13 @@
-     }
- 
-     @Override
--    public boolean stillValid(Player player) {
-+    public boolean stillValid(net.minecraft.world.entity.player.Player player) {
-+        if (!this.checkReachable) return true; // CraftBukkit
-         return stillValid(this.access, player, Blocks.ENCHANTING_TABLE);
-     }
- 
-     @Override
--    public ItemStack quickMoveStack(Player player, int slot) {
-+    public ItemStack quickMoveStack(net.minecraft.world.entity.player.Player player, int slot) {
-         ItemStack itemstack = ItemStack.EMPTY;
-         Slot slot1 = (Slot) this.slots.get(slot);
- 
-@@ -293,4 +385,23 @@
- 
-         return itemstack;
-     }
-+
-+    // CraftBukkit start
-+    @Override
-+    public CraftEnchantmentView getBukkitView() {
-+        if (this.bukkitEntity != null) {
-+            return this.bukkitEntity;
-+        }
-+
-+        CraftInventoryEnchanting inventory = new CraftInventoryEnchanting(this.enchantSlots);
-+        this.bukkitEntity = new CraftEnchantmentView(this.player, inventory, this);
-+        return this.bukkitEntity;
-+    }
-+    // CraftBukkit end
-+
-+    // Paper start - add enchantment seed update API
-+    public void setEnchantmentSeed(int seed) {
-+        this.enchantmentSeed.set(seed);
-+    }
-+    // Paper end - add enchantment seed update API
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/FurnaceResultSlot.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/FurnaceResultSlot.java.patch
deleted file mode 100644
index 488614cbf6..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/FurnaceResultSlot.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/inventory/FurnaceResultSlot.java
-+++ b/net/minecraft/world/inventory/FurnaceResultSlot.java
-@@ -51,7 +51,7 @@
-             Container iinventory = this.container;
- 
-             if (iinventory instanceof AbstractFurnaceBlockEntity tileentityfurnace) {
--                tileentityfurnace.awardUsedRecipesAndPopExperience(entityplayer);
-+                tileentityfurnace.awardUsedRecipesAndPopExperience(entityplayer, stack, this.removeCount); // CraftBukkit
-             }
-         }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/GrindstoneMenu.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/GrindstoneMenu.java.patch
deleted file mode 100644
index 0ba3910b94..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/GrindstoneMenu.java.patch
+++ /dev/null
@@ -1,141 +0,0 @@
---- a/net/minecraft/world/inventory/GrindstoneMenu.java
-+++ b/net/minecraft/world/inventory/GrindstoneMenu.java
-@@ -10,7 +10,6 @@
- import net.minecraft.world.SimpleContainer;
- import net.minecraft.world.entity.ExperienceOrb;
- import net.minecraft.world.entity.player.Inventory;
--import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.item.Items;
- import net.minecraft.world.item.enchantment.Enchantment;
-@@ -19,9 +18,30 @@
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.block.Blocks;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.Location;
-+import org.bukkit.craftbukkit.inventory.CraftInventoryGrindstone;
-+import org.bukkit.craftbukkit.inventory.CraftInventoryView;
-+import org.bukkit.entity.Player;
-+// CraftBukkit end
- 
- public class GrindstoneMenu extends AbstractContainerMenu {
- 
-+    // CraftBukkit start
-+    private CraftInventoryView bukkitEntity = null;
-+    private Player player;
-+
-+    @Override
-+    public CraftInventoryView getBukkitView() {
-+        if (this.bukkitEntity != null) {
-+            return this.bukkitEntity;
-+        }
-+
-+        CraftInventoryGrindstone inventory = new CraftInventoryGrindstone(this.repairSlots, this.resultSlots);
-+        this.bukkitEntity = new CraftInventoryView(this.player, inventory, this);
-+        return this.bukkitEntity;
-+    }
-+    // CraftBukkit end
-     public static final int MAX_NAME_LENGTH = 35;
-     public static final int INPUT_SLOT = 0;
-     public static final int ADDITIONAL_SLOT = 1;
-@@ -40,22 +60,29 @@
- 
-     public GrindstoneMenu(int syncId, Inventory playerInventory, final ContainerLevelAccess context) {
-         super(MenuType.GRINDSTONE, syncId);
--        this.resultSlots = new ResultContainer();
--        this.repairSlots = new SimpleContainer(2) {
-+        this.resultSlots = new ResultContainer(this.createBlockHolder(context)); // Paper - Add missing InventoryHolders
-+        this.repairSlots = new SimpleContainer(this.createBlockHolder(context), 2) { // Paper - Add missing InventoryHolders
-             @Override
-             public void setChanged() {
-                 super.setChanged();
-                 GrindstoneMenu.this.slotsChanged(this);
-             }
-+
-+            // CraftBukkit start
-+            @Override
-+            public Location getLocation() {
-+                return context.getLocation();
-+            }
-+            // CraftBukkit end
-         };
-         this.access = context;
--        this.addSlot(new Slot(this, this.repairSlots, 0, 49, 19) {
-+        this.addSlot(new Slot(this.repairSlots, 0, 49, 19) { // CraftBukkit - decompile error
-             @Override
-             public boolean mayPlace(ItemStack stack) {
-                 return stack.isDamageableItem() || EnchantmentHelper.hasAnyEnchantments(stack);
-             }
-         });
--        this.addSlot(new Slot(this, this.repairSlots, 1, 49, 40) {
-+        this.addSlot(new Slot(this.repairSlots, 1, 49, 40) { // CraftBukkit - decompile error
-             @Override
-             public boolean mayPlace(ItemStack stack) {
-                 return stack.isDamageableItem() || EnchantmentHelper.hasAnyEnchantments(stack);
-@@ -68,10 +95,14 @@
-             }
- 
-             @Override
--            public void onTake(Player player, ItemStack stack) {
-+            public void onTake(net.minecraft.world.entity.player.Player player, ItemStack stack) {
-                 context.execute((world, blockposition) -> {
-                     if (world instanceof ServerLevel) {
--                        ExperienceOrb.award((ServerLevel) world, Vec3.atCenterOf(blockposition), this.getExperienceAmount(world));
-+                        // Paper start - Fire BlockExpEvent on grindstone use
-+                        org.bukkit.event.block.BlockExpEvent event = new org.bukkit.event.block.BlockExpEvent(org.bukkit.craftbukkit.block.CraftBlock.at(world, blockposition), this.getExperienceAmount(world));
-+                        event.callEvent();
-+                        ExperienceOrb.award((ServerLevel) world, Vec3.atCenterOf(blockposition), event.getExpToDrop(), org.bukkit.entity.ExperienceOrb.SpawnReason.GRINDSTONE, player);
-+                        // Paper end - Fire BlockExpEvent on grindstone use
-                     }
- 
-                     world.levelEvent(1042, blockposition, 0);
-@@ -113,6 +144,7 @@
-             }
-         });
-         this.addStandardInventorySlots(playerInventory, 8, 84);
-+        this.player = (Player) playerInventory.player.getBukkitEntity(); // CraftBukkit
-     }
- 
-     @Override
-@@ -120,12 +152,14 @@
-         super.slotsChanged(inventory);
-         if (inventory == this.repairSlots) {
-             this.createResult();
-+            org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareResultEvent(this, RESULT_SLOT); // Paper - Add PrepareResultEvent
-         }
- 
-     }
- 
-     private void createResult() {
--        this.resultSlots.setItem(0, this.computeResult(this.repairSlots.getItem(0), this.repairSlots.getItem(1)));
-+        org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareGrindstoneEvent(this.getBukkitView(), this.computeResult(this.repairSlots.getItem(0), this.repairSlots.getItem(1))); // CraftBukkit
-+        this.sendAllDataToRemote(); // CraftBukkit - SPIGOT-6686: Always send completed inventory to stay in sync with client
-         this.broadcastChanges();
-     }
- 
-@@ -218,7 +252,7 @@
-     }
- 
-     @Override
--    public void removed(Player player) {
-+    public void removed(net.minecraft.world.entity.player.Player player) {
-         super.removed(player);
-         this.access.execute((world, blockposition) -> {
-             this.clearContainer(player, this.repairSlots);
-@@ -226,12 +260,13 @@
-     }
- 
-     @Override
--    public boolean stillValid(Player player) {
-+    public boolean stillValid(net.minecraft.world.entity.player.Player player) {
-+        if (!this.checkReachable) return true; // CraftBukkit
-         return stillValid(this.access, player, Blocks.GRINDSTONE);
-     }
- 
-     @Override
--    public ItemStack quickMoveStack(Player player, int slot) {
-+    public ItemStack quickMoveStack(net.minecraft.world.entity.player.Player player, int slot) {
-         ItemStack itemstack = ItemStack.EMPTY;
-         Slot slot1 = (Slot) this.slots.get(slot);
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/HopperMenu.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/HopperMenu.java.patch
deleted file mode 100644
index 2174c38e81..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/HopperMenu.java.patch
+++ /dev/null
@@ -1,51 +0,0 @@
---- a/net/minecraft/world/inventory/HopperMenu.java
-+++ b/net/minecraft/world/inventory/HopperMenu.java
-@@ -6,11 +6,32 @@
- import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.item.ItemStack;
- 
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.inventory.CraftInventory;
-+import org.bukkit.craftbukkit.inventory.CraftInventoryView;
-+// CraftBukkit end
-+
- public class HopperMenu extends AbstractContainerMenu {
- 
-     public static final int CONTAINER_SIZE = 5;
-     private final Container hopper;
- 
-+    // CraftBukkit start
-+    private CraftInventoryView bukkitEntity = null;
-+    private Inventory player;
-+
-+    @Override
-+    public CraftInventoryView getBukkitView() {
-+        if (this.bukkitEntity != null) {
-+            return this.bukkitEntity;
-+        }
-+
-+        CraftInventory inventory = new CraftInventory(this.hopper);
-+        this.bukkitEntity = new CraftInventoryView(this.player.player.getBukkitEntity(), inventory, this);
-+        return this.bukkitEntity;
-+    }
-+    // CraftBukkit end
-+
-     public HopperMenu(int syncId, Inventory playerInventory) {
-         this(syncId, playerInventory, new SimpleContainer(5));
-     }
-@@ -18,6 +39,7 @@
-     public HopperMenu(int syncId, Inventory playerInventory, Container inventory) {
-         super(MenuType.HOPPER, syncId);
-         this.hopper = inventory;
-+        this.player = playerInventory; // CraftBukkit - save player
-         checkContainerSize(inventory, 5);
-         inventory.startOpen(playerInventory.player);
- 
-@@ -30,6 +52,7 @@
- 
-     @Override
-     public boolean stillValid(Player player) {
-+        if (!this.checkReachable) return true; // CraftBukkit
-         return this.hopper.stillValid(player);
-     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/HorseInventoryMenu.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/HorseInventoryMenu.java.patch
deleted file mode 100644
index 2657cf4ccb..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/HorseInventoryMenu.java.patch
+++ /dev/null
@@ -1,53 +0,0 @@
---- a/net/minecraft/world/inventory/HorseInventoryMenu.java
-+++ b/net/minecraft/world/inventory/HorseInventoryMenu.java
-@@ -11,6 +11,11 @@
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.item.Items;
- 
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.inventory.CraftInventoryView;
-+import org.bukkit.inventory.InventoryView;
-+// CraftBukkit end
-+
- public class HorseInventoryMenu extends AbstractContainerMenu {
- 
-     static final ResourceLocation SADDLE_SLOT_SPRITE = ResourceLocation.withDefaultNamespace("container/slot/saddle");
-@@ -22,13 +27,28 @@
-     public static final int SLOT_BODY_ARMOR = 1;
-     private static final int SLOT_HORSE_INVENTORY_START = 2;
- 
-+    // CraftBukkit start
-+    org.bukkit.craftbukkit.inventory.CraftInventoryView bukkitEntity;
-+    Inventory player;
-+
-+    @Override
-+    public InventoryView getBukkitView() {
-+        if (this.bukkitEntity != null) {
-+            return this.bukkitEntity;
-+        }
-+
-+        return this.bukkitEntity = new CraftInventoryView(this.player.player.getBukkitEntity(), this.horseContainer.getOwner().getInventory(), this);
-+    }
-+
-     public HorseInventoryMenu(int syncId, Inventory playerInventory, Container inventory, final AbstractHorse entity, int slotColumnCount) {
-         super((MenuType) null, syncId);
-+        this.player = playerInventory;
-+        // CraftBukkit end
-         this.horseContainer = inventory;
-         this.armorContainer = entity.getBodyArmorAccess();
-         this.horse = entity;
-         inventory.startOpen(playerInventory.player);
--        this.addSlot(new Slot(this, inventory, 0, 8, 18) {
-+        this.addSlot(new Slot(inventory, 0, 8, 18) { // CraftBukkit - decompile error
-             @Override
-             public boolean mayPlace(ItemStack stack) {
-                 return stack.is(Items.SADDLE) && !this.hasItem() && entity.isSaddleable();
-@@ -46,7 +66,7 @@
-         });
-         ResourceLocation minecraftkey = entity instanceof Llama ? HorseInventoryMenu.LLAMA_ARMOR_SLOT_SPRITE : HorseInventoryMenu.ARMOR_SLOT_SPRITE;
- 
--        this.addSlot(new ArmorSlot(this, this.armorContainer, entity, EquipmentSlot.BODY, 0, 8, 36, minecraftkey) {
-+        this.addSlot(new ArmorSlot(this.armorContainer, entity, EquipmentSlot.BODY, 0, 8, 36, minecraftkey) { // CraftBukkit - decompile error
-             @Override
-             public boolean mayPlace(ItemStack stack) {
-                 return entity.isEquippableInSlot(stack, EquipmentSlot.BODY);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/InventoryMenu.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/InventoryMenu.java.patch
deleted file mode 100644
index 78a5d31f7d..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/InventoryMenu.java.patch
+++ /dev/null
@@ -1,64 +0,0 @@
---- a/net/minecraft/world/inventory/InventoryMenu.java
-+++ b/net/minecraft/world/inventory/InventoryMenu.java
-@@ -2,6 +2,7 @@
- 
- import java.util.List;
- import java.util.Map;
-+import net.minecraft.network.chat.Component;
- import net.minecraft.resources.ResourceLocation;
- import net.minecraft.server.level.ServerLevel;
- import net.minecraft.world.Container;
-@@ -11,6 +12,9 @@
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.item.crafting.RecipeHolder;
- import net.minecraft.world.level.Level;
-+import org.bukkit.craftbukkit.inventory.CraftInventoryCrafting;
-+import org.bukkit.craftbukkit.inventory.CraftInventoryView;
-+// CraftBukkit end
- 
- public class InventoryMenu extends AbstractCraftingMenu {
- 
-@@ -38,9 +42,15 @@
-     private static final EquipmentSlot[] SLOT_IDS = new EquipmentSlot[]{EquipmentSlot.HEAD, EquipmentSlot.CHEST, EquipmentSlot.LEGS, EquipmentSlot.FEET};
-     public final boolean active;
-     private final Player owner;
-+    // CraftBukkit start
-+    private CraftInventoryView bukkitEntity = null;
-+    // CraftBukkit end
- 
-     public InventoryMenu(Inventory inventory, boolean onServer, final Player owner) {
--        super((MenuType) null, 0, 2, 2);
-+        // CraftBukkit start
-+        super((MenuType) null, 0, 2, 2, inventory); // CraftBukkit - save player
-+        this.setTitle(Component.translatable("container.crafting")); // SPIGOT-4722: Allocate title for player inventory
-+        // CraftBukkit end
-         this.active = onServer;
-         this.owner = owner;
-         this.addResultSlot(owner, 154, 28);
-@@ -54,7 +64,7 @@
-         }
- 
-         this.addStandardInventorySlots(inventory, 8, 84);
--        this.addSlot(new Slot(this, inventory, 40, 77, 62) {
-+        this.addSlot(new Slot(inventory, 40, 77, 62) { // CraftBukkit - decompile error
-             @Override
-             public void setByPlayer(ItemStack stack, ItemStack previousStack) {
-                 owner.onEquipItem(EquipmentSlot.OFFHAND, previousStack, stack);
-@@ -190,4 +200,17 @@
-     protected Player owner() {
-         return this.owner;
-     }
-+
-+    // CraftBukkit start
-+    @Override
-+    public CraftInventoryView getBukkitView() {
-+        if (this.bukkitEntity != null) {
-+            return this.bukkitEntity;
-+        }
-+
-+        CraftInventoryCrafting inventory = new CraftInventoryCrafting(this.craftSlots, this.resultSlots);
-+        this.bukkitEntity = new CraftInventoryView(this.owner.getBukkitEntity(), inventory, this);
-+        return this.bukkitEntity;
-+    }
-+    // CraftBukkit end
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/LecternMenu.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/LecternMenu.java.patch
deleted file mode 100644
index 667c188455..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/LecternMenu.java.patch
+++ /dev/null
@@ -1,143 +0,0 @@
---- a/net/minecraft/world/inventory/LecternMenu.java
-+++ b/net/minecraft/world/inventory/LecternMenu.java
-@@ -2,11 +2,33 @@
- 
- import net.minecraft.world.Container;
- import net.minecraft.world.SimpleContainer;
--import net.minecraft.world.entity.player.Player;
-+import net.minecraft.world.entity.player.Inventory;
- import net.minecraft.world.item.ItemStack;
-+import net.minecraft.world.level.block.entity.LecternBlockEntity.LecternInventory;
-+import org.bukkit.Bukkit;
-+import org.bukkit.craftbukkit.inventory.CraftInventoryLectern;
-+import org.bukkit.craftbukkit.inventory.view.CraftLecternView;
-+import org.bukkit.entity.Player;
-+import org.bukkit.event.player.PlayerTakeLecternBookEvent;
-+// CraftBukkit end
- 
- public class LecternMenu extends AbstractContainerMenu {
- 
-+    // CraftBukkit start
-+    private CraftLecternView bukkitEntity = null;
-+    private Player player;
-+
-+    @Override
-+    public CraftLecternView getBukkitView() {
-+        if (this.bukkitEntity != null) {
-+            return this.bukkitEntity;
-+        }
-+
-+        CraftInventoryLectern inventory = new CraftInventoryLectern(this.lectern);
-+        this.bukkitEntity = new CraftLecternView(this.player, inventory, this);
-+        return this.bukkitEntity;
-+    }
-+    // CraftBukkit end
-     private static final int DATA_COUNT = 1;
-     private static final int SLOT_COUNT = 1;
-     public static final int BUTTON_PREV_PAGE = 1;
-@@ -16,29 +38,33 @@
-     private final Container lectern;
-     private final ContainerData lecternData;
- 
--    public LecternMenu(int syncId) {
--        this(syncId, new SimpleContainer(1), new SimpleContainerData(1));
-+    // CraftBukkit start - add player
-+    public LecternMenu(int i, Inventory playerinventory) {
-+        this(i, new SimpleContainer(1), new SimpleContainerData(1), playerinventory);
-     }
- 
--    public LecternMenu(int syncId, Container inventory, ContainerData propertyDelegate) {
--        super(MenuType.LECTERN, syncId);
--        checkContainerSize(inventory, 1);
--        checkContainerDataCount(propertyDelegate, 1);
--        this.lectern = inventory;
--        this.lecternData = propertyDelegate;
--        this.addSlot(new Slot(inventory, 0, 0, 0) {
-+    public LecternMenu(int i, Container iinventory, ContainerData icontainerproperties, Inventory playerinventory) {
-+        // CraftBukkit end
-+        super(MenuType.LECTERN, i);
-+        checkContainerSize(iinventory, 1);
-+        checkContainerDataCount(icontainerproperties, 1);
-+        this.lectern = iinventory;
-+        this.lecternData = icontainerproperties;
-+        this.addSlot(new Slot(iinventory, 0, 0, 0) {
-             @Override
-             public void setChanged() {
-                 super.setChanged();
-                 LecternMenu.this.slotsChanged(this.container);
-             }
-         });
--        this.addDataSlots(propertyDelegate);
-+        this.addDataSlots(icontainerproperties);
-+        this.player = (Player) playerinventory.player.getBukkitEntity(); // CraftBukkit
-     }
- 
-     @Override
--    public boolean clickMenuButton(Player player, int id) {
-+    public boolean clickMenuButton(net.minecraft.world.entity.player.Player player, int id) {
-         int j;
-+        io.papermc.paper.event.player.PlayerLecternPageChangeEvent playerLecternPageChangeEvent; CraftInventoryLectern bukkitView; // Paper - Add PlayerLecternPageChangeEvent
- 
-         if (id >= 100) {
-             j = id - 100;
-@@ -48,17 +74,38 @@
-             switch (id) {
-                 case 1:
-                     j = this.lecternData.get(0);
--                    this.setData(0, j - 1);
-+                    // Paper start - Add PlayerLecternPageChangeEvent
-+                    bukkitView = (CraftInventoryLectern) getBukkitView().getTopInventory();
-+                    playerLecternPageChangeEvent = new io.papermc.paper.event.player.PlayerLecternPageChangeEvent((org.bukkit.entity.Player) player.getBukkitEntity(), bukkitView.getHolder(), bukkitView.getBook(), io.papermc.paper.event.player.PlayerLecternPageChangeEvent.PageChangeDirection.LEFT, j, j - 1);
-+                    if (!playerLecternPageChangeEvent.callEvent()) {
-+                        return false;
-+                    }
-+                    this.setData(0, playerLecternPageChangeEvent.getNewPage());
-+                    // Paper end - Add PlayerLecternPageChangeEvent
-                     return true;
-                 case 2:
-                     j = this.lecternData.get(0);
--                    this.setData(0, j + 1);
-+                    // Paper start - Add PlayerLecternPageChangeEvent
-+                    bukkitView = (CraftInventoryLectern) getBukkitView().getTopInventory();
-+                    playerLecternPageChangeEvent = new io.papermc.paper.event.player.PlayerLecternPageChangeEvent((org.bukkit.entity.Player) player.getBukkitEntity(), bukkitView.getHolder(), bukkitView.getBook(), io.papermc.paper.event.player.PlayerLecternPageChangeEvent.PageChangeDirection.RIGHT, j, j + 1);
-+                    if (!playerLecternPageChangeEvent.callEvent()) {
-+                        return false;
-+                    }
-+                    this.setData(0, playerLecternPageChangeEvent.getNewPage());
-+                    // Paper end - Add PlayerLecternPageChangeEvent
-                     return true;
-                 case 3:
-                     if (!player.mayBuild()) {
-                         return false;
-                     }
- 
-+                    // CraftBukkit start - Event for taking the book
-+                    PlayerTakeLecternBookEvent event = new PlayerTakeLecternBookEvent(this.player, ((CraftInventoryLectern) this.getBukkitView().getTopInventory()).getHolder());
-+                    Bukkit.getServer().getPluginManager().callEvent(event);
-+                    if (event.isCancelled()) {
-+                        return false;
-+                    }
-+                    // CraftBukkit end
-                     ItemStack itemstack = this.lectern.removeItemNoUpdate(0);
- 
-                     this.lectern.setChanged();
-@@ -74,7 +121,7 @@
-     }
- 
-     @Override
--    public ItemStack quickMoveStack(Player player, int slot) {
-+    public ItemStack quickMoveStack(net.minecraft.world.entity.player.Player player, int slot) {
-         return ItemStack.EMPTY;
-     }
- 
-@@ -85,7 +132,9 @@
-     }
- 
-     @Override
--    public boolean stillValid(Player player) {
-+    public boolean stillValid(net.minecraft.world.entity.player.Player player) {
-+        if (this.lectern instanceof LecternInventory && !((LecternInventory) this.lectern).getLectern().hasBook()) return false; // CraftBukkit
-+        if (!this.checkReachable) return true; // CraftBukkit
-         return this.lectern.stillValid(player);
-     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/LoomMenu.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/LoomMenu.java.patch
deleted file mode 100644
index 1d534db5e0..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/LoomMenu.java.patch
+++ /dev/null
@@ -1,203 +0,0 @@
---- a/net/minecraft/world/inventory/LoomMenu.java
-+++ b/net/minecraft/world/inventory/LoomMenu.java
-@@ -12,7 +12,6 @@
- import net.minecraft.world.Container;
- import net.minecraft.world.SimpleContainer;
- import net.minecraft.world.entity.player.Inventory;
--import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.item.BannerItem;
- import net.minecraft.world.item.BannerPatternItem;
- import net.minecraft.world.item.DyeColor;
-@@ -22,9 +21,30 @@
- import net.minecraft.world.level.block.Blocks;
- import net.minecraft.world.level.block.entity.BannerPattern;
- import net.minecraft.world.level.block.entity.BannerPatternLayers;
-+// CraftBukkit start
-+import org.bukkit.Location;
-+import org.bukkit.craftbukkit.inventory.CraftInventoryLoom;
-+import org.bukkit.craftbukkit.inventory.view.CraftLoomView;
-+import org.bukkit.entity.Player;
-+// CraftBukkit end
- 
- public class LoomMenu extends AbstractContainerMenu {
- 
-+    // CraftBukkit start
-+    private CraftLoomView bukkitEntity = null;
-+    private Player player;
-+
-+    @Override
-+    public CraftLoomView getBukkitView() {
-+        if (this.bukkitEntity != null) {
-+            return this.bukkitEntity;
-+        }
-+
-+        CraftInventoryLoom inventory = new CraftInventoryLoom(this.inputContainer, this.outputContainer);
-+        this.bukkitEntity = new CraftLoomView(this.player, inventory, this);
-+        return this.bukkitEntity;
-+    }
-+    // CraftBukkit end
-     private static final int PATTERN_NOT_SET = -1;
-     private static final int INV_SLOT_START = 4;
-     private static final int INV_SLOT_END = 31;
-@@ -53,35 +73,49 @@
-         this.selectablePatterns = List.of();
-         this.slotUpdateListener = () -> {
-         };
--        this.inputContainer = new SimpleContainer(3) {
-+        this.inputContainer = new SimpleContainer(this.createBlockHolder(context), 3) { // Paper - Add missing InventoryHolders
-             @Override
-             public void setChanged() {
-                 super.setChanged();
-                 LoomMenu.this.slotsChanged(this);
-                 LoomMenu.this.slotUpdateListener.run();
-             }
-+
-+            // CraftBukkit start
-+            @Override
-+            public Location getLocation() {
-+                return context.getLocation();
-+            }
-+            // CraftBukkit end
-         };
--        this.outputContainer = new SimpleContainer(1) {
-+        this.outputContainer = new SimpleContainer(this.createBlockHolder(context), 1) { // Paper - Add missing InventoryHolders
-             @Override
-             public void setChanged() {
-                 super.setChanged();
-                 LoomMenu.this.slotUpdateListener.run();
-             }
-+
-+            // CraftBukkit start
-+            @Override
-+            public Location getLocation() {
-+                return context.getLocation();
-+            }
-+            // CraftBukkit end
-         };
-         this.access = context;
--        this.bannerSlot = this.addSlot(new Slot(this, this.inputContainer, 0, 13, 26) {
-+        this.bannerSlot = this.addSlot(new Slot(this.inputContainer, 0, 13, 26) { // CraftBukkit - decompile error
-             @Override
-             public boolean mayPlace(ItemStack stack) {
-                 return stack.getItem() instanceof BannerItem;
-             }
-         });
--        this.dyeSlot = this.addSlot(new Slot(this, this.inputContainer, 1, 33, 26) {
-+        this.dyeSlot = this.addSlot(new Slot(this.inputContainer, 1, 33, 26) { // CraftBukkit - decompile error
-             @Override
-             public boolean mayPlace(ItemStack stack) {
-                 return stack.getItem() instanceof DyeItem;
-             }
-         });
--        this.patternSlot = this.addSlot(new Slot(this, this.inputContainer, 2, 23, 45) {
-+        this.patternSlot = this.addSlot(new Slot(this.inputContainer, 2, 23, 45) { // CraftBukkit - decompile error
-             @Override
-             public boolean mayPlace(ItemStack stack) {
-                 return stack.getItem() instanceof BannerPatternItem;
-@@ -94,7 +128,7 @@
-             }
- 
-             @Override
--            public void onTake(Player player, ItemStack stack) {
-+            public void onTake(net.minecraft.world.entity.player.Player player, ItemStack stack) {
-                 LoomMenu.this.bannerSlot.remove(1);
-                 LoomMenu.this.dyeSlot.remove(1);
-                 if (!LoomMenu.this.bannerSlot.hasItem() || !LoomMenu.this.dyeSlot.hasItem()) {
-@@ -105,7 +139,7 @@
-                     long j = world.getGameTime();
- 
-                     if (LoomMenu.this.lastSoundTime != j) {
--                        world.playSound((Player) null, blockposition, SoundEvents.UI_LOOM_TAKE_RESULT, SoundSource.BLOCKS, 1.0F, 1.0F);
-+                        world.playSound((net.minecraft.world.entity.player.Player) null, blockposition, SoundEvents.UI_LOOM_TAKE_RESULT, SoundSource.BLOCKS, 1.0F, 1.0F);
-                         LoomMenu.this.lastSoundTime = j;
-                     }
- 
-@@ -116,18 +150,44 @@
-         this.addStandardInventorySlots(playerInventory, 8, 84);
-         this.addDataSlot(this.selectedBannerPatternIndex);
-         this.patternGetter = playerInventory.player.registryAccess().lookupOrThrow(Registries.BANNER_PATTERN);
-+        this.player = (Player) playerInventory.player.getBukkitEntity(); // CraftBukkit
-     }
- 
-     @Override
--    public boolean stillValid(Player player) {
-+    public boolean stillValid(net.minecraft.world.entity.player.Player player) {
-+        if (!this.checkReachable) return true; // CraftBukkit
-         return stillValid(this.access, player, Blocks.LOOM);
-     }
- 
-     @Override
--    public boolean clickMenuButton(Player player, int id) {
-+    public boolean clickMenuButton(net.minecraft.world.entity.player.Player player, int id) {
-         if (id >= 0 && id < this.selectablePatterns.size()) {
--            this.selectedBannerPatternIndex.set(id);
--            this.setupResultSlot((Holder) this.selectablePatterns.get(id));
-+            // Paper start - Add PlayerLoomPatternSelectEvent
-+            int selectablePatternIndex = id;
-+            io.papermc.paper.event.player.PlayerLoomPatternSelectEvent event = new io.papermc.paper.event.player.PlayerLoomPatternSelectEvent((Player) player.getBukkitEntity(), ((CraftInventoryLoom) getBukkitView().getTopInventory()), org.bukkit.craftbukkit.block.banner.CraftPatternType.minecraftHolderToBukkit(this.selectablePatterns.get(selectablePatternIndex)));
-+            if (!event.callEvent()) {
-+                player.containerMenu.sendAllDataToRemote();
-+                return false;
-+            }
-+            final Holder<BannerPattern> eventPattern = org.bukkit.craftbukkit.block.banner.CraftPatternType.bukkitToMinecraftHolder(event.getPatternType());
-+            Holder<BannerPattern> selectedPattern = null;
-+            for (int i = 0; i < this.selectablePatterns.size(); i++) {
-+                final Holder<BannerPattern> holder = this.selectablePatterns.get(i);
-+                if (eventPattern.equals(holder)) {
-+                    selectablePatternIndex = i;
-+                    selectedPattern = holder;
-+                    break;
-+                }
-+            }
-+            if (selectedPattern == null) {
-+                selectedPattern = eventPattern;
-+                selectablePatternIndex = -1;
-+            }
-+
-+            player.containerMenu.sendAllDataToRemote();
-+            this.selectedBannerPatternIndex.set(selectablePatternIndex);
-+            this.setupResultSlot(java.util.Objects.requireNonNull(selectedPattern, "selectedPattern was null, this is unexpected"));
-+            // Paper end - Add PlayerLoomPatternSelectEvent
-             return true;
-         } else {
-             return false;
-@@ -201,7 +261,8 @@
-                 this.resultSlot.set(ItemStack.EMPTY);
-             }
- 
--            this.broadcastChanges();
-+            // this.broadcastChanges(); // Paper - Add PrepareResultEvent; done below
-+            org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareResultEvent(this, 3); // Paper - Add PrepareResultEvent
-         } else {
-             this.resultSlot.set(ItemStack.EMPTY);
-             this.selectablePatterns = List.of();
-@@ -222,7 +283,7 @@
-     }
- 
-     @Override
--    public ItemStack quickMoveStack(Player player, int slot) {
-+    public ItemStack quickMoveStack(net.minecraft.world.entity.player.Player player, int slot) {
-         ItemStack itemstack = ItemStack.EMPTY;
-         Slot slot1 = (Slot) this.slots.get(slot);
- 
-@@ -277,7 +338,7 @@
-     }
- 
-     @Override
--    public void removed(Player player) {
-+    public void removed(net.minecraft.world.entity.player.Player player) {
-         super.removed(player);
-         this.access.execute((world, blockposition) -> {
-             this.clearContainer(player, this.inputContainer);
-@@ -294,6 +355,11 @@
-             DyeColor enumcolor = ((DyeItem) itemstack1.getItem()).getDyeColor();
- 
-             itemstack2.update(DataComponents.BANNER_PATTERNS, BannerPatternLayers.EMPTY, (bannerpatternlayers) -> {
-+                // CraftBukkit start
-+                if (bannerpatternlayers.layers().size() > 20) {
-+                    bannerpatternlayers = new BannerPatternLayers(List.copyOf(bannerpatternlayers.layers().subList(0, 20)));
-+                }
-+                // CraftBukkit end
-                 return (new BannerPatternLayers.Builder()).addAll(bannerpatternlayers).add(pattern, enumcolor).build();
-             });
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/MenuType.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/MenuType.java.patch
deleted file mode 100644
index ac3dc03175..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/MenuType.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/inventory/MenuType.java
-+++ b/net/minecraft/world/inventory/MenuType.java
-@@ -28,7 +28,7 @@
-     public static final MenuType<GrindstoneMenu> GRINDSTONE = MenuType.register("grindstone", GrindstoneMenu::new);
-     public static final MenuType<HopperMenu> HOPPER = MenuType.register("hopper", HopperMenu::new);
-     public static final MenuType<LecternMenu> LECTERN = MenuType.register("lectern", (i, playerinventory) -> {
--        return new LecternMenu(i);
-+        return new LecternMenu(i, playerinventory); // CraftBukkit
-     });
-     public static final MenuType<LoomMenu> LOOM = MenuType.register("loom", LoomMenu::new);
-     public static final MenuType<MerchantMenu> MERCHANT = MenuType.register("merchant", MerchantMenu::new);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/MerchantContainer.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/MerchantContainer.java.patch
deleted file mode 100644
index 82bbe5e4c7..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/MerchantContainer.java.patch
+++ /dev/null
@@ -1,70 +0,0 @@
---- a/net/minecraft/world/inventory/MerchantContainer.java
-+++ b/net/minecraft/world/inventory/MerchantContainer.java
-@@ -5,11 +5,20 @@
- import net.minecraft.core.NonNullList;
- import net.minecraft.world.Container;
- import net.minecraft.world.ContainerHelper;
-+import net.minecraft.world.entity.npc.AbstractVillager;
-+import net.minecraft.world.entity.npc.Villager;
- import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.item.trading.Merchant;
- import net.minecraft.world.item.trading.MerchantOffer;
- import net.minecraft.world.item.trading.MerchantOffers;
-+// CraftBukkit start
-+import java.util.List;
-+import org.bukkit.Location;
-+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
-+import org.bukkit.craftbukkit.entity.CraftAbstractVillager;
-+import org.bukkit.entity.HumanEntity;
-+// CraftBukkit end
- 
- public class MerchantContainer implements Container {
- 
-@@ -20,6 +29,46 @@
-     public int selectionHint;
-     private int futureXp;
- 
-+    // CraftBukkit start - add fields and methods
-+    public List<HumanEntity> transaction = new java.util.ArrayList<HumanEntity>();
-+    private int maxStack = MAX_STACK;
-+
-+    public List<ItemStack> getContents() {
-+        return this.itemStacks;
-+    }
-+
-+    public void onOpen(CraftHumanEntity who) {
-+        this.transaction.add(who);
-+    }
-+
-+    public void onClose(CraftHumanEntity who) {
-+        this.transaction.remove(who);
-+        this.merchant.setTradingPlayer((Player) null); // SPIGOT-4860
-+    }
-+
-+    public List<HumanEntity> getViewers() {
-+        return this.transaction;
-+    }
-+
-+    @Override
-+    public int getMaxStackSize() {
-+        return this.maxStack;
-+    }
-+
-+    public void setMaxStackSize(int i) {
-+        this.maxStack = i;
-+    }
-+
-+    public org.bukkit.inventory.InventoryHolder getOwner() {
-+        return (this.merchant instanceof AbstractVillager) ? (CraftAbstractVillager) ((AbstractVillager) this.merchant).getBukkitEntity() : null;
-+    }
-+
-+    @Override
-+    public Location getLocation() {
-+        return (this.merchant instanceof AbstractVillager) ? ((AbstractVillager) this.merchant).getBukkitEntity().getLocation() : null; // Paper - Fix inventories returning null Locations
-+    }
-+    // CraftBukkit end
-+
-     public MerchantContainer(Merchant merchant) {
-         this.itemStacks = NonNullList.withSize(3, ItemStack.EMPTY);
-         this.merchant = merchant;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/MerchantMenu.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/MerchantMenu.java.patch
deleted file mode 100644
index 9fecebd95a..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/MerchantMenu.java.patch
+++ /dev/null
@@ -1,92 +0,0 @@
---- a/net/minecraft/world/inventory/MerchantMenu.java
-+++ b/net/minecraft/world/inventory/MerchantMenu.java
-@@ -12,6 +12,7 @@
- import net.minecraft.world.item.trading.Merchant;
- import net.minecraft.world.item.trading.MerchantOffer;
- import net.minecraft.world.item.trading.MerchantOffers;
-+import org.bukkit.craftbukkit.inventory.view.CraftMerchantView; // CraftBukkit
- 
- public class MerchantMenu extends AbstractContainerMenu {
- 
-@@ -32,6 +33,19 @@
-     private boolean showProgressBar;
-     private boolean canRestock;
- 
-+    // CraftBukkit start
-+    private CraftMerchantView bukkitEntity = null;
-+    private Inventory player;
-+
-+    @Override
-+    public CraftMerchantView getBukkitView() {
-+        if (this.bukkitEntity == null) {
-+            this.bukkitEntity = new CraftMerchantView(this.player.player.getBukkitEntity(), new org.bukkit.craftbukkit.inventory.CraftInventoryMerchant(this.trader, this.tradeContainer), this, this.trader);
-+        }
-+        return this.bukkitEntity;
-+    }
-+    // CraftBukkit end
-+
-     public MerchantMenu(int syncId, Inventory playerInventory) {
-         this(syncId, playerInventory, new ClientSideMerchant(playerInventory.player));
-     }
-@@ -43,6 +57,7 @@
-         this.addSlot(new Slot(this.tradeContainer, 0, 136, 37));
-         this.addSlot(new Slot(this.tradeContainer, 1, 162, 37));
-         this.addSlot(new MerchantResultSlot(playerInventory.player, merchant, this.tradeContainer, 2, 220, 37));
-+        this.player = playerInventory; // CraftBukkit - save player
-         this.addStandardInventorySlots(playerInventory, 108, 84);
-     }
- 
-@@ -108,12 +123,12 @@
- 
-             itemstack = itemstack1.copy();
-             if (slot == 2) {
--                if (!this.moveItemStackTo(itemstack1, 3, 39, true)) {
-+                if (!this.moveItemStackTo(itemstack1, 3, 39, true, true)) { // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
-                     return ItemStack.EMPTY;
-                 }
- 
--                slot1.onQuickCraft(itemstack1, itemstack);
--                this.playTradeSound();
-+                //  slot1.onQuickCraft(itemstack1, itemstack); // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent; moved to after the non-check moveItemStackTo call
-+                // this.playTradeSound();
-             } else if (slot != 0 && slot != 1) {
-                 if (slot >= 3 && slot < 30) {
-                     if (!this.moveItemStackTo(itemstack1, 30, 39, false)) {
-@@ -126,6 +141,7 @@
-                 return ItemStack.EMPTY;
-             }
- 
-+            if (slot != 2) { // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent; moved down for slot 2
-             if (itemstack1.isEmpty()) {
-                 slot1.setByPlayer(ItemStack.EMPTY);
-             } else {
-@@ -137,13 +153,28 @@
-             }
- 
-             slot1.onTake(player, itemstack1);
-+            } // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent; handle slot 2
-+            if (slot == 2) { // is merchant result slot
-+                slot1.onTake(player, itemstack1);
-+                if (itemstack1.isEmpty()) {
-+                    slot1.set(ItemStack.EMPTY);
-+                    return ItemStack.EMPTY;
-+                }
-+
-+                this.moveItemStackTo(itemstack1, 3, 39, true, false); // This should always succeed because it's checked above
-+
-+                slot1.onQuickCraft(itemstack1, itemstack);
-+                this.playTradeSound();
-+                slot1.set(ItemStack.EMPTY); // itemstack1 should ALWAYS be empty
-+            }
-+            // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent
-         }
- 
-         return itemstack;
-     }
- 
-     private void playTradeSound() {
--        if (!this.trader.isClientSide()) {
-+        if (!this.trader.isClientSide() && this.trader instanceof Entity) { // CraftBukkit - SPIGOT-5035
-             Entity entity = (Entity) this.trader;
- 
-             entity.level().playLocalSound(entity.getX(), entity.getY(), entity.getZ(), this.trader.getNotifyTradeSound(), SoundSource.NEUTRAL, 1.0F, 1.0F, false);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/PlayerEnderChestContainer.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/PlayerEnderChestContainer.java.patch
deleted file mode 100644
index e83e892d8e..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/PlayerEnderChestContainer.java.patch
+++ /dev/null
@@ -1,36 +0,0 @@
---- a/net/minecraft/world/inventory/PlayerEnderChestContainer.java
-+++ b/net/minecraft/world/inventory/PlayerEnderChestContainer.java
-@@ -8,14 +8,32 @@
- import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.level.block.entity.EnderChestBlockEntity;
-+// CraftBukkit start
-+import org.bukkit.Location;
-+import org.bukkit.craftbukkit.util.CraftLocation;
-+import org.bukkit.inventory.InventoryHolder;
-+// CraftBukkit end
- 
- public class PlayerEnderChestContainer extends SimpleContainer {
- 
-     @Nullable
-     private EnderChestBlockEntity activeChest;
-+    // CraftBukkit start
-+    private final Player owner;
- 
--    public PlayerEnderChestContainer() {
-+    public InventoryHolder getBukkitOwner() {
-+        return this.owner.getBukkitEntity();
-+    }
-+
-+    @Override
-+    public Location getLocation() {
-+        return this.activeChest != null ? CraftLocation.toBukkit(this.activeChest.getBlockPos(), this.activeChest.getLevel().getWorld()) : null;
-+    }
-+
-+    public PlayerEnderChestContainer(Player owner) {
-         super(27);
-+        this.owner = owner;
-+        // CraftBukkit end
-     }
- 
-     public void setActiveChest(EnderChestBlockEntity blockEntity) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/ShulkerBoxMenu.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/ShulkerBoxMenu.java.patch
deleted file mode 100644
index 4efe4724bc..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/ShulkerBoxMenu.java.patch
+++ /dev/null
@@ -1,49 +0,0 @@
---- a/net/minecraft/world/inventory/ShulkerBoxMenu.java
-+++ b/net/minecraft/world/inventory/ShulkerBoxMenu.java
-@@ -6,11 +6,30 @@
- import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.item.ItemStack;
- 
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.inventory.CraftInventory;
-+import org.bukkit.craftbukkit.inventory.CraftInventoryView;
-+// CraftBukkit end
-+
- public class ShulkerBoxMenu extends AbstractContainerMenu {
- 
-     private static final int CONTAINER_SIZE = 27;
-     private final Container container;
-+    // CraftBukkit start
-+    private CraftInventoryView bukkitEntity;
-+    private Inventory player;
- 
-+    @Override
-+    public CraftInventoryView getBukkitView() {
-+        if (this.bukkitEntity != null) {
-+            return this.bukkitEntity;
-+        }
-+
-+        this.bukkitEntity = new CraftInventoryView(this.player.player.getBukkitEntity(), new CraftInventory(this.container), this);
-+        return this.bukkitEntity;
-+    }
-+    // CraftBukkit end
-+
-     public ShulkerBoxMenu(int syncId, Inventory playerInventory) {
-         this(syncId, playerInventory, new SimpleContainer(27));
-     }
-@@ -19,6 +38,7 @@
-         super(MenuType.SHULKER_BOX, syncId);
-         checkContainerSize(inventory, 27);
-         this.container = inventory;
-+        this.player = playerInventory; // CraftBukkit - save player
-         inventory.startOpen(playerInventory.player);
-         boolean flag = true;
-         boolean flag1 = true;
-@@ -34,6 +54,7 @@
- 
-     @Override
-     public boolean stillValid(Player player) {
-+        if (!this.checkReachable) return true; // CraftBukkit
-         return this.container.stillValid(player);
-     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/SmithingMenu.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/SmithingMenu.java.patch
deleted file mode 100644
index 46bc8ef32c..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/SmithingMenu.java.patch
+++ /dev/null
@@ -1,66 +0,0 @@
---- a/net/minecraft/world/inventory/SmithingMenu.java
-+++ b/net/minecraft/world/inventory/SmithingMenu.java
-@@ -17,6 +17,7 @@
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.block.Blocks;
- import net.minecraft.world.level.block.state.BlockState;
-+import org.bukkit.craftbukkit.inventory.CraftInventoryView; // CraftBukkit
- 
- public class SmithingMenu extends ItemCombinerMenu {
- 
-@@ -34,6 +35,9 @@
-     private final RecipePropertySet templateItemTest;
-     private final RecipePropertySet additionItemTest;
-     private final DataSlot hasRecipeError;
-+    // CraftBukkit start
-+    private CraftInventoryView bukkitEntity;
-+    // CraftBukkit end
- 
-     public SmithingMenu(int syncId, Inventory playerInventory) {
-         this(syncId, playerInventory, ContainerLevelAccess.NULL);
-@@ -111,13 +115,14 @@
-             this.hasRecipeError.set(flag ? 1 : 0);
-         }
- 
-+        org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareResultEvent(this, RESULT_SLOT); // Paper - Add PrepareResultEvent
-     }
- 
-     @Override
-     public void createResult() {
-         SmithingRecipeInput smithingrecipeinput = this.createRecipeInput();
-         Level world = this.level;
--        Optional optional;
-+        Optional<RecipeHolder<SmithingRecipe>> optional; // CraftBukkit - decompile error
- 
-         if (world instanceof ServerLevel worldserver) {
-             optional = worldserver.recipeAccess().getRecipeFor(RecipeType.SMITHING, smithingrecipeinput, worldserver);
-@@ -129,7 +134,9 @@
-             ItemStack itemstack = ((SmithingRecipe) recipeholder.value()).assemble(smithingrecipeinput, this.level.registryAccess());
- 
-             this.resultSlots.setRecipeUsed(recipeholder);
--            this.resultSlots.setItem(0, itemstack);
-+            // CraftBukkit start
-+            org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareSmithingEvent(this.getBukkitView(), itemstack);
-+            // CraftBukkit end
-         }, () -> {
-             this.resultSlots.setRecipeUsed((RecipeHolder) null);
-             this.resultSlots.setItem(0, ItemStack.EMPTY);
-@@ -149,4 +156,18 @@
-     public boolean hasRecipeError() {
-         return this.hasRecipeError.get() > 0;
-     }
-+
-+    // CraftBukkit start
-+    @Override
-+    public CraftInventoryView getBukkitView() {
-+        if (this.bukkitEntity != null) {
-+            return this.bukkitEntity;
-+        }
-+
-+        org.bukkit.craftbukkit.inventory.CraftInventory inventory = new org.bukkit.craftbukkit.inventory.CraftInventorySmithing(
-+                this.access.getLocation(), this.inputSlots, this.resultSlots);
-+        this.bukkitEntity = new CraftInventoryView(this.player.getBukkitEntity(), inventory, this);
-+        return this.bukkitEntity;
-+    }
-+    // CraftBukkit end
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/StonecutterMenu.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/StonecutterMenu.java.patch
deleted file mode 100644
index 95cd1f3220..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/StonecutterMenu.java.patch
+++ /dev/null
@@ -1,188 +0,0 @@
---- a/net/minecraft/world/inventory/StonecutterMenu.java
-+++ b/net/minecraft/world/inventory/StonecutterMenu.java
-@@ -7,7 +7,6 @@
- import net.minecraft.world.Container;
- import net.minecraft.world.SimpleContainer;
- import net.minecraft.world.entity.player.Inventory;
--import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.item.Item;
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.item.crafting.RecipeHolder;
-@@ -17,6 +16,13 @@
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.block.Blocks;
- 
-+// CraftBukkit start
-+import org.bukkit.Location;
-+import org.bukkit.craftbukkit.inventory.CraftInventoryStonecutter;
-+import org.bukkit.craftbukkit.inventory.view.CraftStonecutterView;
-+import org.bukkit.entity.Player;
-+// CraftBukkit end
-+
- public class StonecutterMenu extends AbstractContainerMenu {
- 
-     public static final int INPUT_SLOT = 0;
-@@ -36,27 +42,49 @@
-     Runnable slotUpdateListener;
-     public final Container container;
-     final ResultContainer resultContainer;
-+    // CraftBukkit start
-+    private CraftStonecutterView bukkitEntity = null;
-+    private Player player;
- 
-+    @Override
-+    public CraftStonecutterView getBukkitView() {
-+        if (this.bukkitEntity != null) {
-+            return this.bukkitEntity;
-+        }
-+
-+        CraftInventoryStonecutter inventory = new CraftInventoryStonecutter(this.container, this.resultContainer);
-+        this.bukkitEntity = new CraftStonecutterView(this.player, inventory, this);
-+        return this.bukkitEntity;
-+    }
-+    // CraftBukkit end
-+
-     public StonecutterMenu(int syncId, Inventory playerInventory) {
-         this(syncId, playerInventory, ContainerLevelAccess.NULL);
-     }
- 
-     public StonecutterMenu(int syncId, Inventory playerInventory, final ContainerLevelAccess context) {
-         super(MenuType.STONECUTTER, syncId);
--        this.selectedRecipeIndex = DataSlot.standalone();
-+        this.selectedRecipeIndex = DataSlot.shared(new int[1], 0); // Paper - Add PlayerStonecutterRecipeSelectEvent
-         this.recipesForInput = SelectableRecipe.SingleInputSet.empty();
-         this.input = ItemStack.EMPTY;
-         this.slotUpdateListener = () -> {
-         };
--        this.container = new SimpleContainer(1) {
-+        this.container = new SimpleContainer(this.createBlockHolder(context), 1) { // Paper - Add missing InventoryHolders
-             @Override
-             public void setChanged() {
-                 super.setChanged();
-                 StonecutterMenu.this.slotsChanged(this);
-                 StonecutterMenu.this.slotUpdateListener.run();
-             }
-+
-+            // CraftBukkit start
-+            @Override
-+            public Location getLocation() {
-+                return context.getLocation();
-+            }
-+            // CraftBukkit end
-         };
--        this.resultContainer = new ResultContainer();
-+        this.resultContainer = new ResultContainer(this.createBlockHolder(context)); // Paper - Add missing InventoryHolders
-         this.access = context;
-         this.level = playerInventory.player.level();
-         this.inputSlot = this.addSlot(new Slot(this.container, 0, 20, 33));
-@@ -67,7 +95,7 @@
-             }
- 
-             @Override
--            public void onTake(Player player, ItemStack stack) {
-+            public void onTake(net.minecraft.world.entity.player.Player player, ItemStack stack) {
-                 stack.onCraftedBy(player.level(), player, stack.getCount());
-                 StonecutterMenu.this.resultContainer.awardUsedRecipes(player, this.getRelevantItems());
-                 ItemStack itemstack1 = StonecutterMenu.this.inputSlot.remove(1);
-@@ -80,7 +108,7 @@
-                     long j = world.getGameTime();
- 
-                     if (StonecutterMenu.this.lastSoundTime != j) {
--                        world.playSound((Player) null, blockposition, SoundEvents.UI_STONECUTTER_TAKE_RESULT, SoundSource.BLOCKS, 1.0F, 1.0F);
-+                        world.playSound((net.minecraft.world.entity.player.Player) null, blockposition, SoundEvents.UI_STONECUTTER_TAKE_RESULT, SoundSource.BLOCKS, 1.0F, 1.0F);
-                         StonecutterMenu.this.lastSoundTime = j;
-                     }
- 
-@@ -94,6 +122,7 @@
-         });
-         this.addStandardInventorySlots(playerInventory, 8, 84);
-         this.addDataSlot(this.selectedRecipeIndex);
-+        this.player = (Player) playerInventory.player.getBukkitEntity(); // CraftBukkit
-     }
- 
-     public int getSelectedRecipeIndex() {
-@@ -113,18 +142,45 @@
-     }
- 
-     @Override
--    public boolean stillValid(Player player) {
-+    public boolean stillValid(net.minecraft.world.entity.player.Player player) {
-+        if (!this.checkReachable) return true; // CraftBukkit
-         return stillValid(this.access, player, Blocks.STONECUTTER);
-     }
- 
-     @Override
--    public boolean clickMenuButton(Player player, int id) {
-+    public boolean clickMenuButton(net.minecraft.world.entity.player.Player player, int id) {
-         if (this.selectedRecipeIndex.get() == id) {
-             return false;
-         } else {
-             if (this.isValidRecipeIndex(id)) {
--                this.selectedRecipeIndex.set(id);
--                this.setupResultSlot(id);
-+                // Paper start - Add PlayerStonecutterRecipeSelectEvent
-+                int recipeIndex = id;
-+                this.selectedRecipeIndex.set(recipeIndex);
-+                this.selectedRecipeIndex.checkAndClearUpdateFlag(); // mark as changed
-+                paperEventBlock: if (this.isValidRecipeIndex(id)) {
-+                    final Optional<RecipeHolder<StonecutterRecipe>> recipe = this.recipesForInput.entries().get(id).recipe().recipe();
-+                    if (recipe.isEmpty()) break paperEventBlock; // The recipe selected does not have an actual server recipe (presumably its the empty one). Cannot call the event, just break.
-+
-+                    io.papermc.paper.event.player.PlayerStonecutterRecipeSelectEvent event = new io.papermc.paper.event.player.PlayerStonecutterRecipeSelectEvent((Player) player.getBukkitEntity(), getBukkitView().getTopInventory(), (org.bukkit.inventory.StonecuttingRecipe) recipe.get().toBukkitRecipe());
-+                    if (!event.callEvent()) {
-+                        player.containerMenu.sendAllDataToRemote();
-+                        return false;
-+                    }
-+
-+                    net.minecraft.resources.ResourceLocation key = org.bukkit.craftbukkit.util.CraftNamespacedKey.toMinecraft(event.getStonecuttingRecipe().getKey());
-+                    if (!recipe.get().id().location().equals(key)) { // If the recipe did NOT stay the same
-+                        for (int newRecipeIndex = 0; newRecipeIndex < this.recipesForInput.entries().size(); newRecipeIndex++) {
-+                            if (this.recipesForInput.entries().get(newRecipeIndex).recipe().recipe().filter(r -> r.id().location().equals(key)).isPresent()) {
-+                                recipeIndex = newRecipeIndex;
-+                                break;
-+                            }
-+                        }
-+                    }
-+                }
-+                player.containerMenu.sendAllDataToRemote();
-+                this.selectedRecipeIndex.set(recipeIndex); // set new index, so that listeners can read it
-+                this.setupResultSlot(recipeIndex);
-+                // Paper end - Add PlayerStonecutterRecipeSelectEvent
-             }
- 
-             return true;
-@@ -144,6 +200,7 @@
-             this.setupRecipeList(itemstack);
-         }
- 
-+        org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareResultEvent(this, RESULT_SLOT); // Paper - Add PrepareResultEvent
-     }
- 
-     private void setupRecipeList(ItemStack stack) {
-@@ -158,7 +215,7 @@
-     }
- 
-     void setupResultSlot(int selectedId) {
--        Optional optional;
-+        Optional<RecipeHolder<StonecutterRecipe>> optional; // CraftBukkit - decompile error
- 
-         if (!this.recipesForInput.isEmpty() && this.isValidRecipeIndex(selectedId)) {
-             SelectableRecipe.SingleInputEntry<StonecutterRecipe> selectablerecipe_a = (SelectableRecipe.SingleInputEntry) this.recipesForInput.entries().get(selectedId);
-@@ -193,7 +250,7 @@
-     }
- 
-     @Override
--    public ItemStack quickMoveStack(Player player, int slot) {
-+    public ItemStack quickMoveStack(net.minecraft.world.entity.player.Player player, int slot) {
-         ItemStack itemstack = ItemStack.EMPTY;
-         Slot slot1 = (Slot) this.slots.get(slot);
- 
-@@ -246,7 +303,7 @@
-     }
- 
-     @Override
--    public void removed(Player player) {
-+    public void removed(net.minecraft.world.entity.player.Player player) {
-         super.removed(player);
-         this.resultContainer.removeItemNoUpdate(1);
-         this.access.execute((world, blockposition) -> {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/TransientCraftingContainer.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/TransientCraftingContainer.java.patch
deleted file mode 100644
index 67ed6a0fb4..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/inventory/TransientCraftingContainer.java.patch
+++ /dev/null
@@ -1,93 +0,0 @@
---- a/net/minecraft/world/inventory/TransientCraftingContainer.java
-+++ b/net/minecraft/world/inventory/TransientCraftingContainer.java
-@@ -3,11 +3,21 @@
- import java.util.Iterator;
- import java.util.List;
- import net.minecraft.core.NonNullList;
-+import net.minecraft.world.Container;
- import net.minecraft.world.ContainerHelper;
- import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.entity.player.StackedItemContents;
- import net.minecraft.world.item.ItemStack;
- 
-+// CraftBukkit start
-+import java.util.List;
-+import net.minecraft.world.item.crafting.RecipeHolder;
-+import org.bukkit.Location;
-+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
-+import org.bukkit.entity.HumanEntity;
-+import org.bukkit.event.inventory.InventoryType;
-+// CraftBukkit end
-+
- public class TransientCraftingContainer implements CraftingContainer {
- 
-     private final NonNullList<ItemStack> items;
-@@ -15,6 +25,68 @@
-     private final int height;
-     private final AbstractContainerMenu menu;
- 
-+    // CraftBukkit start - add fields
-+    public List<HumanEntity> transaction = new java.util.ArrayList<HumanEntity>();
-+    private RecipeHolder<?> currentRecipe;
-+    public Container resultInventory;
-+    private Player owner;
-+    private int maxStack = MAX_STACK;
-+
-+    public List<ItemStack> getContents() {
-+        return this.items;
-+    }
-+
-+    public void onOpen(CraftHumanEntity who) {
-+        this.transaction.add(who);
-+    }
-+
-+    public InventoryType getInvType() {
-+        return this.items.size() == 4 ? InventoryType.CRAFTING : InventoryType.WORKBENCH;
-+    }
-+
-+    public void onClose(CraftHumanEntity who) {
-+        this.transaction.remove(who);
-+    }
-+
-+    public List<HumanEntity> getViewers() {
-+        return this.transaction;
-+    }
-+
-+    public org.bukkit.inventory.InventoryHolder getOwner() {
-+        return (this.owner == null) ? null : this.owner.getBukkitEntity();
-+    }
-+
-+    @Override
-+    public int getMaxStackSize() {
-+        return this.maxStack;
-+    }
-+
-+    public void setMaxStackSize(int size) {
-+        this.maxStack = size;
-+        this.resultInventory.setMaxStackSize(size);
-+    }
-+
-+    @Override
-+    public Location getLocation() {
-+        return this.menu instanceof CraftingMenu ? ((CraftingMenu) this.menu).access.getLocation() : this.owner.getBukkitEntity().getLocation();
-+    }
-+
-+    @Override
-+    public RecipeHolder<?> getCurrentRecipe() {
-+        return this.currentRecipe;
-+    }
-+
-+    @Override
-+    public void setCurrentRecipe(RecipeHolder<?> currentRecipe) {
-+        this.currentRecipe = currentRecipe;
-+    }
-+
-+    public TransientCraftingContainer(AbstractContainerMenu container, int i, int j, Player player) {
-+        this(container, i, j);
-+        this.owner = player;
-+    }
-+    // CraftBukkit end
-+
-     public TransientCraftingContainer(AbstractContainerMenu handler, int width, int height) {
-         this(handler, width, height, NonNullList.withSize(width * height, ItemStack.EMPTY));
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/ArmorStandItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/ArmorStandItem.java.patch
deleted file mode 100644
index 5be546c2b6..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/ArmorStandItem.java.patch
+++ /dev/null
@@ -1,15 +0,0 @@
---- a/net/minecraft/world/item/ArmorStandItem.java
-+++ b/net/minecraft/world/item/ArmorStandItem.java
-@@ -53,6 +53,12 @@
-                     float f = (float) Mth.floor((Mth.wrapDegrees(context.getRotation() - 180.0F) + 22.5F) / 45.0F) * 45.0F;
- 
-                     entityarmorstand.moveTo(entityarmorstand.getX(), entityarmorstand.getY(), entityarmorstand.getZ(), f, 0.0F);
-+                    // CraftBukkit start
-+                    if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPlaceEvent(context, entityarmorstand).isCancelled()) {
-+                        if (context.getPlayer() != null) context.getPlayer().containerMenu.sendAllDataToRemote(); // Paper - Fix inventory desync
-+                        return InteractionResult.FAIL;
-+                    }
-+                    // CraftBukkit end
-                     worldserver.addFreshEntityWithPassengers(entityarmorstand);
-                     world.playSound((Player) null, entityarmorstand.getX(), entityarmorstand.getY(), entityarmorstand.getZ(), SoundEvents.ARMOR_STAND_PLACE, SoundSource.BLOCKS, 0.75F, 0.8F);
-                     entityarmorstand.gameEvent(GameEvent.ENTITY_PLACE, context.getPlayer());
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/BlockItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/BlockItem.java.patch
deleted file mode 100644
index 52e43aa9e4..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/BlockItem.java.patch
+++ /dev/null
@@ -1,109 +0,0 @@
---- a/net/minecraft/world/item/BlockItem.java
-+++ b/net/minecraft/world/item/BlockItem.java
-@@ -10,9 +10,9 @@
- import net.minecraft.core.registries.Registries;
- import net.minecraft.nbt.CompoundTag;
- import net.minecraft.network.chat.Component;
-+import net.minecraft.server.level.ServerLevel;
- import net.minecraft.server.level.ServerPlayer;
- import net.minecraft.sounds.SoundEvent;
--import net.minecraft.sounds.SoundSource;
- import net.minecraft.world.InteractionResult;
- import net.minecraft.world.entity.item.ItemEntity;
- import net.minecraft.world.entity.player.Player;
-@@ -31,6 +31,10 @@
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.phys.shapes.CollisionContext;
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.craftbukkit.block.data.CraftBlockData;
-+import org.bukkit.event.block.BlockCanBuildEvent;
-+// CraftBukkit end
- 
- public class BlockItem extends Item {
- 
-@@ -62,6 +66,13 @@
-                 return InteractionResult.FAIL;
-             } else {
-                 BlockState iblockdata = this.getPlacementState(blockactioncontext1);
-+                // CraftBukkit start - special case for handling block placement with water lilies and snow buckets
-+                org.bukkit.block.BlockState blockstate = null;
-+                if (this instanceof PlaceOnWaterBlockItem || this instanceof SolidBucketItem) {
-+                    blockstate = org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(blockactioncontext1.getLevel(), blockactioncontext1.getClickedPos());
-+                }
-+                final org.bukkit.block.BlockState oldBlockstate = blockstate != null ? blockstate : org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(blockactioncontext1.getLevel(), blockactioncontext1.getClickedPos()); // Paper - Reset placed block on exception
-+                // CraftBukkit end
- 
-                 if (iblockdata == null) {
-                     return InteractionResult.FAIL;
-@@ -76,9 +87,34 @@
- 
-                     if (iblockdata1.is(iblockdata.getBlock())) {
-                         iblockdata1 = this.updateBlockStateFromTag(blockposition, world, itemstack, iblockdata1);
-+                        // Paper start - Reset placed block on exception
-+                        try {
-                         this.updateCustomBlockEntityTag(blockposition, world, entityhuman, itemstack, iblockdata1);
-                         BlockItem.updateBlockEntityComponents(world, blockposition, itemstack);
-+                        } catch (Exception e) {
-+                            oldBlockstate.update(true, false);
-+                            if (entityhuman instanceof ServerPlayer player) {
-+                                org.apache.logging.log4j.LogManager.getLogger().error("Player {} tried placing invalid block", player.getScoreboardName(), e);
-+                                player.getBukkitEntity().kickPlayer("Packet processing error");
-+                                return InteractionResult.FAIL;
-+                            }
-+                            throw e; // Rethrow exception if not placed by a player
-+                        }
-+                        // Paper end - Reset placed block on exception
-                         iblockdata1.getBlock().setPlacedBy(world, blockposition, iblockdata1, entityhuman, itemstack);
-+                        // CraftBukkit start
-+                        if (blockstate != null) {
-+                            org.bukkit.event.block.BlockPlaceEvent placeEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPlaceEvent((ServerLevel) world, entityhuman, blockactioncontext1.getHand(), blockstate, blockposition.getX(), blockposition.getY(), blockposition.getZ());
-+                            if (placeEvent != null && (placeEvent.isCancelled() || !placeEvent.canBuild())) {
-+                                blockstate.update(true, false);
-+
-+                                if (true) { // Paper - if the event is called here, the inventory should be updated
-+                                    ((ServerPlayer) entityhuman).getBukkitEntity().updateInventory(); // SPIGOT-4541
-+                                }
-+                                return InteractionResult.FAIL;
-+                            }
-+                        }
-+                        // CraftBukkit end
-                         if (entityhuman instanceof ServerPlayer) {
-                             CriteriaTriggers.PLACED_BLOCK.trigger((ServerPlayer) entityhuman, blockposition, itemstack);
-                         }
-@@ -86,7 +122,7 @@
- 
-                     SoundType soundeffecttype = iblockdata1.getSoundType();
- 
--                    world.playSound(entityhuman, blockposition, this.getPlaceSound(iblockdata1), SoundSource.BLOCKS, (soundeffecttype.getVolume() + 1.0F) / 2.0F, soundeffecttype.getPitch() * 0.8F);
-+                    if (entityhuman == null) world.playSound(entityhuman, blockposition, this.getPlaceSound(iblockdata1), net.minecraft.sounds.SoundSource.BLOCKS, (soundeffecttype.getVolume() + 1.0F) / 2.0F, soundeffecttype.getPitch() * 0.8F); // Paper - Fix block place logic; reintroduce this for the dispenser (i.e the shulker)
-                     world.gameEvent((Holder) GameEvent.BLOCK_PLACE, blockposition, GameEvent.Context.of(entityhuman, iblockdata1));
-                     itemstack.consume(1, entityhuman);
-                     return InteractionResult.SUCCESS;
-@@ -144,8 +180,16 @@
-     protected boolean canPlace(BlockPlaceContext context, BlockState state) {
-         Player entityhuman = context.getPlayer();
-         CollisionContext voxelshapecollision = entityhuman == null ? CollisionContext.empty() : CollisionContext.of(entityhuman);
-+        // CraftBukkit start - store default return
-+        Level world = context.getLevel(); // Paper - Cancel hit for vanished players
-+        boolean defaultReturn = (!this.mustSurvive() || state.canSurvive(context.getLevel(), context.getClickedPos())) && world.checkEntityCollision(state, entityhuman, voxelshapecollision, context.getClickedPos(), true); // Paper - Cancel hit for vanished players
-+        org.bukkit.entity.Player player = (context.getPlayer() instanceof ServerPlayer) ? (org.bukkit.entity.Player) context.getPlayer().getBukkitEntity() : null;
- 
--        return (!this.mustSurvive() || state.canSurvive(context.getLevel(), context.getClickedPos())) && context.getLevel().isUnobstructed(state, context.getClickedPos(), voxelshapecollision);
-+        BlockCanBuildEvent event = new BlockCanBuildEvent(CraftBlock.at(context.getLevel(), context.getClickedPos()), player, CraftBlockData.fromData(state), defaultReturn, org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(context.getHand())); // Paper - Expose hand in BlockCanBuildEvent
-+        context.getLevel().getCraftServer().getPluginManager().callEvent(event);
-+
-+        return event.isBuildable();
-+        // CraftBukkit end
-     }
- 
-     protected boolean mustSurvive() {
-@@ -178,7 +222,7 @@
-                         return false;
-                     }
- 
--                    if (tileentitytypes1.onlyOpCanSetNbt() && (player == null || !player.canUseGameMasterBlocks())) {
-+                    if (tileentitytypes1.onlyOpCanSetNbt() && (player == null || !(player.canUseGameMasterBlocks() || (player.getAbilities().instabuild && player.getBukkitEntity().hasPermission("minecraft.nbt.place"))))) { // Spigot - add permission
-                         return false;
-                     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/BoatItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/BoatItem.java.patch
deleted file mode 100644
index c8502fcea3..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/BoatItem.java.patch
+++ /dev/null
@@ -1,33 +0,0 @@
---- a/net/minecraft/world/item/BoatItem.java
-+++ b/net/minecraft/world/item/BoatItem.java
-@@ -58,6 +58,13 @@
-             }
- 
-             if (movingobjectpositionblock.getType() == HitResult.Type.BLOCK) {
-+                // CraftBukkit start - Boat placement
-+                org.bukkit.event.player.PlayerInteractEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerInteractEvent(user, org.bukkit.event.block.Action.RIGHT_CLICK_BLOCK, movingobjectpositionblock.getBlockPos(), movingobjectpositionblock.getDirection(), itemstack, false, hand, movingobjectpositionblock.getLocation());
-+
-+                if (event.isCancelled()) {
-+                    return InteractionResult.PASS;
-+                }
-+                // CraftBukkit end
-                 AbstractBoat abstractboat = this.getBoat(world, movingobjectpositionblock, itemstack, user);
- 
-                 if (abstractboat == null) {
-@@ -68,7 +75,15 @@
-                         return InteractionResult.FAIL;
-                     } else {
-                         if (!world.isClientSide) {
--                            world.addFreshEntity(abstractboat);
-+                            // CraftBukkit start
-+                            if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPlaceEvent(world, movingobjectpositionblock.getBlockPos(), movingobjectpositionblock.getDirection(), user, abstractboat, hand).isCancelled()) {
-+                                return InteractionResult.FAIL;
-+                            }
-+
-+                            if (!world.addFreshEntity(abstractboat)) {
-+                                return InteractionResult.PASS;
-+                            }
-+                            // CraftBukkit end
-                             world.gameEvent((Entity) user, (Holder) GameEvent.ENTITY_PLACE, movingobjectpositionblock.getLocation());
-                             itemstack.consume(1, user);
-                         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/BoneMealItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/BoneMealItem.java.patch
deleted file mode 100644
index 4cf7f7eebf..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/BoneMealItem.java.patch
+++ /dev/null
@@ -1,41 +0,0 @@
---- a/net/minecraft/world/item/BoneMealItem.java
-+++ b/net/minecraft/world/item/BoneMealItem.java
-@@ -35,24 +35,30 @@
- 
-     @Override
-     public InteractionResult useOn(UseOnContext context) {
--        Level world = context.getLevel();
--        BlockPos blockposition = context.getClickedPos();
--        BlockPos blockposition1 = blockposition.relative(context.getClickedFace());
-+        // CraftBukkit start - extract bonemeal application logic to separate, static method
-+        return BoneMealItem.applyBonemeal(context);
-+    }
- 
--        if (BoneMealItem.growCrop(context.getItemInHand(), world, blockposition)) {
-+    public static InteractionResult applyBonemeal(UseOnContext itemactioncontext) {
-+        // CraftBukkit end
-+        Level world = itemactioncontext.getLevel();
-+        BlockPos blockposition = itemactioncontext.getClickedPos();
-+        BlockPos blockposition1 = blockposition.relative(itemactioncontext.getClickedFace());
-+
-+        if (BoneMealItem.growCrop(itemactioncontext.getItemInHand(), world, blockposition)) {
-             if (!world.isClientSide) {
--                context.getPlayer().gameEvent(GameEvent.ITEM_INTERACT_FINISH);
-+                if (itemactioncontext.getPlayer() != null) itemactioncontext.getPlayer().gameEvent(GameEvent.ITEM_INTERACT_FINISH); // CraftBukkit - SPIGOT-7518
-                 world.levelEvent(1505, blockposition, 15);
-             }
- 
-             return InteractionResult.SUCCESS;
-         } else {
-             BlockState iblockdata = world.getBlockState(blockposition);
--            boolean flag = iblockdata.isFaceSturdy(world, blockposition, context.getClickedFace());
-+            boolean flag = iblockdata.isFaceSturdy(world, blockposition, itemactioncontext.getClickedFace());
- 
--            if (flag && BoneMealItem.growWaterPlant(context.getItemInHand(), world, blockposition1, context.getClickedFace())) {
-+            if (flag && BoneMealItem.growWaterPlant(itemactioncontext.getItemInHand(), world, blockposition1, itemactioncontext.getClickedFace())) {
-                 if (!world.isClientSide) {
--                    context.getPlayer().gameEvent(GameEvent.ITEM_INTERACT_FINISH);
-+                    if (itemactioncontext.getPlayer() != null) itemactioncontext.getPlayer().gameEvent(GameEvent.ITEM_INTERACT_FINISH); // CraftBukkit - SPIGOT-7518
-                     world.levelEvent(1505, blockposition1, 15);
-                 }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/BucketItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/BucketItem.java.patch
deleted file mode 100644
index 6e51878d93..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/BucketItem.java.patch
+++ /dev/null
@@ -1,168 +0,0 @@
---- a/net/minecraft/world/item/BucketItem.java
-+++ b/net/minecraft/world/item/BucketItem.java
-@@ -6,6 +6,8 @@
- import net.minecraft.core.Direction;
- import net.minecraft.core.Holder;
- import net.minecraft.core.particles.ParticleTypes;
-+import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket;
-+import net.minecraft.server.level.ServerLevel;
- import net.minecraft.server.level.ServerPlayer;
- import net.minecraft.sounds.SoundEvent;
- import net.minecraft.sounds.SoundEvents;
-@@ -29,9 +31,17 @@
- import net.minecraft.world.level.material.Fluids;
- import net.minecraft.world.phys.BlockHitResult;
- import net.minecraft.world.phys.HitResult;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.craftbukkit.util.DummyGeneratorAccess;
-+import org.bukkit.event.player.PlayerBucketEmptyEvent;
-+import org.bukkit.event.player.PlayerBucketFillEvent;
-+// CraftBukkit end
- 
- public class BucketItem extends Item implements DispensibleContainerItem {
- 
-+    private static @Nullable ItemStack itemLeftInHandAfterPlayerBucketEmptyEvent = null; // Paper - Fix PlayerBucketEmptyEvent result itemstack
-+
-     public final Fluid content;
- 
-     public BucketItem(Fluid fluid, Item.Properties settings) {
-@@ -63,7 +73,18 @@
- 
-                     if (block instanceof BucketPickup) {
-                         BucketPickup ifluidsource = (BucketPickup) block;
-+                        // CraftBukkit start
-+                        ItemStack dummyFluid = ifluidsource.pickupBlock(user, DummyGeneratorAccess.INSTANCE, blockposition, iblockdata);
-+                        if (dummyFluid.isEmpty()) return InteractionResult.FAIL; // Don't fire event if the bucket won't be filled.
-+                        PlayerBucketFillEvent event = CraftEventFactory.callPlayerBucketFillEvent((ServerLevel) world, user, blockposition, blockposition, movingobjectpositionblock.getDirection(), itemstack, dummyFluid.getItem(), hand);
- 
-+                        if (event.isCancelled()) {
-+                            // ((ServerPlayer) user).connection.send(new ClientboundBlockUpdatePacket(world, blockposition)); // SPIGOT-5163 (see PlayerInteractManager) // Paper - Don't resend blocks
-+                            ((ServerPlayer) user).getBukkitEntity().updateInventory(); // SPIGOT-4541
-+                            return InteractionResult.FAIL;
-+                        }
-+                        // CraftBukkit end
-+
-                         itemstack1 = ifluidsource.pickupBlock(user, world, blockposition, iblockdata);
-                         if (!itemstack1.isEmpty()) {
-                             user.awardStat(Stats.ITEM_USED.get(this));
-@@ -71,7 +92,7 @@
-                                 user.playSound(soundeffect, 1.0F, 1.0F);
-                             });
-                             world.gameEvent((Entity) user, (Holder) GameEvent.FLUID_PICKUP, blockposition);
--                            ItemStack itemstack2 = ItemUtils.createFilledResult(itemstack, user, itemstack1);
-+                            ItemStack itemstack2 = ItemUtils.createFilledResult(itemstack, user, CraftItemStack.asNMSCopy(event.getItemStack())); // CraftBukkit
- 
-                             if (!world.isClientSide) {
-                                 CriteriaTriggers.FILLED_BUCKET.trigger((ServerPlayer) user, itemstack1);
-@@ -86,7 +107,7 @@
-                     iblockdata = world.getBlockState(blockposition);
-                     BlockPos blockposition2 = iblockdata.getBlock() instanceof LiquidBlockContainer && this.content == Fluids.WATER ? blockposition : blockposition1;
- 
--                    if (this.emptyContents(user, world, blockposition2, movingobjectpositionblock)) {
-+                    if (this.emptyContents(user, world, blockposition2, movingobjectpositionblock, movingobjectpositionblock.getDirection(), blockposition, itemstack, hand)) { // CraftBukkit
-                         this.checkExtraContent(user, world, itemstack, blockposition2);
-                         if (user instanceof ServerPlayer) {
-                             CriteriaTriggers.PLACED_BLOCK.trigger((ServerPlayer) user, blockposition2, itemstack);
-@@ -106,6 +127,13 @@
-     }
- 
-     public static ItemStack getEmptySuccessItem(ItemStack stack, Player player) {
-+        // Paper start - Fix PlayerBucketEmptyEvent result itemstack
-+        if (itemLeftInHandAfterPlayerBucketEmptyEvent != null) {
-+            ItemStack itemInHand = itemLeftInHandAfterPlayerBucketEmptyEvent;
-+            itemLeftInHandAfterPlayerBucketEmptyEvent = null;
-+            return itemInHand;
-+        }
-+        // Paper end - Fix PlayerBucketEmptyEvent result itemstack
-         return !player.hasInfiniteMaterials() ? new ItemStack(Items.BUCKET) : stack;
-     }
- 
-@@ -114,6 +142,12 @@
- 
-     @Override
-     public boolean emptyContents(@Nullable Player player, Level world, BlockPos pos, @Nullable BlockHitResult hitResult) {
-+        // CraftBukkit start
-+        return this.emptyContents(player, world, pos, hitResult, null, null, null, InteractionHand.MAIN_HAND);
-+    }
-+
-+    public boolean emptyContents(Player entityhuman, Level world, BlockPos blockposition, @Nullable BlockHitResult movingobjectpositionblock, Direction enumdirection, BlockPos clicked, ItemStack itemstack, InteractionHand enumhand) {
-+        // CraftBukkit end
-         Fluid fluidtype = this.content;
- 
-         if (!(fluidtype instanceof FlowingFluid fluidtypeflowing)) {
-@@ -126,7 +160,7 @@
-             boolean flag1;
-             label70:
-             {
--                iblockdata = world.getBlockState(pos);
-+                iblockdata = world.getBlockState(blockposition);
-                 block = iblockdata.getBlock();
-                 flag = iblockdata.canBeReplaced(this.content);
-                 if (!iblockdata.isAir() && !flag) {
-@@ -134,7 +168,7 @@
-                     {
-                         if (block instanceof LiquidBlockContainer) {
-                             ifluidcontainer = (LiquidBlockContainer) block;
--                            if (ifluidcontainer.canPlaceLiquid(player, world, pos, iblockdata, this.content)) {
-+                            if (ifluidcontainer.canPlaceLiquid(entityhuman, world, blockposition, iblockdata, this.content)) {
-                                 break label67;
-                             }
-                         }
-@@ -149,14 +183,25 @@
- 
-             boolean flag2 = flag1;
- 
-+            // CraftBukkit start
-+            if (flag2 && entityhuman != null) {
-+                PlayerBucketEmptyEvent event = CraftEventFactory.callPlayerBucketEmptyEvent((ServerLevel) world, entityhuman, blockposition, clicked, enumdirection, itemstack, enumhand);
-+                if (event.isCancelled()) {
-+                    // ((ServerPlayer) entityhuman).connection.send(new ClientboundBlockUpdatePacket(world, blockposition)); // SPIGOT-4238: needed when looking through entity // Paper - Don't resend blocks
-+                    ((ServerPlayer) entityhuman).getBukkitEntity().updateInventory(); // SPIGOT-4541
-+                    return false;
-+                }
-+                itemLeftInHandAfterPlayerBucketEmptyEvent = event.getItemStack() != null ? event.getItemStack().equals(CraftItemStack.asNewCraftStack(net.minecraft.world.item.Items.BUCKET)) ? null : CraftItemStack.asNMSCopy(event.getItemStack()) : ItemStack.EMPTY; // Paper - Fix PlayerBucketEmptyEvent result itemstack
-+            }
-+            // CraftBukkit end
-             if (!flag2) {
--                return hitResult != null && this.emptyContents(player, world, hitResult.getBlockPos().relative(hitResult.getDirection()), (BlockHitResult) null);
-+                return movingobjectpositionblock != null && this.emptyContents(entityhuman, world, movingobjectpositionblock.getBlockPos().relative(movingobjectpositionblock.getDirection()), (BlockHitResult) null, enumdirection, clicked, itemstack, enumhand); // CraftBukkit
-             } else if (world.dimensionType().ultraWarm() && this.content.is(FluidTags.WATER)) {
--                int i = pos.getX();
--                int j = pos.getY();
--                int k = pos.getZ();
-+                int i = blockposition.getX();
-+                int j = blockposition.getY();
-+                int k = blockposition.getZ();
- 
--                world.playSound(player, pos, SoundEvents.FIRE_EXTINGUISH, SoundSource.BLOCKS, 0.5F, 2.6F + (world.random.nextFloat() - world.random.nextFloat()) * 0.8F);
-+                world.playSound(entityhuman, blockposition, SoundEvents.FIRE_EXTINGUISH, SoundSource.BLOCKS, 0.5F, 2.6F + (world.random.nextFloat() - world.random.nextFloat()) * 0.8F);
- 
-                 for (int l = 0; l < 8; ++l) {
-                     world.addParticle(ParticleTypes.LARGE_SMOKE, (double) i + Math.random(), (double) j + Math.random(), (double) k + Math.random(), 0.0D, 0.0D, 0.0D);
-@@ -167,20 +212,20 @@
-                 if (block instanceof LiquidBlockContainer) {
-                     ifluidcontainer = (LiquidBlockContainer) block;
-                     if (this.content == Fluids.WATER) {
--                        ifluidcontainer.placeLiquid(world, pos, iblockdata, fluidtypeflowing.getSource(false));
--                        this.playEmptySound(player, world, pos);
-+                        ifluidcontainer.placeLiquid(world, blockposition, iblockdata, fluidtypeflowing.getSource(false));
-+                        this.playEmptySound(entityhuman, world, blockposition);
-                         return true;
-                     }
-                 }
- 
-                 if (!world.isClientSide && flag && !iblockdata.liquid()) {
--                    world.destroyBlock(pos, true);
-+                    world.destroyBlock(blockposition, true);
-                 }
- 
--                if (!world.setBlock(pos, this.content.defaultFluidState().createLegacyBlock(), 11) && !iblockdata.getFluidState().isSource()) {
-+                if (!world.setBlock(blockposition, this.content.defaultFluidState().createLegacyBlock(), 11) && !iblockdata.getFluidState().isSource()) {
-                     return false;
-                 } else {
--                    this.playEmptySound(player, world, pos);
-+                    this.playEmptySound(entityhuman, world, blockposition);
-                     return true;
-                 }
-             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/CrossbowItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/CrossbowItem.java.patch
deleted file mode 100644
index d6f5a8f499..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/CrossbowItem.java.patch
+++ /dev/null
@@ -1,48 +0,0 @@
---- a/net/minecraft/world/item/CrossbowItem.java
-+++ b/net/minecraft/world/item/CrossbowItem.java
-@@ -90,7 +90,14 @@
-     public boolean releaseUsing(ItemStack stack, Level world, LivingEntity user, int remainingUseTicks) {
-         int i = this.getUseDuration(stack, user) - remainingUseTicks;
-         float f = getPowerForTime(i, stack, user);
--        if (f >= 1.0F && !isCharged(stack) && tryLoadProjectiles(user, stack)) {
-+        // Paper start - Add EntityLoadCrossbowEvent
-+        if (f >= 1.0F && !isCharged(stack)) {
-+            final io.papermc.paper.event.entity.EntityLoadCrossbowEvent event = new io.papermc.paper.event.entity.EntityLoadCrossbowEvent(user.getBukkitLivingEntity(), stack.asBukkitMirror(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(user.getUsedItemHand()));
-+            if (!event.callEvent() || !tryLoadProjectiles(user, stack, event.shouldConsumeItem()) || !event.shouldConsumeItem()) {
-+                if (user instanceof ServerPlayer player) player.containerMenu.sendAllDataToRemote();
-+                return false;
-+            }
-+            // Paper end - Add EntityLoadCrossbowEvent
-             CrossbowItem.ChargingSounds chargingSounds = this.getChargingSounds(stack);
-             chargingSounds.end()
-                 .ifPresent(
-@@ -111,8 +118,14 @@
-         }
-     }
- 
--    private static boolean tryLoadProjectiles(LivingEntity shooter, ItemStack crossbow) {
--        List<ItemStack> list = draw(crossbow, shooter.getProjectile(crossbow), shooter);
-+    @io.papermc.paper.annotation.DoNotUse // Paper - Add EntityLoadCrossbowEvent
-+    private static boolean tryLoadProjectiles(LivingEntity shooter, ItemStack crossbow)  {
-+        // Paper start - Add EntityLoadCrossbowEvent
-+        return CrossbowItem.tryLoadProjectiles(shooter, crossbow, true);
-+    }
-+    private static boolean tryLoadProjectiles(LivingEntity shooter, ItemStack crossbow, boolean consume) {
-+        List<ItemStack> list = draw(crossbow, shooter.getProjectile(crossbow), shooter, consume);
-+        // Paper end - Add EntityLoadCrossbowEvent
-         if (!list.isEmpty()) {
-             crossbow.set(DataComponents.CHARGED_PROJECTILES, ChargedProjectiles.of(list));
-             return true;
-@@ -164,7 +177,11 @@
-     @Override
-     protected Projectile createProjectile(Level world, LivingEntity shooter, ItemStack weaponStack, ItemStack projectileStack, boolean critical) {
-         if (projectileStack.is(Items.FIREWORK_ROCKET)) {
--            return new FireworkRocketEntity(world, projectileStack, shooter, shooter.getX(), shooter.getEyeY() - 0.15F, shooter.getZ(), true);
-+            // Paper start
-+            FireworkRocketEntity entity =  new FireworkRocketEntity(world, projectileStack, shooter, shooter.getX(), shooter.getEyeY() - 0.15F, shooter.getZ(), true);
-+            entity.spawningEntity = shooter.getUUID(); // Paper
-+            return entity;
-+            // Paper end
-         } else {
-             Projectile projectile = super.createProjectile(world, shooter, weaponStack, projectileStack, critical);
-             if (projectile instanceof AbstractArrow abstractArrow) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/DebugStickItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/DebugStickItem.java.patch
deleted file mode 100644
index b81a2063de..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/DebugStickItem.java.patch
+++ /dev/null
@@ -1,25 +0,0 @@
---- a/net/minecraft/world/item/DebugStickItem.java
-+++ b/net/minecraft/world/item/DebugStickItem.java
-@@ -1,3 +1,4 @@
-+// mc-dev import
- package net.minecraft.world.item;
- 
- import java.util.Collection;
-@@ -52,7 +53,7 @@
-     }
- 
-     public boolean handleInteraction(Player player, BlockState state, LevelAccessor world, BlockPos pos, boolean update, ItemStack stack) {
--        if (!player.canUseGameMasterBlocks()) {
-+        if (!player.canUseGameMasterBlocks() && !(player.getAbilities().instabuild && player.getBukkitEntity().hasPermission("minecraft.debugstick")) && !player.getBukkitEntity().hasPermission("minecraft.debugstick.always")) { // Spigot
-             return false;
-         } else {
-             Holder<Block> holder = state.getBlockHolder();
-@@ -92,7 +93,7 @@
-     }
- 
-     private static <T extends Comparable<T>> BlockState cycleState(BlockState state, Property<T> property, boolean inverse) {
--        return (BlockState) state.setValue(property, (Comparable) DebugStickItem.getRelative(property.getPossibleValues(), state.getValue(property), inverse));
-+        return (BlockState) state.setValue(property, DebugStickItem.getRelative(property.getPossibleValues(), state.getValue(property), inverse)); // CraftBukkit - decompile error
-     }
- 
-     private static <T> T getRelative(Iterable<T> elements, @Nullable T current, boolean inverse) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/DyeItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/DyeItem.java.patch
deleted file mode 100644
index ca96f3388a..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/DyeItem.java.patch
+++ /dev/null
@@ -1,29 +0,0 @@
---- a/net/minecraft/world/item/DyeItem.java
-+++ b/net/minecraft/world/item/DyeItem.java
-@@ -12,6 +12,7 @@
- import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.block.entity.SignBlockEntity;
-+import org.bukkit.event.entity.SheepDyeWoolEvent; // CraftBukkit
- 
- public class DyeItem extends Item implements SignApplicator {
- 
-@@ -30,7 +31,17 @@
-             if (entitysheep.isAlive() && !entitysheep.isSheared() && entitysheep.getColor() != this.dyeColor) {
-                 entitysheep.level().playSound(user, (Entity) entitysheep, SoundEvents.DYE_USE, SoundSource.PLAYERS, 1.0F, 1.0F);
-                 if (!user.level().isClientSide) {
--                    entitysheep.setColor(this.dyeColor);
-+                    // CraftBukkit start
-+                    byte bColor = (byte) this.dyeColor.getId();
-+                    SheepDyeWoolEvent event = new SheepDyeWoolEvent((org.bukkit.entity.Sheep) entitysheep.getBukkitEntity(), org.bukkit.DyeColor.getByWoolData(bColor), (org.bukkit.entity.Player) user.getBukkitEntity());
-+                    entitysheep.level().getCraftServer().getPluginManager().callEvent(event);
-+
-+                    if (event.isCancelled()) {
-+                        return InteractionResult.PASS;
-+                    }
-+
-+                    entitysheep.setColor(DyeColor.byId((byte) event.getColor().getWoolData()));
-+                    // CraftBukkit end
-                     stack.shrink(1);
-                 }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/EggItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/EggItem.java.patch
deleted file mode 100644
index 6281e950e3..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/EggItem.java.patch
+++ /dev/null
@@ -1,40 +0,0 @@
---- a/net/minecraft/world/item/EggItem.java
-+++ b/net/minecraft/world/item/EggItem.java
-@@ -25,13 +25,32 @@
-     public InteractionResult use(Level world, Player user, InteractionHand hand) {
-         ItemStack itemstack = user.getItemInHand(hand);
- 
--        world.playSound((Player) null, user.getX(), user.getY(), user.getZ(), SoundEvents.EGG_THROW, SoundSource.PLAYERS, 0.5F, 0.4F / (world.getRandom().nextFloat() * 0.4F + 0.8F));
-+        // world.playSound((EntityHuman) null, entityhuman.getX(), entityhuman.getY(), entityhuman.getZ(), SoundEffects.EGG_THROW, SoundCategory.PLAYERS, 0.5F, 0.4F / (world.getRandom().nextFloat() * 0.4F + 0.8F)); // CraftBukkit - moved down
-         if (world instanceof ServerLevel worldserver) {
--            Projectile.spawnProjectileFromRotation(ThrownEgg::new, worldserver, itemstack, user, 0.0F, EggItem.PROJECTILE_SHOOT_POWER, 1.0F);
--        }
-+            // CraftBukkit start
-+            // Paper start - PlayerLaunchProjectileEvent
-+            final Projectile.Delayed<ThrownEgg> thrownEgg = Projectile.spawnProjectileFromRotationDelayed(ThrownEgg::new, worldserver, itemstack, user, 0.0F, EggItem.PROJECTILE_SHOOT_POWER, 1.0F);
-+            com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) user.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack), (org.bukkit.entity.Projectile) thrownEgg.projectile().getBukkitEntity());
-+            if (event.callEvent() && thrownEgg.attemptSpawn()) {
-+                if (event.shouldConsume()) {
-+                    itemstack.consume(1, user);
-+                } else if (user instanceof net.minecraft.server.level.ServerPlayer) {
-+                    ((net.minecraft.server.level.ServerPlayer) user).getBukkitEntity().updateInventory();
-+                }
- 
--        user.awardStat(Stats.ITEM_USED.get(this));
--        itemstack.consume(1, user);
-+                world.playSound((Player) null, user.getX(), user.getY(), user.getZ(), SoundEvents.EGG_THROW, SoundSource.PLAYERS, 0.5F, 0.4F / (world.getRandom().nextFloat() * 0.4F + 0.8F));
-+                user.awardStat(Stats.ITEM_USED.get(this));
-+            } else {
-+                // Paper end - PlayerLaunchProjectileEvent
-+                if (user instanceof net.minecraft.server.level.ServerPlayer) {
-+                    ((net.minecraft.server.level.ServerPlayer) user).getBukkitEntity().updateInventory();
-+                }
-+                return InteractionResult.FAIL;
-+            }
-+            // CraftBukkit end
-+        }
-+        world.playSound((Player) null, user.getX(), user.getY(), user.getZ(), SoundEvents.EGG_THROW, SoundSource.PLAYERS, 0.5F, 0.4F / (world.getRandom().nextFloat() * 0.4F + 0.8F));
-+        // Paper - PlayerLaunchProjectileEvent - moved up
-         return InteractionResult.SUCCESS;
-     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/EndCrystalItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/EndCrystalItem.java.patch
deleted file mode 100644
index 89844fdef2..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/EndCrystalItem.java.patch
+++ /dev/null
@@ -1,31 +0,0 @@
---- a/net/minecraft/world/item/EndCrystalItem.java
-+++ b/net/minecraft/world/item/EndCrystalItem.java
-@@ -30,7 +30,7 @@
-         if (!iblockdata.is(Blocks.OBSIDIAN) && !iblockdata.is(Blocks.BEDROCK)) {
-             return InteractionResult.FAIL;
-         } else {
--            BlockPos blockposition1 = blockposition.above();
-+            BlockPos blockposition1 = blockposition.above(); final BlockPos aboveBlockPosition = blockposition1; // Paper - OBFHELPER
- 
-             if (!world.isEmptyBlock(blockposition1)) {
-                 return InteractionResult.FAIL;
-@@ -47,12 +47,18 @@
-                         EndCrystal entityendercrystal = new EndCrystal(world, d0 + 0.5D, d1, d2 + 0.5D);
- 
-                         entityendercrystal.setShowBottom(false);
-+                        // CraftBukkit start
-+                        if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPlaceEvent(context, entityendercrystal).isCancelled()) {
-+                            if (context.getPlayer() != null) context.getPlayer().containerMenu.sendAllDataToRemote(); // Paper - Fix inventory desync
-+                            return InteractionResult.FAIL;
-+                        }
-+                        // CraftBukkit end
-                         world.addFreshEntity(entityendercrystal);
-                         world.gameEvent((Entity) context.getPlayer(), (Holder) GameEvent.ENTITY_PLACE, blockposition1);
-                         EndDragonFight enderdragonbattle = ((ServerLevel) world).getDragonFight();
- 
-                         if (enderdragonbattle != null) {
--                            enderdragonbattle.tryRespawn();
-+                            enderdragonbattle.tryRespawn(aboveBlockPosition); // Paper - Perf: Do crystal-portal proximity check before entity lookup
-                         }
-                     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/EnderEyeItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/EnderEyeItem.java.patch
deleted file mode 100644
index c343069562..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/EnderEyeItem.java.patch
+++ /dev/null
@@ -1,56 +0,0 @@
---- a/net/minecraft/world/item/EnderEyeItem.java
-+++ b/net/minecraft/world/item/EnderEyeItem.java
-@@ -45,6 +45,11 @@
-                 return InteractionResult.SUCCESS;
-             } else {
-                 BlockState iblockdata1 = (BlockState) iblockdata.setValue(EndPortalFrameBlock.HAS_EYE, true);
-+                // Paper start
-+                if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(context.getPlayer(), blockposition, iblockdata1)) {
-+                    return InteractionResult.PASS;
-+                }
-+                // Paper end
- 
-                 Block.pushEntitiesUp(iblockdata, iblockdata1, world, blockposition);
-                 world.setBlock(blockposition, iblockdata1, 2);
-@@ -62,7 +67,27 @@
-                         }
-                     }
- 
--                    world.globalLevelEvent(1038, blockposition1.offset(1, 0, 1), 0);
-+                    // CraftBukkit start - Use relative location for far away sounds
-+                    // world.globalLevelEvent(1038, blockposition1.offset(1, 0, 1), 0);
-+                    int viewDistance = world.getCraftServer().getViewDistance() * 16;
-+                    BlockPos soundPos = blockposition1.offset(1, 0, 1);
-+                    final net.minecraft.server.level.ServerLevel serverLevel = (net.minecraft.server.level.ServerLevel) world; // Paper - respect global sound events gamerule - ensured by isClientSide check above
-+                    for (ServerPlayer player : serverLevel.getPlayersForGlobalSoundGamerule()) { // Paper - respect global sound events gamerule
-+                        double deltaX = soundPos.getX() - player.getX();
-+                        double deltaZ = soundPos.getZ() - player.getZ();
-+                        double distanceSquared = deltaX * deltaX + deltaZ * deltaZ;
-+                        final double soundRadiusSquared = serverLevel.getGlobalSoundRangeSquared(config -> config.endPortalSoundRadius); // Paper - respect global sound events gamerule
-+                        if (!serverLevel.getGameRules().getBoolean(net.minecraft.world.level.GameRules.RULE_GLOBAL_SOUND_EVENTS) && distanceSquared > soundRadiusSquared) continue; // Spigot // Paper - respect global sound events gamerule
-+                        if (distanceSquared > viewDistance * viewDistance) {
-+                            double deltaLength = Math.sqrt(distanceSquared);
-+                            double relativeX = player.getX() + (deltaX / deltaLength) * viewDistance;
-+                            double relativeZ = player.getZ() + (deltaZ / deltaLength) * viewDistance;
-+                            player.connection.send(new net.minecraft.network.protocol.game.ClientboundLevelEventPacket(1038, new BlockPos((int) relativeX, (int) soundPos.getY(), (int) relativeZ), 0, true));
-+                        } else {
-+                            player.connection.send(new net.minecraft.network.protocol.game.ClientboundLevelEventPacket(1038, soundPos, 0, true));
-+                        }
-+                    }
-+                    // CraftBukkit end
-                 }
- 
-                 return InteractionResult.SUCCESS;
-@@ -99,7 +124,11 @@
-                 entityendersignal.setItem(itemstack);
-                 entityendersignal.signalTo(blockposition);
-                 world.gameEvent((Holder) GameEvent.PROJECTILE_SHOOT, entityendersignal.position(), GameEvent.Context.of((Entity) user));
--                world.addFreshEntity(entityendersignal);
-+                // CraftBukkit start
-+                if (!world.addFreshEntity(entityendersignal)) {
-+                    return InteractionResult.FAIL;
-+                }
-+                // CraftBukkit end
-                 if (user instanceof ServerPlayer) {
-                     ServerPlayer entityplayer = (ServerPlayer) user;
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/EnderpearlItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/EnderpearlItem.java.patch
deleted file mode 100644
index e025880d4f..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/EnderpearlItem.java.patch
+++ /dev/null
@@ -1,39 +0,0 @@
---- a/net/minecraft/world/item/EnderpearlItem.java
-+++ b/net/minecraft/world/item/EnderpearlItem.java
-@@ -23,13 +23,32 @@
-     public InteractionResult use(Level world, Player user, InteractionHand hand) {
-         ItemStack itemstack = user.getItemInHand(hand);
- 
--        world.playSound((Player) null, user.getX(), user.getY(), user.getZ(), SoundEvents.ENDER_PEARL_THROW, SoundSource.NEUTRAL, 0.5F, 0.4F / (world.getRandom().nextFloat() * 0.4F + 0.8F));
-         if (world instanceof ServerLevel worldserver) {
--            Projectile.spawnProjectileFromRotation(ThrownEnderpearl::new, worldserver, itemstack, user, 0.0F, EnderpearlItem.PROJECTILE_SHOOT_POWER, 1.0F);
-+            // CraftBukkit start
-+            // Paper start - PlayerLaunchProjectileEvent
-+            final Projectile.Delayed<ThrownEnderpearl> thrownEnderpearl = Projectile.spawnProjectileFromRotationDelayed(ThrownEnderpearl::new, worldserver, itemstack, user, 0.0F, EnderpearlItem.PROJECTILE_SHOOT_POWER, 1.0F);
-+            com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) user.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack), (org.bukkit.entity.Projectile) thrownEnderpearl.projectile().getBukkitEntity());
-+            if (event.callEvent() && thrownEnderpearl.attemptSpawn()) {
-+                if (event.shouldConsume()) {
-+                    itemstack.consume(1, user);
-+                } else if (user instanceof net.minecraft.server.level.ServerPlayer) {
-+                    ((net.minecraft.server.level.ServerPlayer) user).getBukkitEntity().updateInventory();
-+                }
-+
-+                world.playSound((Player) null, user.getX(), user.getY(), user.getZ(), SoundEvents.ENDER_PEARL_THROW, SoundSource.NEUTRAL, 0.5F, 0.4F / (world.getRandom().nextFloat() * 0.4F + 0.8F));
-+                user.awardStat(Stats.ITEM_USED.get(this));
-+            } else {
-+            // Paper end - PlayerLaunchProjectileEvent
-+                if (user instanceof net.minecraft.server.level.ServerPlayer) {
-+                    ((net.minecraft.server.level.ServerPlayer) user).getBukkitEntity().updateInventory();
-+                }
-+                return InteractionResult.FAIL;
-+            }
-         }
-+        world.playSound((Player) null, user.getX(), user.getY(), user.getZ(), SoundEvents.ENDER_PEARL_THROW, SoundSource.NEUTRAL, 0.5F, 0.4F / (world.getRandom().nextFloat() * 0.4F + 0.8F));
-+        // CraftBukkit end
- 
--        user.awardStat(Stats.ITEM_USED.get(this));
--        itemstack.consume(1, user);
-+        // Paper - PlayerLaunchProjectileEvent - moved up
-         return InteractionResult.SUCCESS;
-     }
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/ExperienceBottleItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/ExperienceBottleItem.java.patch
deleted file mode 100644
index e16430df8d..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/ExperienceBottleItem.java.patch
+++ /dev/null
@@ -1,54 +0,0 @@
---- a/net/minecraft/world/item/ExperienceBottleItem.java
-+++ b/net/minecraft/world/item/ExperienceBottleItem.java
-@@ -21,22 +21,38 @@
-     @Override
-     public InteractionResult use(Level world, Player user, InteractionHand hand) {
-         ItemStack itemStack = user.getItemInHand(hand);
--        world.playSound(
--            null,
--            user.getX(),
--            user.getY(),
--            user.getZ(),
--            SoundEvents.EXPERIENCE_BOTTLE_THROW,
--            SoundSource.NEUTRAL,
--            0.5F,
--            0.4F / (world.getRandom().nextFloat() * 0.4F + 0.8F)
--        );
-+        // Paper - PlayerLaunchProjectileEvent - moved down
-         if (world instanceof ServerLevel serverLevel) {
--            Projectile.spawnProjectileFromRotation(ThrownExperienceBottle::new, serverLevel, itemStack, user, -20.0F, 0.7F, 1.0F);
-+            // Paper start - PlayerLaunchProjectileEvent
-+            final Projectile.Delayed<ThrownExperienceBottle> thrownExperienceBottle = Projectile.spawnProjectileFromRotationDelayed(ThrownExperienceBottle::new, serverLevel, itemStack, user, -20.0F, 0.7F, 1.0F);
-+            com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) user.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack), (org.bukkit.entity.Projectile) thrownExperienceBottle.projectile().getBukkitEntity());
-+            if (event.callEvent() && thrownExperienceBottle.attemptSpawn()) {
-+                if (event.shouldConsume()) {
-+                    itemStack.consume(1, user);
-+                } else if (user instanceof net.minecraft.server.level.ServerPlayer) {
-+                    ((net.minecraft.server.level.ServerPlayer) user).getBukkitEntity().updateInventory();
-+                }
-+
-+                world.playSound(
-+                    null,
-+                    user.getX(),
-+                    user.getY(),
-+                    user.getZ(),
-+                    SoundEvents.EXPERIENCE_BOTTLE_THROW,
-+                    SoundSource.NEUTRAL,
-+                    0.5F,
-+                    0.4F / (world.getRandom().nextFloat() * 0.4F + 0.8F)
-+                );
-+            } else {
-+                if (user instanceof net.minecraft.server.level.ServerPlayer) {
-+                    ((net.minecraft.server.level.ServerPlayer) user).getBukkitEntity().updateInventory();
-+                }
-+                return InteractionResult.FAIL;
-+            }
-+            // Paper end - PlayerLaunchProjectileEvent
-         }
- 
--        user.awardStat(Stats.ITEM_USED.get(this));
--        itemStack.consume(1, user);
-+        // Paper - PlayerLaunchProjectileEvent - moved up
-         return InteractionResult.SUCCESS;
-     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/FireChargeItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/FireChargeItem.java.patch
deleted file mode 100644
index ee72808622..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/FireChargeItem.java.patch
+++ /dev/null
@@ -1,31 +0,0 @@
---- a/net/minecraft/world/item/FireChargeItem.java
-+++ b/net/minecraft/world/item/FireChargeItem.java
-@@ -40,12 +40,28 @@
-         if (!CampfireBlock.canLight(iblockdata) && !CandleBlock.canLight(iblockdata) && !CandleCakeBlock.canLight(iblockdata)) {
-             blockposition = blockposition.relative(context.getClickedFace());
-             if (BaseFireBlock.canBePlacedAt(world, blockposition, context.getHorizontalDirection())) {
-+                // CraftBukkit start - fire BlockIgniteEvent
-+                if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(world, blockposition, org.bukkit.event.block.BlockIgniteEvent.IgniteCause.FIREBALL, context.getPlayer()).isCancelled()) {
-+                    if (!context.getPlayer().getAbilities().instabuild) {
-+                        context.getItemInHand().shrink(1);
-+                    }
-+                    return InteractionResult.PASS;
-+                }
-+                // CraftBukkit end
-                 this.playSound(world, blockposition);
-                 world.setBlockAndUpdate(blockposition, BaseFireBlock.getState(world, blockposition));
-                 world.gameEvent((Entity) context.getPlayer(), (Holder) GameEvent.BLOCK_PLACE, blockposition);
-                 flag = true;
-             }
-         } else {
-+            // CraftBukkit start - fire BlockIgniteEvent
-+            if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(world, blockposition, org.bukkit.event.block.BlockIgniteEvent.IgniteCause.FIREBALL, context.getPlayer()).isCancelled()) {
-+                if (!context.getPlayer().getAbilities().instabuild) {
-+                    context.getItemInHand().shrink(1);
-+                }
-+                return InteractionResult.PASS;
-+            }
-+            // CraftBukkit end
-             this.playSound(world, blockposition);
-             world.setBlockAndUpdate(blockposition, (BlockState) iblockdata.setValue(BlockStateProperties.LIT, true));
-             world.gameEvent((Entity) context.getPlayer(), (Holder) GameEvent.BLOCK_CHANGE, blockposition);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/FireworkRocketItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/FireworkRocketItem.java.patch
deleted file mode 100644
index 9d8f816a26..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/FireworkRocketItem.java.patch
+++ /dev/null
@@ -1,51 +0,0 @@
---- a/net/minecraft/world/item/FireworkRocketItem.java
-+++ b/net/minecraft/world/item/FireworkRocketItem.java
-@@ -33,7 +33,7 @@
-             ItemStack itemStack = context.getItemInHand();
-             Vec3 vec3 = context.getClickLocation();
-             Direction direction = context.getClickedFace();
--            Projectile.spawnProjectile(
-+            final Projectile.Delayed<FireworkRocketEntity> fireworkRocketEntity = Projectile.spawnProjectileDelayed( // Paper - PlayerLaunchProjectileEvent
-                 new FireworkRocketEntity(
-                     level,
-                     context.getPlayer(),
-@@ -43,9 +43,14 @@
-                     itemStack
-                 ),
-                 serverLevel,
--                itemStack
-+                itemStack, f -> f.spawningEntity = context.getPlayer() == null ? null : context.getPlayer().getUUID()  // Paper - firework api - assign spawning entity uuid
-             );
--            itemStack.shrink(1);
-+            // Paper start - PlayerLaunchProjectileEvent
-+            com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) context.getPlayer().getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack), (org.bukkit.entity.Firework) fireworkRocketEntity.projectile().getBukkitEntity());
-+            if (!event.callEvent() || !fireworkRocketEntity.attemptSpawn()) return InteractionResult.PASS;
-+            if (event.shouldConsume() && !context.getPlayer().hasInfiniteMaterials()) itemStack.shrink(1);
-+            else if (context.getPlayer() instanceof net.minecraft.server.level.ServerPlayer) ((net.minecraft.server.level.ServerPlayer) context.getPlayer()).getBukkitEntity().updateInventory();
-+            // Paper end - PlayerLaunchProjectileEvent
-         }
- 
-         return InteractionResult.SUCCESS;
-@@ -56,9 +61,19 @@
-         if (user.isFallFlying()) {
-             ItemStack itemStack = user.getItemInHand(hand);
-             if (world instanceof ServerLevel serverLevel) {
--                Projectile.spawnProjectile(new FireworkRocketEntity(world, itemStack, user), serverLevel, itemStack);
--                itemStack.consume(1, user);
--                user.awardStat(Stats.ITEM_USED.get(this));
-+                // Paper start - PlayerElytraBoostEvent
-+                final Projectile.Delayed<FireworkRocketEntity> delayed = Projectile.spawnProjectileDelayed(new FireworkRocketEntity(world, itemStack, user), serverLevel, itemStack, f -> f.spawningEntity = user.getUUID());  // Paper - firework api - assign spawning entity uuid
-+                com.destroystokyo.paper.event.player.PlayerElytraBoostEvent event = new com.destroystokyo.paper.event.player.PlayerElytraBoostEvent((org.bukkit.entity.Player) user.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack), (org.bukkit.entity.Firework) delayed.projectile().getBukkitEntity(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand));
-+                if (event.callEvent() && delayed.attemptSpawn()) {
-+                    user.awardStat(Stats.ITEM_USED.get(this)); // Moved up from below
-+                    if (event.shouldConsume() && !user.hasInfiniteMaterials()) {
-+                        itemStack.shrink(1); // Moved up from below
-+                    } else ((net.minecraft.server.level.ServerPlayer) user).getBukkitEntity().updateInventory();
-+                } else {
-+                    ((net.minecraft.server.level.ServerPlayer) user).getBukkitEntity().updateInventory();
-+                }
-+                // Moved up consume/stat
-+                // Paper end - PlayerElytraBoostEvent
-             }
- 
-             return InteractionResult.SUCCESS;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/FishingRodItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/FishingRodItem.java.patch
deleted file mode 100644
index 6c64321f95..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/FishingRodItem.java.patch
+++ /dev/null
@@ -1,50 +0,0 @@
---- a/net/minecraft/world/item/FishingRodItem.java
-+++ b/net/minecraft/world/item/FishingRodItem.java
-@@ -14,6 +14,11 @@
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.gameevent.GameEvent;
- 
-+// CraftBukkit start
-+import org.bukkit.event.player.PlayerFishEvent;
-+import org.bukkit.craftbukkit.CraftEquipmentSlot;
-+// CraftBukkit end
-+
- public class FishingRodItem extends Item {
- 
-     public FishingRodItem(Item.Properties settings) {
-@@ -26,7 +31,7 @@
- 
-         if (user.fishing != null) {
-             if (!world.isClientSide) {
--                int i = user.fishing.retrieve(itemstack);
-+                int i = user.fishing.retrieve(hand, itemstack); // Paper - Add hand parameter to PlayerFishEvent
- 
-                 itemstack.hurtAndBreak(i, user, LivingEntity.getSlotForHand(hand));
-             }
-@@ -34,13 +39,24 @@
-             world.playSound((Player) null, user.getX(), user.getY(), user.getZ(), SoundEvents.FISHING_BOBBER_RETRIEVE, SoundSource.NEUTRAL, 1.0F, 0.4F / (world.getRandom().nextFloat() * 0.4F + 0.8F));
-             user.gameEvent(GameEvent.ITEM_INTERACT_FINISH);
-         } else {
--            world.playSound((Player) null, user.getX(), user.getY(), user.getZ(), SoundEvents.FISHING_BOBBER_THROW, SoundSource.NEUTRAL, 0.5F, 0.4F / (world.getRandom().nextFloat() * 0.4F + 0.8F));
-+            // world.playSound((EntityHuman) null, entityhuman.getX(), entityhuman.getY(), entityhuman.getZ(), SoundEffects.FISHING_BOBBER_THROW, SoundCategory.NEUTRAL, 0.5F, 0.4F / (world.getRandom().nextFloat() * 0.4F + 0.8F));
-             if (world instanceof ServerLevel) {
-                 ServerLevel worldserver = (ServerLevel) world;
-                 int j = (int) (EnchantmentHelper.getFishingTimeReduction(worldserver, itemstack, user) * 20.0F);
-                 int k = EnchantmentHelper.getFishingLuckBonus(worldserver, itemstack, user);
- 
--                Projectile.spawnProjectile(new FishingHook(user, world, k, j), worldserver, itemstack);
-+                // CraftBukkit start
-+                FishingHook entityfishinghook = new FishingHook(user, world, k, j);
-+                PlayerFishEvent playerFishEvent = new PlayerFishEvent((org.bukkit.entity.Player) user.getBukkitEntity(), null, (org.bukkit.entity.FishHook) entityfishinghook.getBukkitEntity(), CraftEquipmentSlot.getHand(hand), PlayerFishEvent.State.FISHING);
-+                world.getCraftServer().getPluginManager().callEvent(playerFishEvent);
-+
-+                if (playerFishEvent.isCancelled()) {
-+                    user.fishing = null;
-+                    return InteractionResult.PASS;
-+                }
-+                world.playSound((Player) null, user.getX(), user.getY(), user.getZ(), SoundEvents.FISHING_BOBBER_THROW, SoundSource.NEUTRAL, 0.5F, 0.4F / (world.getRandom().nextFloat() * 0.4F + 0.8F));
-+                Projectile.spawnProjectile(entityfishinghook, worldserver, itemstack);
-+                // CraftBukkit end
-             }
- 
-             user.awardStat(Stats.ITEM_USED.get(this));
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/FlintAndSteelItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/FlintAndSteelItem.java.patch
deleted file mode 100644
index 0d633ff507..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/FlintAndSteelItem.java.patch
+++ /dev/null
@@ -1,28 +0,0 @@
---- a/net/minecraft/world/item/FlintAndSteelItem.java
-+++ b/net/minecraft/world/item/FlintAndSteelItem.java
-@@ -37,6 +37,12 @@
-             BlockPos blockposition1 = blockposition.relative(context.getClickedFace());
- 
-             if (BaseFireBlock.canBePlacedAt(world, blockposition1, context.getHorizontalDirection())) {
-+                // CraftBukkit start - Store the clicked block
-+                if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(world, blockposition1, org.bukkit.event.block.BlockIgniteEvent.IgniteCause.FLINT_AND_STEEL, entityhuman).isCancelled()) {
-+                    context.getItemInHand().hurtAndBreak(1, entityhuman, LivingEntity.getSlotForHand(context.getHand()));
-+                    return InteractionResult.PASS;
-+                }
-+                // CraftBukkit end
-                 world.playSound(entityhuman, blockposition1, SoundEvents.FLINTANDSTEEL_USE, SoundSource.BLOCKS, 1.0F, world.getRandom().nextFloat() * 0.4F + 0.8F);
-                 BlockState iblockdata1 = BaseFireBlock.getState(world, blockposition1);
- 
-@@ -54,6 +60,12 @@
-                 return InteractionResult.FAIL;
-             }
-         } else {
-+            // CraftBukkit start - Store the clicked block
-+            if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(world, blockposition, org.bukkit.event.block.BlockIgniteEvent.IgniteCause.FLINT_AND_STEEL, entityhuman).isCancelled()) {
-+                context.getItemInHand().hurtAndBreak(1, entityhuman, LivingEntity.getSlotForHand(context.getHand()));
-+                return InteractionResult.PASS;
-+            }
-+            // CraftBukkit end
-             world.playSound(entityhuman, blockposition, SoundEvents.FLINTANDSTEEL_USE, SoundSource.BLOCKS, 1.0F, world.getRandom().nextFloat() * 0.4F + 0.8F);
-             world.setBlock(blockposition, (BlockState) iblockdata.setValue(BlockStateProperties.LIT, true), 11);
-             world.gameEvent((Entity) entityhuman, (Holder) GameEvent.BLOCK_CHANGE, blockposition);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/HangingEntityItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/HangingEntityItem.java.patch
deleted file mode 100644
index 79e498f9a5..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/HangingEntityItem.java.patch
+++ /dev/null
@@ -1,67 +0,0 @@
---- a/net/minecraft/world/item/HangingEntityItem.java
-+++ b/net/minecraft/world/item/HangingEntityItem.java
-@@ -19,12 +19,16 @@
- import net.minecraft.world.entity.decoration.ItemFrame;
- import net.minecraft.world.entity.decoration.Painting;
- import net.minecraft.world.entity.decoration.PaintingVariant;
--import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.item.component.CustomData;
- import net.minecraft.world.item.context.UseOnContext;
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.gameevent.GameEvent;
- 
-+// CraftBukkit start
-+import org.bukkit.entity.Player;
-+import org.bukkit.event.hanging.HangingPlaceEvent;
-+// CraftBukkit end
-+
- public class HangingEntityItem extends Item {
- 
-     private static final Component TOOLTIP_RANDOM_VARIANT = Component.translatable("painting.random").withStyle(ChatFormatting.GRAY);
-@@ -40,7 +44,7 @@
-         BlockPos blockposition = context.getClickedPos();
-         Direction enumdirection = context.getClickedFace();
-         BlockPos blockposition1 = blockposition.relative(enumdirection);
--        Player entityhuman = context.getPlayer();
-+        net.minecraft.world.entity.player.Player entityhuman = context.getPlayer();
-         ItemStack itemstack = context.getItemInHand();
- 
-         if (entityhuman != null && !this.mayPlace(entityhuman, enumdirection, itemstack, blockposition1)) {
-@@ -75,6 +79,19 @@
- 
-             if (((HangingEntity) object).survives()) {
-                 if (!world.isClientSide) {
-+                    // CraftBukkit start - fire HangingPlaceEvent
-+                    Player who = (context.getPlayer() == null) ? null : (Player) context.getPlayer().getBukkitEntity();
-+                    org.bukkit.block.Block blockClicked = world.getWorld().getBlockAt(blockposition.getX(), blockposition.getY(), blockposition.getZ());
-+                    org.bukkit.block.BlockFace blockFace = org.bukkit.craftbukkit.block.CraftBlock.notchToBlockFace(enumdirection);
-+                    org.bukkit.inventory.EquipmentSlot hand = org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(context.getHand());
-+
-+                    HangingPlaceEvent event = new HangingPlaceEvent((org.bukkit.entity.Hanging) ((HangingEntity) object).getBukkitEntity(), who, blockClicked, blockFace, hand, org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitCopy(itemstack));
-+                    world.getCraftServer().getPluginManager().callEvent(event);
-+
-+                    if (event.isCancelled()) {
-+                        return InteractionResult.FAIL;
-+                    }
-+                    // CraftBukkit end
-                     ((HangingEntity) object).playPlacementSound();
-                     world.gameEvent((Entity) entityhuman, (Holder) GameEvent.ENTITY_PLACE, ((HangingEntity) object).position());
-                     world.addFreshEntity((Entity) object);
-@@ -88,7 +105,7 @@
-         }
-     }
- 
--    protected boolean mayPlace(Player player, Direction side, ItemStack stack, BlockPos pos) {
-+    protected boolean mayPlace(net.minecraft.world.entity.player.Player player, Direction side, ItemStack stack, BlockPos pos) {
-         return !side.getAxis().isVertical() && player.mayUseItemAt(pos, side, stack);
-     }
- 
-@@ -102,7 +119,7 @@
- 
-             if (!customdata.isEmpty()) {
-                 customdata.read(holderlookup_a.createSerializationContext(NbtOps.INSTANCE), Painting.VARIANT_MAP_CODEC).result().ifPresentOrElse((holder) -> {
--                    Optional optional = ((PaintingVariant) holder.value()).title();
-+                    Optional<Component> optional = ((PaintingVariant) holder.value()).title(); // CraftBukkit - decompile error
- 
-                     Objects.requireNonNull(tooltip);
-                     optional.ifPresent(tooltip::add);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/ItemCooldowns.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/ItemCooldowns.java.patch
deleted file mode 100644
index f8d61e11c7..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/ItemCooldowns.java.patch
+++ /dev/null
@@ -1,16 +0,0 @@
---- a/net/minecraft/world/item/ItemCooldowns.java
-+++ b/net/minecraft/world/item/ItemCooldowns.java
-@@ -56,6 +56,13 @@
-     }
- 
-     public void addCooldown(ResourceLocation groupId, int duration) {
-+        // Paper start - Item cooldown events
-+        this.addCooldown(groupId, duration, true);
-+    }
-+
-+    public void addCooldown(ResourceLocation groupId, int duration, boolean callEvent) {
-+        // Event called in server override
-+        // Paper end - Item cooldown events
-         this.cooldowns.put(groupId, new ItemCooldowns.CooldownInstance(this.tickCount, this.tickCount + duration));
-         this.onCooldownStarted(groupId, duration);
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/ItemStack.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/ItemStack.java.patch
deleted file mode 100644
index 4409d94819..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/ItemStack.java.patch
+++ /dev/null
@@ -1,630 +0,0 @@
---- a/net/minecraft/world/item/ItemStack.java
-+++ b/net/minecraft/world/item/ItemStack.java
-@@ -23,6 +23,7 @@
- import net.minecraft.ChatFormatting;
- import net.minecraft.advancements.CriteriaTriggers;
- import net.minecraft.core.BlockPos;
-+import net.minecraft.core.Direction;
- import net.minecraft.core.Holder;
- import net.minecraft.core.HolderLookup;
- import net.minecraft.core.HolderSet;
-@@ -46,10 +47,12 @@
- import net.minecraft.network.chat.MutableComponent;
- import net.minecraft.network.codec.ByteBufCodecs;
- import net.minecraft.network.codec.StreamCodec;
-+import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket;
- import net.minecraft.resources.RegistryOps;
- import net.minecraft.server.level.ServerLevel;
- import net.minecraft.server.level.ServerPlayer;
- import net.minecraft.sounds.SoundEvent;
-+import net.minecraft.sounds.SoundSource;
- import net.minecraft.stats.Stats;
- import net.minecraft.tags.TagKey;
- import net.minecraft.util.ExtraCodecs;
-@@ -70,7 +73,6 @@
- import net.minecraft.world.entity.ai.attributes.Attributes;
- import net.minecraft.world.entity.decoration.ItemFrame;
- import net.minecraft.world.entity.item.ItemEntity;
--import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.flag.FeatureFlagSet;
- import net.minecraft.world.inventory.ClickAction;
- import net.minecraft.world.inventory.Slot;
-@@ -88,26 +90,53 @@
- import net.minecraft.world.item.enchantment.EnchantmentHelper;
- import net.minecraft.world.item.enchantment.ItemEnchantments;
- import net.minecraft.world.item.enchantment.Repairable;
--import net.minecraft.world.level.ItemLike;
--import net.minecraft.world.level.Level;
--import net.minecraft.world.level.block.state.BlockState;
--import net.minecraft.world.level.block.state.pattern.BlockInWorld;
- import net.minecraft.world.level.saveddata.maps.MapId;
- import org.apache.commons.lang3.mutable.MutableBoolean;
- import org.slf4j.Logger;
- 
-+// CraftBukkit start
-+import java.util.Map;
-+import java.util.Objects;
-+import net.minecraft.world.level.ItemLike;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.block.BaseEntityBlock;
-+import net.minecraft.world.level.block.BedBlock;
-+import net.minecraft.world.level.block.Blocks;
-+import net.minecraft.world.level.block.SaplingBlock;
-+import net.minecraft.world.level.block.SignBlock;
-+import net.minecraft.world.level.block.SoundType;
-+import net.minecraft.world.level.block.WitherSkullBlock;
-+import net.minecraft.world.level.block.entity.BlockEntity;
-+import net.minecraft.world.level.block.entity.SignBlockEntity;
-+import net.minecraft.world.level.block.entity.SkullBlockEntity;
-+import net.minecraft.world.level.block.state.pattern.BlockInWorld;
-+import net.minecraft.world.level.gameevent.GameEvent;
-+import org.bukkit.Location;
-+import org.bukkit.TreeType;
-+import org.bukkit.block.BlockState;
-+import org.bukkit.craftbukkit.block.CapturedBlockState;
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.craftbukkit.block.CraftBlockState;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.craftbukkit.util.CraftLocation;
-+import org.bukkit.entity.Player;
-+import org.bukkit.event.block.BlockFertilizeEvent;
-+import org.bukkit.event.player.PlayerItemDamageEvent;
-+import org.bukkit.event.world.StructureGrowEvent;
-+// CraftBukkit end
-+
- public final class ItemStack implements DataComponentHolder {
- 
-     private static final List<Component> OP_NBT_WARNING = List.of(Component.translatable("item.op_warning.line1").withStyle(ChatFormatting.RED, ChatFormatting.BOLD), Component.translatable("item.op_warning.line2").withStyle(ChatFormatting.RED), Component.translatable("item.op_warning.line3").withStyle(ChatFormatting.RED));
-     public static final Codec<ItemStack> CODEC = Codec.lazyInitialized(() -> {
--        return RecordCodecBuilder.create((instance) -> {
-+        return RecordCodecBuilder.<ItemStack>create((instance) -> { // CraftBukkit - decompile error
-             return instance.group(Item.CODEC.fieldOf("id").forGetter(ItemStack::getItemHolder), ExtraCodecs.intRange(1, 99).fieldOf("count").orElse(1).forGetter(ItemStack::getCount), DataComponentPatch.CODEC.optionalFieldOf("components", DataComponentPatch.EMPTY).forGetter((itemstack) -> {
-                 return itemstack.components.asPatch();
-             })).apply(instance, ItemStack::new);
-         });
-     });
-     public static final Codec<ItemStack> SINGLE_ITEM_CODEC = Codec.lazyInitialized(() -> {
--        return RecordCodecBuilder.create((instance) -> {
-+        return RecordCodecBuilder.<ItemStack>create((instance) -> { // CraftBukkit - decompile error
-             return instance.group(Item.CODEC.fieldOf("id").forGetter(ItemStack::getItemHolder), DataComponentPatch.CODEC.optionalFieldOf("components", DataComponentPatch.EMPTY).forGetter((itemstack) -> {
-                 return itemstack.components.asPatch();
-             })).apply(instance, (holder, datacomponentpatch) -> {
-@@ -132,20 +161,38 @@
-             if (i <= 0) {
-                 return ItemStack.EMPTY;
-             } else {
--                Holder<Item> holder = (Holder) null.ITEM_STREAM_CODEC.decode(registryfriendlybytebuf);
-+                Holder<Item> holder = (Holder) ITEM_STREAM_CODEC.decode(registryfriendlybytebuf); // CraftBukkit - decompile error
-                 DataComponentPatch datacomponentpatch = (DataComponentPatch) DataComponentPatch.STREAM_CODEC.decode(registryfriendlybytebuf);
- 
--                return new ItemStack(holder, i, datacomponentpatch);
-+                // CraftBukkit start
-+                ItemStack itemstack = new ItemStack(holder, i, datacomponentpatch);
-+                if (false && !datacomponentpatch.isEmpty()) { // Paper - This is no longer needed with raw NBT being handled in metadata
-+                    CraftItemStack.setItemMeta(itemstack, CraftItemStack.getItemMeta(itemstack));
-+                }
-+                return itemstack;
-+                // CraftBukkit end
-             }
-         }
- 
-         public void encode(RegistryFriendlyByteBuf registryfriendlybytebuf, ItemStack itemstack) {
--            if (itemstack.isEmpty()) {
-+            if (itemstack.isEmpty() || itemstack.getItem() == null) { // CraftBukkit - NPE fix itemstack.getItem()
-                 registryfriendlybytebuf.writeVarInt(0);
-             } else {
-                 registryfriendlybytebuf.writeVarInt(itemstack.getCount());
--                null.ITEM_STREAM_CODEC.encode(registryfriendlybytebuf, itemstack.getItemHolder());
-+                // Spigot start - filter
-+                // itemstack = itemstack.copy();
-+                // CraftItemStack.setItemMeta(itemstack, CraftItemStack.getItemMeta(itemstack)); // Paper - This is no longer with raw NBT being handled in metadata
-+                // Spigot end
-+                ITEM_STREAM_CODEC.encode(registryfriendlybytebuf, itemstack.getItemHolder()); // CraftBukkit - decompile error
-+                // Paper start - adventure; conditionally render translatable components
-+                boolean prev = net.minecraft.network.chat.ComponentSerialization.DONT_RENDER_TRANSLATABLES.get();
-+                try {
-+                    net.minecraft.network.chat.ComponentSerialization.DONT_RENDER_TRANSLATABLES.set(true);
-                 DataComponentPatch.STREAM_CODEC.encode(registryfriendlybytebuf, itemstack.components.asPatch());
-+                } finally {
-+                    net.minecraft.network.chat.ComponentSerialization.DONT_RENDER_TRANSLATABLES.set(prev);
-+                }
-+                // Paper end - adventure; conditionally render translatable components
-             }
-         }
-     };
-@@ -187,7 +234,7 @@
- 
-         return dataresult.isError() ? dataresult.map((unit) -> {
-             return stack;
--        }) : (stack.getCount() > stack.getMaxStackSize() ? DataResult.error(() -> {
-+        }) : (stack.getCount() > stack.getMaxStackSize() ? DataResult.<ItemStack>error(() -> { // CraftBukkit - decompile error
-             int i = stack.getCount();
- 
-             return "Item stack with stack size of " + i + " was larger than maximum: " + stack.getMaxStackSize();
-@@ -294,8 +341,9 @@
-                 j = itemstack.getMaxStackSize();
-             } while (i <= j);
- 
-+            int finalI = i, finalJ = j; // CraftBukkit - decompile error
-             return DataResult.error(() -> {
--                return "Item stack with count of " + i + " was larger than maximum: " + j;
-+                return "Item stack with count of " + finalI + " was larger than maximum: " + finalJ; // CraftBukkit - decompile error
-             });
-         }
-     }
-@@ -370,32 +418,200 @@
-     }
- 
-     public InteractionResult useOn(UseOnContext context) {
--        Player entityhuman = context.getPlayer();
-+        net.minecraft.world.entity.player.Player entityhuman = context.getPlayer();
-         BlockPos blockposition = context.getClickedPos();
- 
-         if (entityhuman != null && !entityhuman.getAbilities().mayBuild && !this.canPlaceOnBlockInAdventureMode(new BlockInWorld(context.getLevel(), blockposition, false))) {
-             return InteractionResult.PASS;
-         } else {
-             Item item = this.getItem();
--            InteractionResult enuminteractionresult = item.useOn(context);
-+            // CraftBukkit start - handle all block place event logic here
-+            DataComponentPatch oldData = this.components.asPatch();
-+            int oldCount = this.getCount();
-+            ServerLevel world = (ServerLevel) context.getLevel();
- 
-+            if (!(item instanceof BucketItem/* || item instanceof SolidBucketItem*/)) { // if not bucket // Paper - Fix cancelled powdered snow bucket placement
-+                world.captureBlockStates = true;
-+                // special case bonemeal
-+                if (item == Items.BONE_MEAL) {
-+                    world.captureTreeGeneration = true;
-+                }
-+            }
-+            InteractionResult enuminteractionresult;
-+            try {
-+                enuminteractionresult = item.useOn(context);
-+            } finally {
-+                world.captureBlockStates = false;
-+            }
-+            DataComponentPatch newData = this.components.asPatch();
-+            int newCount = this.getCount();
-+            this.setCount(oldCount);
-+            this.restorePatch(oldData);
-+            if (enuminteractionresult.consumesAction() && world.captureTreeGeneration && world.capturedBlockStates.size() > 0) {
-+                world.captureTreeGeneration = false;
-+                Location location = CraftLocation.toBukkit(blockposition, world.getWorld());
-+                TreeType treeType = SaplingBlock.treeType;
-+                SaplingBlock.treeType = null;
-+                List<CraftBlockState> blocks = new java.util.ArrayList<>(world.capturedBlockStates.values());
-+                world.capturedBlockStates.clear();
-+                StructureGrowEvent structureEvent = null;
-+                if (treeType != null) {
-+                    boolean isBonemeal = this.getItem() == Items.BONE_MEAL;
-+                    structureEvent = new StructureGrowEvent(location, treeType, isBonemeal, (Player) entityhuman.getBukkitEntity(), (List< BlockState>) (List<? extends BlockState>) blocks);
-+                    org.bukkit.Bukkit.getPluginManager().callEvent(structureEvent);
-+                }
-+
-+                BlockFertilizeEvent fertilizeEvent = new BlockFertilizeEvent(CraftBlock.at(world, blockposition), (Player) entityhuman.getBukkitEntity(), (List< BlockState>) (List<? extends BlockState>) blocks);
-+                fertilizeEvent.setCancelled(structureEvent != null && structureEvent.isCancelled());
-+                org.bukkit.Bukkit.getPluginManager().callEvent(fertilizeEvent);
-+
-+                if (!fertilizeEvent.isCancelled()) {
-+                    // Change the stack to its new contents if it hasn't been tampered with.
-+                    if (this.getCount() == oldCount && Objects.equals(this.components.asPatch(), oldData)) {
-+                        this.restorePatch(newData);
-+                        this.setCount(newCount);
-+                    }
-+                    for (CraftBlockState blockstate : blocks) {
-+                        // SPIGOT-7572 - Move fix for SPIGOT-7248 to CapturedBlockState, to allow bees in bee nest
-+                        CapturedBlockState.setBlockState(blockstate);
-+                        world.checkCapturedTreeStateForObserverNotify(blockposition, blockstate); // Paper - notify observers even if grow failed
-+                    }
-+                    entityhuman.awardStat(Stats.ITEM_USED.get(item)); // SPIGOT-7236 - award stat
-+                }
-+
-+                SignItem.openSign = null; // SPIGOT-6758 - Reset on early return
-+                return enuminteractionresult;
-+            }
-+            world.captureTreeGeneration = false;
-+
-             if (entityhuman != null && enuminteractionresult instanceof InteractionResult.Success) {
-                 InteractionResult.Success enuminteractionresult_d = (InteractionResult.Success) enuminteractionresult;
- 
-                 if (enuminteractionresult_d.wasItemInteraction()) {
--                    entityhuman.awardStat(Stats.ITEM_USED.get(item));
-+                    InteractionHand enumhand = context.getHand();
-+                    org.bukkit.event.block.BlockPlaceEvent placeEvent = null;
-+                    List<BlockState> blocks = new java.util.ArrayList<>(world.capturedBlockStates.values());
-+                    world.capturedBlockStates.clear();
-+                    if (blocks.size() > 1) {
-+                        placeEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callBlockMultiPlaceEvent(world, entityhuman, enumhand, blocks, blockposition.getX(), blockposition.getY(), blockposition.getZ());
-+                    } else if (blocks.size() == 1 && item != Items.POWDER_SNOW_BUCKET) { // Paper - Fix cancelled powdered snow bucket placement
-+                        placeEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPlaceEvent(world, entityhuman, enumhand, blocks.get(0), blockposition.getX(), blockposition.getY(), blockposition.getZ());
-+                    }
-+
-+                    if (placeEvent != null && (placeEvent.isCancelled() || !placeEvent.canBuild())) {
-+                        enuminteractionresult = InteractionResult.FAIL; // cancel placement
-+                        // PAIL: Remove this when MC-99075 fixed
-+                        placeEvent.getPlayer().updateInventory();
-+                        world.capturedTileEntities.clear(); // Paper - Allow chests to be placed with NBT data; clear out block entities as chests and such will pop loot
-+                        // revert back all captured blocks
-+                        world.preventPoiUpdated = true; // CraftBukkit - SPIGOT-5710
-+                    world.isBlockPlaceCancelled = true; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent
-+                        for (BlockState blockstate : blocks) {
-+                            blockstate.update(true, false);
-+                        }
-+                    world.isBlockPlaceCancelled = false; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent
-+                        world.preventPoiUpdated = false;
-+
-+                        // Brute force all possible updates
-+                        // Paper start - Don't resync blocks
-+                        // BlockPos placedPos = ((CraftBlock) placeEvent.getBlock()).getPosition();
-+                        // for (Direction dir : Direction.values()) {
-+                        //     ((ServerPlayer) entityhuman).connection.send(new ClientboundBlockUpdatePacket(world, placedPos.relative(dir)));
-+                        // }
-+                        // Paper end - Don't resync blocks
-+                        SignItem.openSign = null; // SPIGOT-6758 - Reset on early return
-+                    } else {
-+                        // Change the stack to its new contents if it hasn't been tampered with.
-+                        if (this.getCount() == oldCount && Objects.equals(this.components.asPatch(), oldData)) {
-+                            this.restorePatch(newData);
-+                            this.setCount(newCount);
-+                        }
-+
-+                        for (Map.Entry<BlockPos, BlockEntity> e : world.capturedTileEntities.entrySet()) {
-+                            world.setBlockEntity(e.getValue());
-+                        }
-+
-+                        for (BlockState blockstate : blocks) {
-+                            int updateFlag = ((CraftBlockState) blockstate).getFlag();
-+                            net.minecraft.world.level.block.state.BlockState oldBlock = ((CraftBlockState) blockstate).getHandle();
-+                            BlockPos newblockposition = ((CraftBlockState) blockstate).getPosition();
-+                            net.minecraft.world.level.block.state.BlockState block = world.getBlockState(newblockposition);
-+
-+                            if (!(block.getBlock() instanceof BaseEntityBlock)) { // Containers get placed automatically
-+                                block.onPlace(world, newblockposition, oldBlock, true, context);
-+                            }
-+
-+                            world.notifyAndUpdatePhysics(newblockposition, null, oldBlock, block, world.getBlockState(newblockposition), updateFlag, 512); // send null chunk as chunk.k() returns false by this point
-+                        }
-+
-+                        if (this.item == Items.WITHER_SKELETON_SKULL) { // Special case skulls to allow wither spawns to be cancelled
-+                            BlockPos bp = blockposition;
-+                            if (!world.getBlockState(blockposition).canBeReplaced()) {
-+                                if (!world.getBlockState(blockposition).isSolid()) {
-+                                    bp = null;
-+                                } else {
-+                                    bp = bp.relative(context.getClickedFace());
-+                                }
-+                            }
-+                            if (bp != null) {
-+                                BlockEntity te = world.getBlockEntity(bp);
-+                                if (te instanceof SkullBlockEntity) {
-+                                    WitherSkullBlock.checkSpawn(world, bp, (SkullBlockEntity) te);
-+                                }
-+                            }
-+                        }
-+
-+                        // SPIGOT-4678
-+                        if (this.item instanceof SignItem && SignItem.openSign != null) {
-+                            try {
-+                                if (world.getBlockEntity(SignItem.openSign) instanceof SignBlockEntity tileentitysign) {
-+                                    if (world.getBlockState(SignItem.openSign).getBlock() instanceof SignBlock blocksign) {
-+                                        blocksign.openTextEdit(entityhuman, tileentitysign, true, io.papermc.paper.event.player.PlayerOpenSignEvent.Cause.PLACE); // Craftbukkit // Paper - Add PlayerOpenSignEvent
-+                                    }
-+                                }
-+                            } finally {
-+                                SignItem.openSign = null;
-+                            }
-+                        }
-+
-+                        // SPIGOT-7315: Moved from BlockBed#setPlacedBy
-+                        if (placeEvent != null && this.item instanceof BedItem) {
-+                            BlockPos position = ((CraftBlock) placeEvent.getBlock()).getPosition();
-+                            net.minecraft.world.level.block.state.BlockState blockData = world.getBlockState(position);
-+
-+                            if (blockData.getBlock() instanceof BedBlock) {
-+                                world.blockUpdated(position, Blocks.AIR);
-+                                blockData.updateNeighbourShapes(world, position, 3);
-+                            }
-+                        }
-+
-+                        // SPIGOT-1288 - play sound stripped from ItemBlock
-+                        if (this.item instanceof BlockItem) {
-+                        // Paper start - Fix spigot sound playing for BlockItem ItemStacks
-+                        BlockPos position = new net.minecraft.world.item.context.BlockPlaceContext(context).getClickedPos();
-+                        net.minecraft.world.level.block.state.BlockState blockData = world.getBlockState(position);
-+                        SoundType soundeffecttype = blockData.getSoundType();
-+                        // Paper end - Fix spigot sound playing for BlockItem ItemStacks
-+                            world.playSound(entityhuman, blockposition, soundeffecttype.getPlaceSound(), SoundSource.BLOCKS, (soundeffecttype.getVolume() + 1.0F) / 2.0F, soundeffecttype.getPitch() * 0.8F);
-+                        }
-+
-+                        entityhuman.awardStat(Stats.ITEM_USED.get(item));
-+                    }
-                 }
-             }
-+            world.capturedTileEntities.clear();
-+            world.capturedBlockStates.clear();
-+            // CraftBukkit end
- 
-             return enuminteractionresult;
-         }
-     }
- 
--    public float getDestroySpeed(BlockState state) {
-+    public float getDestroySpeed(net.minecraft.world.level.block.state.BlockState state) {
-         return this.getItem().getDestroySpeed(this, state);
-     }
- 
--    public InteractionResult use(Level world, Player user, InteractionHand hand) {
-+    public InteractionResult use(Level world, net.minecraft.world.entity.player.Player user, InteractionHand hand) {
-         ItemStack itemstack = this.copy();
-         boolean flag = this.getUseDuration(user) <= 0;
-         InteractionResult enuminteractionresult = this.getItem().use(world, user, hand);
-@@ -490,27 +706,66 @@
-         return this.isDamageableItem() && this.getDamageValue() >= this.getMaxDamage() - 1;
-     }
- 
--    public void hurtAndBreak(int amount, ServerLevel world, @Nullable ServerPlayer player, Consumer<Item> breakCallback) {
--        int j = this.processDurabilityChange(amount, world, player);
-+    public void hurtAndBreak(int amount, ServerLevel world, @Nullable LivingEntity player, Consumer<Item> breakCallback) {  // Paper - Add EntityDamageItemEvent
-+        // Paper start - add force boolean overload
-+        this.hurtAndBreak(amount, world, player, breakCallback, false);
-+    }
-+    public void hurtAndBreak(int amount, ServerLevel world, @Nullable LivingEntity player, Consumer<Item> breakCallback, boolean force) {  // Paper - Add EntityDamageItemEvent
-+        // Paper end
-+        int originalDamage = amount; // Paper - Expand PlayerItemDamageEvent
-+        int j = this.processDurabilityChange(amount, world, player, force); // Paper
-+        // CraftBukkit start
-+        if (player instanceof final ServerPlayer serverPlayer) { // Paper - Add EntityDamageItemEvent
-+            PlayerItemDamageEvent event = new PlayerItemDamageEvent(serverPlayer.getBukkitEntity(), CraftItemStack.asCraftMirror(this), j, originalDamage); // Paper - Add EntityDamageItemEvent
-+            event.getPlayer().getServer().getPluginManager().callEvent(event);
- 
-+            if (j != event.getDamage() || event.isCancelled()) {
-+                event.getPlayer().updateInventory();
-+            }
-+            if (event.isCancelled()) {
-+                return;
-+            }
-+
-+            j = event.getDamage();
-+            // Paper start - Add EntityDamageItemEvent
-+        } else if (player != null) {
-+            io.papermc.paper.event.entity.EntityDamageItemEvent event = new io.papermc.paper.event.entity.EntityDamageItemEvent(player.getBukkitLivingEntity(), CraftItemStack.asCraftMirror(this), amount);
-+            if (!event.callEvent()) {
-+                return;
-+            }
-+            j = event.getDamage();
-+            // Paper end - Add EntityDamageItemEvent
-+        }
-+        // CraftBukkit end
-+
-         if (j != 0) {
-             this.applyDamage(this.getDamageValue() + j, player, breakCallback);
-         }
- 
-     }
- 
--    private int processDurabilityChange(int baseDamage, ServerLevel world, @Nullable ServerPlayer player) {
--        return !this.isDamageableItem() ? 0 : (player != null && player.hasInfiniteMaterials() ? 0 : (baseDamage > 0 ? EnchantmentHelper.processDurabilityChange(world, this, baseDamage) : baseDamage));
-+    private int processDurabilityChange(int baseDamage, ServerLevel world, @Nullable LivingEntity player) {  // Paper - Add EntityDamageItemEvent
-+        // Paper start - itemstack damage api
-+        return processDurabilityChange(baseDamage, world, player, false);
-     }
-+    private int processDurabilityChange(int baseDamage, ServerLevel world, @Nullable LivingEntity player, boolean force) {
-+        return !this.isDamageableItem() ? 0 : (player instanceof ServerPlayer && player.hasInfiniteMaterials() && !force ? 0 : (baseDamage > 0 ? EnchantmentHelper.processDurabilityChange(world, this, baseDamage) : baseDamage));  // Paper - Add EntityDamageItemEvent
-+        // Paper end - itemstack damage api
-+    }
- 
--    private void applyDamage(int damage, @Nullable ServerPlayer player, Consumer<Item> breakCallback) {
--        if (player != null) {
--            CriteriaTriggers.ITEM_DURABILITY_CHANGED.trigger(player, this, damage);
-+    private void applyDamage(int damage, @Nullable LivingEntity player, Consumer<Item> breakCallback) { // Paper - Add EntityDamageItemEvent
-+        if (player instanceof final ServerPlayer serverPlayer) { // Paper - Add EntityDamageItemEvent
-+            CriteriaTriggers.ITEM_DURABILITY_CHANGED.trigger(serverPlayer, this, damage); // Paper - Add EntityDamageItemEvent
-         }
- 
-         this.setDamageValue(damage);
-         if (this.isBroken()) {
-             Item item = this.getItem();
-+            // CraftBukkit start - Check for item breaking
-+            if (this.count == 1 && player instanceof final ServerPlayer serverPlayer) { // Paper - Add EntityDamageItemEvent
-+                org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerItemBreakEvent(serverPlayer, this); // Paper - Add EntityDamageItemEvent
-+            }
-+            // CraftBukkit end
- 
-             this.shrink(1);
-             breakCallback.accept(item);
-@@ -518,7 +773,7 @@
- 
-     }
- 
--    public void hurtWithoutBreaking(int amount, Player player) {
-+    public void hurtWithoutBreaking(int amount, net.minecraft.world.entity.player.Player player) {
-         if (player instanceof ServerPlayer entityplayer) {
-             int j = this.processDurabilityChange(amount, entityplayer.serverLevel(), entityplayer);
- 
-@@ -535,6 +790,11 @@
-     }
- 
-     public void hurtAndBreak(int amount, LivingEntity entity, EquipmentSlot slot) {
-+        // Paper start - add param to skip infinite mats check
-+        this.hurtAndBreak(amount, entity, slot, false);
-+    }
-+    public void hurtAndBreak(int amount, LivingEntity entity, EquipmentSlot slot, boolean force) {
-+        // Paper end - add param to skip infinite mats check
-         Level world = entity.level();
- 
-         if (world instanceof ServerLevel worldserver) {
-@@ -546,9 +806,9 @@
-                 entityplayer = null;
-             }
- 
--            this.hurtAndBreak(amount, worldserver, entityplayer, (item) -> {
--                entity.onEquippedItemBroken(item, slot);
--            });
-+            this.hurtAndBreak(amount, worldserver, entity, (item) -> { // Paper - Add EntityDamageItemEvent
-+                if (slot != null) entity.onEquippedItemBroken(item, slot); // Paper - itemstack damage API - do not process entity related callbacks when damaging from API
-+            }, force); // Paper - itemstack damage API
-         }
- 
-     }
-@@ -580,11 +840,11 @@
-         return this.getItem().getBarColor(this);
-     }
- 
--    public boolean overrideStackedOnOther(Slot slot, ClickAction clickType, Player player) {
-+    public boolean overrideStackedOnOther(Slot slot, ClickAction clickType, net.minecraft.world.entity.player.Player player) {
-         return this.getItem().overrideStackedOnOther(this, slot, clickType, player);
-     }
- 
--    public boolean overrideOtherStackedOnMe(ItemStack stack, Slot slot, ClickAction clickType, Player player, SlotAccess cursorStackReference) {
-+    public boolean overrideOtherStackedOnMe(ItemStack stack, Slot slot, ClickAction clickType, net.minecraft.world.entity.player.Player player, SlotAccess cursorStackReference) {
-         return this.getItem().overrideOtherStackedOnMe(this, stack, slot, clickType, player, cursorStackReference);
-     }
- 
-@@ -592,8 +852,8 @@
-         Item item = this.getItem();
- 
-         if (item.hurtEnemy(this, target, user)) {
--            if (user instanceof Player) {
--                Player entityhuman = (Player) user;
-+            if (user instanceof net.minecraft.world.entity.player.Player) {
-+                net.minecraft.world.entity.player.Player entityhuman = (net.minecraft.world.entity.player.Player) user;
- 
-                 entityhuman.awardStat(Stats.ITEM_USED.get(item));
-             }
-@@ -608,7 +868,7 @@
-         this.getItem().postHurtEnemy(this, target, user);
-     }
- 
--    public void mineBlock(Level world, BlockState state, BlockPos pos, Player miner) {
-+    public void mineBlock(Level world, net.minecraft.world.level.block.state.BlockState state, BlockPos pos, net.minecraft.world.entity.player.Player miner) {
-         Item item = this.getItem();
- 
-         if (item.mineBlock(this, world, state, pos, miner)) {
-@@ -617,11 +877,11 @@
- 
-     }
- 
--    public boolean isCorrectToolForDrops(BlockState state) {
-+    public boolean isCorrectToolForDrops(net.minecraft.world.level.block.state.BlockState state) {
-         return this.getItem().isCorrectToolForDrops(this, state);
-     }
- 
--    public InteractionResult interactLivingEntity(Player user, LivingEntity entity, InteractionHand hand) {
-+    public InteractionResult interactLivingEntity(net.minecraft.world.entity.player.Player user, LivingEntity entity, InteractionHand hand) {
-         return this.getItem().interactLivingEntity(this, user, entity, hand);
-     }
- 
-@@ -736,7 +996,7 @@
- 
-     }
- 
--    public void onCraftedBy(Level world, Player player, int amount) {
-+    public void onCraftedBy(Level world, net.minecraft.world.entity.player.Player player, int amount) {
-         player.awardStat(Stats.ITEM_CRAFTED.get(this.getItem()), amount);
-         this.getItem().onCraftedBy(this, world, player);
-     }
-@@ -768,7 +1028,13 @@
- 
-     public boolean useOnRelease() {
-         return this.getItem().useOnRelease(this);
-+    }
-+
-+    // CraftBukkit start
-+    public void restorePatch(DataComponentPatch datacomponentpatch) {
-+        this.components.restorePatch(datacomponentpatch);
-     }
-+    // CraftBukkit end
- 
-     @Nullable
-     public <T> T set(DataComponentType<? super T> type, @Nullable T value) {
-@@ -805,6 +1071,25 @@
-             this.getItem().verifyComponentsAfterLoad(this);
-         }
-     }
-+
-+    // Paper start - (this is just a good no conflict location)
-+    public org.bukkit.inventory.ItemStack asBukkitMirror() {
-+        return CraftItemStack.asCraftMirror(this);
-+    }
-+    public org.bukkit.inventory.ItemStack asBukkitCopy() {
-+        return CraftItemStack.asCraftMirror(this.copy());
-+    }
-+    public static ItemStack fromBukkitCopy(org.bukkit.inventory.ItemStack itemstack) {
-+        return CraftItemStack.asNMSCopy(itemstack);
-+    }
-+    private org.bukkit.craftbukkit.inventory.CraftItemStack bukkitStack;
-+    public org.bukkit.inventory.ItemStack getBukkitStack() {
-+        if (bukkitStack == null || bukkitStack.handle != this) {
-+            bukkitStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(this);
-+        }
-+        return bukkitStack;
-+    }
-+    // Paper end
- 
-     public void applyComponents(DataComponentPatch changes) {
-         this.components.applyPatch(changes);
-@@ -858,7 +1143,7 @@
-     }
- 
-     private <T extends TooltipProvider> void addToTooltip(DataComponentType<T> componentType, Item.TooltipContext context, Consumer<Component> textConsumer, TooltipFlag type) {
--        T t0 = (TooltipProvider) this.get(componentType);
-+        T t0 = (T) this.get(componentType); // CraftBukkit - decompile error
- 
-         if (t0 != null) {
-             t0.addToTooltip(context, textConsumer, type);
-@@ -866,7 +1151,7 @@
- 
-     }
- 
--    public List<Component> getTooltipLines(Item.TooltipContext context, @Nullable Player player, TooltipFlag type) {
-+    public List<Component> getTooltipLines(Item.TooltipContext context, @Nullable net.minecraft.world.entity.player.Player player, TooltipFlag type) {
-         boolean flag = this.getItem().shouldPrintOpWarning(this, player);
- 
-         if (!type.isCreative() && this.has(DataComponents.HIDE_TOOLTIP)) {
-@@ -941,7 +1226,7 @@
-         }
-     }
- 
--    private void addAttributeTooltips(Consumer<Component> textConsumer, @Nullable Player player) {
-+    private void addAttributeTooltips(Consumer<Component> textConsumer, @Nullable net.minecraft.world.entity.player.Player player) {
-         ItemAttributeModifiers itemattributemodifiers = (ItemAttributeModifiers) this.getOrDefault(DataComponents.ATTRIBUTE_MODIFIERS, ItemAttributeModifiers.EMPTY);
- 
-         if (itemattributemodifiers.showInTooltip()) {
-@@ -966,7 +1251,7 @@
-         }
-     }
- 
--    private void addModifierTooltip(Consumer<Component> textConsumer, @Nullable Player player, Holder<Attribute> attribute, AttributeModifier modifier) {
-+    private void addModifierTooltip(Consumer<Component> textConsumer, @Nullable net.minecraft.world.entity.player.Player player, Holder<Attribute> attribute, AttributeModifier modifier) {
-         double d0 = modifier.amount();
-         boolean flag = false;
- 
-@@ -1091,6 +1376,19 @@
-         EnchantmentHelper.forEachModifier(this, slot, attributeModifierConsumer);
-     }
- 
-+    // CraftBukkit start
-+    @Deprecated
-+    public void setItem(Item item) {
-+        this.bukkitStack = null; // Paper
-+        this.item = item;
-+        // Paper start - change base component prototype
-+        final DataComponentPatch patch = this.getComponentsPatch();
-+        this.components = new PatchedDataComponentMap(this.item.components());
-+        this.applyComponents(patch);
-+        // Paper end - change base component prototype
-+    }
-+    // CraftBukkit end
-+
-     public Component getDisplayName() {
-         MutableComponent ichatmutablecomponent = Component.empty().append(this.getHoverName());
- 
-@@ -1153,7 +1451,7 @@
-     }
- 
-     public void consume(int amount, @Nullable LivingEntity entity) {
--        if (entity == null || !entity.hasInfiniteMaterials()) {
-+        if ((entity == null || !entity.hasInfiniteMaterials()) && this != ItemStack.EMPTY) { // CraftBukkit
-             this.shrink(amount);
-         }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/ItemUtils.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/ItemUtils.java.patch
deleted file mode 100644
index 163c53a0d4..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/ItemUtils.java.patch
+++ /dev/null
@@ -1,19 +0,0 @@
---- a/net/minecraft/world/item/ItemUtils.java
-+++ b/net/minecraft/world/item/ItemUtils.java
-@@ -41,7 +41,15 @@
-     public static void onContainerDestroyed(ItemEntity itemEntity, Iterable<ItemStack> contents) {
-         Level level = itemEntity.level();
-         if (!level.isClientSide) {
--            contents.forEach(stack -> level.addFreshEntity(new ItemEntity(level, itemEntity.getX(), itemEntity.getY(), itemEntity.getZ(), stack)));
-+            // Paper start - call EntityDropItemEvent
-+            contents.forEach(stack -> {
-+                ItemEntity droppedItem = new ItemEntity(level, itemEntity.getX(), itemEntity.getY(), itemEntity.getZ(), stack);
-+                org.bukkit.event.entity.EntityDropItemEvent event = new org.bukkit.event.entity.EntityDropItemEvent(itemEntity.getBukkitEntity(), (org.bukkit.entity.Item) droppedItem.getBukkitEntity());
-+                if (event.callEvent()) {
-+                    level.addFreshEntity(droppedItem);
-+                }
-+            });
-+            // Paper end - call EntityDropItemEvent
-         }
-     }
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/LeadItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/LeadItem.java.patch
deleted file mode 100644
index e45ad43774..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/LeadItem.java.patch
+++ /dev/null
@@ -1,92 +0,0 @@
---- a/net/minecraft/world/item/LeadItem.java
-+++ b/net/minecraft/world/item/LeadItem.java
-@@ -18,6 +18,11 @@
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.phys.AABB;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.CraftEquipmentSlot;
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.event.hanging.HangingPlaceEvent;
-+// CraftBukkit end
- 
- public class LeadItem extends Item {
- 
-@@ -35,37 +40,70 @@
-             Player entityhuman = context.getPlayer();
- 
-             if (!world.isClientSide && entityhuman != null) {
--                return LeadItem.bindPlayerMobs(entityhuman, world, blockposition);
-+                return LeadItem.bindPlayerMobs(entityhuman, world, blockposition, context.getHand()); // CraftBukkit - Pass hand
-             }
-         }
- 
-         return InteractionResult.PASS;
-     }
- 
--    public static InteractionResult bindPlayerMobs(Player player, Level world, BlockPos pos) {
-+    public static InteractionResult bindPlayerMobs(Player entityhuman, Level world, BlockPos blockposition, net.minecraft.world.InteractionHand enumhand) { // CraftBukkit - Add EnumHand
-         LeashFenceKnotEntity entityleash = null;
--        List<Leashable> list = LeadItem.leashableInArea(world, pos, (leashable) -> {
--            return leashable.getLeashHolder() == player;
-+        List<Leashable> list = LeadItem.leashableInArea(world, blockposition, (leashable) -> {
-+            return leashable.getLeashHolder() == entityhuman;
-         });
- 
-         Leashable leashable;
- 
--        for (Iterator iterator = list.iterator(); iterator.hasNext(); leashable.setLeashedTo(entityleash, true)) {
-+        for (Iterator iterator = list.iterator(); iterator.hasNext();) { // CraftBukkit - handle setLeashedTo at end of loop
-             leashable = (Leashable) iterator.next();
-             if (entityleash == null) {
--                entityleash = LeashFenceKnotEntity.getOrCreateKnot(world, pos);
-+                entityleash = LeashFenceKnotEntity.getOrCreateKnot(world, blockposition);
-+
-+                // CraftBukkit start - fire HangingPlaceEvent
-+                org.bukkit.inventory.EquipmentSlot hand = CraftEquipmentSlot.getHand(enumhand);
-+                HangingPlaceEvent event = new HangingPlaceEvent((org.bukkit.entity.Hanging) entityleash.getBukkitEntity(), entityhuman != null ? (org.bukkit.entity.Player) entityhuman.getBukkitEntity() : null, CraftBlock.at(world, blockposition), org.bukkit.block.BlockFace.SELF, hand);
-+                world.getCraftServer().getPluginManager().callEvent(event);
-+
-+                if (event.isCancelled()) {
-+                    entityleash.discard(null); // CraftBukkit - add Bukkit remove cause
-+                    return InteractionResult.PASS;
-+                }
-+                // CraftBukkit end
-                 entityleash.playPlacementSound();
-             }
-+
-+            // CraftBukkit start
-+            if (leashable instanceof Entity leashed) {
-+                if (org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerLeashEntityEvent(leashed, entityleash, entityhuman, enumhand).isCancelled()) {
-+                    iterator.remove();
-+                    continue;
-+                }
-+            }
-+
-+            leashable.setLeashedTo(entityleash, true);
-+            // CraftBukkit end
-         }
- 
-         if (!list.isEmpty()) {
--            world.gameEvent((Holder) GameEvent.BLOCK_ATTACH, pos, GameEvent.Context.of((Entity) player));
-+            world.gameEvent((Holder) GameEvent.BLOCK_ATTACH, blockposition, GameEvent.Context.of((Entity) entityhuman));
-             return InteractionResult.SUCCESS_SERVER;
-         } else {
-+            // CraftBukkit start- remove leash if we do not leash any entity because of the cancelled event
-+            if (entityleash != null) {
-+                entityleash.discard(null);
-+            }
-+            // CraftBukkit end
-             return InteractionResult.PASS;
-         }
-     }
- 
-+    // CraftBukkit start
-+    public static InteractionResult bindPlayerMobs(Player player, Level world, BlockPos pos) {
-+        return LeadItem.bindPlayerMobs(player, world, pos, net.minecraft.world.InteractionHand.MAIN_HAND);
-+    }
-+    // CraftBukkit end
-+
-     public static List<Leashable> leashableInArea(Level world, BlockPos pos, Predicate<Leashable> predicate) {
-         double d0 = 7.0D;
-         int i = pos.getX();
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/MapItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/MapItem.java.patch
deleted file mode 100644
index 5374da2aa0..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/MapItem.java.patch
+++ /dev/null
@@ -1,22 +0,0 @@
---- a/net/minecraft/world/item/MapItem.java
-+++ b/net/minecraft/world/item/MapItem.java
-@@ -97,8 +97,8 @@
-                             int r = (j / i + o - 64) * i;
-                             int s = (k / i + p - 64) * i;
-                             Multiset<MapColor> multiset = LinkedHashMultiset.create();
--                            LevelChunk levelChunk = world.getChunk(SectionPos.blockToSectionCoord(r), SectionPos.blockToSectionCoord(s));
--                            if (!levelChunk.isEmpty()) {
-+                            LevelChunk levelChunk = world.getChunkIfLoaded(SectionPos.blockToSectionCoord(r), SectionPos.blockToSectionCoord(s)); // Paper - Maps shouldn't load chunks
-+                            if (levelChunk != null && !levelChunk.isEmpty()) { // Paper - Maps shouldn't load chunks
-                                 int t = 0;
-                                 double e = 0.0;
-                                 if (world.dimensionType().hasCeiling()) {
-@@ -205,7 +205,7 @@
- 
-                 for (int n = 0; n < 128; n++) {
-                     for (int o = 0; o < 128; o++) {
--                        Holder<Biome> holder = world.getBiome(mutableBlockPos.set((l + o) * i, 0, (m + n) * i));
-+                        Holder<Biome> holder = world.getUncachedNoiseBiome((l + o) * i, 0, (m + n) * i); // Paper - Perf: Use seed based lookup for treasure maps
-                         bls[n * 128 + o] = holder.is(BiomeTags.WATER_ON_MAP_OUTLINES);
-                     }
-                 }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/MinecartItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/MinecartItem.java.patch
deleted file mode 100644
index f97004f5f9..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/MinecartItem.java.patch
+++ /dev/null
@@ -1,17 +0,0 @@
---- a/net/minecraft/world/item/MinecartItem.java
-+++ b/net/minecraft/world/item/MinecartItem.java
-@@ -67,7 +67,13 @@
-                 if (world instanceof ServerLevel) {
-                     ServerLevel worldserver = (ServerLevel) world;
- 
--                    worldserver.addFreshEntity(entityminecartabstract);
-+                    // CraftBukkit start
-+                    if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPlaceEvent(context, entityminecartabstract).isCancelled()) {
-+                    if (context.getPlayer() != null) context.getPlayer().containerMenu.sendAllDataToRemote(); // Paper - Fix inventory desync
-+                        return InteractionResult.FAIL;
-+                    }
-+                    // CraftBukkit end
-+                    if (!worldserver.addFreshEntity(entityminecartabstract)) return InteractionResult.PASS; // CraftBukkit
-                     worldserver.gameEvent((Holder) GameEvent.ENTITY_PLACE, blockposition, GameEvent.Context.of(context.getPlayer(), worldserver.getBlockState(blockposition.below())));
-                 }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/PotionItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/PotionItem.java.patch
deleted file mode 100644
index 185bcf8992..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/PotionItem.java.patch
+++ /dev/null
@@ -1,15 +0,0 @@
---- a/net/minecraft/world/item/PotionItem.java
-+++ b/net/minecraft/world/item/PotionItem.java
-@@ -42,6 +42,12 @@
-         PotionContents potionContents = itemStack.getOrDefault(DataComponents.POTION_CONTENTS, PotionContents.EMPTY);
-         BlockState blockState = level.getBlockState(blockPos);
-         if (context.getClickedFace() != Direction.DOWN && blockState.is(BlockTags.CONVERTABLE_TO_MUD) && potionContents.is(Potions.WATER)) {
-+            // Paper start
-+            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(player, blockPos, Blocks.MUD.defaultBlockState())) {
-+                player.containerMenu.sendAllDataToRemote();
-+                return InteractionResult.PASS;
-+            }
-+            // Paper end
-             level.playSound(null, blockPos, SoundEvents.GENERIC_SPLASH, SoundSource.BLOCKS, 1.0F, 1.0F);
-             player.setItemInHand(context.getHand(), ItemUtils.createFilledResult(itemStack, player, new ItemStack(Items.GLASS_BOTTLE)));
-             player.awardStat(Stats.ITEM_USED.get(itemStack.getItem()));
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/ProjectileWeaponItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/ProjectileWeaponItem.java.patch
deleted file mode 100644
index dfdb8d27c6..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/ProjectileWeaponItem.java.patch
+++ /dev/null
@@ -1,52 +0,0 @@
---- a/net/minecraft/world/item/ProjectileWeaponItem.java
-+++ b/net/minecraft/world/item/ProjectileWeaponItem.java
-@@ -54,9 +54,25 @@
-                 float f6 = f4 + f5 * (float) ((i + 1) / 2) * f3;
- 
-                 f5 = -f5;
--                Projectile.spawnProjectile(this.createProjectile(world, shooter, stack, itemstack1, critical), world, itemstack1, (iprojectile) -> {
--                    this.shootProjectile(shooter, iprojectile, i, speed, divergence, f6, target);
--                });
-+                // CraftBukkit start
-+                Projectile iprojectile = this.createProjectile(world, shooter, stack, itemstack1, critical);
-+                this.shootProjectile(shooter, iprojectile, i, speed, divergence, f6, target);
-+
-+                org.bukkit.event.entity.EntityShootBowEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityShootBowEvent(shooter, stack, itemstack1, iprojectile, hand, speed, true);
-+                if (event.isCancelled()) {
-+                    event.getProjectile().remove();
-+                    return;
-+                }
-+
-+                if (event.getProjectile() == iprojectile.getBukkitEntity()) {
-+                    if (Projectile.spawnProjectile(iprojectile, world, itemstack1).isRemoved()) {
-+                        if (shooter instanceof net.minecraft.server.level.ServerPlayer) {
-+                            ((net.minecraft.server.level.ServerPlayer) shooter).getBukkitEntity().updateInventory();
-+                        }
-+                        return;
-+                    }
-+                }
-+                // CraftBukkit end
-                 stack.hurtAndBreak(this.getDurabilityUse(itemstack1), shooter, LivingEntity.getSlotForHand(hand));
-                 if (stack.isEmpty()) {
-                     break;
-@@ -93,6 +109,11 @@
-     }
- 
-     protected static List<ItemStack> draw(ItemStack stack, ItemStack projectileStack, LivingEntity shooter) {
-+        // Paper start
-+        return draw(stack, projectileStack, shooter, true);
-+    }
-+    protected static List<ItemStack> draw(ItemStack stack, ItemStack projectileStack, LivingEntity shooter, boolean consume) {
-+        // Paper end
-         if (projectileStack.isEmpty()) {
-             return List.of();
-         } else {
-@@ -112,7 +133,7 @@
-             ItemStack itemstack2 = projectileStack.copy();
- 
-             for (int k = 0; k < j; ++k) {
--                ItemStack itemstack3 = ProjectileWeaponItem.useAmmo(stack, k == 0 ? projectileStack : itemstack2, shooter, k > 0);
-+                ItemStack itemstack3 = ProjectileWeaponItem.useAmmo(stack, k == 0 ? projectileStack : itemstack2, shooter, k > 0 || !consume); // Paper
- 
-                 if (!itemstack3.isEmpty()) {
-                     list.add(itemstack3);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/ShovelItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/ShovelItem.java.patch
deleted file mode 100644
index 0840475938..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/ShovelItem.java.patch
+++ /dev/null
@@ -1,33 +0,0 @@
---- a/net/minecraft/world/item/ShovelItem.java
-+++ b/net/minecraft/world/item/ShovelItem.java
-@@ -46,20 +46,29 @@
-             Player player = context.getPlayer();
-             BlockState blockState2 = FLATTENABLES.get(blockState.getBlock());
-             BlockState blockState3 = null;
-+            Runnable afterAction = null; // Paper
-             if (blockState2 != null && level.getBlockState(blockPos.above()).isAir()) {
--                level.playSound(player, blockPos, SoundEvents.SHOVEL_FLATTEN, SoundSource.BLOCKS, 1.0F, 1.0F);
-+                afterAction = () -> level.playSound(player, blockPos, SoundEvents.SHOVEL_FLATTEN, SoundSource.BLOCKS, 1.0F, 1.0F); // Paper
-                 blockState3 = blockState2;
-             } else if (blockState.getBlock() instanceof CampfireBlock && blockState.getValue(CampfireBlock.LIT)) {
-+                afterAction = () -> { // Paper
-                 if (!level.isClientSide()) {
-                     level.levelEvent(null, 1009, blockPos, 0);
-                 }
- 
-                 CampfireBlock.dowse(context.getPlayer(), level, blockPos, blockState);
-+                }; // Paper
-                 blockState3 = blockState.setValue(CampfireBlock.LIT, Boolean.valueOf(false));
-             }
- 
-             if (blockState3 != null) {
-                 if (!level.isClientSide) {
-+                    // Paper start
-+                    if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(context.getPlayer(), blockPos, blockState3)) {
-+                        return InteractionResult.PASS;
-+                    }
-+                    afterAction.run();
-+                    // Paper end
-                     level.setBlock(blockPos, blockState3, 11);
-                     level.gameEvent(GameEvent.BLOCK_CHANGE, blockPos, GameEvent.Context.of(player, blockState3));
-                     if (player != null) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/SignItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/SignItem.java.patch
deleted file mode 100644
index 50c0cdf7d1..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/SignItem.java.patch
+++ /dev/null
@@ -1,23 +0,0 @@
---- a/net/minecraft/world/item/SignItem.java
-+++ b/net/minecraft/world/item/SignItem.java
-@@ -13,6 +13,8 @@
- 
- public class SignItem extends StandingAndWallBlockItem {
- 
-+    public static BlockPos openSign; // CraftBukkit
-+
-     public SignItem(Block standingBlock, Block wallBlock, Item.Properties settings) {
-         super(standingBlock, wallBlock, Direction.DOWN, settings);
-     }
-@@ -35,7 +37,10 @@
-                 if (block instanceof SignBlock) {
-                     SignBlock blocksign = (SignBlock) block;
- 
--                    blocksign.openTextEdit(player, tileentitysign, true);
-+                    // CraftBukkit start - SPIGOT-4678
-+                    // blocksign.openTextEdit(entityhuman, tileentitysign, true);
-+                    SignItem.openSign = pos;
-+                    // CraftBukkit end
-                 }
-             }
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/SnowballItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/SnowballItem.java.patch
deleted file mode 100644
index bb5a1788d6..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/SnowballItem.java.patch
+++ /dev/null
@@ -1,37 +0,0 @@
---- a/net/minecraft/world/item/SnowballItem.java
-+++ b/net/minecraft/world/item/SnowballItem.java
-@@ -25,13 +25,30 @@
-     public InteractionResult use(Level world, Player user, InteractionHand hand) {
-         ItemStack itemstack = user.getItemInHand(hand);
- 
--        world.playSound((Player) null, user.getX(), user.getY(), user.getZ(), SoundEvents.SNOWBALL_THROW, SoundSource.NEUTRAL, 0.5F, 0.4F / (world.getRandom().nextFloat() * 0.4F + 0.8F));
-+        // CraftBukkit start - moved down
-+        // world.playSound((EntityHuman) null, entityhuman.getX(), entityhuman.getY(), entityhuman.getZ(), SoundEffects.SNOWBALL_THROW, SoundCategory.NEUTRAL, 0.5F, 0.4F / (world.getRandom().nextFloat() * 0.4F + 0.8F));
-         if (world instanceof ServerLevel worldserver) {
--            Projectile.spawnProjectileFromRotation(Snowball::new, worldserver, itemstack, user, 0.0F, SnowballItem.PROJECTILE_SHOOT_POWER, 1.0F);
-+            // Paper start - PlayerLaunchProjectileEvent
-+            final Projectile.Delayed<Snowball> snowball = Projectile.spawnProjectileFromRotationDelayed(Snowball::new, worldserver, itemstack, user, 0.0F, SnowballItem.PROJECTILE_SHOOT_POWER, 1.0F);
-+            com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) user.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack), (org.bukkit.entity.Projectile) snowball.projectile().getBukkitEntity());
-+            if (event.callEvent() && snowball.attemptSpawn()) {
-+                user.awardStat(Stats.ITEM_USED.get(this));
-+                if (event.shouldConsume()) {
-+                    itemstack.consume(1, user);
-+                } else if (user instanceof net.minecraft.server.level.ServerPlayer) {
-+                    ((net.minecraft.server.level.ServerPlayer) user).getBukkitEntity().updateInventory();
-+                }
-+            // Paper end - PlayerLaunchProjectileEvent
-+
-+                world.playSound((Player) null, user.getX(), user.getY(), user.getZ(), SoundEvents.SNOWBALL_THROW, SoundSource.NEUTRAL, 0.5F, 0.4F / (world.getRandom().nextFloat() * 0.4F + 0.8F));
-+            } else { if (user instanceof net.minecraft.server.level.ServerPlayer) { // Paper - PlayerLaunchProjectileEvent - return fail
-+                ((net.minecraft.server.level.ServerPlayer) user).getBukkitEntity().updateInventory();
-+            } return InteractionResult.FAIL; } // Paper - PlayerLaunchProjectileEvent - return fail
-+            // CraftBukkit end
-         }
- 
--        user.awardStat(Stats.ITEM_USED.get(this));
--        itemstack.consume(1, user);
-+        // Paper - PlayerLaunchProjectileEvent - moved up
-+        // itemstack.consume(1, entityhuman); // CraftBukkit - moved up
-         return InteractionResult.SUCCESS;
-     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/SpawnEggItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/SpawnEggItem.java.patch
deleted file mode 100644
index a33a635c7a..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/SpawnEggItem.java.patch
+++ /dev/null
@@ -1,24 +0,0 @@
---- a/net/minecraft/world/item/SpawnEggItem.java
-+++ b/net/minecraft/world/item/SpawnEggItem.java
-@@ -63,6 +63,8 @@
-             EntityType entitytypes;
- 
-             if (tileentity instanceof Spawner) {
-+                if (world.paperConfig().entities.spawning.disableMobSpawnerSpawnEggTransformation) return InteractionResult.FAIL; // Paper - Allow disabling mob spawner spawn egg transformation
-+
-                 Spawner spawner = (Spawner) tileentity;
- 
-                 entitytypes = this.getType(world.registryAccess(), itemstack);
-@@ -176,10 +178,10 @@
-                     return Optional.empty();
-                 } else {
-                     ((Mob) object).moveTo(pos.x(), pos.y(), pos.z(), 0.0F, 0.0F);
--                    world.addFreshEntityWithPassengers((Entity) object);
-+                    world.addFreshEntityWithPassengers((Entity) object, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.SPAWNER_EGG); // CraftBukkit
-                     ((Mob) object).setCustomName((Component) stack.get(DataComponents.CUSTOM_NAME));
-                     stack.consume(1, user);
--                    return Optional.of(object);
-+                    return Optional.of((Mob) object); // CraftBukkit - decompile error
-                 }
-             }
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/StandingAndWallBlockItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/StandingAndWallBlockItem.java.patch
deleted file mode 100644
index 9e1a576a36..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/StandingAndWallBlockItem.java.patch
+++ /dev/null
@@ -1,41 +0,0 @@
---- a/net/minecraft/world/item/StandingAndWallBlockItem.java
-+++ b/net/minecraft/world/item/StandingAndWallBlockItem.java
-@@ -4,12 +4,17 @@
- import javax.annotation.Nullable;
- import net.minecraft.core.BlockPos;
- import net.minecraft.core.Direction;
-+import net.minecraft.server.level.ServerPlayer;
- import net.minecraft.world.item.context.BlockPlaceContext;
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.LevelReader;
- import net.minecraft.world.level.block.Block;
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.phys.shapes.CollisionContext;
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.craftbukkit.block.data.CraftBlockData;
-+import org.bukkit.event.block.BlockCanBuildEvent;
-+// CraftBukkit end
- 
- public class StandingAndWallBlockItem extends BlockItem {
- 
-@@ -49,7 +54,19 @@
-             }
-         }
- 
--        return iblockdata1 != null && world.isUnobstructed(iblockdata1, blockposition, CollisionContext.empty()) ? iblockdata1 : null;
-+        // CraftBukkit start
-+        if (iblockdata1 != null) {
-+            boolean defaultReturn = world.isUnobstructed(iblockdata1, blockposition, CollisionContext.empty());
-+            org.bukkit.entity.Player player = (context.getPlayer() instanceof ServerPlayer) ? (org.bukkit.entity.Player) context.getPlayer().getBukkitEntity() : null;
-+
-+            BlockCanBuildEvent event = new BlockCanBuildEvent(CraftBlock.at(world, blockposition), player, CraftBlockData.fromData(iblockdata1), defaultReturn, org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(context.getHand())); // Paper - Expose hand in BlockCanBuildEvent
-+            context.getLevel().getCraftServer().getPluginManager().callEvent(event);
-+
-+            return (event.isBuildable()) ? iblockdata1 : null;
-+        } else {
-+            return null;
-+        }
-+        // CraftBukkit end
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/ThrowablePotionItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/ThrowablePotionItem.java.patch
deleted file mode 100644
index 13c480eabd..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/ThrowablePotionItem.java.patch
+++ /dev/null
@@ -1,34 +0,0 @@
---- a/net/minecraft/world/item/ThrowablePotionItem.java
-+++ b/net/minecraft/world/item/ThrowablePotionItem.java
-@@ -22,11 +22,28 @@
-     public InteractionResult use(Level world, Player user, InteractionHand hand) {
-         ItemStack itemStack = user.getItemInHand(hand);
-         if (world instanceof ServerLevel serverLevel) {
--            Projectile.spawnProjectileFromRotation(ThrownPotion::new, serverLevel, itemStack, user, -20.0F, PROJECTILE_SHOOT_POWER, 1.0F);
-+            // Paper start - PlayerLaunchProjectileEvent
-+            final Projectile.Delayed<ThrownPotion> thrownPotion = Projectile.spawnProjectileFromRotationDelayed(ThrownPotion::new, serverLevel, itemStack, user, -20.0F, PROJECTILE_SHOOT_POWER, 1.0F);
-+            // Paper start - PlayerLaunchProjectileEvent
-+            com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) user.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack), (org.bukkit.entity.Projectile) thrownPotion.projectile().getBukkitEntity());
-+            if (event.callEvent() && thrownPotion.attemptSpawn()) {
-+                if (event.shouldConsume()) {
-+                    itemStack.consume(1, user);
-+                } else if (user instanceof net.minecraft.server.level.ServerPlayer) {
-+                    ((net.minecraft.server.level.ServerPlayer) user).getBukkitEntity().updateInventory();
-+                }
-+
-+                user.awardStat(Stats.ITEM_USED.get(this));
-+            } else {
-+                if (user instanceof net.minecraft.server.level.ServerPlayer) {
-+                    ((net.minecraft.server.level.ServerPlayer) user).getBukkitEntity().updateInventory();
-+                }
-+                return InteractionResult.FAIL;
-+            }
-+            // Paper end - PlayerLaunchProjectileEvent
-         }
- 
--        user.awardStat(Stats.ITEM_USED.get(this));
--        itemStack.consume(1, user);
-+        // Paper - PlayerLaunchProjectileEvent - move up
-         return InteractionResult.SUCCESS;
-     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/TridentItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/TridentItem.java.patch
deleted file mode 100644
index a23995cfe3..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/TridentItem.java.patch
+++ /dev/null
@@ -1,51 +0,0 @@
---- a/net/minecraft/world/item/TridentItem.java
-+++ b/net/minecraft/world/item/TridentItem.java
-@@ -86,18 +86,37 @@
-                     if (world instanceof ServerLevel) {
-                         ServerLevel worldserver = (ServerLevel) world;
- 
--                        stack.hurtWithoutBreaking(1, entityhuman);
-+                        // itemstack.hurtWithoutBreaking(1, entityhuman); // CraftBukkit - moved down
-                         if (f == 0.0F) {
--                            ThrownTrident entitythrowntrident = (ThrownTrident) Projectile.spawnProjectileFromRotation(ThrownTrident::new, worldserver, stack, entityhuman, 0.0F, 2.5F, 1.0F);
-+                            // Paper start - PlayerLaunchProjectileEvent
-+                            Projectile.Delayed<ThrownTrident> tridentDelayed = Projectile.spawnProjectileFromRotationDelayed(ThrownTrident::new, worldserver, stack, entityhuman, 0.0F, 2.5F, 1.0F);
-+                            // Paper start - PlayerLaunchProjectileEvent
-+                            com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) entityhuman.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(stack), (org.bukkit.entity.Projectile) tridentDelayed.projectile().getBukkitEntity());
-+                            if (!event.callEvent() || !tridentDelayed.attemptSpawn()) {
-+                                // CraftBukkit start
-+                            // Paper end - PlayerLaunchProjectileEvent
-+                                if (entityhuman instanceof net.minecraft.server.level.ServerPlayer) {
-+                                    ((net.minecraft.server.level.ServerPlayer) entityhuman).getBukkitEntity().updateInventory();
-+                                }
-+                                return false;
-+                            }
-+                            ThrownTrident entitythrowntrident = tridentDelayed.projectile(); // Paper - PlayerLaunchProjectileEvent
-+                            if (event.shouldConsume()) stack.hurtWithoutBreaking(1, entityhuman); // Paper - PlayerLaunchProjectileEvent
-+                            entitythrowntrident.pickupItemStack = stack.copy(); // SPIGOT-4511 update since damage call moved
-+                            // CraftBukkit end
- 
-                             if (entityhuman.hasInfiniteMaterials()) {
-                                 entitythrowntrident.pickup = AbstractArrow.Pickup.CREATIVE_ONLY;
--                            } else {
-+                            } else if (event.shouldConsume()) { // Paper - PlayerLaunchProjectileEvent
-                                 entityhuman.getInventory().removeItem(stack);
-                             }
- 
-                             world.playSound((Player) null, (Entity) entitythrowntrident, (SoundEvent) holder.value(), SoundSource.PLAYERS, 1.0F, 1.0F);
-                             return true;
-+                            // CraftBukkit start - SPIGOT-5458 also need in this branch :(
-+                        } else {
-+                            stack.hurtWithoutBreaking(1, entityhuman);
-+                            // CraftBukkkit end
-                         }
-                     }
- 
-@@ -112,6 +131,7 @@
-                         f3 *= f / f6;
-                         f4 *= f / f6;
-                         f5 *= f / f6;
-+                        org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerRiptideEvent(entityhuman, stack, f3, f4, f5); // CraftBukkit
-                         entityhuman.push((double) f3, (double) f4, (double) f5);
-                         entityhuman.startAutoSpinAttack(20, 8.0F, stack);
-                         if (entityhuman.onGround()) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/WindChargeItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/WindChargeItem.java.patch
deleted file mode 100644
index 8fd7be1352..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/WindChargeItem.java.patch
+++ /dev/null
@@ -1,42 +0,0 @@
---- a/net/minecraft/world/item/WindChargeItem.java
-+++ b/net/minecraft/world/item/WindChargeItem.java
-@@ -27,7 +27,7 @@
-     public InteractionResult use(Level world, Player user, InteractionHand hand) {
-         ItemStack itemStack = user.getItemInHand(hand);
-         if (world instanceof ServerLevel serverLevel) {
--            Projectile.spawnProjectileFromRotation(
-+            final Projectile.Delayed<WindCharge> windCharge = Projectile.spawnProjectileFromRotationDelayed( // Paper - PlayerLaunchProjectileEvent
-                 (world2, shooter, stack) -> new WindCharge(user, world, user.position().x(), user.getEyePosition().y(), user.position().z()),
-                 serverLevel,
-                 itemStack,
-@@ -36,6 +36,21 @@
-                 PROJECTILE_SHOOT_POWER,
-                 1.0F
-             );
-+            com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent event = new com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent((org.bukkit.entity.Player) user.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack), (org.bukkit.entity.Projectile) windCharge.projectile().getBukkitEntity());
-+            if (!event.callEvent() || !windCharge.attemptSpawn()) {
-+                user.containerMenu.sendAllDataToRemote();
-+                if (user instanceof net.minecraft.server.level.ServerPlayer player) {
-+                    player.connection.send(new net.minecraft.network.protocol.game.ClientboundCooldownPacket(user.getCooldowns().getCooldownGroup(itemStack), 0)); // prevent visual desync of cooldown on the slot
-+                }
-+                return InteractionResult.FAIL;
-+            }
-+
-+            user.awardStat(Stats.ITEM_USED.get(this));
-+            if (event.shouldConsume()) itemStack.consume(1, user);
-+            else if (!user.hasInfiniteMaterials()) {
-+                user.containerMenu.sendAllDataToRemote();
-+            }
-+            // Paper end - PlayerLaunchProjectileEvent
-         }
- 
-         world.playSound(
-@@ -48,8 +63,6 @@
-             0.5F,
-             0.4F / (world.getRandom().nextFloat() * 0.4F + 0.8F)
-         );
--        user.awardStat(Stats.ITEM_USED.get(this));
--        itemStack.consume(1, user);
-         return InteractionResult.SUCCESS;
-     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/WrittenBookItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/WrittenBookItem.java.patch
deleted file mode 100644
index f29d9ca860..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/WrittenBookItem.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/item/WrittenBookItem.java
-+++ b/net/minecraft/world/item/WrittenBookItem.java
-@@ -41,7 +41,7 @@
- 
-     public static boolean resolveBookComponents(ItemStack book, CommandSourceStack commandSource, @Nullable Player player) {
-         WrittenBookContent writtenBookContent = book.get(DataComponents.WRITTEN_BOOK_CONTENT);
--        if (writtenBookContent != null && !writtenBookContent.resolved()) {
-+        if (io.papermc.paper.configuration.GlobalConfiguration.get().itemValidation.resolveSelectorsInBooks && writtenBookContent != null && !writtenBookContent.resolved()) { // Paper - Disable component selector resolving in books by default
-             WrittenBookContent writtenBookContent2 = writtenBookContent.resolve(commandSource, player);
-             if (writtenBookContent2 != null) {
-                 book.set(DataComponents.WRITTEN_BOOK_CONTENT, writtenBookContent2);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/alchemy/PotionContents.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/alchemy/PotionContents.java.patch
deleted file mode 100644
index 163fbb5056..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/alchemy/PotionContents.java.patch
+++ /dev/null
@@ -1,20 +0,0 @@
---- a/net/minecraft/world/item/alchemy/PotionContents.java
-+++ b/net/minecraft/world/item/alchemy/PotionContents.java
-@@ -93,7 +93,7 @@
-     }
- 
-     public PotionContents withEffectAdded(MobEffectInstance customEffect) {
--        return new PotionContents(this.potion, this.customColor, Util.copyAndAdd(this.customEffects, (Object) customEffect), this.customName);
-+        return new PotionContents(this.potion, this.customColor, Util.copyAndAdd(this.customEffects, customEffect), this.customName); // CraftBukkit - decompile error
-     }
- 
-     public int getColor() {
-@@ -172,7 +172,7 @@
-                 if (((MobEffect) mobeffect.getEffect().value()).isInstantenous()) {
-                     ((MobEffect) mobeffect.getEffect().value()).applyInstantenousEffect(worldserver, entityhuman2, entityhuman2, user, mobeffect.getAmplifier(), 1.0D);
-                 } else {
--                    user.addEffect(mobeffect);
-+                    user.addEffect(mobeffect, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.POTION_DRINK); // CraftBukkit
-                 }
- 
-             });
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/component/Consumable.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/component/Consumable.java.patch
deleted file mode 100644
index a163ec68c0..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/component/Consumable.java.patch
+++ /dev/null
@@ -1,53 +0,0 @@
---- a/net/minecraft/world/item/component/Consumable.java
-+++ b/net/minecraft/world/item/component/Consumable.java
-@@ -29,6 +29,11 @@
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.gameevent.GameEvent;
- 
-+// CraftBukkit start
-+import net.minecraft.world.item.Items;
-+import org.bukkit.event.entity.EntityPotionEffectEvent;
-+// CraftBukkit end
-+
- public record Consumable(float consumeSeconds, ItemUseAnimation animation, Holder<SoundEvent> sound, boolean hasConsumeParticles, List<ConsumeEffect> onConsumeEffects) {
- 
-     public static final float DEFAULT_CONSUME_SECONDS = 1.6F;
-@@ -69,8 +74,19 @@
-             consumablelistener.onConsume(world, user, stack, this);
-         });
-         if (!world.isClientSide) {
-+            // CraftBukkit start
-+            EntityPotionEffectEvent.Cause cause;
-+            if (stack.is(Items.MILK_BUCKET)) {
-+                cause = EntityPotionEffectEvent.Cause.MILK;
-+            } else if (stack.is(Items.POTION)) {
-+                cause = EntityPotionEffectEvent.Cause.POTION_DRINK;
-+            } else {
-+                cause = EntityPotionEffectEvent.Cause.FOOD;
-+            }
-+
-             this.onConsumeEffects.forEach((consumeeffect) -> {
--                consumeeffect.apply(world, stack, user);
-+                consumeeffect.apply(world, stack, user, cause);
-+                // CraftBukkit end
-             });
-         }
- 
-@@ -79,6 +95,17 @@
-         return stack;
-     }
- 
-+    // CraftBukkit start
-+    public void cancelUsingItem(net.minecraft.server.level.ServerPlayer entityplayer, ItemStack itemstack) {
-+        final java.util.List<net.minecraft.network.protocol.Packet<? super net.minecraft.network.protocol.game.ClientGamePacketListener>> packets = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>(); // Paper - properly resend entities - collect packets for bundle
-+        itemstack.getAllOfType(ConsumableListener.class).forEach((consumablelistener) -> {
-+            consumablelistener.cancelUsingItem(entityplayer, itemstack, packets); // Paper - properly resend entities - collect packets for bundle
-+        });
-+        entityplayer.server.getPlayerList().sendActiveEffects(entityplayer, packets::add); // Paper - properly resend entities - collect packets for bundle
-+        entityplayer.connection.send(new net.minecraft.network.protocol.game.ClientboundBundlePacket(packets));
-+    }
-+    // CraftBukkit end
-+
-     public boolean canConsume(LivingEntity user, ItemStack stack) {
-         FoodProperties foodinfo = (FoodProperties) stack.get(DataComponents.FOOD);
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/component/ConsumableListener.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/component/ConsumableListener.java.patch
deleted file mode 100644
index 2e31354cba..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/component/ConsumableListener.java.patch
+++ /dev/null
@@ -1,9 +0,0 @@
---- a/net/minecraft/world/item/component/ConsumableListener.java
-+++ b/net/minecraft/world/item/component/ConsumableListener.java
-@@ -7,4 +7,6 @@
- public interface ConsumableListener {
- 
-     void onConsume(Level world, LivingEntity user, ItemStack stack, Consumable consumable);
-+
-+    default void cancelUsingItem(net.minecraft.server.level.ServerPlayer entityplayer, ItemStack itemstack, java.util.List<net.minecraft.network.protocol.Packet<? super net.minecraft.network.protocol.game.ClientGamePacketListener>> collectedPackets) {} // CraftBukkit // Paper - properly resend entities - collect packets for bundle
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/component/DeathProtection.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/component/DeathProtection.java.patch
deleted file mode 100644
index 8516839b42..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/component/DeathProtection.java.patch
+++ /dev/null
@@ -1,22 +0,0 @@
---- a/net/minecraft/world/item/component/DeathProtection.java
-+++ b/net/minecraft/world/item/component/DeathProtection.java
-@@ -15,6 +15,10 @@
- import net.minecraft.world.item.consume_effects.ClearAllStatusEffectsConsumeEffect;
- import net.minecraft.world.item.consume_effects.ConsumeEffect;
- 
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityPotionEffectEvent;
-+// CraftBukkit end
-+
- public record DeathProtection(List<ConsumeEffect> deathEffects) {
- 
-     public static final Codec<DeathProtection> CODEC = RecordCodecBuilder.create((instance) -> {
-@@ -29,7 +33,7 @@
-         while (iterator.hasNext()) {
-             ConsumeEffect consumeeffect = (ConsumeEffect) iterator.next();
- 
--            consumeeffect.apply(entity.level(), stack, entity);
-+            consumeeffect.apply(entity.level(), stack, entity, EntityPotionEffectEvent.Cause.TOTEM); // CraftBukkit
-         }
- 
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/component/OminousBottleAmplifier.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/component/OminousBottleAmplifier.java.patch
deleted file mode 100644
index 57338edcbf..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/component/OminousBottleAmplifier.java.patch
+++ /dev/null
@@ -1,18 +0,0 @@
---- a/net/minecraft/world/item/component/OminousBottleAmplifier.java
-+++ b/net/minecraft/world/item/component/OminousBottleAmplifier.java
-@@ -28,8 +28,14 @@
- 
-     @Override
-     public void onConsume(Level world, LivingEntity user, ItemStack stack, Consumable consumable) {
--        user.addEffect(new MobEffectInstance(MobEffects.BAD_OMEN, 120000, this.value, false, false, true));
-+        user.addEffect(new MobEffectInstance(MobEffects.BAD_OMEN, 120000, this.value, false, false, true)); // Paper - properly resend entities - diff on change for below
-     }
-+    // Paper start - properly resend entities - collect packets for bundle
-+    @Override
-+    public void cancelUsingItem(net.minecraft.server.level.ServerPlayer entityplayer, ItemStack itemstack, java.util.List<net.minecraft.network.protocol.Packet<? super net.minecraft.network.protocol.game.ClientGamePacketListener>> collectedPackets) {
-+        collectedPackets.add(new net.minecraft.network.protocol.game.ClientboundRemoveMobEffectPacket(entityplayer.getId(), MobEffects.BAD_OMEN));
-+    }
-+    // Paper end - properly resend entities - collect packets for bundle
- 
-     @Override
-     public void addToTooltip(Item.TooltipContext context, Consumer<Component> tooltip, TooltipFlag type) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/component/SuspiciousStewEffects.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/component/SuspiciousStewEffects.java.patch
deleted file mode 100644
index 2836856f9c..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/component/SuspiciousStewEffects.java.patch
+++ /dev/null
@@ -1,28 +0,0 @@
---- a/net/minecraft/world/item/component/SuspiciousStewEffects.java
-+++ b/net/minecraft/world/item/component/SuspiciousStewEffects.java
-@@ -29,7 +29,7 @@
-     public static final StreamCodec<RegistryFriendlyByteBuf, SuspiciousStewEffects> STREAM_CODEC = SuspiciousStewEffects.Entry.STREAM_CODEC.apply(ByteBufCodecs.list()).map(SuspiciousStewEffects::new, SuspiciousStewEffects::effects);
- 
-     public SuspiciousStewEffects withEffectAdded(SuspiciousStewEffects.Entry stewEffect) {
--        return new SuspiciousStewEffects(Util.copyAndAdd(this.effects, (Object) stewEffect));
-+        return new SuspiciousStewEffects(Util.copyAndAdd(this.effects, stewEffect)); // CraftBukkit - decompile error
-     }
- 
-     @Override
-@@ -44,7 +44,16 @@
- 
-     }
- 
-+    // CraftBukkit start
-     @Override
-+    public void cancelUsingItem(net.minecraft.server.level.ServerPlayer entityplayer, ItemStack itemstack, java.util.List<net.minecraft.network.protocol.Packet<? super net.minecraft.network.protocol.game.ClientGamePacketListener>> collectedPackets) { // Paper - properly resend entities - collect packets for bundle
-+        for (SuspiciousStewEffects.Entry suspicioussteweffects_a : this.effects) {
-+            collectedPackets.add(new net.minecraft.network.protocol.game.ClientboundRemoveMobEffectPacket(entityplayer.getId(), suspicioussteweffects_a.effect())); // Paper - bundlize packets
-+        }
-+    }
-+    // CraftBukkit end
-+
-+    @Override
-     public void addToTooltip(Item.TooltipContext context, Consumer<Component> tooltip, TooltipFlag type) {
-         if (type.isCreative()) {
-             List<MobEffectInstance> list = new ArrayList();
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/consume_effects/ApplyStatusEffectsConsumeEffect.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/consume_effects/ApplyStatusEffectsConsumeEffect.java.patch
deleted file mode 100644
index 2551be5478..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/consume_effects/ApplyStatusEffectsConsumeEffect.java.patch
+++ /dev/null
@@ -1,32 +0,0 @@
---- a/net/minecraft/world/item/consume_effects/ApplyStatusEffectsConsumeEffect.java
-+++ b/net/minecraft/world/item/consume_effects/ApplyStatusEffectsConsumeEffect.java
-@@ -12,6 +12,9 @@
- import net.minecraft.world.entity.LivingEntity;
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.level.Level;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityPotionEffectEvent;
-+// CraftBukkit end
- 
- public record ApplyStatusEffectsConsumeEffect(List<MobEffectInstance> effects, float probability) implements ConsumeEffect {
- 
-@@ -38,8 +41,8 @@
-     }
- 
-     @Override
--    public boolean apply(Level world, ItemStack stack, LivingEntity user) {
--        if (user.getRandom().nextFloat() >= this.probability) {
-+    public boolean apply(Level world, ItemStack itemstack, LivingEntity entityliving, EntityPotionEffectEvent.Cause cause) { // CraftBukkit
-+        if (entityliving.getRandom().nextFloat() >= this.probability) {
-             return false;
-         } else {
-             boolean flag = false;
-@@ -48,7 +51,7 @@
-             while (iterator.hasNext()) {
-                 MobEffectInstance mobeffect = (MobEffectInstance) iterator.next();
- 
--                if (user.addEffect(new MobEffectInstance(mobeffect))) {
-+                if (entityliving.addEffect(new MobEffectInstance(mobeffect), cause)) { // CraftBukkit
-                     flag = true;
-                 }
-             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/consume_effects/ClearAllStatusEffectsConsumeEffect.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/consume_effects/ClearAllStatusEffectsConsumeEffect.java.patch
deleted file mode 100644
index 35168c5339..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/consume_effects/ClearAllStatusEffectsConsumeEffect.java.patch
+++ /dev/null
@@ -1,24 +0,0 @@
---- a/net/minecraft/world/item/consume_effects/ClearAllStatusEffectsConsumeEffect.java
-+++ b/net/minecraft/world/item/consume_effects/ClearAllStatusEffectsConsumeEffect.java
-@@ -6,6 +6,9 @@
- import net.minecraft.world.entity.LivingEntity;
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.level.Level;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityPotionEffectEvent;
-+// CraftBukkit end
- 
- public record ClearAllStatusEffectsConsumeEffect() implements ConsumeEffect {
- 
-@@ -19,7 +22,9 @@
-     }
- 
-     @Override
--    public boolean apply(Level world, ItemStack stack, LivingEntity user) {
--        return user.removeAllEffects();
-+    // CraftBukkit start
-+    public boolean apply(Level world, ItemStack itemstack, LivingEntity entityliving, EntityPotionEffectEvent.Cause cause) {
-+        return entityliving.removeAllEffects(cause);
-+        // CraftBukkit end
-     }
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/consume_effects/ConsumeEffect.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/consume_effects/ConsumeEffect.java.patch
deleted file mode 100644
index c5d56842c7..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/consume_effects/ConsumeEffect.java.patch
+++ /dev/null
@@ -1,30 +0,0 @@
---- a/net/minecraft/world/item/consume_effects/ConsumeEffect.java
-+++ b/net/minecraft/world/item/consume_effects/ConsumeEffect.java
-@@ -11,6 +11,9 @@
- import net.minecraft.world.entity.LivingEntity;
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.level.Level;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityPotionEffectEvent;
-+// CraftBukkit end
- 
- public interface ConsumeEffect {
- 
-@@ -19,8 +22,16 @@
- 
-     ConsumeEffect.Type<? extends ConsumeEffect> getType();
- 
--    boolean apply(Level world, ItemStack stack, LivingEntity user);
-+    // CraftBukkit start
-+    default boolean apply(Level world, ItemStack stack, LivingEntity user) {
-+        return this.apply(world, stack, user, EntityPotionEffectEvent.Cause.UNKNOWN);
-+    }
- 
-+    default boolean apply(Level world, ItemStack itemstack, LivingEntity entityliving, EntityPotionEffectEvent.Cause cause) {
-+        return this.apply(world, itemstack, entityliving);
-+    }
-+    // CraftBukkit end
-+
-     public static record Type<T extends ConsumeEffect>(MapCodec<T> codec, StreamCodec<RegistryFriendlyByteBuf, T> streamCodec) {
- 
-         public static final ConsumeEffect.Type<ApplyStatusEffectsConsumeEffect> APPLY_EFFECTS = register("apply_effects", ApplyStatusEffectsConsumeEffect.CODEC, ApplyStatusEffectsConsumeEffect.STREAM_CODEC);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/consume_effects/RemoveStatusEffectsConsumeEffect.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/consume_effects/RemoveStatusEffectsConsumeEffect.java.patch
deleted file mode 100644
index c5eacaef69..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/consume_effects/RemoveStatusEffectsConsumeEffect.java.patch
+++ /dev/null
@@ -1,29 +0,0 @@
---- a/net/minecraft/world/item/consume_effects/RemoveStatusEffectsConsumeEffect.java
-+++ b/net/minecraft/world/item/consume_effects/RemoveStatusEffectsConsumeEffect.java
-@@ -14,6 +14,9 @@
- import net.minecraft.world.entity.LivingEntity;
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.level.Level;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityPotionEffectEvent;
-+// CraftBukkit end
- 
- public record RemoveStatusEffectsConsumeEffect(HolderSet<MobEffect> effects) implements ConsumeEffect {
- 
-@@ -32,14 +35,14 @@
-     }
- 
-     @Override
--    public boolean apply(Level world, ItemStack stack, LivingEntity user) {
-+    public boolean apply(Level world, ItemStack itemstack, LivingEntity entityliving, EntityPotionEffectEvent.Cause cause) { // CraftBukkit
-         boolean flag = false;
-         Iterator iterator = this.effects.iterator();
- 
-         while (iterator.hasNext()) {
-             Holder<MobEffect> holder = (Holder) iterator.next();
- 
--            if (user.removeEffect(holder)) {
-+            if (entityliving.removeEffect(holder, cause)) { // CraftBukkit
-                 flag = true;
-             }
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/consume_effects/TeleportRandomlyConsumeEffect.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/consume_effects/TeleportRandomlyConsumeEffect.java.patch
deleted file mode 100644
index b419363bd1..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/consume_effects/TeleportRandomlyConsumeEffect.java.patch
+++ /dev/null
@@ -1,20 +0,0 @@
---- a/net/minecraft/world/item/consume_effects/TeleportRandomlyConsumeEffect.java
-+++ b/net/minecraft/world/item/consume_effects/TeleportRandomlyConsumeEffect.java
-@@ -53,7 +53,16 @@
- 
-             Vec3 vec3d = user.position();
- 
--            if (user.randomTeleport(d0, d1, d2, true)) {
-+            // CraftBukkit start - handle canceled status of teleport event
-+            java.util.Optional<Boolean> status = user.randomTeleport(d0, d1, d2, true, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.CHORUS_FRUIT);
-+
-+            if (!status.isPresent()) {
-+                // teleport event was canceled, no more tries
-+                break;
-+            }
-+
-+            if (status.get()) {
-+                // CraftBukkit end
-                 world.gameEvent((Holder) GameEvent.TELEPORT, vec3d, GameEvent.Context.of((Entity) user));
-                 SoundEvent soundeffect;
-                 SoundSource soundcategory;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/BlastingRecipe.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/BlastingRecipe.java.patch
deleted file mode 100644
index 26a37bbc25..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/BlastingRecipe.java.patch
+++ /dev/null
@@ -1,35 +0,0 @@
---- a/net/minecraft/world/item/crafting/BlastingRecipe.java
-+++ b/net/minecraft/world/item/crafting/BlastingRecipe.java
-@@ -4,6 +4,14 @@
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.item.Items;
- 
-+// CraftBukkit start
-+import org.bukkit.NamespacedKey;
-+import org.bukkit.craftbukkit.inventory.CraftBlastingRecipe;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.craftbukkit.inventory.CraftRecipe;
-+import org.bukkit.inventory.Recipe;
-+// CraftBukkit end
-+
- public class BlastingRecipe extends AbstractCookingRecipe {
- 
-     public BlastingRecipe(String group, CookingBookCategory category, Ingredient ingredient, ItemStack result, float experience, int cookingTime) {
-@@ -43,4 +51,17 @@
- 
-         return recipebookcategory;
-     }
-+
-+    // CraftBukkit start
-+    @Override
-+    public Recipe toBukkitRecipe(NamespacedKey id) {
-+        CraftItemStack result = CraftItemStack.asCraftMirror(this.result());
-+
-+        CraftBlastingRecipe recipe = new CraftBlastingRecipe(id, result, CraftRecipe.toBukkit(this.input()), this.experience(), this.cookingTime());
-+        recipe.setGroup(this.group());
-+        recipe.setCategory(CraftRecipe.getCategory(this.category()));
-+
-+        return recipe;
-+    }
-+    // CraftBukkit end
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/CampfireCookingRecipe.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/CampfireCookingRecipe.java.patch
deleted file mode 100644
index 039e214990..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/CampfireCookingRecipe.java.patch
+++ /dev/null
@@ -1,35 +0,0 @@
---- a/net/minecraft/world/item/crafting/CampfireCookingRecipe.java
-+++ b/net/minecraft/world/item/crafting/CampfireCookingRecipe.java
-@@ -4,6 +4,14 @@
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.item.Items;
- 
-+// CraftBukkit start
-+import org.bukkit.NamespacedKey;
-+import org.bukkit.craftbukkit.inventory.CraftCampfireRecipe;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.craftbukkit.inventory.CraftRecipe;
-+import org.bukkit.inventory.Recipe;
-+// CraftBukkit end
-+
- public class CampfireCookingRecipe extends AbstractCookingRecipe {
- 
-     public CampfireCookingRecipe(String group, CookingBookCategory category, Ingredient ingredient, ItemStack result, float experience, int cookingTime) {
-@@ -29,4 +37,17 @@
-     public RecipeBookCategory recipeBookCategory() {
-         return RecipeBookCategories.CAMPFIRE;
-     }
-+
-+    // CraftBukkit start
-+    @Override
-+    public Recipe toBukkitRecipe(NamespacedKey id) {
-+        CraftItemStack result = CraftItemStack.asCraftMirror(this.result());
-+
-+        CraftCampfireRecipe recipe = new CraftCampfireRecipe(id, result, CraftRecipe.toBukkit(this.input()), this.experience(), this.cookingTime());
-+        recipe.setGroup(this.group());
-+        recipe.setCategory(CraftRecipe.getCategory(this.category()));
-+
-+        return recipe;
-+    }
-+    // CraftBukkit end
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/CustomRecipe.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/CustomRecipe.java.patch
deleted file mode 100644
index 773c84fb8b..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/CustomRecipe.java.patch
+++ /dev/null
@@ -1,54 +0,0 @@
---- a/net/minecraft/world/item/crafting/CustomRecipe.java
-+++ b/net/minecraft/world/item/crafting/CustomRecipe.java
-@@ -8,6 +8,15 @@
- import net.minecraft.network.RegistryFriendlyByteBuf;
- import net.minecraft.network.codec.StreamCodec;
- 
-+// CraftBukkit start
-+import net.minecraft.world.item.ItemStack;
-+import org.bukkit.NamespacedKey;
-+import org.bukkit.craftbukkit.inventory.CraftComplexRecipe;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.craftbukkit.inventory.CraftRecipe;
-+import org.bukkit.inventory.Recipe;
-+// CraftBukkit end
-+
- public abstract class CustomRecipe implements CraftingRecipe {
- 
-     private final CraftingBookCategory category;
-@@ -34,6 +43,19 @@
-     @Override
-     public abstract RecipeSerializer<? extends CustomRecipe> getSerializer();
- 
-+    // CraftBukkit start
-+    @Override
-+    public Recipe toBukkitRecipe(NamespacedKey id) {
-+        CraftItemStack result = CraftItemStack.asCraftMirror(ItemStack.EMPTY);
-+
-+        CraftComplexRecipe recipe = new CraftComplexRecipe(id, result, this);
-+        recipe.setGroup(this.group());
-+        recipe.setCategory(CraftRecipe.getCategory(this.category()));
-+
-+        return recipe;
-+    }
-+    // CraftBukkit end
-+
-     public static class Serializer<T extends CraftingRecipe> implements RecipeSerializer<T> {
- 
-         private final MapCodec<T> codec;
-@@ -41,13 +63,13 @@
- 
-         public Serializer(CustomRecipe.Serializer.Factory<T> factory) {
-             this.codec = RecordCodecBuilder.mapCodec((instance) -> {
--                P1 p1 = instance.group(CraftingBookCategory.CODEC.fieldOf("category").orElse(CraftingBookCategory.MISC).forGetter(CraftingRecipe::category));
-+                P1<RecordCodecBuilder.Mu<T>, CraftingBookCategory> p1 = instance.group(CraftingBookCategory.CODEC.fieldOf("category").orElse(CraftingBookCategory.MISC).forGetter(CraftingRecipe::category)); // CraftBukkit - decompile error
- 
-                 Objects.requireNonNull(factory);
-                 return p1.apply(instance, factory::create);
-             });
-             StreamCodec streamcodec = CraftingBookCategory.STREAM_CODEC;
--            Function function = CraftingRecipe::category;
-+            Function<CraftingRecipe, CraftingBookCategory> function = CraftingRecipe::category; // CraftBukkit - decompile error
- 
-             Objects.requireNonNull(factory);
-             this.streamCodec = StreamCodec.composite(streamcodec, function, factory::create);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/Ingredient.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/Ingredient.java.patch
deleted file mode 100644
index 1d00386837..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/Ingredient.java.patch
+++ /dev/null
@@ -1,66 +0,0 @@
---- a/net/minecraft/world/item/crafting/Ingredient.java
-+++ b/net/minecraft/world/item/crafting/Ingredient.java
-@@ -20,6 +20,10 @@
- import net.minecraft.world.item.Items;
- import net.minecraft.world.item.crafting.display.SlotDisplay;
- import net.minecraft.world.level.ItemLike;
-+// CraftBukkit start
-+import java.util.List;
-+import javax.annotation.Nullable;
-+// CraftBukkit end
- 
- public final class Ingredient implements StackedContents.IngredientInfo<Holder<Item>>, Predicate<ItemStack> {
- 
-@@ -38,7 +42,25 @@
-         return recipeitemstack.values;
-     });
-     private final HolderSet<Item> values;
-+    // CraftBukkit start
-+    @Nullable
-+    private List<ItemStack> itemStacks;
- 
-+    public boolean isExact() {
-+        return this.itemStacks != null;
-+    }
-+
-+    public List<ItemStack> itemStacks() {
-+        return this.itemStacks;
-+    }
-+
-+    public static Ingredient ofStacks(List<ItemStack> stacks) {
-+        Ingredient recipe = Ingredient.of(stacks.stream().map(ItemStack::getItem));
-+        recipe.itemStacks = stacks;
-+        return recipe;
-+    }
-+    // CraftBukkit end
-+
-     private Ingredient(HolderSet<Item> entries) {
-         entries.unwrap().ifRight((list) -> {
-             if (list.isEmpty()) {
-@@ -70,6 +92,17 @@
-     }
- 
-     public boolean test(ItemStack itemstack) {
-+        // CraftBukkit start
-+        if (this.isExact()) {
-+            for (ItemStack itemstack1 : this.itemStacks()) {
-+                if (itemstack1.getItem() == itemstack.getItem() && ItemStack.isSameItemSameComponents(itemstack, itemstack1)) {
-+                    return true;
-+                }
-+            }
-+
-+            return false;
-+        }
-+        // CraftBukkit end
-         return itemstack.is(this.values);
-     }
- 
-@@ -79,7 +112,7 @@
- 
-     public boolean equals(Object object) {
-         if (object instanceof Ingredient recipeitemstack) {
--            return Objects.equals(this.values, recipeitemstack.values);
-+            return Objects.equals(this.values, recipeitemstack.values) && Objects.equals(this.itemStacks, recipeitemstack.itemStacks); // CraftBukkit
-         } else {
-             return false;
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/RecipeHolder.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/RecipeHolder.java.patch
deleted file mode 100644
index 818a4d266e..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/RecipeHolder.java.patch
+++ /dev/null
@@ -1,26 +0,0 @@
---- a/net/minecraft/world/item/crafting/RecipeHolder.java
-+++ b/net/minecraft/world/item/crafting/RecipeHolder.java
-@@ -5,10 +5,21 @@
- import net.minecraft.network.codec.StreamCodec;
- import net.minecraft.resources.ResourceKey;
- 
--public record RecipeHolder<T extends Recipe<?>>(ResourceKey<Recipe<?>> id, T value) {
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.util.CraftNamespacedKey;
-+import org.bukkit.inventory.Recipe;
-+// CraftBukkit end
- 
--    public static final StreamCodec<RegistryFriendlyByteBuf, RecipeHolder<?>> STREAM_CODEC = StreamCodec.composite(ResourceKey.streamCodec(Registries.RECIPE), RecipeHolder::id, Recipe.STREAM_CODEC, RecipeHolder::value, RecipeHolder::new);
-+public record RecipeHolder<T extends net.minecraft.world.item.crafting.Recipe<?>>(ResourceKey<net.minecraft.world.item.crafting.Recipe<?>> id, T value) {
- 
-+    // CraftBukkit start
-+    public final Recipe toBukkitRecipe() {
-+        return this.value.toBukkitRecipe(CraftNamespacedKey.fromMinecraft(this.id.location()));
-+    }
-+    // CraftBukkit end
-+
-+    public static final StreamCodec<RegistryFriendlyByteBuf, RecipeHolder<?>> STREAM_CODEC = StreamCodec.composite(ResourceKey.streamCodec(Registries.RECIPE), RecipeHolder::id, net.minecraft.world.item.crafting.Recipe.STREAM_CODEC, RecipeHolder::value, RecipeHolder::new);
-+
-     public boolean equals(Object object) {
-         if (this == object) {
-             return true;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/RecipeManager.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/RecipeManager.java.patch
deleted file mode 100644
index 83a2caa10c..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/RecipeManager.java.patch
+++ /dev/null
@@ -1,111 +0,0 @@
---- a/net/minecraft/world/item/crafting/RecipeManager.java
-+++ b/net/minecraft/world/item/crafting/RecipeManager.java
-@@ -26,11 +26,6 @@
- import net.minecraft.resources.FileToIdConverter;
- import net.minecraft.resources.ResourceKey;
- import net.minecraft.resources.ResourceLocation;
--import net.minecraft.server.level.ServerLevel;
--import net.minecraft.server.packs.resources.ResourceManager;
--import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener;
--import net.minecraft.server.packs.resources.SimplePreparableReloadListener;
--import net.minecraft.util.profiling.ProfilerFiller;
- import net.minecraft.world.flag.FeatureFlagSet;
- import net.minecraft.world.item.Item;
- import net.minecraft.world.item.crafting.display.RecipeDisplay;
-@@ -39,6 +34,16 @@
- import net.minecraft.world.level.Level;
- import org.slf4j.Logger;
- 
-+// CraftBukkit start
-+import java.util.Collections;
-+import net.minecraft.server.MinecraftServer;
-+// CraftBukkit end
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.packs.resources.ResourceManager;
-+import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener;
-+import net.minecraft.server.packs.resources.SimplePreparableReloadListener;
-+import net.minecraft.util.profiling.ProfilerFiller;
-+
- public class RecipeManager extends SimplePreparableReloadListener<RecipeMap> implements RecipeAccess {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
-@@ -111,7 +116,26 @@
-         RecipeManager.LOGGER.info("Loaded {} recipes", prepared.values().size());
-     }
- 
-+    // CraftBukkit start
-+    public void addRecipe(RecipeHolder<?> irecipe) {
-+        org.spigotmc.AsyncCatcher.catchOp("Recipe Add"); // Spigot
-+        this.recipes.addRecipe(irecipe);
-+        this.finalizeRecipeLoading();
-+    }
-+
-+    private FeatureFlagSet featureflagset;
-+
-+    public void finalizeRecipeLoading() {
-+        if (this.featureflagset != null) {
-+            this.finalizeRecipeLoading(this.featureflagset);
-+
-+            MinecraftServer.getServer().getPlayerList().reloadRecipes();
-+        }
-+    }
-+
-     public void finalizeRecipeLoading(FeatureFlagSet features) {
-+        this.featureflagset = features;
-+        // CraftBukkit end
-         List<SelectableRecipe.SingleInputEntry<StonecutterRecipe>> list = new ArrayList();
-         List<RecipeManager.IngredientCollector> list1 = RecipeManager.RECIPE_PROPERTY_SETS.entrySet().stream().map((entry) -> {
-             return new RecipeManager.IngredientCollector((ResourceKey) entry.getKey(), (RecipeManager.IngredientExtractor) entry.getValue());
-@@ -130,7 +154,7 @@
-                     StonecutterRecipe recipestonecutting = (StonecutterRecipe) irecipe;
- 
-                     if (RecipeManager.isIngredientEnabled(features, recipestonecutting.input()) && recipestonecutting.resultDisplay().isEnabled(features)) {
--                        list.add(new SelectableRecipe.SingleInputEntry<>(recipestonecutting.input(), new SelectableRecipe<>(recipestonecutting.resultDisplay(), Optional.of(recipeholder))));
-+                        list.add(new SelectableRecipe.SingleInputEntry<StonecutterRecipe>(recipestonecutting.input(), new SelectableRecipe<>(recipestonecutting.resultDisplay(), Optional.of((RecipeHolder<StonecutterRecipe>) recipeholder)))); // CraftBukkit - decompile error
-                     }
-                 }
- 
-@@ -172,7 +196,10 @@
-     }
- 
-     public <I extends RecipeInput, T extends Recipe<I>> Optional<RecipeHolder<T>> getRecipeFor(RecipeType<T> type, I input, Level world) {
--        return this.recipes.getRecipesFor(type, input, world).findFirst();
-+        // CraftBukkit start
-+        List<RecipeHolder<T>> list = this.recipes.getRecipesFor(type, input, world).toList();
-+        return (list.isEmpty()) ? Optional.empty() : Optional.of(list.getLast()); // CraftBukkit - SPIGOT-4638: last recipe gets priority
-+        // CraftBukkit end
-     }
- 
-     public Optional<RecipeHolder<?>> byKey(ResourceKey<Recipe<?>> key) {
-@@ -183,7 +210,7 @@
-     private <T extends Recipe<?>> RecipeHolder<T> byKeyTyped(RecipeType<T> type, ResourceKey<Recipe<?>> key) {
-         RecipeHolder<?> recipeholder = this.recipes.byKey(key);
- 
--        return recipeholder != null && recipeholder.value().getType().equals(type) ? recipeholder : null;
-+        return recipeholder != null && recipeholder.value().getType().equals(type) ? (RecipeHolder) recipeholder : null; // CraftBukkit - decompile error
-     }
- 
-     public Map<ResourceKey<RecipePropertySet>, RecipePropertySet> getSynchronizedItemProperties() {
-@@ -231,6 +258,22 @@
-         return new RecipeHolder<>(key, irecipe);
-     }
- 
-+    // CraftBukkit start
-+    public boolean removeRecipe(ResourceKey<Recipe<?>> mcKey) {
-+        boolean removed = this.recipes.removeRecipe((ResourceKey<Recipe<RecipeInput>>) (ResourceKey) mcKey); // Paper - generic fix
-+        if (removed) {
-+            this.finalizeRecipeLoading();
-+        }
-+
-+        return removed;
-+    }
-+
-+    public void clearRecipes() {
-+        this.recipes = RecipeMap.create(Collections.emptyList());
-+        this.finalizeRecipeLoading();
-+    }
-+    // CraftBukkit end
-+
-     public static <I extends RecipeInput, T extends Recipe<I>> RecipeManager.CachedCheck<I, T> createCheck(final RecipeType<T> type) {
-         return new RecipeManager.CachedCheck<I, T>() {
-             @Nullable
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/RecipeMap.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/RecipeMap.java.patch
deleted file mode 100644
index 171f3d8e88..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/RecipeMap.java.patch
+++ /dev/null
@@ -1,72 +0,0 @@
---- a/net/minecraft/world/item/crafting/RecipeMap.java
-+++ b/net/minecraft/world/item/crafting/RecipeMap.java
-@@ -11,6 +11,10 @@
- import javax.annotation.Nullable;
- import net.minecraft.resources.ResourceKey;
- import net.minecraft.world.level.Level;
-+// CraftBukkit start
-+import com.google.common.collect.LinkedHashMultimap;
-+import com.google.common.collect.Maps;
-+// CraftBukkit end
- 
- public class RecipeMap {
- 
-@@ -35,11 +39,56 @@
-             com_google_common_collect_immutablemap_builder.put(recipeholder.id(), recipeholder);
-         }
- 
--        return new RecipeMap(builder.build(), com_google_common_collect_immutablemap_builder.build());
-+        // CraftBukkit start - mutable
-+        return new RecipeMap(LinkedHashMultimap.create(builder.build()), Maps.newHashMap(com_google_common_collect_immutablemap_builder.build()));
-     }
- 
-+    public void addRecipe(RecipeHolder<?> irecipe) {
-+        Collection<RecipeHolder<?>> map = this.byType.get(irecipe.value().getType());
-+
-+        if (this.byKey.containsKey(irecipe.id())) {
-+            throw new IllegalStateException("Duplicate recipe ignored with ID " + irecipe.id());
-+        } else {
-+            map.add(irecipe);
-+            this.byKey.put(irecipe.id(), irecipe);
-+        }
-+    }
-+
-+    // public boolean removeRecipe(ResourceKey<Recipe<?>> mcKey) {
-+    //     boolean removed = false;
-+    //     Iterator<RecipeHolder<?>> iter = this.byType.values().iterator();
-+    //     while (iter.hasNext()) {
-+    //         RecipeHolder<?> recipe = iter.next();
-+    //         if (recipe.id().equals(mcKey)) {
-+    //             iter.remove();
-+    //             removed = true;
-+    //         }
-+    //     }
-+    //     removed |= this.byKey.remove(mcKey) != null;
-+    //
-+    //     return removed;
-+    // }
-+    // CraftBukkit end
-+
-+
-+    // Paper start - replace removeRecipe implementation
-+    public <T extends RecipeInput> boolean removeRecipe(ResourceKey<Recipe<T>> mcKey) {
-+        //noinspection unchecked
-+        final RecipeHolder<Recipe<T>> remove = (RecipeHolder<Recipe<T>>) this.byKey.remove(mcKey);
-+        if (remove == null) {
-+            return false;
-+        }
-+        final Collection<? extends RecipeHolder<? extends Recipe<T>>> recipes = this.byType(remove.value().getType());
-+        if (recipes.remove(remove)) {
-+            return true;
-+        }
-+        return false;
-+        // Paper end - why are you using a loop???
-+    }
-+    // Paper end - replace removeRecipe implementation
-+
-     public <I extends RecipeInput, T extends Recipe<I>> Collection<RecipeHolder<T>> byType(RecipeType<T> type) {
--        return this.byType.get(type);
-+        return (Collection) this.byType.get(type); // CraftBukkit - decompile error
-     }
- 
-     public Collection<RecipeHolder<?>> values() {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/ShapelessRecipe.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/ShapelessRecipe.java.patch
deleted file mode 100644
index 167de63903..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/ShapelessRecipe.java.patch
+++ /dev/null
@@ -1,39 +0,0 @@
---- a/net/minecraft/world/item/crafting/ShapelessRecipe.java
-+++ b/net/minecraft/world/item/crafting/ShapelessRecipe.java
-@@ -16,6 +16,12 @@
- import net.minecraft.world.item.crafting.display.ShapelessCraftingRecipeDisplay;
- import net.minecraft.world.item.crafting.display.SlotDisplay;
- import net.minecraft.world.level.Level;
-+// CraftBukkit start
-+import org.bukkit.NamespacedKey;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.craftbukkit.inventory.CraftRecipe;
-+import org.bukkit.craftbukkit.inventory.CraftShapelessRecipe;
-+// CraftBukkit end
- 
- public class ShapelessRecipe implements CraftingRecipe {
- 
-@@ -33,7 +39,23 @@
-         this.ingredients = ingredients;
-     }
- 
-+    // CraftBukkit start
-+    @SuppressWarnings("unchecked")
-     @Override
-+    public org.bukkit.inventory.ShapelessRecipe toBukkitRecipe(NamespacedKey id) {
-+        CraftItemStack result = CraftItemStack.asCraftMirror(this.result);
-+        CraftShapelessRecipe recipe = new CraftShapelessRecipe(id, result, this);
-+        recipe.setGroup(this.group);
-+        recipe.setCategory(CraftRecipe.getCategory(this.category()));
-+
-+        for (Ingredient list : this.ingredients) {
-+            recipe.addIngredient(CraftRecipe.toBukkit(list));
-+        }
-+        return recipe;
-+    }
-+    // CraftBukkit end
-+
-+    @Override
-     public RecipeSerializer<ShapelessRecipe> getSerializer() {
-         return RecipeSerializer.SHAPELESS_RECIPE;
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/SmeltingRecipe.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/SmeltingRecipe.java.patch
deleted file mode 100644
index 4b26fa911a..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/SmeltingRecipe.java.patch
+++ /dev/null
@@ -1,35 +0,0 @@
---- a/net/minecraft/world/item/crafting/SmeltingRecipe.java
-+++ b/net/minecraft/world/item/crafting/SmeltingRecipe.java
-@@ -4,6 +4,14 @@
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.item.Items;
- 
-+// CraftBukkit start
-+import org.bukkit.NamespacedKey;
-+import org.bukkit.craftbukkit.inventory.CraftFurnaceRecipe;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.craftbukkit.inventory.CraftRecipe;
-+import org.bukkit.inventory.Recipe;
-+// CraftBukkit end
-+
- public class SmeltingRecipe extends AbstractCookingRecipe {
- 
-     public SmeltingRecipe(String group, CookingBookCategory category, Ingredient ingredient, ItemStack result, float experience, int cookingTime) {
-@@ -45,4 +53,17 @@
- 
-         return recipebookcategory;
-     }
-+
-+    // CraftBukkit start
-+    @Override
-+    public Recipe toBukkitRecipe(NamespacedKey id) {
-+        CraftItemStack result = CraftItemStack.asCraftMirror(this.result());
-+
-+        CraftFurnaceRecipe recipe = new CraftFurnaceRecipe(id, result, CraftRecipe.toBukkit(this.input()), this.experience(), this.cookingTime());
-+        recipe.setGroup(this.group());
-+        recipe.setCategory(CraftRecipe.getCategory(this.category()));
-+
-+        return recipe;
-+    }
-+    // CraftBukkit end
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/SmithingTransformRecipe.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/SmithingTransformRecipe.java.patch
deleted file mode 100644
index 173f836106..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/SmithingTransformRecipe.java.patch
+++ /dev/null
@@ -1,61 +0,0 @@
---- a/net/minecraft/world/item/crafting/SmithingTransformRecipe.java
-+++ b/net/minecraft/world/item/crafting/SmithingTransformRecipe.java
-@@ -14,6 +14,14 @@
- import net.minecraft.world.item.crafting.display.SlotDisplay;
- import net.minecraft.world.item.crafting.display.SmithingRecipeDisplay;
- 
-+// CraftBukkit start
-+import org.bukkit.NamespacedKey;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.craftbukkit.inventory.CraftRecipe;
-+import org.bukkit.craftbukkit.inventory.CraftSmithingTransformRecipe;
-+import org.bukkit.inventory.Recipe;
-+// CraftBukkit end
-+
- public class SmithingTransformRecipe implements SmithingRecipe {
- 
-     final Optional<Ingredient> template;
-@@ -22,8 +30,15 @@
-     final ItemStack result;
-     @Nullable
-     private PlacementInfo placementInfo;
-+    final boolean copyDataComponents; // Paper - Option to prevent data components copy
- 
-     public SmithingTransformRecipe(Optional<Ingredient> template, Optional<Ingredient> base, Optional<Ingredient> addition, ItemStack result) {
-+        // Paper start - Option to prevent data components copy
-+        this(template, base, addition, result, true);
-+    }
-+    public SmithingTransformRecipe(Optional<Ingredient> template, Optional<Ingredient> base, Optional<Ingredient> addition, ItemStack result, boolean copyDataComponents) {
-+        this.copyDataComponents = copyDataComponents;
-+        // Paper end - Option to prevent data components copy
-         this.template = template;
-         this.base = base;
-         this.addition = addition;
-@@ -33,7 +48,9 @@
-     public ItemStack assemble(SmithingRecipeInput input, HolderLookup.Provider registries) {
-         ItemStack itemstack = input.base().transmuteCopy(this.result.getItem(), this.result.getCount());
- 
-+        if (this.copyDataComponents) { // Paper - Option to prevent data components copy
-         itemstack.applyComponents(this.result.getComponentsPatch());
-+        } // Paper - Option to prevent data components copy
-         return itemstack;
-     }
- 
-@@ -71,6 +88,17 @@
-         return List.of(new SmithingRecipeDisplay(Ingredient.optionalIngredientToDisplay(this.template), Ingredient.optionalIngredientToDisplay(this.base), Ingredient.optionalIngredientToDisplay(this.addition), new SlotDisplay.ItemStackSlotDisplay(this.result), new SlotDisplay.ItemSlotDisplay(Items.SMITHING_TABLE)));
-     }
- 
-+    // CraftBukkit start
-+    @Override
-+    public Recipe toBukkitRecipe(NamespacedKey id) {
-+        CraftItemStack result = CraftItemStack.asCraftMirror(this.result);
-+
-+        CraftSmithingTransformRecipe recipe = new CraftSmithingTransformRecipe(id, result, CraftRecipe.toBukkit(this.template), CraftRecipe.toBukkit(this.base), CraftRecipe.toBukkit(this.addition), this.copyDataComponents); // Paper - Option to prevent data components copy
-+
-+        return recipe;
-+    }
-+    // CraftBukkit end
-+
-     public static class Serializer implements RecipeSerializer<SmithingTransformRecipe> {
- 
-         private static final MapCodec<SmithingTransformRecipe> CODEC = RecordCodecBuilder.mapCodec((instance) -> {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/SmithingTrimRecipe.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/SmithingTrimRecipe.java.patch
deleted file mode 100644
index a5e660382a..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/SmithingTrimRecipe.java.patch
+++ /dev/null
@@ -1,69 +0,0 @@
---- a/net/minecraft/world/item/crafting/SmithingTrimRecipe.java
-+++ b/net/minecraft/world/item/crafting/SmithingTrimRecipe.java
-@@ -21,6 +21,13 @@
- import net.minecraft.world.item.equipment.trim.TrimPattern;
- import net.minecraft.world.item.equipment.trim.TrimPatterns;
- 
-+// CraftBukkit start
-+import org.bukkit.NamespacedKey;
-+import org.bukkit.craftbukkit.inventory.CraftRecipe;
-+import org.bukkit.craftbukkit.inventory.CraftSmithingTrimRecipe;
-+import org.bukkit.inventory.Recipe;
-+// CraftBukkit end
-+
- public class SmithingTrimRecipe implements SmithingRecipe {
- 
-     final Optional<Ingredient> template;
-@@ -28,18 +35,28 @@
-     final Optional<Ingredient> addition;
-     @Nullable
-     private PlacementInfo placementInfo;
-+    final boolean copyDataComponents; // Paper - Option to prevent data components copy
- 
-     public SmithingTrimRecipe(Optional<Ingredient> template, Optional<Ingredient> base, Optional<Ingredient> addition) {
-+        // Paper start - Option to prevent data components copy
-+        this(template, base, addition, true);
-+    }
-+    public SmithingTrimRecipe(Optional<Ingredient> template, Optional<Ingredient> base, Optional<Ingredient> addition, boolean copyDataComponents) {
-+        this.copyDataComponents = copyDataComponents;
-+        // Paper end - Option to prevent data components copy
-         this.template = template;
-         this.base = base;
-         this.addition = addition;
-     }
- 
-     public ItemStack assemble(SmithingRecipeInput input, HolderLookup.Provider registries) {
--        return SmithingTrimRecipe.applyTrim(registries, input.base(), input.addition(), input.template());
-+        return SmithingTrimRecipe.applyTrim(registries, input.base(), input.addition(), input.template(), this.copyDataComponents);
-     }
- 
-     public static ItemStack applyTrim(HolderLookup.Provider registries, ItemStack base, ItemStack addition, ItemStack template) {
-+        return applyTrim(registries, base, addition, template, true);
-+    }
-+    public static ItemStack applyTrim(HolderLookup.Provider registries, ItemStack base, ItemStack addition, ItemStack template, boolean copyDataComponents) {
-         Optional<Holder.Reference<TrimMaterial>> optional = TrimMaterials.getFromIngredient(registries, addition);
-         Optional<Holder.Reference<TrimPattern>> optional1 = TrimPatterns.getFromTemplate(registries, template);
- 
-@@ -49,7 +66,7 @@
-             if (armortrim != null && armortrim.hasPatternAndMaterial((Holder) optional1.get(), (Holder) optional.get())) {
-                 return ItemStack.EMPTY;
-             } else {
--                ItemStack itemstack3 = base.copyWithCount(1);
-+                ItemStack itemstack3 = copyDataComponents ? base.copyWithCount(1) : new ItemStack(base.getItem(), 1); // Paper - Option to prevent data components copy
- 
-                 itemstack3.set(DataComponents.TRIM, new ArmorTrim((Holder) optional.get(), (Holder) optional1.get()));
-                 return itemstack3;
-@@ -97,6 +114,13 @@
-         return List.of(new SmithingRecipeDisplay(slotdisplay2, slotdisplay, slotdisplay1, new SlotDisplay.SmithingTrimDemoSlotDisplay(slotdisplay, slotdisplay1, slotdisplay2), new SlotDisplay.ItemSlotDisplay(Items.SMITHING_TABLE)));
-     }
- 
-+    // CraftBukkit start
-+    @Override
-+    public Recipe toBukkitRecipe(NamespacedKey id) {
-+        return new CraftSmithingTrimRecipe(id, CraftRecipe.toBukkit(this.template), CraftRecipe.toBukkit(this.base), CraftRecipe.toBukkit(this.addition), this.copyDataComponents); // Paper - Option to prevent data components copy
-+    }
-+    // CraftBukkit end
-+
-     public static class Serializer implements RecipeSerializer<SmithingTrimRecipe> {
- 
-         private static final MapCodec<SmithingTrimRecipe> CODEC = RecordCodecBuilder.mapCodec((instance) -> {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/SmokingRecipe.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/SmokingRecipe.java.patch
deleted file mode 100644
index 7c099a0fca..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/SmokingRecipe.java.patch
+++ /dev/null
@@ -1,35 +0,0 @@
---- a/net/minecraft/world/item/crafting/SmokingRecipe.java
-+++ b/net/minecraft/world/item/crafting/SmokingRecipe.java
-@@ -4,6 +4,14 @@
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.item.Items;
- 
-+// CraftBukkit start
-+import org.bukkit.NamespacedKey;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.craftbukkit.inventory.CraftRecipe;
-+import org.bukkit.craftbukkit.inventory.CraftSmokingRecipe;
-+import org.bukkit.inventory.Recipe;
-+// CraftBukkit end
-+
- public class SmokingRecipe extends AbstractCookingRecipe {
- 
-     public SmokingRecipe(String group, CookingBookCategory category, Ingredient ingredient, ItemStack result, float experience, int cookingTime) {
-@@ -29,4 +37,17 @@
-     public RecipeBookCategory recipeBookCategory() {
-         return RecipeBookCategories.SMOKER_FOOD;
-     }
-+
-+    // CraftBukkit start
-+    @Override
-+    public Recipe toBukkitRecipe(NamespacedKey id) {
-+        CraftItemStack result = CraftItemStack.asCraftMirror(this.result());
-+
-+        CraftSmokingRecipe recipe = new CraftSmokingRecipe(id, result, CraftRecipe.toBukkit(this.input()), this.experience(), this.cookingTime());
-+        recipe.setGroup(this.group());
-+        recipe.setCategory(CraftRecipe.getCategory(this.category()));
-+
-+        return recipe;
-+    }
-+    // CraftBukkit end
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/StonecutterRecipe.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/StonecutterRecipe.java.patch
deleted file mode 100644
index fd7dc9145c..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/StonecutterRecipe.java.patch
+++ /dev/null
@@ -1,34 +0,0 @@
---- a/net/minecraft/world/item/crafting/StonecutterRecipe.java
-+++ b/net/minecraft/world/item/crafting/StonecutterRecipe.java
-@@ -7,6 +7,14 @@
- import net.minecraft.world.item.crafting.display.SlotDisplay;
- import net.minecraft.world.item.crafting.display.StonecutterRecipeDisplay;
- 
-+// CraftBukkit start
-+import org.bukkit.NamespacedKey;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.craftbukkit.inventory.CraftRecipe;
-+import org.bukkit.craftbukkit.inventory.CraftStonecuttingRecipe;
-+import org.bukkit.inventory.Recipe;
-+// CraftBukkit end
-+
- public class StonecutterRecipe extends SingleItemRecipe {
- 
-     public StonecutterRecipe(String group, Ingredient ingredient, ItemStack result) {
-@@ -36,4 +44,16 @@
-     public RecipeBookCategory recipeBookCategory() {
-         return RecipeBookCategories.STONECUTTER;
-     }
-+
-+    // CraftBukkit start
-+    @Override
-+    public Recipe toBukkitRecipe(NamespacedKey id) {
-+        CraftItemStack result = CraftItemStack.asCraftMirror(this.result());
-+
-+        CraftStonecuttingRecipe recipe = new CraftStonecuttingRecipe(id, result, CraftRecipe.toBukkit(this.input()));
-+        recipe.setGroup(this.group());
-+
-+        return recipe;
-+    }
-+    // CraftBukkit end
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/TransmuteRecipe.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/TransmuteRecipe.java.patch
deleted file mode 100644
index 7cc6f6f6d9..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/crafting/TransmuteRecipe.java.patch
+++ /dev/null
@@ -1,31 +0,0 @@
---- a/net/minecraft/world/item/crafting/TransmuteRecipe.java
-+++ b/net/minecraft/world/item/crafting/TransmuteRecipe.java
-@@ -19,6 +19,13 @@
- import net.minecraft.world.item.crafting.display.SlotDisplay;
- import net.minecraft.world.level.ItemLike;
- import net.minecraft.world.level.Level;
-+// CraftBukkit start
-+import org.bukkit.NamespacedKey;
-+import org.bukkit.craftbukkit.inventory.CraftItemType;
-+import org.bukkit.craftbukkit.inventory.CraftRecipe;
-+import org.bukkit.craftbukkit.inventory.CraftTransmuteRecipe;
-+import org.bukkit.inventory.Recipe;
-+// CraftBukkit end
- 
- public class TransmuteRecipe implements CraftingRecipe {
- 
-@@ -84,7 +91,14 @@
-         return List.of(new ShapelessCraftingRecipeDisplay(List.of(this.input.display(), this.material.display()), new SlotDisplay.ItemSlotDisplay(this.result), new SlotDisplay.ItemSlotDisplay(Items.CRAFTING_TABLE)));
-     }
- 
-+    // CraftBukkit start
-     @Override
-+    public Recipe toBukkitRecipe(NamespacedKey id) {
-+        return new CraftTransmuteRecipe(id, CraftItemType.minecraftToBukkit(this.result.value()), CraftRecipe.toBukkit(this.input), CraftRecipe.toBukkit(this.material));
-+    }
-+    // CraftBukkit end
-+
-+    @Override
-     public RecipeSerializer<TransmuteRecipe> getSerializer() {
-         return RecipeSerializer.TRANSMUTE;
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/enchantment/effects/ApplyMobEffect.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/enchantment/effects/ApplyMobEffect.java.patch
deleted file mode 100644
index abe8774935..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/enchantment/effects/ApplyMobEffect.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/item/enchantment/effects/ApplyMobEffect.java
-+++ b/net/minecraft/world/item/enchantment/effects/ApplyMobEffect.java
-@@ -34,7 +34,7 @@
-                 int j = Math.round(Mth.randomBetween(randomsource, this.minDuration.calculate(level), this.maxDuration.calculate(level)) * 20.0F);
-                 int k = Math.max(0, Math.round(Mth.randomBetween(randomsource, this.minAmplifier.calculate(level), this.maxAmplifier.calculate(level))));
- 
--                entityliving.addEffect(new MobEffectInstance((Holder) optional.get(), j, k));
-+                entityliving.addEffect(new MobEffectInstance((Holder) optional.get(), j, k), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ATTACK); // CraftBukkit
-             }
-         }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/enchantment/effects/ChangeItemDamage.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/enchantment/effects/ChangeItemDamage.java.patch
deleted file mode 100644
index 711eebddb2..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/enchantment/effects/ChangeItemDamage.java.patch
+++ /dev/null
@@ -1,14 +0,0 @@
---- a/net/minecraft/world/item/enchantment/effects/ChangeItemDamage.java
-+++ b/net/minecraft/world/item/enchantment/effects/ChangeItemDamage.java
-@@ -21,9 +21,9 @@
-     public void apply(ServerLevel world, int level, EnchantedItemInUse context, Entity user, Vec3 pos) {
-         ItemStack itemStack = context.itemStack();
-         if (itemStack.has(DataComponents.MAX_DAMAGE) && itemStack.has(DataComponents.DAMAGE)) {
--            ServerPlayer serverPlayer2 = context.owner() instanceof ServerPlayer serverPlayer ? serverPlayer : null;
-+            // ServerPlayer serverPlayer2 = context.owner() instanceof ServerPlayer serverPlayer ? serverPlayer : null; // Paper - EntityDamageItemEvent - always pass in entity
-             int i = (int)this.amount.calculate(level);
--            itemStack.hurtAndBreak(i, world, serverPlayer2, context.onBreak());
-+            itemStack.hurtAndBreak(i, world, context.owner(), context.onBreak()); // Paper - EntityDamageItemEvent - always pass in entity
-         }
-     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/enchantment/effects/Ignite.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/enchantment/effects/Ignite.java.patch
deleted file mode 100644
index 80efcc0872..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/enchantment/effects/Ignite.java.patch
+++ /dev/null
@@ -1,36 +0,0 @@
---- a/net/minecraft/world/item/enchantment/effects/Ignite.java
-+++ b/net/minecraft/world/item/enchantment/effects/Ignite.java
-@@ -7,6 +7,10 @@
- import net.minecraft.world.item.enchantment.EnchantedItemInUse;
- import net.minecraft.world.item.enchantment.LevelBasedValue;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityCombustByEntityEvent;
-+import org.bukkit.event.entity.EntityCombustEvent;
-+// CraftBukkit end
- 
- public record Ignite(LevelBasedValue duration) implements EnchantmentEntityEffect {
- 
-@@ -18,7 +22,21 @@
- 
-     @Override
-     public void apply(ServerLevel world, int level, EnchantedItemInUse context, Entity user, Vec3 pos) {
--        user.igniteForSeconds(this.duration.calculate(level));
-+        // CraftBukkit start - Call a combust event when somebody hits with a fire enchanted item
-+        EntityCombustEvent entityCombustEvent;
-+        if (context.owner() != null) {
-+            entityCombustEvent = new EntityCombustByEntityEvent(context.owner().getBukkitEntity(), user.getBukkitEntity(), this.duration.calculate(level));
-+        } else {
-+            entityCombustEvent = new EntityCombustEvent(user.getBukkitEntity(), this.duration.calculate(level));
-+        }
-+
-+        org.bukkit.Bukkit.getPluginManager().callEvent(entityCombustEvent);
-+        if (entityCombustEvent.isCancelled()) {
-+            return;
-+        }
-+
-+        user.igniteForSeconds(entityCombustEvent.getDuration(), false);
-+        // CraftBukkit end
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/enchantment/effects/ReplaceBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/enchantment/effects/ReplaceBlock.java.patch
deleted file mode 100644
index 78dc09a7de..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/enchantment/effects/ReplaceBlock.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/item/enchantment/effects/ReplaceBlock.java
-+++ b/net/minecraft/world/item/enchantment/effects/ReplaceBlock.java
-@@ -26,7 +26,7 @@
- 
-         if ((Boolean) this.predicate.map((blockpredicate) -> {
-             return blockpredicate.test(world, blockposition);
--        }).orElse(true) && world.setBlockAndUpdate(blockposition, this.blockState.getState(user.getRandom(), blockposition))) {
-+        }).orElse(true) && org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(world, blockposition, this.blockState.getState(user.getRandom(), blockposition), user)) { // CraftBukkit - Call EntityBlockFormEvent
-             this.triggerGameEvent.ifPresent((holder) -> {
-                 world.gameEvent(user, holder, blockposition);
-             });
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/enchantment/effects/ReplaceDisk.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/enchantment/effects/ReplaceDisk.java.patch
deleted file mode 100644
index 2a0ad9e549..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/enchantment/effects/ReplaceDisk.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/item/enchantment/effects/ReplaceDisk.java
-+++ b/net/minecraft/world/item/enchantment/effects/ReplaceDisk.java
-@@ -37,7 +37,7 @@
- 
-             if (blockposition1.distToCenterSqr(pos.x(), (double) blockposition1.getY() + 0.5D, pos.z()) < (double) Mth.square(j) && (Boolean) this.predicate.map((blockpredicate) -> {
-                 return blockpredicate.test(world, blockposition1);
--            }).orElse(true) && world.setBlockAndUpdate(blockposition1, this.blockState.getState(randomsource, blockposition1))) {
-+            }).orElse(true) && org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(world, blockposition1,  this.blockState.getState(randomsource, blockposition1), user)) { // CraftBukkit - Call EntityBlockFormEvent for Frost Walker
-                 this.triggerGameEvent.ifPresent((holder) -> {
-                     world.gameEvent(user, holder, blockposition1);
-                 });
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/enchantment/effects/SummonEntityEffect.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/enchantment/effects/SummonEntityEffect.java.patch
deleted file mode 100644
index 8ae0ac6d6e..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/enchantment/effects/SummonEntityEffect.java.patch
+++ /dev/null
@@ -1,35 +0,0 @@
---- a/net/minecraft/world/item/enchantment/effects/SummonEntityEffect.java
-+++ b/net/minecraft/world/item/enchantment/effects/SummonEntityEffect.java
-@@ -19,6 +19,11 @@
- import net.minecraft.world.item.enchantment.EnchantedItemInUse;
- import net.minecraft.world.level.Level;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import net.minecraft.world.item.Items;
-+import org.bukkit.event.entity.CreatureSpawnEvent;
-+import org.bukkit.event.weather.LightningStrikeEvent;
-+// CraftBukkit end
- 
- public record SummonEntityEffect(HolderSet<EntityType<?>> entityTypes, boolean joinTeam) implements EnchantmentEntityEffect {
- 
-@@ -34,7 +39,7 @@
-             Optional<Holder<EntityType<?>>> optional = this.entityTypes().getRandomElement(world.getRandom());
- 
-             if (!optional.isEmpty()) {
--                Entity entity1 = ((EntityType) ((Holder) optional.get()).value()).spawn(world, blockposition, EntitySpawnReason.TRIGGERED);
-+                Entity entity1 = ((EntityType) ((Holder) optional.get()).value()).create(world, null, blockposition, EntitySpawnReason.TRIGGERED, false, false); // CraftBukkit
- 
-                 if (entity1 != null) {
-                     if (entity1 instanceof LightningBolt) {
-@@ -46,6 +51,11 @@
- 
-                             entitylightning.setCause(entityplayer);
-                         }
-+                        // CraftBukkit start
-+                        world.strikeLightning(entity1, (context.itemStack().getItem() == Items.TRIDENT) ? LightningStrikeEvent.Cause.TRIDENT : LightningStrikeEvent.Cause.ENCHANTMENT);
-+                    } else {
-+                        world.addFreshEntityWithPassengers(entity1, CreatureSpawnEvent.SpawnReason.ENCHANTMENT);
-+                        // CraftBukkit end
-                     }
- 
-                     if (this.joinTeam && user.getTeam() != null) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/item/trading/MerchantOffer.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/trading/MerchantOffer.java.patch
deleted file mode 100644
index bf956ac4ae..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/item/trading/MerchantOffer.java.patch
+++ /dev/null
@@ -1,90 +0,0 @@
---- a/net/minecraft/world/item/trading/MerchantOffer.java
-+++ b/net/minecraft/world/item/trading/MerchantOffer.java
-@@ -8,6 +8,8 @@
- import net.minecraft.util.Mth;
- import net.minecraft.world.item.ItemStack;
- 
-+import org.bukkit.craftbukkit.inventory.CraftMerchantRecipe; // CraftBukkit
-+
- public class MerchantOffer {
- 
-     public static final Codec<MerchantOffer> CODEC = RecordCodecBuilder.create((instance) -> {
-@@ -31,6 +33,10 @@
-             return merchantrecipe.priceMultiplier;
-         }), Codec.INT.lenientOptionalFieldOf("xp", 1).forGetter((merchantrecipe) -> {
-             return merchantrecipe.xp;
-+        // Paper start
-+        }), Codec.BOOL.lenientOptionalFieldOf("Paper.IgnoreDiscounts", false).forGetter((merchantrecipe) -> {
-+            return merchantrecipe.ignoreDiscounts;
-+        // Paper end
-         })).apply(instance, MerchantOffer::new);
-     });
-     public static final StreamCodec<RegistryFriendlyByteBuf, MerchantOffer> STREAM_CODEC = StreamCodec.of(MerchantOffer::writeToStream, MerchantOffer::createFromStream);
-@@ -44,8 +50,22 @@
-     public int demand;
-     public float priceMultiplier;
-     public int xp;
-+    public boolean ignoreDiscounts; // Paper - Add ignore discounts API
-+    // CraftBukkit start
-+    private CraftMerchantRecipe bukkitHandle;
- 
--    private MerchantOffer(ItemCost firstBuyItem, Optional<ItemCost> secondBuyItem, ItemStack sellItem, int uses, int maxUses, boolean rewardingPlayerExperience, int specialPrice, int demandBonus, float priceMultiplier, int merchantExperience) {
-+    public CraftMerchantRecipe asBukkit() {
-+        return (this.bukkitHandle == null) ? this.bukkitHandle = new CraftMerchantRecipe(this) : this.bukkitHandle;
-+    }
-+
-+    public MerchantOffer(ItemCost baseCostA, Optional<ItemCost> costB, ItemStack result, int uses, int maxUses, int experience, float priceMultiplier, int demand, final boolean ignoreDiscounts, CraftMerchantRecipe bukkit) { // Paper
-+        this(baseCostA, costB, result, uses, maxUses, experience, priceMultiplier, demand);
-+        this.ignoreDiscounts = ignoreDiscounts; // Paper
-+        this.bukkitHandle = bukkit;
-+    }
-+    // CraftBukkit end
-+
-+    private MerchantOffer(ItemCost firstBuyItem, Optional<ItemCost> secondBuyItem, ItemStack sellItem, int uses, int maxUses, boolean rewardingPlayerExperience, int specialPrice, int demandBonus, float priceMultiplier, int merchantExperience, final boolean ignoreDiscounts) { // Paper
-         this.baseCostA = firstBuyItem;
-         this.costB = secondBuyItem;
-         this.result = sellItem;
-@@ -56,6 +76,7 @@
-         this.demand = demandBonus;
-         this.priceMultiplier = priceMultiplier;
-         this.xp = merchantExperience;
-+        this.ignoreDiscounts = ignoreDiscounts; // Paper
-     }
- 
-     public MerchantOffer(ItemCost buyItem, ItemStack sellItem, int maxUses, int merchantExperience, float priceMultiplier) {
-@@ -71,11 +92,11 @@
-     }
- 
-     public MerchantOffer(ItemCost firstBuyItem, Optional<ItemCost> secondBuyItem, ItemStack sellItem, int uses, int maxUses, int merchantExperience, float priceMultiplier, int demandBonus) {
--        this(firstBuyItem, secondBuyItem, sellItem, uses, maxUses, true, 0, demandBonus, priceMultiplier, merchantExperience);
-+        this(firstBuyItem, secondBuyItem, sellItem, uses, maxUses, true, 0, demandBonus, priceMultiplier, merchantExperience, false); // Paper
-     }
- 
-     private MerchantOffer(MerchantOffer offer) {
--        this(offer.baseCostA, offer.costB, offer.result.copy(), offer.uses, offer.maxUses, offer.rewardExp, offer.specialPriceDiff, offer.demand, offer.priceMultiplier, offer.xp);
-+        this(offer.baseCostA, offer.costB, offer.result.copy(), offer.uses, offer.maxUses, offer.rewardExp, offer.specialPriceDiff, offer.demand, offer.priceMultiplier, offer.xp, offer.ignoreDiscounts); // Paper
-     }
- 
-     public ItemStack getBaseCostA() {
-@@ -110,7 +131,7 @@
-     }
- 
-     public void updateDemand() {
--        this.demand = this.demand + this.uses - (this.maxUses - this.uses);
-+        this.demand = Math.max(0, this.demand + this.uses - (this.maxUses - this.uses)); // Paper - Fix MC-163962
-     }
- 
-     public ItemStack assemble() {
-@@ -185,7 +206,11 @@
-         if (!this.satisfiedBy(firstBuyStack, secondBuyStack)) {
-             return false;
-         } else {
--            firstBuyStack.shrink(this.getCostA().getCount());
-+            // CraftBukkit start
-+            if (!this.getCostA().isEmpty()) {
-+                firstBuyStack.shrink(this.getCostA().getCount());
-+            }
-+            // CraftBukkit end
-             if (!this.getCostB().isEmpty()) {
-                 secondBuyStack.shrink(this.getCostB().getCount());
-             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/BaseSpawner.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/BaseSpawner.java.patch
deleted file mode 100644
index c836ef12e6..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/BaseSpawner.java.patch
+++ /dev/null
@@ -1,167 +0,0 @@
---- a/net/minecraft/world/level/BaseSpawner.java
-+++ b/net/minecraft/world/level/BaseSpawner.java
-@@ -49,15 +49,17 @@
-     public int maxNearbyEntities = 6;
-     public int requiredPlayerRange = 16;
-     public int spawnRange = 4;
-+    private int tickDelay = 0; // Paper - Configurable mob spawner tick rate
- 
-     public BaseSpawner() {}
- 
-     public void setEntityId(EntityType<?> type, @Nullable Level world, RandomSource random, BlockPos pos) {
-         this.getOrCreateNextSpawnData(world, random, pos).getEntityToSpawn().putString("id", BuiltInRegistries.ENTITY_TYPE.getKey(type).toString());
-+        this.spawnPotentials = SimpleWeightedRandomList.empty(); // CraftBukkit - SPIGOT-3496, MC-92282
-     }
- 
-     public boolean isNearPlayer(Level world, BlockPos pos) {
--        return world.hasNearbyAlivePlayer((double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D, (double) this.requiredPlayerRange);
-+        return world.hasNearbyAlivePlayerThatAffectsSpawning((double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D, (double) this.requiredPlayerRange); // Paper - Affects Spawning API
-     }
- 
-     public void clientTick(Level world, BlockPos pos) {
-@@ -82,13 +84,19 @@
-     }
- 
-     public void serverTick(ServerLevel world, BlockPos pos) {
-+        if (spawnCount <= 0 || maxNearbyEntities <= 0) return; // Paper - Ignore impossible spawn tick
-+        // Paper start - Configurable mob spawner tick rate
-+        if (spawnDelay > 0 && --tickDelay > 0) return;
-+        tickDelay = world.paperConfig().tickRates.mobSpawner;
-+        if (tickDelay == -1) { return; } // If disabled
-+        // Paper end - Configurable mob spawner tick rate
-         if (this.isNearPlayer(world, pos)) {
--            if (this.spawnDelay == -1) {
-+            if (this.spawnDelay < -tickDelay) { // Paper - Configurable mob spawner tick rate
-                 this.delay(world, pos);
-             }
- 
-             if (this.spawnDelay > 0) {
--                --this.spawnDelay;
-+                this.spawnDelay -= tickDelay; // Paper - Configurable mob spawner tick rate
-             } else {
-                 boolean flag = false;
-                 RandomSource randomsource = world.getRandom();
-@@ -125,6 +133,20 @@
-                         } else if (!SpawnPlacements.checkSpawnRules((EntityType) optional.get(), world, EntitySpawnReason.SPAWNER, blockposition1, world.getRandom())) {
-                             continue;
-                         }
-+                        // Paper start - PreCreatureSpawnEvent
-+                        com.destroystokyo.paper.event.entity.PreSpawnerSpawnEvent event = new com.destroystokyo.paper.event.entity.PreSpawnerSpawnEvent(
-+                            io.papermc.paper.util.MCUtil.toLocation(world, d0, d1, d2),
-+                            org.bukkit.craftbukkit.entity.CraftEntityType.minecraftToBukkit(optional.get()),
-+                            io.papermc.paper.util.MCUtil.toLocation(world, pos)
-+                        );
-+                        if (!event.callEvent()) {
-+                            flag = true;
-+                            if (event.shouldAbortSpawn()) {
-+                                break;
-+                            }
-+                            continue;
-+                        }
-+                        // Paper end - PreCreatureSpawnEvent
- 
-                         Entity entity = EntityType.loadEntityRecursive(nbttagcompound, world, EntitySpawnReason.SPAWNER, (entity1) -> {
-                             entity1.moveTo(d0, d1, d2, entity1.getYRot(), entity1.getXRot());
-@@ -143,6 +165,7 @@
-                             return;
-                         }
- 
-+                        entity.preserveMotion = true; // Paper - Fix Entity Teleportation and cancel velocity if teleported; preserve entity motion from tag
-                         entity.moveTo(entity.getX(), entity.getY(), entity.getZ(), randomsource.nextFloat() * 360.0F, 0.0F);
-                         if (entity instanceof Mob) {
-                             Mob entityinsentient = (Mob) entity;
-@@ -157,13 +180,27 @@
-                                 ((Mob) entity).finalizeSpawn(world, world.getCurrentDifficultyAt(entity.blockPosition()), EntitySpawnReason.SPAWNER, (SpawnGroupData) null);
-                             }
- 
--                            Optional optional1 = mobspawnerdata.getEquipment();
-+                            Optional<net.minecraft.world.entity.EquipmentTable> optional1 = mobspawnerdata.getEquipment(); // CraftBukkit - decompile error
- 
-                             Objects.requireNonNull(entityinsentient);
-                             optional1.ifPresent(entityinsentient::equip);
-+                            // Spigot Start
-+                            if ( entityinsentient.level().spigotConfig.nerfSpawnerMobs )
-+                            {
-+                                entityinsentient.aware = false;
-+                            }
-+                            // Spigot End
-                         }
- 
--                        if (!world.tryAddFreshEntityWithPassengers(entity)) {
-+                        entity.spawnedViaMobSpawner = true; // Paper
-+                        entity.spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.SPAWNER; // Paper - Entity#getEntitySpawnReason
-+                        flag = true; // Paper
-+                        // CraftBukkit start
-+                        if (org.bukkit.craftbukkit.event.CraftEventFactory.callSpawnerSpawnEvent(entity, pos).isCancelled()) {
-+                            continue;
-+                        }
-+                        if (!world.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.SPAWNER)) {
-+                            // CraftBukkit end
-                             this.delay(world, pos);
-                             return;
-                         }
-@@ -174,7 +211,7 @@
-                             ((Mob) entity).spawnAnim();
-                         }
- 
--                        flag = true;
-+                        //flag = true; // Paper - moved up above cancellable event
-                     }
-                 }
- 
-@@ -202,7 +239,13 @@
-     }
- 
-     public void load(@Nullable Level world, BlockPos pos, CompoundTag nbt) {
-+        // Paper start - use larger int if set
-+        if (nbt.contains("Paper.Delay")) {
-+            this.spawnDelay = nbt.getInt("Paper.Delay");
-+        } else {
-         this.spawnDelay = nbt.getShort("Delay");
-+        }
-+        // Paper end
-         boolean flag = nbt.contains("SpawnData", 10);
- 
-         if (flag) {
-@@ -225,9 +268,15 @@
-             this.spawnPotentials = SimpleWeightedRandomList.single(this.nextSpawnData != null ? this.nextSpawnData : new SpawnData());
-         }
- 
-+        // Paper start - use ints if set
-+        if (nbt.contains("Paper.MinSpawnDelay", net.minecraft.nbt.Tag.TAG_ANY_NUMERIC)) {
-+            this.minSpawnDelay = nbt.getInt("Paper.MinSpawnDelay");
-+            this.maxSpawnDelay = nbt.getInt("Paper.MaxSpawnDelay");
-+            this.spawnCount = nbt.getShort("SpawnCount");
-+        } else // Paper end
-         if (nbt.contains("MinSpawnDelay", 99)) {
--            this.minSpawnDelay = nbt.getShort("MinSpawnDelay");
--            this.maxSpawnDelay = nbt.getShort("MaxSpawnDelay");
-+            this.minSpawnDelay = nbt.getInt("MinSpawnDelay"); // Paper - short -> int
-+            this.maxSpawnDelay = nbt.getInt("MaxSpawnDelay"); // Paper - short -> int
-             this.spawnCount = nbt.getShort("SpawnCount");
-         }
- 
-@@ -244,9 +293,20 @@
-     }
- 
-     public CompoundTag save(CompoundTag nbt) {
--        nbt.putShort("Delay", (short) this.spawnDelay);
--        nbt.putShort("MinSpawnDelay", (short) this.minSpawnDelay);
--        nbt.putShort("MaxSpawnDelay", (short) this.maxSpawnDelay);
-+        // Paper start
-+        if (spawnDelay > Short.MAX_VALUE) {
-+            nbt.putInt("Paper.Delay", this.spawnDelay);
-+        }
-+        nbt.putShort("Delay", (short) Math.min(Short.MAX_VALUE, this.spawnDelay));
-+
-+        if (minSpawnDelay > Short.MAX_VALUE || maxSpawnDelay > Short.MAX_VALUE) {
-+            nbt.putInt("Paper.MinSpawnDelay", this.minSpawnDelay);
-+            nbt.putInt("Paper.MaxSpawnDelay", this.maxSpawnDelay);
-+        }
-+
-+        nbt.putShort("MinSpawnDelay", (short) Math.min(Short.MAX_VALUE, this.minSpawnDelay));
-+        nbt.putShort("MaxSpawnDelay", (short) Math.min(Short.MAX_VALUE, this.maxSpawnDelay));
-+        // Paper end
-         nbt.putShort("SpawnCount", (short) this.spawnCount);
-         nbt.putShort("MaxNearbyEntities", (short) this.maxNearbyEntities);
-         nbt.putShort("RequiredPlayerRange", (short) this.requiredPlayerRange);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/BlockGetter.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/BlockGetter.java.patch
deleted file mode 100644
index b3ea51c1e2..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/BlockGetter.java.patch
+++ /dev/null
@@ -1,90 +0,0 @@
---- a/net/minecraft/world/level/BlockGetter.java
-+++ b/net/minecraft/world/level/BlockGetter.java
-@@ -12,6 +12,7 @@
- import net.minecraft.core.BlockPos;
- import net.minecraft.core.Direction;
- import net.minecraft.util.Mth;
-+import net.minecraft.world.level.block.Block;
- import net.minecraft.world.level.block.entity.BlockEntity;
- import net.minecraft.world.level.block.entity.BlockEntityType;
- import net.minecraft.world.level.block.state.BlockState;
-@@ -31,11 +32,20 @@
-     default <T extends BlockEntity> Optional<T> getBlockEntity(BlockPos pos, BlockEntityType<T> type) {
-         BlockEntity tileentity = this.getBlockEntity(pos);
- 
--        return tileentity != null && tileentity.getType() == type ? Optional.of(tileentity) : Optional.empty();
-+        return tileentity != null && tileentity.getType() == type ? (Optional<T>) Optional.of(tileentity) : Optional.empty(); // CraftBukkit - decompile error
-     }
- 
-     BlockState getBlockState(BlockPos pos);
-+    // Paper start - if loaded util
-+    @Nullable BlockState getBlockStateIfLoaded(BlockPos blockposition);
- 
-+    default @Nullable Block getBlockIfLoaded(BlockPos blockposition) {
-+        BlockState type = this.getBlockStateIfLoaded(blockposition);
-+        return type == null ? null : type.getBlock();
-+    }
-+    @Nullable FluidState getFluidIfLoaded(BlockPos blockposition);
-+    // Paper end
-+
-     FluidState getFluidState(BlockPos pos);
- 
-     default int getLightEmission(BlockPos pos) {
-@@ -59,10 +69,25 @@
-         });
-     }
- 
--    default BlockHitResult clip(ClipContext context) {
--        return (BlockHitResult) BlockGetter.traverseBlocks(context.getFrom(), context.getTo(), context, (raytrace1, blockposition) -> {
--            BlockState iblockdata = this.getBlockState(blockposition);
--            FluidState fluid = this.getFluidState(blockposition);
-+    // CraftBukkit start - moved block handling into separate method for use by Block#rayTrace
-+    default BlockHitResult clip(ClipContext raytrace1, BlockPos blockposition) {
-+        // Paper start - Add predicate for blocks when raytracing
-+        return clip(raytrace1, blockposition, null);
-+    }
-+
-+    default BlockHitResult clip(ClipContext raytrace1, BlockPos blockposition, java.util.function.Predicate<? super org.bukkit.block.Block> canCollide) {
-+            // Paper end - Add predicate for blocks when raytracing
-+            // Paper start - Prevent raytrace from loading chunks
-+            BlockState iblockdata = this.getBlockStateIfLoaded(blockposition);
-+            if (iblockdata == null) {
-+                // copied the last function parameter (listed below)
-+                Vec3 vec3d = raytrace1.getFrom().subtract(raytrace1.getTo());
-+
-+                return BlockHitResult.miss(raytrace1.getTo(), Direction.getApproximateNearest(vec3d.x, vec3d.y, vec3d.z), BlockPos.containing(raytrace1.getTo()));
-+            }
-+            // Paper end - Prevent raytrace from loading chunks
-+            if (iblockdata.isAir() || (canCollide != null && this instanceof LevelAccessor levelAccessor && !canCollide.test(org.bukkit.craftbukkit.block.CraftBlock.at(levelAccessor, blockposition)))) return null; // Paper - Perf: optimise air cases & check canCollide predicate
-+            FluidState fluid = iblockdata.getFluidState(); // Paper - Perf: don't need to go to world state again
-             Vec3 vec3d = raytrace1.getFrom();
-             Vec3 vec3d1 = raytrace1.getTo();
-             VoxelShape voxelshape = raytrace1.getBlockShape(iblockdata, this, blockposition);
-@@ -73,6 +98,18 @@
-             double d1 = movingobjectpositionblock1 == null ? Double.MAX_VALUE : raytrace1.getFrom().distanceToSqr(movingobjectpositionblock1.getLocation());
- 
-             return d0 <= d1 ? movingobjectpositionblock : movingobjectpositionblock1;
-+    }
-+    // CraftBukkit end
-+
-+    default BlockHitResult clip(ClipContext context) {
-+        // Paper start - Add predicate for blocks when raytracing
-+        return clip(context, (java.util.function.Predicate<org.bukkit.block.Block>) null);
-+    }
-+
-+    default BlockHitResult clip(ClipContext context, java.util.function.Predicate<? super org.bukkit.block.Block> canCollide) {
-+        // Paper end - Add predicate for blocks when raytracing
-+        return (BlockHitResult) BlockGetter.traverseBlocks(context.getFrom(), context.getTo(), context, (raytrace1, blockposition) -> {
-+            return this.clip(raytrace1, blockposition, canCollide); // CraftBukkit - moved into separate method // Paper - Add predicate for blocks when raytracing
-         }, (raytrace1) -> {
-             Vec3 vec3d = raytrace1.getFrom().subtract(raytrace1.getTo());
- 
-@@ -145,7 +182,7 @@
-                 double d13 = d10 * (i1 > 0 ? 1.0D - Mth.frac(d4) : Mth.frac(d4));
-                 double d14 = d11 * (j1 > 0 ? 1.0D - Mth.frac(d5) : Mth.frac(d5));
- 
--                Object object;
-+                T object; // CraftBukkit - decompile error
- 
-                 do {
-                     if (d12 > 1.0D && d13 > 1.0D && d14 > 1.0D) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/ClipContext.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/ClipContext.java.patch
deleted file mode 100644
index 20d8ff1d94..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/ClipContext.java.patch
+++ /dev/null
@@ -1,20 +0,0 @@
---- a/net/minecraft/world/level/ClipContext.java
-+++ b/net/minecraft/world/level/ClipContext.java
-@@ -22,7 +22,7 @@
-     private final CollisionContext collisionContext;
- 
-     public ClipContext(Vec3 start, Vec3 end, ClipContext.Block shapeType, ClipContext.Fluid fluidHandling, Entity entity) {
--        this(start, end, shapeType, fluidHandling, CollisionContext.of(entity));
-+        this(start, end, shapeType, fluidHandling, (entity == null) ? CollisionContext.empty() : CollisionContext.of(entity)); // CraftBukkit
-     }
- 
-     public ClipContext(Vec3 start, Vec3 end, ClipContext.Block shapeType, ClipContext.Fluid fluidHandling, CollisionContext shapeContext) {
-@@ -79,7 +79,7 @@
- 
-         private final Predicate<FluidState> canPick;
- 
--        private Fluid(final Predicate predicate) {
-+        private Fluid(final Predicate<FluidState> predicate) { // CraftBukkit - decompile error
-             this.canPick = predicate;
-         }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/GameRules.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/GameRules.java.patch
deleted file mode 100644
index 13a583766e..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/GameRules.java.patch
+++ /dev/null
@@ -1,321 +0,0 @@
---- a/net/minecraft/world/level/GameRules.java
-+++ b/net/minecraft/world/level/GameRules.java
-@@ -36,6 +36,14 @@
- 
- public class GameRules {
- 
-+    // Paper start - allow disabling gamerule limits
-+    private static final boolean DISABLE_LIMITS = Boolean.getBoolean("paper.disableGameRuleLimits");
-+
-+    private static int limit(final int limit, final int unlimited) {
-+        return DISABLE_LIMITS ? unlimited : limit;
-+    }
-+    // Paper end - allow disabling gamerule limits
-+
-     public static final int DEFAULT_RANDOM_TICK_SPEED = 3;
-     static final Logger LOGGER = LogUtils.getLogger();
-     private static final Map<GameRules.Key<?>, GameRules.Type<?>> GAME_RULE_TYPES = Maps.newTreeMap(Comparator.comparing((gamerules_gamerulekey) -> {
-@@ -58,7 +66,7 @@
-     public static final GameRules.Key<GameRules.BooleanValue> RULE_SENDCOMMANDFEEDBACK = GameRules.register("sendCommandFeedback", GameRules.Category.CHAT, GameRules.BooleanValue.create(true));
-     public static final GameRules.Key<GameRules.BooleanValue> RULE_REDUCEDDEBUGINFO = GameRules.register("reducedDebugInfo", GameRules.Category.MISC, GameRules.BooleanValue.create(false, (minecraftserver, gamerules_gameruleboolean) -> {
-         int i = gamerules_gameruleboolean.get() ? 22 : 23;
--        Iterator iterator = minecraftserver.getPlayerList().getPlayers().iterator();
-+        Iterator iterator = minecraftserver.players().iterator(); // CraftBukkit - per-world
- 
-         while (iterator.hasNext()) {
-             ServerPlayer entityplayer = (ServerPlayer) iterator.next();
-@@ -74,7 +82,7 @@
-     public static final GameRules.Key<GameRules.IntegerValue> RULE_MAX_ENTITY_CRAMMING = GameRules.register("maxEntityCramming", GameRules.Category.MOBS, GameRules.IntegerValue.create(24));
-     public static final GameRules.Key<GameRules.BooleanValue> RULE_WEATHER_CYCLE = GameRules.register("doWeatherCycle", GameRules.Category.UPDATES, GameRules.BooleanValue.create(true));
-     public static final GameRules.Key<GameRules.BooleanValue> RULE_LIMITED_CRAFTING = GameRules.register("doLimitedCrafting", GameRules.Category.PLAYER, GameRules.BooleanValue.create(false, (minecraftserver, gamerules_gameruleboolean) -> {
--        Iterator iterator = minecraftserver.getPlayerList().getPlayers().iterator();
-+        Iterator iterator = minecraftserver.players().iterator(); // CraftBukkit - per-world
- 
-         while (iterator.hasNext()) {
-             ServerPlayer entityplayer = (ServerPlayer) iterator.next();
-@@ -90,7 +98,7 @@
-     public static final GameRules.Key<GameRules.BooleanValue> RULE_DISABLE_RAIDS = GameRules.register("disableRaids", GameRules.Category.MOBS, GameRules.BooleanValue.create(false));
-     public static final GameRules.Key<GameRules.BooleanValue> RULE_DOINSOMNIA = GameRules.register("doInsomnia", GameRules.Category.SPAWNING, GameRules.BooleanValue.create(true));
-     public static final GameRules.Key<GameRules.BooleanValue> RULE_DO_IMMEDIATE_RESPAWN = GameRules.register("doImmediateRespawn", GameRules.Category.PLAYER, GameRules.BooleanValue.create(false, (minecraftserver, gamerules_gameruleboolean) -> {
--        Iterator iterator = minecraftserver.getPlayerList().getPlayers().iterator();
-+        Iterator iterator = minecraftserver.players().iterator(); // CraftBukkit - per-world
- 
-         while (iterator.hasNext()) {
-             ServerPlayer entityplayer = (ServerPlayer) iterator.next();
-@@ -120,15 +128,16 @@
-     public static final GameRules.Key<GameRules.BooleanValue> RULE_GLOBAL_SOUND_EVENTS = GameRules.register("globalSoundEvents", GameRules.Category.MISC, GameRules.BooleanValue.create(true));
-     public static final GameRules.Key<GameRules.BooleanValue> RULE_DO_VINES_SPREAD = GameRules.register("doVinesSpread", GameRules.Category.UPDATES, GameRules.BooleanValue.create(true));
-     public static final GameRules.Key<GameRules.BooleanValue> RULE_ENDER_PEARLS_VANISH_ON_DEATH = GameRules.register("enderPearlsVanishOnDeath", GameRules.Category.PLAYER, GameRules.BooleanValue.create(true));
--    public static final GameRules.Key<GameRules.IntegerValue> RULE_MINECART_MAX_SPEED = GameRules.register("minecartMaxSpeed", GameRules.Category.MISC, GameRules.IntegerValue.create(8, 1, 1000, FeatureFlagSet.of(FeatureFlags.MINECART_IMPROVEMENTS), (minecraftserver, gamerules_gameruleint) -> {
-+    public static final GameRules.Key<GameRules.IntegerValue> RULE_MINECART_MAX_SPEED = GameRules.register("minecartMaxSpeed", GameRules.Category.MISC, GameRules.IntegerValue.create(8, 1, limit(1000, Integer.MAX_VALUE), FeatureFlagSet.of(FeatureFlags.MINECART_IMPROVEMENTS), (minecraftserver, gamerules_gameruleint) -> { // Paper - allow disabling gamerule limits
-     }));
--    public static final GameRules.Key<GameRules.IntegerValue> RULE_SPAWN_CHUNK_RADIUS = GameRules.register("spawnChunkRadius", GameRules.Category.MISC, GameRules.IntegerValue.create(2, 0, 32, FeatureFlagSet.of(), (minecraftserver, gamerules_gameruleint) -> {
--        ServerLevel worldserver = minecraftserver.overworld();
-+    public static final GameRules.Key<GameRules.IntegerValue> RULE_SPAWN_CHUNK_RADIUS = GameRules.register("spawnChunkRadius", GameRules.Category.MISC, GameRules.IntegerValue.create(2, 0, limit(32, Integer.MAX_VALUE), FeatureFlagSet.of(), (minecraftserver, gamerules_gameruleint) -> { // Paper - allow disabling gamerule limits
-+        ServerLevel worldserver = minecraftserver; // CraftBukkit - per-world
- 
-         worldserver.setDefaultSpawnPos(worldserver.getSharedSpawnPos(), worldserver.getSharedSpawnAngle());
-     }));
-     private final Map<GameRules.Key<?>, GameRules.Value<?>> rules;
-     private final FeatureFlagSet enabledFeatures;
-+    private final GameRules.Value<?>[] gameruleArray; // Paper - Perf: Use array for gamerule storage
- 
-     private static <T extends GameRules.Value<T>> GameRules.Key<T> register(String name, GameRules.Category category, GameRules.Type<T> type) {
-         GameRules.Key<T> gamerules_gamerulekey = new GameRules.Key<>(name, category);
-@@ -161,10 +170,21 @@
-     private GameRules(Map<GameRules.Key<?>, GameRules.Value<?>> rules, FeatureFlagSet enabledFeatures) {
-         this.rules = rules;
-         this.enabledFeatures = enabledFeatures;
-+
-+        // Paper start - Perf: Use array for gamerule storage
-+        int arraySize = GameRules.Key.lastGameRuleIndex + 1;
-+        GameRules.Value<?>[] values = new GameRules.Value[arraySize];
-+
-+        for (Entry<GameRules.Key<?>, GameRules.Value<?>> entry : rules.entrySet()) {
-+            values[entry.getKey().gameRuleIndex] = entry.getValue();
-+        }
-+
-+        this.gameruleArray = values;
-+        // Paper end - Perf: Use array for gamerule storage
-     }
- 
-     public <T extends GameRules.Value<T>> T getRule(GameRules.Key<T> key) {
--        T t0 = (GameRules.Value) this.rules.get(key);
-+        T t0 = key == null ? null : (T) this.gameruleArray[key.gameRuleIndex]; // Paper - Perf: Use array for gamerule storage
- 
-         if (t0 == null) {
-             throw new IllegalArgumentException("Tried to access invalid game rule");
-@@ -184,7 +204,7 @@
- 
-     private void loadFromTag(DynamicLike<?> values) {
-         this.rules.forEach((gamerules_gamerulekey, gamerules_gamerulevalue) -> {
--            DataResult dataresult = values.get(gamerules_gamerulekey.id).asString();
-+            DataResult<String> dataresult = values.get(gamerules_gamerulekey.id).asString(); // CraftBukkit - decompile error
- 
-             Objects.requireNonNull(gamerules_gamerulevalue);
-             dataresult.ifSuccess(gamerules_gamerulevalue::deserialize);
-@@ -205,22 +225,22 @@
- 
-     private <T extends GameRules.Value<T>> void callVisitorCap(GameRules.GameRuleTypeVisitor visitor, GameRules.Key<?> key, GameRules.Type<?> type) {
-         if (type.requiredFeatures.isSubsetOf(this.enabledFeatures)) {
--            visitor.visit(key, type);
--            type.callVisitor(visitor, key);
-+            visitor.visit((GameRules.Key<T>) key, (GameRules.Type<T>) type); // CraftBukkit - decompile error
-+            ((GameRules.Type<T>) type).callVisitor(visitor, (GameRules.Key<T>) key); // CraftBukkit - decompile error
-         }
- 
-     }
- 
--    public void assignFrom(GameRules rules, @Nullable MinecraftServer server) {
--        rules.rules.keySet().forEach((gamerules_gamerulekey) -> {
--            this.assignCap(gamerules_gamerulekey, rules, server);
-+    public void assignFrom(GameRules gamerules, @Nullable ServerLevel minecraftserver) { // CraftBukkit - per-world
-+        gamerules.rules.keySet().forEach((gamerules_gamerulekey) -> {
-+            this.assignCap(gamerules_gamerulekey, gamerules, minecraftserver);
-         });
-     }
- 
--    private <T extends GameRules.Value<T>> void assignCap(GameRules.Key<T> key, GameRules rules, @Nullable MinecraftServer server) {
--        T t0 = rules.getRule(key);
-+    private <T extends GameRules.Value<T>> void assignCap(GameRules.Key<T> gamerules_gamerulekey, GameRules gamerules, @Nullable ServerLevel minecraftserver) { // CraftBukkit - per-world
-+        T t0 = gamerules.getRule(gamerules_gamerulekey);
- 
--        this.getRule(key).setFrom(t0, server);
-+        this.getRule(gamerules_gamerulekey).setFrom(t0, minecraftserver);
-     }
- 
-     public boolean getBoolean(GameRules.Key<GameRules.BooleanValue> rule) {
-@@ -232,6 +252,10 @@
-     }
- 
-     public static final class Key<T extends GameRules.Value<T>> {
-+        // Paper start - Perf: Use array for gamerule storage
-+        public static int lastGameRuleIndex = 0;
-+        public final int gameRuleIndex = lastGameRuleIndex++;
-+        // Paper end - Perf: Use array for gamerule storage
- 
-         final String id;
-         private final GameRules.Category category;
-@@ -285,11 +309,11 @@
- 
-         final Supplier<ArgumentType<?>> argument;
-         private final Function<GameRules.Type<T>, T> constructor;
--        final BiConsumer<MinecraftServer, T> callback;
-+        final BiConsumer<ServerLevel, T> callback; // CraftBukkit - per-world
-         private final GameRules.VisitorCaller<T> visitorCaller;
-         final FeatureFlagSet requiredFeatures;
- 
--        Type(Supplier<ArgumentType<?>> argumentType, Function<GameRules.Type<T>, T> ruleFactory, BiConsumer<MinecraftServer, T> changeCallback, GameRules.VisitorCaller<T> ruleAcceptor, FeatureFlagSet requiredFeatures) {
-+        Type(Supplier<ArgumentType<?>> argumentType, Function<GameRules.Type<T>, T> ruleFactory, BiConsumer<ServerLevel, T> changeCallback, GameRules.VisitorCaller<T> ruleAcceptor, FeatureFlagSet requiredFeatures) { // CraftBukkit - per-world
-             this.argument = argumentType;
-             this.constructor = ruleFactory;
-             this.callback = changeCallback;
-@@ -302,7 +326,7 @@
-         }
- 
-         public T createRule() {
--            return (GameRules.Value) this.constructor.apply(this);
-+            return this.constructor.apply(this); // CraftBukkit - decompile error
-         }
- 
-         public void callVisitor(GameRules.GameRuleTypeVisitor consumer, GameRules.Key<T> key) {
-@@ -322,21 +346,21 @@
-             this.type = type;
-         }
- 
--        protected abstract void updateFromArgument(CommandContext<CommandSourceStack> context, String name);
-+        protected abstract void updateFromArgument(CommandContext<CommandSourceStack> context, String name, GameRules.Key<T> gameRuleKey); // Paper - Add WorldGameRuleChangeEvent
- 
--        public void setFromArgument(CommandContext<CommandSourceStack> context, String name) {
--            this.updateFromArgument(context, name);
--            this.onChanged(((CommandSourceStack) context.getSource()).getServer());
-+        public void setFromArgument(CommandContext<CommandSourceStack> context, String name, GameRules.Key<T> gameRuleKey) { // Paper - Add WorldGameRuleChangeEvent
-+            this.updateFromArgument(context, name, gameRuleKey); // Paper - Add WorldGameRuleChangeEvent
-+            this.onChanged(((CommandSourceStack) context.getSource()).getLevel()); // CraftBukkit - per-world
-         }
- 
--        public void onChanged(@Nullable MinecraftServer server) {
--            if (server != null) {
--                this.type.callback.accept(server, this.getSelf());
-+        public void onChanged(@Nullable ServerLevel minecraftserver) { // CraftBukkit - per-world
-+            if (minecraftserver != null) {
-+                this.type.callback.accept(minecraftserver, this.getSelf());
-             }
- 
-         }
- 
--        protected abstract void deserialize(String value);
-+        public abstract void deserialize(String value); // PAIL - private->public
- 
-         public abstract String serialize();
- 
-@@ -350,7 +374,7 @@
- 
-         protected abstract T copy();
- 
--        public abstract void setFrom(T rule, @Nullable MinecraftServer server);
-+        public abstract void setFrom(T t0, @Nullable ServerLevel minecraftserver); // CraftBukkit - per-world
-     }
- 
-     public interface GameRuleTypeVisitor {
-@@ -366,7 +390,7 @@
- 
-         private boolean value;
- 
--        static GameRules.Type<GameRules.BooleanValue> create(boolean initialValue, BiConsumer<MinecraftServer, GameRules.BooleanValue> changeCallback) {
-+        static GameRules.Type<GameRules.BooleanValue> create(boolean initialValue, BiConsumer<ServerLevel, GameRules.BooleanValue> changeCallback) { // CraftBukkit - per-world
-             return new GameRules.Type<>(BoolArgumentType::bool, (gamerules_gameruledefinition) -> {
-                 return new GameRules.BooleanValue(gamerules_gameruledefinition, initialValue);
-             }, changeCallback, GameRules.GameRuleTypeVisitor::visitBoolean, FeatureFlagSet.of());
-@@ -383,17 +407,20 @@
-         }
- 
-         @Override
--        protected void updateFromArgument(CommandContext<CommandSourceStack> context, String name) {
--            this.value = BoolArgumentType.getBool(context, name);
-+        protected void updateFromArgument(CommandContext<CommandSourceStack> context, String name, GameRules.Key<BooleanValue> gameRuleKey) { // Paper start - Add WorldGameRuleChangeEvent
-+            io.papermc.paper.event.world.WorldGameRuleChangeEvent event = new io.papermc.paper.event.world.WorldGameRuleChangeEvent(context.getSource().getBukkitWorld(), context.getSource().getBukkitSender(), (org.bukkit.GameRule<Boolean>) org.bukkit.GameRule.getByName(gameRuleKey.toString()), String.valueOf(BoolArgumentType.getBool(context, name)));
-+            if (!event.callEvent()) return;
-+            this.value = Boolean.parseBoolean(event.getValue());
-+            // Paper end - Add WorldGameRuleChangeEvent
-         }
- 
-         public boolean get() {
-             return this.value;
-         }
- 
--        public void set(boolean value, @Nullable MinecraftServer server) {
--            this.value = value;
--            this.onChanged(server);
-+        public void set(boolean flag, @Nullable ServerLevel minecraftserver) { // CraftBukkit - per-world
-+            this.value = flag;
-+            this.onChanged(minecraftserver);
-         }
- 
-         @Override
-@@ -402,7 +429,7 @@
-         }
- 
-         @Override
--        protected void deserialize(String value) {
-+        public void deserialize(String value) { // PAIL - protected->public
-             this.value = Boolean.parseBoolean(value);
-         }
- 
-@@ -421,9 +448,9 @@
-             return new GameRules.BooleanValue(this.type, this.value);
-         }
- 
--        public void setFrom(GameRules.BooleanValue rule, @Nullable MinecraftServer server) {
--            this.value = rule.value;
--            this.onChanged(server);
-+        public void setFrom(GameRules.BooleanValue gamerules_gameruleboolean, @Nullable ServerLevel minecraftserver) { // CraftBukkit - per-world
-+            this.value = gamerules_gameruleboolean.value;
-+            this.onChanged(minecraftserver);
-         }
-     }
- 
-@@ -431,13 +458,13 @@
- 
-         private int value;
- 
--        private static GameRules.Type<GameRules.IntegerValue> create(int initialValue, BiConsumer<MinecraftServer, GameRules.IntegerValue> changeCallback) {
-+        private static GameRules.Type<GameRules.IntegerValue> create(int initialValue, BiConsumer<ServerLevel, GameRules.IntegerValue> changeCallback) { // CraftBukkit - per-world
-             return new GameRules.Type<>(IntegerArgumentType::integer, (gamerules_gameruledefinition) -> {
-                 return new GameRules.IntegerValue(gamerules_gameruledefinition, initialValue);
-             }, changeCallback, GameRules.GameRuleTypeVisitor::visitInteger, FeatureFlagSet.of());
-         }
- 
--        static GameRules.Type<GameRules.IntegerValue> create(int initialValue, int min, int max, FeatureFlagSet requiredFeatures, BiConsumer<MinecraftServer, GameRules.IntegerValue> changeCallback) {
-+        static GameRules.Type<GameRules.IntegerValue> create(int initialValue, int min, int max, FeatureFlagSet requiredFeatures, BiConsumer<ServerLevel, GameRules.IntegerValue> changeCallback) { // CraftBukkit - per-world
-             return new GameRules.Type<>(() -> {
-                 return IntegerArgumentType.integer(min, max);
-             }, (gamerules_gameruledefinition) -> {
-@@ -456,17 +483,20 @@
-         }
- 
-         @Override
--        protected void updateFromArgument(CommandContext<CommandSourceStack> context, String name) {
--            this.value = IntegerArgumentType.getInteger(context, name);
-+        protected void updateFromArgument(CommandContext<CommandSourceStack> context, String name, GameRules.Key<IntegerValue> gameRuleKey) { // Paper start - Add WorldGameRuleChangeEvent
-+            io.papermc.paper.event.world.WorldGameRuleChangeEvent event = new io.papermc.paper.event.world.WorldGameRuleChangeEvent(context.getSource().getBukkitWorld(), context.getSource().getBukkitSender(), (org.bukkit.GameRule<Integer>) org.bukkit.GameRule.getByName(gameRuleKey.toString()), String.valueOf(IntegerArgumentType.getInteger(context, name)));
-+            if (!event.callEvent()) return;
-+            this.value = Integer.parseInt(event.getValue());
-+            // Paper end - Add WorldGameRuleChangeEvent
-         }
- 
-         public int get() {
-             return this.value;
-         }
- 
--        public void set(int value, @Nullable MinecraftServer server) {
--            this.value = value;
--            this.onChanged(server);
-+        public void set(int i, @Nullable ServerLevel minecraftserver) { // CraftBukkit - per-world
-+            this.value = i;
-+            this.onChanged(minecraftserver);
-         }
- 
-         @Override
-@@ -475,7 +505,7 @@
-         }
- 
-         @Override
--        protected void deserialize(String value) {
-+        public void deserialize(String value) { // PAIL - protected->public
-             this.value = IntegerValue.safeParse(value);
-         }
- 
-@@ -517,9 +547,9 @@
-             return new GameRules.IntegerValue(this.type, this.value);
-         }
- 
--        public void setFrom(GameRules.IntegerValue rule, @Nullable MinecraftServer server) {
--            this.value = rule.value;
--            this.onChanged(server);
-+        public void setFrom(GameRules.IntegerValue gamerules_gameruleint, @Nullable ServerLevel minecraftserver) { // CraftBukkit - per-world
-+            this.value = gamerules_gameruleint.value;
-+            this.onChanged(minecraftserver);
-         }
-     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/LevelAccessor.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/LevelAccessor.java.patch
deleted file mode 100644
index 7c0828c3ad..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/LevelAccessor.java.patch
+++ /dev/null
@@ -1,9 +0,0 @@
---- a/net/minecraft/world/level/LevelAccessor.java
-+++ b/net/minecraft/world/level/LevelAccessor.java
-@@ -101,4 +101,6 @@
-     default void gameEvent(ResourceKey<GameEvent> event, BlockPos pos, GameEvent.Context emitter) {
-         this.gameEvent((Holder) this.registryAccess().lookupOrThrow(Registries.GAME_EVENT).getOrThrow(event), pos, emitter);
-     }
-+
-+    net.minecraft.server.level.ServerLevel getMinecraftWorld(); // CraftBukkit
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/NaturalSpawner.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/NaturalSpawner.java.patch
deleted file mode 100644
index 28fc9a7cb8..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/NaturalSpawner.java.patch
+++ /dev/null
@@ -1,212 +0,0 @@
---- a/net/minecraft/world/level/NaturalSpawner.java
-+++ b/net/minecraft/world/level/NaturalSpawner.java
-@@ -47,8 +47,13 @@
- import net.minecraft.world.level.levelgen.structure.Structure;
- import net.minecraft.world.level.levelgen.structure.structures.NetherFortressStructure;
- import net.minecraft.world.level.material.FluidState;
-+import net.minecraft.world.level.storage.LevelData;
- import net.minecraft.world.phys.Vec3;
- import org.slf4j.Logger;
-+import org.bukkit.craftbukkit.util.CraftSpawnCategory;
-+import org.bukkit.entity.SpawnCategory;
-+import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
-+// CraftBukkit end
- 
- public final class NaturalSpawner {
- 
-@@ -82,6 +87,13 @@
-             MobCategory enumcreaturetype = entity.getType().getCategory();
- 
-             if (enumcreaturetype != MobCategory.MISC) {
-+                // Paper start - Only count natural spawns
-+                if (!entity.level().paperConfig().entities.spawning.countAllMobsForSpawning &&
-+                    !(entity.spawnReason == org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.NATURAL ||
-+                        entity.spawnReason == org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.CHUNK_GEN)) {
-+                    continue;
-+                }
-+                // Paper end - Only count natural spawns
-                 BlockPos blockposition = entity.blockPosition();
- 
-                 chunkSource.query(ChunkPos.asLong(blockposition), (chunk) -> {
-@@ -107,15 +119,31 @@
-         return (Biome) chunk.getNoiseBiome(QuartPos.fromBlock(pos.getX()), QuartPos.fromBlock(pos.getY()), QuartPos.fromBlock(pos.getZ())).value();
-     }
- 
--    public static List<MobCategory> getFilteredSpawningCategories(NaturalSpawner.SpawnState info, boolean spawnAnimals, boolean spawnMonsters, boolean rare) {
-+    // CraftBukkit start - add server
-+    public static List<MobCategory> getFilteredSpawningCategories(NaturalSpawner.SpawnState spawnercreature_d, boolean flag, boolean flag1, boolean flag2, ServerLevel worldserver) {
-+        LevelData worlddata = worldserver.getLevelData(); // CraftBukkit - Other mob type spawn tick rate
-+        // CraftBukkit end
-         List<MobCategory> list = new ArrayList(NaturalSpawner.SPAWNING_CATEGORIES.length);
-         MobCategory[] aenumcreaturetype = NaturalSpawner.SPAWNING_CATEGORIES;
-         int i = aenumcreaturetype.length;
- 
-         for (int j = 0; j < i; ++j) {
-             MobCategory enumcreaturetype = aenumcreaturetype[j];
-+            // CraftBukkit start - Use per-world spawn limits
-+            boolean spawnThisTick = true;
-+            int limit = enumcreaturetype.getMaxInstancesPerChunk();
-+            SpawnCategory spawnCategory = CraftSpawnCategory.toBukkit(enumcreaturetype);
-+            if (CraftSpawnCategory.isValidForLimits(spawnCategory)) {
-+                spawnThisTick = worldserver.ticksPerSpawnCategory.getLong(spawnCategory) != 0 && worlddata.getGameTime() % worldserver.ticksPerSpawnCategory.getLong(spawnCategory) == 0;
-+                limit = worldserver.getWorld().getSpawnLimit(spawnCategory);
-+            }
- 
--            if ((spawnAnimals || !enumcreaturetype.isFriendly()) && (spawnMonsters || enumcreaturetype.isFriendly()) && (rare || !enumcreaturetype.isPersistent()) && info.canSpawnForCategoryGlobal(enumcreaturetype)) {
-+            if (!spawnThisTick || limit == 0) {
-+                continue;
-+            }
-+
-+            if ((flag || !enumcreaturetype.isFriendly()) && (flag1 || enumcreaturetype.isFriendly()) && (flag2 || !enumcreaturetype.isPersistent()) && spawnercreature_d.canSpawnForCategoryGlobal(enumcreaturetype, limit)) {
-+                // CraftBukkit end
-                 list.add(enumcreaturetype);
-             }
-         }
-@@ -144,6 +172,16 @@
-         gameprofilerfiller.pop();
-     }
- 
-+    // Paper start - Add mobcaps commands
-+    public static int globalLimitForCategory(final ServerLevel level, final MobCategory category, final int spawnableChunkCount) {
-+        final int categoryLimit = level.getWorld().getSpawnLimitUnsafe(CraftSpawnCategory.toBukkit(category));
-+        if (categoryLimit < 1) {
-+            return categoryLimit;
-+        }
-+        return categoryLimit * spawnableChunkCount / NaturalSpawner.MAGIC_NUMBER;
-+    }
-+    // Paper end - Add mobcaps commands
-+
-     public static void spawnCategoryForChunk(MobCategory group, ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner) {
-         BlockPos blockposition = NaturalSpawner.getRandomPosWithin(world, chunk);
- 
-@@ -164,9 +202,9 @@
-         StructureManager structuremanager = world.structureManager();
-         ChunkGenerator chunkgenerator = world.getChunkSource().getGenerator();
-         int i = pos.getY();
--        BlockState iblockdata = chunk.getBlockState(pos);
-+        BlockState iblockdata = world.getBlockStateIfLoadedAndInBounds(pos); // Paper - don't load chunks for mob spawn
- 
--        if (!iblockdata.isRedstoneConductor(chunk, pos)) {
-+        if (iblockdata != null && !iblockdata.isRedstoneConductor(chunk, pos)) { // Paper - don't load chunks for mob spawn
-             BlockPos.MutableBlockPos blockposition_mutableblockposition = new BlockPos.MutableBlockPos();
-             int j = 0;
-             int k = 0;
-@@ -195,7 +233,7 @@
-                             if (entityhuman != null) {
-                                 double d2 = entityhuman.distanceToSqr(d0, (double) i, d1);
- 
--                                if (NaturalSpawner.isRightDistanceToPlayerAndSpawnPoint(world, chunk, blockposition_mutableblockposition, d2)) {
-+                                if (world.isLoadedAndInBounds(blockposition_mutableblockposition) && NaturalSpawner.isRightDistanceToPlayerAndSpawnPoint(world, chunk, blockposition_mutableblockposition, d2)) { // Paper - don't load chunks for mob spawn
-                                     if (biomesettingsmobs_c == null) {
-                                         Optional<MobSpawnSettings.SpawnerData> optional = NaturalSpawner.getRandomSpawnMobAt(world, structuremanager, chunkgenerator, group, world.random, blockposition_mutableblockposition);
- 
-@@ -207,7 +245,13 @@
-                                         j1 = biomesettingsmobs_c.minCount + world.random.nextInt(1 + biomesettingsmobs_c.maxCount - biomesettingsmobs_c.minCount);
-                                     }
- 
--                                    if (NaturalSpawner.isValidSpawnPostitionForType(world, group, structuremanager, chunkgenerator, biomesettingsmobs_c, blockposition_mutableblockposition, d2) && checker.test(biomesettingsmobs_c.type, blockposition_mutableblockposition, chunk)) {
-+                                    // Paper start - PreCreatureSpawnEvent
-+                                    PreSpawnStatus doSpawning = isValidSpawnPostitionForType(world, group, structuremanager, chunkgenerator, biomesettingsmobs_c, blockposition_mutableblockposition, d2);
-+                                    if (doSpawning == PreSpawnStatus.ABORT) {
-+                                        return;
-+                                    }
-+                                    if (doSpawning == PreSpawnStatus.SUCCESS && checker.test(biomesettingsmobs_c.type, blockposition_mutableblockposition, chunk)) {
-+                                        // Paper end - PreCreatureSpawnEvent
-                                         Mob entityinsentient = NaturalSpawner.getMobForSpawn(world, biomesettingsmobs_c.type);
- 
-                                         if (entityinsentient == null) {
-@@ -217,10 +261,15 @@
-                                         entityinsentient.moveTo(d0, (double) i, d1, world.random.nextFloat() * 360.0F, 0.0F);
-                                         if (NaturalSpawner.isValidPositionForMob(world, entityinsentient, d2)) {
-                                             groupdataentity = entityinsentient.finalizeSpawn(world, world.getCurrentDifficultyAt(entityinsentient.blockPosition()), EntitySpawnReason.NATURAL, groupdataentity);
--                                            ++j;
--                                            ++k1;
--                                            world.addFreshEntityWithPassengers(entityinsentient);
--                                            runner.run(entityinsentient, chunk);
-+                                            // CraftBukkit start
-+                                            // SPIGOT-7045: Give ocelot babies back their special spawn reason. Note: This is the only modification required as ocelots count as monsters which means they only spawn during normal chunk ticking and do not spawn during chunk generation as starter mobs.
-+                                            world.addFreshEntityWithPassengers(entityinsentient, (entityinsentient instanceof net.minecraft.world.entity.animal.Ocelot && !((org.bukkit.entity.Ageable) entityinsentient.getBukkitEntity()).isAdult()) ? SpawnReason.OCELOT_BABY : SpawnReason.NATURAL);
-+                                            if (!entityinsentient.isRemoved()) {
-+                                                ++j;
-+                                                ++k1;
-+                                                runner.run(entityinsentient, chunk);
-+                                            }
-+                                            // CraftBukkit end
-                                             if (j >= entityinsentient.getMaxSpawnClusterSize()) {
-                                                 return;
-                                             }
-@@ -250,10 +299,31 @@
-         return squaredDistance <= 576.0D ? false : (world.getSharedSpawnPos().closerToCenterThan(new Vec3((double) pos.getX() + 0.5D, (double) pos.getY(), (double) pos.getZ() + 0.5D), 24.0D) ? false : Objects.equals(new ChunkPos(pos), chunk.getPos()) || world.isNaturalSpawningAllowed((BlockPos) pos));
-     }
- 
--    private static boolean isValidSpawnPostitionForType(ServerLevel world, MobCategory group, StructureManager structureAccessor, ChunkGenerator chunkGenerator, MobSpawnSettings.SpawnerData spawnEntry, BlockPos.MutableBlockPos pos, double squaredDistance) {
-+    // Paper start - PreCreatureSpawnEvent
-+    private enum PreSpawnStatus {
-+        FAIL,
-+        SUCCESS,
-+        CANCELLED,
-+        ABORT
-+    }
-+    private static PreSpawnStatus isValidSpawnPostitionForType(ServerLevel world, MobCategory group, StructureManager structureAccessor, ChunkGenerator chunkGenerator, MobSpawnSettings.SpawnerData spawnEntry, BlockPos.MutableBlockPos pos, double squaredDistance) {
-+        // Paper end - PreCreatureSpawnEvent
-         EntityType<?> entitytypes = spawnEntry.type;
- 
--        return entitytypes.getCategory() == MobCategory.MISC ? false : (!entitytypes.canSpawnFarFromPlayer() && squaredDistance > (double) (entitytypes.getCategory().getDespawnDistance() * entitytypes.getCategory().getDespawnDistance()) ? false : (entitytypes.canSummon() && NaturalSpawner.canSpawnMobAt(world, structureAccessor, chunkGenerator, group, spawnEntry, pos) ? (!SpawnPlacements.isSpawnPositionOk(entitytypes, world, pos) ? false : (!SpawnPlacements.checkSpawnRules(entitytypes, world, EntitySpawnReason.NATURAL, pos, world.random) ? false : world.noCollision(entitytypes.getSpawnAABB((double) pos.getX() + 0.5D, (double) pos.getY(), (double) pos.getZ() + 0.5D)))) : false));
-+        // Paper start - PreCreatureSpawnEvent
-+        com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent event = new com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent(
-+            io.papermc.paper.util.MCUtil.toLocation(world, pos),
-+            org.bukkit.craftbukkit.entity.CraftEntityType.minecraftToBukkit(entitytypes), SpawnReason.NATURAL
-+        );
-+        if (!event.callEvent()) {
-+            if (event.shouldAbortSpawn()) {
-+                return PreSpawnStatus.ABORT;
-+            }
-+            return PreSpawnStatus.CANCELLED;
-+        }
-+        // Paper end - PreCreatureSpawnEvent
-+
-+        return entitytypes.getCategory() == MobCategory.MISC ? PreSpawnStatus.FAIL : (!entitytypes.canSpawnFarFromPlayer() && squaredDistance > (double) (entitytypes.getCategory().getDespawnDistance() * entitytypes.getCategory().getDespawnDistance()) ? PreSpawnStatus.FAIL : (entitytypes.canSummon() && NaturalSpawner.canSpawnMobAt(world, structureAccessor, chunkGenerator, group, spawnEntry, pos) ? (!SpawnPlacements.isSpawnPositionOk(entitytypes, world, pos) ? PreSpawnStatus.FAIL : (!SpawnPlacements.checkSpawnRules(entitytypes, world, EntitySpawnReason.NATURAL, pos, world.random) ? PreSpawnStatus.FAIL : world.noCollision(entitytypes.getSpawnAABB((double) pos.getX() + 0.5D, (double) pos.getY(), (double) pos.getZ() + 0.5D)) ? PreSpawnStatus.SUCCESS : PreSpawnStatus.FAIL)) : PreSpawnStatus.FAIL)); // Paper - PreCreatureSpawnEvent
-     }
- 
-     @Nullable
-@@ -268,6 +338,7 @@
-             NaturalSpawner.LOGGER.warn("Can't spawn entity of type: {}", BuiltInRegistries.ENTITY_TYPE.getKey(type));
-         } catch (Exception exception) {
-             NaturalSpawner.LOGGER.warn("Failed to create mob", exception);
-+            com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(exception); // Paper - ServerExceptionEvent
-         }
- 
-         return null;
-@@ -356,6 +427,7 @@
-                                     entity = biomesettingsmobs_c.type.create(world.getLevel(), EntitySpawnReason.NATURAL);
-                                 } catch (Exception exception) {
-                                     NaturalSpawner.LOGGER.warn("Failed to create mob", exception);
-+                                    com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(exception); // Paper - ServerExceptionEvent
-                                     continue;
-                                 }
- 
-@@ -369,7 +441,7 @@
- 
-                                     if (entityinsentient.checkSpawnRules(world, EntitySpawnReason.CHUNK_GENERATION) && entityinsentient.checkSpawnObstruction(world)) {
-                                         groupdataentity = entityinsentient.finalizeSpawn(world, world.getCurrentDifficultyAt(entityinsentient.blockPosition()), EntitySpawnReason.CHUNK_GENERATION, groupdataentity);
--                                        world.addFreshEntityWithPassengers(entityinsentient);
-+                                        world.addFreshEntityWithPassengers(entityinsentient, SpawnReason.CHUNK_GEN); // CraftBukkit
-                                         flag = true;
-                                     }
-                                 }
-@@ -482,10 +554,12 @@
-             return this.unmodifiableMobCategoryCounts;
-         }
- 
--        boolean canSpawnForCategoryGlobal(MobCategory group) {
--            int i = group.getMaxInstancesPerChunk() * this.spawnableChunkCount / NaturalSpawner.MAGIC_NUMBER;
-+        // CraftBukkit start
-+        boolean canSpawnForCategoryGlobal(MobCategory enumcreaturetype, int limit) {
-+            int i = limit * this.spawnableChunkCount / NaturalSpawner.MAGIC_NUMBER;
-+            // CraftBukkit end
- 
--            return this.mobCategoryCounts.getInt(group) < i;
-+            return this.mobCategoryCounts.getInt(enumcreaturetype) < i;
-         }
- 
-         boolean canSpawnForCategoryLocal(MobCategory group, ChunkPos chunkPos) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/AbstractCauldronBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/AbstractCauldronBlock.java.patch
deleted file mode 100644
index e2c8cb7f03..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/AbstractCauldronBlock.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/level/block/AbstractCauldronBlock.java
-+++ b/net/minecraft/world/level/block/AbstractCauldronBlock.java
-@@ -56,7 +56,7 @@
-     @Override
-     protected InteractionResult useItemOn(ItemStack stack, BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) {
-         CauldronInteraction cauldronInteraction = this.interactions.map().get(stack.getItem());
--        return cauldronInteraction.interact(state, world, pos, player, hand, stack);
-+        return cauldronInteraction.interact(state, world, pos, player, hand, stack, hit.getDirection()); // Paper - pass hit direction
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/BambooSaplingBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/BambooSaplingBlock.java.patch
deleted file mode 100644
index 88344a70cd..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/BambooSaplingBlock.java.patch
+++ /dev/null
@@ -1,19 +0,0 @@
---- a/net/minecraft/world/level/block/BambooSaplingBlock.java
-+++ b/net/minecraft/world/level/block/BambooSaplingBlock.java
-@@ -45,7 +45,7 @@
- 
-     @Override
-     protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
--        if (random.nextInt(3) == 0 && world.isEmptyBlock(pos.above()) && world.getRawBrightness(pos.above(), 0) >= 9) {
-+        if (random.nextFloat() < (world.spigotConfig.bambooModifier / (100.0f * 3)) && world.isEmptyBlock(pos.above()) && world.getRawBrightness(pos.above(), 0) >= 9) { // Spigot - SPIGOT-7159: Better modifier resolution
-             this.growBamboo(world, pos);
-         }
- 
-@@ -87,6 +87,6 @@
-     }
- 
-     protected void growBamboo(Level world, BlockPos pos) {
--        world.setBlock(pos.above(), (BlockState) Blocks.BAMBOO.defaultBlockState().setValue(BambooStalkBlock.LEAVES, BambooLeaves.SMALL), 3);
-+        org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(world, pos, pos.above(), (BlockState) Blocks.BAMBOO.defaultBlockState().setValue(BambooStalkBlock.LEAVES, BambooLeaves.SMALL), 3); // CraftBukkit - BlockSpreadEvent
-     }
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/BambooStalkBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/BambooStalkBlock.java.patch
deleted file mode 100644
index eb12b201ec..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/BambooStalkBlock.java.patch
+++ /dev/null
@@ -1,89 +0,0 @@
---- a/net/minecraft/world/level/block/BambooStalkBlock.java
-+++ b/net/minecraft/world/level/block/BambooStalkBlock.java
-@@ -134,10 +134,10 @@
-     @Override
-     protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
-         if ((Integer) state.getValue(BambooStalkBlock.STAGE) == 0) {
--            if (random.nextInt(3) == 0 && world.isEmptyBlock(pos.above()) && world.getRawBrightness(pos.above(), 0) >= 9) {
-+            if (random.nextFloat() < (world.spigotConfig.bambooModifier / (100.0f * 3)) && world.isEmptyBlock(pos.above()) && world.getRawBrightness(pos.above(), 0) >= 9) { // Spigot - SPIGOT-7159: Better modifier resolution
-                 int i = this.getHeightBelowUpToMax(world, pos) + 1;
- 
--                if (i < 16) {
-+                if (i < world.paperConfig().maxGrowthHeight.bamboo.max) { // Paper - Configurable cactus/bamboo/reed growth height
-                     this.growBamboo(state, world, pos, random, i);
-                 }
-             }
-@@ -164,7 +164,7 @@
-         int i = this.getHeightAboveUpToMax(world, pos);
-         int j = this.getHeightBelowUpToMax(world, pos);
- 
--        return i + j + 1 < 16 && (Integer) world.getBlockState(pos.above(i)).getValue(BambooStalkBlock.STAGE) != 1;
-+        return i + j + 1 < ((Level) world).paperConfig().maxGrowthHeight.bamboo.max && (Integer) world.getBlockState(pos.above(i)).getValue(BambooStalkBlock.STAGE) != 1; // Paper - Configurable cactus/bamboo/reed growth height
-     }
- 
-     @Override
-@@ -183,7 +183,7 @@
-             BlockPos blockposition1 = pos.above(i);
-             BlockState iblockdata1 = world.getBlockState(blockposition1);
- 
--            if (k >= 16 || (Integer) iblockdata1.getValue(BambooStalkBlock.STAGE) == 1 || !world.isEmptyBlock(blockposition1.above())) {
-+            if (k >= world.paperConfig().maxGrowthHeight.bamboo.max || !iblockdata1.is(Blocks.BAMBOO) || (Integer) iblockdata1.getValue(BambooStalkBlock.STAGE) == 1 || !world.isEmptyBlock(blockposition1.above())) { // CraftBukkit - If the BlockSpreadEvent was cancelled, we have no bamboo here // Paper - Configurable cactus/bamboo/reed growth height
-                 return;
-             }
- 
-@@ -204,14 +204,18 @@
-         BlockPos blockposition1 = pos.below(2);
-         BlockState iblockdata2 = world.getBlockState(blockposition1);
-         BambooLeaves blockpropertybamboosize = BambooLeaves.NONE;
-+        boolean shouldUpdateOthers = false; // CraftBukkit
- 
-         if (height >= 1) {
-             if (iblockdata1.is(Blocks.BAMBOO) && iblockdata1.getValue(BambooStalkBlock.LEAVES) != BambooLeaves.NONE) {
-                 if (iblockdata1.is(Blocks.BAMBOO) && iblockdata1.getValue(BambooStalkBlock.LEAVES) != BambooLeaves.NONE) {
-                     blockpropertybamboosize = BambooLeaves.LARGE;
-                     if (iblockdata2.is(Blocks.BAMBOO)) {
--                        world.setBlock(pos.below(), (BlockState) iblockdata1.setValue(BambooStalkBlock.LEAVES, BambooLeaves.SMALL), 3);
--                        world.setBlock(blockposition1, (BlockState) iblockdata2.setValue(BambooStalkBlock.LEAVES, BambooLeaves.NONE), 3);
-+                        // CraftBukkit start - moved down
-+                        // world.setBlock(blockposition.below(), (IBlockData) iblockdata1.setValue(BlockBamboo.LEAVES, BlockPropertyBambooSize.SMALL), 3);
-+                        // world.setBlock(blockposition1, (IBlockData) iblockdata2.setValue(BlockBamboo.LEAVES, BlockPropertyBambooSize.NONE), 3);
-+                        shouldUpdateOthers = true;
-+                        // CraftBukkit end
-                     }
-                 }
-             } else {
-@@ -220,15 +224,22 @@
-         }
- 
-         int j = (Integer) state.getValue(BambooStalkBlock.AGE) != 1 && !iblockdata2.is(Blocks.BAMBOO) ? 0 : 1;
--        int k = (height < 11 || random.nextFloat() >= 0.25F) && height != 15 ? 0 : 1;
-+        int k = (height < world.paperConfig().maxGrowthHeight.bamboo.min || random.nextFloat() >= 0.25F) && height != (world.paperConfig().maxGrowthHeight.bamboo.max - 1) ? 0 : 1; // Paper - Configurable cactus/bamboo/reed growth height
- 
--        world.setBlock(pos.above(), (BlockState) ((BlockState) ((BlockState) this.defaultBlockState().setValue(BambooStalkBlock.AGE, j)).setValue(BambooStalkBlock.LEAVES, blockpropertybamboosize)).setValue(BambooStalkBlock.STAGE, k), 3);
-+        // CraftBukkit start
-+        if (org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(world, pos, pos.above(), (BlockState) ((BlockState) ((BlockState) this.defaultBlockState().setValue(BambooStalkBlock.AGE, j)).setValue(BambooStalkBlock.LEAVES, blockpropertybamboosize)).setValue(BambooStalkBlock.STAGE, k), 3)) {
-+            if (shouldUpdateOthers) {
-+                world.setBlock(pos.below(), (BlockState) iblockdata1.setValue(BambooStalkBlock.LEAVES, BambooLeaves.SMALL), 3);
-+                world.setBlock(blockposition1, (BlockState) iblockdata2.setValue(BambooStalkBlock.LEAVES, BambooLeaves.NONE), 3);
-+            }
-+        }
-+        // CraftBukkit end
-     }
- 
-     protected int getHeightAboveUpToMax(BlockGetter world, BlockPos pos) {
-         int i;
- 
--        for (i = 0; i < 16 && world.getBlockState(pos.above(i + 1)).is(Blocks.BAMBOO); ++i) {
-+        for (i = 0; i < ((Level) world).paperConfig().maxGrowthHeight.bamboo.max && world.getBlockState(pos.above(i + 1)).is(Blocks.BAMBOO); ++i) { // Paper - Configurable cactus/bamboo/reed growth height
-             ;
-         }
- 
-@@ -238,7 +249,7 @@
-     protected int getHeightBelowUpToMax(BlockGetter world, BlockPos pos) {
-         int i;
- 
--        for (i = 0; i < 16 && world.getBlockState(pos.below(i + 1)).is(Blocks.BAMBOO); ++i) {
-+        for (i = 0; i < ((Level) world).paperConfig().maxGrowthHeight.bamboo.max && world.getBlockState(pos.below(i + 1)).is(Blocks.BAMBOO); ++i) { // Paper - Configurable cactus/bamboo/reed growth height
-             ;
-         }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/BasePressurePlateBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/BasePressurePlateBlock.java.patch
deleted file mode 100644
index 65470ccca0..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/BasePressurePlateBlock.java.patch
+++ /dev/null
@@ -1,56 +0,0 @@
---- a/net/minecraft/world/level/block/BasePressurePlateBlock.java
-+++ b/net/minecraft/world/level/block/BasePressurePlateBlock.java
-@@ -22,6 +22,7 @@
- import net.minecraft.world.phys.AABB;
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+import org.bukkit.event.block.BlockRedstoneEvent; // CraftBukkit
- 
- public abstract class BasePressurePlateBlock extends Block {
- 
-@@ -76,6 +77,7 @@
- 
-     @Override
-     protected void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
-+        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
-         if (!world.isClientSide) {
-             int i = this.getSignalForState(state);
- 
-@@ -91,6 +93,19 @@
-         boolean flag = output > 0;
-         boolean flag1 = j > 0;
- 
-+        // CraftBukkit start - Interact Pressure Plate
-+        org.bukkit.World bworld = world.getWorld();
-+        org.bukkit.plugin.PluginManager manager = world.getCraftServer().getPluginManager();
-+
-+        if (flag != flag1) {
-+            BlockRedstoneEvent eventRedstone = new BlockRedstoneEvent(bworld.getBlockAt(pos.getX(), pos.getY(), pos.getZ()), output, j);
-+            manager.callEvent(eventRedstone);
-+
-+            flag1 = eventRedstone.getNewCurrent() > 0;
-+            j = eventRedstone.getNewCurrent();
-+        }
-+        // CraftBukkit end
-+
-         if (output != j) {
-             BlockState iblockdata1 = this.setSignalForState(state, j);
- 
-@@ -145,9 +160,15 @@
-     }
- 
-     protected static int getEntityCount(Level world, AABB box, Class<? extends Entity> entityClass) {
--        return world.getEntitiesOfClass(entityClass, box, EntitySelector.NO_SPECTATORS.and((entity) -> {
-+        // CraftBukkit start
-+        return BasePressurePlateBlock.getEntities(world, box, entityClass).size();
-+    }
-+
-+    protected static <T extends Entity> java.util.List<T> getEntities(Level world, AABB axisalignedbb, Class<T> oclass) {
-+        // CraftBukkit end
-+        return world.getEntitiesOfClass(oclass, axisalignedbb, EntitySelector.NO_SPECTATORS.and((entity) -> {
-             return !entity.isIgnoringBlockTriggers();
--        })).size();
-+        })); // CraftBukkit
-     }
- 
-     protected abstract int getSignalStrength(Level world, BlockPos pos);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/BaseRailBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/BaseRailBlock.java.patch
deleted file mode 100644
index 9e6c76abd9..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/BaseRailBlock.java.patch
+++ /dev/null
@@ -1,10 +0,0 @@
---- a/net/minecraft/world/level/block/BaseRailBlock.java
-+++ b/net/minecraft/world/level/block/BaseRailBlock.java
-@@ -71,6 +71,7 @@
-         state = this.updateDir(world, pos, state, true);
-         if (this.isStraight) {
-             world.neighborChanged(state, pos, this, null, notify);
-+            state = world.getBlockState(pos); // Paper - Fix some rails connecting improperly
-         }
- 
-         return state;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/BeehiveBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/BeehiveBlock.java.patch
deleted file mode 100644
index 73f015dbe6..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/BeehiveBlock.java.patch
+++ /dev/null
@@ -1,79 +0,0 @@
---- a/net/minecraft/world/level/block/BeehiveBlock.java
-+++ b/net/minecraft/world/level/block/BeehiveBlock.java
-@@ -94,8 +94,8 @@
-     }
- 
-     @Override
--    public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool) {
--        super.playerDestroy(world, player, pos, state, blockEntity, tool);
-+    public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool, boolean includeDrops, boolean dropExp) { // Paper - fix drops not preventing stats/food exhaustion
-+        super.playerDestroy(world, player, pos, state, blockEntity, tool, includeDrops, dropExp); // Paper - fix drops not preventing stats/food exhaustion
-         if (!world.isClientSide && blockEntity instanceof BeehiveBlockEntity tileentitybeehive) {
-             if (!EnchantmentHelper.hasTag(tool, EnchantmentTags.PREVENTS_BEE_SPAWNS_WHEN_MINING)) {
-                 tileentitybeehive.emptyAllLivingFromHive(player, state, BeehiveBlockEntity.BeeReleaseStatus.EMERGENCY);
-@@ -103,7 +103,7 @@
-                 this.angerNearbyBees(world, pos);
-             }
- 
--            CriteriaTriggers.BEE_NEST_DESTROYED.trigger((ServerPlayer) player, state, tool, tileentitybeehive.getOccupantCount());
-+            // CriteriaTriggers.BEE_NEST_DESTROYED.trigger((ServerPlayer) player, state, tool, tileentitybeehive.getOccupantCount()); // Paper - Trigger bee_nest_destroyed trigger in the correct place; moved until after items are dropped
-         }
- 
-     }
-@@ -133,7 +133,7 @@
-                 if (entitybee.getTarget() == null) {
-                     Player entityhuman = (Player) Util.getRandom(list1, world.random);
- 
--                    entitybee.setTarget(entityhuman);
-+                    entitybee.setTarget(entityhuman, org.bukkit.event.entity.EntityTargetEvent.TargetReason.CLOSEST_PLAYER, true); // CraftBukkit
-                 }
-             }
-         }
-@@ -141,7 +141,7 @@
-     }
- 
-     public static void dropHoneycomb(Level world, BlockPos pos) {
--        popResource(world, pos, new ItemStack(Items.HONEYCOMB, 3));
-+        popResource(world, pos, new ItemStack(Items.HONEYCOMB, 3)); // Paper - Add PlayerShearBlockEvent; conflict on change, item needs to be set below
-     }
- 
-     @Override
-@@ -153,8 +153,19 @@
-             Item item = stack.getItem();
- 
-             if (stack.is(Items.SHEARS)) {
-+                // Paper start - Add PlayerShearBlockEvent
-+                io.papermc.paper.event.block.PlayerShearBlockEvent event = new io.papermc.paper.event.block.PlayerShearBlockEvent((org.bukkit.entity.Player) player.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(stack), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand), new java.util.ArrayList<>());
-+                event.getDrops().add(org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(new ItemStack(Items.HONEYCOMB, 3)));
-+                if (!event.callEvent()) {
-+                    return InteractionResult.PASS;
-+                }
-+                // Paper end
-                 world.playSound(player, player.getX(), player.getY(), player.getZ(), SoundEvents.BEEHIVE_SHEAR, SoundSource.BLOCKS, 1.0F, 1.0F);
--                BeehiveBlock.dropHoneycomb(world, pos);
-+                // Paper start - Add PlayerShearBlockEvent
-+                for (org.bukkit.inventory.ItemStack itemDrop : event.getDrops()) {
-+                    popResource(world, pos, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(itemDrop));
-+                }
-+                // Paper end - Add PlayerShearBlockEvent
-                 stack.hurtAndBreak(1, player, LivingEntity.getSlotForHand(hand));
-                 flag = true;
-                 world.gameEvent((Entity) player, (Holder) GameEvent.SHEAR, pos);
-@@ -297,7 +308,7 @@
-                         ItemStack itemstack = new ItemStack(this);
- 
-                         itemstack.applyComponents(tileentitybeehive.collectComponents());
--                        itemstack.set(DataComponents.BLOCK_STATE, BlockItemStateProperties.EMPTY.with(BeehiveBlock.HONEY_LEVEL, (Comparable) i));
-+                        itemstack.set(DataComponents.BLOCK_STATE, BlockItemStateProperties.EMPTY.with(BeehiveBlock.HONEY_LEVEL, i)); // CraftBukkit - decompile error
-                         ItemEntity entityitem = new ItemEntity(world, (double) pos.getX(), (double) pos.getY(), (double) pos.getZ(), itemstack);
- 
-                         entityitem.setDefaultPickUpDelay();
-@@ -332,7 +343,7 @@
-         ItemStack itemstack = super.getCloneItemStack(world, pos, state, includeData);
- 
-         if (includeData) {
--            itemstack.set(DataComponents.BLOCK_STATE, BlockItemStateProperties.EMPTY.with(BeehiveBlock.HONEY_LEVEL, (Comparable) ((Integer) state.getValue(BeehiveBlock.HONEY_LEVEL))));
-+            itemstack.set(DataComponents.BLOCK_STATE, BlockItemStateProperties.EMPTY.with(BeehiveBlock.HONEY_LEVEL, ((Integer) state.getValue(BeehiveBlock.HONEY_LEVEL)))); // CraftBukkit - decompile error
-         }
- 
-         return itemstack;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/BellBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/BellBlock.java.patch
deleted file mode 100644
index d677bbedab..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/BellBlock.java.patch
+++ /dev/null
@@ -1,14 +0,0 @@
---- a/net/minecraft/world/level/block/BellBlock.java
-+++ b/net/minecraft/world/level/block/BellBlock.java
-@@ -148,6 +148,11 @@
-             if (direction == null) {
-                 direction = (Direction) world.getBlockState(pos).getValue(BellBlock.FACING);
-             }
-+            // CraftBukkit start
-+            if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBellRingEvent(world, pos, direction, entity)) {
-+                return false;
-+            }
-+            // CraftBukkit end
- 
-             ((BellBlockEntity) tileentity).onHit(direction);
-             world.playSound((Player) null, pos, SoundEvents.BELL_BLOCK, SoundSource.BLOCKS, 2.0F, 1.0F);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/BigDripleafBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/BigDripleafBlock.java.patch
deleted file mode 100644
index c0eab8e717..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/BigDripleafBlock.java.patch
+++ /dev/null
@@ -1,116 +0,0 @@
---- a/net/minecraft/world/level/block/BigDripleafBlock.java
-+++ b/net/minecraft/world/level/block/BigDripleafBlock.java
-@@ -44,6 +44,10 @@
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.Shapes;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.EntityInteractEvent;
-+// CraftBukkit end
- 
- public class BigDripleafBlock extends HorizontalDirectionalBlock implements BonemealableBlock, SimpleWaterloggedBlock {
- 
-@@ -119,7 +123,7 @@
- 
-     @Override
-     protected void onProjectileHit(Level world, BlockState state, BlockHitResult hit, Projectile projectile) {
--        this.setTiltAndScheduleTick(state, world, hit.getBlockPos(), Tilt.FULL, SoundEvents.BIG_DRIPLEAF_TILT_DOWN);
-+        this.setTiltAndScheduleTick(state, world, hit.getBlockPos(), Tilt.FULL, SoundEvents.BIG_DRIPLEAF_TILT_DOWN, projectile); // CraftBukkit
-     }
- 
-     @Override
-@@ -176,9 +180,23 @@
- 
-     @Override
-     protected void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
-+        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
-         if (!world.isClientSide) {
-             if (state.getValue(BigDripleafBlock.TILT) == Tilt.NONE && BigDripleafBlock.canEntityTilt(pos, entity) && !world.hasNeighborSignal(pos)) {
--                this.setTiltAndScheduleTick(state, world, pos, Tilt.UNSTABLE, (SoundEvent) null);
-+                // CraftBukkit start - tilt dripleaf
-+                org.bukkit.event.Cancellable cancellable;
-+                if (entity instanceof Player) {
-+                    cancellable = CraftEventFactory.callPlayerInteractEvent((Player) entity, org.bukkit.event.block.Action.PHYSICAL, pos, null, null, null);
-+                } else {
-+                    cancellable = new EntityInteractEvent(entity.getBukkitEntity(), world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ()));
-+                    world.getCraftServer().getPluginManager().callEvent((EntityInteractEvent) cancellable);
-+                }
-+
-+                if (cancellable.isCancelled()) {
-+                    return;
-+                }
-+                this.setTiltAndScheduleTick(state, world, pos, Tilt.UNSTABLE, (SoundEvent) null, entity);
-+                // CraftBukkit end
-             }
- 
-         }
-@@ -192,9 +210,9 @@
-             Tilt tilt = (Tilt) state.getValue(BigDripleafBlock.TILT);
- 
-             if (tilt == Tilt.UNSTABLE) {
--                this.setTiltAndScheduleTick(state, world, pos, Tilt.PARTIAL, SoundEvents.BIG_DRIPLEAF_TILT_DOWN);
-+                this.setTiltAndScheduleTick(state, world, pos, Tilt.PARTIAL, SoundEvents.BIG_DRIPLEAF_TILT_DOWN, null); // CraftBukkit
-             } else if (tilt == Tilt.PARTIAL) {
--                this.setTiltAndScheduleTick(state, world, pos, Tilt.FULL, SoundEvents.BIG_DRIPLEAF_TILT_DOWN);
-+                this.setTiltAndScheduleTick(state, world, pos, Tilt.FULL, SoundEvents.BIG_DRIPLEAF_TILT_DOWN, null); // CraftBukkit
-             } else if (tilt == Tilt.FULL) {
-                 BigDripleafBlock.resetTilt(state, world, pos);
-             }
-@@ -220,36 +238,46 @@
-         return entity.onGround() && entity.position().y > (double) ((float) pos.getY() + 0.6875F);
-     }
- 
--    private void setTiltAndScheduleTick(BlockState state, Level world, BlockPos pos, Tilt tilt, @Nullable SoundEvent sound) {
--        BigDripleafBlock.setTilt(state, world, pos, tilt);
--        if (sound != null) {
--            BigDripleafBlock.playTiltSound(world, pos, sound);
-+    // CraftBukkit start
-+    private void setTiltAndScheduleTick(BlockState iblockdata, Level world, BlockPos blockposition, Tilt tilt, @Nullable SoundEvent soundeffect, @Nullable Entity entity) {
-+        if (!BigDripleafBlock.setTilt(iblockdata, world, blockposition, tilt, entity)) return;
-+        // CraftBukkit end
-+        if (soundeffect != null) {
-+            BigDripleafBlock.playTiltSound(world, blockposition, soundeffect);
-         }
- 
-         int i = BigDripleafBlock.DELAY_UNTIL_NEXT_TILT_STATE.getInt(tilt);
- 
-         if (i != -1) {
--            world.scheduleTick(pos, (Block) this, i);
-+            world.scheduleTick(blockposition, (Block) this, i);
-         }
- 
-     }
- 
-     private static void resetTilt(BlockState state, Level world, BlockPos pos) {
--        BigDripleafBlock.setTilt(state, world, pos, Tilt.NONE);
-+        BigDripleafBlock.setTilt(state, world, pos, Tilt.NONE, null); // CraftBukkit
-         if (state.getValue(BigDripleafBlock.TILT) != Tilt.NONE) {
-             BigDripleafBlock.playTiltSound(world, pos, SoundEvents.BIG_DRIPLEAF_TILT_UP);
-         }
- 
-     }
- 
--    private static void setTilt(BlockState state, Level world, BlockPos pos, Tilt tilt) {
--        Tilt tilt1 = (Tilt) state.getValue(BigDripleafBlock.TILT);
-+    // CraftBukkit start
-+    private static boolean setTilt(BlockState iblockdata, Level world, BlockPos blockposition, Tilt tilt, @Nullable Entity entity) {
-+        if (entity != null) {
-+            if (!CraftEventFactory.callEntityChangeBlockEvent(entity, blockposition, iblockdata.setValue(BigDripleafBlock.TILT, tilt))) {
-+                return false;
-+            }
-+        }
-+        // CraftBukkit end
-+        Tilt tilt1 = (Tilt) iblockdata.getValue(BigDripleafBlock.TILT);
- 
--        world.setBlock(pos, (BlockState) state.setValue(BigDripleafBlock.TILT, tilt), 2);
-+        world.setBlock(blockposition, (BlockState) iblockdata.setValue(BigDripleafBlock.TILT, tilt), 2);
-         if (tilt.causesVibration() && tilt != tilt1) {
--            world.gameEvent((Entity) null, (Holder) GameEvent.BLOCK_CHANGE, pos);
-+            world.gameEvent((Entity) null, (Holder) GameEvent.BLOCK_CHANGE, blockposition);
-         }
- 
-+        return true; // CraftBukkit
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/BuddingAmethystBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/BuddingAmethystBlock.java.patch
deleted file mode 100644
index ee6f708bf9..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/BuddingAmethystBlock.java.patch
+++ /dev/null
@@ -1,17 +0,0 @@
---- a/net/minecraft/world/level/block/BuddingAmethystBlock.java
-+++ b/net/minecraft/world/level/block/BuddingAmethystBlock.java
-@@ -45,7 +45,13 @@
-             if (block != null) {
-                 BlockState iblockdata2 = (BlockState) ((BlockState) block.defaultBlockState().setValue(AmethystClusterBlock.FACING, enumdirection)).setValue(AmethystClusterBlock.WATERLOGGED, iblockdata1.getFluidState().getType() == Fluids.WATER);
- 
--                world.setBlockAndUpdate(blockposition1, iblockdata2);
-+                // Paper start - Have Amethyst throw both spread and grow events
-+                if (block == Blocks.SMALL_AMETHYST_BUD) {
-+                org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(world, pos, blockposition1, iblockdata2); // CraftBukkit
-+                } else {
-+                    org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(world, blockposition1, iblockdata2);
-+                }
-+                // Paper end - Have Amethyst throw both spread and grow events
-             }
- 
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/BushBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/BushBlock.java.patch
deleted file mode 100644
index d8fb08b681..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/BushBlock.java.patch
+++ /dev/null
@@ -1,27 +0,0 @@
---- a/net/minecraft/world/level/block/BushBlock.java
-+++ b/net/minecraft/world/level/block/BushBlock.java
-@@ -6,6 +6,7 @@
- import net.minecraft.tags.BlockTags;
- import net.minecraft.util.RandomSource;
- import net.minecraft.world.level.BlockGetter;
-+import net.minecraft.world.level.Level;
- import net.minecraft.world.level.LevelReader;
- import net.minecraft.world.level.ScheduledTickAccess;
- import net.minecraft.world.level.block.state.BlockBehaviour;
-@@ -27,7 +28,15 @@
- 
-     @Override
-     protected BlockState updateShape(BlockState state, LevelReader world, ScheduledTickAccess tickView, BlockPos pos, Direction direction, BlockPos neighborPos, BlockState neighborState, RandomSource random) {
--        return !state.canSurvive(world, pos) ? Blocks.AIR.defaultBlockState() : super.updateShape(state, world, tickView, pos, direction, neighborPos, neighborState, random);
-+        // CraftBukkit start
-+        if (!state.canSurvive(world, pos)) {
-+            // Suppress during worldgen
-+            if (!(world instanceof net.minecraft.server.level.ServerLevel world1 && world1.hasPhysicsEvent) || !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(world1, pos).isCancelled()) { // Paper
-+                return Blocks.AIR.defaultBlockState();
-+            }
-+        }
-+        return super.updateShape(state, world, tickView, pos, direction, neighborPos, neighborState, random);
-+        // CraftBukkit end
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/ButtonBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/ButtonBlock.java.patch
deleted file mode 100644
index 8311b29036..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/ButtonBlock.java.patch
+++ /dev/null
@@ -1,78 +0,0 @@
---- a/net/minecraft/world/level/block/ButtonBlock.java
-+++ b/net/minecraft/world/level/block/ButtonBlock.java
-@@ -34,6 +34,10 @@
- import net.minecraft.world.phys.BlockHitResult;
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+// CraftBukkit start
-+import org.bukkit.event.block.BlockRedstoneEvent;
-+import org.bukkit.event.entity.EntityInteractEvent;
-+// CraftBukkit end
- 
- public class ButtonBlock extends FaceAttachedHorizontalDirectionalBlock {
- 
-@@ -126,6 +130,19 @@
-         if ((Boolean) state.getValue(ButtonBlock.POWERED)) {
-             return InteractionResult.CONSUME;
-         } else {
-+            // CraftBukkit start
-+            boolean powered = ((Boolean) state.getValue(ButtonBlock.POWERED));
-+            org.bukkit.block.Block block = world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ());
-+            int old = (powered) ? 15 : 0;
-+            int current = (!powered) ? 15 : 0;
-+
-+            BlockRedstoneEvent eventRedstone = new BlockRedstoneEvent(block, old, current);
-+            world.getCraftServer().getPluginManager().callEvent(eventRedstone);
-+
-+            if ((eventRedstone.getNewCurrent() > 0) != (!powered)) {
-+                return InteractionResult.SUCCESS;
-+            }
-+            // CraftBukkit end
-             this.press(state, world, pos, player);
-             return InteractionResult.SUCCESS;
-         }
-@@ -191,17 +208,43 @@
- 
-     @Override
-     protected void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
-+        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
-         if (!world.isClientSide && this.type.canButtonBeActivatedByArrows() && !(Boolean) state.getValue(ButtonBlock.POWERED)) {
-             this.checkPressed(state, world, pos);
-         }
-     }
- 
-     protected void checkPressed(BlockState state, Level world, BlockPos pos) {
--        AbstractArrow entityarrow = this.type.canButtonBeActivatedByArrows() ? (AbstractArrow) world.getEntitiesOfClass(AbstractArrow.class, state.getShape(world, pos).bounds().move(pos)).stream().findFirst().orElse((Object) null) : null;
-+        AbstractArrow entityarrow = this.type.canButtonBeActivatedByArrows() ? (AbstractArrow) world.getEntitiesOfClass(AbstractArrow.class, state.getShape(world, pos).bounds().move(pos)).stream().findFirst().orElse(null) : null; // CraftBukkit - decompile error
-         boolean flag = entityarrow != null;
-         boolean flag1 = (Boolean) state.getValue(ButtonBlock.POWERED);
- 
-+        // CraftBukkit start - Call interact event when arrows turn on wooden buttons
-+        if (flag1 != flag && flag) {
-+            org.bukkit.block.Block block = world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ());
-+            EntityInteractEvent event = new EntityInteractEvent(entityarrow.getBukkitEntity(), block);
-+            world.getCraftServer().getPluginManager().callEvent(event);
-+
-+            if (event.isCancelled()) {
-+                return;
-+            }
-+        }
-+        // CraftBukkit end
-+
-         if (flag != flag1) {
-+            // CraftBukkit start
-+            boolean powered = flag1;
-+            org.bukkit.block.Block block = world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ());
-+            int old = (powered) ? 15 : 0;
-+            int current = (!powered) ? 15 : 0;
-+
-+            BlockRedstoneEvent eventRedstone = new BlockRedstoneEvent(block, old, current);
-+            world.getCraftServer().getPluginManager().callEvent(eventRedstone);
-+
-+            if ((flag && eventRedstone.getNewCurrent() <= 0) || (!flag && eventRedstone.getNewCurrent() > 0)) {
-+                return;
-+            }
-+            // CraftBukkit end
-             world.setBlock(pos, (BlockState) state.setValue(ButtonBlock.POWERED, flag), 3);
-             this.updateNeighbours(state, world, pos);
-             this.playSound((Player) null, world, pos, flag);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/CactusBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/CactusBlock.java.patch
deleted file mode 100644
index c7084f8b41..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/CactusBlock.java.patch
+++ /dev/null
@@ -1,42 +0,0 @@
---- a/net/minecraft/world/level/block/CactusBlock.java
-+++ b/net/minecraft/world/level/block/CactusBlock.java
-@@ -22,6 +22,7 @@
- import net.minecraft.world.level.redstone.Orientation;
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+import org.bukkit.craftbukkit.event.CraftEventFactory; // CraftBukkit
- 
- public class CactusBlock extends Block {
- 
-@@ -61,16 +62,17 @@
-                 ;
-             }
- 
--            if (i < 3) {
-+            if (i < world.paperConfig().maxGrowthHeight.cactus) { // Paper - Configurable cactus/bamboo/reed growth height
-                 int j = (Integer) state.getValue(CactusBlock.AGE);
- 
--                if (j == 15) {
--                    world.setBlockAndUpdate(blockposition1, this.defaultBlockState());
-+                int modifier = world.spigotConfig.cactusModifier; // Spigot - SPIGOT-7159: Better modifier resolution
-+                if (j >= 15 || (modifier != 100 && random.nextFloat() < (modifier / (100.0f * 16)))) { // Spigot - SPIGOT-7159: Better modifier resolution
-+                    CraftEventFactory.handleBlockGrowEvent(world, blockposition1, this.defaultBlockState()); // CraftBukkit
-                     BlockState iblockdata1 = (BlockState) state.setValue(CactusBlock.AGE, 0);
- 
-                     world.setBlock(pos, iblockdata1, 4);
-                     world.neighborChanged(iblockdata1, blockposition1, this, (Orientation) null, false);
--                } else {
-+                } else if (modifier == 100 || random.nextFloat() < (modifier / (100.0f * 16))) { // Spigot - SPIGOT-7159: Better modifier resolution
-                     world.setBlock(pos, (BlockState) state.setValue(CactusBlock.AGE, j + 1), 4);
-                 }
- 
-@@ -120,7 +122,8 @@
- 
-     @Override
-     protected void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
--        entity.hurt(world.damageSources().cactus(), 1.0F);
-+        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
-+        entity.hurt(world.damageSources().cactus().directBlock(world, pos), 1.0F); // CraftBukkit
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/CampfireBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/CampfireBlock.java.patch
deleted file mode 100644
index 133764e05b..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/CampfireBlock.java.patch
+++ /dev/null
@@ -1,25 +0,0 @@
---- a/net/minecraft/world/level/block/CampfireBlock.java
-+++ b/net/minecraft/world/level/block/CampfireBlock.java
-@@ -112,8 +112,9 @@
- 
-     @Override
-     protected void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
-+        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
-         if ((Boolean) state.getValue(CampfireBlock.LIT) && entity instanceof LivingEntity) {
--            entity.hurt(world.damageSources().campfire(), (float) this.fireDamage);
-+            entity.hurt(world.damageSources().campfire().directBlock(world, pos), (float) this.fireDamage); // CraftBukkit
-         }
- 
-         super.entityInside(state, world, pos, entity);
-@@ -219,6 +220,11 @@
- 
-         if (world instanceof ServerLevel worldserver) {
-             if (projectile.isOnFire() && projectile.mayInteract(worldserver, blockposition) && !(Boolean) state.getValue(CampfireBlock.LIT) && !(Boolean) state.getValue(CampfireBlock.WATERLOGGED)) {
-+                // CraftBukkit start
-+                if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(world, blockposition, projectile).isCancelled()) {
-+                    return;
-+                }
-+                // CraftBukkit end
-                 world.setBlock(blockposition, (BlockState) state.setValue(BlockStateProperties.LIT, true), 11);
-             }
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/CarvedPumpkinBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/CarvedPumpkinBlock.java.patch
deleted file mode 100644
index 8b9fd76990..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/CarvedPumpkinBlock.java.patch
+++ /dev/null
@@ -1,29 +0,0 @@
---- a/net/minecraft/world/level/block/CarvedPumpkinBlock.java
-+++ b/net/minecraft/world/level/block/CarvedPumpkinBlock.java
-@@ -24,6 +24,9 @@
- import net.minecraft.world.level.block.state.pattern.BlockPatternBuilder;
- import net.minecraft.world.level.block.state.predicate.BlockStatePredicate;
- import net.minecraft.world.level.block.state.properties.EnumProperty;
-+// CraftBukkit start
-+import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
-+// CraftBukkit end
- 
- public class CarvedPumpkinBlock extends HorizontalDirectionalBlock {
- 
-@@ -87,9 +90,14 @@
-     }
- 
-     private static void spawnGolemInWorld(Level world, BlockPattern.BlockPatternMatch patternResult, Entity entity, BlockPos pos) {
--        CarvedPumpkinBlock.clearPatternBlocks(world, patternResult);
-+        // clearPatternBlocks(world, shapedetector_shapedetectorcollection); // CraftBukkit - moved down
-         entity.moveTo((double) pos.getX() + 0.5D, (double) pos.getY() + 0.05D, (double) pos.getZ() + 0.5D, 0.0F, 0.0F);
--        world.addFreshEntity(entity);
-+        // CraftBukkit start
-+        if (!world.addFreshEntity(entity, (entity.getType() == EntityType.SNOW_GOLEM) ? SpawnReason.BUILD_SNOWMAN : SpawnReason.BUILD_IRONGOLEM)) {
-+            return;
-+        }
-+        CarvedPumpkinBlock.clearPatternBlocks(world, patternResult); // CraftBukkit - from above
-+        // CraftBukkit end
-         Iterator iterator = world.getEntitiesOfClass(ServerPlayer.class, entity.getBoundingBox().inflate(5.0D)).iterator();
- 
-         while (iterator.hasNext()) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/CauldronBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/CauldronBlock.java.patch
deleted file mode 100644
index 0d9def53a1..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/CauldronBlock.java.patch
+++ /dev/null
@@ -1,56 +0,0 @@
---- a/net/minecraft/world/level/block/CauldronBlock.java
-+++ b/net/minecraft/world/level/block/CauldronBlock.java
-@@ -12,6 +12,9 @@
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.level.material.Fluid;
- import net.minecraft.world.level.material.Fluids;
-+// CraftBukkit start
-+import org.bukkit.event.block.CauldronLevelChangeEvent;
-+// CraftBukkit end
- 
- public class CauldronBlock extends AbstractCauldronBlock {
- 
-@@ -41,9 +44,19 @@
-     public void handlePrecipitation(BlockState state, Level world, BlockPos pos, Biome.Precipitation precipitation) {
-         if (CauldronBlock.shouldHandlePrecipitation(world, precipitation)) {
-             if (precipitation == Biome.Precipitation.RAIN) {
-+                // Paper start - Call CauldronLevelChangeEvent
-+                if (!LayeredCauldronBlock.changeLevel(state, world, pos, Blocks.WATER_CAULDRON.defaultBlockState(), null, CauldronLevelChangeEvent.ChangeReason.NATURAL_FILL, false)) { // avoid duplicate game event
-+                    return;
-+                }
-+                // Paper end - Call CauldronLevelChangeEvent
-                 world.setBlockAndUpdate(pos, Blocks.WATER_CAULDRON.defaultBlockState());
-                 world.gameEvent((Entity) null, (Holder) GameEvent.BLOCK_CHANGE, pos);
-             } else if (precipitation == Biome.Precipitation.SNOW) {
-+                // Paper start - Call CauldronLevelChangeEvent
-+                if (!LayeredCauldronBlock.changeLevel(state, world, pos, Blocks.POWDER_SNOW_CAULDRON.defaultBlockState(), null, CauldronLevelChangeEvent.ChangeReason.NATURAL_FILL, false)) { // avoid duplicate game event
-+                    return;
-+                }
-+                // Paper end - Call CauldronLevelChangeEvent
-                 world.setBlockAndUpdate(pos, Blocks.POWDER_SNOW_CAULDRON.defaultBlockState());
-                 world.gameEvent((Entity) null, (Holder) GameEvent.BLOCK_CHANGE, pos);
-             }
-@@ -62,13 +75,19 @@
- 
-         if (fluid == Fluids.WATER) {
-             iblockdata1 = Blocks.WATER_CAULDRON.defaultBlockState();
--            world.setBlockAndUpdate(pos, iblockdata1);
--            world.gameEvent((Holder) GameEvent.BLOCK_CHANGE, pos, GameEvent.Context.of(iblockdata1));
-+            // Paper start - Call CauldronLevelChangeEvent; don't send level event or game event if cancelled
-+            if (!LayeredCauldronBlock.changeLevel(state, world, pos, iblockdata1, null, CauldronLevelChangeEvent.ChangeReason.NATURAL_FILL)) { // CraftBukkit
-+                return;
-+            }
-+            // Paper end - Call CauldronLevelChangeEvent
-             world.levelEvent(1047, pos, 0);
-         } else if (fluid == Fluids.LAVA) {
-             iblockdata1 = Blocks.LAVA_CAULDRON.defaultBlockState();
--            world.setBlockAndUpdate(pos, iblockdata1);
--            world.gameEvent((Holder) GameEvent.BLOCK_CHANGE, pos, GameEvent.Context.of(iblockdata1));
-+            // Paper start - Call CauldronLevelChangeEvent; don't send level event or game event if cancelled
-+            if (!LayeredCauldronBlock.changeLevel(state, world, pos, iblockdata1, null, CauldronLevelChangeEvent.ChangeReason.NATURAL_FILL)) { // CraftBukkit
-+                return;
-+            }
-+            // Paper end - Call CauldronLevelChangeEvent
-             world.levelEvent(1046, pos, 0);
-         }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/CaveVines.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/CaveVines.java.patch
deleted file mode 100644
index b340d9c9cb..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/CaveVines.java.patch
+++ /dev/null
@@ -1,42 +0,0 @@
---- a/net/minecraft/world/level/block/CaveVines.java
-+++ b/net/minecraft/world/level/block/CaveVines.java
-@@ -19,6 +19,13 @@
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.phys.shapes.VoxelShape;
- 
-+// CraftBukkit start
-+import java.util.Collections;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.event.player.PlayerHarvestBlockEvent;
-+// CraftBukkit end
-+
- public interface CaveVines {
- 
-     VoxelShape SHAPE = Block.box(1.0D, 0.0D, 1.0D, 15.0D, 16.0D, 15.0D);
-@@ -26,7 +33,24 @@
- 
-     static InteractionResult use(@Nullable Entity picker, BlockState state, Level world, BlockPos pos) {
-         if ((Boolean) state.getValue(CaveVines.BERRIES)) {
--            Block.popResource(world, pos, new ItemStack(Items.GLOW_BERRIES, 1));
-+            // CraftBukkit start
-+            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(picker, pos, (BlockState) state.setValue(CaveVines.BERRIES, false))) {
-+                return InteractionResult.SUCCESS;
-+            }
-+
-+            if (picker instanceof Player) {
-+                PlayerHarvestBlockEvent event = CraftEventFactory.callPlayerHarvestBlockEvent(world, pos, (Player) picker, net.minecraft.world.InteractionHand.MAIN_HAND, Collections.singletonList(new ItemStack(Items.GLOW_BERRIES, 1)));
-+                if (event.isCancelled()) {
-+                    return InteractionResult.SUCCESS; // We need to return a success either way, because making it PASS or FAIL will result in a bug where cancelling while harvesting w/ block in hand places block
-+                }
-+                for (org.bukkit.inventory.ItemStack itemStack : event.getItemsHarvested()) {
-+                    Block.popResource(world, pos, CraftItemStack.asNMSCopy(itemStack));
-+                }
-+            } else {
-+                Block.popResource(world, pos, new ItemStack(Items.GLOW_BERRIES, 1));
-+            }
-+            // CraftBukkit end
-+
-             float f = Mth.randomBetween(world.random, 0.8F, 1.2F);
- 
-             world.playSound((Player) null, pos, SoundEvents.CAVE_VINES_PICK_BERRIES, SoundSource.BLOCKS, 1.0F, f);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/CeilingHangingSignBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/CeilingHangingSignBlock.java.patch
deleted file mode 100644
index b85ba5b3dc..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/CeilingHangingSignBlock.java.patch
+++ /dev/null
@@ -1,10 +0,0 @@
---- a/net/minecraft/world/level/block/CeilingHangingSignBlock.java
-+++ b/net/minecraft/world/level/block/CeilingHangingSignBlock.java
-@@ -159,6 +159,6 @@
-     @Nullable
-     @Override
-     public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level world, BlockState state, BlockEntityType<T> type) {
--        return createTickerHelper(type, BlockEntityType.HANGING_SIGN, SignBlockEntity::tick);
-+        return null; // Craftbukkit - remove unnecessary sign ticking
-     }
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/ChangeOverTimeBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/ChangeOverTimeBlock.java.patch
deleted file mode 100644
index 94b24017e5..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/ChangeOverTimeBlock.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/level/block/ChangeOverTimeBlock.java
-+++ b/net/minecraft/world/level/block/ChangeOverTimeBlock.java
-@@ -20,7 +20,7 @@
- 
-         if (random.nextFloat() < 0.05688889F) {
-             this.getNextState(state, world, pos, random).ifPresent((iblockdata1) -> {
--                world.setBlockAndUpdate(pos, iblockdata1);
-+                org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(world, pos, iblockdata1); // CraftBukkit
-             });
-         }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/ChestBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/ChestBlock.java.patch
deleted file mode 100644
index 4b025cb2e4..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/ChestBlock.java.patch
+++ /dev/null
@@ -1,118 +0,0 @@
---- a/net/minecraft/world/level/block/ChestBlock.java
-+++ b/net/minecraft/world/level/block/ChestBlock.java
-@@ -91,24 +91,7 @@
-         public Optional<MenuProvider> acceptDouble(final ChestBlockEntity first, final ChestBlockEntity second) {
-             final CompoundContainer inventorylargechest = new CompoundContainer(first, second);
- 
--            return Optional.of(new MenuProvider(this) {
--                @Nullable
--                @Override
--                public AbstractContainerMenu createMenu(int syncId, Inventory playerInventory, Player player) {
--                    if (first.canOpen(player) && second.canOpen(player)) {
--                        first.unpackLootTable(playerInventory.player);
--                        second.unpackLootTable(playerInventory.player);
--                        return ChestMenu.sixRows(syncId, playerInventory, inventorylargechest);
--                    } else {
--                        return null;
--                    }
--                }
--
--                @Override
--                public Component getDisplayName() {
--                    return (Component) (first.hasCustomName() ? first.getDisplayName() : (second.hasCustomName() ? second.getDisplayName() : Component.translatable("container.chestDouble")));
--                }
--            });
-+            return Optional.of(new DoubleInventory(first, second, inventorylargechest)); // CraftBukkit // CraftBukkit - decompile error
-         }
- 
-         public Optional<MenuProvider> acceptSingle(ChestBlockEntity single) {
-@@ -118,8 +101,40 @@
-         @Override
-         public Optional<MenuProvider> acceptNone() {
-             return Optional.empty();
-+        }
-+    };
-+
-+    // CraftBukkit start
-+    public static class DoubleInventory implements MenuProvider {
-+
-+        private final ChestBlockEntity tileentitychest;
-+        private final ChestBlockEntity tileentitychest1;
-+        public final CompoundContainer inventorylargechest;
-+
-+        public DoubleInventory(ChestBlockEntity tileentitychest, ChestBlockEntity tileentitychest1, CompoundContainer inventorylargechest) {
-+            this.tileentitychest = tileentitychest;
-+            this.tileentitychest1 = tileentitychest1;
-+            this.inventorylargechest = inventorylargechest;
-+        }
-+
-+        @Nullable
-+        @Override
-+        public AbstractContainerMenu createMenu(int syncId, Inventory playerInventory, Player player) {
-+            if (this.tileentitychest.canOpen(player) && this.tileentitychest1.canOpen(player)) {
-+                this.tileentitychest.unpackLootTable(playerInventory.player);
-+                this.tileentitychest1.unpackLootTable(playerInventory.player);
-+                return ChestMenu.sixRows(syncId, playerInventory, this.inventorylargechest);
-+            } else {
-+                return null;
-+            }
-         }
-+
-+        @Override
-+        public Component getDisplayName() {
-+            return (Component) (this.tileentitychest.hasCustomName() ? this.tileentitychest.getDisplayName() : (this.tileentitychest1.hasCustomName() ? this.tileentitychest1.getDisplayName() : Component.translatable("container.chestDouble")));
-+        }
-     };
-+    // CraftBukkit end
- 
-     @Override
-     public MapCodec<? extends ChestBlock> codec() {
-@@ -232,8 +247,7 @@
-         if (world instanceof ServerLevel worldserver) {
-             MenuProvider itileinventory = this.getMenuProvider(state, world, pos);
- 
--            if (itileinventory != null) {
--                player.openMenu(itileinventory);
-+            if (itileinventory != null && player.openMenu(itileinventory).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
-                 player.awardStat(this.getOpenChestStat());
-                 PiglinAi.angerNearbyPiglins(worldserver, player, true);
-             }
-@@ -257,7 +271,7 @@
- 
-     @Override
-     public DoubleBlockCombiner.NeighborCombineResult<? extends ChestBlockEntity> combine(BlockState state, Level world, BlockPos pos, boolean ignoreBlocked) {
--        BiPredicate bipredicate;
-+        BiPredicate<LevelAccessor, BlockPos> bipredicate; // CraftBukkit - decompile error
- 
-         if (ignoreBlocked) {
-             bipredicate = (generatoraccess, blockposition1) -> {
-@@ -273,9 +287,16 @@
-     @Nullable
-     @Override
-     public MenuProvider getMenuProvider(BlockState state, Level world, BlockPos pos) {
--        return (MenuProvider) ((Optional) this.combine(state, world, pos, false).apply(ChestBlock.MENU_PROVIDER_COMBINER)).orElse((Object) null);
-+        // CraftBukkit start
-+        return this.getMenuProvider(state, world, pos, false);
-     }
- 
-+    @Nullable
-+    public MenuProvider getMenuProvider(BlockState iblockdata, Level world, BlockPos blockposition, boolean ignoreObstructions) {
-+        return (MenuProvider) ((Optional) this.combine(iblockdata, world, blockposition, ignoreObstructions).apply(ChestBlock.MENU_PROVIDER_COMBINER)).orElse((Object) null);
-+        // CraftBukkit end
-+    }
-+
-     public static DoubleBlockCombiner.Combiner<ChestBlockEntity, Float2FloatFunction> opennessCombiner(final LidBlockEntity progress) {
-         return new DoubleBlockCombiner.Combiner<ChestBlockEntity, Float2FloatFunction>() {
-             public Float2FloatFunction acceptDouble(ChestBlockEntity first, ChestBlockEntity second) {
-@@ -321,6 +342,11 @@
-     }
- 
-     private static boolean isCatSittingOnChest(LevelAccessor world, BlockPos pos) {
-+        // Paper start - Option to disable chest cat detection
-+        if (world.getMinecraftWorld().paperConfig().entities.behavior.disableChestCatDetection) {
-+            return false;
-+        }
-+        // Paper end - Option to disable chest cat detection
-         List<Cat> list = world.getEntitiesOfClass(Cat.class, new AABB((double) pos.getX(), (double) (pos.getY() + 1), (double) pos.getZ(), (double) (pos.getX() + 1), (double) (pos.getY() + 2), (double) (pos.getZ() + 1)));
- 
-         if (!list.isEmpty()) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/ChorusFlowerBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/ChorusFlowerBlock.java.patch
deleted file mode 100644
index 88160b2d20..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/ChorusFlowerBlock.java.patch
+++ /dev/null
@@ -1,73 +0,0 @@
---- a/net/minecraft/world/level/block/ChorusFlowerBlock.java
-+++ b/net/minecraft/world/level/block/ChorusFlowerBlock.java
-@@ -23,6 +23,8 @@
- import net.minecraft.world.phys.BlockHitResult;
- import net.minecraft.world.phys.shapes.VoxelShape;
- 
-+import org.bukkit.craftbukkit.event.CraftEventFactory; // CraftBukkit
-+
- public class ChorusFlowerBlock extends Block {
- 
-     public static final MapCodec<ChorusFlowerBlock> CODEC = RecordCodecBuilder.mapCodec((instance) -> {
-@@ -103,8 +105,12 @@
-                 }
- 
-                 if (flag && ChorusFlowerBlock.allNeighborsEmpty(world, blockposition1, (Direction) null) && world.isEmptyBlock(pos.above(2))) {
--                    world.setBlock(pos, ChorusPlantBlock.getStateWithConnections(world, pos, this.plant.defaultBlockState()), 2);
--                    this.placeGrownFlower(world, blockposition1, i);
-+                    // CraftBukkit start - add event
-+                    if (CraftEventFactory.handleBlockSpreadEvent(world, pos, blockposition1, this.defaultBlockState().setValue(ChorusFlowerBlock.AGE, Integer.valueOf(i)), 2)) {
-+                        world.setBlock(pos, ChorusPlantBlock.getStateWithConnections(world, pos, this.plant.defaultBlockState()), 2);
-+                        this.placeGrownFlower(world, blockposition1, i);
-+                    }
-+                    // CraftBukkit end
-                 } else if (i < 4) {
-                     j = random.nextInt(4);
-                     if (flag1) {
-@@ -118,18 +124,30 @@
-                         BlockPos blockposition2 = pos.relative(enumdirection);
- 
-                         if (world.isEmptyBlock(blockposition2) && world.isEmptyBlock(blockposition2.below()) && ChorusFlowerBlock.allNeighborsEmpty(world, blockposition2, enumdirection.getOpposite())) {
--                            this.placeGrownFlower(world, blockposition2, i + 1);
--                            flag2 = true;
-+                            // CraftBukkit start - add event
-+                            if (CraftEventFactory.handleBlockSpreadEvent(world, pos, blockposition2, this.defaultBlockState().setValue(ChorusFlowerBlock.AGE, Integer.valueOf(i + 1)), 2)) {
-+                                this.placeGrownFlower(world, blockposition2, i + 1);
-+                                flag2 = true;
-+                            }
-+                            // CraftBukkit end
-                         }
-                     }
- 
-                     if (flag2) {
-                         world.setBlock(pos, ChorusPlantBlock.getStateWithConnections(world, pos, this.plant.defaultBlockState()), 2);
-                     } else {
--                        this.placeDeadFlower(world, pos);
-+                        // CraftBukkit start - add event
-+                        if (CraftEventFactory.handleBlockGrowEvent(world, pos, this.defaultBlockState().setValue(ChorusFlowerBlock.AGE, Integer.valueOf(5)), 2)) {
-+                            this.placeDeadFlower(world, pos);
-+                        }
-+                        // CraftBukkit end
-                     }
-                 } else {
--                    this.placeDeadFlower(world, pos);
-+                    // CraftBukkit start - add event
-+                    if (CraftEventFactory.handleBlockGrowEvent(world, pos, this.defaultBlockState().setValue(ChorusFlowerBlock.AGE, Integer.valueOf(5)), 2)) {
-+                        this.placeDeadFlower(world, pos);
-+                    }
-+                    // CraftBukkit end
-                 }
- 
-             }
-@@ -267,6 +285,11 @@
- 
-         if (world instanceof ServerLevel worldserver) {
-             if (projectile.mayInteract(worldserver, blockposition) && projectile.mayBreak(worldserver)) {
-+                // CraftBukkit
-+                if (!CraftEventFactory.callEntityChangeBlockEvent(projectile, blockposition, state.getFluidState().createLegacyBlock())) { // Paper - fix wrong block state
-+                    return;
-+                }
-+                // CraftBukkit end
-                 world.destroyBlock(blockposition, true, projectile);
-             }
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/CocoaBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/CocoaBlock.java.patch
deleted file mode 100644
index 963cb2ef7b..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/CocoaBlock.java.patch
+++ /dev/null
@@ -1,33 +0,0 @@
---- a/net/minecraft/world/level/block/CocoaBlock.java
-+++ b/net/minecraft/world/level/block/CocoaBlock.java
-@@ -20,6 +20,7 @@
- import net.minecraft.world.level.pathfinder.PathComputationType;
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+import org.bukkit.craftbukkit.event.CraftEventFactory; // CraftBukkit
- 
- public class CocoaBlock extends HorizontalDirectionalBlock implements BonemealableBlock {
- 
-@@ -57,11 +58,11 @@
- 
-     @Override
-     protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
--        if (world.random.nextInt(5) == 0) {
-+        if (world.random.nextFloat() < (world.spigotConfig.cocoaModifier / (100.0f * 5))) { // Spigot - SPIGOT-7159: Better modifier resolution
-             int i = (Integer) state.getValue(CocoaBlock.AGE);
- 
-             if (i < 2) {
--                world.setBlock(pos, (BlockState) state.setValue(CocoaBlock.AGE, i + 1), 2);
-+                CraftEventFactory.handleBlockGrowEvent(world, pos, (BlockState) state.setValue(CocoaBlock.AGE, i + 1), 2); // CraftBukkkit
-             }
-         }
- 
-@@ -131,7 +132,7 @@
- 
-     @Override
-     public void performBonemeal(ServerLevel world, RandomSource random, BlockPos pos, BlockState state) {
--        world.setBlock(pos, (BlockState) state.setValue(CocoaBlock.AGE, (Integer) state.getValue(CocoaBlock.AGE) + 1), 2);
-+        CraftEventFactory.handleBlockGrowEvent(world, pos, (BlockState) state.setValue(CocoaBlock.AGE, (Integer) state.getValue(CocoaBlock.AGE) + 1), 2); // CraftBukkit
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/CommandBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/CommandBlock.java.patch
deleted file mode 100644
index 6c6c7fbbd8..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/CommandBlock.java.patch
+++ /dev/null
@@ -1,37 +0,0 @@
---- a/net/minecraft/world/level/block/CommandBlock.java
-+++ b/net/minecraft/world/level/block/CommandBlock.java
-@@ -31,6 +31,8 @@
- import net.minecraft.world.phys.BlockHitResult;
- import org.slf4j.Logger;
- 
-+import org.bukkit.event.block.BlockRedstoneEvent; // CraftBukkit
-+
- public class CommandBlock extends BaseEntityBlock implements GameMasterBlock {
- 
-     public static final MapCodec<CommandBlock> CODEC = RecordCodecBuilder.mapCodec((instance) -> {
-@@ -78,7 +80,16 @@
- 
-     private void setPoweredAndUpdate(Level world, BlockPos pos, CommandBlockEntity blockEntity, boolean powered) {
-         boolean flag1 = blockEntity.isPowered();
-+        // CraftBukkit start
-+        org.bukkit.block.Block bukkitBlock = world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ());
-+        int old = flag1 ? 15 : 0;
-+        int current = powered ? 15 : 0;
- 
-+        BlockRedstoneEvent eventRedstone = new BlockRedstoneEvent(bukkitBlock, old, current);
-+        world.getCraftServer().getPluginManager().callEvent(eventRedstone);
-+        powered = eventRedstone.getNewCurrent() > 0;
-+        // CraftBukkit end
-+
-         if (powered != flag1) {
-             blockEntity.setPowered(powered);
-             if (powered) {
-@@ -141,7 +152,7 @@
-     protected InteractionResult useWithoutItem(BlockState state, Level world, BlockPos pos, Player player, BlockHitResult hit) {
-         BlockEntity tileentity = world.getBlockEntity(pos);
- 
--        if (tileentity instanceof CommandBlockEntity && player.canUseGameMasterBlocks()) {
-+        if (tileentity instanceof CommandBlockEntity && (player.canUseGameMasterBlocks() || (player.isCreative() && player.getBukkitEntity().hasPermission("minecraft.commandblock")))) { // Paper - command block permission
-             player.openCommandBlock((CommandBlockEntity) tileentity);
-             return InteractionResult.SUCCESS;
-         } else {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/ComparatorBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/ComparatorBlock.java.patch
deleted file mode 100644
index a59662f5c7..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/ComparatorBlock.java.patch
+++ /dev/null
@@ -1,39 +0,0 @@
---- a/net/minecraft/world/level/block/ComparatorBlock.java
-+++ b/net/minecraft/world/level/block/ComparatorBlock.java
-@@ -27,6 +27,7 @@
- import net.minecraft.world.phys.AABB;
- import net.minecraft.world.phys.BlockHitResult;
- import net.minecraft.world.ticks.TickPriority;
-+import org.bukkit.craftbukkit.event.CraftEventFactory; // CraftBukkit
- 
- public class ComparatorBlock extends DiodeBlock implements EntityBlock {
- 
-@@ -110,7 +111,8 @@
- 
-     @Nullable
-     private ItemFrame getItemFrame(Level world, Direction facing, BlockPos pos) {
--        List<ItemFrame> list = world.getEntitiesOfClass(ItemFrame.class, new AABB((double) pos.getX(), (double) pos.getY(), (double) pos.getZ(), (double) (pos.getX() + 1), (double) (pos.getY() + 1), (double) (pos.getZ() + 1)), (entityitemframe) -> {
-+        // CraftBukkit - decompile error
-+        List<ItemFrame> list = world.getEntitiesOfClass(ItemFrame.class, new AABB((double) pos.getX(), (double) pos.getY(), (double) pos.getZ(), (double) (pos.getX() + 1), (double) (pos.getY() + 1), (double) (pos.getZ() + 1)), (java.util.function.Predicate<ItemFrame>) (entityitemframe) -> {
-             return entityitemframe != null && entityitemframe.getDirection() == facing;
-         });
- 
-@@ -163,8 +165,18 @@
-             boolean flag1 = (Boolean) state.getValue(ComparatorBlock.POWERED);
- 
-             if (flag1 && !flag) {
-+                // CraftBukkit start
-+                if (CraftEventFactory.callRedstoneChange(world, pos, 15, 0).getNewCurrent() != 0) {
-+                    return;
-+                }
-+                // CraftBukkit end
-                 world.setBlock(pos, (BlockState) state.setValue(ComparatorBlock.POWERED, false), 2);
-             } else if (!flag1 && flag) {
-+                // CraftBukkit start
-+                if (CraftEventFactory.callRedstoneChange(world, pos, 0, 15).getNewCurrent() != 15) {
-+                    return;
-+                }
-+                // CraftBukkit end
-                 world.setBlock(pos, (BlockState) state.setValue(ComparatorBlock.POWERED, true), 2);
-             }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/ComposterBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/ComposterBlock.java.patch
deleted file mode 100644
index 919fb7f0cb..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/ComposterBlock.java.patch
+++ /dev/null
@@ -1,184 +0,0 @@
---- a/net/minecraft/world/level/block/ComposterBlock.java
-+++ b/net/minecraft/world/level/block/ComposterBlock.java
-@@ -41,6 +41,10 @@
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.Shapes;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.inventory.CraftBlockInventoryHolder;
-+import org.bukkit.craftbukkit.util.DummyGeneratorAccess;
-+// CraftBukkit end
- 
- public class ComposterBlock extends Block implements WorldlyContainerHolder {
- 
-@@ -241,6 +245,11 @@
-         if (i < 8 && ComposterBlock.COMPOSTABLES.containsKey(stack.getItem())) {
-             if (i < 7 && !world.isClientSide) {
-                 BlockState iblockdata1 = ComposterBlock.addItem(player, state, world, pos, stack);
-+                // Paper start - handle cancelled events
-+                if (iblockdata1 == null) {
-+                    return InteractionResult.PASS;
-+                }
-+                // Paper end
- 
-                 world.levelEvent(1500, pos, state != iblockdata1 ? 1 : 0);
-                 player.awardStat(Stats.ITEM_USED.get(stack.getItem()));
-@@ -269,7 +278,19 @@
-         int i = (Integer) state.getValue(ComposterBlock.LEVEL);
- 
-         if (i < 7 && ComposterBlock.COMPOSTABLES.containsKey(stack.getItem())) {
--            BlockState iblockdata1 = ComposterBlock.addItem(user, state, world, pos, stack);
-+            // CraftBukkit start
-+            double rand = world.getRandom().nextDouble();
-+            BlockState iblockdata1 = null; // Paper
-+            if (false && (state == iblockdata1 || !org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(user, pos, iblockdata1))) { // Paper - move event call into addItem
-+                return state;
-+            }
-+            iblockdata1 = ComposterBlock.addItem(user, state, world, pos, stack, rand);
-+            // Paper start - handle cancelled events
-+            if (iblockdata1 == null) {
-+                return state;
-+            }
-+            // Paper end
-+            // CraftBukkit end
- 
-             stack.shrink(1);
-             return iblockdata1;
-@@ -279,6 +300,14 @@
-     }
- 
-     public static BlockState extractProduce(Entity user, BlockState state, Level world, BlockPos pos) {
-+        // CraftBukkit start
-+        if (user != null && !(user instanceof Player)) {
-+            BlockState iblockdata1 = ComposterBlock.empty(user, state, DummyGeneratorAccess.INSTANCE, pos);
-+            if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(user, pos, iblockdata1)) {
-+                return state;
-+            }
-+        }
-+        // CraftBukkit end
-         if (!world.isClientSide) {
-             Vec3 vec3d = Vec3.atLowerCornerWithOffset(pos, 0.5D, 1.01D, 0.5D).offsetRandom(world.random, 0.7F);
-             ItemEntity entityitem = new ItemEntity(world, vec3d.x(), vec3d.y(), vec3d.z(), new ItemStack(Items.BONE_MEAL));
-@@ -301,20 +330,47 @@
-         return iblockdata1;
-     }
- 
-+    @Nullable // Paper
-     static BlockState addItem(@Nullable Entity user, BlockState state, LevelAccessor world, BlockPos pos, ItemStack stack) {
--        int i = (Integer) state.getValue(ComposterBlock.LEVEL);
--        float f = ComposterBlock.COMPOSTABLES.getFloat(stack.getItem());
-+        // CraftBukkit start
-+        return ComposterBlock.addItem(user, state, world, pos, stack, world.getRandom().nextDouble());
-+    }
- 
--        if ((i != 0 || f <= 0.0F) && world.getRandom().nextDouble() >= (double) f) {
--            return state;
--        } else {
--            int j = i + 1;
--            BlockState iblockdata1 = (BlockState) state.setValue(ComposterBlock.LEVEL, j);
-+    @Nullable // Paper - make it nullable
-+    static BlockState addItem(@Nullable Entity entity, BlockState iblockdata, LevelAccessor generatoraccess, BlockPos blockposition, ItemStack itemstack, double rand) {
-+        // CraftBukkit end
-+        int i = (Integer) iblockdata.getValue(ComposterBlock.LEVEL);
-+        float f = ComposterBlock.COMPOSTABLES.getFloat(itemstack.getItem());
- 
--            world.setBlock(pos, iblockdata1, 3);
--            world.gameEvent((Holder) GameEvent.BLOCK_CHANGE, pos, GameEvent.Context.of(user, iblockdata1));
-+        // Paper start - Add CompostItemEvent and EntityCompostItemEvent
-+        boolean willRaiseLevel = !((i != 0 || f <= 0.0F) && rand >= (double) f);
-+        final io.papermc.paper.event.block.CompostItemEvent event;
-+        if (entity == null) {
-+            event = new io.papermc.paper.event.block.CompostItemEvent(org.bukkit.craftbukkit.block.CraftBlock.at(generatoraccess, blockposition), itemstack.getBukkitStack(), willRaiseLevel);
-+        } else {
-+            event = new io.papermc.paper.event.entity.EntityCompostItemEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(generatoraccess, blockposition), itemstack.getBukkitStack(), willRaiseLevel);
-+        }
-+        if (!event.callEvent()) { // check for cancellation of entity event (non entity event can't be cancelled cause of hoppers)
-+            return null;
-+        }
-+        willRaiseLevel = event.willRaiseLevel();
-+
-+        if (!willRaiseLevel) {
-+            // Paper end - Add CompostItemEvent and EntityCompostItemEvent
-+            return iblockdata;
-+        } else {
-+            int j = i + 1;
-+            BlockState iblockdata1 = (BlockState) iblockdata.setValue(ComposterBlock.LEVEL, j);
-+            // Paper start - move the EntityChangeBlockEvent here to avoid conflict later for the compost events
-+            if (entity != null && !org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(entity, blockposition, iblockdata1)) {
-+                return null;
-+            }
-+            // Paper end
-+
-+            generatoraccess.setBlock(blockposition, iblockdata1, 3);
-+            generatoraccess.gameEvent((Holder) GameEvent.BLOCK_CHANGE, blockposition, GameEvent.Context.of(entity, iblockdata1));
-             if (j == 7) {
--                world.scheduleTick(pos, state.getBlock(), 20);
-+                generatoraccess.scheduleTick(blockposition, iblockdata.getBlock(), 20);
-             }
- 
-             return iblockdata1;
-@@ -354,7 +410,8 @@
-     public WorldlyContainer getContainer(BlockState state, LevelAccessor world, BlockPos pos) {
-         int i = (Integer) state.getValue(ComposterBlock.LEVEL);
- 
--        return (WorldlyContainer) (i == 8 ? new ComposterBlock.OutputContainer(state, world, pos, new ItemStack(Items.BONE_MEAL)) : (i < 7 ? new ComposterBlock.InputContainer(state, world, pos) : new ComposterBlock.EmptyContainer()));
-+        // CraftBukkit - empty generatoraccess, blockposition
-+        return (WorldlyContainer) (i == 8 ? new ComposterBlock.OutputContainer(state, world, pos, new ItemStack(Items.BONE_MEAL)) : (i < 7 ? new ComposterBlock.InputContainer(state, world, pos) : new ComposterBlock.EmptyContainer(world, pos)));
-     }
- 
-     public static class OutputContainer extends SimpleContainer implements WorldlyContainer {
-@@ -369,6 +426,7 @@
-             this.state = state;
-             this.level = world;
-             this.pos = pos;
-+            this.bukkitOwner = new CraftBlockInventoryHolder(world, pos, this); // CraftBukkit
-         }
- 
-         @Override
-@@ -393,8 +451,15 @@
- 
-         @Override
-         public void setChanged() {
-+            // CraftBukkit start - allow putting items back (eg cancelled InventoryMoveItemEvent)
-+            if (this.isEmpty()) {
-             ComposterBlock.empty((Entity) null, this.state, this.level, this.pos);
-             this.changed = true;
-+            } else {
-+                this.level.setBlock(this.pos, this.state, 3);
-+                this.changed = false;
-+            }
-+            // CraftBukkit end
-         }
-     }
- 
-@@ -407,6 +472,7 @@
- 
-         public InputContainer(BlockState state, LevelAccessor world, BlockPos pos) {
-             super(1);
-+            this.bukkitOwner = new CraftBlockInventoryHolder(world, pos, this); // CraftBukkit
-             this.state = state;
-             this.level = world;
-             this.pos = pos;
-@@ -439,6 +505,11 @@
-             if (!itemstack.isEmpty()) {
-                 this.changed = true;
-                 BlockState iblockdata = ComposterBlock.addItem((Entity) null, this.state, this.level, this.pos, itemstack);
-+                // Paper start - Add CompostItemEvent and EntityCompostItemEvent
-+                if (iblockdata == null) {
-+                    return;
-+                }
-+                // Paper end - Add CompostItemEvent and EntityCompostItemEvent
- 
-                 this.level.levelEvent(1500, this.pos, iblockdata != this.state ? 1 : 0);
-                 this.removeItemNoUpdate(0);
-@@ -449,8 +520,9 @@
- 
-     public static class EmptyContainer extends SimpleContainer implements WorldlyContainer {
- 
--        public EmptyContainer() {
-+        public EmptyContainer(LevelAccessor generatoraccess, BlockPos blockposition) { // CraftBukkit
-             super(0);
-+            this.bukkitOwner = new CraftBlockInventoryHolder(generatoraccess, blockposition, this); // CraftBukkit
-         }
- 
-         @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/ConcretePowderBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/ConcretePowderBlock.java.patch
deleted file mode 100644
index 5778abe978..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/ConcretePowderBlock.java.patch
+++ /dev/null
@@ -1,76 +0,0 @@
---- a/net/minecraft/world/level/block/ConcretePowderBlock.java
-+++ b/net/minecraft/world/level/block/ConcretePowderBlock.java
-@@ -15,6 +15,11 @@
- import net.minecraft.world.level.ScheduledTickAccess;
- import net.minecraft.world.level.block.state.BlockBehaviour;
- import net.minecraft.world.level.block.state.BlockState;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.block.CraftBlockState;
-+import org.bukkit.craftbukkit.block.CraftBlockStates;
-+import org.bukkit.event.block.BlockFormEvent;
-+// CraftBukkit end
- 
- public class ConcretePowderBlock extends FallingBlock {
- 
-@@ -38,7 +43,7 @@
-     @Override
-     public void onLand(Level world, BlockPos pos, BlockState fallingBlockState, BlockState currentStateInPos, FallingBlockEntity fallingBlockEntity) {
-         if (ConcretePowderBlock.shouldSolidify(world, pos, currentStateInPos)) {
--            world.setBlock(pos, this.concrete.defaultBlockState(), 3);
-+            org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(world, pos, this.concrete.defaultBlockState(), 3); // CraftBukkit
-         }
- 
-     }
-@@ -49,7 +54,24 @@
-         BlockPos blockposition = ctx.getClickedPos();
-         BlockState iblockdata = world.getBlockState(blockposition);
- 
--        return ConcretePowderBlock.shouldSolidify(world, blockposition, iblockdata) ? this.concrete.defaultBlockState() : super.getStateForPlacement(ctx);
-+        // CraftBukkit start
-+        if (!ConcretePowderBlock.shouldSolidify(world, blockposition, iblockdata)) {
-+            return super.getStateForPlacement(ctx);
-+        }
-+
-+        // TODO: An event factory call for methods like this
-+        CraftBlockState blockState = CraftBlockStates.getBlockState(world, blockposition);
-+        blockState.setData(this.concrete.defaultBlockState());
-+
-+        BlockFormEvent event = new BlockFormEvent(blockState.getBlock(), blockState);
-+        world.getServer().server.getPluginManager().callEvent(event);
-+
-+        if (!event.isCancelled()) {
-+            return blockState.getHandle();
-+        }
-+
-+        return super.getStateForPlacement(ctx);
-+        // CraftBukkit end
-     }
- 
-     private static boolean shouldSolidify(BlockGetter world, BlockPos pos, BlockState state) {
-@@ -85,7 +107,25 @@
- 
-     @Override
-     protected BlockState updateShape(BlockState state, LevelReader world, ScheduledTickAccess tickView, BlockPos pos, Direction direction, BlockPos neighborPos, BlockState neighborState, RandomSource random) {
--        return ConcretePowderBlock.touchesLiquid(world, pos) ? this.concrete.defaultBlockState() : super.updateShape(state, world, tickView, pos, direction, neighborPos, neighborState, random);
-+        // CraftBukkit start
-+        if (ConcretePowderBlock.touchesLiquid(world, pos)) {
-+            // Suppress during worldgen
-+            if (!(world instanceof Level world1)) {
-+                return this.concrete.defaultBlockState();
-+            }
-+            CraftBlockState blockState = CraftBlockStates.getBlockState(world1, pos);
-+            blockState.setData(this.concrete.defaultBlockState());
-+
-+            BlockFormEvent event = new BlockFormEvent(blockState.getBlock(), blockState);
-+            world1.getCraftServer().getPluginManager().callEvent(event);
-+
-+            if (!event.isCancelled()) {
-+                return blockState.getHandle();
-+            }
-+        }
-+
-+        return super.updateShape(state, world, tickView, pos, direction, neighborPos, neighborState, random);
-+        // CraftBukkit end
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/CrafterBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/CrafterBlock.java.patch
deleted file mode 100644
index b3f1ac312a..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/CrafterBlock.java.patch
+++ /dev/null
@@ -1,89 +0,0 @@
---- a/net/minecraft/world/level/block/CrafterBlock.java
-+++ b/net/minecraft/world/level/block/CrafterBlock.java
-@@ -12,6 +12,7 @@
- import net.minecraft.server.level.ServerLevel;
- import net.minecraft.server.level.ServerPlayer;
- import net.minecraft.util.RandomSource;
-+import net.minecraft.world.CompoundContainer;
- import net.minecraft.world.Container;
- import net.minecraft.world.Containers;
- import net.minecraft.world.InteractionResult;
-@@ -39,6 +40,12 @@
- import net.minecraft.world.phys.AABB;
- import net.minecraft.world.phys.BlockHitResult;
- import net.minecraft.world.phys.Vec3;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.event.block.CrafterCraftEvent;
-+import org.bukkit.event.inventory.InventoryMoveItemEvent;
-+import org.bukkit.inventory.Inventory;
-+// CraftBukkit end
- 
- public class CrafterBlock extends BaseEntityBlock {
- 
-@@ -189,6 +196,13 @@
-                 RecipeHolder<CraftingRecipe> recipeholder = (RecipeHolder) optional.get();
-                 ItemStack itemstack = ((CraftingRecipe) recipeholder.value()).assemble(craftinginput, world.registryAccess());
- 
-+                // CraftBukkit start
-+                CrafterCraftEvent event = CraftEventFactory.callCrafterCraftEvent(pos, world, crafterblockentity, itemstack, recipeholder);
-+                if (event.isCancelled()) {
-+                    return;
-+                }
-+                itemstack = CraftItemStack.asNMSCopy(event.getResult());
-+                // CraftBukkit end
-                 if (itemstack.isEmpty()) {
-                     world.levelEvent(1050, pos, 0);
-                 } else {
-@@ -227,7 +241,25 @@
-         ItemStack itemstack1 = stack.copy();
- 
-         if (iinventory != null && (iinventory instanceof CrafterBlockEntity || stack.getCount() > iinventory.getMaxStackSize(stack))) {
-+            // CraftBukkit start - InventoryMoveItemEvent
-+            CraftItemStack oitemstack = CraftItemStack.asCraftMirror(itemstack1);
-+
-+            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 {
-+                destinationInventory = iinventory.getOwner().getInventory();
-+            }
-+
-+            InventoryMoveItemEvent event = new InventoryMoveItemEvent(blockEntity.getOwner().getInventory(), oitemstack, destinationInventory, true);
-+            world.getCraftServer().getPluginManager().callEvent(event);
-+            itemstack1 = CraftItemStack.asNMSCopy(event.getItem());
-             while (!itemstack1.isEmpty()) {
-+                if (event.isCancelled()) {
-+                    break;
-+                }
-+                // CraftBukkit end
-                 ItemStack itemstack2 = itemstack1.copyWithCount(1);
-                 ItemStack itemstack3 = HopperBlockEntity.addItem(blockEntity, iinventory, itemstack2, enumdirection.getOpposite());
- 
-@@ -238,7 +270,25 @@
-                 itemstack1.shrink(1);
-             }
-         } else if (iinventory != null) {
-+            // CraftBukkit start - InventoryMoveItemEvent
-+            CraftItemStack oitemstack = CraftItemStack.asCraftMirror(itemstack1);
-+
-+            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 {
-+                destinationInventory = iinventory.getOwner().getInventory();
-+            }
-+
-+            InventoryMoveItemEvent event = new InventoryMoveItemEvent(blockEntity.getOwner().getInventory(), oitemstack, destinationInventory, true);
-+            world.getCraftServer().getPluginManager().callEvent(event);
-+            itemstack1 = CraftItemStack.asNMSCopy(event.getItem());
-             while (!itemstack1.isEmpty()) {
-+                if (event.isCancelled()) {
-+                    break;
-+                }
-+                // CraftBukkit end
-                 int i = itemstack1.getCount();
- 
-                 itemstack1 = HopperBlockEntity.addItem(blockEntity, iinventory, itemstack1, enumdirection.getOpposite());
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/CropBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/CropBlock.java.patch
deleted file mode 100644
index 4883d93884..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/CropBlock.java.patch
+++ /dev/null
@@ -1,59 +0,0 @@
---- a/net/minecraft/world/level/block/CropBlock.java
-+++ b/net/minecraft/world/level/block/CropBlock.java
-@@ -21,6 +21,7 @@
- import net.minecraft.world.level.block.state.properties.IntegerProperty;
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+import org.bukkit.craftbukkit.event.CraftEventFactory; // CraftBukkit
- 
- public class CropBlock extends BushBlock implements BonemealableBlock {
- 
-@@ -82,9 +83,26 @@
-             if (i < this.getMaxAge()) {
-                 float f = CropBlock.getGrowthSpeed(this, world, pos);
- 
--                if (random.nextInt((int) (25.0F / f) + 1) == 0) {
--                    world.setBlock(pos, this.getStateForAge(i + 1), 2);
-+                // Spigot start
-+                int modifier;
-+                if (this == Blocks.BEETROOTS) {
-+                    modifier = world.spigotConfig.beetrootModifier;
-+                } else if (this == Blocks.CARROTS) {
-+                    modifier = world.spigotConfig.carrotModifier;
-+                } else if (this == Blocks.POTATOES) {
-+                    modifier = world.spigotConfig.potatoModifier;
-+                // Paper start - Fix Spigot growth modifiers
-+                } else if (this == Blocks.TORCHFLOWER_CROP) {
-+                    modifier = world.spigotConfig.torchFlowerModifier;
-+                // Paper end - Fix Spigot growth modifiers
-+                } else {
-+                    modifier = world.spigotConfig.wheatModifier;
-                 }
-+
-+                if (random.nextFloat() < (modifier / (100.0f * (Math.floor((25.0F / f) + 1))))) { // Spigot - SPIGOT-7159: Better modifier resolution
-+                    // Spigot end
-+                    CraftEventFactory.handleBlockGrowEvent(world, pos, this.getStateForAge(i + 1), 2); // CraftBukkit
-+                }
-             }
-         }
- 
-@@ -98,7 +116,7 @@
-             i = j;
-         }
- 
--        world.setBlock(pos, this.getStateForAge(i), 2);
-+        CraftEventFactory.handleBlockGrowEvent(world, pos, this.getStateForAge(i), 2); // CraftBukkit
-     }
- 
-     protected int getBonemealAgeIncrease(Level world) {
-@@ -160,8 +178,9 @@
- 
-     @Override
-     protected void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
-+        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
-         if (world instanceof ServerLevel worldserver) {
--            if (entity instanceof Ravager && worldserver.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) {
-+            if (entity instanceof Ravager && CraftEventFactory.callEntityChangeBlockEvent(entity, pos, Blocks.AIR.defaultBlockState(), !worldserver.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING))) { // CraftBukkit
-                 worldserver.destroyBlock(pos, true, entity);
-             }
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/DaylightDetectorBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/DaylightDetectorBlock.java.patch
deleted file mode 100644
index 4631339b55..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/DaylightDetectorBlock.java.patch
+++ /dev/null
@@ -1,10 +0,0 @@
---- a/net/minecraft/world/level/block/DaylightDetectorBlock.java
-+++ b/net/minecraft/world/level/block/DaylightDetectorBlock.java
-@@ -74,6 +74,7 @@
- 
-         i = Mth.clamp(i, 0, 15);
-         if ((Integer) state.getValue(DaylightDetectorBlock.POWER) != i) {
-+            i = org.bukkit.craftbukkit.event.CraftEventFactory.callRedstoneChange(world, pos, ((Integer) state.getValue(DaylightDetectorBlock.POWER)), i).getNewCurrent(); // CraftBukkit - Call BlockRedstoneEvent
-             world.setBlock(pos, (BlockState) state.setValue(DaylightDetectorBlock.POWER, i), 3);
-         }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/DecoratedPotBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/DecoratedPotBlock.java.patch
deleted file mode 100644
index 6b148eba67..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/DecoratedPotBlock.java.patch
+++ /dev/null
@@ -1,14 +0,0 @@
---- a/net/minecraft/world/level/block/DecoratedPotBlock.java
-+++ b/net/minecraft/world/level/block/DecoratedPotBlock.java
-@@ -240,6 +240,11 @@
- 
-         if (world instanceof ServerLevel worldserver) {
-             if (projectile.mayInteract(worldserver, blockposition) && projectile.mayBreak(worldserver)) {
-+                // CraftBukkit start - call EntityChangeBlockEvent
-+                if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(projectile, blockposition, this.getFluidState(state).createLegacyBlock())) {
-+                    return;
-+                }
-+                // CraftBukkit end
-                 world.setBlock(blockposition, (BlockState) state.setValue(DecoratedPotBlock.CRACKED, true), 4);
-                 world.destroyBlock(blockposition, true, projectile);
-             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/DetectorRailBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/DetectorRailBlock.java.patch
deleted file mode 100644
index de7bae1c90..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/DetectorRailBlock.java.patch
+++ /dev/null
@@ -1,44 +0,0 @@
---- a/net/minecraft/world/level/block/DetectorRailBlock.java
-+++ b/net/minecraft/world/level/block/DetectorRailBlock.java
-@@ -26,6 +26,7 @@
- import net.minecraft.world.level.block.state.properties.RailShape;
- import net.minecraft.world.level.redstone.Orientation;
- import net.minecraft.world.phys.AABB;
-+import org.bukkit.event.block.BlockRedstoneEvent; // CraftBukkit
- 
- public class DetectorRailBlock extends BaseRailBlock {
- 
-@@ -51,6 +52,7 @@
- 
-     @Override
-     protected void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
-+        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
-         if (!world.isClientSide) {
-             if (!(Boolean) state.getValue(DetectorRailBlock.POWERED)) {
-                 this.checkPressed(world, pos, state);
-@@ -77,6 +79,7 @@
- 
-     private void checkPressed(Level world, BlockPos pos, BlockState state) {
-         if (this.canSurvive(state, world, pos)) {
-+            if (state.getBlock() != this) { return; } // Paper - Fix some rails connecting improperly
-             boolean flag = (Boolean) state.getValue(DetectorRailBlock.POWERED);
-             boolean flag1 = false;
-             List<AbstractMinecart> list = this.getInteractingMinecartOfType(world, pos, AbstractMinecart.class, (entity) -> {
-@@ -88,7 +91,17 @@
-             }
- 
-             BlockState iblockdata1;
-+            // CraftBukkit start
-+            if (flag != flag1) {
-+                org.bukkit.block.Block block = world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ());
- 
-+                BlockRedstoneEvent eventRedstone = new BlockRedstoneEvent(block, flag ? 15 : 0, flag1 ? 15 : 0);
-+                world.getCraftServer().getPluginManager().callEvent(eventRedstone);
-+
-+                flag1 = eventRedstone.getNewCurrent() > 0;
-+            }
-+            // CraftBukkit end
-+
-             if (flag1 && !flag) {
-                 iblockdata1 = (BlockState) state.setValue(DetectorRailBlock.POWERED, true);
-                 world.setBlock(pos, iblockdata1, 3);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/DiodeBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/DiodeBlock.java.patch
deleted file mode 100644
index 91c2eb8ada..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/DiodeBlock.java.patch
+++ /dev/null
@@ -1,29 +0,0 @@
---- a/net/minecraft/world/level/block/DiodeBlock.java
-+++ b/net/minecraft/world/level/block/DiodeBlock.java
-@@ -23,6 +23,7 @@
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.VoxelShape;
- import net.minecraft.world.ticks.TickPriority;
-+import org.bukkit.craftbukkit.event.CraftEventFactory; // CraftBukkit
- 
- public abstract class DiodeBlock extends HorizontalDirectionalBlock {
- 
-@@ -59,8 +60,18 @@
-             boolean flag1 = this.shouldTurnOn(world, pos, state);
- 
-             if (flag && !flag1) {
-+                // CraftBukkit start
-+                if (CraftEventFactory.callRedstoneChange(world, pos, 15, 0).getNewCurrent() != 0) {
-+                    return;
-+                }
-+                // CraftBukkit end
-                 world.setBlock(pos, (BlockState) state.setValue(DiodeBlock.POWERED, false), 2);
-             } else if (!flag) {
-+                // CraftBukkit start
-+                if (CraftEventFactory.callRedstoneChange(world, pos, 0, 15).getNewCurrent() != 15) {
-+                    return;
-+                }
-+                // CraftBukkit end
-                 world.setBlock(pos, (BlockState) state.setValue(DiodeBlock.POWERED, true), 2);
-                 if (!flag1) {
-                     world.scheduleTick(pos, (Block) this, this.getDelay(state), TickPriority.VERY_HIGH);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/DispenserBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/DispenserBlock.java.patch
deleted file mode 100644
index 71019ffd00..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/DispenserBlock.java.patch
+++ /dev/null
@@ -1,61 +0,0 @@
---- a/net/minecraft/world/level/block/DispenserBlock.java
-+++ b/net/minecraft/world/level/block/DispenserBlock.java
-@@ -52,6 +52,7 @@
-     private static final DefaultDispenseItemBehavior DEFAULT_BEHAVIOR = new DefaultDispenseItemBehavior();
-     public static final Map<Item, DispenseItemBehavior> DISPENSER_REGISTRY = new IdentityHashMap();
-     private static final int TRIGGER_DURATION = 4;
-+    public static boolean eventFired = false; // CraftBukkit
- 
-     @Override
-     public MapCodec<? extends DispenserBlock> codec() {
-@@ -79,8 +80,9 @@
-             if (tileentity instanceof DispenserBlockEntity) {
-                 DispenserBlockEntity tileentitydispenser = (DispenserBlockEntity) tileentity;
- 
--                player.openMenu(tileentitydispenser);
-+                if (player.openMenu(tileentitydispenser).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
-                 player.awardStat(tileentitydispenser instanceof DropperBlockEntity ? Stats.INSPECT_DROPPER : Stats.INSPECT_DISPENSER);
-+                } // Paper - Fix InventoryOpenEvent cancellation
-             }
-         }
- 
-@@ -88,7 +90,7 @@
-     }
- 
-     public void dispenseFrom(ServerLevel world, BlockState state, BlockPos pos) {
--        DispenserBlockEntity tileentitydispenser = (DispenserBlockEntity) world.getBlockEntity(pos, BlockEntityType.DISPENSER).orElse((Object) null);
-+        DispenserBlockEntity tileentitydispenser = (DispenserBlockEntity) world.getBlockEntity(pos, BlockEntityType.DISPENSER).orElse(null); // CraftBukkit - decompile error
- 
-         if (tileentitydispenser == null) {
-             DispenserBlock.LOGGER.warn("Ignoring dispensing attempt for Dispenser without matching block entity at {}", pos);
-@@ -97,13 +99,17 @@
-             int i = tileentitydispenser.getRandomSlot(world.random);
- 
-             if (i < 0) {
-+                if (org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFailedDispenseEvent(world, pos)) { // Paper - Add BlockFailedDispenseEvent
-                 world.levelEvent(1001, pos, 0);
-                 world.gameEvent((Holder) GameEvent.BLOCK_ACTIVATE, pos, GameEvent.Context.of(tileentitydispenser.getBlockState()));
-+                } // Paper - Add BlockFailedDispenseEvent
-             } else {
-                 ItemStack itemstack = tileentitydispenser.getItem(i);
-                 DispenseItemBehavior idispensebehavior = this.getDispenseMethod(world, itemstack);
- 
-                 if (idispensebehavior != DispenseItemBehavior.NOOP) {
-+                    if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockPreDispenseEvent(world, pos, itemstack, i)) return; // Paper - Add BlockPreDispenseEvent
-+                    DispenserBlock.eventFired = false; // CraftBukkit - reset event status
-                     tileentitydispenser.setItem(i, idispensebehavior.dispense(sourceblock, itemstack));
-                 }
- 
-@@ -111,6 +117,12 @@
-         }
-     }
- 
-+    // Paper start - Fix NPE with equippable and items without behavior
-+    public static DispenseItemBehavior getDispenseBehavior(BlockSource pointer, ItemStack stack) {
-+        return ((DispenserBlock) pointer.state().getBlock()).getDispenseMethod(pointer.level(), stack);
-+    }
-+    // Paper end - Fix NPE with equippable and items without behavior
-+
-     protected DispenseItemBehavior getDispenseMethod(Level world, ItemStack stack) {
-         if (!stack.isItemEnabled(world.enabledFeatures())) {
-             return DispenserBlock.DEFAULT_BEHAVIOR;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/DoorBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/DoorBlock.java.patch
deleted file mode 100644
index ea278f700f..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/DoorBlock.java.patch
+++ /dev/null
@@ -1,37 +0,0 @@
---- a/net/minecraft/world/level/block/DoorBlock.java
-+++ b/net/minecraft/world/level/block/DoorBlock.java
-@@ -38,6 +38,7 @@
- import net.minecraft.world.phys.Vec3;
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+import org.bukkit.event.block.BlockRedstoneEvent; // CraftBukkit
- 
- public class DoorBlock extends Block {
- 
-@@ -222,9 +223,24 @@
- 
-     @Override
-     protected void neighborChanged(BlockState state, Level world, BlockPos pos, Block sourceBlock, @Nullable Orientation wireOrientation, boolean notify) {
--        boolean flag1 = world.hasNeighborSignal(pos) || world.hasNeighborSignal(pos.relative(state.getValue(DoorBlock.HALF) == DoubleBlockHalf.LOWER ? Direction.UP : Direction.DOWN));
-+        // CraftBukkit start
-+        BlockPos otherHalf = pos.relative(state.getValue(DoorBlock.HALF) == DoubleBlockHalf.LOWER ? Direction.UP : Direction.DOWN);
- 
--        if (!this.defaultBlockState().is(sourceBlock) && flag1 != (Boolean) state.getValue(DoorBlock.POWERED)) {
-+        org.bukkit.World bworld = world.getWorld();
-+        org.bukkit.block.Block bukkitBlock = bworld.getBlockAt(pos.getX(), pos.getY(), pos.getZ());
-+        org.bukkit.block.Block blockTop = bworld.getBlockAt(otherHalf.getX(), otherHalf.getY(), otherHalf.getZ());
-+
-+        int power = bukkitBlock.getBlockPower();
-+        int powerTop = blockTop.getBlockPower();
-+        if (powerTop > power) power = powerTop;
-+        int oldPower = (Boolean) state.getValue(DoorBlock.POWERED) ? 15 : 0;
-+
-+        if (oldPower == 0 ^ power == 0) {
-+            BlockRedstoneEvent eventRedstone = new BlockRedstoneEvent(bukkitBlock, oldPower, power);
-+            world.getCraftServer().getPluginManager().callEvent(eventRedstone);
-+
-+            boolean flag1 = eventRedstone.getNewCurrent() > 0;
-+            // CraftBukkit end
-             if (flag1 != (Boolean) state.getValue(DoorBlock.OPEN)) {
-                 this.playSound((Entity) null, world, pos, flag1);
-                 world.gameEvent((Entity) null, (Holder) (flag1 ? GameEvent.BLOCK_OPEN : GameEvent.BLOCK_CLOSE), pos);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/DoublePlantBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/DoublePlantBlock.java.patch
deleted file mode 100644
index a59d6fc4f3..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/DoublePlantBlock.java.patch
+++ /dev/null
@@ -1,21 +0,0 @@
---- a/net/minecraft/world/level/block/DoublePlantBlock.java
-+++ b/net/minecraft/world/level/block/DoublePlantBlock.java
-@@ -98,11 +98,16 @@
-     }
- 
-     @Override
--    public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool) {
--        super.playerDestroy(world, player, pos, Blocks.AIR.defaultBlockState(), blockEntity, tool);
-+    public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool, boolean includeDrops, boolean dropExp) { // Paper - fix drops not preventing stats/food exhaustion
-+        super.playerDestroy(world, player, pos, Blocks.AIR.defaultBlockState(), blockEntity, tool, includeDrops, dropExp); // Paper - fix drops not preventing stats/food exhaustion
-     }
- 
-     protected static void preventDropFromBottomPart(Level world, BlockPos pos, BlockState state, Player player) {
-+        // CraftBukkit start
-+        if (((net.minecraft.server.level.ServerLevel)world).hasPhysicsEvent && org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(world, pos).isCancelled()) { // Paper
-+            return;
-+        }
-+        // CraftBukkit end
-         DoubleBlockHalf blockpropertydoubleblockhalf = (DoubleBlockHalf) state.getValue(DoublePlantBlock.HALF);
- 
-         if (blockpropertydoubleblockhalf == DoubleBlockHalf.UPPER) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/DragonEggBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/DragonEggBlock.java.patch
deleted file mode 100644
index b73b669baf..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/DragonEggBlock.java.patch
+++ /dev/null
@@ -1,29 +0,0 @@
---- a/net/minecraft/world/level/block/DragonEggBlock.java
-+++ b/net/minecraft/world/level/block/DragonEggBlock.java
-@@ -15,6 +15,7 @@
- import net.minecraft.world.phys.BlockHitResult;
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+import org.bukkit.event.block.BlockFromToEvent; // CraftBukkit
- 
- public class DragonEggBlock extends FallingBlock {
- 
-@@ -53,6 +54,18 @@
-             BlockPos blockposition1 = pos.offset(world.random.nextInt(16) - world.random.nextInt(16), world.random.nextInt(8) - world.random.nextInt(8), world.random.nextInt(16) - world.random.nextInt(16));
- 
-             if (world.getBlockState(blockposition1).isAir() && worldborder.isWithinBounds(blockposition1)) {
-+                // CraftBukkit start
-+                org.bukkit.block.Block from = world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ());
-+                org.bukkit.block.Block to = world.getWorld().getBlockAt(blockposition1.getX(), blockposition1.getY(), blockposition1.getZ());
-+                BlockFromToEvent event = new BlockFromToEvent(from, to);
-+                org.bukkit.Bukkit.getPluginManager().callEvent(event);
-+
-+                if (event.isCancelled()) {
-+                    return;
-+                }
-+
-+                blockposition1 = new BlockPos(event.getToBlock().getX(), event.getToBlock().getY(), event.getToBlock().getZ());
-+                // CraftBukkit end
-                 if (world.isClientSide) {
-                     for (int j = 0; j < 128; ++j) {
-                         double d0 = world.random.nextDouble();
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/DropperBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/DropperBlock.java.patch
deleted file mode 100644
index 0a8d6b5e2e..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/DropperBlock.java.patch
+++ /dev/null
@@ -1,75 +0,0 @@
---- a/net/minecraft/world/level/block/DropperBlock.java
-+++ b/net/minecraft/world/level/block/DropperBlock.java
-@@ -8,6 +8,7 @@
- import net.minecraft.core.dispenser.DefaultDispenseItemBehavior;
- import net.minecraft.core.dispenser.DispenseItemBehavior;
- import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.world.CompoundContainer;
- import net.minecraft.world.Container;
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.level.Level;
-@@ -19,12 +20,15 @@
- import net.minecraft.world.level.block.state.BlockBehaviour;
- import net.minecraft.world.level.block.state.BlockState;
- import org.slf4j.Logger;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.event.inventory.InventoryMoveItemEvent;
-+// CraftBukkit end
- 
- public class DropperBlock extends DispenserBlock {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
-     public static final MapCodec<DropperBlock> CODEC = simpleCodec(DropperBlock::new);
--    private static final DispenseItemBehavior DISPENSE_BEHAVIOUR = new DefaultDispenseItemBehavior();
-+    private static final DispenseItemBehavior DISPENSE_BEHAVIOUR = new DefaultDispenseItemBehavior(true); // CraftBukkit
- 
-     @Override
-     public MapCodec<DropperBlock> codec() {
-@@ -47,7 +51,7 @@
- 
-     @Override
-     public void dispenseFrom(ServerLevel world, BlockState state, BlockPos pos) {
--        DispenserBlockEntity tileentitydispenser = (DispenserBlockEntity) world.getBlockEntity(pos, BlockEntityType.DROPPER).orElse((Object) null);
-+        DispenserBlockEntity tileentitydispenser = (DispenserBlockEntity) world.getBlockEntity(pos, BlockEntityType.DROPPER).orElse(null); // CraftBukkit - decompile error
- 
-         if (tileentitydispenser == null) {
-             DropperBlock.LOGGER.warn("Ignoring dispensing attempt for Dropper without matching block entity at {}", pos);
-@@ -56,6 +60,7 @@
-             int i = tileentitydispenser.getRandomSlot(world.random);
- 
-             if (i < 0) {
-+                if (org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFailedDispenseEvent(world, pos)) // Paper - Add BlockFailedDispenseEvent
-                 world.levelEvent(1001, pos, 0);
-             } else {
-                 ItemStack itemstack = tileentitydispenser.getItem(i);
-@@ -66,10 +71,28 @@
-                     ItemStack itemstack1;
- 
-                     if (iinventory == null) {
-+                        if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockPreDispenseEvent(world, pos, itemstack, i)) return; // Paper - Add BlockPreDispenseEvent
-                         itemstack1 = DropperBlock.DISPENSE_BEHAVIOUR.dispense(sourceblock, itemstack);
-                     } else {
--                        itemstack1 = HopperBlockEntity.addItem(tileentitydispenser, iinventory, itemstack.copyWithCount(1), enumdirection.getOpposite());
--                        if (itemstack1.isEmpty()) {
-+                        // CraftBukkit start - Fire event when pushing items into other inventories
-+                        CraftItemStack oitemstack = CraftItemStack.asCraftMirror(itemstack.copyWithCount(1));
-+
-+                        org.bukkit.inventory.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 {
-+                            destinationInventory = iinventory.getOwner().getInventory();
-+                        }
-+
-+                        InventoryMoveItemEvent event = new InventoryMoveItemEvent(tileentitydispenser.getOwner().getInventory(), oitemstack, destinationInventory, true);
-+                        world.getCraftServer().getPluginManager().callEvent(event);
-+                        if (event.isCancelled()) {
-+                            return;
-+                        }
-+                        itemstack1 = HopperBlockEntity.addItem(tileentitydispenser, iinventory, CraftItemStack.asNMSCopy(event.getItem()), enumdirection.getOpposite());
-+                        if (event.getItem().equals(oitemstack) && itemstack1.isEmpty()) {
-+                            // CraftBukkit end
-                             itemstack1 = itemstack.copy();
-                             itemstack1.shrink(1);
-                         } else {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/EndGatewayBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/EndGatewayBlock.java.patch
deleted file mode 100644
index 2e2055c634..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/EndGatewayBlock.java.patch
+++ /dev/null
@@ -1,34 +0,0 @@
---- a/net/minecraft/world/level/block/EndGatewayBlock.java
-+++ b/net/minecraft/world/level/block/EndGatewayBlock.java
-@@ -22,6 +22,9 @@
- import net.minecraft.world.level.material.Fluid;
- import net.minecraft.world.level.portal.TeleportTransition;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.event.player.PlayerTeleportEvent;
-+// CraftBukkit end
- 
- public class EndGatewayBlock extends BaseEntityBlock implements Portal {
- 
-@@ -89,7 +92,12 @@
- 
-     @Override
-     protected void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
-+        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
-         if (entity.canUsePortal(false)) {
-+            // Paper start - call EntityPortalEnterEvent
-+            org.bukkit.event.entity.EntityPortalEnterEvent event = new org.bukkit.event.entity.EntityPortalEnterEvent(entity.getBukkitEntity(), new org.bukkit.Location(world.getWorld(), pos.getX(), pos.getY(), pos.getZ()), org.bukkit.PortalType.END_GATEWAY); // Paper - add portal type
-+            if (!event.callEvent()) return;
-+            // Paper end - call EntityPortalEnterEvent
-             BlockEntity tileentity = world.getBlockEntity(pos);
- 
-             if (!world.isClientSide && tileentity instanceof TheEndGatewayBlockEntity) {
-@@ -112,7 +120,7 @@
-         if (tileentity instanceof TheEndGatewayBlockEntity tileentityendgateway) {
-             Vec3 vec3d = tileentityendgateway.getPortalPosition(world, pos);
- 
--            return vec3d == null ? null : (entity instanceof ThrownEnderpearl ? new TeleportTransition(world, vec3d, Vec3.ZERO, 0.0F, 0.0F, Set.of(), TeleportTransition.PLACE_PORTAL_TICKET) : new TeleportTransition(world, vec3d, Vec3.ZERO, 0.0F, 0.0F, Relative.union(Relative.DELTA, Relative.ROTATION), TeleportTransition.PLACE_PORTAL_TICKET));
-+            return vec3d == null ? null : (entity instanceof ThrownEnderpearl ? new TeleportTransition(world, vec3d, Vec3.ZERO, 0.0F, 0.0F, Set.of(), TeleportTransition.PLACE_PORTAL_TICKET, PlayerTeleportEvent.TeleportCause.END_GATEWAY) : new TeleportTransition(world, vec3d, Vec3.ZERO, 0.0F, 0.0F, Relative.union(Relative.DELTA, Relative.ROTATION), TeleportTransition.PLACE_PORTAL_TICKET, PlayerTeleportEvent.TeleportCause.END_GATEWAY)); // CraftBukkit
-         } else {
-             return null;
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/EndPortalBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/EndPortalBlock.java.patch
deleted file mode 100644
index c075ce1cd8..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/EndPortalBlock.java.patch
+++ /dev/null
@@ -1,91 +0,0 @@
---- a/net/minecraft/world/level/block/EndPortalBlock.java
-+++ b/net/minecraft/world/level/block/EndPortalBlock.java
-@@ -19,12 +19,23 @@
- import net.minecraft.world.level.block.entity.TheEndPortalBlockEntity;
- import net.minecraft.world.level.block.state.BlockBehaviour;
- import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.world.level.dimension.LevelStem;
- import net.minecraft.world.level.levelgen.feature.EndPlatformFeature;
- import net.minecraft.world.level.material.Fluid;
- import net.minecraft.world.level.portal.TeleportTransition;
- import net.minecraft.world.phys.Vec3;
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+// CraftBukkit start
-+import java.util.List;
-+import org.bukkit.Location;
-+import org.bukkit.craftbukkit.CraftWorld;
-+import org.bukkit.craftbukkit.event.CraftPortalEvent;
-+import org.bukkit.craftbukkit.util.CraftLocation;
-+import org.bukkit.event.entity.EntityPortalEnterEvent;
-+import org.bukkit.event.player.PlayerRespawnEvent;
-+import org.bukkit.event.player.PlayerTeleportEvent;
-+// CraftBukkit end
- 
- public class EndPortalBlock extends BaseEntityBlock implements Portal {
- 
-@@ -57,10 +68,17 @@
- 
-     @Override
-     protected void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
-+        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
-         if (entity.canUsePortal(false)) {
-+            // CraftBukkit start - Entity in portal
-+            EntityPortalEnterEvent event = new EntityPortalEnterEvent(entity.getBukkitEntity(), new org.bukkit.Location(world.getWorld(), pos.getX(), pos.getY(), pos.getZ()), org.bukkit.PortalType.ENDER); // Paper - add portal type
-+            world.getCraftServer().getPluginManager().callEvent(event);
-+            if (event.isCancelled()) return; // Paper - make cancellable
-+            // CraftBukkit end
-             if (!world.isClientSide && world.dimension() == Level.END && entity instanceof ServerPlayer) {
-                 ServerPlayer entityplayer = (ServerPlayer) entity;
- 
-+                if (world.paperConfig().misc.disableEndCredits) entityplayer.seenCredits = true; // Paper - Option to disable end credits
-                 if (!entityplayer.seenCredits) {
-                     entityplayer.showEndCredits();
-                     return;
-@@ -74,11 +92,11 @@
- 
-     @Override
-     public TeleportTransition getPortalDestination(ServerLevel world, Entity entity, BlockPos pos) {
--        ResourceKey<Level> resourcekey = world.dimension() == Level.END ? Level.OVERWORLD : Level.END;
-+        ResourceKey<Level> resourcekey = world.getTypeKey() == LevelStem.END ? Level.OVERWORLD : Level.END; // CraftBukkit - SPIGOT-6152: send back to main overworld in custom ends
-         ServerLevel worldserver1 = world.getServer().getLevel(resourcekey);
- 
-         if (worldserver1 == null) {
--            return null;
-+            return null; // Paper - keep previous behavior of not firing PlayerTeleportEvent if the target world doesn't exist
-         } else {
-             boolean flag = resourcekey == Level.END;
-             BlockPos blockposition1 = flag ? ServerLevel.END_SPAWN_POINT : worldserver1.getSharedSpawnPos();
-@@ -87,7 +105,7 @@
-             Set set;
- 
-             if (flag) {
--                EndPlatformFeature.createEndPlatform(worldserver1, BlockPos.containing(vec3d).below(), true);
-+                EndPlatformFeature.createEndPlatform(worldserver1, BlockPos.containing(vec3d).below(), true, entity); // CraftBukkit
-                 f = Direction.WEST.toYRot();
-                 set = Relative.union(Relative.DELTA, Set.of(Relative.X_ROT));
-                 if (entity instanceof ServerPlayer) {
-@@ -99,13 +117,21 @@
-                 if (entity instanceof ServerPlayer) {
-                     ServerPlayer entityplayer = (ServerPlayer) entity;
- 
--                    return entityplayer.findRespawnPositionAndUseSpawnBlock(false, TeleportTransition.DO_NOTHING);
-+                    return entityplayer.findRespawnPositionAndUseSpawnBlock(false, TeleportTransition.DO_NOTHING, PlayerRespawnEvent.RespawnReason.END_PORTAL); // CraftBukkit
-                 }
- 
-                 vec3d = entity.adjustSpawnLocation(worldserver1, blockposition1).getBottomCenter();
-             }
- 
--            return new TeleportTransition(worldserver1, vec3d, Vec3.ZERO, f, 0.0F, set, TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET));
-+            // CraftBukkit start
-+            CraftPortalEvent event = entity.callPortalEvent(entity, CraftLocation.toBukkit(vec3d, worldserver1.getWorld(), f, entity.getXRot()), PlayerTeleportEvent.TeleportCause.END_PORTAL, 0, 0);
-+            if (event == null) {
-+                return null;
-+            }
-+            Location to = event.getTo();
-+
-+            return new TeleportTransition(((CraftWorld) to.getWorld()).getHandle(), CraftLocation.toVec3D(to), entity.getDeltaMovement(), to.getYaw(), to.getPitch(), set, TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET), PlayerTeleportEvent.TeleportCause.END_PORTAL);
-+            // CraftBukkit end
-         }
-     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/EnderChestBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/EnderChestBlock.java.patch
deleted file mode 100644
index 0b1e52d838..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/EnderChestBlock.java.patch
+++ /dev/null
@@ -1,25 +0,0 @@
---- a/net/minecraft/world/level/block/EnderChestBlock.java
-+++ b/net/minecraft/world/level/block/EnderChestBlock.java
-@@ -78,14 +78,16 @@
-         PlayerEnderChestContainer playerEnderChestContainer = player.getEnderChestInventory();
-         if (playerEnderChestContainer != null && world.getBlockEntity(pos) instanceof EnderChestBlockEntity enderChestBlockEntity) {
-             BlockPos blockPos = pos.above();
--            if (world.getBlockState(blockPos).isRedstoneConductor(world, blockPos)) {
-+            if (world.getBlockState(blockPos).isRedstoneConductor(world, blockPos)) { // Paper - diff on change; make sure that EnderChest#isBlocked uses the same logic
-                 return InteractionResult.SUCCESS;
-             } else {
--                if (world instanceof ServerLevel serverLevel) {
--                    playerEnderChestContainer.setActiveChest(enderChestBlockEntity);
--                    player.openMenu(
--                        new SimpleMenuProvider((i, inventory, playerx) -> ChestMenu.threeRows(i, inventory, playerEnderChestContainer), CONTAINER_TITLE)
--                    );
-+                // Paper start - Fix InventoryOpenEvent cancellation - moved up;
-+                playerEnderChestContainer.setActiveChest(enderChestBlockEntity); // Needs to happen before ChestMenu.threeRows as it is required for opening animations
-+                if (world instanceof ServerLevel serverLevel && player.openMenu(
-+                    new SimpleMenuProvider((i, inventory, playerx) -> ChestMenu.threeRows(i, inventory, playerEnderChestContainer), CONTAINER_TITLE)
-+                ).isPresent()) {
-+                // Paper end - Fix InventoryOpenEvent cancellation - moved up;
-+                    // Paper - Fix InventoryOpenEvent cancellation - moved up;
-                     player.awardStat(Stats.OPEN_ENDERCHEST);
-                     PiglinAi.angerNearbyPiglins(serverLevel, player, true);
-                 }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/FarmBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/FarmBlock.java.patch
deleted file mode 100644
index d862f84ba3..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/FarmBlock.java.patch
+++ /dev/null
@@ -1,112 +0,0 @@
---- a/net/minecraft/world/level/block/FarmBlock.java
-+++ b/net/minecraft/world/level/block/FarmBlock.java
-@@ -29,6 +29,10 @@
- import net.minecraft.world.level.pathfinder.PathComputationType;
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityInteractEvent;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+// CraftBukkit end
- 
- public class FarmBlock extends Block {
- 
-@@ -89,31 +93,56 @@
-     @Override
-     protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
-         int i = (Integer) state.getValue(FarmBlock.MOISTURE);
-+        if (i > 0 && world.paperConfig().tickRates.wetFarmland != 1 && (world.paperConfig().tickRates.wetFarmland < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % world.paperConfig().tickRates.wetFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks
-+        if (i == 0 && world.paperConfig().tickRates.dryFarmland != 1 && (world.paperConfig().tickRates.dryFarmland < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % world.paperConfig().tickRates.dryFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks
- 
-         if (!FarmBlock.isNearWater(world, pos) && !world.isRainingAt(pos.above())) {
-             if (i > 0) {
--                world.setBlock(pos, (BlockState) state.setValue(FarmBlock.MOISTURE, i - 1), 2);
-+                org.bukkit.craftbukkit.event.CraftEventFactory.handleMoistureChangeEvent(world, pos, (BlockState) state.setValue(FarmBlock.MOISTURE, i - 1), 2); // CraftBukkit
-             } else if (!FarmBlock.shouldMaintainFarmland(world, pos)) {
-                 FarmBlock.turnToDirt((Entity) null, state, world, pos);
-             }
-         } else if (i < 7) {
--            world.setBlock(pos, (BlockState) state.setValue(FarmBlock.MOISTURE, 7), 2);
-+            org.bukkit.craftbukkit.event.CraftEventFactory.handleMoistureChangeEvent(world, pos, (BlockState) state.setValue(FarmBlock.MOISTURE, 7), 2); // CraftBukkit
-         }
- 
-     }
- 
-     @Override
-     public void fallOn(Level world, BlockState state, BlockPos pos, Entity entity, float fallDistance) {
-+        super.fallOn(world, state, pos, entity, fallDistance); // CraftBukkit - moved here as game rules / events shouldn't affect fall damage.
-         if (world instanceof ServerLevel worldserver) {
-             if (world.random.nextFloat() < fallDistance - 0.5F && entity instanceof LivingEntity && (entity instanceof Player || worldserver.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) && entity.getBbWidth() * entity.getBbWidth() * entity.getBbHeight() > 0.512F) {
-+                // CraftBukkit start - Interact soil
-+                org.bukkit.event.Cancellable cancellable;
-+                if (entity instanceof Player) {
-+                    cancellable = CraftEventFactory.callPlayerInteractEvent((Player) entity, org.bukkit.event.block.Action.PHYSICAL, pos, null, null, null);
-+                } else {
-+                    cancellable = new EntityInteractEvent(entity.getBukkitEntity(), world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ()));
-+                    world.getCraftServer().getPluginManager().callEvent((EntityInteractEvent) cancellable);
-+                }
-+
-+                if (cancellable.isCancelled()) {
-+                    return;
-+                }
-+
-+                if (!CraftEventFactory.callEntityChangeBlockEvent(entity, pos, Blocks.DIRT.defaultBlockState())) {
-+                    return;
-+                }
-+                // CraftBukkit end
-                 FarmBlock.turnToDirt(entity, state, world, pos);
-             }
-         }
- 
--        super.fallOn(world, state, pos, entity, fallDistance);
-+        // super.fallOn(world, iblockdata, blockposition, entity, f); // CraftBukkit - moved up
-     }
- 
-     public static void turnToDirt(@Nullable Entity entity, BlockState state, Level world, BlockPos pos) {
-+        // CraftBukkit start
-+        if (CraftEventFactory.callBlockFadeEvent(world, pos, Blocks.DIRT.defaultBlockState()).isCancelled()) {
-+            return;
-+        }
-+        // CraftBukkit end
-         BlockState iblockdata1 = pushEntitiesUp(state, Blocks.DIRT.defaultBlockState(), world, pos);
- 
-         world.setBlockAndUpdate(pos, iblockdata1);
-@@ -125,19 +154,28 @@
-     }
- 
-     private static boolean isNearWater(LevelReader world, BlockPos pos) {
--        Iterator iterator = BlockPos.betweenClosed(pos.offset(-4, 0, -4), pos.offset(4, 1, 4)).iterator();
-+        // Paper start - Perf: remove abstract block iteration
-+        int xOff = pos.getX();
-+        int yOff = pos.getY();
-+        int zOff = pos.getZ();
- 
--        BlockPos blockposition1;
--
--        do {
--            if (!iterator.hasNext()) {
--                return false;
-+        for (int dz = -4; dz <= 4; ++dz) {
-+            int z = dz + zOff;
-+            for (int dx = -4; dx <= 4; ++dx) {
-+                int x = xOff + dx;
-+                for (int dy = 0; dy <= 1; ++dy) {
-+                    int y = dy + yOff;
-+                    net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)world.getChunk(x >> 4, z >> 4);
-+                    net.minecraft.world.level.material.FluidState fluid = chunk.getBlockStateFinal(x, y, z).getFluidState();
-+                    if (fluid.is(FluidTags.WATER)) {
-+                        return true;
-+                    }
-+                }
-             }
-+        }
- 
--            blockposition1 = (BlockPos) iterator.next();
--        } while (!world.getFluidState(blockposition1).is(FluidTags.WATER));
--
--        return true;
-+        return false;
-+        // Paper end - Perf: remove abstract block iteration
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/FenceGateBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/FenceGateBlock.java.patch
deleted file mode 100644
index 78872a0616..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/FenceGateBlock.java.patch
+++ /dev/null
@@ -1,20 +0,0 @@
---- a/net/minecraft/world/level/block/FenceGateBlock.java
-+++ b/net/minecraft/world/level/block/FenceGateBlock.java
-@@ -173,6 +173,17 @@
-     protected void neighborChanged(BlockState state, Level world, BlockPos pos, Block sourceBlock, @Nullable Orientation wireOrientation, boolean notify) {
-         if (!world.isClientSide) {
-             boolean flag1 = world.hasNeighborSignal(pos);
-+            // CraftBukkit start
-+            boolean oldPowered = state.getValue(FenceGateBlock.POWERED);
-+            if (oldPowered != flag1) {
-+                int newPower = flag1 ? 15 : 0;
-+                int oldPower = oldPowered ? 15 : 0;
-+                org.bukkit.block.Block bukkitBlock = org.bukkit.craftbukkit.block.CraftBlock.at(world, pos);
-+                org.bukkit.event.block.BlockRedstoneEvent eventRedstone = new org.bukkit.event.block.BlockRedstoneEvent(bukkitBlock, oldPower, newPower);
-+                world.getCraftServer().getPluginManager().callEvent(eventRedstone);
-+                flag1 = eventRedstone.getNewCurrent() > 0;
-+            }
-+            // CraftBukkit end
- 
-             if ((Boolean) state.getValue(FenceGateBlock.POWERED) != flag1) {
-                 world.setBlock(pos, (BlockState) ((BlockState) state.setValue(FenceGateBlock.POWERED, flag1)).setValue(FenceGateBlock.OPEN, flag1), 2);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/FireBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/FireBlock.java.patch
deleted file mode 100644
index 5d840efc2e..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/FireBlock.java.patch
+++ /dev/null
@@ -1,205 +0,0 @@
---- a/net/minecraft/world/level/block/FireBlock.java
-+++ b/net/minecraft/world/level/block/FireBlock.java
-@@ -14,6 +14,7 @@
- import net.minecraft.tags.BiomeTags;
- import net.minecraft.util.RandomSource;
- import net.minecraft.world.item.context.BlockPlaceContext;
-+import net.minecraft.world.item.context.UseOnContext;
- import net.minecraft.world.level.BlockGetter;
- import net.minecraft.world.level.GameRules;
- import net.minecraft.world.level.Level;
-@@ -28,6 +29,12 @@
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.Shapes;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+import org.bukkit.craftbukkit.block.CraftBlockState;
-+import org.bukkit.craftbukkit.block.CraftBlockStates;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.block.BlockBurnEvent;
-+import org.bukkit.event.block.BlockFadeEvent;
-+// CraftBukkit end
- 
- public class FireBlock extends BaseFireBlock {
- 
-@@ -100,7 +107,25 @@
- 
-     @Override
-     protected BlockState updateShape(BlockState state, LevelReader world, ScheduledTickAccess tickView, BlockPos pos, Direction direction, BlockPos neighborPos, BlockState neighborState, RandomSource random) {
--        return this.canSurvive(state, world, pos) ? this.getStateWithAge(world, pos, (Integer) state.getValue(FireBlock.AGE)) : Blocks.AIR.defaultBlockState();
-+        // CraftBukkit start
-+        if (!(world instanceof ServerLevel)) return this.canSurvive(state, world, pos) ? (BlockState) this.getStateWithAge(world, pos, (Integer) state.getValue(FireBlock.AGE)) : Blocks.AIR.defaultBlockState(); // Paper - don't fire events in world generation
-+        if (!this.canSurvive(state, world, pos)) {
-+            // Suppress during worldgen
-+            if (!(world instanceof Level world1)) {
-+                return Blocks.AIR.defaultBlockState();
-+            }
-+            CraftBlockState blockState = CraftBlockStates.getBlockState(world1, pos);
-+            blockState.setData(Blocks.AIR.defaultBlockState());
-+
-+            BlockFadeEvent event = new BlockFadeEvent(blockState.getBlock(), blockState);
-+            world1.getCraftServer().getPluginManager().callEvent(event);
-+
-+            if (!event.isCancelled()) {
-+                return blockState.getHandle();
-+            }
-+        }
-+        return this.getStateWithAge(world, pos, (Integer) state.getValue(FireBlock.AGE)); // Paper - don't fire events in world generation; diff on change, see "don't fire events in world generation"
-+        // CraftBukkit end
-     }
- 
-     @Override
-@@ -146,10 +171,10 @@
- 
-     @Override
-     protected void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
--        world.scheduleTick(pos, (Block) this, FireBlock.getFireTickDelay(world.random));
-+        world.scheduleTick(pos, (Block) this, FireBlock.getFireTickDelay(world)); // Paper - Add fire-tick-delay option
-         if (world.getGameRules().getBoolean(GameRules.RULE_DOFIRETICK)) {
-             if (!state.canSurvive(world, pos)) {
--                world.removeBlock(pos, false);
-+                this.fireExtinguished(world, pos); // CraftBukkit - invalid place location
-             }
- 
-             BlockState iblockdata1 = world.getBlockState(pos.below());
-@@ -157,7 +182,7 @@
-             int i = (Integer) state.getValue(FireBlock.AGE);
- 
-             if (!flag && world.isRaining() && this.isNearRain(world, pos) && random.nextFloat() < 0.2F + (float) i * 0.03F) {
--                world.removeBlock(pos, false);
-+                this.fireExtinguished(world, pos); // CraftBukkit - extinguished by rain
-             } else {
-                 int j = Math.min(15, i + random.nextInt(3) / 2);
- 
-@@ -171,14 +196,14 @@
-                         BlockPos blockposition1 = pos.below();
- 
-                         if (!world.getBlockState(blockposition1).isFaceSturdy(world, blockposition1, Direction.UP) || i > 3) {
--                            world.removeBlock(pos, false);
-+                            this.fireExtinguished(world, pos); // CraftBukkit
-                         }
- 
-                         return;
-                     }
- 
-                     if (i == 15 && random.nextInt(4) == 0 && !this.canBurn(world.getBlockState(pos.below()))) {
--                        world.removeBlock(pos, false);
-+                        this.fireExtinguished(world, pos); // CraftBukkit
-                         return;
-                     }
-                 }
-@@ -186,12 +211,14 @@
-                 boolean flag1 = world.getBiome(pos).is(BiomeTags.INCREASED_FIRE_BURNOUT);
-                 int k = flag1 ? -50 : 0;
- 
--                this.checkBurnOut(world, pos.east(), 300 + k, random, i);
--                this.checkBurnOut(world, pos.west(), 300 + k, random, i);
--                this.checkBurnOut(world, pos.below(), 250 + k, random, i);
--                this.checkBurnOut(world, pos.above(), 250 + k, random, i);
--                this.checkBurnOut(world, pos.north(), 300 + k, random, i);
--                this.checkBurnOut(world, pos.south(), 300 + k, random, i);
-+                // CraftBukkit start - add source blockposition to burn calls
-+                this.trySpread(world, pos.east(), 300 + k, random, i, pos);
-+                this.trySpread(world, pos.west(), 300 + k, random, i, pos);
-+                this.trySpread(world, pos.below(), 250 + k, random, i, pos);
-+                this.trySpread(world, pos.above(), 250 + k, random, i, pos);
-+                this.trySpread(world, pos.north(), 300 + k, random, i, pos);
-+                this.trySpread(world, pos.south(), 300 + k, random, i, pos);
-+                // CraftBukkit end
-                 BlockPos.MutableBlockPos blockposition_mutableblockposition = new BlockPos.MutableBlockPos();
- 
-                 for (int l = -1; l <= 1; ++l) {
-@@ -217,7 +244,15 @@
-                                     if (i2 > 0 && random.nextInt(k1) <= i2 && (!world.isRaining() || !this.isNearRain(world, blockposition_mutableblockposition))) {
-                                         int j2 = Math.min(15, i + random.nextInt(5) / 4);
- 
--                                        world.setBlock(blockposition_mutableblockposition, this.getStateWithAge(world, blockposition_mutableblockposition, j2), 3);
-+                                        // CraftBukkit start - Call to stop spread of fire
-+                                        if (world.getBlockState(blockposition_mutableblockposition).getBlock() != Blocks.FIRE) {
-+                                            if (CraftEventFactory.callBlockIgniteEvent(world, blockposition_mutableblockposition, pos).isCancelled()) {
-+                                                continue;
-+                                            }
-+
-+                                            CraftEventFactory.handleBlockSpreadEvent(world, pos, blockposition_mutableblockposition, this.getStateWithAge(world, blockposition_mutableblockposition, j2), 3); // CraftBukkit
-+                                        }
-+                                        // CraftBukkit end
-                                     }
-                                 }
-                             }
-@@ -241,24 +276,47 @@
-         return state.hasProperty(BlockStateProperties.WATERLOGGED) && (Boolean) state.getValue(BlockStateProperties.WATERLOGGED) ? 0 : this.igniteOdds.getInt(state.getBlock());
-     }
- 
--    private void checkBurnOut(Level world, BlockPos pos, int spreadFactor, RandomSource random, int currentAge) {
--        int k = this.getBurnOdds(world.getBlockState(pos));
-+    private void trySpread(Level world, BlockPos blockposition, int i, RandomSource randomsource, int j, BlockPos sourceposition) { // CraftBukkit add sourceposition
-+        int k = this.getBurnOdds(world.getBlockState(blockposition));
- 
--        if (random.nextInt(spreadFactor) < k) {
--            BlockState iblockdata = world.getBlockState(pos);
-+        if (randomsource.nextInt(i) < k) {
-+            BlockState iblockdata = world.getBlockState(blockposition);
- 
--            if (random.nextInt(currentAge + 10) < 5 && !world.isRainingAt(pos)) {
--                int l = Math.min(currentAge + random.nextInt(5) / 4, 15);
-+            // CraftBukkit start
-+            org.bukkit.block.Block theBlock = world.getWorld().getBlockAt(blockposition.getX(), blockposition.getY(), blockposition.getZ());
-+            org.bukkit.block.Block sourceBlock = world.getWorld().getBlockAt(sourceposition.getX(), sourceposition.getY(), sourceposition.getZ());
- 
--                world.setBlock(pos, this.getStateWithAge(world, pos, l), 3);
-+            BlockBurnEvent event = new BlockBurnEvent(theBlock, sourceBlock);
-+            world.getCraftServer().getPluginManager().callEvent(event);
-+
-+            if (event.isCancelled()) {
-+                return;
-+            }
-+
-+            if (iblockdata.getBlock() instanceof TntBlock && !CraftEventFactory.callTNTPrimeEvent(world, blockposition, org.bukkit.event.block.TNTPrimeEvent.PrimeCause.FIRE, null, sourceposition)) {
-+                return;
-+            }
-+            // CraftBukkit end
-+
-+            if (randomsource.nextInt(j + 10) < 5 && !world.isRainingAt(blockposition)) {
-+                int l = Math.min(j + randomsource.nextInt(5) / 4, 15);
-+
-+                world.setBlock(blockposition, this.getStateWithAge(world, blockposition, l), 3);
-             } else {
--                world.removeBlock(pos, false);
-+                if(iblockdata.getBlock() != Blocks.TNT) world.removeBlock(blockposition, false); // Paper - TNTPrimeEvent; We might be cancelling it below, move the setAir down
-             }
- 
-             Block block = iblockdata.getBlock();
- 
-             if (block instanceof TntBlock) {
--                TntBlock.explode(world, pos);
-+                // Paper start - TNTPrimeEvent
-+                org.bukkit.block.Block tntBlock = org.bukkit.craftbukkit.block.CraftBlock.at(world, blockposition);
-+                if (!new com.destroystokyo.paper.event.block.TNTPrimeEvent(tntBlock, com.destroystokyo.paper.event.block.TNTPrimeEvent.PrimeReason.FIRE, null).callEvent()) {
-+                    return;
-+                }
-+                world.removeBlock(blockposition, false);
-+                // Paper end - TNTPrimeEvent
-+                TntBlock.explode(world, blockposition);
-             }
-         }
- 
-@@ -310,13 +368,15 @@
-     }
- 
-     @Override
--    protected void onPlace(BlockState state, Level world, BlockPos pos, BlockState oldState, boolean notify) {
--        super.onPlace(state, world, pos, oldState, notify);
--        world.scheduleTick(pos, (Block) this, FireBlock.getFireTickDelay(world.random));
-+    // CraftBukkit start - context
-+    protected void onPlace(BlockState iblockdata, Level world, BlockPos blockposition, BlockState iblockdata1, boolean flag, UseOnContext context) {
-+        super.onPlace(iblockdata, world, blockposition, iblockdata1, flag, context);
-+        // CraftBukkit end
-+        world.scheduleTick(blockposition, (Block) this, FireBlock.getFireTickDelay(world)); // Paper - Add fire-tick-delay option
-     }
- 
--    private static int getFireTickDelay(RandomSource random) {
--        return 30 + random.nextInt(10);
-+    private static int getFireTickDelay(Level world) { // Paper - Add fire-tick-delay option
-+        return world.paperConfig().environment.fireTickDelay + world.random.nextInt(10); // Paper - Add fire-tick-delay option
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/FrogspawnBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/FrogspawnBlock.java.patch
deleted file mode 100644
index c5fefd2f6b..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/FrogspawnBlock.java.patch
+++ /dev/null
@@ -1,31 +0,0 @@
---- a/net/minecraft/world/level/block/FrogspawnBlock.java
-+++ b/net/minecraft/world/level/block/FrogspawnBlock.java
-@@ -89,6 +89,7 @@
- 
-     @Override
-     protected void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
-+        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
-         if (entity.getType().equals(EntityType.FALLING_BLOCK)) {
-             this.destroyBlock(world, pos);
-         }
-@@ -101,6 +102,11 @@
-     }
- 
-     private void hatchFrogspawn(ServerLevel world, BlockPos pos, RandomSource random) {
-+        // Paper start - Call BlockFadeEvent
-+        if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(world, pos, Blocks.AIR.defaultBlockState()).isCancelled()) {
-+            return;
-+        }
-+        // Paper end - Call BlockFadeEvent
-         this.destroyBlock(world, pos);
-         world.playSound(null, pos, SoundEvents.FROGSPAWN_HATCH, SoundSource.BLOCKS, 1.0F, 1.0F);
-         this.spawnTadpoles(world, pos, random);
-@@ -121,7 +127,7 @@
-                 int k = random.nextInt(1, 361);
-                 tadpole.moveTo(d, (double)pos.getY() - 0.5, e, (float)k, 0.0F);
-                 tadpole.setPersistenceRequired();
--                world.addFreshEntity(tadpole);
-+                world.addFreshEntity(tadpole, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.EGG); // Paper - use correct spawn reason
-             }
-         }
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/FrostedIceBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/FrostedIceBlock.java.patch
deleted file mode 100644
index 9d1efc3454..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/FrostedIceBlock.java.patch
+++ /dev/null
@@ -1,24 +0,0 @@
---- a/net/minecraft/world/level/block/FrostedIceBlock.java
-+++ b/net/minecraft/world/level/block/FrostedIceBlock.java
-@@ -42,6 +42,7 @@
- 
-     @Override
-     protected void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
-+        if (!world.paperConfig().environment.frostedIce.enabled) return; // Paper - Frosted ice options
-         if ((random.nextInt(3) == 0 || this.fewerNeigboursThan(world, pos, 4))
-             && world.getMaxLocalRawBrightness(pos) > 11 - state.getValue(AGE) - state.getLightBlock()
-             && this.slightlyMelt(state, world, pos)) {
-@@ -51,11 +52,11 @@
-                 mutableBlockPos.setWithOffset(pos, direction);
-                 BlockState blockState = world.getBlockState(mutableBlockPos);
-                 if (blockState.is(this) && !this.slightlyMelt(blockState, world, mutableBlockPos)) {
--                    world.scheduleTick(mutableBlockPos, this, Mth.nextInt(random, 20, 40));
-+                    world.scheduleTick(mutableBlockPos, this, Mth.nextInt(random, world.paperConfig().environment.frostedIce.delay.min, world.paperConfig().environment.frostedIce.delay.max)); // Paper - Frosted ice options
-                 }
-             }
-         } else {
--            world.scheduleTick(pos, this, Mth.nextInt(random, 20, 40));
-+            world.scheduleTick(pos, this, Mth.nextInt(random, world.paperConfig().environment.frostedIce.delay.min, world.paperConfig().environment.frostedIce.delay.max)); // Paper - Frosted ice options
-         }
-     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/FungusBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/FungusBlock.java.patch
deleted file mode 100644
index 6508c8df14..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/FungusBlock.java.patch
+++ /dev/null
@@ -1,16 +0,0 @@
---- a/net/minecraft/world/level/block/FungusBlock.java
-+++ b/net/minecraft/world/level/block/FungusBlock.java
-@@ -74,6 +74,13 @@
-     @Override
-     public void performBonemeal(ServerLevel world, RandomSource random, BlockPos pos, BlockState state) {
-         this.getFeature(world).ifPresent((holder) -> {
-+            // CraftBukkit start
-+            if (this == Blocks.WARPED_FUNGUS) {
-+                SaplingBlock.treeType = org.bukkit.TreeType.WARPED_FUNGUS;
-+            } else if (this == Blocks.CRIMSON_FUNGUS) {
-+                SaplingBlock.treeType = org.bukkit.TreeType.CRIMSON_FUNGUS;
-+            }
-+            // CraftBukkit end
-             ((ConfiguredFeature) holder.value()).place(world, world.getChunkSource().getGenerator(), random, pos);
-         });
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/GrindstoneBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/GrindstoneBlock.java.patch
deleted file mode 100644
index 860a309048..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/GrindstoneBlock.java.patch
+++ /dev/null
@@ -1,13 +0,0 @@
---- a/net/minecraft/world/level/block/GrindstoneBlock.java
-+++ b/net/minecraft/world/level/block/GrindstoneBlock.java
-@@ -152,8 +152,9 @@
-     @Override
-     protected InteractionResult useWithoutItem(BlockState state, Level world, BlockPos pos, Player player, BlockHitResult hit) {
-         if (!world.isClientSide) {
--            player.openMenu(state.getMenuProvider(world, pos));
-+            if (player.openMenu(state.getMenuProvider(world, pos)).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
-             player.awardStat(Stats.INTERACT_WITH_GRINDSTONE);
-+            } // Paper - Fix InventoryOpenEvent cancellation
-         }
- 
-         return InteractionResult.SUCCESS;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/GrowingPlantHeadBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/GrowingPlantHeadBlock.java.patch
deleted file mode 100644
index b55ee5aef9..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/GrowingPlantHeadBlock.java.patch
+++ /dev/null
@@ -1,39 +0,0 @@
---- a/net/minecraft/world/level/block/GrowingPlantHeadBlock.java
-+++ b/net/minecraft/world/level/block/GrowingPlantHeadBlock.java
-@@ -44,16 +44,34 @@
- 
-     @Override
-     protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
--        if ((Integer) state.getValue(GrowingPlantHeadBlock.AGE) < 25 && random.nextDouble() < this.growPerTickProbability) {
-+        // Spigot start
-+        int modifier;
-+        if (this == Blocks.KELP) {
-+            modifier = world.spigotConfig.kelpModifier;
-+        } else if (this == Blocks.TWISTING_VINES) {
-+            modifier = world.spigotConfig.twistingVinesModifier;
-+        } else if (this == Blocks.WEEPING_VINES) {
-+            modifier = world.spigotConfig.weepingVinesModifier;
-+        } else {
-+            modifier = world.spigotConfig.caveVinesModifier;
-+        }
-+        if ((Integer) state.getValue(GrowingPlantHeadBlock.AGE) < 25 && random.nextDouble() < ((modifier / 100.0D) * this.growPerTickProbability)) { // Spigot - SPIGOT-7159: Better modifier resolution
-+            // Spigot end
-             BlockPos blockposition1 = pos.relative(this.growthDirection);
- 
-             if (this.canGrowInto(world.getBlockState(blockposition1))) {
--                world.setBlockAndUpdate(blockposition1, this.getGrowIntoState(state, world.random));
-+                org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(world, pos, blockposition1, this.getGrowIntoState(state, world.random, world)); // CraftBukkit // Paper - Fix Spigot growth modifiers
-             }
-         }
- 
-     }
- 
-+    // Paper start - Fix Spigot growth modifiers
-+    protected BlockState getGrowIntoState(BlockState state, RandomSource random, @javax.annotation.Nullable Level level) {
-+        return this.getGrowIntoState(state, random);
-+    }
-+    // Paper end - Fix Spigot growth modifiers
-+
-     protected BlockState getGrowIntoState(BlockState state, RandomSource random) {
-         return (BlockState) state.cycle(GrowingPlantHeadBlock.AGE);
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/IceBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/IceBlock.java.patch
deleted file mode 100644
index 003b651052..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/IceBlock.java.patch
+++ /dev/null
@@ -1,30 +0,0 @@
---- a/net/minecraft/world/level/block/IceBlock.java
-+++ b/net/minecraft/world/level/block/IceBlock.java
-@@ -34,8 +34,13 @@
-     }
- 
-     @Override
--    public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool) {
--        super.playerDestroy(world, player, pos, state, blockEntity, tool);
-+    public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool, boolean includeDrops, boolean dropExp) { // Paper - fix drops not preventing stats/food exhaustion
-+        super.playerDestroy(world, player, pos, state, blockEntity, tool, includeDrops, dropExp); // Paper - fix drops not preventing stats/food exhaustion
-+        // Paper start - Improve Block#breakNaturally API
-+        this.afterDestroy(world, pos, tool);
-+    }
-+    public void afterDestroy(Level world, BlockPos pos, ItemStack tool) {
-+        // Paper end - Improve Block#breakNaturally API
-         if (!EnchantmentHelper.hasTag(tool, EnchantmentTags.PREVENTS_ICE_MELTING)) {
-             if (world.dimensionType().ultraWarm()) {
-                 world.removeBlock(pos, false);
-@@ -60,6 +65,11 @@
-     }
- 
-     protected void melt(BlockState state, Level world, BlockPos pos) {
-+        // CraftBukkit start
-+        if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(world, pos, world.dimensionType().ultraWarm() ? Blocks.AIR.defaultBlockState() : Blocks.WATER.defaultBlockState()).isCancelled()) {
-+            return;
-+        }
-+        // CraftBukkit end
-         if (world.dimensionType().ultraWarm()) {
-             world.removeBlock(pos, false);
-         } else {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/InfestedBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/InfestedBlock.java.patch
deleted file mode 100644
index 49d54e7dd2..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/InfestedBlock.java.patch
+++ /dev/null
@@ -1,19 +0,0 @@
---- a/net/minecraft/world/level/block/InfestedBlock.java
-+++ b/net/minecraft/world/level/block/InfestedBlock.java
-@@ -19,6 +19,7 @@
- import net.minecraft.world.level.block.state.BlockBehaviour;
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.block.state.properties.Property;
-+import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason; // CraftBukkit
- 
- public class InfestedBlock extends Block {
- 
-@@ -54,7 +55,7 @@
- 
-         if (entitysilverfish != null) {
-             entitysilverfish.moveTo((double) pos.getX() + 0.5D, (double) pos.getY(), (double) pos.getZ() + 0.5D, 0.0F, 0.0F);
--            world.addFreshEntity(entitysilverfish);
-+            world.addFreshEntity(entitysilverfish, SpawnReason.SILVERFISH_BLOCK); // CraftBukkit - add SpawnReason
-             entitysilverfish.spawnAnim();
-         }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/LayeredCauldronBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/LayeredCauldronBlock.java.patch
deleted file mode 100644
index 67743248ac..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/LayeredCauldronBlock.java.patch
+++ /dev/null
@@ -1,127 +0,0 @@
---- a/net/minecraft/world/level/block/LayeredCauldronBlock.java
-+++ b/net/minecraft/world/level/block/LayeredCauldronBlock.java
-@@ -17,6 +17,11 @@
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.level.material.Fluid;
- import net.minecraft.world.level.material.Fluids;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.block.CraftBlockState;
-+import org.bukkit.craftbukkit.block.CraftBlockStates;
-+import org.bukkit.event.block.CauldronLevelChangeEvent;
-+// CraftBukkit end
- 
- public class LayeredCauldronBlock extends AbstractCauldronBlock {
- 
-@@ -62,41 +67,86 @@
- 
-     @Override
-     protected void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
-+        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
-         if (world instanceof ServerLevel worldserver) {
-             if (entity.isOnFire() && this.isEntityInsideContent(state, pos, entity)) {
--                entity.clearFire();
--                if (entity.mayInteract(worldserver, pos)) {
--                    this.handleEntityOnFireInside(state, world, pos);
-+                // CraftBukkit start - moved down
-+                // entity.clearFire();
-+                if ((entity instanceof net.minecraft.world.entity.player.Player || worldserver.getGameRules().getBoolean(net.minecraft.world.level.GameRules.RULE_MOBGRIEFING)) && entity.mayInteract(worldserver, pos)) { // Paper - Fixes MC-248588
-+                    if (this.handleEntityOnFireInsideWithEvent(state, world, pos, entity)) { // Paper - fix powdered snow cauldron extinguishing entities
-+                        entity.clearFire();
-+                    }
-+                    // CraftBukkit end
-                 }
-             }
-         }
- 
-     }
- 
--    private void handleEntityOnFireInside(BlockState state, Level world, BlockPos pos) {
-+    // CraftBukkit start
-+    @io.papermc.paper.annotation.DoNotUse @Deprecated // Paper - fix powdered snow cauldron extinguishing entities; use #handleEntityOnFireInsideWithEvent
-+    private boolean handleEntityOnFireInside(BlockState iblockdata, Level world, BlockPos blockposition, Entity entity) {
-         if (this.precipitationType == Biome.Precipitation.SNOW) {
--            LayeredCauldronBlock.lowerFillLevel((BlockState) Blocks.WATER_CAULDRON.defaultBlockState().setValue(LayeredCauldronBlock.LEVEL, (Integer) state.getValue(LayeredCauldronBlock.LEVEL)), world, pos);
-+            return LayeredCauldronBlock.lowerFillLevel((BlockState) Blocks.WATER_CAULDRON.defaultBlockState().setValue(LayeredCauldronBlock.LEVEL, (Integer) iblockdata.getValue(LayeredCauldronBlock.LEVEL)), world, blockposition, entity, CauldronLevelChangeEvent.ChangeReason.EXTINGUISH);
-         } else {
--            LayeredCauldronBlock.lowerFillLevel(state, world, pos);
-+            return LayeredCauldronBlock.lowerFillLevel(iblockdata, world, blockposition, entity, CauldronLevelChangeEvent.ChangeReason.EXTINGUISH);
-+            // CraftBukkit end
-         }
- 
-     }
-+    // Paper start - fix powdered snow cauldron extinguishing entities
-+    protected boolean handleEntityOnFireInsideWithEvent(BlockState state, Level world, BlockPos pos, Entity entity) {
-+        if (this.precipitationType == Biome.Precipitation.SNOW) {
-+            return LayeredCauldronBlock.lowerFillLevel((BlockState) Blocks.WATER_CAULDRON.defaultBlockState().setValue(LayeredCauldronBlock.LEVEL, (Integer) state.getValue(LayeredCauldronBlock.LEVEL)), world, pos, entity, CauldronLevelChangeEvent.ChangeReason.EXTINGUISH);
-+        } else {
-+            return LayeredCauldronBlock.lowerFillLevel(state, world, pos, entity, CauldronLevelChangeEvent.ChangeReason.EXTINGUISH);
-+        }
-+    }
-+    // Paper end - fix powdered snow cauldron extinguishing entities
- 
-     public static void lowerFillLevel(BlockState state, Level world, BlockPos pos) {
--        int i = (Integer) state.getValue(LayeredCauldronBlock.LEVEL) - 1;
--        BlockState iblockdata1 = i == 0 ? Blocks.CAULDRON.defaultBlockState() : (BlockState) state.setValue(LayeredCauldronBlock.LEVEL, i);
-+        // CraftBukkit start
-+        LayeredCauldronBlock.lowerFillLevel(state, world, pos, null, CauldronLevelChangeEvent.ChangeReason.UNKNOWN);
-+    }
- 
--        world.setBlockAndUpdate(pos, iblockdata1);
--        world.gameEvent((Holder) GameEvent.BLOCK_CHANGE, pos, GameEvent.Context.of(iblockdata1));
-+    public static boolean lowerFillLevel(BlockState iblockdata, Level world, BlockPos blockposition, Entity entity, CauldronLevelChangeEvent.ChangeReason reason) {
-+        int i = (Integer) iblockdata.getValue(LayeredCauldronBlock.LEVEL) - 1;
-+        BlockState iblockdata1 = i == 0 ? Blocks.CAULDRON.defaultBlockState() : (BlockState) iblockdata.setValue(LayeredCauldronBlock.LEVEL, i);
-+
-+        return LayeredCauldronBlock.changeLevel(iblockdata, world, blockposition, iblockdata1, entity, reason);
-     }
- 
-+    // CraftBukkit start
-+    // Paper start - Call CauldronLevelChangeEvent
-+    public static boolean changeLevel(BlockState iblockdata, Level world, BlockPos blockposition, BlockState newBlock, @javax.annotation.Nullable Entity entity, CauldronLevelChangeEvent.ChangeReason reason) { // Paper - entity is nullable
-+        return changeLevel(iblockdata, world, blockposition, newBlock, entity, reason, true);
-+    }
-+
-+    public static boolean changeLevel(BlockState iblockdata, Level world, BlockPos blockposition, BlockState newBlock, @javax.annotation.Nullable Entity entity, CauldronLevelChangeEvent.ChangeReason reason, boolean sendGameEvent) { // Paper - entity is nullable
-+    // Paper end - Call CauldronLevelChangeEvent
-+        CraftBlockState newState = CraftBlockStates.getBlockState(world, blockposition);
-+        newState.setData(newBlock);
-+
-+        CauldronLevelChangeEvent event = new CauldronLevelChangeEvent(
-+                world.getWorld().getBlockAt(blockposition.getX(), blockposition.getY(), blockposition.getZ()),
-+                (entity == null) ? null : entity.getBukkitEntity(), reason, newState
-+        );
-+        world.getCraftServer().getPluginManager().callEvent(event);
-+        if (event.isCancelled()) {
-+            return false;
-+        }
-+        newState.update(true);
-+        if (sendGameEvent) world.gameEvent((Holder) GameEvent.BLOCK_CHANGE, blockposition, GameEvent.Context.of(newBlock)); // Paper - Call CauldronLevelChangeEvent
-+        return true;
-+    }
-+    // CraftBukkit end
-+
-     @Override
-     public void handlePrecipitation(BlockState state, Level world, BlockPos pos, Biome.Precipitation precipitation) {
-         if (CauldronBlock.shouldHandlePrecipitation(world, precipitation) && (Integer) state.getValue(LayeredCauldronBlock.LEVEL) != 3 && precipitation == this.precipitationType) {
-             BlockState iblockdata1 = (BlockState) state.cycle(LayeredCauldronBlock.LEVEL);
- 
--            world.setBlockAndUpdate(pos, iblockdata1);
--            world.gameEvent((Holder) GameEvent.BLOCK_CHANGE, pos, GameEvent.Context.of(iblockdata1));
-+            LayeredCauldronBlock.changeLevel(state, world, pos, iblockdata1, null, CauldronLevelChangeEvent.ChangeReason.NATURAL_FILL); // CraftBukkit
-         }
-     }
- 
-@@ -115,8 +165,11 @@
-         if (!this.isFull(state)) {
-             BlockState iblockdata1 = (BlockState) state.setValue(LayeredCauldronBlock.LEVEL, (Integer) state.getValue(LayeredCauldronBlock.LEVEL) + 1);
- 
--            world.setBlockAndUpdate(pos, iblockdata1);
--            world.gameEvent((Holder) GameEvent.BLOCK_CHANGE, pos, GameEvent.Context.of(iblockdata1));
-+            // CraftBukkit start
-+            if (!LayeredCauldronBlock.changeLevel(state, world, pos, iblockdata1, null, CauldronLevelChangeEvent.ChangeReason.NATURAL_FILL)) {
-+                return;
-+            }
-+            // CraftBukkit end
-             world.levelEvent(1047, pos, 0);
-         }
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/LeavesBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/LeavesBlock.java.patch
deleted file mode 100644
index 4e3cc600f3..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/LeavesBlock.java.patch
+++ /dev/null
@@ -1,25 +0,0 @@
---- a/net/minecraft/world/level/block/LeavesBlock.java
-+++ b/net/minecraft/world/level/block/LeavesBlock.java
-@@ -26,6 +26,7 @@
- import net.minecraft.world.level.material.Fluids;
- import net.minecraft.world.phys.shapes.Shapes;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+import org.bukkit.event.block.LeavesDecayEvent; // CraftBukkit
- 
- public class LeavesBlock extends Block implements SimpleWaterloggedBlock {
- 
-@@ -59,6 +60,14 @@
-     @Override
-     protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
-         if (this.decaying(state)) {
-+            // CraftBukkit start
-+            LeavesDecayEvent event = new LeavesDecayEvent(world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ()));
-+            world.getCraftServer().getPluginManager().callEvent(event);
-+
-+            if (event.isCancelled() || world.getBlockState(pos).getBlock() != this) {
-+                return;
-+            }
-+            // CraftBukkit end
-             dropResources(state, world, pos);
-             world.removeBlock(pos, false);
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/LecternBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/LecternBlock.java.patch
deleted file mode 100644
index a928fea703..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/LecternBlock.java.patch
+++ /dev/null
@@ -1,69 +0,0 @@
---- a/net/minecraft/world/level/block/LecternBlock.java
-+++ b/net/minecraft/world/level/block/LecternBlock.java
-@@ -153,7 +153,24 @@
-         BlockEntity tileentity = world.getBlockEntity(pos);
- 
-         if (tileentity instanceof LecternBlockEntity tileentitylectern) {
--            tileentitylectern.setBook(stack.consumeAndReturn(1, user));
-+            // Paper start - Add PlayerInsertLecternBookEvent
-+            ItemStack eventSourcedBookStack = null;
-+            if (user instanceof final net.minecraft.server.level.ServerPlayer serverPlayer) {
-+                final io.papermc.paper.event.player.PlayerInsertLecternBookEvent event = new io.papermc.paper.event.player.PlayerInsertLecternBookEvent(
-+                    serverPlayer.getBukkitEntity(),
-+                    org.bukkit.craftbukkit.block.CraftBlock.at(world, pos),
-+                    org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(stack.copyWithCount(1))
-+                );
-+                if (!event.callEvent()) return;
-+                eventSourcedBookStack = org.bukkit.craftbukkit.inventory.CraftItemStack.unwrap(event.getBook());
-+            }
-+            if (eventSourcedBookStack == null) {
-+                eventSourcedBookStack = stack.consumeAndReturn(1, user);
-+            } else {
-+                stack.consume(1, user);
-+            }
-+            tileentitylectern.setBook(eventSourcedBookStack);
-+            // Paper end - Add PlayerInsertLecternBookEvent
-             LecternBlock.resetBookState(user, world, pos, state, true);
-             world.playSound((Player) null, pos, SoundEvents.BOOK_PUT, SoundSource.BLOCKS, 1.0F, 1.0F);
-         }
-@@ -175,6 +192,16 @@
-     }
- 
-     private static void changePowered(Level world, BlockPos pos, BlockState state, boolean powered) {
-+        // Paper start - Call BlockRedstoneEvent properly
-+        final int currentRedstoneLevel = state.getValue(LecternBlock.POWERED) ? 15 : 0, targetRedstoneLevel = powered ? 15 : 0;
-+        if (currentRedstoneLevel != targetRedstoneLevel) {
-+            final org.bukkit.event.block.BlockRedstoneEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callRedstoneChange(world, pos, currentRedstoneLevel, targetRedstoneLevel);
-+
-+            if (event.getNewCurrent() != targetRedstoneLevel) {
-+                return;
-+            }
-+        }
-+        // Paper end - Call BlockRedstoneEvent properly
-         world.setBlock(pos, (BlockState) state.setValue(LecternBlock.POWERED, powered), 3);
-         LecternBlock.updateBelow(world, pos, state);
-     }
-@@ -206,11 +233,12 @@
-     }
- 
-     private void popBook(BlockState state, Level world, BlockPos pos) {
--        BlockEntity tileentity = world.getBlockEntity(pos);
-+        BlockEntity tileentity = world.getBlockEntity(pos, false); // CraftBukkit - don't validate, type may be changed already
- 
-         if (tileentity instanceof LecternBlockEntity tileentitylectern) {
-             Direction enumdirection = (Direction) state.getValue(LecternBlock.FACING);
-             ItemStack itemstack = tileentitylectern.getBook().copy();
-+            if (itemstack.isEmpty()) return; // CraftBukkit - SPIGOT-5500
-             float f = 0.25F * (float) enumdirection.getStepX();
-             float f1 = 0.25F * (float) enumdirection.getStepZ();
-             ItemEntity entityitem = new ItemEntity(world, (double) pos.getX() + 0.5D + (double) f, (double) (pos.getY() + 1), (double) pos.getZ() + 0.5D + (double) f1, itemstack);
-@@ -282,8 +310,7 @@
-     private void openScreen(Level world, BlockPos pos, Player player) {
-         BlockEntity tileentity = world.getBlockEntity(pos);
- 
--        if (tileentity instanceof LecternBlockEntity) {
--            player.openMenu((LecternBlockEntity) tileentity);
-+        if (tileentity instanceof LecternBlockEntity && player.openMenu((LecternBlockEntity) tileentity).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
-             player.awardStat(Stats.INTERACT_WITH_LECTERN);
-         }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/LeverBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/LeverBlock.java.patch
deleted file mode 100644
index 325443e6f3..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/LeverBlock.java.patch
+++ /dev/null
@@ -1,31 +0,0 @@
---- a/net/minecraft/world/level/block/LeverBlock.java
-+++ b/net/minecraft/world/level/block/LeverBlock.java
-@@ -31,6 +31,7 @@
- import net.minecraft.world.phys.BlockHitResult;
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+import org.bukkit.event.block.BlockRedstoneEvent; // CraftBukkit
- 
- public class LeverBlock extends FaceAttachedHorizontalDirectionalBlock {
- 
-@@ -102,6 +103,20 @@
-                 LeverBlock.makeParticle(iblockdata1, world, pos, 1.0F);
-             }
-         } else {
-+            // CraftBukkit start - Interact Lever
-+            boolean powered = state.getValue(LeverBlock.POWERED); // Old powered state
-+            org.bukkit.block.Block block = world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ());
-+            int old = (powered) ? 15 : 0;
-+            int current = (!powered) ? 15 : 0;
-+
-+            BlockRedstoneEvent eventRedstone = new BlockRedstoneEvent(block, old, current);
-+            world.getCraftServer().getPluginManager().callEvent(eventRedstone);
-+
-+            if ((eventRedstone.getNewCurrent() > 0) != (!powered)) {
-+                return InteractionResult.SUCCESS;
-+            }
-+            // CraftBukkit end
-+
-             this.pull(state, world, pos, (Player) null);
-         }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/LightningRodBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/LightningRodBlock.java.patch
deleted file mode 100644
index c97ad7d96c..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/LightningRodBlock.java.patch
+++ /dev/null
@@ -1,33 +0,0 @@
---- a/net/minecraft/world/level/block/LightningRodBlock.java
-+++ b/net/minecraft/world/level/block/LightningRodBlock.java
-@@ -24,6 +24,11 @@
- import net.minecraft.world.level.material.Fluids;
- import net.minecraft.world.level.redstone.ExperimentalRedstoneUtils;
- 
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.event.block.BlockRedstoneEvent;
-+// CraftBukkit end
-+
- public class LightningRodBlock extends RodBlock implements SimpleWaterloggedBlock {
- 
-     public static final MapCodec<LightningRodBlock> CODEC = simpleCodec(LightningRodBlock::new);
-@@ -76,6 +81,18 @@
-     }
- 
-     public void onLightningStrike(BlockState state, Level world, BlockPos pos) {
-+        // CraftBukkit start
-+        boolean powered = state.getValue(LightningRodBlock.POWERED);
-+        int old = (powered) ? 15 : 0;
-+        int current = (!powered) ? 15 : 0;
-+
-+        BlockRedstoneEvent eventRedstone = new BlockRedstoneEvent(CraftBlock.at(world, pos), old, current);
-+        world.getCraftServer().getPluginManager().callEvent(eventRedstone);
-+
-+        if (eventRedstone.getNewCurrent() <= 0) {
-+            return;
-+        }
-+        // CraftBukkit end
-         world.setBlock(pos, (BlockState) state.setValue(LightningRodBlock.POWERED, true), 3);
-         this.updateNeighbours(state, world, pos);
-         world.scheduleTick(pos, (Block) this, 8);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/LiquidBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/LiquidBlock.java.patch
deleted file mode 100644
index 42df446a82..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/LiquidBlock.java.patch
+++ /dev/null
@@ -1,78 +0,0 @@
---- a/net/minecraft/world/level/block/LiquidBlock.java
-+++ b/net/minecraft/world/level/block/LiquidBlock.java
-@@ -42,7 +42,7 @@
- public class LiquidBlock extends Block implements BucketPickup {
- 
-     private static final Codec<FlowingFluid> FLOWING_FLUID = BuiltInRegistries.FLUID.byNameCodec().comapFlatMap((fluidtype) -> {
--        DataResult dataresult;
-+        DataResult<FlowingFluid> dataresult; // CraftBukkit - decompile error
- 
-         if (fluidtype instanceof FlowingFluid fluidtypeflowing) {
-             dataresult = DataResult.success(fluidtypeflowing);
-@@ -141,11 +141,31 @@
-     @Override
-     protected void onPlace(BlockState state, Level world, BlockPos pos, BlockState oldState, boolean notify) {
-         if (this.shouldSpreadLiquid(world, pos, state)) {
--            world.scheduleTick(pos, state.getFluidState().getType(), this.fluid.getTickDelay(world));
-+            world.scheduleTick(pos, state.getFluidState().getType(), this.getFlowSpeed(world, pos)); // Paper - Configurable speed for water flowing over lava
-         }
- 
-     }
- 
-+    // Paper start - Configurable speed for water flowing over lava
-+    public int getFlowSpeed(Level world, BlockPos blockposition) {
-+        if (net.minecraft.core.registries.BuiltInRegistries.FLUID.wrapAsHolder(this.fluid).is(FluidTags.WATER)) {
-+            if (
-+                isLava(world, blockposition.north(1)) ||
-+                isLava(world, blockposition.south(1)) ||
-+                isLava(world, blockposition.west(1)) ||
-+                isLava(world, blockposition.east(1))
-+            ) {
-+                return world.paperConfig().environment.waterOverLavaFlowSpeed;
-+            }
-+        }
-+        return this.fluid.getTickDelay(world);
-+    }
-+    private static boolean isLava(Level world, BlockPos blockPos) {
-+        final FluidState fluidState = world.getFluidIfLoaded(blockPos);
-+        return fluidState != null && fluidState.is(FluidTags.LAVA);
-+    }
-+    // Paper end - Configurable speed for water flowing over lava
-+
-     @Override
-     protected BlockState updateShape(BlockState state, LevelReader world, ScheduledTickAccess tickView, BlockPos pos, Direction direction, BlockPos neighborPos, BlockState neighborState, RandomSource random) {
-         if (state.getFluidState().isSource() || neighborState.getFluidState().isSource()) {
-@@ -158,7 +178,7 @@
-     @Override
-     protected void neighborChanged(BlockState state, Level world, BlockPos pos, Block sourceBlock, @Nullable Orientation wireOrientation, boolean notify) {
-         if (this.shouldSpreadLiquid(world, pos, state)) {
--            world.scheduleTick(pos, state.getFluidState().getType(), this.fluid.getTickDelay(world));
-+            world.scheduleTick(pos, state.getFluidState().getType(), this.getFlowSpeed(world, pos)); // Paper - Configurable speed for water flowing over lava
-         }
- 
-     }
-@@ -175,14 +195,20 @@
-                 if (world.getFluidState(blockposition1).is(FluidTags.WATER)) {
-                     Block block = world.getFluidState(pos).isSource() ? Blocks.OBSIDIAN : Blocks.COBBLESTONE;
- 
--                    world.setBlockAndUpdate(pos, block.defaultBlockState());
--                    this.fizz(world, pos);
-+                    // CraftBukkit start
-+                    if (org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(world, pos, block.defaultBlockState())) {
-+                        this.fizz(world, pos);
-+                    }
-+                    // CraftBukkit end
-                     return false;
-                 }
- 
-                 if (flag && world.getBlockState(blockposition1).is(Blocks.BLUE_ICE)) {
--                    world.setBlockAndUpdate(pos, Blocks.BASALT.defaultBlockState());
--                    this.fizz(world, pos);
-+                    // CraftBukkit start
-+                    if (org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(world, pos, Blocks.BASALT.defaultBlockState())) {
-+                        this.fizz(world, pos);
-+                    }
-+                    // CraftBukkit end
-                     return false;
-                 }
-             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/LoomBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/LoomBlock.java.patch
deleted file mode 100644
index 9fc3631803..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/LoomBlock.java.patch
+++ /dev/null
@@ -1,13 +0,0 @@
---- a/net/minecraft/world/level/block/LoomBlock.java
-+++ b/net/minecraft/world/level/block/LoomBlock.java
-@@ -33,8 +33,9 @@
-     @Override
-     protected InteractionResult useWithoutItem(BlockState state, Level world, BlockPos pos, Player player, BlockHitResult hit) {
-         if (!world.isClientSide) {
--            player.openMenu(state.getMenuProvider(world, pos));
-+            if (player.openMenu(state.getMenuProvider(world, pos)).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
-             player.awardStat(Stats.INTERACT_WITH_LOOM);
-+            } // Paper - Fix InventoryOpenEvent cancellation
-         }
- 
-         return InteractionResult.SUCCESS;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/MagmaBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/MagmaBlock.java.patch
deleted file mode 100644
index 6febd2458e..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/MagmaBlock.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/level/block/MagmaBlock.java
-+++ b/net/minecraft/world/level/block/MagmaBlock.java
-@@ -30,7 +30,7 @@
-     @Override
-     public void stepOn(Level world, BlockPos pos, BlockState state, Entity entity) {
-         if (!entity.isSteppingCarefully() && entity instanceof LivingEntity) {
--            entity.hurt(world.damageSources().hotFloor(), 1.0F);
-+            entity.hurt(world.damageSources().hotFloor().directBlock(world, pos), 1.0F); // CraftBukkit
-         }
- 
-         super.stepOn(world, pos, state, entity);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/MultifaceSpreader.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/MultifaceSpreader.java.patch
deleted file mode 100644
index 31dd40a615..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/MultifaceSpreader.java.patch
+++ /dev/null
@@ -1,43 +0,0 @@
---- a/net/minecraft/world/level/block/MultifaceSpreader.java
-+++ b/net/minecraft/world/level/block/MultifaceSpreader.java
-@@ -156,7 +156,7 @@
-                     world.getChunk(growPos.pos()).markPosForPostprocessing(growPos.pos());
-                 }
- 
--                return world.setBlock(growPos.pos(), iblockdata1, 2);
-+                return org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(world, growPos.source(), growPos.pos(), iblockdata1, 2); // CraftBukkit
-             } else {
-                 return false;
-             }
-@@ -174,19 +174,19 @@
-         SAME_POSITION {
-             @Override
-             public MultifaceSpreader.SpreadPos getSpreadPos(BlockPos pos, Direction newDirection, Direction oldDirection) {
--                return new MultifaceSpreader.SpreadPos(pos, newDirection);
-+                return new MultifaceSpreader.SpreadPos(pos, newDirection, pos); // CraftBukkit
-             }
-         },
-         SAME_PLANE {
-             @Override
-             public MultifaceSpreader.SpreadPos getSpreadPos(BlockPos pos, Direction newDirection, Direction oldDirection) {
--                return new MultifaceSpreader.SpreadPos(pos.relative(newDirection), oldDirection);
-+                return new MultifaceSpreader.SpreadPos(pos.relative(newDirection), oldDirection, pos); // CraftBukkit
-             }
-         },
-         WRAP_AROUND {
-             @Override
-             public MultifaceSpreader.SpreadPos getSpreadPos(BlockPos pos, Direction newDirection, Direction oldDirection) {
--                return new MultifaceSpreader.SpreadPos(pos.relative(newDirection).relative(oldDirection), newDirection.getOpposite());
-+                return new MultifaceSpreader.SpreadPos(pos.relative(newDirection).relative(oldDirection), newDirection.getOpposite(), pos); // CraftBukkit
-             }
-         };
- 
-@@ -195,7 +195,7 @@
-         public abstract MultifaceSpreader.SpreadPos getSpreadPos(BlockPos pos, Direction newDirection, Direction oldDirection);
-     }
- 
--    public static record SpreadPos(BlockPos pos, Direction face) {
-+    public static record SpreadPos(BlockPos pos, Direction face, BlockPos source) { // CraftBukkit
- 
-     }
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/MushroomBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/MushroomBlock.java.patch
deleted file mode 100644
index 7fa4b44f28..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/MushroomBlock.java.patch
+++ /dev/null
@@ -1,46 +0,0 @@
---- a/net/minecraft/world/level/block/MushroomBlock.java
-+++ b/net/minecraft/world/level/block/MushroomBlock.java
-@@ -19,6 +19,9 @@
- import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+// CraftBukkit start
-+import org.bukkit.TreeType;
-+// CraftBukkit end
- 
- public class MushroomBlock extends BushBlock implements BonemealableBlock {
- 
-@@ -48,7 +51,7 @@
- 
-     @Override
-     protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
--        if (random.nextInt(25) == 0) {
-+        if (random.nextFloat() < (world.spigotConfig.mushroomModifier / (100.0f * 25))) { // Spigot - SPIGOT-7159: Better modifier resolution
-             int i = 5;
-             boolean flag = true;
-             Iterator iterator = BlockPos.betweenClosed(pos.offset(-4, -1, -4), pos.offset(4, 1, 4)).iterator();
-@@ -65,6 +68,7 @@
-             }
- 
-             BlockPos blockposition2 = pos.offset(random.nextInt(3) - 1, random.nextInt(2) - random.nextInt(2), random.nextInt(3) - 1);
-+            final BlockPos sourcePos = pos; // Paper - Use correct source for mushroom block spread event
- 
-             for (int j = 0; j < 4; ++j) {
-                 if (world.isEmptyBlock(blockposition2) && state.canSurvive(world, blockposition2)) {
-@@ -75,7 +79,7 @@
-             }
- 
-             if (world.isEmptyBlock(blockposition2) && state.canSurvive(world, blockposition2)) {
--                world.setBlock(blockposition2, state, 2);
-+                org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(world, sourcePos, blockposition2, state, 2); // CraftBukkit // Paper - Use correct source for mushroom block spread event
-             }
-         }
- 
-@@ -101,6 +105,7 @@
-             return false;
-         } else {
-             world.removeBlock(pos, false);
-+            SaplingBlock.treeType = (this == Blocks.BROWN_MUSHROOM) ? TreeType.BROWN_MUSHROOM : TreeType.RED_MUSHROOM; // CraftBukkit
-             if (((ConfiguredFeature) ((Holder) optional.get()).value()).place(world, world.getChunkSource().getGenerator(), random, pos)) {
-                 return true;
-             } else {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/NetherPortalBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/NetherPortalBlock.java.patch
deleted file mode 100644
index 5b83537d76..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/NetherPortalBlock.java.patch
+++ /dev/null
@@ -1,163 +0,0 @@
---- a/net/minecraft/world/level/block/NetherPortalBlock.java
-+++ b/net/minecraft/world/level/block/NetherPortalBlock.java
-@@ -32,12 +32,19 @@
- import net.minecraft.world.level.block.state.properties.EnumProperty;
- import net.minecraft.world.level.border.WorldBorder;
- import net.minecraft.world.level.dimension.DimensionType;
-+import net.minecraft.world.level.dimension.LevelStem;
- import net.minecraft.world.level.portal.PortalShape;
- import net.minecraft.world.level.portal.TeleportTransition;
- import net.minecraft.world.phys.Vec3;
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.VoxelShape;
- import org.slf4j.Logger;
-+import org.bukkit.craftbukkit.CraftWorld;
-+import org.bukkit.craftbukkit.event.CraftPortalEvent;
-+import org.bukkit.craftbukkit.util.CraftLocation;
-+import org.bukkit.event.entity.EntityPortalEnterEvent;
-+import org.bukkit.event.player.PlayerTeleportEvent;
-+// CraftBukkit end
- 
- public class NetherPortalBlock extends Block implements Portal {
- 
-@@ -71,16 +78,21 @@
- 
-     @Override
-     protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
--        if (world.dimensionType().natural() && world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && random.nextInt(2000) < world.getDifficulty().getId()) {
-+        if (world.spigotConfig.enableZombiePigmenPortalSpawns && world.dimensionType().natural() && world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && random.nextInt(2000) < world.getDifficulty().getId()) { // Spigot
-             while (world.getBlockState(pos).is((Block) this)) {
-                 pos = pos.below();
-             }
- 
-             if (world.getBlockState(pos).isValidSpawn(world, pos, EntityType.ZOMBIFIED_PIGLIN)) {
--                Entity entity = EntityType.ZOMBIFIED_PIGLIN.spawn(world, pos.above(), EntitySpawnReason.STRUCTURE);
-+                // CraftBukkit - set spawn reason to NETHER_PORTAL
-+                Entity entity = EntityType.ZOMBIFIED_PIGLIN.spawn(world, pos.above(), EntitySpawnReason.STRUCTURE, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.NETHER_PORTAL);
- 
-                 if (entity != null) {
-                     entity.setPortalCooldown();
-+                    // Paper start - Add option to nerf pigmen from nether portals
-+                    entity.fromNetherPortal = true;
-+                    if (world.paperConfig().entities.behavior.nerfPigmenFromNetherPortals) ((net.minecraft.world.entity.Mob) entity).aware = false;
-+                    // Paper end - Add option to nerf pigmen from nether portals
-                     Entity entity1 = entity.getVehicle();
- 
-                     if (entity1 != null) {
-@@ -103,7 +115,13 @@
- 
-     @Override
-     protected void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
-+        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
-         if (entity.canUsePortal(false)) {
-+            // CraftBukkit start - Entity in portal
-+            EntityPortalEnterEvent event = new EntityPortalEnterEvent(entity.getBukkitEntity(), new org.bukkit.Location(world.getWorld(), pos.getX(), pos.getY(), pos.getZ()), org.bukkit.PortalType.NETHER); // Paper - add portal type
-+            world.getCraftServer().getPluginManager().callEvent(event);
-+            if (event.isCancelled()) return; // Paper - make cancellable
-+            // CraftBukkit end
-             entity.setAsInsidePortal(this, pos);
-         }
- 
-@@ -121,51 +139,80 @@
-     @Nullable
-     @Override
-     public TeleportTransition getPortalDestination(ServerLevel world, Entity entity, BlockPos pos) {
--        ResourceKey<Level> resourcekey = world.dimension() == Level.NETHER ? Level.OVERWORLD : Level.NETHER;
-+        // CraftBukkit start
-+        ResourceKey<Level> resourcekey = world.getTypeKey() == LevelStem.NETHER ? Level.OVERWORLD : Level.NETHER;
-         ServerLevel worldserver1 = world.getServer().getLevel(resourcekey);
-+        // Paper start - Add EntityPortalReadyEvent
-+        io.papermc.paper.event.entity.EntityPortalReadyEvent portalReadyEvent = new io.papermc.paper.event.entity.EntityPortalReadyEvent(entity.getBukkitEntity(), worldserver1 == null ? null : worldserver1.getWorld(), org.bukkit.PortalType.NETHER);
-+        if (!portalReadyEvent.callEvent()) {
-+            entity.portalProcess = null;
-+            return null;
-+        }
-+        worldserver1 = portalReadyEvent.getTargetWorld() == null ? null : ((org.bukkit.craftbukkit.CraftWorld) portalReadyEvent.getTargetWorld()).getHandle();
-+        // Paper end - Add EntityPortalReadyEvent
- 
-         if (worldserver1 == null) {
--            return null;
-+            return null; // Paper - keep previous behavior of not firing PlayerTeleportEvent if the target world doesn't exist
-         } else {
--            boolean flag = worldserver1.dimension() == Level.NETHER;
-+            boolean flag = worldserver1.getTypeKey() == LevelStem.NETHER;
-+            // CraftBukkit end
-             WorldBorder worldborder = worldserver1.getWorldBorder();
-             double d0 = DimensionType.getTeleportationScale(world.dimensionType(), worldserver1.dimensionType());
-             BlockPos blockposition1 = worldborder.clampToBounds(entity.getX() * d0, entity.getY(), entity.getZ() * d0);
-+            // Paper start - Configurable portal search radius
-+            int portalSearchRadius = worldserver1.paperConfig().environment.portalSearchRadius;
-+            if (entity.level().paperConfig().environment.portalSearchVanillaDimensionScaling && flag) { // flag = is going to nether
-+                portalSearchRadius = (int) (portalSearchRadius / worldserver1.dimensionType().coordinateScale());
-+            }
-+            // Paper end - Configurable portal search radius
-+            // CraftBukkit start
-+            CraftPortalEvent event = entity.callPortalEvent(entity, CraftLocation.toBukkit(blockposition1, worldserver1.getWorld()), PlayerTeleportEvent.TeleportCause.NETHER_PORTAL, portalSearchRadius, worldserver1.paperConfig().environment.portalCreateRadius); // Paper - use custom portal search radius
-+            if (event == null) {
-+                return null;
-+            }
-+            worldserver1 = ((CraftWorld) event.getTo().getWorld()).getHandle();
-+            worldborder = worldserver1.getWorldBorder();
-+            blockposition1 = worldborder.clampToBounds(event.getTo().getX(), event.getTo().getY(), event.getTo().getZ());
- 
--            return this.getExitPortal(worldserver1, entity, pos, blockposition1, flag, worldborder);
-+            return this.getExitPortal(worldserver1, entity, pos, blockposition1, flag, worldborder, event.getSearchRadius(), event.getCanCreatePortal(), event.getCreationRadius());
-         }
-     }
- 
-     @Nullable
--    private TeleportTransition getExitPortal(ServerLevel world, Entity entity, BlockPos pos, BlockPos scaledPos, boolean inNether, WorldBorder worldBorder) {
--        Optional<BlockPos> optional = world.getPortalForcer().findClosestPortalPosition(scaledPos, inNether, worldBorder);
-+    private TeleportTransition getExitPortal(ServerLevel worldserver, Entity entity, BlockPos blockposition, BlockPos blockposition1, boolean flag, WorldBorder worldborder, int searchRadius, boolean canCreatePortal, int createRadius) {
-+        Optional<BlockPos> optional = worldserver.getPortalForcer().findClosestPortalPosition(blockposition1, worldborder, searchRadius);
-         BlockUtil.FoundRectangle blockutil_rectangle;
-         TeleportTransition.PostTeleportTransition teleporttransition_a;
- 
-         if (optional.isPresent()) {
-             BlockPos blockposition2 = (BlockPos) optional.get();
--            BlockState iblockdata = world.getBlockState(blockposition2);
-+            BlockState iblockdata = worldserver.getBlockState(blockposition2);
- 
-             blockutil_rectangle = BlockUtil.getLargestRectangleAround(blockposition2, (Direction.Axis) iblockdata.getValue(BlockStateProperties.HORIZONTAL_AXIS), 21, Direction.Axis.Y, 21, (blockposition3) -> {
--                return world.getBlockState(blockposition3) == iblockdata;
-+                return worldserver.getBlockState(blockposition3) == iblockdata;
-             });
-             teleporttransition_a = TeleportTransition.PLAY_PORTAL_SOUND.then((entity1) -> {
-                 entity1.placePortalTicket(blockposition2);
-             });
--        } else {
--            Direction.Axis enumdirection_enumaxis = (Direction.Axis) entity.level().getBlockState(pos).getOptionalValue(NetherPortalBlock.AXIS).orElse(Direction.Axis.X);
--            Optional<BlockUtil.FoundRectangle> optional1 = world.getPortalForcer().createPortal(scaledPos, enumdirection_enumaxis);
-+        } else if (canCreatePortal) {
-+            Direction.Axis enumdirection_enumaxis = (Direction.Axis) entity.level().getBlockState(blockposition).getOptionalValue(NetherPortalBlock.AXIS).orElse(Direction.Axis.X);
-+            Optional<BlockUtil.FoundRectangle> optional1 = worldserver.getPortalForcer().createPortal(blockposition1, enumdirection_enumaxis, entity, createRadius);
-+            // CraftBukkit end
- 
-             if (optional1.isEmpty()) {
--                NetherPortalBlock.LOGGER.error("Unable to create a portal, likely target out of worldborder");
-+                // BlockPortal.LOGGER.error("Unable to create a portal, likely target out of worldborder"); // CraftBukkit
-                 return null;
-             }
- 
-             blockutil_rectangle = (BlockUtil.FoundRectangle) optional1.get();
-             teleporttransition_a = TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET);
-+            // CraftBukkit start
-+        } else {
-+            return null;
-+            // CraftBukkit end
-         }
- 
--        return NetherPortalBlock.getDimensionTransitionFromExit(entity, pos, blockutil_rectangle, world, teleporttransition_a);
-+        return NetherPortalBlock.getDimensionTransitionFromExit(entity, blockposition, blockutil_rectangle, worldserver, teleporttransition_a);
-     }
- 
-     private static TeleportTransition getDimensionTransitionFromExit(Entity entity, BlockPos pos, BlockUtil.FoundRectangle exitPortalRectangle, ServerLevel world, TeleportTransition.PostTeleportTransition postDimensionTransition) {
-@@ -203,7 +250,7 @@
-         Vec3 vec3d1 = new Vec3((double) blockposition.getX() + (flag ? d2 : d4), (double) blockposition.getY() + d3, (double) blockposition.getZ() + (flag ? d4 : d2));
-         Vec3 vec3d2 = PortalShape.findCollisionFreePosition(vec3d1, world, entity, entitysize);
- 
--        return new TeleportTransition(world, vec3d2, Vec3.ZERO, (float) i, 0.0F, Relative.union(Relative.DELTA, Relative.ROTATION), postDimensionTransition);
-+        return new TeleportTransition(world, vec3d2, Vec3.ZERO, (float) i, 0.0F, Relative.union(Relative.DELTA, Relative.ROTATION), postDimensionTransition, PlayerTeleportEvent.TeleportCause.NETHER_PORTAL); // CraftBukkit
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/NetherWartBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/NetherWartBlock.java.patch
deleted file mode 100644
index 849dc04024..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/NetherWartBlock.java.patch
+++ /dev/null
@@ -1,14 +0,0 @@
---- a/net/minecraft/world/level/block/NetherWartBlock.java
-+++ b/net/minecraft/world/level/block/NetherWartBlock.java
-@@ -52,9 +52,9 @@
-     protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
-         int i = (Integer) state.getValue(NetherWartBlock.AGE);
- 
--        if (i < 3 && random.nextInt(10) == 0) {
-+        if (i < 3 && random.nextFloat() < (world.spigotConfig.wartModifier / (100.0f * 10))) { // Spigot - SPIGOT-7159: Better modifier resolution
-             state = (BlockState) state.setValue(NetherWartBlock.AGE, i + 1);
--            world.setBlock(pos, state, 2);
-+            org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(world, pos, state, 2); // CraftBukkit
-         }
- 
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/NoteBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/NoteBlock.java.patch
deleted file mode 100644
index b1d5c77400..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/NoteBlock.java.patch
+++ /dev/null
@@ -1,78 +0,0 @@
---- a/net/minecraft/world/level/block/NoteBlock.java
-+++ b/net/minecraft/world/level/block/NoteBlock.java
-@@ -68,11 +68,13 @@
- 
-     @Override
-     public BlockState getStateForPlacement(BlockPlaceContext ctx) {
-+        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableNoteblockUpdates) return this.defaultBlockState(); // Paper - place without considering instrument
-         return this.setInstrument(ctx.getLevel(), ctx.getClickedPos(), this.defaultBlockState());
-     }
- 
-     @Override
-     protected BlockState updateShape(BlockState state, LevelReader world, ScheduledTickAccess tickView, BlockPos pos, Direction direction, BlockPos neighborPos, BlockState neighborState, RandomSource random) {
-+        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableNoteblockUpdates) return state; // Paper - prevent noteblock instrument from updating
-         boolean flag = direction.getAxis() == Direction.Axis.Y;
- 
-         return flag ? this.setInstrument(world, pos, state) : super.updateShape(state, world, tickView, pos, direction, neighborPos, neighborState, random);
-@@ -80,11 +82,13 @@
- 
-     @Override
-     protected void neighborChanged(BlockState state, Level world, BlockPos pos, Block sourceBlock, @Nullable Orientation wireOrientation, boolean notify) {
-+        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableNoteblockUpdates) return; // Paper - prevent noteblock powered-state from updating
-         boolean flag1 = world.hasNeighborSignal(pos);
- 
-         if (flag1 != (Boolean) state.getValue(NoteBlock.POWERED)) {
-             if (flag1) {
-                 this.playNote((Entity) null, state, world, pos);
-+                state = world.getBlockState(pos); // CraftBukkit - SPIGOT-5617: update in case changed in event
-             }
- 
-             world.setBlock(pos, (BlockState) state.setValue(NoteBlock.POWERED, flag1), 3);
-@@ -94,6 +98,13 @@
- 
-     private void playNote(@Nullable Entity entity, BlockState state, Level world, BlockPos pos) {
-         if (((NoteBlockInstrument) state.getValue(NoteBlock.INSTRUMENT)).worksAboveNoteBlock() || world.getBlockState(pos.above()).isAir()) {
-+            // CraftBukkit start
-+            // org.bukkit.event.block.NotePlayEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callNotePlayEvent(world, pos, state.getValue(NoteBlock.INSTRUMENT), state.getValue(NoteBlock.NOTE));
-+            // if (event.isCancelled()) {
-+            //     return;
-+            // }
-+            // CraftBukkit end
-+            // Paper - move NotePlayEvent call to fix instrument/note changes; TODO any way to cancel the game event?
-             world.blockEvent(pos, this, 0, 0);
-             world.gameEvent(entity, (Holder) GameEvent.NOTE_BLOCK_PLAY, pos);
-         }
-@@ -108,7 +119,7 @@
-     @Override
-     protected InteractionResult useWithoutItem(BlockState state, Level world, BlockPos pos, Player player, BlockHitResult hit) {
-         if (!world.isClientSide) {
--            state = (BlockState) state.cycle(NoteBlock.NOTE);
-+            if (!io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableNoteblockUpdates) state = (BlockState) state.cycle(NoteBlock.NOTE); // Paper - prevent noteblock note from updating
-             world.setBlock(pos, state, 3);
-             this.playNote(player, state, world, pos);
-             player.awardStat(Stats.TUNE_NOTEBLOCK);
-@@ -132,10 +143,14 @@
-     @Override
-     protected boolean triggerEvent(BlockState state, Level world, BlockPos pos, int type, int data) {
-         NoteBlockInstrument blockpropertyinstrument = (NoteBlockInstrument) state.getValue(NoteBlock.INSTRUMENT);
-+        // Paper start - move NotePlayEvent call to fix instrument/note changes
-+        org.bukkit.event.block.NotePlayEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callNotePlayEvent(world, pos, blockpropertyinstrument, state.getValue(NOTE));
-+        if (event.isCancelled()) return false;
-+        // Paper end - move NotePlayEvent call to fix instrument/note changes
-         float f;
- 
-         if (blockpropertyinstrument.isTunable()) {
--            int k = (Integer) state.getValue(NoteBlock.NOTE);
-+            int k = event.getNote().getId(); // Paper - move NotePlayEvent call to fix instrument/note changes
- 
-             f = NoteBlock.getPitchFromNote(k);
-             world.addParticle(ParticleTypes.NOTE, (double) pos.getX() + 0.5D, (double) pos.getY() + 1.2D, (double) pos.getZ() + 0.5D, (double) k / 24.0D, 0.0D, 0.0D);
-@@ -154,7 +169,7 @@
- 
-             holder = Holder.direct(SoundEvent.createVariableRangeEvent(minecraftkey));
-         } else {
--            holder = blockpropertyinstrument.getSoundEvent();
-+            holder = org.bukkit.craftbukkit.block.data.CraftBlockData.toNMS(event.getInstrument(), NoteBlockInstrument.class).getSoundEvent(); // Paper - move NotePlayEvent call to fix instrument/note changes
-         }
- 
-         world.playSeededSound((Player) null, (double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D, holder, SoundSource.RECORDS, 3.0F, f, world.random.nextLong());
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/ObserverBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/ObserverBlock.java.patch
deleted file mode 100644
index 0718f3ab29..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/ObserverBlock.java.patch
+++ /dev/null
@@ -1,30 +0,0 @@
---- a/net/minecraft/world/level/block/ObserverBlock.java
-+++ b/net/minecraft/world/level/block/ObserverBlock.java
-@@ -18,6 +18,8 @@
- import net.minecraft.world.level.redstone.ExperimentalRedstoneUtils;
- import net.minecraft.world.level.redstone.Orientation;
- 
-+import org.bukkit.craftbukkit.event.CraftEventFactory; // CraftBukkit
-+
- public class ObserverBlock extends DirectionalBlock {
- 
-     public static final MapCodec<ObserverBlock> CODEC = simpleCodec(ObserverBlock::new);
-@@ -51,8 +53,18 @@
-     @Override
-     protected void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
-         if ((Boolean) state.getValue(ObserverBlock.POWERED)) {
-+            // CraftBukkit start
-+            if (CraftEventFactory.callRedstoneChange(world, pos, 15, 0).getNewCurrent() != 0) {
-+                return;
-+            }
-+            // CraftBukkit end
-             world.setBlock(pos, (BlockState) state.setValue(ObserverBlock.POWERED, false), 2);
-         } else {
-+            // CraftBukkit start
-+            if (CraftEventFactory.callRedstoneChange(world, pos, 0, 15).getNewCurrent() != 15) {
-+                return;
-+            }
-+            // CraftBukkit end
-             world.setBlock(pos, (BlockState) state.setValue(ObserverBlock.POWERED, true), 2);
-             world.scheduleTick(pos, (Block) this, 2);
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/PitcherCropBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/PitcherCropBlock.java.patch
deleted file mode 100644
index bdd3b319d7..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/PitcherCropBlock.java.patch
+++ /dev/null
@@ -1,28 +0,0 @@
---- a/net/minecraft/world/level/block/PitcherCropBlock.java
-+++ b/net/minecraft/world/level/block/PitcherCropBlock.java
-@@ -107,6 +107,7 @@
- 
-     @Override
-     public void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
-+        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
-         if (world instanceof ServerLevel serverLevel && entity instanceof Ravager && serverLevel.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) {
-             serverLevel.destroyBlock(pos, true, entity);
-         }
-@@ -131,7 +132,7 @@
-     @Override
-     public void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
-         float f = CropBlock.getGrowthSpeed(this, world, pos);
--        boolean bl = random.nextInt((int)(25.0F / f) + 1) == 0;
-+        boolean bl = random.nextFloat() < (world.spigotConfig.pitcherPlantModifier / (100.0F * (Math.floor(25.0F / f) + 1))); // Paper - Fix Spigot growth modifiers
-         if (bl) {
-             this.grow(world, state, pos, 1);
-         }
-@@ -141,7 +142,7 @@
-         int i = Math.min(state.getValue(AGE) + amount, 4);
-         if (this.canGrow(world, pos, state, i)) {
-             BlockState blockState = state.setValue(AGE, Integer.valueOf(i));
--            world.setBlock(pos, blockState, 2);
-+            if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(world, pos, blockState, 2)) return; // Paper
-             if (isDouble(i)) {
-                 world.setBlock(pos.above(), blockState.setValue(HALF, DoubleBlockHalf.UPPER), 3);
-             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/PointedDripstoneBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/PointedDripstoneBlock.java.patch
deleted file mode 100644
index 3536598553..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/PointedDripstoneBlock.java.patch
+++ /dev/null
@@ -1,96 +0,0 @@
---- a/net/minecraft/world/level/block/PointedDripstoneBlock.java
-+++ b/net/minecraft/world/level/block/PointedDripstoneBlock.java
-@@ -136,6 +136,11 @@
-                 ServerLevel worldserver = (ServerLevel) world;
- 
-                 if (projectile.mayInteract(worldserver, blockposition) && projectile.mayBreak(worldserver) && projectile instanceof ThrownTrident && projectile.getDeltaMovement().length() > 0.6D) {
-+                    // CraftBukkit start
-+                    if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(projectile, blockposition, state.getFluidState().createLegacyBlock())) { // Paper - fix wrong block state
-+                        return;
-+                    }
-+                    // CraftBukkit end
-                     world.destroyBlock(blockposition, true);
-                 }
-             }
-@@ -146,7 +151,7 @@
-     @Override
-     public void fallOn(Level world, BlockState state, BlockPos pos, Entity entity, float fallDistance) {
-         if (state.getValue(PointedDripstoneBlock.TIP_DIRECTION) == Direction.UP && state.getValue(PointedDripstoneBlock.THICKNESS) == DripstoneThickness.TIP) {
--            entity.causeFallDamage(fallDistance + 2.0F, 2.0F, world.damageSources().stalagmite());
-+            entity.causeFallDamage(fallDistance + 2.0F, 2.0F, world.damageSources().stalagmite().directBlock(world, pos)); // CraftBukkit
-         } else {
-             super.fallOn(world, state, pos, entity, fallDistance);
-         }
-@@ -214,10 +219,13 @@
-                             if (((PointedDripstoneBlock.FluidInfo) optional.get()).sourceState.is(Blocks.MUD) && fluidtype == Fluids.WATER) {
-                                 BlockState iblockdata1 = Blocks.CLAY.defaultBlockState();
- 
--                                world.setBlockAndUpdate(((PointedDripstoneBlock.FluidInfo) optional.get()).pos, iblockdata1);
-+                                // Paper start - Call BlockFormEvent
-+                                if (org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(world, ((PointedDripstoneBlock.FluidInfo) optional.get()).pos, iblockdata1)) {
-                                 Block.pushEntitiesUp(((PointedDripstoneBlock.FluidInfo) optional.get()).sourceState, iblockdata1, world, ((PointedDripstoneBlock.FluidInfo) optional.get()).pos);
-                                 world.gameEvent((Holder) GameEvent.BLOCK_CHANGE, ((PointedDripstoneBlock.FluidInfo) optional.get()).pos, GameEvent.Context.of(iblockdata1));
-                                 world.levelEvent(1504, blockposition1, 0);
-+                                }
-+                                // Paper end - Call BlockFormEvent
-                             } else {
-                                 BlockPos blockposition2 = PointedDripstoneBlock.findFillableCauldronBelowStalactiteTip(world, blockposition1, fluidtype);
- 
-@@ -391,15 +399,15 @@
-         if (PointedDripstoneBlock.isUnmergedTipWithDirection(iblockdata, direction.getOpposite())) {
-             PointedDripstoneBlock.createMergedTips(iblockdata, world, blockposition1);
-         } else if (iblockdata.isAir() || iblockdata.is(Blocks.WATER)) {
--            PointedDripstoneBlock.createDripstone(world, blockposition1, direction, DripstoneThickness.TIP);
-+            PointedDripstoneBlock.createDripstone(world, blockposition1, direction, DripstoneThickness.TIP, pos); // CraftBukkit
-         }
- 
-     }
- 
--    private static void createDripstone(LevelAccessor world, BlockPos pos, Direction direction, DripstoneThickness thickness) {
--        BlockState iblockdata = (BlockState) ((BlockState) ((BlockState) Blocks.POINTED_DRIPSTONE.defaultBlockState().setValue(PointedDripstoneBlock.TIP_DIRECTION, direction)).setValue(PointedDripstoneBlock.THICKNESS, thickness)).setValue(PointedDripstoneBlock.WATERLOGGED, world.getFluidState(pos).getType() == Fluids.WATER);
-+    private static void createDripstone(LevelAccessor generatoraccess, BlockPos blockposition, Direction enumdirection, DripstoneThickness dripstonethickness, BlockPos source) { // CraftBukkit
-+        BlockState iblockdata = (BlockState) ((BlockState) ((BlockState) Blocks.POINTED_DRIPSTONE.defaultBlockState().setValue(PointedDripstoneBlock.TIP_DIRECTION, enumdirection)).setValue(PointedDripstoneBlock.THICKNESS, dripstonethickness)).setValue(PointedDripstoneBlock.WATERLOGGED, generatoraccess.getFluidState(blockposition).getType() == Fluids.WATER);
- 
--        world.setBlock(pos, iblockdata, 3);
-+        org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(generatoraccess, source, blockposition, iblockdata, 3); // CraftBukkit
-     }
- 
-     private static void createMergedTips(BlockState state, LevelAccessor world, BlockPos pos) {
-@@ -414,8 +422,8 @@
-             blockposition1 = pos.below();
-         }
- 
--        PointedDripstoneBlock.createDripstone(world, blockposition2, Direction.DOWN, DripstoneThickness.TIP_MERGE);
--        PointedDripstoneBlock.createDripstone(world, blockposition1, Direction.UP, DripstoneThickness.TIP_MERGE);
-+        PointedDripstoneBlock.createDripstone(world, blockposition2, Direction.DOWN, DripstoneThickness.TIP_MERGE, pos); // CraftBukkit
-+        PointedDripstoneBlock.createDripstone(world, blockposition1, Direction.UP, DripstoneThickness.TIP_MERGE, pos); // CraftBukkit
-     }
- 
-     public static void spawnDripParticle(Level world, BlockPos pos, BlockState state) {
-@@ -448,7 +456,7 @@
- 
-             return (BlockPos) PointedDripstoneBlock.findBlockVertical(world, pos, enumdirection.getAxisDirection(), bipredicate, (iblockdata1) -> {
-                 return PointedDripstoneBlock.isTip(iblockdata1, allowMerged);
--            }, range).orElse((Object) null);
-+            }, range).orElse(null); // CraftBukkit - decompile error
-         }
-     }
- 
-@@ -564,7 +572,7 @@
-             return PointedDripstoneBlock.canDripThrough(world, blockposition1, iblockdata);
-         };
- 
--        return (BlockPos) PointedDripstoneBlock.findBlockVertical(world, pos, Direction.DOWN.getAxisDirection(), bipredicate, predicate, 11).orElse((Object) null);
-+        return (BlockPos) PointedDripstoneBlock.findBlockVertical(world, pos, Direction.DOWN.getAxisDirection(), bipredicate, predicate, 11).orElse(null); // CraftBukkit - decompile error
-     }
- 
-     @Nullable
-@@ -573,7 +581,7 @@
-             return PointedDripstoneBlock.canDripThrough(world, blockposition1, iblockdata);
-         };
- 
--        return (BlockPos) PointedDripstoneBlock.findBlockVertical(world, pos, Direction.UP.getAxisDirection(), bipredicate, PointedDripstoneBlock::canDrip, 11).orElse((Object) null);
-+        return (BlockPos) PointedDripstoneBlock.findBlockVertical(world, pos, Direction.UP.getAxisDirection(), bipredicate, PointedDripstoneBlock::canDrip, 11).orElse(null); // CraftBukkit - decompile error
-     }
- 
-     public static Fluid getCauldronFillFluidType(ServerLevel world, BlockPos pos) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/PowderSnowBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/PowderSnowBlock.java.patch
deleted file mode 100644
index 0f012c2b02..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/PowderSnowBlock.java.patch
+++ /dev/null
@@ -1,24 +0,0 @@
---- a/net/minecraft/world/level/block/PowderSnowBlock.java
-+++ b/net/minecraft/world/level/block/PowderSnowBlock.java
-@@ -59,6 +59,7 @@
- 
-     @Override
-     protected void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
-+        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
-         if (!(entity instanceof LivingEntity) || entity.getInBlockState().is((Block) this)) {
-             entity.makeStuckInBlock(state, new Vec3(0.8999999761581421D, 1.5D, 0.8999999761581421D));
-             if (world.isClientSide) {
-@@ -73,7 +74,12 @@
- 
-         entity.setIsInPowderSnow(true);
-         if (world instanceof ServerLevel worldserver) {
--            if (entity.isOnFire() && (worldserver.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) || entity instanceof Player) && entity.mayInteract(worldserver, pos)) {
-+            // CraftBukkit start
-+            if (entity.isOnFire() && entity.mayInteract(worldserver, pos)) {
-+                if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(entity, pos, Blocks.AIR.defaultBlockState(), !(worldserver.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) || entity instanceof Player))) {
-+                    return;
-+                }
-+                // CraftBukkit end
-                 world.destroyBlock(pos, false);
-             }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/PoweredRailBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/PoweredRailBlock.java.patch
deleted file mode 100644
index e0443a21f6..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/PoweredRailBlock.java.patch
+++ /dev/null
@@ -1,24 +0,0 @@
---- a/net/minecraft/world/level/block/PoweredRailBlock.java
-+++ b/net/minecraft/world/level/block/PoweredRailBlock.java
-@@ -11,6 +11,7 @@
- import net.minecraft.world.level.block.state.properties.EnumProperty;
- import net.minecraft.world.level.block.state.properties.Property;
- import net.minecraft.world.level.block.state.properties.RailShape;
-+import org.bukkit.craftbukkit.event.CraftEventFactory; // CraftBukkit
- 
- public class PoweredRailBlock extends BaseRailBlock {
- 
-@@ -120,6 +121,13 @@
-         boolean flag1 = world.hasNeighborSignal(pos) || this.findPoweredRailSignal(world, pos, state, true, 0) || this.findPoweredRailSignal(world, pos, state, false, 0);
- 
-         if (flag1 != flag) {
-+            // CraftBukkit start
-+            int power = flag ? 15 : 0;
-+            int newPower = CraftEventFactory.callRedstoneChange(world, pos, power, 15 - power).getNewCurrent();
-+            if (newPower == power) {
-+                return;
-+            }
-+            // CraftBukkit end
-             world.setBlock(pos, (BlockState) state.setValue(PoweredRailBlock.POWERED, flag1), 3);
-             world.updateNeighborsAt(pos.below(), this);
-             if (((RailShape) state.getValue(PoweredRailBlock.SHAPE)).isSlope()) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/PressurePlateBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/PressurePlateBlock.java.patch
deleted file mode 100644
index 4b6ba046eb..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/PressurePlateBlock.java.patch
+++ /dev/null
@@ -1,61 +0,0 @@
---- a/net/minecraft/world/level/block/PressurePlateBlock.java
-+++ b/net/minecraft/world/level/block/PressurePlateBlock.java
-@@ -5,6 +5,7 @@
- import net.minecraft.core.BlockPos;
- import net.minecraft.world.entity.Entity;
- import net.minecraft.world.entity.LivingEntity;
-+import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.block.state.BlockBehaviour;
- import net.minecraft.world.level.block.state.BlockState;
-@@ -12,6 +13,8 @@
- import net.minecraft.world.level.block.state.properties.BlockSetType;
- import net.minecraft.world.level.block.state.properties.BlockStateProperties;
- import net.minecraft.world.level.block.state.properties.BooleanProperty;
-+import org.bukkit.event.entity.EntityInteractEvent;
-+// CraftBukkit end
- 
- public class PressurePlateBlock extends BasePressurePlateBlock {
- 
-@@ -44,7 +47,7 @@
- 
-     @Override
-     protected int getSignalStrength(Level world, BlockPos pos) {
--        Class oclass;
-+        Class<? extends Entity> oclass; // CraftBukkit
- 
-         switch (this.type.pressurePlateSensitivity()) {
-             case EVERYTHING:
-@@ -59,7 +62,31 @@
- 
-         Class<? extends Entity> oclass1 = oclass;
- 
--        return getEntityCount(world, PressurePlateBlock.TOUCH_AABB.move(pos), oclass1) > 0 ? 15 : 0;
-+        // CraftBukkit start - Call interact event when turning on a pressure plate
-+        for (Entity entity : getEntities(world, PressurePlateBlock.TOUCH_AABB.move(pos), oclass)) {
-+            if (this.getSignalForState(world.getBlockState(pos)) == 0) {
-+                org.bukkit.World bworld = world.getWorld();
-+                org.bukkit.plugin.PluginManager manager = world.getCraftServer().getPluginManager();
-+                org.bukkit.event.Cancellable cancellable;
-+
-+                if (entity instanceof Player) {
-+                    cancellable = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerInteractEvent((Player) entity, org.bukkit.event.block.Action.PHYSICAL, pos, null, null, null);
-+                } else {
-+                    cancellable = new EntityInteractEvent(entity.getBukkitEntity(), bworld.getBlockAt(pos.getX(), pos.getY(), pos.getZ()));
-+                    manager.callEvent((EntityInteractEvent) cancellable);
-+                }
-+
-+                // We only want to block turning the plate on if all events are cancelled
-+                if (cancellable.isCancelled()) {
-+                    continue;
-+                }
-+            }
-+
-+            return 15;
-+        }
-+
-+        return 0;
-+        // CraftBukkit end
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/RedStoneOreBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/RedStoneOreBlock.java.patch
deleted file mode 100644
index 29e5229732..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/RedStoneOreBlock.java.patch
+++ /dev/null
@@ -1,102 +0,0 @@
---- a/net/minecraft/world/level/block/RedStoneOreBlock.java
-+++ b/net/minecraft/world/level/block/RedStoneOreBlock.java
-@@ -20,6 +20,10 @@
- import net.minecraft.world.level.block.state.StateDefinition;
- import net.minecraft.world.level.block.state.properties.BooleanProperty;
- import net.minecraft.world.phys.BlockHitResult;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.EntityInteractEvent;
-+// CraftBukkit end
- 
- public class RedStoneOreBlock extends Block {
- 
-@@ -38,14 +42,27 @@
- 
-     @Override
-     protected void attack(BlockState state, Level world, BlockPos pos, Player player) {
--        RedStoneOreBlock.interact(state, world, pos);
-+        RedStoneOreBlock.interact(state, world, pos, player); // CraftBukkit - add entityhuman
-         super.attack(state, world, pos, player);
-     }
- 
-     @Override
-     public void stepOn(Level world, BlockPos pos, BlockState state, Entity entity) {
-         if (!entity.isSteppingCarefully()) {
--            RedStoneOreBlock.interact(state, world, pos);
-+            // CraftBukkit start
-+            if (entity instanceof Player) {
-+                org.bukkit.event.player.PlayerInteractEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerInteractEvent((Player) entity, org.bukkit.event.block.Action.PHYSICAL, pos, null, null, null);
-+                if (!event.isCancelled()) {
-+                    RedStoneOreBlock.interact(world.getBlockState(pos), world, pos, entity); // add entity
-+                }
-+            } else {
-+                EntityInteractEvent event = new EntityInteractEvent(entity.getBukkitEntity(), world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ()));
-+                world.getCraftServer().getPluginManager().callEvent(event);
-+                if (!event.isCancelled()) {
-+                    RedStoneOreBlock.interact(world.getBlockState(pos), world, pos, entity); // add entity
-+                }
-+            }
-+            // CraftBukkit end
-         }
- 
-         super.stepOn(world, pos, state, entity);
-@@ -56,16 +73,21 @@
-         if (world.isClientSide) {
-             RedStoneOreBlock.spawnParticles(world, pos);
-         } else {
--            RedStoneOreBlock.interact(state, world, pos);
-+            RedStoneOreBlock.interact(state, world, pos, player); // CraftBukkit - add entityhuman
-         }
- 
-         return (InteractionResult) (stack.getItem() instanceof BlockItem && (new BlockPlaceContext(player, hand, stack, hit)).canPlace() ? InteractionResult.PASS : InteractionResult.SUCCESS);
-     }
- 
--    private static void interact(BlockState state, Level world, BlockPos pos) {
--        RedStoneOreBlock.spawnParticles(world, pos);
--        if (!(Boolean) state.getValue(RedStoneOreBlock.LIT)) {
--            world.setBlock(pos, (BlockState) state.setValue(RedStoneOreBlock.LIT, true), 3);
-+    private static void interact(BlockState iblockdata, Level world, BlockPos blockposition, Entity entity) { // CraftBukkit - add Entity
-+        RedStoneOreBlock.spawnParticles(world, blockposition);
-+        if (!(Boolean) iblockdata.getValue(RedStoneOreBlock.LIT)) {
-+            // CraftBukkit start
-+            if (!CraftEventFactory.callEntityChangeBlockEvent(entity, blockposition, iblockdata.setValue(RedStoneOreBlock.LIT, true))) {
-+                return;
-+            }
-+            // CraftBukkit end
-+            world.setBlock(blockposition, (BlockState) iblockdata.setValue(RedStoneOreBlock.LIT, true), 3);
-         }
- 
-     }
-@@ -78,6 +100,11 @@
-     @Override
-     protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
-         if ((Boolean) state.getValue(RedStoneOreBlock.LIT)) {
-+            // CraftBukkit start
-+            if (CraftEventFactory.callBlockFadeEvent(world, pos, state.setValue(RedStoneOreBlock.LIT, false)).isCancelled()) {
-+                return;
-+            }
-+            // CraftBukkit end
-             world.setBlock(pos, (BlockState) state.setValue(RedStoneOreBlock.LIT, false), 3);
-         }
- 
-@@ -86,10 +113,17 @@
-     @Override
-     protected void spawnAfterBreak(BlockState state, ServerLevel world, BlockPos pos, ItemStack tool, boolean dropExperience) {
-         super.spawnAfterBreak(state, world, pos, tool, dropExperience);
--        if (dropExperience) {
--            this.tryDropExperience(world, pos, tool, UniformInt.of(1, 5));
-+        // CraftBukkit start - Delegated to getExpDrop
-+    }
-+
-+    @Override
-+    public int getExpDrop(BlockState iblockdata, ServerLevel worldserver, BlockPos blockposition, ItemStack itemstack, boolean flag) {
-+        if (flag) {
-+            return this.tryDropExperience(worldserver, blockposition, itemstack, UniformInt.of(1, 5));
-         }
- 
-+        return 0;
-+        // CraftBukkit end
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/RedstoneLampBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/RedstoneLampBlock.java.patch
deleted file mode 100644
index dc1b67c34c..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/RedstoneLampBlock.java.patch
+++ /dev/null
@@ -1,35 +0,0 @@
---- a/net/minecraft/world/level/block/RedstoneLampBlock.java
-+++ b/net/minecraft/world/level/block/RedstoneLampBlock.java
-@@ -13,6 +13,8 @@
- import net.minecraft.world.level.block.state.properties.BooleanProperty;
- import net.minecraft.world.level.redstone.Orientation;
- 
-+import org.bukkit.craftbukkit.event.CraftEventFactory; // CraftBukkit
-+
- public class RedstoneLampBlock extends Block {
- 
-     public static final MapCodec<RedstoneLampBlock> CODEC = simpleCodec(RedstoneLampBlock::new);
-@@ -43,6 +45,11 @@
-                 if (flag1) {
-                     world.scheduleTick(pos, (Block) this, 4);
-                 } else {
-+                    // CraftBukkit start
-+                    if (CraftEventFactory.callRedstoneChange(world, pos, 0, 15).getNewCurrent() != 15) {
-+                        return;
-+                    }
-+                    // CraftBukkit end
-                     world.setBlock(pos, (BlockState) state.cycle(RedstoneLampBlock.LIT), 2);
-                 }
-             }
-@@ -53,6 +60,11 @@
-     @Override
-     protected void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
-         if ((Boolean) state.getValue(RedstoneLampBlock.LIT) && !world.hasNeighborSignal(pos)) {
-+            // CraftBukkit start
-+            if (CraftEventFactory.callRedstoneChange(world, pos, 15, 0).getNewCurrent() != 0) {
-+                return;
-+            }
-+            // CraftBukkit end
-             world.setBlock(pos, (BlockState) state.cycle(RedstoneLampBlock.LIT), 2);
-         }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/RespawnAnchorBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/RespawnAnchorBlock.java.patch
deleted file mode 100644
index 86e3e80aca..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/RespawnAnchorBlock.java.patch
+++ /dev/null
@@ -1,46 +0,0 @@
---- a/net/minecraft/world/level/block/RespawnAnchorBlock.java
-+++ b/net/minecraft/world/level/block/RespawnAnchorBlock.java
-@@ -88,9 +88,14 @@
-                 ServerPlayer entityplayer = (ServerPlayer) player;
- 
-                 if (entityplayer.getRespawnDimension() != world.dimension() || !pos.equals(entityplayer.getRespawnPosition())) {
--                    entityplayer.setRespawnPosition(world.dimension(), pos, 0.0F, false, true);
-+                    if (entityplayer.setRespawnPosition(world.dimension(), pos, 0.0F, false, true, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.RESPAWN_ANCHOR)) { // Paper - Add PlayerSetSpawnEvent
-                     world.playSound((Player) null, (double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D, SoundEvents.RESPAWN_ANCHOR_SET_SPAWN, SoundSource.BLOCKS, 1.0F, 1.0F);
-                     return InteractionResult.SUCCESS_SERVER;
-+                    // Paper start - Add PlayerSetSpawnEvent
-+                    } else {
-+                        return InteractionResult.FAIL;
-+                    }
-+                    // Paper end - Add PlayerSetSpawnEvent
-                 }
-             }
- 
-@@ -127,15 +132,16 @@
-     }
- 
-     private void explode(BlockState state, Level world, final BlockPos explodedPos) {
-+        org.bukkit.block.BlockState blockState = org.bukkit.craftbukkit.block.CraftBlock.at(world, explodedPos).getState(); // CraftBukkit - capture BlockState before remove block
-         world.removeBlock(explodedPos, false);
--        Stream stream = Direction.Plane.HORIZONTAL.stream();
-+        Stream<Direction> stream = Direction.Plane.HORIZONTAL.stream(); // CraftBukkit - decompile error
- 
-         Objects.requireNonNull(explodedPos);
-         boolean flag = stream.map(explodedPos::relative).anyMatch((blockposition1) -> {
-             return RespawnAnchorBlock.isWaterThatWouldFlow(blockposition1, world);
-         });
-         final boolean flag1 = flag || world.getFluidState(explodedPos.above()).is(FluidTags.WATER);
--        ExplosionDamageCalculator explosiondamagecalculator = new ExplosionDamageCalculator(this) {
-+        ExplosionDamageCalculator explosiondamagecalculator = new ExplosionDamageCalculator() { // CraftBukkit - decompile error
-             @Override
-             public Optional<Float> getBlockExplosionResistance(Explosion explosion, BlockGetter world, BlockPos pos, BlockState blockState, FluidState fluidState) {
-                 return pos.equals(explodedPos) && flag1 ? Optional.of(Blocks.WATER.getExplosionResistance()) : super.getBlockExplosionResistance(explosion, world, pos, blockState, fluidState);
-@@ -143,7 +149,7 @@
-         };
-         Vec3 vec3d = explodedPos.getCenter();
- 
--        world.explode((Entity) null, world.damageSources().badRespawnPointExplosion(vec3d), explosiondamagecalculator, vec3d, 5.0F, true, Level.ExplosionInteraction.BLOCK);
-+        world.explode((Entity) null, world.damageSources().badRespawnPointExplosion(vec3d, blockState), explosiondamagecalculator, vec3d, 5.0F, true, Level.ExplosionInteraction.BLOCK); // CraftBukkit - add state
-     }
- 
-     public static boolean canSetSpawn(Level world) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/SaplingBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/SaplingBlock.java.patch
deleted file mode 100644
index dbd9b130a0..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/SaplingBlock.java.patch
+++ /dev/null
@@ -1,117 +0,0 @@
---- a/net/minecraft/world/level/block/SaplingBlock.java
-+++ b/net/minecraft/world/level/block/SaplingBlock.java
-@@ -10,12 +10,19 @@
- import net.minecraft.world.level.LevelReader;
- import net.minecraft.world.level.block.grower.TreeGrower;
- import net.minecraft.world.level.block.state.BlockBehaviour;
--import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.block.state.StateDefinition;
- import net.minecraft.world.level.block.state.properties.BlockStateProperties;
- import net.minecraft.world.level.block.state.properties.IntegerProperty;
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+// CraftBukkit start
-+import org.bukkit.Location;
-+import org.bukkit.TreeType;
-+import org.bukkit.block.BlockState;
-+import org.bukkit.craftbukkit.block.CapturedBlockState;
-+import org.bukkit.craftbukkit.util.CraftLocation;
-+import org.bukkit.event.world.StructureGrowEvent;
-+// CraftBukkit end
- 
- public class SaplingBlock extends BushBlock implements BonemealableBlock {
- 
-@@ -28,6 +35,7 @@
-     protected static final float AABB_OFFSET = 6.0F;
-     protected static final VoxelShape SHAPE = Block.box(2.0D, 0.0D, 2.0D, 14.0D, 12.0D, 14.0D);
-     protected final TreeGrower treeGrower;
-+    public static TreeType treeType; // CraftBukkit
- 
-     @Override
-     public MapCodec<? extends SaplingBlock> codec() {
-@@ -37,48 +45,74 @@
-     protected SaplingBlock(TreeGrower generator, BlockBehaviour.Properties settings) {
-         super(settings);
-         this.treeGrower = generator;
--        this.registerDefaultState((BlockState) ((BlockState) this.stateDefinition.any()).setValue(SaplingBlock.STAGE, 0));
-+        this.registerDefaultState((net.minecraft.world.level.block.state.BlockState) ((net.minecraft.world.level.block.state.BlockState) this.stateDefinition.any()).setValue(SaplingBlock.STAGE, 0));
-     }
- 
-     @Override
--    protected VoxelShape getShape(BlockState state, BlockGetter world, BlockPos pos, CollisionContext context) {
-+    protected VoxelShape getShape(net.minecraft.world.level.block.state.BlockState state, BlockGetter world, BlockPos pos, CollisionContext context) {
-         return SaplingBlock.SHAPE;
-     }
- 
-     @Override
--    protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
--        if (world.getMaxLocalRawBrightness(pos.above()) >= 9 && random.nextInt(7) == 0) {
-+    protected void randomTick(net.minecraft.world.level.block.state.BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
-+        if (world.getMaxLocalRawBrightness(pos.above()) >= 9 && random.nextFloat() < (world.spigotConfig.saplingModifier / (100.0f * 7))) { // Spigot - SPIGOT-7159: Better modifier resolution
-             this.advanceTree(world, pos, state, random);
-         }
- 
-     }
- 
--    public void advanceTree(ServerLevel world, BlockPos pos, BlockState state, RandomSource random) {
-+    public void advanceTree(ServerLevel world, BlockPos pos, net.minecraft.world.level.block.state.BlockState state, RandomSource random) {
-         if ((Integer) state.getValue(SaplingBlock.STAGE) == 0) {
--            world.setBlock(pos, (BlockState) state.cycle(SaplingBlock.STAGE), 4);
-+            world.setBlock(pos, (net.minecraft.world.level.block.state.BlockState) state.cycle(SaplingBlock.STAGE), 4);
-         } else {
--            this.treeGrower.growTree(world, world.getChunkSource().getGenerator(), pos, state, random);
-+            // CraftBukkit start
-+            if (world.captureTreeGeneration) {
-+                this.treeGrower.growTree(world, world.getChunkSource().getGenerator(), pos, state, random);
-+            } else {
-+                world.captureTreeGeneration = true;
-+                this.treeGrower.growTree(world, world.getChunkSource().getGenerator(), pos, state, random);
-+                world.captureTreeGeneration = false;
-+                if (world.capturedBlockStates.size() > 0) {
-+                    TreeType treeType = SaplingBlock.treeType;
-+                    SaplingBlock.treeType = null;
-+                    Location location = CraftLocation.toBukkit(pos, world.getWorld());
-+                    java.util.List<BlockState> blocks = new java.util.ArrayList<>(world.capturedBlockStates.values());
-+                    world.capturedBlockStates.clear();
-+                    StructureGrowEvent event = null;
-+                    if (treeType != null) {
-+                        event = new StructureGrowEvent(location, treeType, false, null, blocks);
-+                        org.bukkit.Bukkit.getPluginManager().callEvent(event);
-+                    }
-+                    if (event == null || !event.isCancelled()) {
-+                        for (BlockState blockstate : blocks) {
-+                            CapturedBlockState.setBlockState(blockstate);
-+                            world.checkCapturedTreeStateForObserverNotify(pos, (org.bukkit.craftbukkit.block.CraftBlockState) blockstate); // Paper - notify observers even if grow failed
-+                        }
-+                    }
-+                }
-+            }
-+            // CraftBukkit end
-         }
- 
-     }
- 
-     @Override
--    public boolean isValidBonemealTarget(LevelReader world, BlockPos pos, BlockState state) {
-+    public boolean isValidBonemealTarget(LevelReader world, BlockPos pos, net.minecraft.world.level.block.state.BlockState state) {
-         return true;
-     }
- 
-     @Override
--    public boolean isBonemealSuccess(Level world, RandomSource random, BlockPos pos, BlockState state) {
-+    public boolean isBonemealSuccess(Level world, RandomSource random, BlockPos pos, net.minecraft.world.level.block.state.BlockState state) {
-         return (double) world.random.nextFloat() < 0.45D;
-     }
- 
-     @Override
--    public void performBonemeal(ServerLevel world, RandomSource random, BlockPos pos, BlockState state) {
-+    public void performBonemeal(ServerLevel world, RandomSource random, BlockPos pos, net.minecraft.world.level.block.state.BlockState state) {
-         this.advanceTree(world, pos, state, random);
-     }
- 
-     @Override
--    protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
-+    protected void createBlockStateDefinition(StateDefinition.Builder<Block, net.minecraft.world.level.block.state.BlockState> builder) {
-         builder.add(SaplingBlock.STAGE);
-     }
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/ScaffoldingBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/ScaffoldingBlock.java.patch
deleted file mode 100644
index d5960ee388..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/ScaffoldingBlock.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/level/block/ScaffoldingBlock.java
-+++ b/net/minecraft/world/level/block/ScaffoldingBlock.java
-@@ -103,7 +103,7 @@
-         int i = ScaffoldingBlock.getDistance(world, pos);
-         BlockState iblockdata1 = (BlockState) ((BlockState) state.setValue(ScaffoldingBlock.DISTANCE, i)).setValue(ScaffoldingBlock.BOTTOM, this.isBottom(world, pos, i));
- 
--        if ((Integer) iblockdata1.getValue(ScaffoldingBlock.DISTANCE) == 7) {
-+        if ((Integer) iblockdata1.getValue(ScaffoldingBlock.DISTANCE) == 7 && !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(world, pos, iblockdata1.getFluidState().createLegacyBlock()).isCancelled()) { // CraftBukkit - BlockFadeEvent // Paper - fix wrong block state
-             if ((Integer) state.getValue(ScaffoldingBlock.DISTANCE) == 7) {
-                 FallingBlockEntity.fall(world, pos, iblockdata1);
-             } else {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/SculkBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/SculkBlock.java.patch
deleted file mode 100644
index b7e23a6303..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/SculkBlock.java.patch
+++ /dev/null
@@ -1,16 +0,0 @@
---- a/net/minecraft/world/level/block/SculkBlock.java
-+++ b/net/minecraft/world/level/block/SculkBlock.java
-@@ -43,8 +43,11 @@
-                     BlockPos blockposition2 = blockposition1.above();
-                     BlockState iblockdata = this.getRandomGrowthState(world, blockposition2, random, spreadManager.isWorldGeneration());
- 
--                    world.setBlock(blockposition2, iblockdata, 3);
--                    world.playSound((Player) null, blockposition1, iblockdata.getSoundType().getPlaceSound(), SoundSource.BLOCKS, 1.0F, 1.0F);
-+                    // CraftBukkit start - Call BlockSpreadEvent
-+                    if (org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(world, catalystPos, blockposition2, iblockdata, 3)) {
-+                        world.playSound((Player) null, blockposition1, iblockdata.getSoundType().getPlaceSound(), SoundSource.BLOCKS, 1.0F, 1.0F);
-+                    }
-+                    // CraftBukkit end
-                 }
- 
-                 return Math.max(0, i - j);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/SculkCatalystBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/SculkCatalystBlock.java.patch
deleted file mode 100644
index 12314e3c0c..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/SculkCatalystBlock.java.patch
+++ /dev/null
@@ -1,21 +0,0 @@
---- a/net/minecraft/world/level/block/SculkCatalystBlock.java
-+++ b/net/minecraft/world/level/block/SculkCatalystBlock.java
-@@ -63,9 +63,16 @@
-     @Override
-     protected void spawnAfterBreak(BlockState state, ServerLevel world, BlockPos pos, ItemStack tool, boolean dropExperience) {
-         super.spawnAfterBreak(state, world, pos, tool, dropExperience);
--        if (dropExperience) {
--            this.tryDropExperience(world, pos, tool, this.xpRange);
-+        // CraftBukkit start - Delegate to getExpDrop
-+    }
-+
-+    @Override
-+    public int getExpDrop(BlockState iblockdata, ServerLevel worldserver, BlockPos blockposition, ItemStack itemstack, boolean flag) {
-+        if (flag) {
-+            return this.tryDropExperience(worldserver, blockposition, itemstack, this.xpRange);
-         }
- 
-+        return 0;
-+        // CraftBukkit end
-     }
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/SculkSensorBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/SculkSensorBlock.java.patch
deleted file mode 100644
index 8e63b99d10..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/SculkSensorBlock.java.patch
+++ /dev/null
@@ -1,88 +0,0 @@
---- a/net/minecraft/world/level/block/SculkSensorBlock.java
-+++ b/net/minecraft/world/level/block/SculkSensorBlock.java
-@@ -43,6 +43,10 @@
- import net.minecraft.world.level.pathfinder.PathComputationType;
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.event.block.BlockRedstoneEvent;
-+// CraftBukkit end
- 
- public class SculkSensorBlock extends BaseEntityBlock implements SimpleWaterloggedBlock {
- 
-@@ -104,6 +108,18 @@
-     @Override
-     public void stepOn(Level world, BlockPos pos, BlockState state, Entity entity) {
-         if (!world.isClientSide() && SculkSensorBlock.canActivate(state) && entity.getType() != EntityType.WARDEN) {
-+            // CraftBukkit start
-+            org.bukkit.event.Cancellable cancellable;
-+            if (entity instanceof Player) {
-+                cancellable = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerInteractEvent((Player) entity, org.bukkit.event.block.Action.PHYSICAL, pos, null, null, null);
-+            } else {
-+                cancellable = new org.bukkit.event.entity.EntityInteractEvent(entity.getBukkitEntity(), world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ()));
-+                world.getCraftServer().getPluginManager().callEvent((org.bukkit.event.entity.EntityInteractEvent) cancellable);
-+            }
-+            if (cancellable.isCancelled()) {
-+                return;
-+            }
-+            // CraftBukkit end
-             BlockEntity tileentity = world.getBlockEntity(pos);
- 
-             if (tileentity instanceof SculkSensorBlockEntity) {
-@@ -198,10 +214,19 @@
-     }
- 
-     public static boolean canActivate(BlockState state) {
--        return SculkSensorBlock.getPhase(state) == SculkSensorPhase.INACTIVE;
-+        return state.getBlock() instanceof SculkSensorBlock && SculkSensorBlock.getPhase(state) == SculkSensorPhase.INACTIVE; // Paper - Check for a valid type
-     }
- 
-     public static void deactivate(Level world, BlockPos pos, BlockState state) {
-+        // CraftBukkit start
-+        BlockRedstoneEvent eventRedstone = new BlockRedstoneEvent(CraftBlock.at(world, pos), state.getValue(SculkSensorBlock.POWER), 0);
-+        world.getCraftServer().getPluginManager().callEvent(eventRedstone);
-+
-+        if (eventRedstone.getNewCurrent() > 0) {
-+            world.setBlock(pos, state.setValue(SculkSensorBlock.POWER, eventRedstone.getNewCurrent()), 3);
-+            return;
-+        }
-+        // CraftBukkit end
-         world.setBlock(pos, (BlockState) ((BlockState) state.setValue(SculkSensorBlock.PHASE, SculkSensorPhase.COOLDOWN)).setValue(SculkSensorBlock.POWER, 0), 3);
-         world.scheduleTick(pos, state.getBlock(), 10);
-         SculkSensorBlock.updateNeighbours(world, pos, state);
-@@ -213,6 +238,15 @@
-     }
- 
-     public void activate(@Nullable Entity sourceEntity, Level world, BlockPos pos, BlockState state, int power, int frequency) {
-+        // CraftBukkit start
-+        BlockRedstoneEvent eventRedstone = new BlockRedstoneEvent(CraftBlock.at(world, pos), state.getValue(SculkSensorBlock.POWER), power);
-+        world.getCraftServer().getPluginManager().callEvent(eventRedstone);
-+
-+        if (eventRedstone.getNewCurrent() <= 0) {
-+            return;
-+        }
-+        power = eventRedstone.getNewCurrent();
-+        // CraftBukkit end
-         world.setBlock(pos, (BlockState) ((BlockState) state.setValue(SculkSensorBlock.PHASE, SculkSensorPhase.ACTIVE)).setValue(SculkSensorBlock.POWER, power), 3);
-         world.scheduleTick(pos, state.getBlock(), this.getActiveTicks());
-         SculkSensorBlock.updateNeighbours(world, pos, state);
-@@ -293,9 +327,16 @@
-     @Override
-     protected void spawnAfterBreak(BlockState state, ServerLevel world, BlockPos pos, ItemStack tool, boolean dropExperience) {
-         super.spawnAfterBreak(state, world, pos, tool, dropExperience);
--        if (dropExperience) {
--            this.tryDropExperience(world, pos, tool, ConstantInt.of(5));
-+        // CraftBukkit start - Delegate to getExpDrop
-+    }
-+
-+    @Override
-+    public int getExpDrop(BlockState iblockdata, ServerLevel worldserver, BlockPos blockposition, ItemStack itemstack, boolean flag) {
-+        if (flag) {
-+            return this.tryDropExperience(worldserver, blockposition, itemstack, ConstantInt.of(5));
-         }
- 
-+        return 0;
-+        // CraftBukkit end
-     }
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/SculkShriekerBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/SculkShriekerBlock.java.patch
deleted file mode 100644
index 8c254d2936..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/SculkShriekerBlock.java.patch
+++ /dev/null
@@ -1,30 +0,0 @@
---- a/net/minecraft/world/level/block/SculkShriekerBlock.java
-+++ b/net/minecraft/world/level/block/SculkShriekerBlock.java
-@@ -63,6 +63,7 @@
-             ServerPlayer entityplayer = SculkShriekerBlockEntity.tryGetPlayer(entity);
- 
-             if (entityplayer != null) {
-+                if (org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerInteractEvent(entityplayer, org.bukkit.event.block.Action.PHYSICAL, pos, null, null, null).isCancelled()) return; // CraftBukkit
-                 worldserver.getBlockEntity(pos, BlockEntityType.SCULK_SHRIEKER).ifPresent((sculkshriekerblockentity) -> {
-                     sculkshriekerblockentity.tryShriek(worldserver, entityplayer);
-                 });
-@@ -140,10 +141,17 @@
-     @Override
-     protected void spawnAfterBreak(BlockState state, ServerLevel world, BlockPos pos, ItemStack tool, boolean dropExperience) {
-         super.spawnAfterBreak(state, world, pos, tool, dropExperience);
--        if (dropExperience) {
--            this.tryDropExperience(world, pos, tool, ConstantInt.of(5));
-+        // CraftBukkit start - Delegate to getExpDrop
-+    }
-+
-+    @Override
-+    public int getExpDrop(BlockState iblockdata, ServerLevel worldserver, BlockPos blockposition, ItemStack itemstack, boolean flag) {
-+        if (flag) {
-+            return this.tryDropExperience(worldserver, blockposition, itemstack, ConstantInt.of(5));
-         }
- 
-+        return 0;
-+        // CraftBukkit end
-     }
- 
-     @Nullable
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/SculkSpreader.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/SculkSpreader.java.patch
deleted file mode 100644
index a334f182c5..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/SculkSpreader.java.patch
+++ /dev/null
@@ -1,98 +0,0 @@
---- a/net/minecraft/world/level/block/SculkSpreader.java
-+++ b/net/minecraft/world/level/block/SculkSpreader.java
-@@ -30,6 +30,7 @@
- import net.minecraft.core.Vec3i;
- import net.minecraft.nbt.CompoundTag;
- import net.minecraft.nbt.NbtOps;
-+import net.minecraft.nbt.Tag;
- import net.minecraft.server.level.ServerLevel;
- import net.minecraft.sounds.SoundEvents;
- import net.minecraft.sounds.SoundSource;
-@@ -37,9 +38,14 @@
- import net.minecraft.tags.TagKey;
- import net.minecraft.util.RandomSource;
- import net.minecraft.world.entity.player.Player;
-+import net.minecraft.world.level.Level;
- import net.minecraft.world.level.LevelAccessor;
- import net.minecraft.world.level.block.state.BlockState;
- import org.slf4j.Logger;
-+import org.bukkit.Bukkit;
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.event.block.SculkBloomEvent;
-+// CraftBukkit end
- 
- public class SculkSpreader {
- 
-@@ -57,6 +63,7 @@
-     private final int additionalDecayRate;
-     private List<SculkSpreader.ChargeCursor> cursors = new ArrayList();
-     private static final Logger LOGGER = LogUtils.getLogger();
-+    public Level level; // CraftBukkit
- 
-     public SculkSpreader(boolean worldGen, TagKey<Block> replaceableTag, int extraBlockChance, int maxDistance, int spreadChance, int decayChance) {
-         this.isWorldGeneration = worldGen;
-@@ -111,7 +118,7 @@
-     public void load(CompoundTag nbt) {
-         if (nbt.contains("cursors", 9)) {
-             this.cursors.clear();
--            DataResult dataresult = SculkSpreader.ChargeCursor.CODEC.listOf().parse(new Dynamic(NbtOps.INSTANCE, nbt.getList("cursors", 10)));
-+            DataResult<List<SculkSpreader.ChargeCursor>> dataresult = SculkSpreader.ChargeCursor.CODEC.listOf().parse(new Dynamic<>(NbtOps.INSTANCE, nbt.getList("cursors", 10))); // CraftBukkit - decompile error
-             Logger logger = SculkSpreader.LOGGER;
- 
-             Objects.requireNonNull(logger);
-@@ -119,14 +126,14 @@
-             int i = Math.min(list.size(), 32);
- 
-             for (int j = 0; j < i; ++j) {
--                this.addCursor((SculkSpreader.ChargeCursor) list.get(j));
-+                this.addCursor((SculkSpreader.ChargeCursor) list.get(j), false); // Paper - don't fire event for block entity loading
-             }
-         }
- 
-     }
- 
-     public void save(CompoundTag nbt) {
--        DataResult dataresult = SculkSpreader.ChargeCursor.CODEC.listOf().encodeStart(NbtOps.INSTANCE, this.cursors);
-+        DataResult<Tag> dataresult = SculkSpreader.ChargeCursor.CODEC.listOf().encodeStart(NbtOps.INSTANCE, this.cursors); // CraftBukkit - decompile error
-         Logger logger = SculkSpreader.LOGGER;
- 
-         Objects.requireNonNull(logger);
-@@ -139,14 +146,27 @@
-         while (charge > 0) {
-             int j = Math.min(charge, 1000);
- 
--            this.addCursor(new SculkSpreader.ChargeCursor(pos, j));
-+            this.addCursor(new SculkSpreader.ChargeCursor(pos, j), true); // Paper - allow firing event for other causes
-             charge -= j;
-         }
- 
-     }
- 
--    private void addCursor(SculkSpreader.ChargeCursor cursor) {
-+    private void addCursor(SculkSpreader.ChargeCursor cursor, boolean fireEvent) { // Paper - add boolean to conditionally fire SculkBloomEvent
-         if (this.cursors.size() < 32) {
-+            // CraftBukkit start
-+            if (!this.isWorldGeneration() && fireEvent) { // CraftBukkit - SPIGOT-7475: Don't call event during world generation // Paper - add boolean to conditionally fire SculkBloomEvent
-+                CraftBlock bukkitBlock = CraftBlock.at(this.level, cursor.pos);
-+                SculkBloomEvent event = new SculkBloomEvent(bukkitBlock, cursor.getCharge());
-+                Bukkit.getPluginManager().callEvent(event);
-+                if (event.isCancelled()) {
-+                    return;
-+                }
-+
-+                cursor.charge = event.getCharge();
-+            }
-+            // CraftBukkit end
-+
-             this.cursors.add(cursor);
-         }
-     }
-@@ -244,7 +264,7 @@
-             this.charge = charge;
-             this.decayDelay = decay;
-             this.updateDelay = update;
--            this.facings = (Set) faces.orElse((Object) null);
-+            this.facings = (Set) faces.orElse(null); // CraftBukkit - decompile error
-         }
- 
-         public ChargeCursor(BlockPos pos, int charge) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/SculkVeinBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/SculkVeinBlock.java.patch
deleted file mode 100644
index 4d009bd7c1..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/SculkVeinBlock.java.patch
+++ /dev/null
@@ -1,60 +0,0 @@
---- a/net/minecraft/world/level/block/SculkVeinBlock.java
-+++ b/net/minecraft/world/level/block/SculkVeinBlock.java
-@@ -101,28 +101,33 @@
- 
-     @Override
-     public int attemptUseCharge(SculkSpreader.ChargeCursor cursor, LevelAccessor world, BlockPos catalystPos, RandomSource random, SculkSpreader spreadManager, boolean shouldConvertToBlock) {
--        return shouldConvertToBlock && this.attemptPlaceSculk(spreadManager, world, cursor.getPos(), random) ? cursor.getCharge() - 1 : (random.nextInt(spreadManager.chargeDecayRate()) == 0 ? Mth.floor((float) cursor.getCharge() * 0.5F) : cursor.getCharge());
-+        // CraftBukkit - add source block
-+        return shouldConvertToBlock && this.attemptPlaceSculk(spreadManager, world, cursor.getPos(), random, catalystPos) ? cursor.getCharge() - 1 : (random.nextInt(spreadManager.chargeDecayRate()) == 0 ? Mth.floor((float) cursor.getCharge() * 0.5F) : cursor.getCharge());
-     }
- 
--    private boolean attemptPlaceSculk(SculkSpreader spreadManager, LevelAccessor world, BlockPos pos, RandomSource random) {
--        BlockState iblockdata = world.getBlockState(pos);
--        TagKey<Block> tagkey = spreadManager.replaceableBlocks();
--        Iterator iterator = Direction.allShuffled(random).iterator();
-+    private boolean attemptPlaceSculk(SculkSpreader sculkspreader, LevelAccessor generatoraccess, BlockPos blockposition, RandomSource randomsource, BlockPos sourceBlock) { // CraftBukkit
-+        BlockState iblockdata = generatoraccess.getBlockState(blockposition);
-+        TagKey<Block> tagkey = sculkspreader.replaceableBlocks();
-+        Iterator iterator = Direction.allShuffled(randomsource).iterator();
- 
-         while (iterator.hasNext()) {
-             Direction enumdirection = (Direction) iterator.next();
- 
-             if (hasFace(iblockdata, enumdirection)) {
--                BlockPos blockposition1 = pos.relative(enumdirection);
--                BlockState iblockdata1 = world.getBlockState(blockposition1);
-+                BlockPos blockposition1 = blockposition.relative(enumdirection);
-+                BlockState iblockdata1 = generatoraccess.getBlockState(blockposition1);
- 
-                 if (iblockdata1.is(tagkey)) {
-                     BlockState iblockdata2 = Blocks.SCULK.defaultBlockState();
- 
--                    world.setBlock(blockposition1, iblockdata2, 3);
--                    Block.pushEntitiesUp(iblockdata1, iblockdata2, world, blockposition1);
--                    world.playSound((Player) null, blockposition1, SoundEvents.SCULK_BLOCK_SPREAD, SoundSource.BLOCKS, 1.0F, 1.0F);
--                    this.veinSpreader.spreadAll(iblockdata2, world, blockposition1, spreadManager.isWorldGeneration());
-+                    // CraftBukkit start - Call BlockSpreadEvent
-+                    if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(generatoraccess, sourceBlock, blockposition1, iblockdata2, 3)) {
-+                        return false;
-+                    }
-+                    // CraftBukkit end
-+                    Block.pushEntitiesUp(iblockdata1, iblockdata2, generatoraccess, blockposition1);
-+                    generatoraccess.playSound((Player) null, blockposition1, SoundEvents.SCULK_BLOCK_SPREAD, SoundSource.BLOCKS, 1.0F, 1.0F);
-+                    this.veinSpreader.spreadAll(iblockdata2, generatoraccess, blockposition1, sculkspreader.isWorldGeneration());
-                     Direction enumdirection1 = enumdirection.getOpposite();
-                     Direction[] aenumdirection = SculkVeinBlock.DIRECTIONS;
-                     int i = aenumdirection.length;
-@@ -132,10 +137,10 @@
- 
-                         if (enumdirection2 != enumdirection1) {
-                             BlockPos blockposition2 = blockposition1.relative(enumdirection2);
--                            BlockState iblockdata3 = world.getBlockState(blockposition2);
-+                            BlockState iblockdata3 = generatoraccess.getBlockState(blockposition2);
- 
-                             if (iblockdata3.is((Block) this)) {
--                                this.onDischarged(world, iblockdata3, blockposition2, random);
-+                                this.onDischarged(generatoraccess, iblockdata3, blockposition2, randomsource);
-                             }
-                         }
-                     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/SignBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/SignBlock.java.patch
deleted file mode 100644
index 5dac1e3a2d..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/SignBlock.java.patch
+++ /dev/null
@@ -1,58 +0,0 @@
---- a/net/minecraft/world/level/block/SignBlock.java
-+++ b/net/minecraft/world/level/block/SignBlock.java
-@@ -140,7 +140,7 @@
-             } else if (flag1) {
-                 return InteractionResult.SUCCESS_SERVER;
-             } else if (!this.otherPlayerIsEditingSign(player, tileentitysign) && player.mayBuild() && this.hasEditableText(player, tileentitysign, flag)) {
--                this.openTextEdit(player, tileentitysign, flag);
-+                this.openTextEdit(player, tileentitysign, flag, io.papermc.paper.event.player.PlayerOpenSignEvent.Cause.INTERACT); // Paper - Add PlayerOpenSignEvent
-                 return InteractionResult.SUCCESS_SERVER;
-             } else {
-                 return InteractionResult.PASS;
-@@ -185,10 +185,36 @@
-         return blockpropertywood;
-     }
- 
-+    @io.papermc.paper.annotation.DoNotUse @Deprecated // Paper - Add PlayerOpenSignEvent
-     public void openTextEdit(Player player, SignBlockEntity blockEntity, boolean front) {
--        blockEntity.setAllowedPlayerEditor(player.getUUID());
--        player.openTextEdit(blockEntity, front);
-+        // Paper start - Add PlayerOpenSignEvent
-+        this.openTextEdit(player, blockEntity, front, io.papermc.paper.event.player.PlayerOpenSignEvent.Cause.UNKNOWN);
-     }
-+    public void openTextEdit(Player entityhuman, SignBlockEntity tileentitysign, boolean flag, io.papermc.paper.event.player.PlayerOpenSignEvent.Cause cause) {
-+        org.bukkit.entity.Player bukkitPlayer = (org.bukkit.entity.Player) entityhuman.getBukkitEntity();
-+        org.bukkit.block.Block bukkitBlock = org.bukkit.craftbukkit.block.CraftBlock.at(tileentitysign.getLevel(), tileentitysign.getBlockPos());
-+        org.bukkit.craftbukkit.block.CraftSign<?> bukkitSign = (org.bukkit.craftbukkit.block.CraftSign<?>) org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(bukkitBlock);
-+        io.papermc.paper.event.player.PlayerOpenSignEvent event = new io.papermc.paper.event.player.PlayerOpenSignEvent(
-+            bukkitPlayer,
-+            bukkitSign,
-+            flag ? org.bukkit.block.sign.Side.FRONT : org.bukkit.block.sign.Side.BACK,
-+            cause);
-+        if (!event.callEvent()) return;
-+        if (org.bukkit.event.player.PlayerSignOpenEvent.getHandlerList().getRegisteredListeners().length > 0) {
-+            final org.bukkit.event.player.PlayerSignOpenEvent.Cause legacyCause = switch (cause) {
-+                case PLACE -> org.bukkit.event.player.PlayerSignOpenEvent.Cause.PLACE;
-+                case PLUGIN -> org.bukkit.event.player.PlayerSignOpenEvent.Cause.PLUGIN;
-+                case INTERACT -> org.bukkit.event.player.PlayerSignOpenEvent.Cause.INTERACT;
-+                case UNKNOWN -> org.bukkit.event.player.PlayerSignOpenEvent.Cause.UNKNOWN;
-+            };
-+        if (!org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerSignOpenEvent(entityhuman, tileentitysign, flag, legacyCause)) {
-+        // Paper end - Add PlayerOpenSignEvent
-+            return;
-+        }
-+        } // Paper - Add PlayerOpenSignEvent
-+        tileentitysign.setAllowedPlayerEditor(entityhuman.getUUID());
-+        entityhuman.openTextEdit(tileentitysign, flag);
-+    }
- 
-     private boolean otherPlayerIsEditingSign(Player player, SignBlockEntity blockEntity) {
-         UUID uuid = blockEntity.getPlayerWhoMayEdit();
-@@ -199,6 +225,6 @@
-     @Nullable
-     @Override
-     public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level world, BlockState state, BlockEntityType<T> type) {
--        return createTickerHelper(type, BlockEntityType.SIGN, SignBlockEntity::tick);
-+        return null; // Craftbukkit - remove unnecessary sign ticking
-     }
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/SnowLayerBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/SnowLayerBlock.java.patch
deleted file mode 100644
index 5db7dbeea9..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/SnowLayerBlock.java.patch
+++ /dev/null
@@ -1,14 +0,0 @@
---- a/net/minecraft/world/level/block/SnowLayerBlock.java
-+++ b/net/minecraft/world/level/block/SnowLayerBlock.java
-@@ -99,6 +99,11 @@
-     @Override
-     protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
-         if (world.getBrightness(LightLayer.BLOCK, pos) > 11) {
-+            // CraftBukkit start
-+            if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(world, pos, Blocks.AIR.defaultBlockState()).isCancelled()) {
-+                return;
-+            }
-+            // CraftBukkit end
-             dropResources(state, world, pos);
-             world.removeBlock(pos, false);
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/SpawnerBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/SpawnerBlock.java.patch
deleted file mode 100644
index 8f4ea72cba..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/SpawnerBlock.java.patch
+++ /dev/null
@@ -1,26 +0,0 @@
---- a/net/minecraft/world/level/block/SpawnerBlock.java
-+++ b/net/minecraft/world/level/block/SpawnerBlock.java
-@@ -45,12 +45,20 @@
-     @Override
-     protected void spawnAfterBreak(BlockState state, ServerLevel world, BlockPos pos, ItemStack tool, boolean dropExperience) {
-         super.spawnAfterBreak(state, world, pos, tool, dropExperience);
--        if (dropExperience) {
--            int i = 15 + world.random.nextInt(15) + world.random.nextInt(15);
-+        // CraftBukkit start - Delegate to getExpDrop
-+    }
- 
--            this.popExperience(world, pos, i);
-+    @Override
-+    public int getExpDrop(BlockState iblockdata, ServerLevel worldserver, BlockPos blockposition, ItemStack itemstack, boolean flag) {
-+        if (flag) {
-+            int i = 15 + worldserver.random.nextInt(15) + worldserver.random.nextInt(15);
-+
-+            // this.popExperience(worldserver, blockposition, i);
-+            return i;
-         }
- 
-+        return 0;
-+        // CraftBukkit end
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/SpongeBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/SpongeBlock.java.patch
deleted file mode 100644
index d0c197c400..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/SpongeBlock.java.patch
+++ /dev/null
@@ -1,114 +0,0 @@
---- a/net/minecraft/world/level/block/SpongeBlock.java
-+++ b/net/minecraft/world/level/block/SpongeBlock.java
-@@ -15,6 +15,13 @@
- import net.minecraft.world.level.material.FluidState;
- import net.minecraft.world.level.redstone.Orientation;
- 
-+// CraftBukkit start
-+import java.util.List;
-+import org.bukkit.craftbukkit.block.CraftBlockState;
-+import org.bukkit.craftbukkit.util.BlockStateListPopulator;
-+import org.bukkit.event.block.SpongeAbsorbEvent;
-+// CraftBukkit end
-+
- public class SpongeBlock extends Block {
- 
-     public static final MapCodec<SpongeBlock> CODEC = simpleCodec(SpongeBlock::new);
-@@ -53,7 +60,8 @@
-     }
- 
-     private boolean removeWaterBreadthFirstSearch(Level world, BlockPos pos) {
--        return BlockPos.breadthFirstTraversal(pos, 6, 65, (blockposition1, consumer) -> {
-+        BlockStateListPopulator blockList = new BlockStateListPopulator(world); // CraftBukkit - Use BlockStateListPopulator
-+        BlockPos.breadthFirstTraversal(pos, 6, 65, (blockposition1, consumer) -> {
-             Direction[] aenumdirection = SpongeBlock.ALL_DIRECTIONS;
-             int i = aenumdirection.length;
- 
-@@ -67,8 +75,10 @@
-             if (blockposition1.equals(pos)) {
-                 return BlockPos.TraversalNodeStatus.ACCEPT;
-             } else {
--                BlockState iblockdata = world.getBlockState(blockposition1);
--                FluidState fluid = world.getFluidState(blockposition1);
-+                // CraftBukkit start
-+                BlockState iblockdata = blockList.getBlockState(blockposition1);
-+                FluidState fluid = blockList.getFluidState(blockposition1);
-+                // CraftBukkit end
- 
-                 if (!fluid.is(FluidTags.WATER)) {
-                     return BlockPos.TraversalNodeStatus.SKIP;
-@@ -78,27 +88,68 @@
-                     if (block instanceof BucketPickup) {
-                         BucketPickup ifluidsource = (BucketPickup) block;
- 
--                        if (!ifluidsource.pickupBlock((Player) null, world, blockposition1, iblockdata).isEmpty()) {
-+                        if (!ifluidsource.pickupBlock((Player) null, blockList, blockposition1, iblockdata).isEmpty()) { // CraftBukkit
-                             return BlockPos.TraversalNodeStatus.ACCEPT;
-                         }
-                     }
- 
-                     if (iblockdata.getBlock() instanceof LiquidBlock) {
--                        world.setBlock(blockposition1, Blocks.AIR.defaultBlockState(), 3);
-+                        blockList.setBlock(blockposition1, Blocks.AIR.defaultBlockState(), 3); // CraftBukkit
-                     } else {
-                         if (!iblockdata.is(Blocks.KELP) && !iblockdata.is(Blocks.KELP_PLANT) && !iblockdata.is(Blocks.SEAGRASS) && !iblockdata.is(Blocks.TALL_SEAGRASS)) {
-                             return BlockPos.TraversalNodeStatus.SKIP;
-                         }
- 
--                        BlockEntity tileentity = iblockdata.hasBlockEntity() ? world.getBlockEntity(blockposition1) : null;
-+                        // CraftBukkit start
-+                        // TileEntity tileentity = iblockdata.hasBlockEntity() ? world.getBlockEntity(blockposition1) : null;
- 
--                        dropResources(iblockdata, world, blockposition1, tileentity);
--                        world.setBlock(blockposition1, Blocks.AIR.defaultBlockState(), 3);
-+                        // dropResources(iblockdata, world, blockposition1, tileentity);
-+                        blockList.setBlock(blockposition1, Blocks.AIR.defaultBlockState(), 3);
-+                        // CraftBukkit end
-                     }
- 
-                     return BlockPos.TraversalNodeStatus.ACCEPT;
-                 }
-             }
--        }) > 1;
-+        });
-+        // CraftBukkit start
-+        List<CraftBlockState> blocks = blockList.getList(); // Is a clone
-+        if (!blocks.isEmpty()) {
-+            final org.bukkit.block.Block bblock = world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ());
-+
-+            SpongeAbsorbEvent event = new SpongeAbsorbEvent(bblock, (List<org.bukkit.block.BlockState>) (List) blocks);
-+            world.getCraftServer().getPluginManager().callEvent(event);
-+
-+            if (event.isCancelled()) {
-+                return false;
-+            }
-+
-+            for (CraftBlockState block : blocks) {
-+                BlockPos blockposition1 = block.getPosition();
-+                BlockState iblockdata = world.getBlockState(blockposition1);
-+                FluidState fluid = world.getFluidState(blockposition1);
-+
-+                if (fluid.is(FluidTags.WATER)) {
-+                    if (iblockdata.getBlock() instanceof BucketPickup && !((BucketPickup) iblockdata.getBlock()).pickupBlock((Player) null, blockList, blockposition1, iblockdata).isEmpty()) {
-+                        // NOP
-+                    } else if (iblockdata.getBlock() instanceof LiquidBlock) {
-+                        // NOP
-+                    } else if (iblockdata.is(Blocks.KELP) || iblockdata.is(Blocks.KELP_PLANT) || iblockdata.is(Blocks.SEAGRASS) || iblockdata.is(Blocks.TALL_SEAGRASS)) {
-+                        BlockEntity tileentity = iblockdata.hasBlockEntity() ? world.getBlockEntity(blockposition1) : null;
-+
-+                        // Paper start - Fix SpongeAbsortEvent handling
-+                        if (block.getHandle().isAir()) {
-+                        dropResources(iblockdata, world, blockposition1, tileentity);
-+                        }
-+                        // Paper end - Fix SpongeAbsortEvent handling
-+                    }
-+                }
-+                world.setBlock(blockposition1, block.getHandle(), block.getFlag());
-+            }
-+
-+            return true;
-+        }
-+        return false;
-+        // CraftBukkit end
-     }
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java.patch
deleted file mode 100644
index 5777f59736..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java.patch
+++ /dev/null
@@ -1,25 +0,0 @@
---- a/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java
-+++ b/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java
-@@ -43,7 +43,13 @@
- 
-     @Override
-     protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
-+        if (this instanceof GrassBlock && world.paperConfig().tickRates.grassSpread != 1 && (world.paperConfig().tickRates.grassSpread < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % world.paperConfig().tickRates.grassSpread != 0)) { return; } // Paper - Configurable random tick rates for blocks
-         if (!SpreadingSnowyDirtBlock.canBeGrass(state, world, pos)) {
-+            // CraftBukkit start
-+            if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(world, pos, Blocks.DIRT.defaultBlockState()).isCancelled()) {
-+                return;
-+            }
-+            // CraftBukkit end
-             world.setBlockAndUpdate(pos, Blocks.DIRT.defaultBlockState());
-         } else {
-             if (world.getMaxLocalRawBrightness(pos.above()) >= 9) {
-@@ -53,7 +59,7 @@
-                     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)) {
--                        world.setBlockAndUpdate(blockposition1, (BlockState) iblockdata1.setValue(SpreadingSnowyDirtBlock.SNOWY, isSnowySetting(world.getBlockState(blockposition1.above()))));
-+                        org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(world, pos, blockposition1, (BlockState) iblockdata1.setValue(SpreadingSnowyDirtBlock.SNOWY, isSnowySetting(world.getBlockState(blockposition1.above())))); // CraftBukkit
-                     }
-                 }
-             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/StemBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/StemBlock.java.patch
deleted file mode 100644
index 625e104922..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/StemBlock.java.patch
+++ /dev/null
@@ -1,47 +0,0 @@
---- a/net/minecraft/world/level/block/StemBlock.java
-+++ b/net/minecraft/world/level/block/StemBlock.java
-@@ -26,6 +26,7 @@
- import net.minecraft.world.level.block.state.properties.IntegerProperty;
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+import org.bukkit.craftbukkit.event.CraftEventFactory; // CraftBukkit
- 
- public class StemBlock extends BushBlock implements BonemealableBlock {
- 
-@@ -74,12 +75,12 @@
-         if (world.getRawBrightness(pos, 0) >= 9) {
-             float f = CropBlock.getGrowthSpeed(this, world, pos);
- 
--            if (random.nextInt((int) (25.0F / f) + 1) == 0) {
-+            if (random.nextFloat() < ((this == Blocks.PUMPKIN_STEM ? world.spigotConfig.pumpkinModifier : world.spigotConfig.melonModifier) / (100.0f * (Math.floor((25.0F / f) + 1))))) { // Spigot - SPIGOT-7159: Better modifier resolution
-                 int i = (Integer) state.getValue(StemBlock.AGE);
- 
-                 if (i < 7) {
-                     state = (BlockState) state.setValue(StemBlock.AGE, i + 1);
--                    world.setBlock(pos, state, 2);
-+                    CraftEventFactory.handleBlockGrowEvent(world, pos, state, 2); // CraftBukkit
-                 } else {
-                     Direction enumdirection = Direction.Plane.HORIZONTAL.getRandomDirection(random);
-                     BlockPos blockposition1 = pos.relative(enumdirection);
-@@ -91,7 +92,11 @@
-                         Optional<Block> optional1 = iregistry.getOptional(this.attachedStem);
- 
-                         if (optional.isPresent() && optional1.isPresent()) {
--                            world.setBlockAndUpdate(blockposition1, ((Block) optional.get()).defaultBlockState());
-+                            // CraftBukkit start
-+                            if (!CraftEventFactory.handleBlockGrowEvent(world, blockposition1, ((Block) optional.get()).defaultBlockState())) {
-+                                return;
-+                            }
-+                            // CraftBukkit end
-                             world.setBlockAndUpdate(pos, (BlockState) ((Block) optional1.get()).defaultBlockState().setValue(HorizontalDirectionalBlock.FACING, enumdirection));
-                         }
-                     }
-@@ -121,7 +126,7 @@
-         int i = Math.min(7, (Integer) state.getValue(StemBlock.AGE) + Mth.nextInt(world.random, 2, 5));
-         BlockState iblockdata1 = (BlockState) state.setValue(StemBlock.AGE, i);
- 
--        world.setBlock(pos, iblockdata1, 2);
-+        CraftEventFactory.handleBlockGrowEvent(world, pos, iblockdata1, 2); // CraftBukkit
-         if (i == 7) {
-             iblockdata1.randomTick(world, pos, world.random);
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/SugarCaneBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/SugarCaneBlock.java.patch
deleted file mode 100644
index 903b77b2e2..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/SugarCaneBlock.java.patch
+++ /dev/null
@@ -1,21 +0,0 @@
---- a/net/minecraft/world/level/block/SugarCaneBlock.java
-+++ b/net/minecraft/world/level/block/SugarCaneBlock.java
-@@ -59,13 +59,14 @@
-                 ;
-             }
- 
--            if (i < 3) {
-+            if (i < world.paperConfig().maxGrowthHeight.reeds) { // Paper - Configurable cactus/bamboo/reed growth heigh
-                 int j = (Integer) state.getValue(SugarCaneBlock.AGE);
- 
--                if (j == 15) {
--                    world.setBlockAndUpdate(pos.above(), this.defaultBlockState());
-+                int modifier = world.spigotConfig.caneModifier; // Spigot - SPIGOT-7159: Better modifier resolution
-+                if (j >= 15 || (modifier != 100 && random.nextFloat() < (modifier / (100.0f * 16)))) { // Spigot - SPIGOT-7159: Better modifier resolution
-+                    org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockGrowEvent(world, pos.above(), this.defaultBlockState()); // CraftBukkit
-                     world.setBlock(pos, (BlockState) state.setValue(SugarCaneBlock.AGE, 0), 4);
--                } else {
-+                } else if (modifier == 100 || random.nextFloat() < (modifier / (100.0f * 16))) { // Spigot - SPIGOT-7159: Better modifier resolution
-                     world.setBlock(pos, (BlockState) state.setValue(SugarCaneBlock.AGE, j + 1), 4);
-                 }
-             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/SweetBerryBushBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/SweetBerryBushBlock.java.patch
deleted file mode 100644
index c0fadbe893..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/SweetBerryBushBlock.java.patch
+++ /dev/null
@@ -1,62 +0,0 @@
---- a/net/minecraft/world/level/block/SweetBerryBushBlock.java
-+++ b/net/minecraft/world/level/block/SweetBerryBushBlock.java
-@@ -28,6 +28,12 @@
- import net.minecraft.world.phys.Vec3;
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+// CraftBukkit start
-+import java.util.Collections;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.event.player.PlayerHarvestBlockEvent;
-+// CraftBukkit end
- 
- public class SweetBerryBushBlock extends BushBlock implements BonemealableBlock {
- 
-@@ -67,10 +73,10 @@
-     protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
-         int i = (Integer) state.getValue(SweetBerryBushBlock.AGE);
- 
--        if (i < 3 && random.nextInt(5) == 0 && world.getRawBrightness(pos.above(), 0) >= 9) {
-+        if (i < 3 && random.nextFloat() < (world.spigotConfig.sweetBerryModifier / (100.0f * 5)) && world.getRawBrightness(pos.above(), 0) >= 9) { // Spigot - SPIGOT-7159: Better modifier resolution
-             BlockState iblockdata1 = (BlockState) state.setValue(SweetBerryBushBlock.AGE, i + 1);
- 
--            world.setBlock(pos, iblockdata1, 2);
-+            if (!CraftEventFactory.handleBlockGrowEvent(world, pos, iblockdata1, 2)) return; // CraftBukkit
-             world.gameEvent((Holder) GameEvent.BLOCK_CHANGE, pos, GameEvent.Context.of(iblockdata1));
-         }
- 
-@@ -78,6 +84,7 @@
- 
-     @Override
-     protected void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
-+        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
-         if (entity instanceof LivingEntity && entity.getType() != EntityType.FOX && entity.getType() != EntityType.BEE) {
-             entity.makeStuckInBlock(state, new Vec3(0.800000011920929D, 0.75D, 0.800000011920929D));
-             if (world instanceof ServerLevel) {
-@@ -91,7 +98,7 @@
-                         double d1 = Math.abs(vec3d.z());
- 
-                         if (d0 >= 0.003000000026077032D || d1 >= 0.003000000026077032D) {
--                            entity.hurtServer(worldserver, world.damageSources().sweetBerryBush(), 1.0F);
-+                            entity.hurtServer(worldserver, world.damageSources().sweetBerryBush().directBlock(world, pos), 1.0F); // CraftBukkit
-                         }
-                     }
- 
-@@ -118,7 +125,15 @@
-         if (i > 1) {
-             int j = 1 + world.random.nextInt(2);
- 
--            popResource(world, pos, new ItemStack(Items.SWEET_BERRIES, j + (flag ? 1 : 0)));
-+            // CraftBukkit start - useWithoutItem is always MAIN_HAND
-+            PlayerHarvestBlockEvent event = CraftEventFactory.callPlayerHarvestBlockEvent(world, pos, player, InteractionHand.MAIN_HAND, Collections.singletonList(new ItemStack(Items.SWEET_BERRIES, j + (flag ? 1 : 0))));
-+            if (event.isCancelled()) {
-+                return InteractionResult.SUCCESS; // We need to return a success either way, because making it PASS or FAIL will result in a bug where cancelling while harvesting w/ block in hand places block
-+            }
-+            for (org.bukkit.inventory.ItemStack itemStack : event.getItemsHarvested()) {
-+                popResource(world, pos, CraftItemStack.asNMSCopy(itemStack));
-+            }
-+            // CraftBukkit end
-             world.playSound((Player) null, pos, SoundEvents.SWEET_BERRY_BUSH_PICK_BERRIES, SoundSource.BLOCKS, 1.0F, 0.8F + world.random.nextFloat() * 0.4F);
-             BlockState iblockdata1 = (BlockState) state.setValue(SweetBerryBushBlock.AGE, 1);
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/TntBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/TntBlock.java.patch
deleted file mode 100644
index 42cf7a6881..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/TntBlock.java.patch
+++ /dev/null
@@ -1,102 +0,0 @@
---- a/net/minecraft/world/level/block/TntBlock.java
-+++ b/net/minecraft/world/level/block/TntBlock.java
-@@ -28,6 +28,10 @@
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.level.redstone.Orientation;
- import net.minecraft.world.phys.BlockHitResult;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.block.TNTPrimeEvent.PrimeCause;
-+// CraftBukkit end
- 
- public class TntBlock extends Block {
- 
-@@ -47,7 +51,13 @@
-     @Override
-     protected void onPlace(BlockState state, Level world, BlockPos pos, BlockState oldState, boolean notify) {
-         if (!oldState.is(state.getBlock())) {
--            if (world.hasNeighborSignal(pos)) {
-+            if (world.hasNeighborSignal(pos) && CraftEventFactory.callTNTPrimeEvent(world, pos, PrimeCause.REDSTONE, null, null)) { // CraftBukkit - TNTPrimeEvent
-+                // Paper start - TNTPrimeEvent
-+                org.bukkit.block.Block tntBlock = org.bukkit.craftbukkit.block.CraftBlock.at(world, pos);
-+                if (!new com.destroystokyo.paper.event.block.TNTPrimeEvent(tntBlock, com.destroystokyo.paper.event.block.TNTPrimeEvent.PrimeReason.REDSTONE, null).callEvent()) {
-+                    return;
-+                }
-+                // Paper end - TNTPrimeEvent
-                 TntBlock.explode(world, pos);
-                 world.removeBlock(pos, false);
-             }
-@@ -57,7 +67,13 @@
- 
-     @Override
-     protected void neighborChanged(BlockState state, Level world, BlockPos pos, Block sourceBlock, @Nullable Orientation wireOrientation, boolean notify) {
--        if (world.hasNeighborSignal(pos)) {
-+        if (world.hasNeighborSignal(pos) && CraftEventFactory.callTNTPrimeEvent(world, pos, PrimeCause.REDSTONE, null, null)) { // CraftBukkit - TNTPrimeEvent
-+            // Paper start - TNTPrimeEvent
-+            org.bukkit.block.Block tntBlock = org.bukkit.craftbukkit.block.CraftBlock.at(world, pos);
-+            if (!new com.destroystokyo.paper.event.block.TNTPrimeEvent(tntBlock, com.destroystokyo.paper.event.block.TNTPrimeEvent.PrimeReason.REDSTONE, null).callEvent()) {
-+                return;
-+            }
-+            // Paper end - TNTPrimeEvent
-             TntBlock.explode(world, pos);
-             world.removeBlock(pos, false);
-         }
-@@ -66,7 +82,7 @@
- 
-     @Override
-     public BlockState playerWillDestroy(Level world, BlockPos pos, BlockState state, Player player) {
--        if (!world.isClientSide() && !player.isCreative() && (Boolean) state.getValue(TntBlock.UNSTABLE)) {
-+        if (!world.isClientSide() && !player.isCreative() && (Boolean) state.getValue(TntBlock.UNSTABLE) && CraftEventFactory.callTNTPrimeEvent(world, pos, PrimeCause.BLOCK_BREAK, player, null)) { // CraftBukkit - TNTPrimeEvent
-             TntBlock.explode(world, pos);
-         }
- 
-@@ -75,6 +91,13 @@
- 
-     @Override
-     public void wasExploded(ServerLevel world, BlockPos pos, Explosion explosion) {
-+        // Paper start - TNTPrimeEvent
-+        org.bukkit.block.Block tntBlock = org.bukkit.craftbukkit.block.CraftBlock.at(world, pos);
-+        org.bukkit.entity.Entity source = explosion.getDirectSourceEntity() != null ? explosion.getDirectSourceEntity().getBukkitEntity() : null;
-+        if (!new com.destroystokyo.paper.event.block.TNTPrimeEvent(tntBlock, com.destroystokyo.paper.event.block.TNTPrimeEvent.PrimeReason.EXPLOSION, source).callEvent()) {
-+            return;
-+        }
-+        // Paper end - TNTPrimeEvent
-         PrimedTnt entitytntprimed = new PrimedTnt(world, (double) pos.getX() + 0.5D, (double) pos.getY(), (double) pos.getZ() + 0.5D, explosion.getIndirectSourceEntity());
-         int i = entitytntprimed.getFuse();
- 
-@@ -101,6 +124,17 @@
-         if (!stack.is(Items.FLINT_AND_STEEL) && !stack.is(Items.FIRE_CHARGE)) {
-             return super.useItemOn(stack, state, world, pos, player, hand, hit);
-         } else {
-+            // CraftBukkit start - TNTPrimeEvent
-+            if (!CraftEventFactory.callTNTPrimeEvent(world, pos, PrimeCause.PLAYER, player, null)) {
-+                return InteractionResult.CONSUME;
-+            }
-+            // CraftBukkit end
-+            // Paper start - TNTPrimeEvent
-+            org.bukkit.block.Block tntBlock = org.bukkit.craftbukkit.block.CraftBlock.at(world, pos);
-+            if (!new com.destroystokyo.paper.event.block.TNTPrimeEvent(tntBlock, com.destroystokyo.paper.event.block.TNTPrimeEvent.PrimeReason.ITEM, player.getBukkitEntity()).callEvent()) {
-+                return InteractionResult.FAIL;
-+            }
-+            // Paper end - TNTPrimeEvent
-             TntBlock.explode(world, pos, player);
-             world.setBlock(pos, Blocks.AIR.defaultBlockState(), 11);
-             Item item = stack.getItem();
-@@ -123,6 +157,17 @@
-             Entity entity = projectile.getOwner();
- 
-             if (projectile.isOnFire() && projectile.mayInteract(worldserver, blockposition)) {
-+                // CraftBukkit start
-+                if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(projectile, blockposition, state.getFluidState().createLegacyBlock()) || !CraftEventFactory.callTNTPrimeEvent(world, blockposition, PrimeCause.PROJECTILE, projectile, null)) { // Paper - fix wrong block state
-+                    return;
-+                }
-+                // CraftBukkit end
-+                // Paper start - TNTPrimeEvent
-+                org.bukkit.block.Block tntBlock = org.bukkit.craftbukkit.block.CraftBlock.at(world, blockposition);
-+                if (!new com.destroystokyo.paper.event.block.TNTPrimeEvent(tntBlock, com.destroystokyo.paper.event.block.TNTPrimeEvent.PrimeReason.PROJECTILE, projectile.getBukkitEntity()).callEvent()) {
-+                    return;
-+                }
-+                // Paper end - TNTPrimeEvent
-                 TntBlock.explode(world, blockposition, entity instanceof LivingEntity ? (LivingEntity) entity : null);
-                 world.removeBlock(blockposition, false);
-             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/TrapDoorBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/TrapDoorBlock.java.patch
deleted file mode 100644
index 0f858bdcd3..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/TrapDoorBlock.java.patch
+++ /dev/null
@@ -1,51 +0,0 @@
---- a/net/minecraft/world/level/block/TrapDoorBlock.java
-+++ b/net/minecraft/world/level/block/TrapDoorBlock.java
-@@ -37,6 +37,7 @@
- import net.minecraft.world.phys.BlockHitResult;
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+import org.bukkit.event.block.BlockRedstoneEvent; // CraftBukkit
- 
- public class TrapDoorBlock extends HorizontalDirectionalBlock implements SimpleWaterloggedBlock {
- 
-@@ -143,7 +144,39 @@
-             boolean flag1 = world.hasNeighborSignal(pos);
- 
-             if (flag1 != (Boolean) state.getValue(TrapDoorBlock.POWERED)) {
--                if ((Boolean) state.getValue(TrapDoorBlock.OPEN) != flag1) {
-+                // CraftBukkit start
-+                org.bukkit.World bworld = world.getWorld();
-+                org.bukkit.block.Block bblock = bworld.getBlockAt(pos.getX(), pos.getY(), pos.getZ());
-+
-+                int power = bblock.getBlockPower();
-+                int oldPower = (Boolean) state.getValue(TrapDoorBlock.OPEN) ? 15 : 0;
-+
-+                if (oldPower == 0 ^ power == 0 || sourceBlock.defaultBlockState().isSignalSource()) {
-+                    BlockRedstoneEvent eventRedstone = new BlockRedstoneEvent(bblock, oldPower, power);
-+                    world.getCraftServer().getPluginManager().callEvent(eventRedstone);
-+                    flag1 = eventRedstone.getNewCurrent() > 0;
-+                }
-+                // CraftBukkit end
-+                // Paper start - break redstone on trapdoors early
-+                boolean open = (Boolean) state.getValue(TrapDoorBlock.OPEN) != flag1;
-+                // note: this must run before any state for this block/its neighborus are written to the world
-+                // we allow the redstone event to fire so that plugins can block
-+                if (flag1 && open) { // if we are now powered and it caused the trap door to open
-+                    // in this case, first check for the redstone on top first
-+                    BlockPos abovePos = pos.above();
-+                    BlockState above = world.getBlockState(abovePos);
-+                    if (above.getBlock() instanceof RedStoneWireBlock) {
-+                        world.setBlock(abovePos, Blocks.AIR.defaultBlockState(), Block.UPDATE_CLIENTS | Block.UPDATE_NEIGHBORS);
-+                        Block.popResource(world, abovePos, new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.REDSTONE));
-+                        // now check that this didn't change our state
-+                        if (world.getBlockState(pos) != state) {
-+                            // our state was changed, so we cannot propagate this update
-+                            return;
-+                        }
-+                    }
-+                }
-+                if (open) {
-+                // Paper end - break redstone on trapdoors early
-                     state = (BlockState) state.setValue(TrapDoorBlock.OPEN, flag1);
-                     this.playSound((Player) null, world, pos, flag1);
-                 }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/TripWireBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/TripWireBlock.java.patch
deleted file mode 100644
index b081063a3f..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/TripWireBlock.java.patch
+++ /dev/null
@@ -1,114 +0,0 @@
---- a/net/minecraft/world/level/block/TripWireBlock.java
-+++ b/net/minecraft/world/level/block/TripWireBlock.java
-@@ -28,6 +28,7 @@
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+import org.bukkit.event.entity.EntityInteractEvent; // CraftBukkit
- 
- public class TripWireBlock extends Block {
- 
-@@ -67,6 +68,7 @@
- 
-     @Override
-     public BlockState getStateForPlacement(BlockPlaceContext ctx) {
-+        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableTripwireUpdates) return this.defaultBlockState(); // Paper - place tripwire without updating
-         Level world = ctx.getLevel();
-         BlockPos blockposition = ctx.getClickedPos();
- 
-@@ -75,11 +77,13 @@
- 
-     @Override
-     protected BlockState updateShape(BlockState state, LevelReader world, ScheduledTickAccess tickView, BlockPos pos, Direction direction, BlockPos neighborPos, BlockState neighborState, RandomSource random) {
-+        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableTripwireUpdates) return state; // Paper - prevent tripwire from updating
-         return direction.getAxis().isHorizontal() ? (BlockState) state.setValue((Property) TripWireBlock.PROPERTY_BY_DIRECTION.get(direction), this.shouldConnectTo(neighborState, direction)) : super.updateShape(state, world, tickView, pos, direction, neighborPos, neighborState, random);
-     }
- 
-     @Override
-     protected void onPlace(BlockState state, Level world, BlockPos pos, BlockState oldState, boolean notify) {
-+        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableTripwireUpdates) return; // Paper - prevent adjacent tripwires from updating
-         if (!oldState.is(state.getBlock())) {
-             this.updateSource(world, pos, state);
-         }
-@@ -87,6 +91,7 @@
- 
-     @Override
-     protected void onRemove(BlockState state, Level world, BlockPos pos, BlockState newState, boolean moved) {
-+        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableTripwireUpdates) return; // Paper - prevent adjacent tripwires from updating
-         if (!moved && !state.is(newState.getBlock())) {
-             this.updateSource(world, pos, (BlockState) state.setValue(TripWireBlock.POWERED, true));
-         }
-@@ -94,6 +99,7 @@
- 
-     @Override
-     public BlockState playerWillDestroy(Level world, BlockPos pos, BlockState state, Player player) {
-+        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableTripwireUpdates) return state; // Paper - prevent disarming tripwires
-         if (!world.isClientSide && !player.getMainHandItem().isEmpty() && player.getMainHandItem().is(Items.SHEARS)) {
-             world.setBlock(pos, (BlockState) state.setValue(TripWireBlock.DISARMED, true), 4);
-             world.gameEvent((Entity) player, (Holder) GameEvent.SHEAR, pos);
-@@ -103,6 +109,7 @@
-     }
- 
-     private void updateSource(Level world, BlockPos pos, BlockState state) {
-+        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableTripwireUpdates) return; // Paper - prevent adjacent tripwires from updating
-         Direction[] aenumdirection = new Direction[]{Direction.SOUTH, Direction.WEST};
-         int i = aenumdirection.length;
-         int j = 0;
-@@ -140,6 +147,8 @@
- 
-     @Override
-     protected void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
-+        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableTripwireUpdates) return; // Paper - prevent tripwires from detecting collision
-+        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
-         if (!world.isClientSide) {
-             if (!(Boolean) state.getValue(TripWireBlock.POWERED)) {
-                 this.checkPressed(world, pos, List.of(entity));
-@@ -149,6 +158,7 @@
- 
-     @Override
-     protected void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
-+        if (io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableTripwireUpdates) return; // Paper - prevent tripwire pressed check
-         if ((Boolean) world.getBlockState(pos).getValue(TripWireBlock.POWERED)) {
-             this.checkPressed(world, pos);
-         }
-@@ -179,6 +189,40 @@
-             }
-         }
- 
-+        // CraftBukkit start - Call interact even when triggering connected tripwire
-+        if (flag != flag1 && flag1 && (Boolean)iblockdata.getValue(TripWireBlock.ATTACHED)) {
-+            org.bukkit.World bworld = world.getWorld();
-+            org.bukkit.plugin.PluginManager manager = world.getCraftServer().getPluginManager();
-+            org.bukkit.block.Block block = bworld.getBlockAt(pos.getX(), pos.getY(), pos.getZ());
-+            boolean allowed = false;
-+
-+            // If all of the events are cancelled block the tripwire trigger, else allow
-+            for (Object object : entities) {
-+                if (object != null) {
-+                    org.bukkit.event.Cancellable cancellable;
-+
-+                    if (object instanceof Player) {
-+                        cancellable = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerInteractEvent((Player) object, org.bukkit.event.block.Action.PHYSICAL, pos, null, null, null);
-+                    } else if (object instanceof Entity) {
-+                        cancellable = new EntityInteractEvent(((Entity) object).getBukkitEntity(), block);
-+                        manager.callEvent((EntityInteractEvent) cancellable);
-+                    } else {
-+                        continue;
-+                    }
-+
-+                    if (!cancellable.isCancelled()) {
-+                        allowed = true;
-+                        break;
-+                    }
-+                }
-+            }
-+
-+            if (!allowed) {
-+                return;
-+            }
-+        }
-+        // CraftBukkit end
-+
-         if (flag1 != flag) {
-             iblockdata = (BlockState) iblockdata.setValue(TripWireBlock.POWERED, flag1);
-             world.setBlock(pos, iblockdata, 3);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/TripWireHookBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/TripWireHookBlock.java.patch
deleted file mode 100644
index 85f745ec8f..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/TripWireHookBlock.java.patch
+++ /dev/null
@@ -1,34 +0,0 @@
---- a/net/minecraft/world/level/block/TripWireHookBlock.java
-+++ b/net/minecraft/world/level/block/TripWireHookBlock.java
-@@ -31,6 +31,10 @@
- import net.minecraft.world.level.redstone.Orientation;
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.event.block.BlockRedstoneEvent;
-+// CraftBukkit end
- 
- public class TripWireHookBlock extends Block {
- 
-@@ -174,10 +178,20 @@
-                 world.setBlock(blockposition1, (BlockState) iblockdata3.setValue(TripWireHookBlock.FACING, enumdirection1), 3);
-                 TripWireHookBlock.notifyNeighbors(block, world, blockposition1, enumdirection1);
-                 TripWireHookBlock.emitState(world, blockposition1, flag4, flag5, flag2, flag3);
-+            }
-+
-+            // CraftBukkit start
-+            BlockRedstoneEvent eventRedstone = new BlockRedstoneEvent(CraftBlock.at(world, pos), 15, 0);
-+            world.getCraftServer().getPluginManager().callEvent(eventRedstone);
-+
-+            if (eventRedstone.getNewCurrent() > 0) {
-+                return;
-             }
-+            // CraftBukkit end
- 
-             TripWireHookBlock.emitState(world, pos, flag4, flag5, flag2, flag3);
-             if (!flag) {
-+                if (world.getBlockState(pos).getBlock() == Blocks.TRIPWIRE_HOOK) // Paper - Validate tripwire hook placement before update
-                 world.setBlock(pos, (BlockState) iblockdata3.setValue(TripWireHookBlock.FACING, enumdirection), 3);
-                 if (flag1) {
-                     TripWireHookBlock.notifyNeighbors(block, world, pos, enumdirection);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/TurtleEggBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/TurtleEggBlock.java.patch
deleted file mode 100644
index c27f68ac72..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/TurtleEggBlock.java.patch
+++ /dev/null
@@ -1,76 +0,0 @@
---- a/net/minecraft/world/level/block/TurtleEggBlock.java
-+++ b/net/minecraft/world/level/block/TurtleEggBlock.java
-@@ -31,6 +31,11 @@
- import net.minecraft.world.level.gameevent.GameEvent;
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityInteractEvent;
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+// CraftBukkit end
- 
- public class TurtleEggBlock extends Block {
- 
-@@ -74,6 +79,19 @@
-     private void destroyEgg(Level world, BlockState state, BlockPos pos, Entity entity, int inverseChance) {
-         if (state.is(Blocks.TURTLE_EGG) && world instanceof ServerLevel worldserver) {
-             if (this.canDestroyEgg(worldserver, entity) && world.random.nextInt(inverseChance) == 0) {
-+                // CraftBukkit start - Step on eggs
-+                org.bukkit.event.Cancellable cancellable;
-+                if (entity instanceof Player) {
-+                    cancellable = CraftEventFactory.callPlayerInteractEvent((Player) entity, org.bukkit.event.block.Action.PHYSICAL, pos, null, null, null);
-+                } else {
-+                    cancellable = new EntityInteractEvent(entity.getBukkitEntity(), CraftBlock.at(worldserver, pos));
-+                    worldserver.getCraftServer().getPluginManager().callEvent((EntityInteractEvent) cancellable);
-+                }
-+
-+                if (cancellable.isCancelled()) {
-+                    return;
-+                }
-+                // CraftBukkit end
-                 this.decreaseEggs(worldserver, pos, state);
-             }
-         }
-@@ -100,10 +118,20 @@
-             int i = (Integer) state.getValue(TurtleEggBlock.HATCH);
- 
-             if (i < 2) {
-+                // CraftBukkit start - Call BlockGrowEvent
-+                if (!CraftEventFactory.handleBlockGrowEvent(world, pos, state.setValue(TurtleEggBlock.HATCH, i + 1), 2)) {
-+                    return;
-+                }
-+                // CraftBukkit end
-                 world.playSound((Player) null, pos, SoundEvents.TURTLE_EGG_CRACK, SoundSource.BLOCKS, 0.7F, 0.9F + random.nextFloat() * 0.2F);
--                world.setBlock(pos, (BlockState) state.setValue(TurtleEggBlock.HATCH, i + 1), 2);
-+                // worldserver.setBlock(blockposition, (IBlockData) iblockdata.setValue(BlockTurtleEgg.HATCH, i + 1), 2); // CraftBukkit - handled above
-                 world.gameEvent((Holder) GameEvent.BLOCK_CHANGE, pos, GameEvent.Context.of(state));
-             } else {
-+                // CraftBukkit start - Call BlockFadeEvent
-+                if (CraftEventFactory.callBlockFadeEvent(world, pos, Blocks.AIR.defaultBlockState()).isCancelled()) {
-+                    return;
-+                }
-+                // CraftBukkit end
-                 world.playSound((Player) null, pos, SoundEvents.TURTLE_EGG_HATCH, SoundSource.BLOCKS, 0.7F, 0.9F + random.nextFloat() * 0.2F);
-                 world.removeBlock(pos, false);
-                 world.gameEvent((Holder) GameEvent.BLOCK_DESTROY, pos, GameEvent.Context.of(state));
-@@ -116,7 +144,7 @@
-                         entityturtle.setAge(-24000);
-                         entityturtle.setHomePos(pos);
-                         entityturtle.moveTo((double) pos.getX() + 0.3D + (double) j * 0.2D, (double) pos.getY(), (double) pos.getZ() + 0.3D, 0.0F, 0.0F);
--                        world.addFreshEntity(entityturtle);
-+                        world.addFreshEntity(entityturtle, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.EGG); // CraftBukkit
-                     }
-                 }
-             }
-@@ -147,8 +175,8 @@
-     }
- 
-     @Override
--    public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool) {
--        super.playerDestroy(world, player, pos, state, blockEntity, tool);
-+    public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool, boolean includeDrops, boolean dropExp) { // Paper - fix drops not preventing stats/food exhaustion
-+        super.playerDestroy(world, player, pos, state, blockEntity, tool, includeDrops, dropExp); // Paper - fix drops not preventing stats/food exhaustion
-         this.decreaseEggs(world, pos, state);
-     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/VineBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/VineBlock.java.patch
deleted file mode 100644
index 0403549dc8..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/VineBlock.java.patch
+++ /dev/null
@@ -1,79 +0,0 @@
---- a/net/minecraft/world/level/block/VineBlock.java
-+++ b/net/minecraft/world/level/block/VineBlock.java
-@@ -24,6 +24,7 @@
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.Shapes;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+import org.bukkit.craftbukkit.event.CraftEventFactory; // CraftBukkit
- 
- public class VineBlock extends Block {
- 
-@@ -184,7 +185,7 @@
-     @Override
-     protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
-         if (world.getGameRules().getBoolean(GameRules.RULE_DO_VINES_SPREAD)) {
--            if (random.nextInt(4) == 0) {
-+            if (random.nextFloat() < (world.spigotConfig.vineModifier / (100.0f * 4))) { // Spigot - SPIGOT-7159: Better modifier resolution
-                 Direction enumdirection = Direction.getRandom(random);
-                 BlockPos blockposition1 = pos.above();
-                 BlockPos blockposition2;
-@@ -203,30 +204,34 @@
-                             BlockPos blockposition3 = blockposition2.relative(enumdirection1);
-                             BlockPos blockposition4 = blockposition2.relative(enumdirection2);
- 
-+                            // CraftBukkit start - Call BlockSpreadEvent
-+                            BlockPos source = pos;
-+
-                             if (flag && VineBlock.isAcceptableNeighbour(world, blockposition3, enumdirection1)) {
--                                world.setBlock(blockposition2, (BlockState) this.defaultBlockState().setValue(VineBlock.getPropertyForFace(enumdirection1), true), 2);
-+                                CraftEventFactory.handleBlockSpreadEvent(world, source, blockposition2, (BlockState) this.defaultBlockState().setValue(VineBlock.getPropertyForFace(enumdirection1), true), 2);
-                             } else if (flag1 && VineBlock.isAcceptableNeighbour(world, blockposition4, enumdirection2)) {
--                                world.setBlock(blockposition2, (BlockState) this.defaultBlockState().setValue(VineBlock.getPropertyForFace(enumdirection2), true), 2);
-+                                CraftEventFactory.handleBlockSpreadEvent(world, source, blockposition2, (BlockState) this.defaultBlockState().setValue(VineBlock.getPropertyForFace(enumdirection2), true), 2);
-                             } else {
-                                 Direction enumdirection3 = enumdirection.getOpposite();
- 
-                                 if (flag && world.isEmptyBlock(blockposition3) && VineBlock.isAcceptableNeighbour(world, pos.relative(enumdirection1), enumdirection3)) {
--                                    world.setBlock(blockposition3, (BlockState) this.defaultBlockState().setValue(VineBlock.getPropertyForFace(enumdirection3), true), 2);
-+                                    CraftEventFactory.handleBlockSpreadEvent(world, source, blockposition3, (BlockState) this.defaultBlockState().setValue(VineBlock.getPropertyForFace(enumdirection3), true), 2);
-                                 } else if (flag1 && world.isEmptyBlock(blockposition4) && VineBlock.isAcceptableNeighbour(world, pos.relative(enumdirection2), enumdirection3)) {
--                                    world.setBlock(blockposition4, (BlockState) this.defaultBlockState().setValue(VineBlock.getPropertyForFace(enumdirection3), true), 2);
-+                                    CraftEventFactory.handleBlockSpreadEvent(world, source, blockposition4, (BlockState) this.defaultBlockState().setValue(VineBlock.getPropertyForFace(enumdirection3), true), 2);
-                                 } else if ((double) random.nextFloat() < 0.05D && VineBlock.isAcceptableNeighbour(world, blockposition2.above(), Direction.UP)) {
--                                    world.setBlock(blockposition2, (BlockState) this.defaultBlockState().setValue(VineBlock.UP, true), 2);
-+                                    CraftEventFactory.handleBlockSpreadEvent(world, source, blockposition2, (BlockState) this.defaultBlockState().setValue(VineBlock.UP, true), 2);
-                                 }
-+                                // CraftBukkit end
-                             }
-                         } else if (VineBlock.isAcceptableNeighbour(world, blockposition2, enumdirection)) {
--                            world.setBlock(pos, (BlockState) state.setValue(VineBlock.getPropertyForFace(enumdirection), true), 2);
-+                            CraftEventFactory.handleBlockGrowEvent(world, pos, (BlockState) state.setValue(VineBlock.getPropertyForFace(enumdirection), true), 2); // CraftBukkit
-                         }
- 
-                     }
-                 } else {
-                     if (enumdirection == Direction.UP && pos.getY() < world.getMaxY()) {
-                         if (this.canSupportAtFace(world, pos, enumdirection)) {
--                            world.setBlock(pos, (BlockState) state.setValue(VineBlock.UP, true), 2);
-+                            CraftEventFactory.handleBlockGrowEvent(world, pos, (BlockState) state.setValue(VineBlock.UP, true), 2); // CraftBukkit
-                             return;
-                         }
- 
-@@ -246,7 +251,7 @@
-                             }
- 
-                             if (this.hasHorizontalConnection(iblockdata2)) {
--                                world.setBlock(blockposition1, iblockdata2, 2);
-+                                CraftEventFactory.handleBlockSpreadEvent(world, pos, blockposition1, iblockdata2, 2); // CraftBukkit
-                             }
- 
-                             return;
-@@ -261,7 +266,7 @@
-                             BlockState iblockdata4 = this.copyRandomFaces(state, iblockdata3, random);
- 
-                             if (iblockdata3 != iblockdata4 && this.hasHorizontalConnection(iblockdata4)) {
--                                world.setBlock(blockposition2, iblockdata4, 2);
-+                                CraftEventFactory.handleBlockSpreadEvent(world, pos, blockposition2, iblockdata4, 2); // CraftBukkit
-                             }
-                         }
-                     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/WallHangingSignBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/WallHangingSignBlock.java.patch
deleted file mode 100644
index 73be8c6267..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/WallHangingSignBlock.java.patch
+++ /dev/null
@@ -1,10 +0,0 @@
---- a/net/minecraft/world/level/block/WallHangingSignBlock.java
-+++ b/net/minecraft/world/level/block/WallHangingSignBlock.java
-@@ -179,6 +179,6 @@
-     @Nullable
-     @Override
-     public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level world, BlockState state, BlockEntityType<T> type) {
--        return createTickerHelper(type, BlockEntityType.HANGING_SIGN, SignBlockEntity::tick);
-+        return null; // Craftbukkit - remove unnecessary sign ticking
-     }
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/WaterlilyBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/WaterlilyBlock.java.patch
deleted file mode 100644
index 279e050da1..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/WaterlilyBlock.java.patch
+++ /dev/null
@@ -1,27 +0,0 @@
---- a/net/minecraft/world/level/block/WaterlilyBlock.java
-+++ b/net/minecraft/world/level/block/WaterlilyBlock.java
-@@ -13,6 +13,9 @@
- import net.minecraft.world.level.material.Fluids;
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+// CraftBukkit end
- 
- public class WaterlilyBlock extends BushBlock {
- 
-@@ -30,8 +33,14 @@
- 
-     @Override
-     protected void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
-+        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
-         super.entityInside(state, world, pos, entity);
-         if (world instanceof ServerLevel && entity instanceof AbstractBoat) {
-+            // CraftBukkit start
-+            if (!CraftEventFactory.callEntityChangeBlockEvent(entity, pos, state.getFluidState().createLegacyBlock())) { // Paper - fix wrong block state
-+                return;
-+            }
-+            // CraftBukkit end
-             world.destroyBlock(new BlockPos(pos), true, entity);
-         }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/WeightedPressurePlateBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/WeightedPressurePlateBlock.java.patch
deleted file mode 100644
index 67ce37fdf0..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/WeightedPressurePlateBlock.java.patch
+++ /dev/null
@@ -1,49 +0,0 @@
---- a/net/minecraft/world/level/block/WeightedPressurePlateBlock.java
-+++ b/net/minecraft/world/level/block/WeightedPressurePlateBlock.java
-@@ -6,6 +6,7 @@
- import net.minecraft.core.BlockPos;
- import net.minecraft.util.Mth;
- import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.block.state.BlockBehaviour;
- import net.minecraft.world.level.block.state.BlockState;
-@@ -13,6 +14,8 @@
- import net.minecraft.world.level.block.state.properties.BlockSetType;
- import net.minecraft.world.level.block.state.properties.BlockStateProperties;
- import net.minecraft.world.level.block.state.properties.IntegerProperty;
-+import org.bukkit.event.entity.EntityInteractEvent;
-+// CraftBukkit end
- 
- public class WeightedPressurePlateBlock extends BasePressurePlateBlock {
- 
-@@ -39,8 +42,28 @@
- 
-     @Override
-     protected int getSignalStrength(Level world, BlockPos pos) {
--        int i = Math.min(getEntityCount(world, WeightedPressurePlateBlock.TOUCH_AABB.move(pos), Entity.class), this.maxWeight);
-+        // CraftBukkit start
-+        // int i = Math.min(getEntityCount(world, BlockPressurePlateWeighted.TOUCH_AABB.move(blockposition), Entity.class), this.maxWeight);
-+        int i = 0;
-+        for (Entity entity : getEntities(world, WeightedPressurePlateBlock.TOUCH_AABB.move(pos), Entity.class)) {
-+            org.bukkit.event.Cancellable cancellable;
- 
-+            if (entity instanceof Player) {
-+                cancellable = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerInteractEvent((Player) entity, org.bukkit.event.block.Action.PHYSICAL, pos, null, null, null);
-+            } else {
-+                cancellable = new EntityInteractEvent(entity.getBukkitEntity(), world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ()));
-+                world.getCraftServer().getPluginManager().callEvent((EntityInteractEvent) cancellable);
-+            }
-+
-+            // We only want to block turning the plate on if all events are cancelled
-+            if (!cancellable.isCancelled()) {
-+                i++;
-+            }
-+        }
-+
-+        i = Math.min(i, this.maxWeight);
-+        // CraftBukkit end
-+
-         if (i > 0) {
-             float f = (float) Math.min(this.maxWeight, i) / (float) this.maxWeight;
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/WitherRoseBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/WitherRoseBlock.java.patch
deleted file mode 100644
index c456abf513..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/WitherRoseBlock.java.patch
+++ /dev/null
@@ -1,15 +0,0 @@
---- a/net/minecraft/world/level/block/WitherRoseBlock.java
-+++ b/net/minecraft/world/level/block/WitherRoseBlock.java
-@@ -63,10 +63,11 @@
- 
-     @Override
-     protected void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
-+        if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - Add EntityInsideBlockEvent
-         if (world instanceof ServerLevel worldserver) {
-             if (world.getDifficulty() != Difficulty.PEACEFUL && entity instanceof LivingEntity entityliving) {
-                 if (!entityliving.isInvulnerableTo(worldserver, world.damageSources().wither())) {
--                    entityliving.addEffect(this.getBeeInteractionEffect());
-+                    entityliving.addEffect(this.getBeeInteractionEffect(), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.WITHER_ROSE); // CraftBukkit
-                 }
-             }
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/WitherSkullBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/WitherSkullBlock.java.patch
deleted file mode 100644
index e34748cc0e..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/WitherSkullBlock.java.patch
+++ /dev/null
@@ -1,50 +0,0 @@
---- a/net/minecraft/world/level/block/WitherSkullBlock.java
-+++ b/net/minecraft/world/level/block/WitherSkullBlock.java
-@@ -26,6 +26,10 @@
- import net.minecraft.world.level.block.state.pattern.BlockPatternBuilder;
- import net.minecraft.world.level.block.state.predicate.BlockStatePredicate;
- 
-+// CraftBukkit start
-+import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
-+// CraftBukkit end
-+
- public class WitherSkullBlock extends SkullBlock {
- 
-     public static final MapCodec<WitherSkullBlock> CODEC = simpleCodec(WitherSkullBlock::new);
-@@ -58,6 +62,7 @@
-     }
- 
-     public static void checkSpawn(Level world, BlockPos pos, SkullBlockEntity blockEntity) {
-+        if (world.captureBlockStates) return; // CraftBukkit
-         if (!world.isClientSide) {
-             BlockState iblockdata = blockEntity.getBlockState();
-             boolean flag = iblockdata.is(Blocks.WITHER_SKELETON_SKULL) || iblockdata.is(Blocks.WITHER_SKELETON_WALL_SKULL);
-@@ -69,12 +74,18 @@
-                     WitherBoss entitywither = (WitherBoss) EntityType.WITHER.create(world, EntitySpawnReason.TRIGGERED);
- 
-                     if (entitywither != null) {
--                        CarvedPumpkinBlock.clearPatternBlocks(world, shapedetector_shapedetectorcollection);
-+                        // BlockPumpkinCarved.clearPatternBlocks(world, shapedetector_shapedetectorcollection); // CraftBukkit - move down
-                         BlockPos blockposition1 = shapedetector_shapedetectorcollection.getBlock(1, 2, 0).getPos();
- 
-                         entitywither.moveTo((double) blockposition1.getX() + 0.5D, (double) blockposition1.getY() + 0.55D, (double) blockposition1.getZ() + 0.5D, shapedetector_shapedetectorcollection.getForwards().getAxis() == Direction.Axis.X ? 0.0F : 90.0F, 0.0F);
-                         entitywither.yBodyRot = shapedetector_shapedetectorcollection.getForwards().getAxis() == Direction.Axis.X ? 0.0F : 90.0F;
-                         entitywither.makeInvulnerable();
-+                        // CraftBukkit start
-+                        if (!world.addFreshEntity(entitywither, SpawnReason.BUILD_WITHER)) {
-+                            return;
-+                        }
-+                        CarvedPumpkinBlock.clearPatternBlocks(world, shapedetector_shapedetectorcollection); // CraftBukkit - from above
-+                        // CraftBukkit end
-                         Iterator iterator = world.getEntitiesOfClass(ServerPlayer.class, entitywither.getBoundingBox().inflate(50.0D)).iterator();
- 
-                         while (iterator.hasNext()) {
-@@ -83,7 +94,7 @@
-                             CriteriaTriggers.SUMMONED_ENTITY.trigger(entityplayer, (Entity) entitywither);
-                         }
- 
--                        world.addFreshEntity(entitywither);
-+                        // world.addFreshEntity(entitywither); // CraftBukkit - moved up
-                         CarvedPumpkinBlock.updatePatternBlocks(world, shapedetector_shapedetectorcollection);
-                     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java.patch
deleted file mode 100644
index 8438fd826a..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java.patch
+++ /dev/null
@@ -1,356 +0,0 @@
---- a/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java
-+++ b/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java
-@@ -22,7 +22,6 @@
- import net.minecraft.world.ContainerHelper;
- import net.minecraft.world.WorldlyContainer;
- import net.minecraft.world.entity.ExperienceOrb;
--import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.entity.player.StackedItemContents;
- import net.minecraft.world.inventory.ContainerData;
- import net.minecraft.world.inventory.RecipeCraftingHolder;
-@@ -41,6 +40,20 @@
- import net.minecraft.world.level.block.Blocks;
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.craftbukkit.inventory.CraftItemType;
-+import org.bukkit.entity.HumanEntity;
-+import org.bukkit.entity.Player;
-+import org.bukkit.event.block.BlockExpEvent;
-+import org.bukkit.event.inventory.FurnaceBurnEvent;
-+import org.bukkit.event.inventory.FurnaceExtractEvent;
-+import org.bukkit.event.inventory.FurnaceSmeltEvent;
-+import org.bukkit.event.inventory.FurnaceStartSmeltEvent;
-+import org.bukkit.inventory.CookingRecipe;
-+// CraftBukkit end
- 
- public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntity implements WorldlyContainer, RecipeCraftingHolder, StackedContentsCompatible {
- 
-@@ -65,6 +78,8 @@
-     protected final ContainerData dataAccess;
-     public final Reference2IntOpenHashMap<ResourceKey<Recipe<?>>> recipesUsed;
-     private final RecipeManager.CachedCheck<SingleRecipeInput, ? extends AbstractCookingRecipe> quickCheck;
-+    public final RecipeType<? extends AbstractCookingRecipe> recipeType; // Paper - cook speed multiplier API
-+    public double cookSpeedMultiplier = 1.0; // Paper - cook speed multiplier API
- 
-     protected AbstractFurnaceBlockEntity(BlockEntityType<?> blockEntityType, BlockPos pos, BlockState state, RecipeType<? extends AbstractCookingRecipe> recipeType) {
-         super(blockEntityType, pos, state);
-@@ -110,9 +125,40 @@
-             }
-         };
-         this.recipesUsed = new Reference2IntOpenHashMap();
--        this.quickCheck = RecipeManager.createCheck(recipeType);
-+        this.quickCheck = RecipeManager.createCheck((RecipeType<AbstractCookingRecipe>) recipeType); // CraftBukkit - decompile error // Eclipse fail
-+        this.recipeType = recipeType; // Paper - cook speed multiplier API
-     }
- 
-+    // CraftBukkit start - add fields and methods
-+    private int maxStack = MAX_STACK;
-+    public List<HumanEntity> transaction = new java.util.ArrayList<HumanEntity>();
-+
-+    public List<ItemStack> getContents() {
-+        return this.items;
-+    }
-+
-+    public void onOpen(CraftHumanEntity who) {
-+        this.transaction.add(who);
-+    }
-+
-+    public void onClose(CraftHumanEntity who) {
-+        this.transaction.remove(who);
-+    }
-+
-+    public List<HumanEntity> getViewers() {
-+        return this.transaction;
-+    }
-+
-+    @Override
-+    public int getMaxStackSize() {
-+        return this.maxStack;
-+    }
-+
-+    public void setMaxStackSize(int size) {
-+        this.maxStack = size;
-+    }
-+    // CraftBukkit end
-+
-     private boolean isLit() {
-         return this.litTimeRemaining > 0;
-     }
-@@ -132,9 +178,18 @@
-         while (iterator.hasNext()) {
-             String s = (String) iterator.next();
- 
--            this.recipesUsed.put(ResourceKey.create(Registries.RECIPE, ResourceLocation.parse(s)), nbttagcompound1.getInt(s));
-+            // Paper start - Validate ResourceLocation
-+            final ResourceLocation resourceLocation = ResourceLocation.tryParse(s);
-+            if (resourceLocation != null) {
-+                this.recipesUsed.put(ResourceKey.create(Registries.RECIPE, resourceLocation), nbttagcompound1.getInt(s));
-+            }
-         }
- 
-+        // Paper start - cook speed multiplier API
-+        if (nbt.contains("Paper.CookSpeedMultiplier")) {
-+            this.cookSpeedMultiplier = nbt.getDouble("Paper.CookSpeedMultiplier");
-+        }
-+        // Paper end - cook speed multiplier API
-     }
- 
-     @Override
-@@ -144,6 +199,7 @@
-         nbt.putShort("cooking_total_time", (short) this.cookingTotalTime);
-         nbt.putShort("lit_time_remaining", (short) this.litTimeRemaining);
-         nbt.putShort("lit_total_time", (short) this.litTotalTime);
-+        nbt.putDouble("Paper.CookSpeedMultiplier", this.cookSpeedMultiplier); // Paper - cook speed multiplier API
-         ContainerHelper.saveAllItems(nbt, this.items, registries);
-         CompoundTag nbttagcompound1 = new CompoundTag();
- 
-@@ -175,7 +231,7 @@
-             RecipeHolder recipeholder;
- 
-             if (flag2) {
--                recipeholder = (RecipeHolder) blockEntity.quickCheck.getRecipeFor(singlerecipeinput, world).orElse((Object) null);
-+                recipeholder = (RecipeHolder) blockEntity.quickCheck.getRecipeFor(singlerecipeinput, world).orElse(null); // CraftBukkit - decompile error
-             } else {
-                 recipeholder = null;
-             }
-@@ -183,11 +239,22 @@
-             int i = blockEntity.getMaxStackSize();
- 
-             if (!blockEntity.isLit() && AbstractFurnaceBlockEntity.canBurn(world.registryAccess(), recipeholder, singlerecipeinput, blockEntity.items, i)) {
--                blockEntity.litTimeRemaining = blockEntity.getBurnDuration(world.fuelValues(), itemstack);
-+                // CraftBukkit start
-+                CraftItemStack fuel = CraftItemStack.asCraftMirror(itemstack);
-+
-+                FurnaceBurnEvent furnaceBurnEvent = new FurnaceBurnEvent(CraftBlock.at(world, pos), fuel, blockEntity.getBurnDuration(world.fuelValues(), itemstack));
-+                world.getCraftServer().getPluginManager().callEvent(furnaceBurnEvent);
-+
-+                if (furnaceBurnEvent.isCancelled()) {
-+                    return;
-+                }
-+
-+                blockEntity.litTimeRemaining = furnaceBurnEvent.getBurnTime();
-                 blockEntity.litTotalTime = blockEntity.litTimeRemaining;
--                if (blockEntity.isLit()) {
-+                if (blockEntity.isLit() && furnaceBurnEvent.isBurning()) {
-+                    // CraftBukkit end
-                     flag1 = true;
--                    if (flag3) {
-+                    if (flag3 && furnaceBurnEvent.willConsumeFuel()) { // Paper - add consumeFuel to FurnaceBurnEvent
-                         Item item = itemstack.getItem();
- 
-                         itemstack.shrink(1);
-@@ -199,11 +266,23 @@
-             }
- 
-             if (blockEntity.isLit() && AbstractFurnaceBlockEntity.canBurn(world.registryAccess(), recipeholder, singlerecipeinput, blockEntity.items, i)) {
-+                // CraftBukkit start
-+                if (recipeholder != null && blockEntity.cookingTimer == 0) {
-+                    CraftItemStack source = CraftItemStack.asCraftMirror(blockEntity.items.get(0));
-+                    CookingRecipe<?> recipe = (CookingRecipe<?>) recipeholder.toBukkitRecipe();
-+
-+                    FurnaceStartSmeltEvent event = new FurnaceStartSmeltEvent(CraftBlock.at(world, pos), source, recipe, AbstractFurnaceBlockEntity.getTotalCookTime(world, blockEntity, blockEntity.recipeType, blockEntity.cookSpeedMultiplier)); // Paper - cook speed multiplier API
-+                    world.getCraftServer().getPluginManager().callEvent(event);
-+
-+                    blockEntity.cookingTotalTime = event.getTotalCookTime();
-+                }
-+                // CraftBukkit end
-+
-                 ++blockEntity.cookingTimer;
--                if (blockEntity.cookingTimer == blockEntity.cookingTotalTime) {
-+                if (blockEntity.cookingTimer >= blockEntity.cookingTotalTime) { // Paper - cook speed multiplier API
-                     blockEntity.cookingTimer = 0;
--                    blockEntity.cookingTotalTime = AbstractFurnaceBlockEntity.getTotalCookTime(world, blockEntity);
--                    if (AbstractFurnaceBlockEntity.burn(world.registryAccess(), recipeholder, singlerecipeinput, blockEntity.items, i)) {
-+                    blockEntity.cookingTotalTime = AbstractFurnaceBlockEntity.getTotalCookTime(world, blockEntity, blockEntity.recipeType, blockEntity.cookSpeedMultiplier); // Paper - cook speed multiplier API
-+                    if (AbstractFurnaceBlockEntity.burn(blockEntity.level, blockEntity.worldPosition, world.registryAccess(), recipeholder, singlerecipeinput, blockEntity.items, i)) { // CraftBukkit
-                         blockEntity.setRecipeUsed(recipeholder);
-                     }
- 
-@@ -242,20 +321,47 @@
-         }
-     }
- 
--    private static boolean burn(RegistryAccess dynamicRegistryManager, @Nullable RecipeHolder<? extends AbstractCookingRecipe> recipe, SingleRecipeInput input, NonNullList<ItemStack> inventory, int maxCount) {
--        if (recipe != null && AbstractFurnaceBlockEntity.canBurn(dynamicRegistryManager, recipe, input, inventory, maxCount)) {
--            ItemStack itemstack = (ItemStack) inventory.get(0);
--            ItemStack itemstack1 = ((AbstractCookingRecipe) recipe.value()).assemble(input, dynamicRegistryManager);
--            ItemStack itemstack2 = (ItemStack) inventory.get(2);
-+    private static boolean burn(Level world, BlockPos blockposition, RegistryAccess iregistrycustom, @Nullable RecipeHolder<? extends AbstractCookingRecipe> recipeholder, SingleRecipeInput singlerecipeinput, NonNullList<ItemStack> nonnulllist, int i) { // CraftBukkit
-+        if (recipeholder != null && AbstractFurnaceBlockEntity.canBurn(iregistrycustom, recipeholder, singlerecipeinput, nonnulllist, i)) {
-+            ItemStack itemstack = (ItemStack) nonnulllist.get(0);
-+            ItemStack itemstack1 = ((AbstractCookingRecipe) recipeholder.value()).assemble(singlerecipeinput, iregistrycustom);
-+            ItemStack itemstack2 = (ItemStack) nonnulllist.get(2);
- 
-+            // CraftBukkit start - fire FurnaceSmeltEvent
-+            CraftItemStack source = CraftItemStack.asCraftMirror(itemstack);
-+            org.bukkit.inventory.ItemStack result = CraftItemStack.asBukkitCopy(itemstack1);
-+
-+            FurnaceSmeltEvent furnaceSmeltEvent = new FurnaceSmeltEvent(CraftBlock.at(world, blockposition), source, result, (org.bukkit.inventory.CookingRecipe<?>) recipeholder.toBukkitRecipe()); // Paper - Add recipe to cook events
-+            world.getCraftServer().getPluginManager().callEvent(furnaceSmeltEvent);
-+
-+            if (furnaceSmeltEvent.isCancelled()) {
-+                return false;
-+            }
-+
-+            result = furnaceSmeltEvent.getResult();
-+            itemstack1 = CraftItemStack.asNMSCopy(result);
-+
-+            if (!itemstack1.isEmpty()) {
-+                if (itemstack2.isEmpty()) {
-+                    nonnulllist.set(2, itemstack1.copy());
-+                } else if (CraftItemStack.asCraftMirror(itemstack2).isSimilar(result)) {
-+                    itemstack2.grow(itemstack1.getCount());
-+                } else {
-+                    return false;
-+                }
-+            }
-+
-+            /*
-             if (itemstack2.isEmpty()) {
--                inventory.set(2, itemstack1.copy());
-+                nonnulllist.set(2, itemstack1.copy());
-             } else if (ItemStack.isSameItemSameComponents(itemstack2, itemstack1)) {
-                 itemstack2.grow(1);
-             }
-+            */
-+            // CraftBukkit end
- 
--            if (itemstack.is(Blocks.WET_SPONGE.asItem()) && !((ItemStack) inventory.get(1)).isEmpty() && ((ItemStack) inventory.get(1)).is(Items.BUCKET)) {
--                inventory.set(1, new ItemStack(Items.WATER_BUCKET));
-+            if (itemstack.is(Blocks.WET_SPONGE.asItem()) && !((ItemStack) nonnulllist.get(1)).isEmpty() && ((ItemStack) nonnulllist.get(1)).is(Items.BUCKET)) {
-+                nonnulllist.set(1, new ItemStack(Items.WATER_BUCKET));
-             }
- 
-             itemstack.shrink(1);
-@@ -269,12 +375,14 @@
-         return fuelRegistry.burnDuration(stack);
-     }
- 
--    public static int getTotalCookTime(ServerLevel world, AbstractFurnaceBlockEntity furnace) {
-+    public static int getTotalCookTime(@Nullable ServerLevel world, AbstractFurnaceBlockEntity furnace, RecipeType<? extends AbstractCookingRecipe> recipeType, double cookSpeedMultiplier) { // Paper - cook speed multiplier API
-         SingleRecipeInput singlerecipeinput = new SingleRecipeInput(furnace.getItem(0));
- 
--        return (Integer) furnace.quickCheck.getRecipeFor(singlerecipeinput, world).map((recipeholder) -> {
--            return ((AbstractCookingRecipe) recipeholder.value()).cookingTime();
--        }).orElse(200);
-+        // Paper start - cook speed multiplier API
-+        /* Scale the recipe's cooking time to the current cookSpeedMultiplier */
-+        int cookTime = world != null ? furnace.quickCheck.getRecipeFor(singlerecipeinput, world).map(holder -> holder.value().cookingTime()).orElse(200) : (net.minecraft.server.MinecraftServer.getServer().getRecipeManager().getRecipeFor(recipeType, singlerecipeinput, world /* passing a null level here is safe. world is only used for map extending recipes which won't happen here */).map(holder -> holder.value().cookingTime()).orElse(200));
-+        return (int) Math.ceil (cookTime / cookSpeedMultiplier);
-+        // Paper end - cook speed multiplier API
-     }
- 
-     @Override
-@@ -320,12 +428,11 @@
-             if (world instanceof ServerLevel) {
-                 ServerLevel worldserver = (ServerLevel) world;
- 
--                this.cookingTotalTime = AbstractFurnaceBlockEntity.getTotalCookTime(worldserver, this);
-+                this.cookingTotalTime = AbstractFurnaceBlockEntity.getTotalCookTime(worldserver, this, this.recipeType, this.cookSpeedMultiplier); // Paper - cook speed multiplier API
-                 this.cookingTimer = 0;
-                 this.setChanged();
-             }
-         }
--
-     }
- 
-     @Override
-@@ -358,19 +465,19 @@
-     }
- 
-     @Override
--    public void awardUsedRecipes(Player player, List<ItemStack> ingredients) {}
-+    public void awardUsedRecipes(net.minecraft.world.entity.player.Player player, List<ItemStack> ingredients) {}
- 
--    public void awardUsedRecipesAndPopExperience(ServerPlayer player) {
--        List<RecipeHolder<?>> list = this.getRecipesToAwardAndPopExperience(player.serverLevel(), player.position());
-+    public void awardUsedRecipesAndPopExperience(ServerPlayer entityplayer, ItemStack itemstack, int amount) { // CraftBukkit
-+        List<RecipeHolder<?>> list = this.getRecipesToAwardAndPopExperience(entityplayer.serverLevel(), entityplayer.position(), this.worldPosition, entityplayer, itemstack, amount); // CraftBukkit
- 
--        player.awardRecipes(list);
-+        entityplayer.awardRecipes(list);
-         Iterator iterator = list.iterator();
- 
-         while (iterator.hasNext()) {
-             RecipeHolder<?> recipeholder = (RecipeHolder) iterator.next();
- 
-             if (recipeholder != null) {
--                player.triggerRecipeCrafted(recipeholder, this.items);
-+                entityplayer.triggerRecipeCrafted(recipeholder, this.items);
-             }
-         }
- 
-@@ -378,41 +485,56 @@
-     }
- 
-     public List<RecipeHolder<?>> getRecipesToAwardAndPopExperience(ServerLevel world, Vec3 pos) {
-+        // CraftBukkit start
-+        return this.getRecipesToAwardAndPopExperience(world, pos, this.worldPosition, null, null, 0);
-+    }
-+
-+    public List<RecipeHolder<?>> getRecipesToAwardAndPopExperience(ServerLevel worldserver, Vec3 vec3d, BlockPos blockposition, ServerPlayer entityplayer, ItemStack itemstack, int amount) {
-+        // CraftBukkit end
-         List<RecipeHolder<?>> list = Lists.newArrayList();
-         ObjectIterator objectiterator = this.recipesUsed.reference2IntEntrySet().iterator();
- 
-         while (objectiterator.hasNext()) {
-             Entry<ResourceKey<Recipe<?>>> entry = (Entry) objectiterator.next();
- 
--            world.recipeAccess().byKey((ResourceKey) entry.getKey()).ifPresent((recipeholder) -> {
-+            worldserver.recipeAccess().byKey(entry.getKey()).ifPresent((recipeholder) -> { // CraftBukkit - decompile error
-+                if (!(recipeholder.value() instanceof AbstractCookingRecipe)) return; // Paper - don't process non-cooking recipes
-                 list.add(recipeholder);
--                AbstractFurnaceBlockEntity.createExperience(world, pos, entry.getIntValue(), ((AbstractCookingRecipe) recipeholder.value()).experience());
-+                AbstractFurnaceBlockEntity.createExperience(worldserver, vec3d, entry.getIntValue(), ((AbstractCookingRecipe) recipeholder.value()).experience(), blockposition, entityplayer, itemstack, amount); // CraftBukkit
-             });
-         }
- 
-         return list;
-     }
- 
--    private static void createExperience(ServerLevel world, Vec3 pos, int multiplier, float experience) {
--        int j = Mth.floor((float) multiplier * experience);
--        float f1 = Mth.frac((float) multiplier * experience);
-+    private static void createExperience(ServerLevel worldserver, Vec3 vec3d, int i, float f, BlockPos blockposition, net.minecraft.world.entity.player.Player entityhuman, ItemStack itemstack, int amount) { // CraftBukkit
-+        int j = Mth.floor((float) i * f);
-+        float f1 = Mth.frac((float) i * f);
- 
-         if (f1 != 0.0F && Math.random() < (double) f1) {
-             ++j;
-         }
- 
--        ExperienceOrb.award(world, pos, j);
-+        // CraftBukkit start - fire FurnaceExtractEvent / BlockExpEvent
-+        BlockExpEvent event;
-+        if (amount != 0) {
-+            event = new FurnaceExtractEvent((Player) entityhuman.getBukkitEntity(), CraftBlock.at(worldserver, blockposition), CraftItemType.minecraftToBukkit(itemstack.getItem()), amount, j);
-+        } else {
-+            event = new BlockExpEvent(CraftBlock.at(worldserver, blockposition), j);
-+        }
-+        worldserver.getCraftServer().getPluginManager().callEvent(event);
-+        j = event.getExpToDrop();
-+        // CraftBukkit end
-+
-+        ExperienceOrb.award(worldserver, vec3d, j, org.bukkit.entity.ExperienceOrb.SpawnReason.FURNACE, entityhuman); // Paper
-     }
- 
-     @Override
-     public void fillStackedContents(StackedItemContents finder) {
--        Iterator iterator = this.items.iterator();
-+        // Paper start - don't account fuel stack (fixes MC-243057)
-+        finder.accountStack(this.items.get(SLOT_INPUT));
-+        finder.accountStack(this.items.get(SLOT_RESULT));
-+        // Paper end
- 
--        while (iterator.hasNext()) {
--            ItemStack itemstack = (ItemStack) iterator.next();
--
--            finder.accountStack(itemstack);
--        }
--
-     }
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BannerBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BannerBlockEntity.java.patch
deleted file mode 100644
index 3b8ce9376c..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BannerBlockEntity.java.patch
+++ /dev/null
@@ -1,81 +0,0 @@
---- a/net/minecraft/world/level/block/entity/BannerBlockEntity.java
-+++ b/net/minecraft/world/level/block/entity/BannerBlockEntity.java
-@@ -19,13 +19,17 @@
- import net.minecraft.world.level.block.state.BlockState;
- import org.slf4j.Logger;
- 
-+// CraftBukkit start
-+import java.util.List;
-+// CraftBukkit end
-+
- public class BannerBlockEntity extends BlockEntity implements Nameable {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
-     public static final int MAX_PATTERNS = 6;
-     private static final String TAG_PATTERNS = "patterns";
-     @Nullable
--    private Component name;
-+    public Component name; // Paper - public
-     public DyeColor baseColor;
-     private BannerPatternLayers patterns;
- 
-@@ -53,7 +57,7 @@
-     @Override
-     protected void saveAdditional(CompoundTag nbt, HolderLookup.Provider registries) {
-         super.saveAdditional(nbt, registries);
--        if (!this.patterns.equals(BannerPatternLayers.EMPTY)) {
-+        if (!this.patterns.equals(BannerPatternLayers.EMPTY) || serialisingForNetwork.get()) { // Paper - always send patterns to client
-             nbt.put("patterns", (Tag) BannerPatternLayers.CODEC.encodeStart(registries.createSerializationContext(NbtOps.INSTANCE), this.patterns).getOrThrow());
-         }
- 
-@@ -74,7 +78,7 @@
-             BannerPatternLayers.CODEC.parse(registries.createSerializationContext(NbtOps.INSTANCE), nbt.get("patterns")).resultOrPartial((s) -> {
-                 BannerBlockEntity.LOGGER.error("Failed to parse banner patterns: '{}'", s);
-             }).ifPresent((bannerpatternlayers) -> {
--                this.patterns = bannerpatternlayers;
-+                this.setPatterns(bannerpatternlayers); // CraftBukkit - apply limits
-             });
-         }
- 
-@@ -85,9 +89,18 @@
-         return ClientboundBlockEntityDataPacket.create(this);
-     }
- 
-+    // Paper start - always send patterns to client
-+    ThreadLocal<Boolean> serialisingForNetwork = ThreadLocal.withInitial(() -> Boolean.FALSE);
-     @Override
-     public CompoundTag getUpdateTag(HolderLookup.Provider registries) {
-+        final Boolean wasSerialisingForNetwork = serialisingForNetwork.get();
-+        try {
-+            serialisingForNetwork.set(Boolean.TRUE);
-         return this.saveWithoutMetadata(registries);
-+        } finally {
-+            serialisingForNetwork.set(wasSerialisingForNetwork);
-+        }
-+        // Paper end - always send patterns to client
-     }
- 
-     public BannerPatternLayers getPatterns() {
-@@ -108,7 +121,7 @@
-     @Override
-     protected void applyImplicitComponents(BlockEntity.DataComponentInput components) {
-         super.applyImplicitComponents(components);
--        this.patterns = (BannerPatternLayers) components.getOrDefault(DataComponents.BANNER_PATTERNS, BannerPatternLayers.EMPTY);
-+        this.setPatterns((BannerPatternLayers) components.getOrDefault(DataComponents.BANNER_PATTERNS, BannerPatternLayers.EMPTY)); // CraftBukkit - apply limits
-         this.name = (Component) components.get(DataComponents.CUSTOM_NAME);
-     }
- 
-@@ -124,4 +137,13 @@
-         nbt.remove("patterns");
-         nbt.remove("CustomName");
-     }
-+
-+    // CraftBukkit start
-+    public void setPatterns(BannerPatternLayers bannerpatternlayers) {
-+        if (bannerpatternlayers.layers().size() > 20) {
-+            bannerpatternlayers = new BannerPatternLayers(List.copyOf(bannerpatternlayers.layers().subList(0, 20)));
-+        }
-+        this.patterns = bannerpatternlayers;
-+    }
-+    // CraftBukkit end
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BarrelBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BarrelBlockEntity.java.patch
deleted file mode 100644
index bc89832782..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BarrelBlockEntity.java.patch
+++ /dev/null
@@ -1,52 +0,0 @@
---- a/net/minecraft/world/level/block/entity/BarrelBlockEntity.java
-+++ b/net/minecraft/world/level/block/entity/BarrelBlockEntity.java
-@@ -20,9 +20,49 @@
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.block.BarrelBlock;
- import net.minecraft.world.level.block.state.BlockState;
-+// CraftBukkit start
-+import java.util.ArrayList;
-+import java.util.List;
-+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
-+import org.bukkit.entity.HumanEntity;
-+// CraftBukkit end
- 
- public class BarrelBlockEntity extends RandomizableContainerBlockEntity {
- 
-+    // CraftBukkit start - add fields and methods
-+    public List<HumanEntity> transaction = new ArrayList<>();
-+    private int maxStack = MAX_STACK;
-+
-+    @Override
-+    public List<ItemStack> getContents() {
-+        return this.items;
-+    }
-+
-+    @Override
-+    public void onOpen(CraftHumanEntity who) {
-+        this.transaction.add(who);
-+    }
-+
-+    @Override
-+    public void onClose(CraftHumanEntity who) {
-+        this.transaction.remove(who);
-+    }
-+
-+    @Override
-+    public List<HumanEntity> getViewers() {
-+        return this.transaction;
-+    }
-+
-+    @Override
-+    public int getMaxStackSize() {
-+       return this.maxStack;
-+    }
-+
-+    @Override
-+    public void setMaxStackSize(int i) {
-+        this.maxStack = i;
-+    }
-+    // CraftBukkit end
-     private NonNullList<ItemStack> items;
-     public final ContainerOpenersCounter openersCounter;
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BeaconBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BeaconBlockEntity.java.patch
deleted file mode 100644
index 736be5da6c..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BeaconBlockEntity.java.patch
+++ /dev/null
@@ -1,259 +0,0 @@
---- a/net/minecraft/world/level/block/entity/BeaconBlockEntity.java
-+++ b/net/minecraft/world/level/block/entity/BeaconBlockEntity.java
-@@ -1,5 +1,6 @@
- package net.minecraft.world.level.block.entity;
- 
-+import com.destroystokyo.paper.event.block.BeaconEffectEvent;
- import com.google.common.collect.ImmutableList;
- import com.google.common.collect.Lists;
- import java.util.Collection;
-@@ -45,6 +46,11 @@
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.levelgen.Heightmap;
- import net.minecraft.world.phys.AABB;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.craftbukkit.potion.CraftPotionUtil;
-+import org.bukkit.potion.PotionEffect;
-+// CraftBukkit end
- 
- public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Nameable {
- 
-@@ -71,7 +77,36 @@
-     public Component name;
-     public LockCode lockKey;
-     private final ContainerData dataAccess;
-+    // CraftBukkit start - add fields and methods
-+    public PotionEffect getPrimaryEffect() {
-+        return (this.primaryPower != null) ? CraftPotionUtil.toBukkit(new MobEffectInstance(this.primaryPower, BeaconBlockEntity.getLevel(this.levels), BeaconBlockEntity.getAmplification(this.levels, this.primaryPower, this.secondaryPower), true, true)) : null;
-+    }
- 
-+    public PotionEffect getSecondaryEffect() {
-+        return (BeaconBlockEntity.hasSecondaryEffect(this.levels, this.primaryPower, this.secondaryPower)) ? CraftPotionUtil.toBukkit(new MobEffectInstance(this.secondaryPower, BeaconBlockEntity.getLevel(this.levels), BeaconBlockEntity.getAmplification(this.levels, this.primaryPower, this.secondaryPower), true, true)) : null;
-+    }
-+    // CraftBukkit end
-+    // Paper start - Custom beacon ranges
-+    private final String PAPER_RANGE_TAG = "Paper.Range";
-+    private double effectRange = -1;
-+
-+    public double getEffectRange() {
-+        if (this.effectRange < 0) {
-+            return this.levels * 10 + 10;
-+        } else {
-+            return effectRange;
-+        }
-+    }
-+
-+    public void setEffectRange(double range) {
-+        this.effectRange = range;
-+    }
-+
-+    public void resetEffectRange() {
-+        this.effectRange = -1;
-+    }
-+    // Paper end - Custom beacon ranges
-+
-     @Nullable
-     static Holder<MobEffect> filterEffect(@Nullable Holder<MobEffect> effect) {
-         return BeaconBlockEntity.VALID_EFFECTS.contains(effect) ? effect : null;
-@@ -186,10 +221,19 @@
-             }
- 
-             if (blockEntity.levels > 0 && !blockEntity.beamSections.isEmpty()) {
--                BeaconBlockEntity.applyEffects(world, pos, blockEntity.levels, blockEntity.primaryPower, blockEntity.secondaryPower);
-+                BeaconBlockEntity.applyEffects(world, pos, blockEntity.levels, blockEntity.primaryPower, blockEntity.secondaryPower, blockEntity); // Paper - Custom beacon ranges
-                 BeaconBlockEntity.playSound(world, pos, SoundEvents.BEACON_AMBIENT);
-             }
-         }
-+        // Paper start - beacon activation/deactivation events
-+        if (i1 <= 0 && blockEntity.levels > 0) {
-+            org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(world, pos);
-+            new io.papermc.paper.event.block.BeaconActivatedEvent(block).callEvent();
-+        } else if (i1 > 0 && blockEntity.levels <= 0) {
-+            org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(world, pos);
-+            new io.papermc.paper.event.block.BeaconDeactivatedEvent(block).callEvent();
-+        }
-+        // Paper end - beacon activation/deactivation events
- 
-         if (blockEntity.lastCheckY >= l) {
-             blockEntity.lastCheckY = world.getMinY() - 1;
-@@ -247,43 +291,123 @@
- 
-     @Override
-     public void setRemoved() {
-+        // Paper start - beacon activation/deactivation events
-+        org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(level, worldPosition);
-+        new io.papermc.paper.event.block.BeaconDeactivatedEvent(block).callEvent();
-+        // Paper end - beacon activation/deactivation events
-+        // Paper start - fix MC-153086
-+        if (this.levels > 0 && !this.beamSections.isEmpty()) {
-         BeaconBlockEntity.playSound(this.level, this.worldPosition, SoundEvents.BEACON_DEACTIVATE);
-+        }
-+        // Paper end
-         super.setRemoved();
-     }
- 
--    private static void applyEffects(Level world, BlockPos pos, int beaconLevel, @Nullable Holder<MobEffect> primaryEffect, @Nullable Holder<MobEffect> secondaryEffect) {
--        if (!world.isClientSide && primaryEffect != null) {
--            double d0 = (double) (beaconLevel * 10 + 10);
-+    // CraftBukkit start - split into components
-+    private static byte getAmplification(int i, @Nullable Holder<MobEffect> holder, @Nullable Holder<MobEffect> holder1) {
-+        {
-             byte b0 = 0;
- 
--            if (beaconLevel >= 4 && Objects.equals(primaryEffect, secondaryEffect)) {
-+            if (i >= 4 && Objects.equals(holder, holder1)) {
-                 b0 = 1;
-             }
- 
--            int j = (9 + beaconLevel * 2) * 20;
--            AABB axisalignedbb = (new AABB(pos)).inflate(d0).expandTowards(0.0D, (double) world.getHeight(), 0.0D);
--            List<Player> list = world.getEntitiesOfClass(Player.class, axisalignedbb);
-+            return b0;
-+        }
-+    }
-+
-+    private static int getLevel(int i) {
-+        {
-+            int j = (9 + i * 2) * 20;
-+            return j;
-+        }
-+    }
-+
-+    public static List getHumansInRange(Level world, BlockPos blockposition, int i) {
-+        // Paper start - Custom beacon ranges
-+        return BeaconBlockEntity.getHumansInRange(world, blockposition, i, null);
-+    }
-+    public static List getHumansInRange(Level world, BlockPos blockposition, int i, @Nullable BeaconBlockEntity blockEntity) {
-+        // Paper end - Custom beacon ranges
-+        {
-+            double d0 = blockEntity != null ? blockEntity.getEffectRange() : (i * 10 + 10); // Paper - Custom beacon ranges
-+
-+            AABB axisalignedbb = (new AABB(blockposition)).inflate(d0).expandTowards(0.0D, (double) world.getHeight(), 0.0D);
-+            // Paper start - Perf: optimize player lookup for beacons
-+            List<Player> list;
-+            if (d0 <= 128.0) {
-+                list = world.getEntitiesOfClass(Player.class, axisalignedbb);
-+            } else {
-+                list = new java.util.ArrayList<>();
-+                for (Player player : world.players()) {
-+                    if (player.isSpectator()) {
-+                        continue;
-+                    }
-+                    if (player.getBoundingBox().intersects(axisalignedbb)) {
-+                        list.add(player);
-+                    }
-+                }
-+            }
-+            // Paper end - Perf: optimize player lookup for beacons
-+
-+            return list;
-+        }
-+    }
-+
-+    private static void applyEffect(List list, @Nullable Holder<MobEffect> holder, int j, int b0, boolean isPrimary, BlockPos worldPosition) { // Paper - BeaconEffectEvent
-+        if (!list.isEmpty()) { // Paper - BeaconEffectEvent
-             Iterator iterator = list.iterator();
- 
-             Player entityhuman;
-+            // Paper start - BeaconEffectEvent
-+            org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(((Player) list.get(0)).level(), worldPosition);
-+            PotionEffect effect = CraftPotionUtil.toBukkit(new MobEffectInstance(holder, j, b0, true, true));
-+            // Paper end - BeaconEffectEvent
- 
-             while (iterator.hasNext()) {
--                entityhuman = (Player) iterator.next();
--                entityhuman.addEffect(new MobEffectInstance(primaryEffect, j, b0, true, true));
-+                // Paper start - BeaconEffectEvent
-+                entityhuman = (ServerPlayer) iterator.next();
-+                BeaconEffectEvent event = new BeaconEffectEvent(block, effect, (org.bukkit.entity.Player) entityhuman.getBukkitEntity(), isPrimary);
-+                if (CraftEventFactory.callEvent(event).isCancelled()) continue;
-+                entityhuman.addEffect(new MobEffectInstance(CraftPotionUtil.fromBukkit(event.getEffect())), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.BEACON);
-+                // Paper end - BeaconEffectEvent
-             }
-+        }
-+    }
- 
--            if (beaconLevel >= 4 && !Objects.equals(primaryEffect, secondaryEffect) && secondaryEffect != null) {
--                iterator = list.iterator();
--
--                while (iterator.hasNext()) {
--                    entityhuman = (Player) iterator.next();
--                    entityhuman.addEffect(new MobEffectInstance(secondaryEffect, j, 0, true, true));
--                }
-+    private static boolean hasSecondaryEffect(int i, @Nullable Holder<MobEffect> holder, @Nullable Holder<MobEffect> holder1) {
-+        {
-+            if (i >= 4 && !Objects.equals(holder, holder1) && holder1 != null) {
-+                return true;
-             }
- 
-+            return false;
-         }
-     }
- 
-+    private static void applyEffects(Level world, BlockPos pos, int beaconLevel, @Nullable Holder<MobEffect> primaryEffect, @Nullable Holder<MobEffect> secondaryEffect) {
-+    // Paper start - Custom beacon ranges
-+        BeaconBlockEntity.applyEffects(world, pos, beaconLevel, primaryEffect, secondaryEffect, null);
-+    }
-+    private static void applyEffects(Level world, BlockPos pos, int beaconLevel, @Nullable Holder<MobEffect> primaryEffect, @Nullable Holder<MobEffect> secondaryEffect, @Nullable BeaconBlockEntity blockEntity) {
-+        // Paper end - Custom beacon ranges
-+        if (!world.isClientSide && primaryEffect != null) {
-+            double d0 = (double) (beaconLevel * 10 + 10);
-+            byte b0 = BeaconBlockEntity.getAmplification(beaconLevel, primaryEffect, secondaryEffect);
-+
-+            int j = BeaconBlockEntity.getLevel(beaconLevel);
-+            List list = BeaconBlockEntity.getHumansInRange(world, pos, beaconLevel, blockEntity); // Paper - Custom beacon ranges
-+
-+            BeaconBlockEntity.applyEffect(list, primaryEffect, j, b0, true, pos); // Paper - BeaconEffectEvent
-+
-+            if (BeaconBlockEntity.hasSecondaryEffect(beaconLevel, primaryEffect, secondaryEffect)) {
-+                BeaconBlockEntity.applyEffect(list, secondaryEffect, j, 0, false, pos); // Paper - BeaconEffectEvent
-+            }
-+        }
-+
-+    }
-+    // CraftBukkit end
-+
-     public static void playSound(Level world, BlockPos pos, SoundEvent sound) {
-         world.playSound((Player) null, pos, sound, SoundSource.BLOCKS, 1.0F, 1.0F);
-     }
-@@ -316,7 +440,7 @@
-         if (nbt.contains(key, 8)) {
-             ResourceLocation minecraftkey = ResourceLocation.tryParse(nbt.getString(key));
- 
--            return minecraftkey == null ? null : (Holder) BuiltInRegistries.MOB_EFFECT.get(minecraftkey).map(BeaconBlockEntity::filterEffect).orElse((Object) null);
-+            return minecraftkey == null ? null : (Holder) BuiltInRegistries.MOB_EFFECT.get(minecraftkey).orElse(null); // CraftBukkit - persist manually set non-default beacon effects (SPIGOT-3598)
-         } else {
-             return null;
-         }
-@@ -327,11 +451,13 @@
-         super.loadAdditional(nbt, registries);
-         this.primaryPower = BeaconBlockEntity.loadEffect(nbt, "primary_effect");
-         this.secondaryPower = BeaconBlockEntity.loadEffect(nbt, "secondary_effect");
-+        this.levels = nbt.getInt("Levels"); // CraftBukkit - SPIGOT-5053, use where available
-         if (nbt.contains("CustomName", 8)) {
-             this.name = parseCustomNameSafe(nbt.getString("CustomName"), registries);
-         }
- 
-         this.lockKey = LockCode.fromTag(nbt, registries);
-+        this.effectRange = nbt.contains(PAPER_RANGE_TAG, 6) ? nbt.getDouble(PAPER_RANGE_TAG) : -1; // Paper - Custom beacon ranges
-     }
- 
-     @Override
-@@ -345,6 +471,7 @@
-         }
- 
-         this.lockKey.addToTag(nbt, registries);
-+        nbt.putDouble(PAPER_RANGE_TAG, this.effectRange); // Paper - Custom beacon ranges
-     }
- 
-     public void setCustomName(@Nullable Component customName) {
-@@ -360,7 +487,7 @@
-     @Nullable
-     @Override
-     public AbstractContainerMenu createMenu(int syncId, Inventory playerInventory, Player player) {
--        return BaseContainerBlockEntity.canUnlock(player, this.lockKey, this.getDisplayName()) ? new BeaconMenu(syncId, playerInventory, this.dataAccess, ContainerLevelAccess.create(this.level, this.getBlockPos())) : null;
-+        return BaseContainerBlockEntity.canUnlock(player, this.lockKey, this.getDisplayName(), this) ? new BeaconMenu(syncId, playerInventory, this.dataAccess, ContainerLevelAccess.create(this.level, this.getBlockPos())) : null; // Paper - Add BlockLockCheckEvent
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java.patch
deleted file mode 100644
index 8457946183..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java.patch
+++ /dev/null
@@ -1,306 +0,0 @@
---- a/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java
-+++ b/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java
-@@ -43,6 +43,10 @@
- import net.minecraft.world.level.gameevent.GameEvent;
- import org.slf4j.Logger;
- 
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
-+
- public class BeehiveBlockEntity extends BlockEntity {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
-@@ -56,6 +60,7 @@
-     private List<BeehiveBlockEntity.BeeData> stored = Lists.newArrayList();
-     @Nullable
-     public BlockPos savedFlowerPos;
-+    public int maxBees = 3; // CraftBukkit - allow setting max amount of bees a hive can hold
- 
-     public BeehiveBlockEntity(BlockPos pos, BlockState state) {
-         super(BlockEntityType.BEEHIVE, pos, state);
-@@ -95,7 +100,7 @@
-     }
- 
-     public boolean isFull() {
--        return this.stored.size() == 3;
-+        return this.stored.size() == this.maxBees; // CraftBukkit
-     }
- 
-     public void emptyAllLivingFromHive(@Nullable Player player, BlockState state, BeehiveBlockEntity.BeeReleaseStatus beeState) {
-@@ -112,7 +117,7 @@
- 
-                     if (player.position().distanceToSqr(entity.position()) <= 16.0D) {
-                         if (!this.isSedated()) {
--                            entitybee.setTarget(player);
-+                            entitybee.setTarget(player, org.bukkit.event.entity.EntityTargetEvent.TargetReason.CLOSEST_PLAYER, true); // CraftBukkit
-                         } else {
-                             entitybee.setStayOutOfHiveCountdown(400);
-                         }
-@@ -124,10 +129,16 @@
-     }
- 
-     private List<Entity> releaseAllOccupants(BlockState state, BeehiveBlockEntity.BeeReleaseStatus beeState) {
-+        // CraftBukkit start - This allows us to bypass the night/rain/emergency check
-+        return this.releaseBees(state, beeState, false);
-+    }
-+
-+    public List<Entity> releaseBees(BlockState iblockdata, BeehiveBlockEntity.BeeReleaseStatus tileentitybeehive_releasestatus, boolean force) {
-         List<Entity> list = Lists.newArrayList();
- 
-         this.stored.removeIf((tileentitybeehive_hivebee) -> {
--            return BeehiveBlockEntity.releaseOccupant(this.level, this.worldPosition, state, tileentitybeehive_hivebee.toOccupant(), list, beeState, this.savedFlowerPos);
-+            return BeehiveBlockEntity.releaseOccupant(this.level, this.worldPosition, iblockdata, tileentitybeehive_hivebee.toOccupant(), list, tileentitybeehive_releasestatus, this.savedFlowerPos, force);
-+            // CraftBukkit end
-         });
-         if (!list.isEmpty()) {
-             super.setChanged();
-@@ -141,6 +152,11 @@
-         return this.stored.size();
-     }
- 
-+    // Paper start - Add EntityBlockStorage clearEntities
-+    public void clearBees() {
-+        this.stored.clear();
-+    }
-+    // Paper end - Add EntityBlockStorage clearEntities
-     public static int getHoneyLevel(BlockState state) {
-         return (Integer) state.getValue(BeehiveBlock.HONEY_LEVEL);
-     }
-@@ -151,7 +167,17 @@
-     }
- 
-     public void addOccupant(Bee entity) {
--        if (this.stored.size() < 3) {
-+        if (this.stored.size() < this.maxBees) { // CraftBukkit
-+            // CraftBukkit start
-+            if (this.level != null) {
-+                org.bukkit.event.entity.EntityEnterBlockEvent event = new org.bukkit.event.entity.EntityEnterBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(this.level, this.getBlockPos()));
-+                org.bukkit.Bukkit.getPluginManager().callEvent(event);
-+                if (event.isCancelled()) {
-+                    entity.setStayOutOfHiveCountdown(400);
-+                    return;
-+                }
-+            }
-+            // CraftBukkit end
-             entity.stopRiding();
-             entity.ejectPassengers();
-             entity.dropLeash();
-@@ -167,7 +193,7 @@
-                 this.level.gameEvent((Holder) GameEvent.BLOCK_CHANGE, blockposition, GameEvent.Context.of(entity, this.getBlockState()));
-             }
- 
--            entity.discard();
-+            entity.discard(EntityRemoveEvent.Cause.ENTER_BLOCK); // CraftBukkit - add Bukkit remove cause
-             super.setChanged();
-         }
-     }
-@@ -177,32 +203,50 @@
-     }
- 
-     private static boolean releaseOccupant(Level world, BlockPos pos, BlockState state, BeehiveBlockEntity.Occupant bee, @Nullable List<Entity> entities, BeehiveBlockEntity.BeeReleaseStatus beeState, @Nullable BlockPos flowerPos) {
--        if (Bee.isNightOrRaining(world) && beeState != BeehiveBlockEntity.BeeReleaseStatus.EMERGENCY) {
-+        // CraftBukkit start - This allows us to bypass the night/rain/emergency check
-+        return BeehiveBlockEntity.releaseOccupant(world, pos, state, bee, entities, beeState, flowerPos, false);
-+    }
-+
-+    private static boolean releaseOccupant(Level world, BlockPos blockposition, BlockState iblockdata, BeehiveBlockEntity.Occupant tileentitybeehive_c, @Nullable List<Entity> list, BeehiveBlockEntity.BeeReleaseStatus tileentitybeehive_releasestatus, @Nullable BlockPos blockposition1, boolean force) {
-+        if (!force && Bee.isNightOrRaining(world) && tileentitybeehive_releasestatus != BeehiveBlockEntity.BeeReleaseStatus.EMERGENCY) {
-+            // CraftBukkit end
-             return false;
-         } else {
--            Direction enumdirection = (Direction) state.getValue(BeehiveBlock.FACING);
--            BlockPos blockposition2 = pos.relative(enumdirection);
-+            Direction enumdirection = (Direction) iblockdata.getValue(BeehiveBlock.FACING);
-+            BlockPos blockposition2 = blockposition.relative(enumdirection);
-             boolean flag = !world.getBlockState(blockposition2).getCollisionShape(world, blockposition2).isEmpty();
- 
--            if (flag && beeState != BeehiveBlockEntity.BeeReleaseStatus.EMERGENCY) {
-+            if (flag && tileentitybeehive_releasestatus != BeehiveBlockEntity.BeeReleaseStatus.EMERGENCY) {
-                 return false;
-             } else {
--                Entity entity = bee.createEntity(world, pos);
-+                Entity entity = tileentitybeehive_c.createEntity(world, blockposition);
- 
-                 if (entity != null) {
-+                    // CraftBukkit start
-                     if (entity instanceof Bee) {
-+                        float f = entity.getBbWidth();
-+                        double d0 = flag ? 0.0D : 0.55D + (double) (f / 2.0F);
-+                        double d1 = (double) blockposition.getX() + 0.5D + d0 * (double) enumdirection.getStepX();
-+                        double d2 = (double) blockposition.getY() + 0.5D - (double) (entity.getBbHeight() / 2.0F);
-+                        double d3 = (double) blockposition.getZ() + 0.5D + d0 * (double) enumdirection.getStepZ();
-+
-+                        entity.moveTo(d1, d2, d3, entity.getYRot(), entity.getXRot());
-+                    }
-+                    if (!world.addFreshEntity(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.BEEHIVE)) return false; // CraftBukkit - SpawnReason, moved from below
-+                    // CraftBukkit end
-+                    if (entity instanceof Bee) {
-                         Bee entitybee = (Bee) entity;
- 
--                        if (flowerPos != null && !entitybee.hasSavedFlowerPos() && world.random.nextFloat() < 0.9F) {
--                            entitybee.setSavedFlowerPos(flowerPos);
-+                        if (blockposition1 != null && !entitybee.hasSavedFlowerPos() && world.random.nextFloat() < 0.9F) {
-+                            entitybee.setSavedFlowerPos(blockposition1);
-                         }
- 
--                        if (beeState == BeehiveBlockEntity.BeeReleaseStatus.HONEY_DELIVERED) {
-+                        if (tileentitybeehive_releasestatus == BeehiveBlockEntity.BeeReleaseStatus.HONEY_DELIVERED) {
-                             entitybee.dropOffNectar();
--                            if (state.is(BlockTags.BEEHIVES, (blockbase_blockdata) -> {
-+                            if (iblockdata.is(BlockTags.BEEHIVES, (blockbase_blockdata) -> {
-                                 return blockbase_blockdata.hasProperty(BeehiveBlock.HONEY_LEVEL);
-                             })) {
--                                int i = BeehiveBlockEntity.getHoneyLevel(state);
-+                                int i = BeehiveBlockEntity.getHoneyLevel(iblockdata);
- 
-                                 if (i < 5) {
-                                     int j = world.random.nextInt(100) == 0 ? 2 : 1;
-@@ -211,27 +255,35 @@
-                                         --j;
-                                     }
- 
--                                    world.setBlockAndUpdate(pos, (BlockState) state.setValue(BeehiveBlock.HONEY_LEVEL, i + j));
-+                                    // Paper start - Fire EntityChangeBlockEvent in more places
-+                                    BlockState newBlockState = iblockdata.setValue(BeehiveBlock.HONEY_LEVEL, i + j);
-+
-+                                    if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(entitybee, blockposition, newBlockState)) {
-+                                        world.setBlockAndUpdate(blockposition, newBlockState);
-+                                    }
-+                                    // Paper end - Fire EntityChangeBlockEvent in more places
-                                 }
-                             }
-                         }
- 
--                        if (entities != null) {
--                            entities.add(entitybee);
-+                        if (list != null) {
-+                            list.add(entitybee);
-                         }
- 
-+                        /* // CraftBukkit start
-                         float f = entity.getBbWidth();
-                         double d0 = flag ? 0.0D : 0.55D + (double) (f / 2.0F);
--                        double d1 = (double) pos.getX() + 0.5D + d0 * (double) enumdirection.getStepX();
--                        double d2 = (double) pos.getY() + 0.5D - (double) (entity.getBbHeight() / 2.0F);
--                        double d3 = (double) pos.getZ() + 0.5D + d0 * (double) enumdirection.getStepZ();
-+                        double d1 = (double) blockposition.getX() + 0.5D + d0 * (double) enumdirection.getStepX();
-+                        double d2 = (double) blockposition.getY() + 0.5D - (double) (entity.getBbHeight() / 2.0F);
-+                        double d3 = (double) blockposition.getZ() + 0.5D + d0 * (double) enumdirection.getStepZ();
- 
-                         entity.moveTo(d1, d2, d3, entity.getYRot(), entity.getXRot());
-+                         */ // CraftBukkit end
-                     }
- 
--                    world.playSound((Player) null, pos, SoundEvents.BEEHIVE_EXIT, SoundSource.BLOCKS, 1.0F, 1.0F);
--                    world.gameEvent((Holder) GameEvent.BLOCK_CHANGE, pos, GameEvent.Context.of(entity, world.getBlockState(pos)));
--                    return world.addFreshEntity(entity);
-+                    world.playSound((Player) null, blockposition, SoundEvents.BEEHIVE_EXIT, SoundSource.BLOCKS, 1.0F, 1.0F);
-+                    world.gameEvent((Holder) GameEvent.BLOCK_CHANGE, blockposition, GameEvent.Context.of(entity, world.getBlockState(blockposition)));
-+                    return true; // return this.world.addFreshEntity(entity); // CraftBukkit - moved up
-                 } else {
-                     return false;
-                 }
-@@ -256,6 +308,10 @@
-                 if (BeehiveBlockEntity.releaseOccupant(world, pos, state, tileentitybeehive_hivebee.toOccupant(), (List) null, tileentitybeehive_releasestatus, flowerPos)) {
-                     flag = true;
-                     iterator.remove();
-+                    // CraftBukkit start
-+                } else {
-+                    tileentitybeehive_hivebee.exitTickCounter = tileentitybeehive_hivebee.occupant.minTicksInHive / 2; // Not strictly Vanilla behaviour in cases where bees cannot spawn but still reasonable // Paper - Fix bees aging inside hives; use exitTickCounter to keep actual bee life
-+                    // CraftBukkit end
-                 }
-             }
-         }
-@@ -282,7 +338,7 @@
-     @Override
-     protected void loadAdditional(CompoundTag nbt, HolderLookup.Provider registries) {
-         super.loadAdditional(nbt, registries);
--        this.stored.clear();
-+        this.stored = Lists.newArrayList(); // CraftBukkit - SPIGOT-7790: create new copy (may be modified in physics event triggered by honey change)
-         if (nbt.contains("bees")) {
-             BeehiveBlockEntity.Occupant.LIST_CODEC.parse(NbtOps.INSTANCE, nbt.get("bees")).resultOrPartial((s) -> {
-                 BeehiveBlockEntity.LOGGER.error("Failed to parse bees: '{}'", s);
-@@ -291,7 +347,12 @@
-             });
-         }
- 
--        this.savedFlowerPos = (BlockPos) NbtUtils.readBlockPos(nbt, "flower_pos").orElse((Object) null);
-+        this.savedFlowerPos = (BlockPos) NbtUtils.readBlockPos(nbt, "flower_pos").orElse(null); // CraftBukkit - decompile error
-+        // CraftBukkit start
-+        if (nbt.contains("Bukkit.MaxEntities")) {
-+            this.maxBees = nbt.getInt("Bukkit.MaxEntities");
-+        }
-+        // CraftBukkit end
-     }
- 
-     @Override
-@@ -301,13 +362,14 @@
-         if (this.hasSavedFlowerPos()) {
-             nbt.put("flower_pos", NbtUtils.writeBlockPos(this.savedFlowerPos));
-         }
-+        nbt.putInt("Bukkit.MaxEntities", this.maxBees); // CraftBukkit
- 
-     }
- 
-     @Override
-     protected void applyImplicitComponents(BlockEntity.DataComponentInput components) {
-         super.applyImplicitComponents(components);
--        this.stored.clear();
-+        this.stored = Lists.newArrayList(); // CraftBukkit - SPIGOT-7790: create new copy (may be modified in physics event triggered by honey change)
-         List<BeehiveBlockEntity.Occupant> list = (List) components.getOrDefault(DataComponents.BEES, List.of());
- 
-         list.forEach(this::storeBee);
-@@ -348,7 +410,7 @@
-             CompoundTag nbttagcompound = new CompoundTag();
- 
-             entity.save(nbttagcompound);
--            List list = BeehiveBlockEntity.IGNORED_BEE_TAGS;
-+            List<String> list = BeehiveBlockEntity.IGNORED_BEE_TAGS; // CraftBukkit - decompile error
- 
-             Objects.requireNonNull(nbttagcompound);
-             list.forEach(nbttagcompound::remove);
-@@ -367,7 +429,7 @@
-         @Nullable
-         public Entity createEntity(Level world, BlockPos pos) {
-             CompoundTag nbttagcompound = this.entityData.copyTag();
--            List list = BeehiveBlockEntity.IGNORED_BEE_TAGS;
-+            List<String> list = BeehiveBlockEntity.IGNORED_BEE_TAGS; // CraftBukkit - decompile error
- 
-             Objects.requireNonNull(nbttagcompound);
-             list.forEach(nbttagcompound::remove);
-@@ -391,6 +453,7 @@
-         }
- 
-         private static void setBeeReleaseData(int ticksInHive, Bee beeEntity) {
-+            if (!beeEntity.ageLocked) { // Paper - Honor ageLock
-             int j = beeEntity.getAge();
- 
-             if (j < 0) {
-@@ -400,21 +463,25 @@
-             }
- 
-             beeEntity.setInLoveTime(Math.max(0, beeEntity.getInLoveTime() - ticksInHive));
-+            } // Paper - Honor ageLock
-         }
-     }
- 
-     private static class BeeData {
- 
-         private final BeehiveBlockEntity.Occupant occupant;
-+        private int exitTickCounter; // Paper - Fix bees aging inside hives; separate counter for checking if bee should exit to reduce exit attempts
-         private int ticksInHive;
- 
-         BeeData(BeehiveBlockEntity.Occupant data) {
-             this.occupant = data;
-             this.ticksInHive = data.ticksInHive();
-+            this.exitTickCounter = this.ticksInHive; // Paper - Fix bees aging inside hives
-         }
- 
-         public boolean tick() {
--            return this.ticksInHive++ > this.occupant.minTicksInHive;
-+            this.ticksInHive++; // Paper - Fix bees aging inside hives
-+            return this.exitTickCounter++ > this.occupant.minTicksInHive; // Paper - Fix bees aging inside hives
-         }
- 
-         public BeehiveBlockEntity.Occupant toOccupant() {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BlockEntity.java.patch
deleted file mode 100644
index aca157f8aa..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BlockEntity.java.patch
+++ /dev/null
@@ -1,153 +0,0 @@
---- a/net/minecraft/world/level/block/entity/BlockEntity.java
-+++ b/net/minecraft/world/level/block/entity/BlockEntity.java
-@@ -26,8 +26,18 @@
- import net.minecraft.world.level.block.state.BlockState;
- import org.slf4j.Logger;
- 
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer;
-+import org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry;
-+import org.bukkit.inventory.InventoryHolder;
-+// CraftBukkit end
-+
- public abstract class BlockEntity {
- 
-+    // CraftBukkit start - data containers
-+    private static final CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new CraftPersistentDataTypeRegistry();
-+    public CraftPersistentDataContainer persistentDataContainer;
-+    // CraftBukkit end
-     private static final Logger LOGGER = LogUtils.getLogger();
-     private final BlockEntityType<?> type;
-     @Nullable
-@@ -43,6 +53,7 @@
-         this.worldPosition = pos.immutable();
-         this.validateBlockState(state);
-         this.blockState = state;
-+        this.persistentDataContainer = new CraftPersistentDataContainer(DATA_TYPE_REGISTRY); // Paper - always init
-     }
- 
-     private void validateBlockState(BlockState state) {
-@@ -74,7 +85,16 @@
-         return this.level != null;
-     }
- 
--    protected void loadAdditional(CompoundTag nbt, HolderLookup.Provider registries) {}
-+    // CraftBukkit start - read container
-+    protected void loadAdditional(CompoundTag nbt, HolderLookup.Provider registries) {
-+        this.persistentDataContainer.clear(); // Paper - clear instead of init
-+
-+        net.minecraft.nbt.Tag persistentDataTag = nbt.get("PublicBukkitValues");
-+        if (persistentDataTag instanceof CompoundTag) {
-+            this.persistentDataContainer.putAll((CompoundTag) persistentDataTag);
-+        }
-+    }
-+    // CraftBukkit end
- 
-     public final void loadWithComponents(CompoundTag nbt, HolderLookup.Provider registries) {
-         this.loadAdditional(nbt, registries);
-@@ -114,6 +134,11 @@
-         }).ifPresent((nbtbase) -> {
-             nbttagcompound.merge((CompoundTag) nbtbase);
-         });
-+        // CraftBukkit start - store container
-+        if (this.persistentDataContainer != null && !this.persistentDataContainer.isEmpty()) {
-+            nbttagcompound.put("PublicBukkitValues", this.persistentDataContainer.toTagCompound());
-+        }
-+        // CraftBukkit end
-         return nbttagcompound;
-     }
- 
-@@ -121,6 +146,11 @@
-         CompoundTag nbttagcompound = new CompoundTag();
- 
-         this.saveAdditional(nbttagcompound, registries);
-+        // Paper start - store PDC here as well
-+        if (this.persistentDataContainer != null && !this.persistentDataContainer.isEmpty()) {
-+            nbttagcompound.put("PublicBukkitValues", this.persistentDataContainer.toTagCompound());
-+        }
-+        // Paper end
-         return nbttagcompound;
-     }
- 
-@@ -234,7 +264,12 @@
-     public void fillCrashReportCategory(CrashReportCategory crashReportSection) {
-         crashReportSection.setDetail("Name", this::getNameForReporting);
-         if (this.level != null) {
--            CrashReportCategory.populateBlockDetails(crashReportSection, this.level, this.worldPosition, this.getBlockState());
-+            // Paper start - Prevent block entity and entity crashes
-+            BlockState block = this.getBlockState();
-+            if (block != null) {
-+                CrashReportCategory.populateBlockDetails(crashReportSection, this.level, this.worldPosition, block);
-+            }
-+            // Paper end - Prevent block entity and entity crashes
-             CrashReportCategory.populateBlockDetails(crashReportSection, this.level, this.worldPosition, this.level.getBlockState(this.worldPosition));
-         }
-     }
-@@ -263,13 +298,19 @@
-     }
- 
-     public final void applyComponents(DataComponentMap defaultComponents, DataComponentPatch components) {
-+        // CraftBukkit start
-+        this.applyComponentsSet(defaultComponents, components);
-+    }
-+
-+    public final Set<DataComponentType<?>> applyComponentsSet(DataComponentMap datacomponentmap, DataComponentPatch datacomponentpatch) {
-+        // CraftBukkit end
-         final Set<DataComponentType<?>> set = new HashSet();
- 
-         set.add(DataComponents.BLOCK_ENTITY_DATA);
-         set.add(DataComponents.BLOCK_STATE);
--        final PatchedDataComponentMap patcheddatacomponentmap = PatchedDataComponentMap.fromPatch(defaultComponents, components);
-+        final PatchedDataComponentMap patcheddatacomponentmap = PatchedDataComponentMap.fromPatch(datacomponentmap, datacomponentpatch);
- 
--        this.applyImplicitComponents(new BlockEntity.DataComponentInput(this) {
-+        this.applyImplicitComponents(new BlockEntity.DataComponentInput() { // CraftBukkit - decompile error
-             @Nullable
-             @Override
-             public <T> T get(DataComponentType<T> type) {
-@@ -284,9 +325,13 @@
-             }
-         });
-         Objects.requireNonNull(set);
--        DataComponentPatch datacomponentpatch1 = components.forget(set::contains);
-+        DataComponentPatch datacomponentpatch1 = datacomponentpatch.forget(set::contains);
- 
-         this.components = datacomponentpatch1.split().added();
-+        // CraftBukkit start
-+        set.remove(DataComponents.BLOCK_ENTITY_DATA); // Remove as never actually added by applyImplicitComponents
-+        return set;
-+        // CraftBukkit end
-     }
- 
-     protected void collectImplicitComponents(DataComponentMap.Builder builder) {}
-@@ -321,6 +366,30 @@
-         }
-     }
- 
-+    // CraftBukkit start - add method
-+    public InventoryHolder getOwner() {
-+        // Paper start
-+        return getOwner(true);
-+    }
-+    public InventoryHolder getOwner(boolean useSnapshot) {
-+        // Paper end
-+        if (this.level == null) return null;
-+        org.bukkit.block.Block block = this.level.getWorld().getBlockAt(this.worldPosition.getX(), this.worldPosition.getY(), this.worldPosition.getZ());
-+        // if (block.getType() == org.bukkit.Material.AIR) return null; // Paper - actually get the tile entity if it still exists
-+        org.bukkit.block.BlockState state = block.getState(useSnapshot); // Paper
-+        if (state instanceof InventoryHolder) return (InventoryHolder) state;
-+        return null;
-+    }
-+    // CraftBukkit end
-+
-+    // Paper start - Sanitize sent data
-+    public CompoundTag sanitizeSentNbt(CompoundTag tag) {
-+        tag.remove("PublicBukkitValues");
-+
-+        return tag;
-+    }
-+    // Paper end - Sanitize sent data
-+
-     private static class ComponentHelper {
- 
-         public static final Codec<DataComponentMap> COMPONENTS_CODEC = DataComponentMap.CODEC.optionalFieldOf("components", DataComponentMap.EMPTY).codec();
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BlockEntityType.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BlockEntityType.java.patch
deleted file mode 100644
index c2e5be1463..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BlockEntityType.java.patch
+++ /dev/null
@@ -1,20 +0,0 @@
---- a/net/minecraft/world/level/block/entity/BlockEntityType.java
-+++ b/net/minecraft/world/level/block/entity/BlockEntityType.java
-@@ -66,7 +66,7 @@
-     public static final BlockEntityType<CrafterBlockEntity> CRAFTER = BlockEntityType.register("crafter", CrafterBlockEntity::new, Blocks.CRAFTER);
-     public static final BlockEntityType<TrialSpawnerBlockEntity> TRIAL_SPAWNER = BlockEntityType.register("trial_spawner", TrialSpawnerBlockEntity::new, Blocks.TRIAL_SPAWNER);
-     public static final BlockEntityType<VaultBlockEntity> VAULT = BlockEntityType.register("vault", VaultBlockEntity::new, Blocks.VAULT);
--    private static final Set<BlockEntityType<?>> OP_ONLY_CUSTOM_DATA = Set.of(BlockEntityType.COMMAND_BLOCK, BlockEntityType.LECTERN, BlockEntityType.SIGN, BlockEntityType.HANGING_SIGN, BlockEntityType.MOB_SPAWNER, BlockEntityType.TRIAL_SPAWNER);
-+    private static final Set<BlockEntityType<?>> OP_ONLY_CUSTOM_DATA = Set.of(BlockEntityType.COMMAND_BLOCK, BlockEntityType.LECTERN, BlockEntityType.SIGN, BlockEntityType.HANGING_SIGN, BlockEntityType.MOB_SPAWNER, BlockEntityType.TRIAL_SPAWNER); // CraftBukkit // Paper - Allow chests to be placed with NBT data
-     private final BlockEntityType.BlockEntitySupplier<? extends T> factory;
-     public final Set<Block> validBlocks;
-     private final Holder.Reference<BlockEntityType<?>> builtInRegistryHolder;
-@@ -110,7 +110,7 @@
-     public T getBlockEntity(BlockGetter world, BlockPos pos) {
-         BlockEntity tileentity = world.getBlockEntity(pos);
- 
--        return tileentity != null && tileentity.getType() == this ? tileentity : null;
-+        return tileentity != null && tileentity.getType() == this ? (T) tileentity : null; // CraftBukkit - decompile error
-     }
- 
-     public boolean onlyOpCanSetNbt() {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java.patch
deleted file mode 100644
index 39dec9ecc2..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java.patch
+++ /dev/null
@@ -1,232 +0,0 @@
---- a/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java
-+++ b/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java
-@@ -8,7 +8,6 @@
- import net.minecraft.core.NonNullList;
- import net.minecraft.nbt.CompoundTag;
- import net.minecraft.network.chat.Component;
--import net.minecraft.tags.ItemTags;
- import net.minecraft.world.ContainerHelper;
- import net.minecraft.world.Containers;
- import net.minecraft.world.WorldlyContainer;
-@@ -23,6 +22,20 @@
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.block.BrewingStandBlock;
- import net.minecraft.world.level.block.state.BlockState;
-+// CraftBukkit start
-+import java.util.ArrayList;
-+import java.util.List;
-+import net.minecraft.server.MinecraftServer;
-+import net.minecraft.tags.ItemTags;
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.entity.HumanEntity;
-+import org.bukkit.event.block.BrewingStartEvent;
-+import org.bukkit.event.inventory.BrewEvent;
-+import org.bukkit.event.inventory.BrewingStandFuelEvent;
-+import org.bukkit.inventory.InventoryHolder;
-+// CraftBukkit end
- 
- public class BrewingStandBlockEntity extends BaseContainerBlockEntity implements WorldlyContainer {
- 
-@@ -37,11 +50,42 @@
-     public static final int NUM_DATA_VALUES = 2;
-     private NonNullList<ItemStack> items;
-     public int brewTime;
-+    public int recipeBrewTime = 400; // Paper - Add recipeBrewTime
-     private boolean[] lastPotionCount;
-     private Item ingredient;
-     public int fuel;
-     protected final ContainerData dataAccess;
-+    // CraftBukkit start - add fields and methods
-+    // private int lastTick = MinecraftServer.currentTick; // Paper - remove anti tick skipping measures / wall time
-+    public List<HumanEntity> transaction = new java.util.ArrayList<HumanEntity>();
-+    private int maxStack = MAX_STACK;
- 
-+    public void onOpen(CraftHumanEntity who) {
-+        this.transaction.add(who);
-+    }
-+
-+    public void onClose(CraftHumanEntity who) {
-+        this.transaction.remove(who);
-+    }
-+
-+    public List<HumanEntity> getViewers() {
-+        return this.transaction;
-+    }
-+
-+    public List<ItemStack> getContents() {
-+        return this.items;
-+    }
-+
-+    @Override
-+    public int getMaxStackSize() {
-+        return this.maxStack;
-+    }
-+
-+    public void setMaxStackSize(int size) {
-+        this.maxStack = size;
-+    }
-+    // CraftBukkit end
-+
-     public BrewingStandBlockEntity(BlockPos pos, BlockState state) {
-         super(BlockEntityType.BREWING_STAND, pos, state);
-         this.items = NonNullList.withSize(5, ItemStack.EMPTY);
-@@ -57,6 +101,11 @@
-                     case 1:
-                         j = BrewingStandBlockEntity.this.fuel;
-                         break;
-+                    // Paper start - Add recipeBrewTime
-+                    case 2:
-+                        j = BrewingStandBlockEntity.this.recipeBrewTime;
-+                        break;
-+                    // Paper end - Add recipeBrewTime
-                     default:
-                         j = 0;
-                 }
-@@ -72,13 +121,18 @@
-                         break;
-                     case 1:
-                         BrewingStandBlockEntity.this.fuel = value;
-+                        // Paper start - Add recipeBrewTime
-+                    case 2:
-+                        BrewingStandBlockEntity.this.recipeBrewTime = value;
-+                        break;
-+                    // Paper end - Add recipeBrewTime
-                 }
- 
-             }
- 
-             @Override
-             public int getCount() {
--                return 2;
-+                return 3; // Paper - Add recipeBrewTime
-             }
-         };
-     }
-@@ -107,8 +161,19 @@
-         ItemStack itemstack = (ItemStack) blockEntity.items.get(4);
- 
-         if (blockEntity.fuel <= 0 && itemstack.is(ItemTags.BREWING_FUEL)) {
--            blockEntity.fuel = 20;
--            itemstack.shrink(1);
-+            // CraftBukkit start
-+            BrewingStandFuelEvent event = new BrewingStandFuelEvent(CraftBlock.at(world, pos), CraftItemStack.asCraftMirror(itemstack), 20);
-+            world.getCraftServer().getPluginManager().callEvent(event);
-+
-+            if (event.isCancelled()) {
-+                return;
-+            }
-+
-+            blockEntity.fuel = event.getFuelPower();
-+            if (blockEntity.fuel > 0 && event.isConsuming()) {
-+                itemstack.shrink(1);
-+            }
-+            // CraftBukkit end
-             setChanged(world, pos, state);
-         }
- 
-@@ -116,12 +181,15 @@
-         boolean flag1 = blockEntity.brewTime > 0;
-         ItemStack itemstack1 = (ItemStack) blockEntity.items.get(3);
- 
-+        // Paper - remove anti tick skipping measures / wall time
-+
-         if (flag1) {
--            --blockEntity.brewTime;
--            boolean flag2 = blockEntity.brewTime == 0;
-+            --blockEntity.brewTime; // Paper - remove anti tick skipping measures / wall time - revert to vanilla
-+            boolean flag2 = blockEntity.brewTime <= 0; // == -> <=
-+            // CraftBukkit end
- 
-             if (flag2 && flag) {
--                BrewingStandBlockEntity.doBrew(world, pos, blockEntity.items);
-+                BrewingStandBlockEntity.doBrew(world, pos, blockEntity.items, blockEntity); // CraftBukkit
-             } else if (!flag || !itemstack1.is(blockEntity.ingredient)) {
-                 blockEntity.brewTime = 0;
-             }
-@@ -129,7 +197,12 @@
-             setChanged(world, pos, state);
-         } else if (flag && blockEntity.fuel > 0) {
-             --blockEntity.fuel;
--            blockEntity.brewTime = 400;
-+            // CraftBukkit start
-+            BrewingStartEvent event = new BrewingStartEvent(CraftBlock.at(world, pos), CraftItemStack.asCraftMirror(itemstack1), 400);
-+            world.getCraftServer().getPluginManager().callEvent(event);
-+            blockEntity.recipeBrewTime = event.getRecipeBrewTime(); // Paper - use recipe brew time from event
-+            blockEntity.brewTime = event.getBrewingTime(); // 400 -> event.getTotalBrewTime() // Paper - use brewing time from event
-+            // CraftBukkit end
-             blockEntity.ingredient = itemstack1.getItem();
-             setChanged(world, pos, state);
-         }
-@@ -185,14 +258,36 @@
-         }
-     }
- 
--    private static void doBrew(Level world, BlockPos pos, NonNullList<ItemStack> slots) {
--        ItemStack itemstack = (ItemStack) slots.get(3);
-+    private static void doBrew(Level world, BlockPos blockposition, NonNullList<ItemStack> nonnulllist, BrewingStandBlockEntity tileentitybrewingstand) { // CraftBukkit
-+        ItemStack itemstack = (ItemStack) nonnulllist.get(3);
-         PotionBrewing potionbrewer = world.potionBrewing();
- 
-+        // CraftBukkit start
-+        InventoryHolder owner = tileentitybrewingstand.getOwner();
-+        List<org.bukkit.inventory.ItemStack> brewResults = new ArrayList<>(3);
-         for (int i = 0; i < 3; ++i) {
--            slots.set(i, potionbrewer.mix(itemstack, (ItemStack) slots.get(i)));
-+            brewResults.add(i, CraftItemStack.asCraftMirror(potionbrewer.mix(itemstack, (ItemStack) nonnulllist.get(i))));
-         }
- 
-+        if (owner != null) {
-+            BrewEvent event = new BrewEvent(CraftBlock.at(world, blockposition), (org.bukkit.inventory.BrewerInventory) owner.getInventory(), brewResults, tileentitybrewingstand.fuel);
-+            org.bukkit.Bukkit.getPluginManager().callEvent(event);
-+            if (event.isCancelled()) {
-+                return;
-+            }
-+        }
-+        // CraftBukkit end
-+
-+        for (int i = 0; i < 3; ++i) {
-+            // CraftBukkit start - validate index in case it is cleared by plugins
-+            if (i < brewResults.size()) {
-+                nonnulllist.set(i, CraftItemStack.asNMSCopy(brewResults.get(i)));
-+            } else {
-+                nonnulllist.set(i, ItemStack.EMPTY);
-+            }
-+            // CraftBukkit end
-+        }
-+
-         itemstack.shrink(1);
-         ItemStack itemstack1 = itemstack.getItem().getCraftingRemainder();
- 
-@@ -200,12 +295,12 @@
-             if (itemstack.isEmpty()) {
-                 itemstack = itemstack1;
-             } else {
--                Containers.dropItemStack(world, (double) pos.getX(), (double) pos.getY(), (double) pos.getZ(), itemstack1);
-+                Containers.dropItemStack(world, (double) blockposition.getX(), (double) blockposition.getY(), (double) blockposition.getZ(), itemstack1);
-             }
-         }
- 
--        slots.set(3, itemstack);
--        world.levelEvent(1035, pos, 0);
-+        nonnulllist.set(3, itemstack);
-+        world.levelEvent(1035, blockposition, 0);
-     }
- 
-     @Override
-@@ -231,12 +326,12 @@
- 
-     @Override
-     public boolean canPlaceItem(int slot, ItemStack stack) {
-+        PotionBrewing potionbrewer = this.level != null ? this.level.potionBrewing() : PotionBrewing.EMPTY; // Paper - move up
-         if (slot == 3) {
--            PotionBrewing potionbrewer = this.level != null ? this.level.potionBrewing() : PotionBrewing.EMPTY;
- 
-             return potionbrewer.isIngredient(stack);
-         } else {
--            return slot == 4 ? stack.is(ItemTags.BREWING_FUEL) : (stack.is(Items.POTION) || stack.is(Items.SPLASH_POTION) || stack.is(Items.LINGERING_POTION) || stack.is(Items.GLASS_BOTTLE)) && this.getItem(slot).isEmpty();
-+            return slot == 4 ? stack.is(ItemTags.BREWING_FUEL) : (stack.is(Items.POTION) || stack.is(Items.SPLASH_POTION) || stack.is(Items.LINGERING_POTION) || stack.is(Items.GLASS_BOTTLE) || potionbrewer.isCustomInput(stack)) && this.getItem(slot).isEmpty(); // Paper - Custom Potion Mixes
-         }
-     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BrushableBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BrushableBlockEntity.java.patch
deleted file mode 100644
index 57515e3834..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BrushableBlockEntity.java.patch
+++ /dev/null
@@ -1,36 +0,0 @@
---- a/net/minecraft/world/level/block/entity/BrushableBlockEntity.java
-+++ b/net/minecraft/world/level/block/entity/BrushableBlockEntity.java
-@@ -31,6 +31,12 @@
- import net.minecraft.world.phys.Vec3;
- import org.slf4j.Logger;
- 
-+// CraftBukkit start
-+import java.util.Arrays;
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+// CraftBukkit end
-+
- public class BrushableBlockEntity extends BlockEntity {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
-@@ -151,7 +157,10 @@
-             ItemEntity entityitem = new ItemEntity(world, d3, d4, d5, this.item.split(world.random.nextInt(21) + 10));
- 
-             entityitem.setDeltaMovement(Vec3.ZERO);
--            world.addFreshEntity(entityitem);
-+            // CraftBukkit start
-+            org.bukkit.block.Block bblock = CraftBlock.at(this.level, this.worldPosition);
-+            CraftEventFactory.handleBlockDropItemEvent(bblock, bblock.getState(), (ServerPlayer) player, Arrays.asList(entityitem));
-+            // CraftBukkit end
-             this.item = ItemStack.EMPTY;
-         }
- 
-@@ -185,7 +194,7 @@
- 
-     private boolean tryLoadLootTable(CompoundTag nbt) {
-         if (nbt.contains("LootTable", 8)) {
--            this.lootTable = ResourceKey.create(Registries.LOOT_TABLE, ResourceLocation.parse(nbt.getString("LootTable")));
-+            this.lootTable = net.minecraft.Optionull.map(ResourceLocation.tryParse(nbt.getString("LootTable")), rl -> ResourceKey.create(Registries.LOOT_TABLE, rl)); // Paper - Validate ResourceLocation
-             this.lootTableSeed = nbt.getLong("LootTableSeed");
-             return true;
-         } else {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/CampfireBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/CampfireBlockEntity.java.patch
deleted file mode 100644
index aea8cb9be4..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/CampfireBlockEntity.java.patch
+++ /dev/null
@@ -1,121 +0,0 @@
---- a/net/minecraft/world/level/block/entity/CampfireBlockEntity.java
-+++ b/net/minecraft/world/level/block/entity/CampfireBlockEntity.java
-@@ -31,6 +31,14 @@
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.gameevent.GameEvent;
- 
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.event.block.BlockCookEvent;
-+import org.bukkit.event.block.CampfireStartEvent;
-+import org.bukkit.inventory.CampfireRecipe;
-+// CraftBukkit end
-+
- public class CampfireBlockEntity extends BlockEntity implements Clearable {
- 
-     private static final int BURN_COOL_SPEED = 2;
-@@ -38,12 +46,14 @@
-     private final NonNullList<ItemStack> items;
-     public final int[] cookingProgress;
-     public final int[] cookingTime;
-+    public final boolean[] stopCooking; // Paper - Add more Campfire API
- 
-     public CampfireBlockEntity(BlockPos pos, BlockState state) {
-         super(BlockEntityType.CAMPFIRE, pos, state);
-         this.items = NonNullList.withSize(4, ItemStack.EMPTY);
-         this.cookingProgress = new int[4];
-         this.cookingTime = new int[4];
-+        this.stopCooking = new boolean[4]; // Paper - Add more Campfire API
-     }
- 
-     public static void cookTick(ServerLevel world, BlockPos pos, BlockState state, CampfireBlockEntity blockEntity, RecipeManager.CachedCheck<SingleRecipeInput, CampfireCookingRecipe> recipeMatchGetter) {
-@@ -54,16 +64,42 @@
- 
-             if (!itemstack.isEmpty()) {
-                 flag = true;
-+                if (!blockEntity.stopCooking[i]) { // Paper - Add more Campfire API
-                 int j = blockEntity.cookingProgress[i]++;
-+                } // Paper - Add more Campfire API
- 
-                 if (blockEntity.cookingProgress[i] >= blockEntity.cookingTime[i]) {
-                     SingleRecipeInput singlerecipeinput = new SingleRecipeInput(itemstack);
--                    ItemStack itemstack1 = (ItemStack) recipeMatchGetter.getRecipeFor(singlerecipeinput, world).map((recipeholder) -> {
-+                    // Paper start - add recipe to cook events
-+                    final Optional<RecipeHolder<CampfireCookingRecipe>> recipeHolderOptional = recipeMatchGetter.getRecipeFor(singlerecipeinput, world);
-+                    ItemStack itemstack1 = (ItemStack) recipeHolderOptional.map((recipeholder) -> {
-+                    // Paper end - add recipe to cook events
-                         return ((CampfireCookingRecipe) recipeholder.value()).assemble(singlerecipeinput, world.registryAccess());
-                     }).orElse(itemstack);
- 
-                     if (itemstack1.isItemEnabled(world.enabledFeatures())) {
--                        Containers.dropItemStack(world, (double) pos.getX(), (double) pos.getY(), (double) pos.getZ(), itemstack1);
-+                        // CraftBukkit start - fire BlockCookEvent
-+                        CraftItemStack source = CraftItemStack.asCraftMirror(itemstack);
-+                        org.bukkit.inventory.ItemStack result = CraftItemStack.asBukkitCopy(itemstack1);
-+
-+                        BlockCookEvent blockCookEvent = new BlockCookEvent(CraftBlock.at(world, pos), source, result, (org.bukkit.inventory.CookingRecipe<?>) recipeHolderOptional.map(RecipeHolder::toBukkitRecipe).orElse(null)); // Paper - Add recipe to cook events
-+                        world.getCraftServer().getPluginManager().callEvent(blockCookEvent);
-+
-+                        if (blockCookEvent.isCancelled()) {
-+                            return;
-+                        }
-+
-+                        result = blockCookEvent.getResult();
-+                        itemstack1 = CraftItemStack.asNMSCopy(result);
-+                        // CraftBukkit end
-+                        // Paper start - Fix item locations dropped from campfires
-+                        double deviation = 0.05F * RandomSource.GAUSSIAN_SPREAD_FACTOR;
-+                        while (!itemstack1.isEmpty()) {
-+                            net.minecraft.world.entity.item.ItemEntity droppedItem = new net.minecraft.world.entity.item.ItemEntity(world, pos.getX() + 0.5D, pos.getY() + 0.5D, pos.getZ() + 0.5D, itemstack1.split(world.random.nextInt(21) + 10));
-+                            droppedItem.setDeltaMovement(world.random.triangle(0.0D, deviation), world.random.triangle(0.2D, deviation), world.random.triangle(0.0D, deviation));
-+                            world.addFreshEntity(droppedItem);
-+                        }
-+                        // Paper end - Fix item locations dropped from campfires
-                         blockEntity.items.set(i, ItemStack.EMPTY);
-                         world.sendBlockUpdated(pos, state, state, 3);
-                         world.gameEvent((Holder) GameEvent.BLOCK_CHANGE, pos, GameEvent.Context.of(state));
-@@ -143,6 +179,16 @@
-             System.arraycopy(aint, 0, this.cookingTime, 0, Math.min(this.cookingTime.length, aint.length));
-         }
- 
-+        // Paper start - Add more Campfire API
-+        if (nbt.contains("Paper.StopCooking", org.bukkit.craftbukkit.util.CraftMagicNumbers.NBT.TAG_BYTE_ARRAY)) {
-+            byte[] abyte = nbt.getByteArray("Paper.StopCooking");
-+            boolean[] cookingState = new boolean[4];
-+            for (int index = 0; index < abyte.length; index++) {
-+                cookingState[index] = abyte[index] == 1;
-+            }
-+            System.arraycopy(cookingState, 0, this.stopCooking, 0, Math.min(this.stopCooking.length, abyte.length));
-+        }
-+        // Paper end - Add more Campfire API
-     }
- 
-     @Override
-@@ -151,6 +197,13 @@
-         ContainerHelper.saveAllItems(nbt, this.items, true, registries);
-         nbt.putIntArray("CookingTimes", this.cookingProgress);
-         nbt.putIntArray("CookingTotalTimes", this.cookingTime);
-+        // Paper start - Add more Campfire API
-+        byte[] cookingState = new byte[4];
-+        for (int index = 0; index < cookingState.length; index++) {
-+            cookingState[index] = (byte) (this.stopCooking[index] ? 1 : 0);
-+        }
-+        nbt.putByteArray("Paper.StopCooking", cookingState);
-+        // Paper end - Add more Campfire API
-     }
- 
-     @Override
-@@ -177,7 +230,11 @@
-                     return false;
-                 }
- 
--                this.cookingTime[i] = ((CampfireCookingRecipe) ((RecipeHolder) optional.get()).value()).cookingTime();
-+                // CraftBukkit start
-+                CampfireStartEvent event = new CampfireStartEvent(CraftBlock.at(this.level,this.worldPosition), CraftItemStack.asCraftMirror(stack), (CampfireRecipe) optional.get().toBukkitRecipe());
-+                this.level.getCraftServer().getPluginManager().callEvent(event);
-+                this.cookingTime[i] = event.getTotalCookTime(); // i -> event.getTotalCookTime()
-+                // CraftBukkit end
-                 this.cookingProgress[i] = 0;
-                 this.items.set(i, stack.consumeAndReturn(1, entity));
-                 world.gameEvent((Holder) GameEvent.BLOCK_CHANGE, this.getBlockPos(), GameEvent.Context.of(entity, this.getBlockState()));
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ChestBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ChestBlockEntity.java.patch
deleted file mode 100644
index 537f1913b2..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ChestBlockEntity.java.patch
+++ /dev/null
@@ -1,51 +0,0 @@
---- a/net/minecraft/world/level/block/entity/ChestBlockEntity.java
-+++ b/net/minecraft/world/level/block/entity/ChestBlockEntity.java
-@@ -23,6 +23,11 @@
- import net.minecraft.world.level.block.ChestBlock;
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.block.state.properties.ChestType;
-+// CraftBukkit start
-+import java.util.List;
-+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
-+import org.bukkit.entity.HumanEntity;
-+// CraftBukkit end
- 
- public class ChestBlockEntity extends RandomizableContainerBlockEntity implements LidBlockEntity {
- 
-@@ -31,6 +36,36 @@
-     public final ContainerOpenersCounter openersCounter;
-     private final ChestLidController chestLidController;
- 
-+    // CraftBukkit start - add fields and methods
-+    public List<HumanEntity> transaction = new java.util.ArrayList<HumanEntity>();
-+    private int maxStack = MAX_STACK;
-+
-+    public List<ItemStack> getContents() {
-+        return this.items;
-+    }
-+
-+    public void onOpen(CraftHumanEntity who) {
-+        this.transaction.add(who);
-+    }
-+
-+    public void onClose(CraftHumanEntity who) {
-+        this.transaction.remove(who);
-+    }
-+
-+    public List<HumanEntity> getViewers() {
-+        return this.transaction;
-+    }
-+
-+    @Override
-+    public int getMaxStackSize() {
-+        return this.maxStack;
-+    }
-+
-+    public void setMaxStackSize(int size) {
-+        this.maxStack = size;
-+    }
-+    // CraftBukkit end
-+
-     protected ChestBlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) {
-         super(type, pos, state);
-         this.items = NonNullList.withSize(27, ItemStack.EMPTY);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/CommandBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/CommandBlockEntity.java.patch
deleted file mode 100644
index 49a6bff68c..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/CommandBlockEntity.java.patch
+++ /dev/null
@@ -1,26 +0,0 @@
---- a/net/minecraft/world/level/block/entity/CommandBlockEntity.java
-+++ b/net/minecraft/world/level/block/entity/CommandBlockEntity.java
-@@ -24,7 +24,14 @@
-     private boolean auto;
-     private boolean conditionMet;
-     private final BaseCommandBlock commandBlock = new BaseCommandBlock() {
-+        // CraftBukkit start
-         @Override
-+        public org.bukkit.command.CommandSender getBukkitSender(CommandSourceStack wrapper) {
-+            return new org.bukkit.craftbukkit.command.CraftBlockCommandSender(wrapper, CommandBlockEntity.this);
-+        }
-+        // CraftBukkit end
-+
-+        @Override
-         public void setCommand(String command) {
-             super.setCommand(command);
-             CommandBlockEntity.this.setChanged();
-@@ -51,7 +58,7 @@
-         public CommandSourceStack createCommandSourceStack() {
-             Direction enumdirection = (Direction) CommandBlockEntity.this.getBlockState().getValue(CommandBlock.FACING);
- 
--            return new CommandSourceStack(this, Vec3.atCenterOf(CommandBlockEntity.this.worldPosition), new Vec2(0.0F, enumdirection.toYRot()), this.getLevel(), 2, this.getName().getString(), this.getName(), this.getLevel().getServer(), (Entity) null);
-+            return new CommandSourceStack(this, Vec3.atCenterOf(CommandBlockEntity.this.worldPosition), new Vec2(0.0F, enumdirection.toYRot()), this.getLevel(), this.getLevel().paperConfig().commandBlocks.permissionsLevel, this.getName().getString(), this.getName(), this.getLevel().getServer(), (Entity) null); // Paper - configurable command block perm level
-         }
- 
-         @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ConduitBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ConduitBlockEntity.java.patch
deleted file mode 100644
index 6dd6c8627e..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ConduitBlockEntity.java.patch
+++ /dev/null
@@ -1,108 +0,0 @@
---- a/net/minecraft/world/level/block/entity/ConduitBlockEntity.java
-+++ b/net/minecraft/world/level/block/entity/ConduitBlockEntity.java
-@@ -10,6 +10,7 @@
- import net.minecraft.core.particles.ParticleTypes;
- import net.minecraft.nbt.CompoundTag;
- import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
-+import net.minecraft.server.level.ServerLevel;
- import net.minecraft.sounds.SoundEvent;
- import net.minecraft.sounds.SoundEvents;
- import net.minecraft.sounds.SoundSource;
-@@ -187,11 +188,23 @@
-     }
- 
-     private static void applyEffects(Level world, BlockPos pos, List<BlockPos> activatingBlocks) {
--        int i = activatingBlocks.size();
-+        // CraftBukkit start
-+        ConduitBlockEntity.applyEffects(world, pos, ConduitBlockEntity.getRange(activatingBlocks));
-+    }
-+
-+    public static int getRange(List<BlockPos> list) {
-+        // CraftBukkit end
-+        int i = list.size();
-         int j = i / 7 * 16;
--        int k = pos.getX();
--        int l = pos.getY();
--        int i1 = pos.getZ();
-+        // CraftBukkit start
-+        return j;
-+    }
-+
-+    private static void applyEffects(Level world, BlockPos blockposition, int j) { // j = effect range in blocks
-+        // CraftBukkit end
-+        int k = blockposition.getX();
-+        int l = blockposition.getY();
-+        int i1 = blockposition.getZ();
-         AABB axisalignedbb = (new AABB((double) k, (double) l, (double) i1, (double) (k + 1), (double) (l + 1), (double) (i1 + 1))).inflate((double) j).expandTowards(0.0D, (double) world.getHeight(), 0.0D);
-         List<Player> list1 = world.getEntitiesOfClass(Player.class, axisalignedbb);
- 
-@@ -201,8 +214,8 @@
-             while (iterator.hasNext()) {
-                 Player entityhuman = (Player) iterator.next();
- 
--                if (pos.closerThan(entityhuman.blockPosition(), (double) j) && entityhuman.isInWaterOrRain()) {
--                    entityhuman.addEffect(new MobEffectInstance(MobEffects.CONDUIT_POWER, 260, 0, true, true));
-+                if (blockposition.closerThan(entityhuman.blockPosition(), (double) j) && entityhuman.isInWaterOrRain()) {
-+                    entityhuman.addEffect(new MobEffectInstance(MobEffects.CONDUIT_POWER, 260, 0, true, true), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.CONDUIT); // CraftBukkit
-                 }
-             }
- 
-@@ -210,33 +223,42 @@
-     }
- 
-     private static void updateDestroyTarget(Level world, BlockPos pos, BlockState state, List<BlockPos> activatingBlocks, ConduitBlockEntity blockEntity) {
--        LivingEntity entityliving = blockEntity.destroyTarget;
--        int i = activatingBlocks.size();
-+        // CraftBukkit start - add "damageTarget" boolean
-+        ConduitBlockEntity.updateDestroyTarget(world, pos, state, activatingBlocks, blockEntity, true);
-+    }
- 
-+    public static void updateDestroyTarget(Level world, BlockPos blockposition, BlockState iblockdata, List<BlockPos> list, ConduitBlockEntity tileentityconduit, boolean damageTarget) {
-+        // CraftBukkit end
-+        LivingEntity entityliving = tileentityconduit.destroyTarget;
-+        int i = list.size();
-+
-         if (i < 42) {
--            blockEntity.destroyTarget = null;
--        } else if (blockEntity.destroyTarget == null && blockEntity.destroyTargetUUID != null) {
--            blockEntity.destroyTarget = ConduitBlockEntity.findDestroyTarget(world, pos, blockEntity.destroyTargetUUID);
--            blockEntity.destroyTargetUUID = null;
--        } else if (blockEntity.destroyTarget == null) {
--            List<LivingEntity> list1 = world.getEntitiesOfClass(LivingEntity.class, ConduitBlockEntity.getDestroyRangeAABB(pos), (entityliving1) -> {
-+            tileentityconduit.destroyTarget = null;
-+        } else if (tileentityconduit.destroyTarget == null && tileentityconduit.destroyTargetUUID != null) {
-+            tileentityconduit.destroyTarget = ConduitBlockEntity.findDestroyTarget(world, blockposition, tileentityconduit.destroyTargetUUID);
-+            tileentityconduit.destroyTargetUUID = null;
-+        } else if (tileentityconduit.destroyTarget == null) {
-+            List<LivingEntity> list1 = world.getEntitiesOfClass(LivingEntity.class, ConduitBlockEntity.getDestroyRangeAABB(blockposition), (entityliving1) -> {
-                 return entityliving1 instanceof Enemy && entityliving1.isInWaterOrRain();
-             });
- 
-             if (!list1.isEmpty()) {
--                blockEntity.destroyTarget = (LivingEntity) list1.get(world.random.nextInt(list1.size()));
-+                tileentityconduit.destroyTarget = (LivingEntity) list1.get(world.random.nextInt(list1.size()));
-             }
--        } else if (!blockEntity.destroyTarget.isAlive() || !pos.closerThan(blockEntity.destroyTarget.blockPosition(), 8.0D)) {
--            blockEntity.destroyTarget = null;
-+        } else if (!tileentityconduit.destroyTarget.isAlive() || !blockposition.closerThan(tileentityconduit.destroyTarget.blockPosition(), 8.0D)) {
-+            tileentityconduit.destroyTarget = null;
-         }
- 
--        if (blockEntity.destroyTarget != null) {
--            world.playSound((Player) null, blockEntity.destroyTarget.getX(), blockEntity.destroyTarget.getY(), blockEntity.destroyTarget.getZ(), SoundEvents.CONDUIT_ATTACK_TARGET, SoundSource.BLOCKS, 1.0F, 1.0F);
--            blockEntity.destroyTarget.hurt(world.damageSources().magic(), 4.0F);
-+        // CraftBukkit start
-+        if (damageTarget && tileentityconduit.destroyTarget != null) {
-+            if (tileentityconduit.destroyTarget.hurtServer((ServerLevel) world, world.damageSources().magic().directBlock(world, blockposition), 4.0F)) {
-+                world.playSound(null, tileentityconduit.destroyTarget.getX(), tileentityconduit.destroyTarget.getY(), tileentityconduit.destroyTarget.getZ(), SoundEvents.CONDUIT_ATTACK_TARGET, SoundSource.BLOCKS, 1.0F, 1.0F);
-+            }
-+            // CraftBukkit end
-         }
- 
--        if (entityliving != blockEntity.destroyTarget) {
--            world.sendBlockUpdated(pos, state, state, 2);
-+        if (entityliving != tileentityconduit.destroyTarget) {
-+            world.sendBlockUpdated(blockposition, iblockdata, iblockdata, 2);
-         }
- 
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/CrafterBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/CrafterBlockEntity.java.patch
deleted file mode 100644
index e71ac50ca3..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/CrafterBlockEntity.java.patch
+++ /dev/null
@@ -1,68 +0,0 @@
---- a/net/minecraft/world/level/block/entity/CrafterBlockEntity.java
-+++ b/net/minecraft/world/level/block/entity/CrafterBlockEntity.java
-@@ -22,6 +22,11 @@
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.block.CrafterBlock;
- import net.minecraft.world.level.block.state.BlockState;
-+// CraftBukkit start
-+import org.bukkit.Location;
-+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
-+import org.bukkit.entity.HumanEntity;
-+// CraftBukkit end
- 
- public class CrafterBlockEntity extends RandomizableContainerBlockEntity implements CraftingContainer {
- 
-@@ -35,12 +40,52 @@
-     private NonNullList<ItemStack> items;
-     public int craftingTicksRemaining;
-     protected final ContainerData containerData;
-+    // CraftBukkit start - add fields and methods
-+    public List<HumanEntity> transaction = new java.util.ArrayList<>();
-+    private int maxStack = MAX_STACK;
- 
-+    @Override
-+    public List<ItemStack> getContents() {
-+        return this.items;
-+    }
-+
-+    @Override
-+    public void onOpen(CraftHumanEntity who) {
-+        this.transaction.add(who);
-+    }
-+
-+    @Override
-+    public void onClose(CraftHumanEntity who) {
-+        this.transaction.remove(who);
-+    }
-+
-+    @Override
-+    public List<HumanEntity> getViewers() {
-+        return this.transaction;
-+    }
-+
-+    @Override
-+    public int getMaxStackSize() {
-+        return this.maxStack;
-+    }
-+
-+    @Override
-+    public void setMaxStackSize(int size) {
-+        this.maxStack = size;
-+    }
-+
-+    @Override
-+    public Location getLocation() {
-+        if (this.level == null) return null;
-+        return new org.bukkit.Location(this.level.getWorld(), this.worldPosition.getX(), this.worldPosition.getY(), this.worldPosition.getZ());
-+    }
-+    // CraftBukkit end
-+
-     public CrafterBlockEntity(BlockPos pos, BlockState state) {
-         super(BlockEntityType.CRAFTER, pos, state);
-         this.items = NonNullList.withSize(9, ItemStack.EMPTY);
-         this.craftingTicksRemaining = 0;
--        this.containerData = new ContainerData(this) {
-+        this.containerData = new ContainerData() { // CraftBukkit - decompile error
-             private final int[] slotStates = new int[9];
-             private int triggered = 0;
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/DispenserBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/DispenserBlockEntity.java.patch
deleted file mode 100644
index ae1688f953..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/DispenserBlockEntity.java.patch
+++ /dev/null
@@ -1,50 +0,0 @@
---- a/net/minecraft/world/level/block/entity/DispenserBlockEntity.java
-+++ b/net/minecraft/world/level/block/entity/DispenserBlockEntity.java
-@@ -13,12 +13,47 @@
- import net.minecraft.world.inventory.DispenserMenu;
- import net.minecraft.world.item.ItemStack;
- import net.minecraft.world.level.block.state.BlockState;
-+// CraftBukkit start
-+import java.util.List;
-+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
-+import org.bukkit.entity.HumanEntity;
-+// CraftBukkit end
- 
- public class DispenserBlockEntity extends RandomizableContainerBlockEntity {
- 
-     public static final int CONTAINER_SIZE = 9;
-     private NonNullList<ItemStack> items;
- 
-+    // CraftBukkit start - add fields and methods
-+    public List<HumanEntity> transaction = new java.util.ArrayList<HumanEntity>();
-+    private int maxStack = MAX_STACK;
-+
-+    public List<ItemStack> getContents() {
-+        return this.items;
-+    }
-+
-+    public void onOpen(CraftHumanEntity who) {
-+        this.transaction.add(who);
-+    }
-+
-+    public void onClose(CraftHumanEntity who) {
-+        this.transaction.remove(who);
-+    }
-+
-+    public List<HumanEntity> getViewers() {
-+        return this.transaction;
-+    }
-+
-+    @Override
-+    public int getMaxStackSize() {
-+        return this.maxStack;
-+    }
-+
-+    public void setMaxStackSize(int size) {
-+        this.maxStack = size;
-+    }
-+    // CraftBukkit end
-+
-     protected DispenserBlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) {
-         super(type, pos, state);
-         this.items = NonNullList.withSize(9, ItemStack.EMPTY);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/HopperBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/HopperBlockEntity.java.patch
deleted file mode 100644
index 58e1ea39e6..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/HopperBlockEntity.java.patch
+++ /dev/null
@@ -1,322 +0,0 @@
---- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java
-+++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java
-@@ -11,6 +11,7 @@
- import net.minecraft.nbt.CompoundTag;
- import net.minecraft.network.chat.Component;
- import net.minecraft.tags.BlockTags;
-+import net.minecraft.world.CompoundContainer;
- import net.minecraft.world.Container;
- import net.minecraft.world.ContainerHelper;
- import net.minecraft.world.WorldlyContainer;
-@@ -18,7 +19,6 @@
- import net.minecraft.world.entity.Entity;
- import net.minecraft.world.entity.EntitySelector;
- import net.minecraft.world.entity.item.ItemEntity;
--import net.minecraft.world.entity.player.Inventory;
- import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.inventory.AbstractContainerMenu;
- import net.minecraft.world.inventory.HopperMenu;
-@@ -29,6 +29,18 @@
- import net.minecraft.world.level.block.HopperBlock;
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.phys.AABB;
-+import org.bukkit.Bukkit;
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
-+import org.bukkit.craftbukkit.inventory.CraftInventory;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.entity.HumanEntity;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+import org.bukkit.event.inventory.HopperInventorySearchEvent;
-+import org.bukkit.event.inventory.InventoryMoveItemEvent;
-+import org.bukkit.event.inventory.InventoryPickupItemEvent;
-+import org.bukkit.inventory.Inventory;
-+// CraftBukkit end
- 
- public class HopperBlockEntity extends RandomizableContainerBlockEntity implements Hopper {
- 
-@@ -40,6 +52,36 @@
-     private long tickedGameTime;
-     private Direction facing;
- 
-+    // CraftBukkit start - add fields and methods
-+    public List<HumanEntity> transaction = new java.util.ArrayList<HumanEntity>();
-+    private int maxStack = MAX_STACK;
-+
-+    public List<ItemStack> getContents() {
-+        return this.items;
-+    }
-+
-+    public void onOpen(CraftHumanEntity who) {
-+        this.transaction.add(who);
-+    }
-+
-+    public void onClose(CraftHumanEntity who) {
-+        this.transaction.remove(who);
-+    }
-+
-+    public List<HumanEntity> getViewers() {
-+        return this.transaction;
-+    }
-+
-+    @Override
-+    public int getMaxStackSize() {
-+        return this.maxStack;
-+    }
-+
-+    public void setMaxStackSize(int size) {
-+        this.maxStack = size;
-+    }
-+    // CraftBukkit end
-+
-     public HopperBlockEntity(BlockPos pos, BlockState state) {
-         super(BlockEntityType.HOPPER, pos, state);
-         this.items = NonNullList.withSize(5, ItemStack.EMPTY);
-@@ -102,9 +144,14 @@
-         blockEntity.tickedGameTime = world.getGameTime();
-         if (!blockEntity.isOnCooldown()) {
-             blockEntity.setCooldown(0);
--            HopperBlockEntity.tryMoveItems(world, pos, state, blockEntity, () -> {
-+            // Spigot start
-+            boolean result = HopperBlockEntity.tryMoveItems(world, pos, state, blockEntity, () -> {
-                 return HopperBlockEntity.suckInItems(world, blockEntity);
-             });
-+            if (!result && blockEntity.level.spigotConfig.hopperCheck > 1) {
-+                blockEntity.setCooldown(blockEntity.level.spigotConfig.hopperCheck);
-+            }
-+            // Spigot end
-         }
- 
-     }
-@@ -125,7 +172,7 @@
-                 }
- 
-                 if (flag) {
--                    blockEntity.setCooldown(8);
-+                    blockEntity.setCooldown(world.spigotConfig.hopperTransfer); // Spigot
-                     setChanged(world, pos, state);
-                     return true;
-                 }
-@@ -167,15 +214,41 @@
- 
-                     if (!itemstack.isEmpty()) {
-                         int j = itemstack.getCount();
--                        ItemStack itemstack1 = HopperBlockEntity.addItem(blockEntity, iinventory, blockEntity.removeItem(i, 1), enumdirection);
-+                        // CraftBukkit start - Call event when pushing items into other inventories
-+                        ItemStack original = itemstack.copy();
-+                        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, original);
-+                            blockEntity.setCooldown(world.spigotConfig.hopperTransfer); // Delay hopper checks // Spigot
-+                            return false;
-+                        }
-+                        int origCount = event.getItem().getAmount(); // Spigot
-+                        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) {
-+                        // Spigot start
-+                        itemstack.shrink(origCount - itemstack1.getCount());
-+                        if (j <= world.spigotConfig.hopperAmount) {
-+                            // Spigot end
-                             blockEntity.setItem(i, itemstack);
-                         }
-                     }
-@@ -249,7 +322,7 @@
-             for (int j = 0; j < i; ++j) {
-                 int k = aint[j];
- 
--                if (HopperBlockEntity.tryTakeInItemFromSlot(hopper, iinventory, k, enumdirection)) {
-+                if (HopperBlockEntity.tryTakeInItemFromSlot(hopper, iinventory, k, enumdirection, world)) { // Spigot
-                     return true;
-                 }
-             }
-@@ -274,21 +347,52 @@
-         }
-     }
- 
--    private static boolean tryTakeInItemFromSlot(Hopper hopper, Container inventory, int slot, Direction side) {
--        ItemStack itemstack = inventory.getItem(slot);
-+    private static boolean tryTakeInItemFromSlot(Hopper ihopper, Container iinventory, int i, Direction enumdirection, Level world) { // Spigot
-+        ItemStack itemstack = iinventory.getItem(i);
- 
--        if (!itemstack.isEmpty() && HopperBlockEntity.canTakeItemFromContainer(hopper, inventory, itemstack, slot, side)) {
-+        if (!itemstack.isEmpty() && HopperBlockEntity.canTakeItemFromContainer(ihopper, iinventory, itemstack, i, enumdirection)) {
-             int j = itemstack.getCount();
--            ItemStack itemstack1 = HopperBlockEntity.addItem(inventory, hopper, inventory.removeItem(slot, 1), (Direction) null);
-+            // CraftBukkit start - Call event on collection of items from inventories into the hopper
-+            ItemStack original = itemstack.copy();
-+            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, original);
-+
-+                if (ihopper instanceof HopperBlockEntity) {
-+                    ((HopperBlockEntity) ihopper).setCooldown(world.spigotConfig.hopperTransfer); // Spigot
-+                }
-+
-+                return false;
-+            }
-+            int origCount = event.getItem().getAmount(); // Spigot
-+            ItemStack itemstack1 = HopperBlockEntity.addItem(iinventory, ihopper, CraftItemStack.asNMSCopy(event.getItem()), null);
-+            // CraftBukkit end
-+
-             if (itemstack1.isEmpty()) {
--                inventory.setChanged();
-+                iinventory.setChanged();
-                 return true;
-             }
- 
-             itemstack.setCount(j);
--            if (j == 1) {
--                inventory.setItem(slot, itemstack);
-+            // Spigot start
-+            itemstack.shrink(origCount - itemstack1.getCount());
-+            if (j <= world.spigotConfig.hopperAmount) {
-+                // Spigot end
-+                iinventory.setItem(i, itemstack);
-             }
-         }
- 
-@@ -297,13 +401,20 @@
- 
-     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());
-+        itemEntity.level().getCraftServer().getPluginManager().callEvent(event);
-+        if (event.isCancelled()) {
-+            return false;
-+        }
-+        // CraftBukkit end
-         ItemStack itemstack = itemEntity.getItem().copy();
-         ItemStack itemstack1 = HopperBlockEntity.addItem((Container) null, inventory, itemstack, (Direction) null);
- 
-         if (itemstack1.isEmpty()) {
-             flag = true;
-             itemEntity.setItem(ItemStack.EMPTY);
--            itemEntity.discard();
-+            itemEntity.discard(EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
-         } else {
-             itemEntity.setItem(itemstack1);
-         }
-@@ -383,11 +494,18 @@
-             boolean flag1 = to.isEmpty();
- 
-             if (itemstack1.isEmpty()) {
-+                // Spigot start - SPIGOT-6693, InventorySubcontainer#setItem
-+                ItemStack leftover = ItemStack.EMPTY; // Paper - Make hoppers respect inventory max stack size
-+                if (!stack.isEmpty() && stack.getCount() > to.getMaxStackSize()) {
-+                    leftover = stack; // Paper - Make hoppers respect inventory max stack size
-+                    stack = stack.split(to.getMaxStackSize());
-+                }
-+                // Spigot end
-                 to.setItem(slot, stack);
--                stack = ItemStack.EMPTY;
-+                stack = leftover; // Paper - Make hoppers respect inventory max stack size
-                 flag = true;
-             } else if (HopperBlockEntity.canMergeItems(itemstack1, stack)) {
--                int j = stack.getMaxStackSize() - itemstack1.getCount();
-+                int j = Math.min(stack.getMaxStackSize(), to.getMaxStackSize()) - itemstack1.getCount(); // Paper - Make hoppers respect inventory max stack size
-                 int k = Math.min(stack.getCount(), j);
- 
-                 stack.shrink(k);
-@@ -410,7 +528,7 @@
-                             }
-                         }
- 
--                        tileentityhopper.setCooldown(8 - b0);
-+                        tileentityhopper.setCooldown(tileentityhopper.level.spigotConfig.hopperTransfer - b0); // Spigot
-                     }
-                 }
- 
-@@ -421,14 +539,38 @@
-         return stack;
-     }
- 
-+    // CraftBukkit start
-     @Nullable
-+    private static Container runHopperInventorySearchEvent(Container inventory, CraftBlock hopper, CraftBlock searchLocation, HopperInventorySearchEvent.ContainerType containerType) {
-+        HopperInventorySearchEvent event = new HopperInventorySearchEvent((inventory != null) ? new CraftInventory(inventory) : null, containerType, hopper, searchLocation);
-+        Bukkit.getServer().getPluginManager().callEvent(event);
-+        CraftInventory craftInventory = (CraftInventory) event.getInventory();
-+        return (craftInventory != null) ? craftInventory.getInventory() : null;
-+    }
-+    // CraftBukkit end
-+
-+    @Nullable
-     private static Container getAttachedContainer(Level world, BlockPos pos, HopperBlockEntity blockEntity) {
--        return HopperBlockEntity.getContainerAt(world, pos.relative(blockEntity.facing));
-+        // CraftBukkit start
-+        BlockPos searchPosition = pos.relative(blockEntity.facing);
-+        Container inventory = HopperBlockEntity.getContainerAt(world, searchPosition);
-+
-+        CraftBlock hopper = CraftBlock.at(world, pos);
-+        CraftBlock searchBlock = CraftBlock.at(world, searchPosition);
-+        return HopperBlockEntity.runHopperInventorySearchEvent(inventory, hopper, searchBlock, HopperInventorySearchEvent.ContainerType.DESTINATION);
-+        // CraftBukkit end
-     }
- 
-     @Nullable
-     private static Container getSourceContainer(Level world, Hopper hopper, BlockPos pos, BlockState state) {
--        return HopperBlockEntity.getContainerAt(world, pos, state, hopper.getLevelX(), hopper.getLevelY() + 1.0D, hopper.getLevelZ());
-+        // CraftBukkit start
-+        Container inventory = HopperBlockEntity.getContainerAt(world, pos, state, hopper.getLevelX(), hopper.getLevelY() + 1.0D, hopper.getLevelZ());
-+
-+        BlockPos blockPosition = BlockPos.containing(hopper.getLevelX(), hopper.getLevelY(), hopper.getLevelZ());
-+        CraftBlock hopper1 = CraftBlock.at(world, blockPosition);
-+        CraftBlock container = CraftBlock.at(world, blockPosition.above());
-+        return HopperBlockEntity.runHopperInventorySearchEvent(inventory, hopper1, container, HopperInventorySearchEvent.ContainerType.SOURCE);
-+        // CraftBukkit end
-     }
- 
-     public static List<ItemEntity> getItemsAtAndAbove(Level world, Hopper hopper) {
-@@ -455,6 +597,7 @@
- 
-     @Nullable
-     private static Container getBlockContainer(Level world, BlockPos pos, BlockState state) {
-+        if ( !world.spigotConfig.hopperCanLoadChunks && !world.hasChunkAt( pos ) ) return null; // Spigot
-         Block block = state.getBlock();
- 
-         if (block instanceof WorldlyContainerHolder) {
-@@ -543,7 +686,7 @@
-     }
- 
-     @Override
--    protected AbstractContainerMenu createMenu(int syncId, Inventory playerInventory) {
-+    protected AbstractContainerMenu createMenu(int syncId, net.minecraft.world.entity.player.Inventory playerInventory) {
-         return new HopperMenu(syncId, playerInventory, this);
-     }
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java.patch
deleted file mode 100644
index 49d4011082..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java.patch
+++ /dev/null
@@ -1,29 +0,0 @@
---- a/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java
-+++ b/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java
-@@ -37,8 +37,18 @@
-         this.catalystListener = new SculkCatalystBlockEntity.CatalystListener(state, new BlockPositionSource(pos));
-     }
- 
-+    // Paper start - Fix NPE in SculkBloomEvent world access
-+    @Override
-+    public void setLevel(Level level) {
-+        super.setLevel(level);
-+        this.catalystListener.sculkSpreader.level = level;
-+    }
-+    // Paper end - Fix NPE in SculkBloomEvent world access
-+
-     public static void serverTick(Level world, BlockPos pos, BlockState state, SculkCatalystBlockEntity blockEntity) {
-+        org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = blockEntity.getBlockPos(); // CraftBukkit - SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep.
-         blockEntity.catalystListener.getSculkSpreader().updateCursors(world, pos, world.getRandom(), true);
-+        org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = null; // CraftBukkit
-     }
- 
-     @Override
-@@ -69,6 +79,7 @@
-             this.blockState = state;
-             this.positionSource = positionSource;
-             this.sculkSpreader = SculkSpreader.createLevelSpreader();
-+            // this.sculkSpreader.level = this.level; // CraftBukkit // Paper - Fix NPE in SculkBloomEvent world access
-         }
- 
-         @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SignBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SignBlockEntity.java.patch
deleted file mode 100644
index 788235f327..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SignBlockEntity.java.patch
+++ /dev/null
@@ -1,269 +0,0 @@
---- a/net/minecraft/world/level/block/entity/SignBlockEntity.java
-+++ b/net/minecraft/world/level/block/entity/SignBlockEntity.java
-@@ -22,12 +22,12 @@
- import net.minecraft.network.chat.Style;
- import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
- import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
- import net.minecraft.server.network.FilteredText;
- import net.minecraft.sounds.SoundEvent;
- import net.minecraft.sounds.SoundEvents;
- import net.minecraft.util.Mth;
- import net.minecraft.world.entity.Entity;
--import net.minecraft.world.entity.player.Player;
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.block.Block;
- import net.minecraft.world.level.block.SignBlock;
-@@ -35,6 +35,12 @@
- import net.minecraft.world.phys.Vec2;
- import net.minecraft.world.phys.Vec3;
- import org.slf4j.Logger;
-+import org.bukkit.block.sign.Side;
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.craftbukkit.util.CraftChatMessage;
-+import org.bukkit.entity.Player;
-+import org.bukkit.event.block.SignChangeEvent;
-+// CraftBukkit end
- 
- public class SignBlockEntity extends BlockEntity {
- 
-@@ -61,13 +67,18 @@
-         return new SignText();
-     }
- 
--    public boolean isFacingFrontText(Player player) {
-+    public boolean isFacingFrontText(net.minecraft.world.entity.player.Player player) {
-+        // Paper start - More Sign Block API
-+        return this.isFacingFrontText(player.getX(), player.getZ());
-+    }
-+    public boolean isFacingFrontText(double x, double z) {
-+        // Paper end - More Sign Block API
-         Block block = this.getBlockState().getBlock();
- 
-         if (block instanceof SignBlock blocksign) {
-             Vec3 vec3d = blocksign.getSignHitboxCenterPosition(this.getBlockState());
--            double d0 = player.getX() - ((double) this.getBlockPos().getX() + vec3d.x);
--            double d1 = player.getZ() - ((double) this.getBlockPos().getZ() + vec3d.z);
-+            double d0 = x - ((double) this.getBlockPos().getX() + vec3d.x); // Paper - More Sign Block API
-+            double d1 = z - ((double) this.getBlockPos().getZ() + vec3d.z); // Paper - More Sign Block API
-             float f = blocksign.getYRotationDegrees(this.getBlockState());
-             float f1 = (float) (Mth.atan2(d1, d0) * 57.2957763671875D) - 90.0F;
- 
-@@ -101,7 +112,7 @@
-     protected void saveAdditional(CompoundTag nbt, HolderLookup.Provider registries) {
-         super.saveAdditional(nbt, registries);
-         DynamicOps<Tag> dynamicops = registries.createSerializationContext(NbtOps.INSTANCE);
--        DataResult dataresult = SignText.DIRECT_CODEC.encodeStart(dynamicops, this.frontText);
-+        DataResult<Tag> dataresult = SignText.DIRECT_CODEC.encodeStart(dynamicops, this.frontText); // CraftBukkit - decompile error
-         Logger logger = SignBlockEntity.LOGGER;
- 
-         Objects.requireNonNull(logger);
-@@ -121,7 +132,7 @@
-     protected void loadAdditional(CompoundTag nbt, HolderLookup.Provider registries) {
-         super.loadAdditional(nbt, registries);
-         DynamicOps<Tag> dynamicops = registries.createSerializationContext(NbtOps.INSTANCE);
--        DataResult dataresult;
-+        DataResult<SignText> dataresult; // CraftBukkit - decompile error
-         Logger logger;
- 
-         if (nbt.contains("front_text")) {
-@@ -161,7 +172,7 @@
- 
-         if (world instanceof ServerLevel worldserver) {
-             try {
--                return ComponentUtils.updateForEntity(SignBlockEntity.createCommandSourceStack((Player) null, worldserver, this.worldPosition), text, (Entity) null, 0);
-+                return ComponentUtils.updateForEntity(this.createCommandSourceStack((net.minecraft.world.entity.player.Player) null, worldserver, this.worldPosition), text, (Entity) null, 0);
-             } catch (CommandSyntaxException commandsyntaxexception) {
-                 ;
-             }
-@@ -170,15 +181,17 @@
-         return text;
-     }
- 
--    public void updateSignText(Player player, boolean front, List<FilteredText> messages) {
-+    public void updateSignText(net.minecraft.world.entity.player.Player player, boolean front, List<FilteredText> messages) {
-         if (!this.isWaxed() && player.getUUID().equals(this.getPlayerWhoMayEdit()) && this.level != null) {
-             this.updateText((signtext) -> {
--                return this.setMessages(player, messages, signtext);
-+                return this.setMessages(player, messages, signtext, front); // CraftBukkit
-             }, front);
-             this.setAllowedPlayerEditor((UUID) null);
-             this.level.sendBlockUpdated(this.getBlockPos(), this.getBlockState(), this.getBlockState(), 3);
-         } else {
-             SignBlockEntity.LOGGER.warn("Player {} just tried to change non-editable sign", player.getName().getString());
-+            if (player.distanceToSqr(this.getBlockPos().getX(), this.getBlockPos().getY(), this.getBlockPos().getZ()) < 32 * 32) // Paper - Dont send far away sign update
-+            ((ServerPlayer) player).connection.send(this.getUpdatePacket()); // CraftBukkit
-         }
-     }
- 
-@@ -188,19 +201,43 @@
-         return this.setText((SignText) textChanger.apply(signtext), front);
-     }
- 
--    private SignText setMessages(Player player, List<FilteredText> messages, SignText text) {
--        for (int i = 0; i < messages.size(); ++i) {
--            FilteredText filteredtext = (FilteredText) messages.get(i);
--            Style chatmodifier = text.getMessage(i, player.isTextFilteringEnabled()).getStyle();
-+    private SignText setMessages(net.minecraft.world.entity.player.Player entityhuman, List<FilteredText> list, SignText signtext, boolean front) { // CraftBukkit
-+        SignText originalText = signtext; // CraftBukkit
-+        for (int i = 0; i < list.size(); ++i) {
-+            FilteredText filteredtext = (FilteredText) list.get(i);
-+            Style chatmodifier = signtext.getMessage(i, entityhuman.isTextFilteringEnabled()).getStyle();
- 
--            if (player.isTextFilteringEnabled()) {
--                text = text.setMessage(i, Component.literal(filteredtext.filteredOrEmpty()).setStyle(chatmodifier));
-+            if (entityhuman.isTextFilteringEnabled()) {
-+                signtext = signtext.setMessage(i, Component.literal(net.minecraft.util.StringUtil.filterText(filteredtext.filteredOrEmpty())).setStyle(chatmodifier)); // Paper - filter sign text to chat only
-             } else {
--                text = text.setMessage(i, Component.literal(filteredtext.raw()).setStyle(chatmodifier), Component.literal(filteredtext.filteredOrEmpty()).setStyle(chatmodifier));
-+                signtext = signtext.setMessage(i, Component.literal(net.minecraft.util.StringUtil.filterText(filteredtext.raw())).setStyle(chatmodifier), Component.literal(net.minecraft.util.StringUtil.filterText(filteredtext.filteredOrEmpty())).setStyle(chatmodifier)); // Paper - filter sign text to chat only
-             }
-         }
- 
--        return text;
-+        // CraftBukkit start
-+        Player player = ((ServerPlayer) entityhuman).getBukkitEntity();
-+        List<net.kyori.adventure.text.Component> lines = new java.util.ArrayList<>(); // Paper - adventure
-+
-+        for (int i = 0; i < list.size(); ++i) {
-+            lines.add(io.papermc.paper.adventure.PaperAdventure.asAdventure(signtext.getMessage(i, entityhuman.isTextFilteringEnabled()))); // Paper - Adventure
-+        }
-+
-+        SignChangeEvent event = new SignChangeEvent(CraftBlock.at(this.level, this.worldPosition), player, new java.util.ArrayList<>(lines), (front) ? Side.FRONT : Side.BACK); // Paper - Adventure
-+        entityhuman.level().getCraftServer().getPluginManager().callEvent(event);
-+
-+        if (event.isCancelled()) {
-+            return originalText;
-+        }
-+
-+        Component[] components = org.bukkit.craftbukkit.block.CraftSign.sanitizeLines(event.lines()); // Paper - Adventure
-+        for (int i = 0; i < components.length; i++) {
-+            if (!Objects.equals(lines.get(i), event.line(i))) { // Paper - Adventure
-+                signtext = signtext.setMessage(i, components[i]);
-+            }
-+        }
-+        // CraftBukkit end
-+
-+        return signtext;
-     }
- 
-     public boolean setText(SignText text, boolean front) {
-@@ -227,11 +264,11 @@
-         }
-     }
- 
--    public boolean canExecuteClickCommands(boolean front, Player player) {
-+    public boolean canExecuteClickCommands(boolean front, net.minecraft.world.entity.player.Player player) {
-         return this.isWaxed() && this.getText(front).hasAnyClickCommands(player);
-     }
- 
--    public boolean executeClickCommandsIfPresent(Player player, Level world, BlockPos pos, boolean front) {
-+    public boolean executeClickCommandsIfPresent(net.minecraft.world.entity.player.Player player, Level world, BlockPos pos, boolean front) {
-         boolean flag1 = false;
-         Component[] aichatbasecomponent = this.getText(front).getMessages(player.isTextFilteringEnabled());
-         int i = aichatbasecomponent.length;
-@@ -242,7 +279,17 @@
-             ClickEvent chatclickable = chatmodifier.getClickEvent();
- 
-             if (chatclickable != null && chatclickable.getAction() == ClickEvent.Action.RUN_COMMAND) {
--                player.getServer().getCommands().performPrefixedCommand(SignBlockEntity.createCommandSourceStack(player, world, pos), chatclickable.getValue());
-+                // Paper start - Fix commands from signs not firing command events
-+                String command = chatclickable.getValue().startsWith("/") ? chatclickable.getValue() : "/" + chatclickable.getValue();
-+                if (org.spigotmc.SpigotConfig.logCommands)  {
-+                    LOGGER.info("{} issued server command: {}", player.getScoreboardName(), command);
-+                }
-+                io.papermc.paper.event.player.PlayerSignCommandPreprocessEvent event = new io.papermc.paper.event.player.PlayerSignCommandPreprocessEvent((org.bukkit.entity.Player) player.getBukkitEntity(), command, new org.bukkit.craftbukkit.util.LazyPlayerSet(player.getServer()), (org.bukkit.block.Sign) CraftBlock.at(this.level, this.worldPosition).getState(), front ? Side.FRONT : Side.BACK);
-+                if (!event.callEvent()) {
-+                    return false;
-+                }
-+                player.getServer().getCommands().performPrefixedCommand(this.createCommandSourceStack(((org.bukkit.craftbukkit.entity.CraftPlayer) event.getPlayer()).getHandle(), world, pos), event.getMessage());
-+                // Paper end - Fix commands from signs not firing command events
-                 flag1 = true;
-             }
-         }
-@@ -250,11 +297,55 @@
-         return flag1;
-     }
- 
--    private static CommandSourceStack createCommandSourceStack(@Nullable Player player, Level world, BlockPos pos) {
-+    // CraftBukkit start
-+    private final CommandSource commandSource = new CommandSource() {
-+
-+        @Override
-+        public void sendSystemMessage(Component message) {}
-+
-+        @Override
-+        public org.bukkit.command.CommandSender getBukkitSender(CommandSourceStack wrapper) {
-+            return wrapper.getEntity() != null ? wrapper.getEntity().getBukkitEntity() : new org.bukkit.craftbukkit.command.CraftBlockCommandSender(wrapper, SignBlockEntity.this);
-+        }
-+
-+        @Override
-+        public boolean acceptsSuccess() {
-+            return false;
-+        }
-+
-+        @Override
-+        public boolean acceptsFailure() {
-+            return false;
-+        }
-+
-+        @Override
-+        public boolean shouldInformAdmins() {
-+            return false;
-+        }
-+    };
-+
-+    private CommandSourceStack createCommandSourceStack(@Nullable net.minecraft.world.entity.player.Player player, Level world, BlockPos pos) {
-+        // CraftBukkit end
-         String s = player == null ? "Sign" : player.getName().getString();
-         Object object = player == null ? Component.literal("Sign") : player.getDisplayName();
- 
--        return new CommandSourceStack(CommandSource.NULL, Vec3.atCenterOf(pos), Vec2.ZERO, (ServerLevel) world, 2, s, (Component) object, world.getServer(), player);
-+        // Paper start - Fix commands from signs not firing command events
-+        CommandSource commandSource = this.level.paperConfig().misc.showSignClickCommandFailureMsgsToPlayer ? new io.papermc.paper.commands.DelegatingCommandSource(this.commandSource) {
-+            @Override
-+            public void sendSystemMessage(Component message) {
-+                if (player instanceof final ServerPlayer serverPlayer) {
-+                    serverPlayer.sendSystemMessage(message);
-+                }
-+            }
-+
-+            @Override
-+            public boolean acceptsFailure() {
-+                return true;
-+            }
-+        } : this.commandSource;
-+        // Paper end - Fix commands from signs not firing command events
-+        // CraftBukkit - this
-+        return new CommandSourceStack(commandSource, Vec3.atCenterOf(pos), Vec2.ZERO, (ServerLevel) world, 2, s, (Component) object, world.getServer(), player); // Paper - Fix commands from signs not firing command events
-     }
- 
-     @Override
-@@ -273,12 +364,17 @@
- 
-     @Nullable
-     public UUID getPlayerWhoMayEdit() {
-+        // CraftBukkit start - unnecessary sign ticking removed, so do this lazily
-+        if (this.level != null && this.playerWhoMayEdit != null) {
-+            this.clearInvalidPlayerWhoMayEdit(this, this.level, this.playerWhoMayEdit);
-+        }
-+        // CraftBukkit end
-         return this.playerWhoMayEdit;
-     }
- 
-     private void markUpdated() {
-         this.setChanged();
--        this.level.sendBlockUpdated(this.getBlockPos(), this.getBlockState(), this.getBlockState(), 3);
-+        if (this.level != null) this.level.sendBlockUpdated(this.getBlockPos(), this.getBlockState(), this.getBlockState(), 3); // CraftBukkit - skip notify if world is null (SPIGOT-5122)
-     }
- 
-     public boolean isWaxed() {
-@@ -296,7 +392,7 @@
-     }
- 
-     public boolean playerIsTooFarAwayToEdit(UUID uuid) {
--        Player entityhuman = this.level.getPlayerByUUID(uuid);
-+        net.minecraft.world.entity.player.Player entityhuman = this.level.getPlayerByUUID(uuid);
- 
-         return entityhuman == null || !entityhuman.canInteractWithBlock(this.getBlockPos(), 4.0D);
-     }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java.patch
deleted file mode 100644
index b5587ac5d9..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java.patch
+++ /dev/null
@@ -1,19 +0,0 @@
---- a/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java
-+++ b/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java
-@@ -21,6 +21,7 @@
- import net.minecraft.world.level.block.Blocks;
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.chunk.LevelChunk;
-+import net.minecraft.world.level.dimension.LevelStem;
- import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
- import net.minecraft.world.level.levelgen.feature.Feature;
- import net.minecraft.world.level.levelgen.feature.configurations.EndGatewayConfiguration;
-@@ -143,7 +144,7 @@
-     public Vec3 getPortalPosition(ServerLevel world, BlockPos pos) {
-         BlockPos blockposition1;
- 
--        if (this.exitPortal == null && world.dimension() == Level.END) {
-+        if (this.exitPortal == null && world.getTypeKey() == LevelStem.END) { // CraftBukkit - work in alternate worlds
-             blockposition1 = TheEndGatewayBlockEntity.findOrCreateValidTeleportPos(world, pos);
-             blockposition1 = blockposition1.above(10);
-             TheEndGatewayBlockEntity.LOGGER.debug("Creating portal at {}", blockposition1);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/trialspawner/TrialSpawner.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/trialspawner/TrialSpawner.java.patch
deleted file mode 100644
index e58a4bb1dc..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/trialspawner/TrialSpawner.java.patch
+++ /dev/null
@@ -1,63 +0,0 @@
---- a/net/minecraft/world/level/block/entity/trialspawner/TrialSpawner.java
-+++ b/net/minecraft/world/level/block/entity/trialspawner/TrialSpawner.java
-@@ -46,6 +46,11 @@
- import net.minecraft.world.phys.HitResult;
- import net.minecraft.world.phys.Vec3;
- import net.minecraft.world.phys.shapes.CollisionContext;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.event.block.BlockDispenseLootEvent;
-+// CraftBukkit end
- 
- public final class TrialSpawner {
- 
-@@ -219,14 +224,21 @@
-                                 }
- 
-                                 entityinsentient.setPersistenceRequired();
--                                Optional optional1 = mobspawnerdata.getEquipment();
-+                                Optional<net.minecraft.world.entity.EquipmentTable> optional1 = mobspawnerdata.getEquipment(); // CraftBukkit - decompile error
- 
-                                 Objects.requireNonNull(entityinsentient);
-                                 optional1.ifPresent(entityinsentient::equip);
-                             }
- 
--                            if (!world.tryAddFreshEntityWithPassengers(entity)) {
-+                            entity.spawnedViaMobSpawner = true; // Paper
-+                            entity.spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.TRIAL_SPAWNER; // Paper - Entity#getEntitySpawnReason
-+                            // CraftBukkit start
-+                            if (org.bukkit.craftbukkit.event.CraftEventFactory.callTrialSpawnerSpawnEvent(entity, pos).isCancelled()) {
-                                 return Optional.empty();
-+                            }
-+                            if (!world.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.TRIAL_SPAWNER)) {
-+                                // CraftBukkit end
-+                                return Optional.empty();
-                             } else {
-                                 TrialSpawner.FlameParticle trialspawner_a = this.isOminous ? TrialSpawner.FlameParticle.OMINOUS : TrialSpawner.FlameParticle.NORMAL;
- 
-@@ -248,6 +260,15 @@
-         ObjectArrayList<ItemStack> objectarraylist = loottable.getRandomItems(lootparams);
- 
-         if (!objectarraylist.isEmpty()) {
-+            // CraftBukkit start
-+            BlockDispenseLootEvent spawnerDispenseLootEvent = CraftEventFactory.callBlockDispenseLootEvent(world, pos, null, objectarraylist);
-+            if (spawnerDispenseLootEvent.isCancelled()) {
-+                return;
-+            }
-+
-+            objectarraylist = new ObjectArrayList<>(spawnerDispenseLootEvent.getDispensedLoot().stream().map(CraftItemStack::asNMSCopy).toList());
-+            // CraftBukkit end
-+
-             ObjectListIterator objectlistiterator = objectarraylist.iterator();
- 
-             while (objectlistiterator.hasNext()) {
-@@ -370,7 +391,7 @@
-     }
- 
-     public void overrideEntityToSpawn(EntityType<?> entityType, Level world) {
--        this.data.reset();
-+        this.data.reset(this); // Paper
-         this.normalConfig = Holder.direct(((TrialSpawnerConfig) this.normalConfig.value()).withSpawning(entityType));
-         this.ominousConfig = Holder.direct(((TrialSpawnerConfig) this.ominousConfig.value()).withSpawning(entityType));
-         this.setState(world, TrialSpawnerState.INACTIVE);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerData.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerData.java.patch
deleted file mode 100644
index 9f48f83f27..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerData.java.patch
+++ /dev/null
@@ -1,32 +0,0 @@
---- a/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerData.java
-+++ b/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerData.java
-@@ -100,9 +100,9 @@
-         this.ejectingLootTable = rewardLootTable;
-     }
- 
--    public void reset() {
-+    public void reset(TrialSpawner logic) { // Paper - Fix TrialSpawner forgets assigned mob; MC-273635
-         this.currentMobs.clear();
--        this.nextSpawnData = Optional.empty();
-+        if (!logic.getConfig().spawnPotentialsDefinition().isEmpty()) this.nextSpawnData = Optional.empty(); // Paper - Fix TrialSpawner forgets assigned mob; MC-273635
-         this.resetStatistics();
-     }
- 
-@@ -210,7 +210,7 @@
-     }
- 
-     public void resetAfterBecomingOminous(TrialSpawner logic, ServerLevel world) {
--        Stream stream = this.currentMobs.stream();
-+        Stream<UUID> stream = this.currentMobs.stream(); // CraftBukkit - decompile error
- 
-         Objects.requireNonNull(world);
-         stream.map(world::getEntity).forEach((entity) -> {
-@@ -222,7 +222,7 @@
-                     entityinsentient.dropPreservedEquipment(world);
-                 }
- 
--                entity.remove(Entity.RemovalReason.DISCARDED);
-+                entity.remove(Entity.RemovalReason.DISCARDED, org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - Add bukkit remove cause;
-             }
-         });
-         if (!logic.getOminousConfig().spawnPotentialsDefinition().isEmpty()) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerState.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerState.java.patch
deleted file mode 100644
index 293d3ea411..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerState.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerState.java
-+++ b/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerState.java
-@@ -145,7 +145,7 @@
-                     yield ACTIVE;
-                 } else if (trialSpawnerData.isCooldownFinished(world)) {
-                     logic.removeOminous(world, pos);
--                    trialSpawnerData.reset();
-+                    trialSpawnerData.reset(logic); // Paper - Fix TrialSpawner forgets assigned mob; MC-273635
-                     yield WAITING_FOR_PLAYERS;
-                 } else {
-                     yield this;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/vault/VaultBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/vault/VaultBlockEntity.java.patch
deleted file mode 100644
index 928e901a76..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/vault/VaultBlockEntity.java.patch
+++ /dev/null
@@ -1,83 +0,0 @@
---- a/net/minecraft/world/level/block/entity/vault/VaultBlockEntity.java
-+++ b/net/minecraft/world/level/block/entity/vault/VaultBlockEntity.java
-@@ -46,6 +46,13 @@
- import net.minecraft.world.phys.Vec3;
- import org.slf4j.Logger;
- 
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.event.block.BlockDispenseLootEvent;
-+import org.bukkit.event.block.VaultDisplayItemEvent;
-+// CraftBukkit end
-+
- public class VaultBlockEntity extends BlockEntity {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
-@@ -96,18 +103,18 @@
-             dataresult = VaultServerData.CODEC.parse(dynamicops, nbt.get("server_data"));
-             logger = VaultBlockEntity.LOGGER;
-             Objects.requireNonNull(logger);
--            optional = dataresult.resultOrPartial(logger::error);
-+            optional = ((DataResult<VaultServerData>) dataresult).resultOrPartial(logger::error); // CraftBukkit - decompile error
-             VaultServerData vaultserverdata = this.serverData;
- 
-             Objects.requireNonNull(this.serverData);
--            optional.ifPresent(vaultserverdata::set);
-+            ((Optional<VaultServerData>) optional).ifPresent(vaultserverdata::set); // CraftBukkit - decompile error
-         }
- 
-         if (nbt.contains("config")) {
-             dataresult = VaultConfig.CODEC.parse(dynamicops, nbt.get("config"));
-             logger = VaultBlockEntity.LOGGER;
-             Objects.requireNonNull(logger);
--            dataresult.resultOrPartial(logger::error).ifPresent((vaultconfig) -> {
-+            ((DataResult<VaultConfig>) dataresult).resultOrPartial(logger::error).ifPresent((vaultconfig) -> { // CraftBukkit - decompile error
-                 this.config = vaultconfig;
-             });
-         }
-@@ -116,11 +123,11 @@
-             dataresult = VaultSharedData.CODEC.parse(dynamicops, nbt.get("shared_data"));
-             logger = VaultBlockEntity.LOGGER;
-             Objects.requireNonNull(logger);
--            optional = dataresult.resultOrPartial(logger::error);
-+            optional = ((DataResult<VaultSharedData>) dataresult).resultOrPartial(logger::error); // CraftBukkit - decompile error
-             VaultSharedData vaultshareddata = this.sharedData;
- 
-             Objects.requireNonNull(this.sharedData);
--            optional.ifPresent(vaultshareddata::set);
-+            ((Optional<VaultSharedData>) optional).ifPresent(vaultshareddata::set); // CraftBukkit - decompile error
-         }
- 
-     }
-@@ -320,6 +327,14 @@
-                     if (!list.isEmpty()) {
-                         player.awardStat(Stats.ITEM_USED.get(stack.getItem()));
-                         stack.consume(config.keyItem().getCount(), player);
-+                        // CraftBukkit start
-+                        BlockDispenseLootEvent vaultDispenseLootEvent = CraftEventFactory.callBlockDispenseLootEvent(world, pos, player, list);
-+                        if (vaultDispenseLootEvent.isCancelled()) {
-+                            return;
-+                        }
-+
-+                        list = vaultDispenseLootEvent.getDispensedLoot().stream().map(CraftItemStack::asNMSCopy).toList();
-+                        // CraftBukkit end
-                         Server.unlock(world, state, pos, config, serverData, sharedData, list);
-                         serverData.addToRewardedPlayers(player);
-                         sharedData.updateConnectedPlayersWithinRange(world, pos, serverData, config, config.deactivationRange());
-@@ -341,7 +356,15 @@
-                 sharedData.setDisplayItem(ItemStack.EMPTY);
-             } else {
-                 ItemStack itemstack = Server.getRandomDisplayItemFromLootTable(world, pos, (ResourceKey) config.overrideLootTableToDisplay().orElse(config.lootTable()));
-+                // CraftBukkit start
-+                VaultDisplayItemEvent event = CraftEventFactory.callVaultDisplayItemEvent(world, pos, itemstack);
-+                if (event.isCancelled()) {
-+                    return;
-+                }
- 
-+                itemstack = CraftItemStack.asNMSCopy(event.getDisplayItem());
-+                // CraftBukkit end
-+
-                 sharedData.setDisplayItem(itemstack);
-             }
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/grower/TreeGrower.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/grower/TreeGrower.java.patch
deleted file mode 100644
index 859e74b185..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/grower/TreeGrower.java.patch
+++ /dev/null
@@ -1,126 +0,0 @@
---- a/net/minecraft/world/level/block/grower/TreeGrower.java
-+++ b/net/minecraft/world/level/block/grower/TreeGrower.java
-@@ -20,9 +20,14 @@
- import net.minecraft.world.level.LevelAccessor;
- import net.minecraft.world.level.block.Block;
- import net.minecraft.world.level.block.Blocks;
-+import net.minecraft.world.level.block.SaplingBlock;
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.chunk.ChunkGenerator;
- import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
-+// CraftBukkit start
-+import net.minecraft.data.worldgen.features.TreeFeatures;
-+import org.bukkit.TreeType;
-+// CraftBukkit end
- 
- public final class TreeGrower {
- 
-@@ -75,21 +80,22 @@
-             }
-         }
- 
--        return flowersNearby && this.flowers.isPresent() ? (ResourceKey) this.flowers.get() : (ResourceKey) this.tree.orElse((Object) null);
-+        return flowersNearby && this.flowers.isPresent() ? (ResourceKey) this.flowers.get() : (ResourceKey) this.tree.orElse(null); // CraftBukkit - decompile error
-     }
- 
-     @Nullable
-     private ResourceKey<ConfiguredFeature<?, ?>> getConfiguredMegaFeature(RandomSource random) {
--        return this.secondaryMegaTree.isPresent() && random.nextFloat() < this.secondaryChance ? (ResourceKey) this.secondaryMegaTree.get() : (ResourceKey) this.megaTree.orElse((Object) null);
-+        return this.secondaryMegaTree.isPresent() && random.nextFloat() < this.secondaryChance ? (ResourceKey) this.secondaryMegaTree.get() : (ResourceKey) this.megaTree.orElse(null); // CraftBukkit - decompile error
-     }
- 
-     public boolean growTree(ServerLevel world, ChunkGenerator chunkGenerator, BlockPos pos, BlockState state, RandomSource random) {
-         ResourceKey<ConfiguredFeature<?, ?>> resourcekey = this.getConfiguredMegaFeature(random);
- 
-         if (resourcekey != null) {
--            Holder<ConfiguredFeature<?, ?>> holder = (Holder) world.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE).get(resourcekey).orElse((Object) null);
-+            Holder<ConfiguredFeature<?, ?>> holder = (Holder) world.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE).get(resourcekey).orElse(null); // CraftBukkit - decompile error
- 
-             if (holder != null) {
-+                this.setTreeType(holder); // CraftBukkit
-                 for (int i = 0; i >= -1; --i) {
-                     for (int j = 0; j >= -1; --j) {
-                         if (TreeGrower.isTwoByTwoSapling(state, world, pos, i, j)) {
-@@ -120,11 +126,12 @@
-         if (resourcekey1 == null) {
-             return false;
-         } else {
--            Holder<ConfiguredFeature<?, ?>> holder1 = (Holder) world.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE).get(resourcekey1).orElse((Object) null);
-+            Holder<ConfiguredFeature<?, ?>> holder1 = (Holder) world.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE).get(resourcekey1).orElse(null); // CraftBukkit - decompile error
- 
-             if (holder1 == null) {
-                 return false;
-             } else {
-+                this.setTreeType(holder1); // CraftBukkit
-                 ConfiguredFeature<?, ?> worldgenfeatureconfigured1 = (ConfiguredFeature) holder1.value();
-                 BlockState iblockdata2 = world.getFluidState(pos).createLegacyBlock();
- 
-@@ -165,11 +172,66 @@
-         return true;
-     }
- 
-+    // CraftBukkit start
-+    private void setTreeType(Holder<ConfiguredFeature<?, ?>> holder) {
-+        ResourceKey<ConfiguredFeature<?, ?>> worldgentreeabstract = holder.unwrapKey().get();
-+        if (worldgentreeabstract == TreeFeatures.OAK || worldgentreeabstract == TreeFeatures.OAK_BEES_005) {
-+            SaplingBlock.treeType = TreeType.TREE;
-+        } else if (worldgentreeabstract == TreeFeatures.HUGE_RED_MUSHROOM) {
-+            SaplingBlock.treeType = TreeType.RED_MUSHROOM;
-+        } else if (worldgentreeabstract == TreeFeatures.HUGE_BROWN_MUSHROOM) {
-+            SaplingBlock.treeType = TreeType.BROWN_MUSHROOM;
-+        } else if (worldgentreeabstract == TreeFeatures.JUNGLE_TREE) {
-+            SaplingBlock.treeType = TreeType.COCOA_TREE;
-+        } else if (worldgentreeabstract == TreeFeatures.JUNGLE_TREE_NO_VINE) {
-+            SaplingBlock.treeType = TreeType.SMALL_JUNGLE;
-+        } else if (worldgentreeabstract == TreeFeatures.PINE) {
-+            SaplingBlock.treeType = TreeType.TALL_REDWOOD;
-+        } else if (worldgentreeabstract == TreeFeatures.SPRUCE) {
-+            SaplingBlock.treeType = TreeType.REDWOOD;
-+        } else if (worldgentreeabstract == TreeFeatures.ACACIA) {
-+            SaplingBlock.treeType = TreeType.ACACIA;
-+        } else if (worldgentreeabstract == TreeFeatures.BIRCH || worldgentreeabstract == TreeFeatures.BIRCH_BEES_005) {
-+            SaplingBlock.treeType = TreeType.BIRCH;
-+        } else if (worldgentreeabstract == TreeFeatures.SUPER_BIRCH_BEES_0002) {
-+            SaplingBlock.treeType = TreeType.TALL_BIRCH;
-+        } else if (worldgentreeabstract == TreeFeatures.SWAMP_OAK) {
-+            SaplingBlock.treeType = TreeType.SWAMP;
-+        } else if (worldgentreeabstract == TreeFeatures.FANCY_OAK || worldgentreeabstract == TreeFeatures.FANCY_OAK_BEES_005) {
-+            SaplingBlock.treeType = TreeType.BIG_TREE;
-+        } else if (worldgentreeabstract == TreeFeatures.JUNGLE_BUSH) {
-+            SaplingBlock.treeType = TreeType.JUNGLE_BUSH;
-+        } else if (worldgentreeabstract == TreeFeatures.DARK_OAK) {
-+            SaplingBlock.treeType = TreeType.DARK_OAK;
-+        } else if (worldgentreeabstract == TreeFeatures.MEGA_SPRUCE) {
-+            SaplingBlock.treeType = TreeType.MEGA_REDWOOD;
-+        } else if (worldgentreeabstract == TreeFeatures.MEGA_PINE) {
-+            SaplingBlock.treeType = TreeType.MEGA_PINE;
-+        } else if (worldgentreeabstract == TreeFeatures.MEGA_JUNGLE_TREE) {
-+            SaplingBlock.treeType = TreeType.JUNGLE;
-+        } else if (worldgentreeabstract == TreeFeatures.AZALEA_TREE) {
-+            SaplingBlock.treeType = TreeType.AZALEA;
-+        } else if (worldgentreeabstract == TreeFeatures.MANGROVE) {
-+            SaplingBlock.treeType = TreeType.MANGROVE;
-+        } else if (worldgentreeabstract == TreeFeatures.TALL_MANGROVE) {
-+            SaplingBlock.treeType = TreeType.TALL_MANGROVE;
-+        } else if (worldgentreeabstract == TreeFeatures.CHERRY || worldgentreeabstract == TreeFeatures.CHERRY_BEES_005) {
-+            SaplingBlock.treeType = TreeType.CHERRY;
-+        } else if (worldgentreeabstract == TreeFeatures.PALE_OAK || worldgentreeabstract == TreeFeatures.PALE_OAK_BONEMEAL) {
-+            SaplingBlock.treeType = TreeType.PALE_OAK;
-+        } else if (worldgentreeabstract == TreeFeatures.PALE_OAK_CREAKING) {
-+            SaplingBlock.treeType = TreeType.PALE_OAK_CREAKING;
-+        } else {
-+            throw new IllegalArgumentException("Unknown tree generator " + worldgentreeabstract);
-+        }
-+    }
-+    // CraftBukkit end
-+
-     static {
--        Function function = (worldgentreeprovider) -> {
-+        Function<TreeGrower, String> function = (worldgentreeprovider) -> { // CraftBukkit - decompile error
-             return worldgentreeprovider.name;
-         };
--        Map map = TreeGrower.GROWERS;
-+        Map<String, TreeGrower> map = TreeGrower.GROWERS; // CraftBukkit - decompile error
- 
-         Objects.requireNonNull(map);
-         CODEC = Codec.stringResolver(function, map::get);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/piston/PistonBaseBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/piston/PistonBaseBlock.java.patch
deleted file mode 100644
index d269336ba6..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/piston/PistonBaseBlock.java.patch
+++ /dev/null
@@ -1,180 +0,0 @@
---- a/net/minecraft/world/level/block/piston/PistonBaseBlock.java
-+++ b/net/minecraft/world/level/block/piston/PistonBaseBlock.java
-@@ -44,6 +44,13 @@
- import net.minecraft.world.phys.shapes.CollisionContext;
- import net.minecraft.world.phys.shapes.Shapes;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+// CraftBukkit start
-+import com.google.common.collect.ImmutableList;
-+import java.util.AbstractList;
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.event.block.BlockPistonRetractEvent;
-+import org.bukkit.event.block.BlockPistonExtendEvent;
-+// CraftBukkit end
- 
- public class PistonBaseBlock extends DirectionalBlock {
- 
-@@ -155,6 +162,18 @@
-                 }
-             }
- 
-+            // CraftBukkit start
-+            // if (!this.isSticky) { // Paper - Fix sticky pistons and BlockPistonRetractEvent; Move further down
-+            //     org.bukkit.block.Block block = world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ());
-+            //     BlockPistonRetractEvent event = new BlockPistonRetractEvent(block, ImmutableList.<org.bukkit.block.Block>of(), CraftBlock.notchToBlockFace(enumdirection));
-+            //     world.getCraftServer().getPluginManager().callEvent(event);
-+            //
-+            //     if (event.isCancelled()) {
-+            //         return;
-+            //     }
-+            // }
-+            // PAIL: checkME - what happened to setTypeAndData?
-+            // CraftBukkit end
-             world.blockEvent(pos, this, b0, enumdirection.get3DDataValue());
-         }
- 
-@@ -197,6 +216,12 @@
-     @Override
-     protected boolean triggerEvent(BlockState state, Level world, BlockPos pos, int type, int data) {
-         Direction enumdirection = (Direction) state.getValue(PistonBaseBlock.FACING);
-+        // Paper start - Protect Bedrock and End Portal/Frames from being destroyed; prevent retracting when we're facing the wrong way (we were replaced before retraction could occur)
-+        Direction directionQueuedAs = Direction.from3DDataValue(data & 7); // Paper - copied from below
-+        if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits && enumdirection != directionQueuedAs) {
-+            return false;
-+        }
-+        // Paper end - Protect Bedrock and End Portal/Frames from being destroyed
-         BlockState iblockdata1 = (BlockState) state.setValue(PistonBaseBlock.EXTENDED, true);
- 
-         if (!world.isClientSide) {
-@@ -229,8 +254,15 @@
- 
-             BlockState iblockdata2 = (BlockState) ((BlockState) Blocks.MOVING_PISTON.defaultBlockState().setValue(MovingPistonBlock.FACING, enumdirection)).setValue(MovingPistonBlock.TYPE, this.isSticky ? PistonType.STICKY : PistonType.DEFAULT);
- 
-+            // Paper start - Fix sticky pistons and BlockPistonRetractEvent; Move empty piston retract call to fix multiple event fires
-+            if (!this.isSticky) {
-+                if (!new BlockPistonRetractEvent(CraftBlock.at(world, pos), java.util.Collections.emptyList(), CraftBlock.notchToBlockFace(enumdirection)).callEvent()) {
-+                    return false;
-+                }
-+            }
-+            // Paper end - Fix sticky pistons and BlockPistonRetractEvent
-             world.setBlock(pos, iblockdata2, 20);
--            world.setBlockEntity(MovingPistonBlock.newMovingBlockEntity(pos, iblockdata2, (BlockState) this.defaultBlockState().setValue(PistonBaseBlock.FACING, Direction.from3DDataValue(data & 7)), enumdirection, false, true));
-+            world.setBlockEntity(MovingPistonBlock.newMovingBlockEntity(pos, iblockdata2, (BlockState) this.defaultBlockState().setValue(PistonBaseBlock.FACING, Direction.from3DDataValue(data & 7)), enumdirection, false, true)); // Paper - Protect Bedrock and End Portal/Frames from being destroyed; diff on change
-             world.blockUpdated(pos, iblockdata2.getBlock());
-             iblockdata2.updateNeighbourShapes(world, pos, 2);
-             if (this.isSticky) {
-@@ -255,11 +287,25 @@
-                     if (type == 1 && !iblockdata3.isAir() && PistonBaseBlock.isPushable(iblockdata3, world, blockposition1, enumdirection.getOpposite(), false, enumdirection) && (iblockdata3.getPistonPushReaction() == PushReaction.NORMAL || iblockdata3.is(Blocks.PISTON) || iblockdata3.is(Blocks.STICKY_PISTON))) {
-                         this.moveBlocks(world, pos, enumdirection, false);
-                     } else {
-+                        // Paper start - Fix sticky pistons and BlockPistonRetractEvent; fire BlockPistonRetractEvent for sticky pistons retracting nothing (air)
-+                        if (type == TRIGGER_CONTRACT && iblockdata2.isAir()) {
-+                            if (!new BlockPistonRetractEvent(CraftBlock.at(world, pos), java.util.Collections.emptyList(), CraftBlock.notchToBlockFace(enumdirection)).callEvent()) {
-+                                return false;
-+                            }
-+                        }
-+                        // Paper end - Fix sticky pistons and BlockPistonRetractEvent
-                         world.removeBlock(pos.relative(enumdirection), false);
-                     }
-                 }
-             } else {
--                world.removeBlock(pos.relative(enumdirection), false);
-+                // Paper start - Protect Bedrock and End Portal/Frames from being destroyed; fix headless pistons breaking blocks
-+                BlockPos headPos = pos.relative(enumdirection);
-+                if (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits || world.getBlockState(headPos) == Blocks.PISTON_HEAD.defaultBlockState().setValue(FACING, enumdirection)) { // double check to make sure we're not a headless piston.
-+                    world.removeBlock(headPos, false);
-+                } else {
-+                    ((ServerLevel) world).getChunkSource().blockChanged(headPos); // ... fix client desync
-+                }
-+                // Paper end - Protect Bedrock and End Portal/Frames from being destroyed
-             }
- 
-             world.playSound((Player) null, pos, SoundEvents.PISTON_CONTRACT, SoundSource.BLOCKS, 0.5F, world.random.nextFloat() * 0.15F + 0.6F);
-@@ -335,7 +381,49 @@
-             BlockState[] aiblockdata = new BlockState[list.size() + list2.size()];
-             Direction enumdirection1 = extend ? dir : dir.getOpposite();
-             int i = 0;
-+            // CraftBukkit start
-+            final org.bukkit.block.Block bblock = world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ());
-+
-+            final List<BlockPos> moved = pistonextendschecker.getToPush();
-+            final List<BlockPos> broken = pistonextendschecker.getToDestroy();
-+
-+            List<org.bukkit.block.Block> blocks = new AbstractList<org.bukkit.block.Block>() {
- 
-+                @Override
-+                public int size() {
-+                    return moved.size() + broken.size();
-+                }
-+
-+                @Override
-+                public org.bukkit.block.Block get(int index) {
-+                    if (index >= this.size() || index < 0) {
-+                        throw new ArrayIndexOutOfBoundsException(index);
-+                    }
-+                    BlockPos pos = (BlockPos) (index < moved.size() ? moved.get(index) : broken.get(index - moved.size()));
-+                    return bblock.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ());
-+                }
-+            };
-+            org.bukkit.event.block.BlockPistonEvent event;
-+            if (extend) {
-+                event = new BlockPistonExtendEvent(bblock, blocks, CraftBlock.notchToBlockFace(enumdirection1));
-+            } else {
-+                event = new BlockPistonRetractEvent(bblock, blocks, CraftBlock.notchToBlockFace(enumdirection1));
-+            }
-+            world.getCraftServer().getPluginManager().callEvent(event);
-+
-+            if (event.isCancelled()) {
-+                for (BlockPos b : broken) {
-+                    world.sendBlockUpdated(b, Blocks.AIR.defaultBlockState(), world.getBlockState(b), 3);
-+                }
-+                for (BlockPos b : moved) {
-+                    world.sendBlockUpdated(b, Blocks.AIR.defaultBlockState(), world.getBlockState(b), 3);
-+                    b = b.relative(enumdirection1);
-+                    world.sendBlockUpdated(b, Blocks.AIR.defaultBlockState(), world.getBlockState(b), 3);
-+                }
-+                return false;
-+            }
-+            // CraftBukkit end
-+
-             BlockPos blockposition3;
-             int j;
-             BlockState iblockdata1;
-@@ -345,7 +433,7 @@
-                 iblockdata1 = world.getBlockState(blockposition3);
-                 BlockEntity tileentity = iblockdata1.hasBlockEntity() ? world.getBlockEntity(blockposition3) : null;
- 
--                dropResources(iblockdata1, world, blockposition3, tileentity);
-+                dropResources(iblockdata1, world, blockposition3, tileentity, pos); // Paper - Add BlockBreakBlockEvent
-                 world.setBlock(blockposition3, Blocks.AIR.defaultBlockState(), 18);
-                 world.gameEvent((Holder) GameEvent.BLOCK_DESTROY, blockposition3, GameEvent.Context.of(iblockdata1));
-                 if (!iblockdata1.is(BlockTags.FIRE)) {
-@@ -358,13 +446,25 @@
-             BlockState iblockdata2;
- 
-             for (j = list.size() - 1; j >= 0; --j) {
--                blockposition3 = (BlockPos) list.get(j);
--                iblockdata1 = world.getBlockState(blockposition3);
-+                // Paper start - fix a variety of piston desync dupes
-+                boolean allowDesync = io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPistonDuplication;
-+                BlockPos oldPos = blockposition3 = (BlockPos) list.get(j);
-+                iblockdata1 = allowDesync ? world.getBlockState(oldPos) : null;
-+                // Paper end - fix a variety of piston desync dupes
-                 blockposition3 = blockposition3.relative(enumdirection1);
-                 map.remove(blockposition3);
-                 iblockdata2 = (BlockState) Blocks.MOVING_PISTON.defaultBlockState().setValue(PistonBaseBlock.FACING, dir);
-                 world.setBlock(blockposition3, iblockdata2, 68);
--                world.setBlockEntity(MovingPistonBlock.newMovingBlockEntity(blockposition3, iblockdata2, (BlockState) list1.get(j), dir, extend, false));
-+                // Paper start - fix a variety of piston desync dupes
-+                if (!allowDesync) {
-+                    iblockdata1 = world.getBlockState(oldPos);
-+                    map.replace(oldPos, iblockdata1);
-+                }
-+                world.setBlockEntity(MovingPistonBlock.newMovingBlockEntity(blockposition3, iblockdata2, allowDesync ? (BlockState) list1.get(j) : iblockdata1, dir, extend, false));
-+                if (!allowDesync) {
-+                    world.setBlock(oldPos, Blocks.AIR.defaultBlockState(), Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_MOVE_BY_PISTON | 1024); // set air to prevent later physics updates from seeing this block
-+                }
-+                // Paper end - fix a variety of piston desync dupes
-                 aiblockdata[i++] = iblockdata1;
-             }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/state/properties/EnumProperty.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/state/properties/EnumProperty.java.patch
deleted file mode 100644
index 95194b370f..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/state/properties/EnumProperty.java.patch
+++ /dev/null
@@ -1,12 +0,0 @@
---- a/net/minecraft/world/level/block/state/properties/EnumProperty.java
-+++ b/net/minecraft/world/level/block/state/properties/EnumProperty.java
-@@ -59,8 +59,7 @@
-         return this.ordinalToIndex[enum_.ordinal()];
-     }
- 
--    @Override
--    public boolean equals(Object object) {
-+    public boolean equals_unused(Object object) { // Paper - Perf: Optimize hashCode/equals
-         if (this == object) {
-             return true;
-         } else {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/state/properties/IntegerProperty.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/state/properties/IntegerProperty.java.patch
deleted file mode 100644
index 6542f0bc6f..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/state/properties/IntegerProperty.java.patch
+++ /dev/null
@@ -1,12 +0,0 @@
---- a/net/minecraft/world/level/block/state/properties/IntegerProperty.java
-+++ b/net/minecraft/world/level/block/state/properties/IntegerProperty.java
-@@ -28,8 +28,7 @@
-         return this.values;
-     }
- 
--    @Override
--    public boolean equals(Object object) {
-+    public boolean equals_unused(Object object) { // Paper - Perf: Optimize hashCode/equals
-         if (this == object) {
-             return true;
-         } else {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/state/properties/Property.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/state/properties/Property.java.patch
deleted file mode 100644
index e0a6a659ba..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/block/state/properties/Property.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/level/block/state/properties/Property.java
-+++ b/net/minecraft/world/level/block/state/properties/Property.java
-@@ -72,7 +72,7 @@
- 
-     @Override
-     public boolean equals(Object object) {
--        return this == object || object instanceof Property<?> property && this.clazz.equals(property.clazz) && this.name.equals(property.name);
-+        return this == object; // Paper - Perf: Optimize hashCode/equals
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/ChunkGenerator.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/ChunkGenerator.java.patch
deleted file mode 100644
index 337768ffa6..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/ChunkGenerator.java.patch
+++ /dev/null
@@ -1,224 +0,0 @@
---- a/net/minecraft/world/level/chunk/ChunkGenerator.java
-+++ b/net/minecraft/world/level/chunk/ChunkGenerator.java
-@@ -108,8 +108,8 @@
- 
-     protected abstract MapCodec<? extends ChunkGenerator> codec();
- 
--    public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> structureSetRegistry, RandomState noiseConfig, long seed) {
--        return ChunkGeneratorStructureState.createForNormal(noiseConfig, seed, this.biomeSource, structureSetRegistry);
-+    public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> holderlookup, RandomState randomstate, long i, org.spigotmc.SpigotWorldConfig conf) { // Spigot
-+        return ChunkGeneratorStructureState.createForNormal(randomstate, i, this.biomeSource, holderlookup, conf); // Spigot
-     }
- 
-     public Optional<ResourceKey<MapCodec<? extends ChunkGenerator>>> getTypeNameForDataFixer() {
-@@ -127,6 +127,24 @@
- 
-     @Nullable
-     public Pair<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel world, HolderSet<Structure> structures, BlockPos center, int radius, boolean skipReferencedStructures) {
-+        // Paper start - StructuresLocateEvent
-+        final org.bukkit.World bukkitWorld = world.getWorld();
-+        final org.bukkit.Location origin = io.papermc.paper.util.MCUtil.toLocation(world, center);
-+        final List<org.bukkit.generator.structure.Structure> apiStructures = structures.stream().map(Holder::value).map(nms -> org.bukkit.craftbukkit.generator.structure.CraftStructure.minecraftToBukkit(nms)).toList();
-+        if (!apiStructures.isEmpty()) {
-+            final io.papermc.paper.event.world.StructuresLocateEvent event = new io.papermc.paper.event.world.StructuresLocateEvent(bukkitWorld, origin, apiStructures, radius, skipReferencedStructures);
-+            if (!event.callEvent()) {
-+                return null;
-+            }
-+            if (event.getResult() != null) {
-+                return Pair.of(io.papermc.paper.util.MCUtil.toBlockPos(event.getResult().pos()), world.registryAccess().lookupOrThrow(Registries.STRUCTURE).wrapAsHolder(org.bukkit.craftbukkit.generator.structure.CraftStructure.bukkitToMinecraft(event.getResult().structure())));
-+            }
-+            center = io.papermc.paper.util.MCUtil.toBlockPosition(event.getOrigin());
-+            radius = event.getRadius();
-+            skipReferencedStructures = event.shouldFindUnexplored();
-+            structures = HolderSet.direct(api -> world.registryAccess().lookupOrThrow(Registries.STRUCTURE).wrapAsHolder(org.bukkit.craftbukkit.generator.structure.CraftStructure.bukkitToMinecraft(api)), event.getStructures());
-+        }
-+        // Paper end
-         ChunkGeneratorStructureState chunkgeneratorstructurestate = world.getChunkSource().getGeneratorState();
-         Map<StructurePlacement, Set<Holder<Structure>>> map = new Object2ObjectArrayMap();
-         Iterator iterator = structures.iterator();
-@@ -223,6 +241,7 @@
- 
-             while (iterator.hasNext()) {
-                 ChunkPos chunkcoordintpair = (ChunkPos) iterator.next();
-+                if (!world.paperConfig().environment.locateStructuresOutsideWorldBorder && !world.getWorldBorder().isChunkInBounds(chunkcoordintpair.x, chunkcoordintpair.z)) { continue; } // Paper - Bound treasure maps to world border
- 
-                 blockposition_mutableblockposition.set(SectionPos.sectionToBlockCoord(chunkcoordintpair.x, 8), 32, SectionPos.sectionToBlockCoord(chunkcoordintpair.z, 8));
-                 double d1 = blockposition_mutableblockposition.distSqr(center);
-@@ -247,12 +266,15 @@
-         int i1 = placement.spacing();
- 
-         for (int j1 = -radius; j1 <= radius; ++j1) {
--            boolean flag1 = j1 == -radius || j1 == radius;
-+            // Paper start - Perf: iterate over border chunks instead of entire square chunk area
-+            boolean flag1 = j1 == -radius || j1 == radius; final boolean onBorderAlongZAxis = flag1; // Paper - OBFHELPER
- 
--            for (int k1 = -radius; k1 <= radius; ++k1) {
--                boolean flag2 = k1 == -radius || k1 == radius;
-+            for (int k1 = -radius; k1 <= radius; k1 += onBorderAlongZAxis ? 1 : radius * 2) {
-+                // boolean flag2 = k1 == -radius || k1 == radius;
- 
--                if (flag1 || flag2) {
-+                // if (flag1 || flag2) {
-+                if (true) {
-+                    // Paper end - Perf: iterate over border chunks instead of entire square chunk area
-                     int l1 = centerChunkX + i1 * j1;
-                     int i2 = centerChunkZ + i1 * k1;
-                     ChunkPos chunkcoordintpair = placement.getPotentialStructureChunk(seed, l1, i2);
-@@ -312,29 +334,29 @@
-         }
-     }
- 
--    public void applyBiomeDecoration(WorldGenLevel world, ChunkAccess chunk, StructureManager structureAccessor) {
--        ChunkPos chunkcoordintpair = chunk.getPos();
-+    public void addVanillaDecorations(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager) { // CraftBukkit
-+        ChunkPos chunkcoordintpair = ichunkaccess.getPos();
- 
-         if (!SharedConstants.debugVoidTerrain(chunkcoordintpair)) {
--            SectionPos sectionposition = SectionPos.of(chunkcoordintpair, world.getMinSectionY());
-+            SectionPos sectionposition = SectionPos.of(chunkcoordintpair, generatoraccessseed.getMinSectionY());
-             BlockPos blockposition = sectionposition.origin();
--            Registry<Structure> iregistry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE);
-+            Registry<Structure> iregistry = generatoraccessseed.registryAccess().lookupOrThrow(Registries.STRUCTURE);
-             Map<Integer, List<Structure>> map = (Map) iregistry.stream().collect(Collectors.groupingBy((structure) -> {
-                 return structure.step().ordinal();
-             }));
-             List<FeatureSorter.StepFeatureData> list = (List) this.featuresPerStep.get();
-             WorldgenRandom seededrandom = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
--            long i = seededrandom.setDecorationSeed(world.getSeed(), blockposition.getX(), blockposition.getZ());
-+            long i = seededrandom.setDecorationSeed(generatoraccessseed.getSeed(), blockposition.getX(), blockposition.getZ());
-             Set<Holder<Biome>> set = new ObjectArraySet();
- 
-             ChunkPos.rangeClosed(sectionposition.chunk(), 1).forEach((chunkcoordintpair1) -> {
--                ChunkAccess ichunkaccess1 = world.getChunk(chunkcoordintpair1.x, chunkcoordintpair1.z);
-+                ChunkAccess ichunkaccess1 = generatoraccessseed.getChunk(chunkcoordintpair1.x, chunkcoordintpair1.z);
-                 LevelChunkSection[] achunksection = ichunkaccess1.getSections();
-                 int j = achunksection.length;
- 
-                 for (int k = 0; k < j; ++k) {
-                     LevelChunkSection chunksection = achunksection[k];
--                    PalettedContainerRO palettedcontainerro = chunksection.getBiomes();
-+                    PalettedContainerRO<Holder<Biome>> palettedcontainerro = chunksection.getBiomes(); // CraftBukkit - decompile error
- 
-                     Objects.requireNonNull(set);
-                     palettedcontainerro.getAll(set::add);
-@@ -345,7 +367,7 @@
-             int j = list.size();
- 
-             try {
--                Registry<PlacedFeature> iregistry1 = world.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE);
-+                Registry<PlacedFeature> iregistry1 = generatoraccessseed.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE);
-                 int k = Math.max(GenerationStep.Decoration.values().length, j);
- 
-                 for (int l = 0; l < k; ++l) {
-@@ -353,7 +375,7 @@
-                     Iterator iterator;
-                     CrashReportCategory crashreportsystemdetails;
- 
--                    if (structureAccessor.shouldGenerateStructures()) {
-+                    if (structuremanager.shouldGenerateStructures()) {
-                         List<Structure> list1 = (List) map.getOrDefault(l, Collections.emptyList());
- 
-                         for (iterator = list1.iterator(); iterator.hasNext(); ++i1) {
-@@ -368,9 +390,9 @@
-                             };
- 
-                             try {
--                                world.setCurrentlyGenerating(supplier);
--                                structureAccessor.startsForStructure(sectionposition, structure).forEach((structurestart) -> {
--                                    structurestart.placeInChunk(world, structureAccessor, this, seededrandom, ChunkGenerator.getWritableArea(chunk), chunkcoordintpair);
-+                                generatoraccessseed.setCurrentlyGenerating(supplier);
-+                                structuremanager.startsForStructure(sectionposition, structure).forEach((structurestart) -> {
-+                                    structurestart.placeInChunk(generatoraccessseed, structuremanager, this, seededrandom, ChunkGenerator.getWritableArea(ichunkaccess), chunkcoordintpair);
-                                 });
-                             } catch (Exception exception) {
-                                 CrashReport crashreport = CrashReport.forThrowable(exception, "Feature placement");
-@@ -418,11 +440,18 @@
-                                 return (String) optional.orElseGet(placedfeature::toString);
-                             };
- 
--                            seededrandom.setFeatureSeed(i, l1, l);
-+                            // Paper start - Configurable feature seeds; change populationSeed used in random
-+                            long featurePopulationSeed = i;
-+                            final long configFeatureSeed = generatoraccessseed.getMinecraftWorld().paperConfig().featureSeeds.features.getLong(placedfeature.feature());
-+                            if (configFeatureSeed != -1) {
-+                                featurePopulationSeed = seededrandom.setDecorationSeed(configFeatureSeed, blockposition.getX(), blockposition.getZ()); // See seededrandom.setDecorationSeed from above
-+                            }
-+                            seededrandom.setFeatureSeed(featurePopulationSeed, l1, l);
-+                            // Paper end - Configurable feature seeds
- 
-                             try {
--                                world.setCurrentlyGenerating(supplier1);
--                                placedfeature.placeWithBiomeCheck(world, this, seededrandom, blockposition);
-+                                generatoraccessseed.setCurrentlyGenerating(supplier1);
-+                                placedfeature.placeWithBiomeCheck(generatoraccessseed, this, seededrandom, blockposition);
-                             } catch (Exception exception1) {
-                                 CrashReport crashreport1 = CrashReport.forThrowable(exception1, "Feature placement");
- 
-@@ -435,15 +464,42 @@
-                     }
-                 }
- 
--                world.setCurrentlyGenerating((Supplier) null);
-+                generatoraccessseed.setCurrentlyGenerating((Supplier) null);
-             } catch (Exception exception2) {
-                 CrashReport crashreport2 = CrashReport.forThrowable(exception2, "Biome decoration");
- 
-                 crashreport2.addCategory("Generation").setDetail("CenterX", (Object) chunkcoordintpair.x).setDetail("CenterZ", (Object) chunkcoordintpair.z).setDetail("Decoration Seed", (Object) i);
-                 throw new ReportedException(crashreport2);
-+            }
-+        }
-+    }
-+
-+   // CraftBukkit start
-+    public void applyBiomeDecoration(WorldGenLevel world, ChunkAccess chunk, StructureManager structureAccessor) {
-+        this.applyBiomeDecoration(world, chunk, structureAccessor, true);
-+    }
-+
-+    public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) {
-+        if (vanilla) {
-+            this.addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
-+        }
-+
-+        org.bukkit.World world = generatoraccessseed.getMinecraftWorld().getWorld();
-+        // only call when a populator is present (prevents unnecessary entity conversion)
-+        if (!world.getPopulators().isEmpty()) {
-+            org.bukkit.craftbukkit.generator.CraftLimitedRegion limitedRegion = new org.bukkit.craftbukkit.generator.CraftLimitedRegion(generatoraccessseed, ichunkaccess.getPos());
-+            int x = ichunkaccess.getPos().x;
-+            int z = ichunkaccess.getPos().z;
-+            for (org.bukkit.generator.BlockPopulator populator : world.getPopulators()) {
-+                WorldgenRandom seededrandom = new WorldgenRandom(new net.minecraft.world.level.levelgen.LegacyRandomSource(generatoraccessseed.getSeed()));
-+                seededrandom.setDecorationSeed(generatoraccessseed.getSeed(), x, z);
-+                populator.populate(world, new org.bukkit.craftbukkit.util.RandomSourceWrapper.RandomWrapper(seededrandom), x, z, limitedRegion);
-             }
-+            limitedRegion.saveEntities();
-+            limitedRegion.breakLink();
-         }
-     }
-+    // CraftBukkit end
- 
-     private static BoundingBox getWritableArea(ChunkAccess chunk) {
-         ChunkPos chunkcoordintpair = chunk.getPos();
-@@ -521,7 +577,7 @@
-                 }
-             }
- 
--            if (structureplacement.isStructureChunk(placementCalculator, chunkcoordintpair.x, chunkcoordintpair.z)) {
-+            if (structureplacement.isStructureChunk(placementCalculator, chunkcoordintpair.x, chunkcoordintpair.z, structureplacement instanceof net.minecraft.world.level.chunk.ChunkGeneratorStructureState.KeyedRandomSpreadStructurePlacement keyed ? keyed.key : null)) { // Paper - Add missing structure set seed configs
-                 if (list.size() == 1) {
-                     this.tryGenerateStructure((StructureSet.StructureSelectionEntry) list.get(0), structureAccessor, registryManager, randomstate, structureTemplateManager, placementCalculator.getLevelSeed(), chunk, chunkcoordintpair, sectionposition, dimension);
-                 } else {
-@@ -582,6 +638,14 @@
-         StructureStart structurestart = structure.generate(weightedEntry.structure(), dimension, dynamicRegistryManager, this, this.biomeSource, noiseConfig, structureManager, seed, pos, j, chunk, predicate);
- 
-         if (structurestart.isValid()) {
-+            // CraftBukkit start
-+            BoundingBox box = structurestart.getBoundingBox();
-+            org.bukkit.event.world.AsyncStructureSpawnEvent event = new org.bukkit.event.world.AsyncStructureSpawnEvent(structureAccessor.level.getMinecraftWorld().getWorld(), org.bukkit.craftbukkit.generator.structure.CraftStructure.minecraftToBukkit(structure), new org.bukkit.util.BoundingBox(box.minX(), box.minY(), box.minZ(), box.maxX(), box.maxY(), box.maxZ()), pos.x, pos.z);
-+            org.bukkit.Bukkit.getPluginManager().callEvent(event);
-+            if (event.isCancelled()) {
-+                return true;
-+            }
-+            // CraftBukkit end
-             structureAccessor.setStartForStructure(sectionPos, structure, structurestart, chunk);
-             return true;
-         } else {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/DataLayer.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/DataLayer.java.patch
deleted file mode 100644
index e1be97122d..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/DataLayer.java.patch
+++ /dev/null
@@ -1,7 +0,0 @@
---- a/net/minecraft/world/level/chunk/DataLayer.java
-+++ b/net/minecraft/world/level/chunk/DataLayer.java
-@@ -1,3 +1,4 @@
-+// mc-dev import
- package net.minecraft.world.level.chunk;
- 
- import java.util.Arrays;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/HashMapPalette.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/HashMapPalette.java.patch
deleted file mode 100644
index ce7c9bcdcb..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/HashMapPalette.java.patch
+++ /dev/null
@@ -1,30 +0,0 @@
---- a/net/minecraft/world/level/chunk/HashMapPalette.java
-+++ b/net/minecraft/world/level/chunk/HashMapPalette.java
-@@ -20,7 +20,7 @@
-     }
- 
-     public HashMapPalette(IdMap<T> idList, int indexBits, PaletteResize<T> listener) {
--        this(idList, indexBits, listener, CrudeIncrementalIntIdentityHashBiMap.create(1 << indexBits));
-+        this(idList, indexBits, listener, CrudeIncrementalIntIdentityHashBiMap.create((1 << indexBits) + 1)); // Paper - Perf: Avoid unnecessary resize operation in CrudeIncrementalIntIdentityHashBiMap
-     }
- 
-     private HashMapPalette(IdMap<T> idList, int indexBits, PaletteResize<T> listener, CrudeIncrementalIntIdentityHashBiMap<T> map) {
-@@ -38,10 +38,16 @@
-     public int idFor(T object) {
-         int i = this.values.getId(object);
-         if (i == -1) {
--            i = this.values.add(object);
--            if (i >= 1 << this.bits) {
-+            // Paper start - Perf: Avoid unnecessary resize operation in CrudeIncrementalIntIdentityHashBiMap and optimize
-+            // We use size() instead of the result from add(K)
-+            // This avoids adding another object unnecessarily
-+            // Without this change, + 2 would be required in the constructor
-+            if (this.values.size() >= 1 << this.bits) {
-                 i = this.resizeHandler.onResize(this.bits + 1, object);
-+            } else {
-+                i = this.values.add(object);
-             }
-+            // Paper end - Perf: Avoid unnecessary resize operation in CrudeIncrementalIntIdentityHashBiMap and optimize
-         }
- 
-         return i;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/LevelChunk.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/LevelChunk.java.patch
deleted file mode 100644
index d33242d709..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/LevelChunk.java.patch
+++ /dev/null
@@ -1,409 +0,0 @@
---- a/net/minecraft/world/level/chunk/LevelChunk.java
-+++ b/net/minecraft/world/level/chunk/LevelChunk.java
-@@ -79,7 +79,7 @@
-     };
-     private final Map<BlockPos, LevelChunk.RebindableTickingBlockEntityWrapper> tickersInLevel;
-     public boolean loaded;
--    public final Level level;
-+    public final ServerLevel level; // CraftBukkit - type
-     @Nullable
-     private Supplier<FullChunkStatus> fullStatus;
-     @Nullable
-@@ -98,7 +98,7 @@
-         this.tickersInLevel = Maps.newHashMap();
-         this.unsavedListener = (chunkcoordintpair1) -> {
-         };
--        this.level = world;
-+        this.level = (ServerLevel) world; // CraftBukkit - type
-         this.gameEventListenerRegistrySections = new Int2ObjectOpenHashMap();
-         Heightmap.Types[] aheightmap_type = Heightmap.Types.values();
-         int j = aheightmap_type.length;
-@@ -116,6 +116,15 @@
-         this.fluidTicks = fluidTickScheduler;
-     }
- 
-+    // CraftBukkit start
-+    public boolean mustNotSave;
-+    public boolean needsDecoration;
-+    // CraftBukkit end
-+
-+    // Paper start
-+    boolean loadedTicketLevel;
-+    // Paper end
-+
-     public LevelChunk(ServerLevel world, ProtoChunk protoChunk, @Nullable LevelChunk.PostLoadProcessor entityLoader) {
-         this(world, protoChunk.getPos(), protoChunk.getUpgradeData(), protoChunk.unpackBlockTicks(), protoChunk.unpackFluidTicks(), protoChunk.getInhabitedTime(), protoChunk.getSections(), entityLoader, protoChunk.getBlendingData());
-         if (!Collections.disjoint(protoChunk.pendingBlockEntities.keySet(), protoChunk.blockEntities.keySet())) {
-@@ -151,6 +160,10 @@
-         this.skyLightSources = protoChunk.skyLightSources;
-         this.setLightCorrect(protoChunk.isLightCorrect());
-         this.markUnsaved();
-+        this.needsDecoration = true; // CraftBukkit
-+        // CraftBukkit start
-+        this.persistentDataContainer = protoChunk.persistentDataContainer; // SPIGOT-6814: copy PDC to account for 1.17 to 1.18 chunk upgrading.
-+        // CraftBukkit end
-     }
- 
-     public void setUnsavedListener(LevelChunk.UnsavedListener unsavedListener) {
-@@ -187,7 +200,14 @@
-         return new ChunkAccess.PackedTicks(this.blockTicks.pack(time), this.fluidTicks.pack(time));
-     }
- 
-+    // Paper start
-     @Override
-+    public long getInhabitedTime() {
-+        return this.level.paperConfig().chunks.fixedChunkInhabitedTime < 0 ? super.getInhabitedTime() : this.level.paperConfig().chunks.fixedChunkInhabitedTime;
-+    }
-+    // Paper end
-+
-+    @Override
-     public GameEventListenerRegistry getListenerRegistry(int ySectionCoord) {
-         Level world = this.level;
- 
-@@ -200,8 +220,25 @@
-         }
-     }
- 
-+    // Paper start - Perf: Reduce instructions and provide final method
-+    public BlockState getBlockState(final int x, final int y, final int z) {
-+        return this.getBlockStateFinal(x, y, z);
-+    }
-+    public BlockState getBlockStateFinal(final int x, final int y, final int z) {
-+        // Copied and modified from below
-+        final int sectionIndex = this.getSectionIndex(y);
-+        if (sectionIndex < 0 || sectionIndex >= this.sections.length
-+            || this.sections[sectionIndex].nonEmptyBlockCount == 0) {
-+            return Blocks.AIR.defaultBlockState();
-+        }
-+        return this.sections[sectionIndex].states.get((y & 15) << 8 | (z & 15) << 4 | x & 15);
-+    }
-     @Override
-     public BlockState getBlockState(BlockPos pos) {
-+        if (true) {
-+            return this.getBlockStateFinal(pos.getX(), pos.getY(), pos.getZ());
-+        }
-+        // Paper end - Perf: Reduce instructions and provide final method
-         int i = pos.getX();
-         int j = pos.getY();
-         int k = pos.getZ();
-@@ -243,24 +280,38 @@
-         }
-     }
- 
-+    // Paper start - If loaded util
-     @Override
-+    public final FluidState getFluidIfLoaded(BlockPos blockposition) {
-+        return this.getFluidState(blockposition);
-+    }
-+
-+    @Override
-+    public final BlockState getBlockStateIfLoaded(BlockPos blockposition) {
-+        return this.getBlockState(blockposition);
-+    }
-+    // Paper end
-+
-+    @Override
-     public FluidState getFluidState(BlockPos pos) {
-         return this.getFluidState(pos.getX(), pos.getY(), pos.getZ());
-     }
- 
-     public FluidState getFluidState(int x, int y, int z) {
--        try {
--            int l = this.getSectionIndex(y);
-+        // Paper start - Perf: Optimise Chunk#getFluid
-+        // try {  // Remove try catch
-+        int index = this.getSectionIndex(y);
-+            if (index >= 0 && index < this.sections.length) {
-+                LevelChunkSection chunksection = this.sections[index];
- 
--            if (l >= 0 && l < this.sections.length) {
--                LevelChunkSection chunksection = this.sections[l];
--
-                 if (!chunksection.hasOnlyAir()) {
--                    return chunksection.getFluidState(x & 15, y & 15, z & 15);
-+                    return chunksection.states.get((y & 15) << 8 | (z & 15) << 4 | x & 15).getFluidState();
-+                    // Paper end - Perf: Optimise Chunk#getFluid
-                 }
-             }
- 
-             return Fluids.EMPTY.defaultFluidState();
-+        /* // Paper - Perf: Optimise Chunk#getFluid
-         } catch (Throwable throwable) {
-             CrashReport crashreport = CrashReport.forThrowable(throwable, "Getting fluid state");
-             CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Block being got");
-@@ -270,80 +321,89 @@
-             });
-             throw new ReportedException(crashreport);
-         }
-+         */  // Paper - Perf: Optimise Chunk#getFluid
-     }
- 
-+    // CraftBukkit start
-     @Nullable
-     @Override
-     public BlockState setBlockState(BlockPos pos, BlockState state, boolean moved) {
--        int i = pos.getY();
-+        return this.setBlockState(pos, state, moved, true);
-+    }
-+
-+    @Nullable
-+    public BlockState setBlockState(BlockPos blockposition, BlockState iblockdata, boolean flag, boolean doPlace) {
-+        // CraftBukkit end
-+        int i = blockposition.getY();
-         LevelChunkSection chunksection = this.getSection(this.getSectionIndex(i));
-         boolean flag1 = chunksection.hasOnlyAir();
- 
--        if (flag1 && state.isAir()) {
-+        if (flag1 && iblockdata.isAir()) {
-             return null;
-         } else {
--            int j = pos.getX() & 15;
-+            int j = blockposition.getX() & 15;
-             int k = i & 15;
--            int l = pos.getZ() & 15;
--            BlockState iblockdata1 = chunksection.setBlockState(j, k, l, state);
-+            int l = blockposition.getZ() & 15;
-+            BlockState iblockdata1 = chunksection.setBlockState(j, k, l, iblockdata);
- 
--            if (iblockdata1 == state) {
-+            if (iblockdata1 == iblockdata) {
-                 return null;
-             } else {
--                Block block = state.getBlock();
-+                Block block = iblockdata.getBlock();
- 
--                ((Heightmap) this.heightmaps.get(Heightmap.Types.MOTION_BLOCKING)).update(j, i, l, state);
--                ((Heightmap) this.heightmaps.get(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES)).update(j, i, l, state);
--                ((Heightmap) this.heightmaps.get(Heightmap.Types.OCEAN_FLOOR)).update(j, i, l, state);
--                ((Heightmap) this.heightmaps.get(Heightmap.Types.WORLD_SURFACE)).update(j, i, l, state);
-+                ((Heightmap) this.heightmaps.get(Heightmap.Types.MOTION_BLOCKING)).update(j, i, l, iblockdata);
-+                ((Heightmap) this.heightmaps.get(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES)).update(j, i, l, iblockdata);
-+                ((Heightmap) this.heightmaps.get(Heightmap.Types.OCEAN_FLOOR)).update(j, i, l, iblockdata);
-+                ((Heightmap) this.heightmaps.get(Heightmap.Types.WORLD_SURFACE)).update(j, i, l, iblockdata);
-                 boolean flag2 = chunksection.hasOnlyAir();
- 
-                 if (flag1 != flag2) {
--                    this.level.getChunkSource().getLightEngine().updateSectionStatus(pos, flag2);
-+                    this.level.getChunkSource().getLightEngine().updateSectionStatus(blockposition, flag2);
-                     this.level.getChunkSource().onSectionEmptinessChanged(this.chunkPos.x, SectionPos.blockToSectionCoord(i), this.chunkPos.z, flag2);
-                 }
- 
--                if (LightEngine.hasDifferentLightProperties(iblockdata1, state)) {
-+                if (LightEngine.hasDifferentLightProperties(iblockdata1, iblockdata)) {
-                     ProfilerFiller gameprofilerfiller = Profiler.get();
- 
-                     gameprofilerfiller.push("updateSkyLightSources");
-                     this.skyLightSources.update(this, j, i, l);
-                     gameprofilerfiller.popPush("queueCheckLight");
--                    this.level.getChunkSource().getLightEngine().checkBlock(pos);
-+                    this.level.getChunkSource().getLightEngine().checkBlock(blockposition);
-                     gameprofilerfiller.pop();
-                 }
- 
-                 boolean flag3 = iblockdata1.hasBlockEntity();
- 
--                if (!this.level.isClientSide) {
--                    iblockdata1.onRemove(this.level, pos, state, moved);
-+                if (!this.level.isClientSide && !this.level.isBlockPlaceCancelled) { // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent
-+                    iblockdata1.onRemove(this.level, blockposition, iblockdata, flag);
-                 } else if (!iblockdata1.is(block) && flag3) {
--                    this.removeBlockEntity(pos);
-+                    this.removeBlockEntity(blockposition);
-                 }
- 
-                 if (!chunksection.getBlockState(j, k, l).is(block)) {
-                     return null;
-                 } else {
--                    if (!this.level.isClientSide) {
--                        state.onPlace(this.level, pos, iblockdata1, moved);
-+                    // CraftBukkit - Don't place while processing the BlockPlaceEvent, unless it's a BlockContainer. Prevents blocks such as TNT from activating when cancelled.
-+                    if (!this.level.isClientSide && doPlace && (!this.level.captureBlockStates || block instanceof net.minecraft.world.level.block.BaseEntityBlock)) {
-+                        iblockdata.onPlace(this.level, blockposition, iblockdata1, flag);
-                     }
- 
--                    if (state.hasBlockEntity()) {
--                        BlockEntity tileentity = this.getBlockEntity(pos, LevelChunk.EntityCreationType.CHECK);
-+                    if (iblockdata.hasBlockEntity()) {
-+                        BlockEntity tileentity = this.getBlockEntity(blockposition, LevelChunk.EntityCreationType.CHECK);
- 
--                        if (tileentity != null && !tileentity.isValidBlockState(state)) {
--                            LevelChunk.LOGGER.warn("Found mismatched block entity @ {}: type = {}, state = {}", new Object[]{pos, tileentity.getType().builtInRegistryHolder().key().location(), state});
--                            this.removeBlockEntity(pos);
-+                        if (tileentity != null && !tileentity.isValidBlockState(iblockdata)) {
-+                            LevelChunk.LOGGER.warn("Found mismatched block entity @ {}: type = {}, state = {}", new Object[]{blockposition, tileentity.getType().builtInRegistryHolder().key().location(), iblockdata});
-+                            this.removeBlockEntity(blockposition);
-                             tileentity = null;
-                         }
- 
-                         if (tileentity == null) {
--                            tileentity = ((EntityBlock) block).newBlockEntity(pos, state);
-+                            tileentity = ((EntityBlock) block).newBlockEntity(blockposition, iblockdata);
-                             if (tileentity != null) {
-                                 this.addAndRegisterBlockEntity(tileentity);
-                             }
-                         } else {
--                            tileentity.setBlockState(state);
-+                            tileentity.setBlockState(iblockdata);
-                             this.updateBlockEntityTicker(tileentity);
-                         }
-                     }
-@@ -375,7 +435,12 @@
- 
-     @Nullable
-     public BlockEntity getBlockEntity(BlockPos pos, LevelChunk.EntityCreationType creationType) {
--        BlockEntity tileentity = (BlockEntity) this.blockEntities.get(pos);
-+        // CraftBukkit start
-+        BlockEntity tileentity = this.level.capturedTileEntities.get(pos);
-+        if (tileentity == null) {
-+            tileentity = (BlockEntity) this.blockEntities.get(pos);
-+        }
-+        // CraftBukkit end
- 
-         if (tileentity == null) {
-             CompoundTag nbttagcompound = (CompoundTag) this.pendingBlockEntities.remove(pos);
-@@ -446,7 +511,13 @@
-         BlockState iblockdata = this.getBlockState(blockposition);
- 
-         if (!iblockdata.hasBlockEntity()) {
--            LevelChunk.LOGGER.warn("Trying to set block entity {} at position {}, but state {} does not allow it", new Object[]{blockEntity, blockposition, iblockdata});
-+            // Paper start - ServerExceptionEvent
-+            com.destroystokyo.paper.exception.ServerInternalException e = new com.destroystokyo.paper.exception.ServerInternalException(
-+                "Trying to set block entity %s at position %s, but state %s does not allow it".formatted(blockEntity, blockposition, iblockdata)
-+            );
-+            e.printStackTrace();
-+            com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(e);
-+            // Paper end - ServerExceptionEvent
-         } else {
-             BlockState iblockdata1 = blockEntity.getBlockState();
- 
-@@ -500,6 +571,12 @@
-         if (this.isInLevel()) {
-             BlockEntity tileentity = (BlockEntity) this.blockEntities.remove(pos);
- 
-+            // CraftBukkit start - SPIGOT-5561: Also remove from pending map
-+            if (!this.pendingBlockEntities.isEmpty()) {
-+                this.pendingBlockEntities.remove(pos);
-+            }
-+            // CraftBukkit end
-+
-             if (tileentity != null) {
-                 Level world = this.level;
- 
-@@ -553,6 +630,65 @@
- 
-     }
- 
-+    // CraftBukkit start
-+    public void loadCallback() {
-+        // Paper start
-+        this.loadedTicketLevel = true;
-+        // Paper end
-+        org.bukkit.Server server = this.level.getCraftServer();
-+        this.level.getChunkSource().addLoadedChunk(this); // Paper
-+        if (server != null) {
-+            /*
-+             * If it's a new world, the first few chunks are generated inside
-+             * the World constructor. We can't reliably alter that, so we have
-+             * no way of creating a CraftWorld/CraftServer at that point.
-+             */
-+            org.bukkit.Chunk bukkitChunk = new org.bukkit.craftbukkit.CraftChunk(this);
-+            server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkLoadEvent(bukkitChunk, this.needsDecoration));
-+
-+            if (this.needsDecoration) {
-+                this.needsDecoration = false;
-+                java.util.Random random = new java.util.Random();
-+                random.setSeed(this.level.getSeed());
-+                long xRand = random.nextLong() / 2L * 2L + 1L;
-+                long zRand = random.nextLong() / 2L * 2L + 1L;
-+                random.setSeed((long) this.chunkPos.x * xRand + (long) this.chunkPos.z * zRand ^ this.level.getSeed());
-+
-+                org.bukkit.World world = this.level.getWorld();
-+                if (world != null) {
-+                    this.level.populating = true;
-+                    try {
-+                        for (org.bukkit.generator.BlockPopulator populator : world.getPopulators()) {
-+                            populator.populate(world, random, bukkitChunk);
-+                        }
-+                    } finally {
-+                        this.level.populating = false;
-+                    }
-+                }
-+                server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkPopulateEvent(bukkitChunk));
-+            }
-+        }
-+    }
-+
-+    public void unloadCallback() {
-+        org.bukkit.Server server = this.level.getCraftServer();
-+        org.bukkit.Chunk bukkitChunk = new org.bukkit.craftbukkit.CraftChunk(this);
-+        org.bukkit.event.world.ChunkUnloadEvent unloadEvent = new org.bukkit.event.world.ChunkUnloadEvent(bukkitChunk, this.isUnsaved());
-+        server.getPluginManager().callEvent(unloadEvent);
-+        // note: saving can be prevented, but not forced if no saving is actually required
-+        this.mustNotSave = !unloadEvent.isSaveChunk();
-+        this.level.getChunkSource().removeLoadedChunk(this); // Paper
-+        // Paper start
-+        this.loadedTicketLevel = false;
-+        // Paper end
-+    }
-+
-+    @Override
-+    public boolean isUnsaved() {
-+        return super.isUnsaved() && !this.mustNotSave;
-+    }
-+    // CraftBukkit end
-+
-     public boolean isEmpty() {
-         return false;
-     }
-@@ -750,7 +886,7 @@
- 
-     private <T extends BlockEntity> void updateBlockEntityTicker(T blockEntity) {
-         BlockState iblockdata = blockEntity.getBlockState();
--        BlockEntityTicker<T> blockentityticker = iblockdata.getTicker(this.level, blockEntity.getType());
-+        BlockEntityTicker<T> blockentityticker = iblockdata.getTicker(this.level, (BlockEntityType<T>) blockEntity.getType()); // CraftBukkit - decompile error
- 
-         if (blockentityticker == null) {
-             this.removeBlockEntityTicker(blockEntity.getBlockPos());
-@@ -841,7 +977,7 @@
-         private boolean loggedInvalidBlockState;
- 
-         BoundTickingBlockEntity(final BlockEntity tileentity, final BlockEntityTicker blockentityticker) {
--            this.blockEntity = tileentity;
-+            this.blockEntity = (T) tileentity; // CraftBukkit - decompile error
-             this.ticker = blockentityticker;
-         }
- 
-@@ -860,18 +996,25 @@
-                         if (this.blockEntity.getType().isValid(iblockdata)) {
-                             this.ticker.tick(LevelChunk.this.level, this.blockEntity.getBlockPos(), iblockdata, this.blockEntity);
-                             this.loggedInvalidBlockState = false;
--                        } else if (!this.loggedInvalidBlockState) {
--                            this.loggedInvalidBlockState = true;
--                            LevelChunk.LOGGER.warn("Block entity {} @ {} state {} invalid for ticking:", new Object[]{LogUtils.defer(this::getType), LogUtils.defer(this::getPos), iblockdata});
-+                        // Paper start - Remove the Block Entity if it's invalid
-+                        } else {
-+                            LevelChunk.this.removeBlockEntity(this.getPos());
-+                            if (!this.loggedInvalidBlockState) {
-+                                this.loggedInvalidBlockState = true;
-+                                LevelChunk.LOGGER.warn("Block entity {} @ {} state {} invalid for ticking:", new Object[]{LogUtils.defer(this::getType), LogUtils.defer(this::getPos), iblockdata});
-+                            }
-+                            // Paper end - Remove the Block Entity if it's invalid
-                         }
- 
-                         gameprofilerfiller.pop();
-                     } catch (Throwable throwable) {
--                        CrashReport crashreport = CrashReport.forThrowable(throwable, "Ticking block entity");
--                        CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Block entity being ticked");
--
--                        this.blockEntity.fillCrashReportCategory(crashreportsystemdetails);
--                        throw new ReportedException(crashreport);
-+                        // Paper start - Prevent block entity and entity crashes
-+                        final String msg = String.format("BlockEntity threw exception at %s:%s,%s,%s", LevelChunk.this.getLevel().getWorld().getName(), this.getPos().getX(), this.getPos().getY(), this.getPos().getZ());
-+                        net.minecraft.server.MinecraftServer.LOGGER.error(msg, throwable);
-+                        net.minecraft.world.level.chunk.LevelChunk.this.level.getCraftServer().getPluginManager().callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerInternalException(msg, throwable))); // Paper - ServerExceptionEvent
-+                        LevelChunk.this.removeBlockEntity(this.getPos());
-+                        // Paper end - Prevent block entity and entity crashes
-+                        // Spigot start
-                     }
-                 }
-             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/PalettedContainer.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/PalettedContainer.java.patch
deleted file mode 100644
index aff5b81eae..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/PalettedContainer.java.patch
+++ /dev/null
@@ -1,74 +0,0 @@
---- a/net/minecraft/world/level/chunk/PalettedContainer.java
-+++ b/net/minecraft/world/level/chunk/PalettedContainer.java
-@@ -30,14 +30,14 @@
-     public final IdMap<T> registry;
-     private volatile PalettedContainer.Data<T> data;
-     private final PalettedContainer.Strategy strategy;
--    private final ThreadingDetector threadingDetector = new ThreadingDetector("PalettedContainer");
-+    // private final ThreadingDetector threadingDetector = new ThreadingDetector("PalettedContainer"); // Paper - unused
- 
-     public void acquire() {
--        this.threadingDetector.checkAndLock();
-+        // this.threadingDetector.checkAndLock(); // Paper - disable this - use proper synchronization
-     }
- 
-     public void release() {
--        this.threadingDetector.checkAndUnlock();
-+        // this.threadingDetector.checkAndUnlock(); // Paper - disable this
-     }
- 
-     public static <T> Codec<PalettedContainer<T>> codecRW(IdMap<T> idList, Codec<T> entryCodec, PalettedContainer.Strategy paletteProvider, T defaultValue) {
-@@ -110,7 +110,7 @@
-     }
- 
-     @Override
--    public int onResize(int newBits, T object) {
-+    public synchronized int onResize(int newBits, T object) { // Paper - synchronize
-         PalettedContainer.Data<T> data = this.data;
-         PalettedContainer.Data<T> data2 = this.createOrReuseData(data, newBits);
-         data2.copyFrom(data.palette, data.storage);
-@@ -135,7 +135,7 @@
-         return this.getAndSet(this.strategy.getIndex(x, y, z), value);
-     }
- 
--    private T getAndSet(int index, T value) {
-+    private synchronized T getAndSet(int index, T value) { // Paper - synchronize
-         int i = this.data.palette.idFor(value);
-         int j = this.data.storage.getAndSet(index, i);
-         return this.data.palette.valueFor(j);
-@@ -151,7 +151,7 @@
-         }
-     }
- 
--    private void set(int index, T value) {
-+    private synchronized void set(int index, T value) { // Paper - synchronize
-         int i = this.data.palette.idFor(value);
-         this.data.storage.set(index, i);
-     }
-@@ -174,7 +174,7 @@
-         intSet.forEach(id -> action.accept(palette.valueFor(id)));
-     }
- 
--    public void read(FriendlyByteBuf buf) {
-+    public synchronized void read(FriendlyByteBuf buf) { // Paper - synchronize
-         this.acquire();
- 
-         try {
-@@ -189,7 +189,7 @@
-     }
- 
-     @Override
--    public void write(FriendlyByteBuf buf) {
-+    public synchronized void write(FriendlyByteBuf buf) { // Paper - synchronize
-         this.acquire();
- 
-         try {
-@@ -237,7 +237,7 @@
-     }
- 
-     @Override
--    public PalettedContainerRO.PackedData<T> pack(IdMap<T> idList, PalettedContainer.Strategy paletteProvider) {
-+    public synchronized PalettedContainerRO.PackedData<T> pack(IdMap<T> idList, PalettedContainer.Strategy paletteProvider) { // Paper - synchronize
-         this.acquire();
- 
-         PalettedContainerRO.PackedData var12;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/ProtoChunk.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/ProtoChunk.java.patch
deleted file mode 100644
index 1f9c01eb79..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/ProtoChunk.java.patch
+++ /dev/null
@@ -1,22 +0,0 @@
---- a/net/minecraft/world/level/chunk/ProtoChunk.java
-+++ b/net/minecraft/world/level/chunk/ProtoChunk.java
-@@ -81,7 +81,19 @@
-     @Override
-     public ChunkAccess.PackedTicks getTicksForSerialization(long time) {
-         return new ChunkAccess.PackedTicks(this.blockTicks.pack(time), this.fluidTicks.pack(time));
-+    }
-+
-+    // Paper start - If loaded util
-+    @Override
-+    public final FluidState getFluidIfLoaded(BlockPos blockposition) {
-+        return this.getFluidState(blockposition);
-+    }
-+
-+    @Override
-+    public final BlockState getBlockStateIfLoaded(BlockPos blockposition) {
-+        return this.getBlockState(blockposition);
-     }
-+    // Paper end
- 
-     @Override
-     public BlockState getBlockState(BlockPos pos) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/storage/ChunkStorage.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/storage/ChunkStorage.java.patch
deleted file mode 100644
index b82d242d2c..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/storage/ChunkStorage.java.patch
+++ /dev/null
@@ -1,158 +0,0 @@
---- a/net/minecraft/world/level/chunk/storage/ChunkStorage.java
-+++ b/net/minecraft/world/level/chunk/storage/ChunkStorage.java
-@@ -15,10 +15,16 @@
- import net.minecraft.nbt.CompoundTag;
- import net.minecraft.nbt.NbtUtils;
- import net.minecraft.resources.ResourceKey;
-+import net.minecraft.server.level.ServerChunkCache;
-+import net.minecraft.server.level.ServerLevel;
- import net.minecraft.util.datafix.DataFixTypes;
- import net.minecraft.world.level.ChunkPos;
--import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.LevelAccessor;
- import net.minecraft.world.level.chunk.ChunkGenerator;
-+// CraftBukkit start
-+import java.util.concurrent.ExecutionException;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import net.minecraft.world.level.dimension.LevelStem;
- import net.minecraft.world.level.levelgen.structure.LegacyStructureDataHandler;
- import net.minecraft.world.level.storage.DimensionDataStorage;
- 
-@@ -39,27 +45,86 @@
-         return this.worker.isOldChunkAround(chunkPos, checkRadius);
-     }
- 
--    public CompoundTag upgradeChunkTag(ResourceKey<Level> worldKey, Supplier<DimensionDataStorage> persistentStateManagerFactory, CompoundTag nbt, Optional<ResourceKey<MapCodec<? extends ChunkGenerator>>> generatorCodecKey) {
--        int i = ChunkStorage.getVersion(nbt);
-+    // CraftBukkit start
-+    private boolean check(ServerChunkCache cps, int x, int z) {
-+        if (true) return true; // Paper - Perf: this isn't even needed anymore, light is purged updating to 1.14+, why are we holding up the conversion process reading chunk data off disk - return true, we need to set light populated to true so the converter recognizes the chunk as being "full"
-+        ChunkPos pos = new ChunkPos(x, z);
-+        if (cps != null) {
-+            com.google.common.base.Preconditions.checkState(org.bukkit.Bukkit.isPrimaryThread(), "primary thread");
-+            if (cps.hasChunk(x, z)) {
-+                return true;
-+            }
-+        }
- 
-+        CompoundTag nbt;
-+        try {
-+            nbt = this.read(pos).get().orElse(null);
-+        } catch (InterruptedException | ExecutionException ex) {
-+            throw new RuntimeException(ex);
-+        }
-+        if (nbt != null) {
-+            CompoundTag level = nbt.getCompound("Level");
-+            if (level.getBoolean("TerrainPopulated")) {
-+                return true;
-+            }
-+
-+            ChunkStatus status = ChunkStatus.byName(level.getString("Status"));
-+            if (status != null && status.isOrAfter(ChunkStatus.FEATURES)) {
-+                return true;
-+            }
-+        }
-+
-+        return false;
-+    }
-+
-+    public CompoundTag upgradeChunkTag(ResourceKey<LevelStem> resourcekey, Supplier<DimensionDataStorage> supplier, CompoundTag nbttagcompound, Optional<ResourceKey<MapCodec<? extends ChunkGenerator>>> optional, ChunkPos pos, @Nullable LevelAccessor generatoraccess) {
-+        // CraftBukkit end
-+        int i = ChunkStorage.getVersion(nbttagcompound);
-+
-         if (i == SharedConstants.getCurrentVersion().getDataVersion().getVersion()) {
--            return nbt;
-+            return nbttagcompound;
-         } else {
-             try {
-+                // CraftBukkit start
-+                if (i < 1466) {
-+                    CompoundTag level = nbttagcompound.getCompound("Level");
-+                    if (level.getBoolean("TerrainPopulated") && !level.getBoolean("LightPopulated")) {
-+                        ServerChunkCache cps = (generatoraccess == null) ? null : ((ServerLevel) generatoraccess).getChunkSource();
-+                        if (this.check(cps, pos.x - 1, pos.z) && this.check(cps, pos.x - 1, pos.z - 1) && this.check(cps, pos.x, pos.z - 1)) {
-+                            level.putBoolean("LightPopulated", true);
-+                        }
-+                    }
-+                }
-+                // CraftBukkit end
-+
-                 if (i < 1493) {
--                    nbt = DataFixTypes.CHUNK.update(this.fixerUpper, nbt, i, 1493);
--                    if (nbt.getCompound("Level").getBoolean("hasLegacyStructureData")) {
--                        LegacyStructureDataHandler persistentstructurelegacy = this.getLegacyStructureHandler(worldKey, persistentStateManagerFactory);
-+                    nbttagcompound = DataFixTypes.CHUNK.update(this.fixerUpper, nbttagcompound, i, 1493);
-+                    if (nbttagcompound.getCompound("Level").getBoolean("hasLegacyStructureData")) {
-+                        LegacyStructureDataHandler persistentstructurelegacy = this.getLegacyStructureHandler(resourcekey, supplier);
- 
--                        nbt = persistentstructurelegacy.updateFromLegacy(nbt);
-+                        nbttagcompound = persistentstructurelegacy.updateFromLegacy(nbttagcompound);
-                     }
-                 }
- 
--                ChunkStorage.injectDatafixingContext(nbt, worldKey, generatorCodecKey);
--                nbt = DataFixTypes.CHUNK.updateToCurrentVersion(this.fixerUpper, nbt, Math.max(1493, i));
--                ChunkStorage.removeDatafixingContext(nbt);
--                NbtUtils.addCurrentDataVersion(nbt);
--                return nbt;
-+                // Spigot start - SPIGOT-6806: Quick and dirty way to prevent below zero generation in old chunks, by setting the status to heightmap instead of empty
-+                boolean stopBelowZero = false;
-+                boolean belowZeroGenerationInExistingChunks = (generatoraccess != null) ? ((ServerLevel) generatoraccess).spigotConfig.belowZeroGenerationInExistingChunks : org.spigotmc.SpigotConfig.belowZeroGenerationInExistingChunks;
-+
-+                if (i <= 2730 && !belowZeroGenerationInExistingChunks) {
-+                    stopBelowZero = "full".equals(nbttagcompound.getCompound("Level").getString("Status"));
-+                }
-+                // Spigot end
-+
-+                ChunkStorage.injectDatafixingContext(nbttagcompound, resourcekey, optional);
-+                nbttagcompound = DataFixTypes.CHUNK.updateToCurrentVersion(this.fixerUpper, nbttagcompound, Math.max(1493, i));
-+                // Spigot start
-+                if (stopBelowZero) {
-+                    nbttagcompound.putString("Status", net.minecraft.core.registries.BuiltInRegistries.CHUNK_STATUS.getKey(ChunkStatus.SPAWN).toString());
-+                }
-+                // Spigot end
-+                ChunkStorage.removeDatafixingContext(nbttagcompound);
-+                NbtUtils.addCurrentDataVersion(nbttagcompound);
-+                return nbttagcompound;
-             } catch (Exception exception) {
-                 CrashReport crashreport = CrashReport.forThrowable(exception, "Updated chunk");
-                 CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Updated chunk details");
-@@ -70,7 +135,7 @@
-         }
-     }
- 
--    private LegacyStructureDataHandler getLegacyStructureHandler(ResourceKey<Level> worldKey, Supplier<DimensionDataStorage> stateManagerGetter) {
-+    private LegacyStructureDataHandler getLegacyStructureHandler(ResourceKey<LevelStem> worldKey, Supplier<DimensionDataStorage> stateManagerGetter) { // CraftBukkit
-         LegacyStructureDataHandler persistentstructurelegacy = this.legacyStructureHandler;
- 
-         if (persistentstructurelegacy == null) {
-@@ -85,7 +150,7 @@
-         return persistentstructurelegacy;
-     }
- 
--    public static void injectDatafixingContext(CompoundTag nbt, ResourceKey<Level> worldKey, Optional<ResourceKey<MapCodec<? extends ChunkGenerator>>> generatorCodecKey) {
-+    public static void injectDatafixingContext(CompoundTag nbt, ResourceKey<LevelStem> worldKey, Optional<ResourceKey<MapCodec<? extends ChunkGenerator>>> generatorCodecKey) { // CraftBukkit
-         CompoundTag nbttagcompound1 = new CompoundTag();
- 
-         nbttagcompound1.putString("dimension", worldKey.location().toString());
-@@ -108,8 +173,19 @@
-     }
- 
-     public CompletableFuture<Void> write(ChunkPos chunkPos, Supplier<CompoundTag> nbtSupplier) {
-+        // Paper start - guard against possible chunk pos desync
-+        final Supplier<CompoundTag> guardedPosCheck = () -> {
-+            CompoundTag nbt = nbtSupplier.get();
-+            if (nbt != null && !chunkPos.equals(SerializableChunkData.getChunkCoordinate(nbt))) {
-+                final String world = (ChunkStorage.this instanceof net.minecraft.server.level.ChunkMap) ? ((net.minecraft.server.level.ChunkMap) ChunkStorage.this).level.getWorld().getName() : null;
-+                throw new IllegalArgumentException("Chunk coordinate and serialized data do not have matching coordinates, trying to serialize coordinate " + chunkPos
-+                    + " but compound says coordinate is " + SerializableChunkData.getChunkCoordinate(nbt) + (world == null ? " for an unknown world" : (" for world: " + world)));
-+            }
-+            return nbt;
-+        };
-+        // Paper end - guard against possible chunk pos desync
-         this.handleLegacyStructureIndex(chunkPos);
--        return this.worker.store(chunkPos, nbtSupplier);
-+        return this.worker.store(chunkPos, guardedPosCheck); // Paper - guard against possible chunk pos desync
-     }
- 
-     protected void handleLegacyStructureIndex(ChunkPos chunkPos) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/storage/RegionFile.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/storage/RegionFile.java.patch
deleted file mode 100644
index 3955a59294..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/storage/RegionFile.java.patch
+++ /dev/null
@@ -1,127 +0,0 @@
---- a/net/minecraft/world/level/chunk/storage/RegionFile.java
-+++ b/net/minecraft/world/level/chunk/storage/RegionFile.java
-@@ -1,3 +1,4 @@
-+// mc-dev import
- package net.minecraft.world.level.chunk.storage;
- 
- import com.google.common.annotations.VisibleForTesting;
-@@ -49,7 +50,7 @@
-     protected final RegionBitmap usedSectors;
- 
-     public RegionFile(RegionStorageInfo storageKey, Path directory, Path path, boolean dsync) throws IOException {
--        this(storageKey, directory, path, RegionFileVersion.getSelected(), dsync);
-+        this(storageKey, directory, path, RegionFileVersion.getCompressionFormat(), dsync); // Paper - Configurable region compression format
-     }
- 
-     public RegionFile(RegionStorageInfo storageKey, Path path, Path directory, RegionFileVersion compressionFormat, boolean dsync) throws IOException {
-@@ -63,8 +64,8 @@
-         } else {
-             this.externalFileDir = directory;
-             this.offsets = this.header.asIntBuffer();
--            this.offsets.limit(1024);
--            this.header.position(4096);
-+            ((java.nio.Buffer) this.offsets).limit(1024); // CraftBukkit - decompile error
-+            ((java.nio.Buffer) this.header).position(4096); // CraftBukkit - decompile error
-             this.timestamps = this.header.asIntBuffer();
-             if (dsync) {
-                 this.file = FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.DSYNC);
-@@ -73,7 +74,7 @@
-             }
- 
-             this.usedSectors.force(0, 2);
--            this.header.position(0);
-+            ((java.nio.Buffer) this.header).position(0); // CraftBukkit - decompile error
-             int i = this.file.read(this.header, 0L);
- 
-             if (i != -1) {
-@@ -89,6 +90,14 @@
-                     if (l != 0) {
-                         int i1 = RegionFile.getSectorNumber(l);
-                         int j1 = RegionFile.getNumSectors(l);
-+                        // Spigot start
-+                        if (j1 == 255) {
-+                            // We're maxed out, so we need to read the proper length from the section
-+                            ByteBuffer realLen = ByteBuffer.allocate(4);
-+                            this.file.read(realLen, i1 * 4096);
-+                            j1 = (realLen.getInt(0) + 4) / 4096 + 1;
-+                        }
-+                        // Spigot end
- 
-                         if (i1 < 2) {
-                             RegionFile.LOGGER.warn("Region file {} has invalid sector at index: {}; sector {} overlaps with header", new Object[]{path, k, i1});
-@@ -128,11 +137,18 @@
-         } else {
-             int j = RegionFile.getSectorNumber(i);
-             int k = RegionFile.getNumSectors(i);
-+            // Spigot start
-+            if (k == 255) {
-+                ByteBuffer realLen = ByteBuffer.allocate(4);
-+                this.file.read(realLen, j * 4096);
-+                k = (realLen.getInt(0) + 4) / 4096 + 1;
-+            }
-+            // Spigot end
-             int l = k * 4096;
-             ByteBuffer bytebuffer = ByteBuffer.allocate(l);
- 
-             this.file.read(bytebuffer, (long) (j * 4096));
--            bytebuffer.flip();
-+            ((java.nio.Buffer) bytebuffer).flip(); // CraftBukkit - decompile error
-             if (bytebuffer.remaining() < 5) {
-                 RegionFile.LOGGER.error("Chunk {} header is truncated: expected {} but read {}", new Object[]{pos, l, bytebuffer.remaining()});
-                 return null;
-@@ -246,7 +262,7 @@
- 
-             try {
-                 this.file.read(bytebuffer, (long) (j * 4096));
--                bytebuffer.flip();
-+                ((java.nio.Buffer) bytebuffer).flip(); // CraftBukkit - decompile error
-                 if (bytebuffer.remaining() != 5) {
-                     return false;
-                 } else {
-@@ -280,6 +296,7 @@
-                     return true;
-                 }
-             } catch (IOException ioexception) {
-+                com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(ioexception); // Paper - ServerExceptionEvent
-                 return false;
-             }
-         }
-@@ -349,7 +366,7 @@
- 
-         bytebuffer.putInt(1);
-         bytebuffer.put((byte) (this.version.getId() | 128));
--        bytebuffer.flip();
-+        ((java.nio.Buffer) bytebuffer).flip(); // CraftBukkit - decompile error
-         return bytebuffer;
-     }
- 
-@@ -358,9 +375,10 @@
-         FileChannel filechannel = FileChannel.open(path1, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
- 
-         try {
--            buf.position(5);
-+            ((java.nio.Buffer) buf).position(5); // CraftBukkit - decompile error
-             filechannel.write(buf);
-         } catch (Throwable throwable) {
-+            com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(throwable); // Paper - ServerExceptionEvent
-             if (filechannel != null) {
-                 try {
-                     filechannel.close();
-@@ -382,7 +400,7 @@
-     }
- 
-     private void writeHeader() throws IOException {
--        this.header.position(0);
-+        ((java.nio.Buffer) this.header).position(0); // CraftBukkit - decompile error
-         this.file.write(this.header, 0L);
-     }
- 
-@@ -418,7 +436,7 @@
-         if (i != j) {
-             ByteBuffer bytebuffer = RegionFile.PADDING_BUFFER.duplicate();
- 
--            bytebuffer.position(0);
-+            ((java.nio.Buffer) bytebuffer).position(0); // CraftBukkit - decompile error
-             this.file.write(bytebuffer, (long) (j - 1));
-         }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/storage/RegionFileStorage.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/storage/RegionFileStorage.java.patch
deleted file mode 100644
index 4e4fd07bda..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/storage/RegionFileStorage.java.patch
+++ /dev/null
@@ -1,67 +0,0 @@
---- a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
-+++ b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
-@@ -32,21 +32,22 @@
-         this.info = storageKey;
-     }
- 
--    private RegionFile getRegionFile(ChunkPos pos) throws IOException {
--        long i = ChunkPos.asLong(pos.getRegionX(), pos.getRegionZ());
-+    private RegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit
-+        long i = ChunkPos.asLong(chunkcoordintpair.getRegionX(), chunkcoordintpair.getRegionZ());
-         RegionFile regionfile = (RegionFile) this.regionCache.getAndMoveToFirst(i);
- 
-         if (regionfile != null) {
-             return regionfile;
-         } else {
--            if (this.regionCache.size() >= 256) {
-+            if (this.regionCache.size() >= io.papermc.paper.configuration.GlobalConfiguration.get().misc.regionFileCacheSize) { // Paper - Sanitise RegionFileCache and make configurable
-                 ((RegionFile) this.regionCache.removeLast()).close();
-             }
- 
-             FileUtil.createDirectoriesSafe(this.folder);
-             Path path = this.folder;
--            int j = pos.getRegionX();
--            Path path1 = path.resolve("r." + j + "." + pos.getRegionZ() + ".mca");
-+            int j = chunkcoordintpair.getRegionX();
-+            Path path1 = path.resolve("r." + j + "." + chunkcoordintpair.getRegionZ() + ".mca");
-+            if (existingOnly && !java.nio.file.Files.exists(path1)) return null; // CraftBukkit
-             RegionFile regionfile1 = new RegionFile(this.info, path1, this.folder, this.sync);
- 
-             this.regionCache.putAndMoveToFirst(i, regionfile1);
-@@ -56,7 +57,12 @@
- 
-     @Nullable
-     public CompoundTag read(ChunkPos pos) throws IOException {
--        RegionFile regionfile = this.getRegionFile(pos);
-+        // CraftBukkit start - SPIGOT-5680: There's no good reason to preemptively create files on read, save that for writing
-+        RegionFile regionfile = this.getRegionFile(pos, true);
-+        if (regionfile == null) {
-+            return null;
-+        }
-+        // CraftBukkit end
-         DataInputStream datainputstream = regionfile.getChunkDataInputStream(pos);
- 
-         CompoundTag nbttagcompound;
-@@ -96,7 +102,12 @@
-     }
- 
-     public void scanChunk(ChunkPos chunkPos, StreamTagVisitor scanner) throws IOException {
--        RegionFile regionfile = this.getRegionFile(chunkPos);
-+        // CraftBukkit start - SPIGOT-5680: There's no good reason to preemptively create files on read, save that for writing
-+        RegionFile regionfile = this.getRegionFile(chunkPos, true);
-+        if (regionfile == null) {
-+            return;
-+        }
-+        // CraftBukkit end
-         DataInputStream datainputstream = regionfile.getChunkDataInputStream(chunkPos);
- 
-         try {
-@@ -122,7 +133,7 @@
-     }
- 
-     protected void write(ChunkPos pos, @Nullable CompoundTag nbt) throws IOException {
--        RegionFile regionfile = this.getRegionFile(pos);
-+        RegionFile regionfile = this.getRegionFile(pos, false); // CraftBukkit
- 
-         if (nbt == null) {
-             regionfile.clear(pos);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/storage/SerializableChunkData.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/storage/SerializableChunkData.java.patch
deleted file mode 100644
index 15c1561f05..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/storage/SerializableChunkData.java.patch
+++ /dev/null
@@ -1,216 +0,0 @@
---- a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
-+++ b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
-@@ -76,7 +76,8 @@
- import net.minecraft.world.ticks.SavedTick;
- import org.slf4j.Logger;
- 
--public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chunkPos, int minSectionY, long lastUpdateTime, long inhabitedTime, ChunkStatus chunkStatus, @Nullable BlendingData.Packed blendingData, @Nullable BelowZeroRetrogen belowZeroRetrogen, UpgradeData upgradeData, @Nullable long[] carvingMask, Map<Heightmap.Types, long[]> heightmaps, ChunkAccess.PackedTicks packedTicks, ShortList[] postProcessingSections, boolean lightCorrect, List<SerializableChunkData.SectionData> sectionData, List<CompoundTag> entities, List<CompoundTag> blockEntities, CompoundTag structureData) {
-+// CraftBukkit - persistentDataContainer
-+public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chunkPos, int minSectionY, long lastUpdateTime, long inhabitedTime, ChunkStatus chunkStatus, @Nullable BlendingData.Packed blendingData, @Nullable BelowZeroRetrogen belowZeroRetrogen, UpgradeData upgradeData, @Nullable long[] carvingMask, Map<Heightmap.Types, long[]> heightmaps, ChunkAccess.PackedTicks packedTicks, ShortList[] postProcessingSections, boolean lightCorrect, List<SerializableChunkData.SectionData> sectionData, List<CompoundTag> entities, List<CompoundTag> blockEntities, CompoundTag structureData, @Nullable Tag persistentDataContainer) {
- 
-     public static final Codec<PalettedContainer<BlockState>> BLOCK_STATE_CODEC = PalettedContainer.codecRW(Block.BLOCK_STATE_REGISTRY, BlockState.CODEC, PalettedContainer.Strategy.SECTION_STATES, Blocks.AIR.defaultBlockState());
-     private static final Logger LOGGER = LogUtils.getLogger();
-@@ -90,13 +91,39 @@
-     public static final String SECTIONS_TAG = "sections";
-     public static final String BLOCK_LIGHT_TAG = "BlockLight";
-     public static final String SKY_LIGHT_TAG = "SkyLight";
-+    // Paper start - guard against serializing mismatching coordinates
-+    // TODO Note: This needs to be re-checked each update
-+    public static ChunkPos getChunkCoordinate(final CompoundTag chunkData) {
-+        final int dataVersion = ChunkStorage.getVersion(chunkData);
-+        if (dataVersion < 2842) { // Level tag is removed after this version
-+            final CompoundTag levelData = chunkData.getCompound("Level");
-+            return new ChunkPos(levelData.getInt("xPos"), levelData.getInt("zPos"));
-+        } else {
-+            return new ChunkPos(chunkData.getInt("xPos"), chunkData.getInt("zPos"));
-+        }
-+    }
-+    // Paper end - guard against serializing mismatching coordinates
- 
-+    // Paper start - Do not let the server load chunks from newer versions
-+    private static final int CURRENT_DATA_VERSION = net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion();
-+    private static final boolean JUST_CORRUPT_IT = Boolean.getBoolean("Paper.ignoreWorldDataVersion");
-+    // Paper end - Do not let the server load chunks from newer versions
-+
-     @Nullable
-     public static SerializableChunkData parse(LevelHeightAccessor world, RegistryAccess registryManager, CompoundTag nbt) {
-         if (!nbt.contains("Status", 8)) {
-             return null;
-         } else {
--            ChunkPos chunkcoordintpair = new ChunkPos(nbt.getInt("xPos"), nbt.getInt("zPos"));
-+            // Paper start - Do not let the server load chunks from newer versions
-+            if (nbt.contains("DataVersion", net.minecraft.nbt.Tag.TAG_ANY_NUMERIC)) {
-+                final int dataVersion = nbt.getInt("DataVersion");
-+                if (!JUST_CORRUPT_IT && dataVersion > CURRENT_DATA_VERSION) {
-+                    new RuntimeException("Server attempted to load chunk saved with newer version of minecraft! " + dataVersion + " > " + CURRENT_DATA_VERSION).printStackTrace();
-+                    System.exit(1);
-+                }
-+            }
-+            // Paper end - Do not let the server load chunks from newer versions
-+            ChunkPos chunkcoordintpair = new ChunkPos(nbt.getInt("xPos"), nbt.getInt("zPos")); // Paper - guard against serializing mismatching coordinates; diff on change, see ChunkSerializer#getChunkCoordinate
-             long i = nbt.getLong("LastUpdate");
-             long j = nbt.getLong("InhabitedTime");
-             ChunkStatus chunkstatus = ChunkStatus.byName(nbt.getString("Status"));
-@@ -110,7 +137,7 @@
-                 dataresult = BlendingData.Packed.CODEC.parse(NbtOps.INSTANCE, nbt.getCompound("blending_data"));
-                 logger = SerializableChunkData.LOGGER;
-                 Objects.requireNonNull(logger);
--                blendingdata_d = (BlendingData.Packed) dataresult.resultOrPartial(logger::error).orElse((Object) null);
-+                blendingdata_d = (BlendingData.Packed) ((DataResult<BlendingData.Packed>) dataresult).resultOrPartial(logger::error).orElse(null); // CraftBukkit - decompile error
-             } else {
-                 blendingdata_d = null;
-             }
-@@ -121,7 +148,7 @@
-                 dataresult = BelowZeroRetrogen.CODEC.parse(NbtOps.INSTANCE, nbt.getCompound("below_zero_retrogen"));
-                 logger = SerializableChunkData.LOGGER;
-                 Objects.requireNonNull(logger);
--                belowzeroretrogen = (BelowZeroRetrogen) dataresult.resultOrPartial(logger::error).orElse((Object) null);
-+                belowzeroretrogen = (BelowZeroRetrogen) ((DataResult<BelowZeroRetrogen>) dataresult).resultOrPartial(logger::error).orElse(null); // CraftBukkit - decompile error
-             } else {
-                 belowzeroretrogen = null;
-             }
-@@ -178,7 +205,7 @@
-             ListTag nbttaglist2 = nbt.getList("sections", 10);
-             List<SerializableChunkData.SectionData> list4 = new ArrayList(nbttaglist2.size());
-             Registry<Biome> iregistry = registryManager.lookupOrThrow(Registries.BIOME);
--            Codec<PalettedContainerRO<Holder<Biome>>> codec = makeBiomeCodec(iregistry);
-+            Codec<PalettedContainer<Holder<Biome>>> codec = makeBiomeCodecRW(iregistry); // CraftBukkit - read/write
- 
-             for (int i1 = 0; i1 < nbttaglist2.size(); ++i1) {
-                 CompoundTag nbttagcompound3 = nbttaglist2.getCompound(i1);
-@@ -196,17 +223,17 @@
-                         datapaletteblock = new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES);
-                     }
- 
--                    Object object;
-+                    PalettedContainer object; // CraftBukkit - read/write
- 
-                     if (nbttagcompound3.contains("biomes", 10)) {
--                        object = (PalettedContainerRO) codec.parse(NbtOps.INSTANCE, nbttagcompound3.getCompound("biomes")).promotePartial((s1) -> {
-+                        object = codec.parse(NbtOps.INSTANCE, nbttagcompound3.getCompound("biomes")).promotePartial((s1) -> { // CraftBukkit - read/write
-                             logErrors(chunkcoordintpair, b0, s1);
-                         }).getOrThrow(SerializableChunkData.ChunkReadException::new);
-                     } else {
-                         object = new PalettedContainer<>(iregistry.asHolderIdMap(), iregistry.getOrThrow(Biomes.PLAINS), PalettedContainer.Strategy.SECTION_BIOMES);
-                     }
- 
--                    chunksection = new LevelChunkSection(datapaletteblock, (PalettedContainerRO) object);
-+                    chunksection = new LevelChunkSection(datapaletteblock, (PalettedContainer) object); // CraftBukkit - read/write
-                 } else {
-                     chunksection = null;
-                 }
-@@ -217,7 +244,8 @@
-                 list4.add(new SerializableChunkData.SectionData(b0, chunksection, nibblearray, nibblearray1));
-             }
- 
--            return new SerializableChunkData(iregistry, chunkcoordintpair, world.getMinSectionY(), i, j, chunkstatus, blendingdata_d, belowzeroretrogen, chunkconverter, along, map, ichunkaccess_a, ashortlist, flag, list4, list2, list3, nbttagcompound2);
-+            // CraftBukkit - ChunkBukkitValues
-+            return new SerializableChunkData(iregistry, chunkcoordintpair, world.getMinSectionY(), i, j, chunkstatus, blendingdata_d, belowzeroretrogen, chunkconverter, along, map, ichunkaccess_a, ashortlist, flag, list4, list2, list3, nbttagcompound2, nbt.get("ChunkBukkitValues"));
-         }
-     }
- 
-@@ -287,7 +315,13 @@
-             if (this.chunkStatus.isOrAfter(ChunkStatus.INITIALIZE_LIGHT)) {
-                 protochunk.setLightEngine(levellightengine);
-             }
-+        }
-+
-+        // CraftBukkit start - load chunk persistent data from nbt - SPIGOT-6814: Already load PDC here to account for 1.17 to 1.18 chunk upgrading.
-+        if (this.persistentDataContainer instanceof CompoundTag) {
-+            ((ChunkAccess) object).persistentDataContainer.putAll((CompoundTag) this.persistentDataContainer);
-         }
-+        // CraftBukkit end
- 
-         ((ChunkAccess) object).setLightCorrect(this.lightCorrect);
-         EnumSet<Heightmap.Types> enumset = EnumSet.noneOf(Heightmap.Types.class);
-@@ -329,6 +363,13 @@
- 
-             while (iterator2.hasNext()) {
-                 nbttagcompound = (CompoundTag) iterator2.next();
-+                // Paper start - do not read tile entities positioned outside the chunk
-+                final BlockPos blockposition = BlockEntity.getPosFromTag(nbttagcompound);
-+                if ((blockposition.getX() >> 4) != chunkPos.x || (blockposition.getZ() >> 4) != chunkPos.z) {
-+                    LOGGER.warn("Tile entity serialized in chunk {} in world '{}' positioned at {} is located outside of the chunk", chunkPos, world.getWorld().getName(), blockposition);
-+                    continue;
-+                }
-+                // Paper end - do not read tile entities positioned outside the chunk
-                 protochunk1.setBlockEntityNbt(nbttagcompound);
-             }
- 
-@@ -348,6 +389,12 @@
-         return PalettedContainer.codecRO(biomeRegistry.asHolderIdMap(), biomeRegistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, biomeRegistry.getOrThrow(Biomes.PLAINS));
-     }
- 
-+    // CraftBukkit start - read/write
-+    private static Codec<PalettedContainer<Holder<Biome>>> makeBiomeCodecRW(Registry<Biome> iregistry) {
-+        return PalettedContainer.codecRW(iregistry.asHolderIdMap(), iregistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, iregistry.getOrThrow(Biomes.PLAINS));
-+    }
-+    // CraftBukkit end
-+
-     public static SerializableChunkData copyOf(ServerLevel world, ChunkAccess chunk) {
-         if (!chunk.canBeSerialized()) {
-             throw new IllegalArgumentException("Chunk can't be serialized: " + String.valueOf(chunk));
-@@ -419,7 +466,14 @@
-             });
-             CompoundTag nbttagcompound1 = packStructureData(StructurePieceSerializationContext.fromLevel(world), chunkcoordintpair, chunk.getAllStarts(), chunk.getAllReferences());
- 
--            return new SerializableChunkData(world.registryAccess().lookupOrThrow(Registries.BIOME), chunkcoordintpair, chunk.getMinSectionY(), world.getGameTime(), chunk.getInhabitedTime(), chunk.getPersistedStatus(), (BlendingData.Packed) Optionull.map(chunk.getBlendingData(), BlendingData::pack), chunk.getBelowZeroRetrogen(), chunk.getUpgradeData().copy(), along, map, ichunkaccess_a, ashortlist, chunk.isLightCorrect(), list, list2, list1, nbttagcompound1);
-+            // CraftBukkit start - store chunk persistent data in nbt
-+            CompoundTag persistentDataContainer = null;
-+            if (!chunk.persistentDataContainer.isEmpty()) { // SPIGOT-6814: Always save PDC to account for 1.17 to 1.18 chunk upgrading.
-+                persistentDataContainer = chunk.persistentDataContainer.toTagCompound();
-+            }
-+
-+            return new SerializableChunkData(world.registryAccess().lookupOrThrow(Registries.BIOME), chunkcoordintpair, chunk.getMinSectionY(), world.getGameTime(), chunk.getInhabitedTime(), chunk.getPersistedStatus(), (BlendingData.Packed) Optionull.map(chunk.getBlendingData(), BlendingData::pack), chunk.getBelowZeroRetrogen(), chunk.getUpgradeData().copy(), along, map, ichunkaccess_a, ashortlist, chunk.isLightCorrect(), list, list2, list1, nbttagcompound1, persistentDataContainer);
-+            // CraftBukkit end
-         }
-     }
- 
-@@ -432,7 +486,7 @@
-         nbttagcompound.putLong("LastUpdate", this.lastUpdateTime);
-         nbttagcompound.putLong("InhabitedTime", this.inhabitedTime);
-         nbttagcompound.putString("Status", BuiltInRegistries.CHUNK_STATUS.getKey(this.chunkStatus).toString());
--        DataResult dataresult;
-+        DataResult<Tag> dataresult; // CraftBukkit - decompile error
-         Logger logger;
- 
-         if (this.blendingData != null) {
-@@ -513,6 +567,11 @@
-         });
-         nbttagcompound.put("Heightmaps", nbttagcompound2);
-         nbttagcompound.put("structures", this.structureData);
-+        // CraftBukkit start - store chunk persistent data in nbt
-+        if (this.persistentDataContainer != null) { // SPIGOT-6814: Always save PDC to account for 1.17 to 1.18 chunk upgrading.
-+            nbttagcompound.put("ChunkBukkitValues", this.persistentDataContainer);
-+        }
-+        // CraftBukkit end
-         return nbttagcompound;
-     }
- 
-@@ -564,6 +623,13 @@
-                     chunk.setBlockEntityNbt(nbttagcompound);
-                 } else {
-                     BlockPos blockposition = BlockEntity.getPosFromTag(nbttagcompound);
-+                    // Paper start - do not read tile entities positioned outside the chunk
-+                    ChunkPos chunkPos = chunk.getPos();
-+                    if ((blockposition.getX() >> 4) != chunkPos.x || (blockposition.getZ() >> 4) != chunkPos.z) {
-+                        LOGGER.warn("Tile entity serialized in chunk " + chunkPos + " in world '" + world.getWorld().getName() + "' positioned at " + blockposition + " is located outside of the chunk");
-+                        continue;
-+                    }
-+                    // Paper end - do not read tile entities positioned outside the chunk
-                     BlockEntity tileentity = BlockEntity.loadStatic(blockposition, chunk.getBlockState(blockposition), nbttagcompound, world.registryAccess());
- 
-                     if (tileentity != null) {
-@@ -623,6 +689,12 @@
-                 StructureStart structurestart = StructureStart.loadStaticStart(context, nbttagcompound1.getCompound(s), worldSeed);
- 
-                 if (structurestart != null) {
-+                    // CraftBukkit start - load persistent data for structure start
-+                    net.minecraft.nbt.Tag persistentBase = nbttagcompound1.getCompound(s).get("StructureBukkitValues");
-+                    if (persistentBase instanceof CompoundTag) {
-+                        structurestart.persistentDataContainer.putAll((CompoundTag) persistentBase);
-+                    }
-+                    // CraftBukkit end
-                     map.put(structure, structurestart);
-                 }
-             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/dimension/end/DragonRespawnAnimation.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/dimension/end/DragonRespawnAnimation.java.patch
deleted file mode 100644
index bc18c1f95e..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/dimension/end/DragonRespawnAnimation.java.patch
+++ /dev/null
@@ -1,57 +0,0 @@
---- a/net/minecraft/world/level/dimension/end/DragonRespawnAnimation.java
-+++ b/net/minecraft/world/level/dimension/end/DragonRespawnAnimation.java
-@@ -12,6 +12,9 @@
- import net.minecraft.world.level.levelgen.feature.Feature;
- import net.minecraft.world.level.levelgen.feature.SpikeFeature;
- import net.minecraft.world.level.levelgen.feature.configurations.SpikeConfiguration;
-+// CraftBukkit start
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public enum DragonRespawnAnimation {
- 
-@@ -27,7 +30,7 @@
-                 entityendercrystal.setBeamTarget(blockposition1);
-             }
- 
--            fight.setRespawnStage(null.PREPARING_TO_SUMMON_PILLARS);
-+            fight.setRespawnStage(PREPARING_TO_SUMMON_PILLARS); // CraftBukkit - decompile error
-         }
-     },
-     PREPARING_TO_SUMMON_PILLARS {
-@@ -38,7 +41,7 @@
-                     world.levelEvent(3001, new BlockPos(0, 128, 0), 0);
-                 }
-             } else {
--                fight.setRespawnStage(null.SUMMONING_PILLARS);
-+                fight.setRespawnStage(SUMMONING_PILLARS); // CraftBukkit - decompile error
-             }
- 
-         }
-@@ -81,7 +84,7 @@
-                         Feature.END_SPIKE.place(worldgenfeatureendspikeconfiguration, world, world.getChunkSource().getGenerator(), RandomSource.create(), new BlockPos(worldgenender_spike.getCenterX(), 45, worldgenender_spike.getCenterZ()));
-                     }
-                 } else if (flag1) {
--                    fight.setRespawnStage(null.SUMMONING_DRAGON);
-+                    fight.setRespawnStage(SUMMONING_DRAGON); // CraftBukkit - decompile error
-                 }
-             }
- 
-@@ -94,7 +97,7 @@
-             EndCrystal entityendercrystal;
- 
-             if (tick >= 100) {
--                fight.setRespawnStage(null.END);
-+                fight.setRespawnStage(END); // CraftBukkit - decompile error
-                 fight.resetSpikeCrystals();
-                 iterator = crystals.iterator();
- 
-@@ -102,7 +105,7 @@
-                     entityendercrystal = (EndCrystal) iterator.next();
-                     entityendercrystal.setBeamTarget((BlockPos) null);
-                     world.explode(entityendercrystal, entityendercrystal.getX(), entityendercrystal.getY(), entityendercrystal.getZ(), 6.0F, Level.ExplosionInteraction.NONE);
--                    entityendercrystal.discard();
-+                    entityendercrystal.discard(EntityRemoveEvent.Cause.EXPLODE); // CraftBukkit - add Bukkit remove cause
-                 }
-             } else if (tick >= 80) {
-                 world.levelEvent(3001, new BlockPos(0, 128, 0), 0);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/entity/PersistentEntitySectionManager.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/entity/PersistentEntitySectionManager.java.patch
deleted file mode 100644
index 68a70b169a..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/entity/PersistentEntitySectionManager.java.patch
+++ /dev/null
@@ -1,276 +0,0 @@
---- a/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
-+++ b/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
-@@ -29,8 +29,13 @@
- import net.minecraft.util.CsvOutput;
- import net.minecraft.util.VisibleForDebug;
- import net.minecraft.world.entity.Entity;
--import net.minecraft.world.level.ChunkPos;
- import org.slf4j.Logger;
-+import net.minecraft.world.level.ChunkPos;
-+// CraftBukkit start
-+import net.minecraft.world.level.chunk.storage.EntityStorage;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.EntityRemoveEvent;
-+// CraftBukkit end
- 
- public class PersistentEntitySectionManager<T extends EntityAccess> implements AutoCloseable {
- 
-@@ -55,6 +60,16 @@
-         this.entityGetter = new LevelEntityGetterAdapter<>(this.visibleEntityStorage, this.sectionStorage);
-     }
- 
-+    // CraftBukkit start - add method to get all entities in chunk
-+    public List<Entity> getEntities(ChunkPos chunkCoordIntPair) {
-+        return this.sectionStorage.getExistingSectionsInChunk(chunkCoordIntPair.toLong()).flatMap(EntitySection::getEntities).map(entity -> (Entity) entity).collect(Collectors.toList());
-+    }
-+
-+    public boolean isPending(long pair) {
-+        return this.chunkLoadStatuses.get(pair) == ChunkLoadStatus.PENDING;
-+    }
-+    // CraftBukkit end
-+
-     void removeSectionIfEmpty(long sectionPos, EntitySection<T> section) {
-         if (section.isEmpty()) {
-             this.sectionStorage.remove(sectionPos);
-@@ -63,6 +78,7 @@
-     }
- 
-     private boolean addEntityUuid(T entity) {
-+        org.spigotmc.AsyncCatcher.catchOp("Entity add by UUID"); // Paper
-         if (!this.knownUuids.add(entity.getUUID())) {
-             PersistentEntitySectionManager.LOGGER.warn("UUID of added entity already exists: {}", entity);
-             return false;
-@@ -76,6 +92,17 @@
-     }
- 
-     private boolean addEntity(T entity, boolean existing) {
-+        org.spigotmc.AsyncCatcher.catchOp("Entity add"); // Paper
-+        // Paper start - chunk system hooks
-+        // I don't want to know why this is a generic type.
-+        Entity entityCasted = (Entity)entity;
-+        boolean wasRemoved = entityCasted.isRemoved();
-+        boolean screened = ca.spottedleaf.moonrise.common.util.ChunkSystem.screenEntity((net.minecraft.server.level.ServerLevel)entityCasted.level(), entityCasted, existing, true);
-+        if ((!wasRemoved && entityCasted.isRemoved()) || !screened) {
-+            // removed by callback
-+            return false;
-+        }
-+        // Paper end - chunk system hooks
-         if (!this.addEntityUuid(entity)) {
-             return false;
-         } else {
-@@ -119,19 +146,23 @@
-     }
- 
-     void startTicking(T entity) {
-+        org.spigotmc.AsyncCatcher.catchOp("Entity start ticking"); // Paper
-         this.callbacks.onTickingStart(entity);
-     }
- 
-     void stopTicking(T entity) {
-+        org.spigotmc.AsyncCatcher.catchOp("Entity stop ticking"); // Paper
-         this.callbacks.onTickingEnd(entity);
-     }
- 
-     void startTracking(T entity) {
-+        org.spigotmc.AsyncCatcher.catchOp("Entity start tracking"); // Paper
-         this.visibleEntityStorage.add(entity);
-         this.callbacks.onTrackingStart(entity);
-     }
- 
-     void stopTracking(T entity) {
-+        org.spigotmc.AsyncCatcher.catchOp("Entity stop tracking"); // Paper
-         this.callbacks.onTrackingEnd(entity);
-         this.visibleEntityStorage.remove(entity);
-     }
-@@ -143,6 +174,7 @@
-     }
- 
-     public void updateChunkStatus(ChunkPos chunkPos, Visibility trackingStatus) {
-+        org.spigotmc.AsyncCatcher.catchOp("Update chunk status"); // Paper
-         long i = chunkPos.toLong();
- 
-         if (trackingStatus == Visibility.HIDDEN) {
-@@ -187,6 +219,7 @@
-     }
- 
-     public void ensureChunkQueuedForLoad(long chunkPos) {
-+        org.spigotmc.AsyncCatcher.catchOp("Entity chunk save"); // Paper
-         PersistentEntitySectionManager.ChunkLoadStatus persistententitysectionmanager_b = (PersistentEntitySectionManager.ChunkLoadStatus) this.chunkLoadStatuses.get(chunkPos);
- 
-         if (persistententitysectionmanager_b == PersistentEntitySectionManager.ChunkLoadStatus.FRESH) {
-@@ -196,33 +229,42 @@
-     }
- 
-     private boolean storeChunkSections(long chunkPos, Consumer<T> action) {
--        PersistentEntitySectionManager.ChunkLoadStatus persistententitysectionmanager_b = (PersistentEntitySectionManager.ChunkLoadStatus) this.chunkLoadStatuses.get(chunkPos);
-+        // CraftBukkit start - add boolean for event call
-+        return this.storeChunkSections(chunkPos, action, false);
-+    }
- 
-+    private boolean storeChunkSections(long i, Consumer<T> consumer, boolean callEvent) {
-+        // CraftBukkit end
-+        PersistentEntitySectionManager.ChunkLoadStatus persistententitysectionmanager_b = (PersistentEntitySectionManager.ChunkLoadStatus) this.chunkLoadStatuses.get(i);
-+
-         if (persistententitysectionmanager_b == PersistentEntitySectionManager.ChunkLoadStatus.PENDING) {
-             return false;
-         } else {
--            List<T> list = (List) this.sectionStorage.getExistingSectionsInChunk(chunkPos).flatMap((entitysection) -> {
-+            List<T> list = (List) this.sectionStorage.getExistingSectionsInChunk(i).flatMap((entitysection) -> {
-                 return entitysection.getEntities().filter(EntityAccess::shouldBeSaved);
-             }).collect(Collectors.toList());
- 
-             if (list.isEmpty()) {
-                 if (persistententitysectionmanager_b == PersistentEntitySectionManager.ChunkLoadStatus.LOADED) {
--                    this.permanentStorage.storeEntities(new ChunkEntities<>(new ChunkPos(chunkPos), ImmutableList.of()));
-+                    if (callEvent) CraftEventFactory.callEntitiesUnloadEvent(((EntityStorage) this.permanentStorage).level, new ChunkPos(i), ImmutableList.of()); // CraftBukkit
-+                    this.permanentStorage.storeEntities(new ChunkEntities<>(new ChunkPos(i), ImmutableList.of()));
-                 }
- 
-                 return true;
-             } else if (persistententitysectionmanager_b == PersistentEntitySectionManager.ChunkLoadStatus.FRESH) {
--                this.requestChunkLoad(chunkPos);
-+                this.requestChunkLoad(i);
-                 return false;
-             } else {
--                this.permanentStorage.storeEntities(new ChunkEntities<>(new ChunkPos(chunkPos), list));
--                list.forEach(action);
-+                if (callEvent) CraftEventFactory.callEntitiesUnloadEvent(((EntityStorage) this.permanentStorage).level, new ChunkPos(i), list.stream().map(entity -> (Entity) entity).collect(Collectors.toList())); // CraftBukkit
-+                this.permanentStorage.storeEntities(new ChunkEntities<>(new ChunkPos(i), list));
-+                list.forEach(consumer);
-                 return true;
-             }
-         }
-     }
- 
-     private void requestChunkLoad(long chunkPos) {
-+        org.spigotmc.AsyncCatcher.catchOp("Entity chunk load request"); // Paper
-         this.chunkLoadStatuses.put(chunkPos, PersistentEntitySectionManager.ChunkLoadStatus.PENDING);
-         ChunkPos chunkcoordintpair = new ChunkPos(chunkPos);
-         CompletableFuture completablefuture = this.permanentStorage.loadEntities(chunkcoordintpair);
-@@ -236,9 +278,10 @@
-     }
- 
-     private boolean processChunkUnload(long chunkPos) {
-+        org.spigotmc.AsyncCatcher.catchOp("Entity chunk unload process"); // Paper
-         boolean flag = this.storeChunkSections(chunkPos, (entityaccess) -> {
-             entityaccess.getPassengersAndSelf().forEach(this::unloadEntity);
--        });
-+        }, true); // CraftBukkit - add boolean for event call
- 
-         if (!flag) {
-             return false;
-@@ -249,29 +292,35 @@
-     }
- 
-     private void unloadEntity(EntityAccess entity) {
--        entity.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK);
-+        entity.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK, EntityRemoveEvent.Cause.UNLOAD); // CraftBukkit - add Bukkit remove cause
-         entity.setLevelCallback(EntityInLevelCallback.NULL);
-     }
- 
-     private void processUnloads() {
--        this.chunksToUnload.removeIf((i) -> {
-+        this.chunksToUnload.removeIf((java.util.function.LongPredicate) (i) -> { // CraftBukkit - decompile error
-             return this.chunkVisibility.get(i) != Visibility.HIDDEN ? true : this.processChunkUnload(i);
-         });
-     }
- 
-     private void processPendingLoads() {
--        ChunkEntities chunkentities;
-+        org.spigotmc.AsyncCatcher.catchOp("Entity chunk process pending loads"); // Paper
-+        ChunkEntities<T> chunkentities; // CraftBukkit - decompile error
- 
-         while ((chunkentities = (ChunkEntities) this.loadingInbox.poll()) != null) {
-             chunkentities.getEntities().forEach((entityaccess) -> {
-                 this.addEntity(entityaccess, true);
-             });
-             this.chunkLoadStatuses.put(chunkentities.getPos().toLong(), PersistentEntitySectionManager.ChunkLoadStatus.LOADED);
-+            // CraftBukkit start - call entity load event
-+            List<Entity> entities = this.getEntities(chunkentities.getPos());
-+            CraftEventFactory.callEntitiesLoadEvent(((EntityStorage) this.permanentStorage).level, chunkentities.getPos(), entities);
-+            // CraftBukkit end
-         }
- 
-     }
- 
-     public void tick() {
-+        org.spigotmc.AsyncCatcher.catchOp("Entity manager tick"); // Paper
-         this.processPendingLoads();
-         this.processUnloads();
-     }
-@@ -292,7 +341,8 @@
-     }
- 
-     public void autoSave() {
--        this.getAllChunksToSave().forEach((i) -> {
-+        org.spigotmc.AsyncCatcher.catchOp("Entity manager autosave"); // Paper
-+        this.getAllChunksToSave().forEach((java.util.function.LongConsumer) (i) -> { // CraftBukkit - decompile error
-             boolean flag = this.chunkVisibility.get(i) == Visibility.HIDDEN;
- 
-             if (flag) {
-@@ -306,12 +356,13 @@
-     }
- 
-     public void saveAll() {
-+        org.spigotmc.AsyncCatcher.catchOp("Entity manager save"); // Paper
-         LongSet longset = this.getAllChunksToSave();
- 
-         while (!longset.isEmpty()) {
-             this.permanentStorage.flush(false);
-             this.processPendingLoads();
--            longset.removeIf((i) -> {
-+            longset.removeIf((java.util.function.LongPredicate) (i) -> { // CraftBukkit - decompile error
-                 boolean flag = this.chunkVisibility.get(i) == Visibility.HIDDEN;
- 
-                 return flag ? this.processChunkUnload(i) : this.storeChunkSections(i, (entityaccess) -> {
-@@ -323,7 +374,15 @@
-     }
- 
-     public void close() throws IOException {
--        this.saveAll();
-+        // CraftBukkit start - add save boolean
-+        this.close(true);
-+    }
-+
-+    public void close(boolean save) throws IOException {
-+        if (save) {
-+            this.saveAll();
-+        }
-+        // CraftBukkit end
-         this.permanentStorage.close();
-     }
- 
-@@ -350,7 +409,7 @@
-     public void dumpSections(Writer writer) throws IOException {
-         CsvOutput csvwriter = CsvOutput.builder().addColumn("x").addColumn("y").addColumn("z").addColumn("visibility").addColumn("load_status").addColumn("entity_count").build(writer);
- 
--        this.sectionStorage.getAllChunksWithExistingSections().forEach((i) -> {
-+        this.sectionStorage.getAllChunksWithExistingSections().forEach((java.util.function.LongConsumer) (i) -> { // CraftBukkit - decompile error
-             PersistentEntitySectionManager.ChunkLoadStatus persistententitysectionmanager_b = (PersistentEntitySectionManager.ChunkLoadStatus) this.chunkLoadStatuses.get(i);
- 
-             this.sectionStorage.getExistingSectionPositionsInChunk(i).forEach((j) -> {
-@@ -394,7 +453,7 @@
-         private EntitySection<T> currentSection;
- 
-         Callback(final EntityAccess entityaccess, final long i, final EntitySection entitysection) {
--            this.entity = entityaccess;
-+            this.entity = (T) entityaccess; // CraftBukkit - decompile error
-             this.currentSectionKey = i;
-             this.currentSection = entitysection;
-         }
-@@ -405,6 +464,7 @@
-             long i = SectionPos.asLong(blockposition);
- 
-             if (i != this.currentSectionKey) {
-+                org.spigotmc.AsyncCatcher.catchOp("Entity move"); // Paper
-                 Visibility visibility = this.currentSection.getStatus();
- 
-                 if (!this.currentSection.remove(this.entity)) {
-@@ -459,6 +519,7 @@
- 
-         @Override
-         public void onRemove(Entity.RemovalReason reason) {
-+            org.spigotmc.AsyncCatcher.catchOp("Entity remove"); // Paper
-             if (!this.currentSection.remove(this.entity)) {
-                 PersistentEntitySectionManager.LOGGER.warn("Entity {} wasn't found in section {} (destroying due to {})", new Object[]{this.entity, SectionPos.of(this.currentSectionKey), reason});
-             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/gameevent/DynamicGameEventListener.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/gameevent/DynamicGameEventListener.java.patch
deleted file mode 100644
index bcaf4b1a37..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/gameevent/DynamicGameEventListener.java.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/net/minecraft/world/level/gameevent/DynamicGameEventListener.java
-+++ b/net/minecraft/world/level/gameevent/DynamicGameEventListener.java
-@@ -41,7 +41,7 @@
- 
-     private static void ifChunkExists(LevelReader world, @Nullable SectionPos sectionPos, Consumer<GameEventListenerRegistry> dispatcherConsumer) {
-         if (sectionPos != null) {
--            ChunkAccess chunkAccess = world.getChunk(sectionPos.x(), sectionPos.z(), ChunkStatus.FULL, false);
-+            ChunkAccess chunkAccess = world.getChunkIfLoadedImmediately(sectionPos.getX(), sectionPos.getZ()); // Paper - Perf: can cause sync loads while completing a chunk, resulting in deadlock
-             if (chunkAccess != null) {
-                 dispatcherConsumer.accept(chunkAccess.getListenerRegistry(sectionPos.y()));
-             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/gameevent/GameEventDispatcher.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/gameevent/GameEventDispatcher.java.patch
deleted file mode 100644
index 25f012f615..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/gameevent/GameEventDispatcher.java.patch
+++ /dev/null
@@ -1,39 +0,0 @@
---- a/net/minecraft/world/level/gameevent/GameEventDispatcher.java
-+++ b/net/minecraft/world/level/gameevent/GameEventDispatcher.java
-@@ -11,6 +11,12 @@
- import net.minecraft.server.level.ServerLevel;
- import net.minecraft.world.level.chunk.LevelChunk;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.Bukkit;
-+import org.bukkit.craftbukkit.CraftGameEvent;
-+import org.bukkit.craftbukkit.util.CraftLocation;
-+import org.bukkit.event.world.GenericGameEvent;
-+// CraftBukkit end
- 
- public class GameEventDispatcher {
- 
-@@ -23,6 +29,14 @@
-     public void post(Holder<GameEvent> event, Vec3 emitterPos, GameEvent.Context emitter) {
-         int i = ((GameEvent) event.value()).notificationRadius();
-         BlockPos blockposition = BlockPos.containing(emitterPos);
-+        // CraftBukkit start
-+        GenericGameEvent event1 = new GenericGameEvent(CraftGameEvent.minecraftToBukkit(event.value()), CraftLocation.toBukkit(blockposition, this.level.getWorld()), (emitter.sourceEntity() == null) ? null : emitter.sourceEntity().getBukkitEntity(), i, !Bukkit.isPrimaryThread());
-+        this.level.getCraftServer().getPluginManager().callEvent(event1);
-+        if (event1.isCancelled()) {
-+            return;
-+        }
-+        i = event1.getRadius();
-+        // CraftBukkit end
-         int j = SectionPos.blockToSectionCoord(blockposition.getX() - i);
-         int k = SectionPos.blockToSectionCoord(blockposition.getY() - i);
-         int l = SectionPos.blockToSectionCoord(blockposition.getZ() - i);
-@@ -42,7 +56,7 @@
- 
-         for (int l1 = j; l1 <= i1; ++l1) {
-             for (int i2 = l; i2 <= k1; ++i2) {
--                LevelChunk chunk = this.level.getChunkSource().getChunkNow(l1, i2);
-+                LevelChunk chunk = (LevelChunk) this.level.getChunkIfLoadedImmediately(l1, i2); // Paper - Use getChunkIfLoadedImmediately
- 
-                 if (chunk != null) {
-                     for (int j2 = k; j2 <= j1; ++j2) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/gameevent/vibrations/VibrationSystem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/gameevent/vibrations/VibrationSystem.java.patch
deleted file mode 100644
index d96bdd56da..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/gameevent/vibrations/VibrationSystem.java.patch
+++ /dev/null
@@ -1,52 +0,0 @@
---- a/net/minecraft/world/level/gameevent/vibrations/VibrationSystem.java
-+++ b/net/minecraft/world/level/gameevent/vibrations/VibrationSystem.java
-@@ -30,6 +30,11 @@
- import net.minecraft.world.level.gameevent.PositionSource;
- import net.minecraft.world.phys.HitResult;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.CraftGameEvent;
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.event.block.BlockReceiveGameEvent;
-+// CraftBukkit end
- 
- public interface VibrationSystem {
- 
-@@ -233,7 +238,8 @@
-             if (callback.requiresAdjacentChunksToBeTicking() && !Ticker.areAdjacentChunksTicking(world, blockposition1)) {
-                 return false;
-             } else {
--                callback.onReceiveVibration(world, blockposition, vibration.gameEvent(), (Entity) vibration.getEntity(world).orElse((Object) null), (Entity) vibration.getProjectileOwner(world).orElse((Object) null), VibrationSystem.Listener.distanceBetweenInBlocks(blockposition, blockposition1));
-+                // CraftBukkit - decompile error
-+                callback.onReceiveVibration(world, blockposition, vibration.gameEvent(), (Entity) vibration.getEntity(world).orElse(null), (Entity) vibration.getProjectileOwner(world).orElse(null), VibrationSystem.Listener.distanceBetweenInBlocks(blockposition, blockposition1));
-                 listenerData.setCurrentVibration((VibrationInfo) null);
-                 return true;
-             }
-@@ -288,8 +294,14 @@
-                     return false;
-                 } else {
-                     Vec3 vec3d1 = (Vec3) optional.get();
--
--                    if (!vibrationsystem_d.canReceiveVibration(world, BlockPos.containing(emitterPos), event, emitter)) {
-+                    // CraftBukkit start
-+                    boolean defaultCancel = !vibrationsystem_d.canReceiveVibration(world, BlockPos.containing(emitterPos), event, emitter);
-+                    Entity entity = emitter.sourceEntity();
-+                    BlockReceiveGameEvent event1 = new BlockReceiveGameEvent(CraftGameEvent.minecraftToBukkit(event.value()), CraftBlock.at(world, BlockPos.containing(vec3d1)), (entity == null) ? null : entity.getBukkitEntity());
-+                    event1.setCancelled(defaultCancel);
-+                    world.getCraftServer().getPluginManager().callEvent(event1);
-+                    if (event1.isCancelled()) {
-+                        // CraftBukkit end
-                         return false;
-                     } else if (Listener.isOccluded(world, emitterPos, vec3d1)) {
-                         return false;
-@@ -341,8 +353,8 @@
-         public static Codec<VibrationSystem.Data> CODEC = RecordCodecBuilder.create((instance) -> {
-             return instance.group(VibrationInfo.CODEC.lenientOptionalFieldOf("event").forGetter((vibrationsystem_a) -> {
-                 return Optional.ofNullable(vibrationsystem_a.currentVibration);
--            }), VibrationSelector.CODEC.fieldOf("selector").forGetter(VibrationSystem.Data::getSelectionStrategy), ExtraCodecs.NON_NEGATIVE_INT.fieldOf("event_delay").orElse(0).forGetter(VibrationSystem.Data::getTravelTimeInTicks)).apply(instance, (optional, vibrationselector, integer) -> {
--                return new VibrationSystem.Data((VibrationInfo) optional.orElse((Object) null), vibrationselector, integer, true);
-+            }), VibrationSelector.CODEC.optionalFieldOf("selector").xmap(o -> o.orElseGet(VibrationSelector::new), Optional::of).forGetter(VibrationSystem.Data::getSelectionStrategy), ExtraCodecs.NON_NEGATIVE_INT.fieldOf("event_delay").orElse(0).forGetter(VibrationSystem.Data::getTravelTimeInTicks)).apply(instance, (optional, vibrationselector, integer) -> { // Paper - fix MapLike spam for missing "selector" in 1.19.2
-+                return new VibrationSystem.Data((VibrationInfo) optional.orElse(null), vibrationselector, integer, true); // CraftBukkit - decompile error
-             });
-         });
-         public static final String NBT_TAG_KEY = "listener";
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/DensityFunctions.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/DensityFunctions.java.patch
deleted file mode 100644
index 3f778b5424..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/DensityFunctions.java.patch
+++ /dev/null
@@ -1,52 +0,0 @@
---- a/net/minecraft/world/level/levelgen/DensityFunctions.java
-+++ b/net/minecraft/world/level/levelgen/DensityFunctions.java
-@@ -509,6 +509,16 @@
-         );
-         private static final float ISLAND_THRESHOLD = -0.9F;
-         private final SimplexNoise islandNoise;
-+        // Paper start - Perf: Optimize end generation
-+        private static final class NoiseCache {
-+            public long[] keys = new long[8192];
-+            public float[] values = new float[8192];
-+            public NoiseCache() {
-+                java.util.Arrays.fill(keys, Long.MIN_VALUE);
-+            }
-+        }
-+        private static final ThreadLocal<java.util.Map<SimplexNoise, NoiseCache>> noiseCache = ThreadLocal.withInitial(java.util.WeakHashMap::new);
-+        // Paper end - Perf: Optimize end generation
- 
-         public EndIslandDensityFunction(long seed) {
-             RandomSource randomSource = new LegacyRandomSource(seed);
-@@ -521,15 +531,29 @@
-             int j = z / 2;
-             int k = x % 2;
-             int l = z % 2;
--            float f = 100.0F - Mth.sqrt((float)(x * x + z * z)) * 8.0F;
-+            float f = 100.0F - Mth.sqrt((long) x * (long) x + (long) z * (long) z) * 8.0F; // Paper - cast ints to long to avoid integer overflow
-             f = Mth.clamp(f, -100.0F, 80.0F);
- 
-+            NoiseCache cache = noiseCache.get().computeIfAbsent(sampler, noiseKey -> new NoiseCache()); // Paper - Perf: Optimize end generation
-             for (int m = -12; m <= 12; m++) {
-                 for (int n = -12; n <= 12; n++) {
-                     long o = (long)(i + m);
-                     long p = (long)(j + n);
--                    if (o * o + p * p > 4096L && sampler.getValue((double)o, (double)p) < -0.9F) {
--                        float g = (Mth.abs((float)o) * 3439.0F + Mth.abs((float)p) * 147.0F) % 13.0F + 9.0F;
-+                    // Paper start - Perf: Optimize end generation by using a noise cache
-+                    long key = net.minecraft.world.level.ChunkPos.asLong((int) o, (int) p);
-+                    int index = (int) it.unimi.dsi.fastutil.HashCommon.mix(key) & 8191;
-+                    float g = Float.MIN_VALUE;
-+                    if (cache.keys[index] == key) {
-+                        g = cache.values[index];
-+                    } else {
-+                        if (o * o + p * p > 4096L && sampler.getValue((double)o, (double)p) < -0.9F) {
-+                            g = (Mth.abs((float)o) * 3439.0F + Mth.abs((float)p) * 147.0F) % 13.0F + 9.0F;
-+                        }
-+                        cache.keys[index] = key;
-+                        cache.values[index] = g;
-+                    }
-+                    if (g != Float.MIN_VALUE) {
-+                        // Paper end - Perf: Optimize end generation
-                         float h = (float)(k - m * 2);
-                         float q = (float)(l - n * 2);
-                         float r = 100.0F - Mth.sqrt(h * h + q * q) * g;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/FlatLevelSource.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/FlatLevelSource.java.patch
deleted file mode 100644
index da8a1b29d5..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/FlatLevelSource.java.patch
+++ /dev/null
@@ -1,38 +0,0 @@
---- a/net/minecraft/world/level/levelgen/FlatLevelSource.java
-+++ b/net/minecraft/world/level/levelgen/FlatLevelSource.java
-@@ -34,22 +34,28 @@
-     private final FlatLevelGeneratorSettings settings;
- 
-     public FlatLevelSource(FlatLevelGeneratorSettings config) {
--        FixedBiomeSource worldchunkmanagerhell = new FixedBiomeSource(config.getBiome());
-+        // CraftBukkit start
-+        // WorldChunkManagerHell worldchunkmanagerhell = new WorldChunkManagerHell(generatorsettingsflat.getBiome());
- 
--        Objects.requireNonNull(config);
--        super(worldchunkmanagerhell, Util.memoize(config::adjustGenerationSettings));
--        this.settings = config;
-+        // Objects.requireNonNull(generatorsettingsflat);
-+        this(config, new FixedBiomeSource(config.getBiome()));
-     }
- 
-+    public FlatLevelSource(FlatLevelGeneratorSettings generatorsettingsflat, net.minecraft.world.level.biome.BiomeSource worldchunkmanager) {
-+        super(worldchunkmanager, Util.memoize(generatorsettingsflat::adjustGenerationSettings));
-+        // CraftBukkit end
-+        this.settings = generatorsettingsflat;
-+    }
-+
-     @Override
--    public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> structureSetRegistry, RandomState noiseConfig, long seed) {
-+    public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> holderlookup, RandomState randomstate, long i, org.spigotmc.SpigotWorldConfig conf) { // Spigot
-         Stream<Holder<StructureSet>> stream = (Stream) this.settings.structureOverrides().map(HolderSet::stream).orElseGet(() -> {
--            return structureSetRegistry.listElements().map((holder_c) -> {
-+            return holderlookup.listElements().map((holder_c) -> {
-                 return holder_c;
-             });
-         });
- 
--        return ChunkGeneratorStructureState.createForFlat(noiseConfig, seed, this.biomeSource, stream);
-+        return ChunkGeneratorStructureState.createForFlat(randomstate, i, this.biomeSource, stream, conf); // Spigot
-     }
- 
-     @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java.patch
deleted file mode 100644
index e3e53c8dde..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java.patch
+++ /dev/null
@@ -1,7 +0,0 @@
---- a/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
-+++ b/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
-@@ -1,3 +1,4 @@
-+// keep
- package net.minecraft.world.level.levelgen;
- 
- import com.google.common.annotations.VisibleForTesting;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/PatrolSpawner.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/PatrolSpawner.java.patch
deleted file mode 100644
index 9d044b3016..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/PatrolSpawner.java.patch
+++ /dev/null
@@ -1,79 +0,0 @@
---- a/net/minecraft/world/level/levelgen/PatrolSpawner.java
-+++ b/net/minecraft/world/level/levelgen/PatrolSpawner.java
-@@ -24,6 +24,7 @@
- 
-     @Override
-     public int tick(ServerLevel world, boolean spawnMonsters, boolean spawnAnimals) {
-+        if (world.paperConfig().entities.behavior.pillagerPatrols.disable || world.paperConfig().entities.behavior.pillagerPatrols.spawnChance == 0) return 0; // Paper - Add option to disable pillager patrols & Pillager patrol spawn settings and per player options
-         if (!spawnMonsters) {
-             return 0;
-         } else if (!world.getGameRules().getBoolean(GameRules.RULE_DO_PATROL_SPAWNING)) {
-@@ -31,23 +32,51 @@
-         } else {
-             RandomSource randomsource = world.random;
- 
--            --this.nextTick;
--            if (this.nextTick > 0) {
-+            // Paper start - Pillager patrol spawn settings and per player options
-+            // Random player selection moved up for per player spawning and configuration
-+            int j = world.players().size();
-+            if (j < 1) {
-                 return 0;
-+            }
-+
-+            net.minecraft.server.level.ServerPlayer entityhuman = world.players().get(randomsource.nextInt(j));
-+            if (entityhuman.isSpectator()) {
-+                return 0;
-+            }
-+
-+            int patrolSpawnDelay;
-+            if (world.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.perPlayer) {
-+                --entityhuman.patrolSpawnDelay;
-+                patrolSpawnDelay = entityhuman.patrolSpawnDelay;
-             } else {
--                this.nextTick += 12000 + randomsource.nextInt(1200);
--                long i = world.getDayTime() / 24000L;
-+                this.nextTick--;
-+                patrolSpawnDelay = this.nextTick;
-+            }
- 
--                if (i >= 5L && world.isDay()) {
--                    if (randomsource.nextInt(5) != 0) {
-+            if (patrolSpawnDelay > 0) {
-+                return 0;
-+            } else {
-+                long days;
-+                if (world.paperConfig().entities.behavior.pillagerPatrols.start.perPlayer) {
-+                    days = entityhuman.getStats().getValue(net.minecraft.stats.Stats.CUSTOM.get(net.minecraft.stats.Stats.PLAY_TIME)) / 24000L; // PLAY_ONE_MINUTE is actually counting in ticks, a misnomer by Mojang
-+                } else {
-+                    days = world.getDayTime() / 24000L;
-+                }
-+                if (world.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.perPlayer) {
-+                    entityhuman.patrolSpawnDelay += world.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomsource.nextInt(1200);
-+                } else {
-+                    this.nextTick += world.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomsource.nextInt(1200);
-+                }
-+
-+                if (days >= world.paperConfig().entities.behavior.pillagerPatrols.start.day && world.isDay()) {
-+                    if (randomsource.nextDouble() >= world.paperConfig().entities.behavior.pillagerPatrols.spawnChance) {
-+                        // Paper end - Pillager patrol spawn settings and per player options
-                         return 0;
-                     } else {
--                        int j = world.players().size();
- 
-                         if (j < 1) {
-                             return 0;
-                         } else {
--                            Player entityhuman = (Player) world.players().get(randomsource.nextInt(j));
- 
-                             if (entityhuman.isSpectator()) {
-                                 return 0;
-@@ -116,7 +145,7 @@
- 
-                 entitymonsterpatrolling.setPos((double) pos.getX(), (double) pos.getY(), (double) pos.getZ());
-                 entitymonsterpatrolling.finalizeSpawn(world, world.getCurrentDifficultyAt(pos), EntitySpawnReason.PATROL, (SpawnGroupData) null);
--                world.addFreshEntityWithPassengers(entitymonsterpatrolling);
-+                world.addFreshEntityWithPassengers(entitymonsterpatrolling, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.PATROL); // CraftBukkit
-                 return true;
-             } else {
-                 return false;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/PhantomSpawner.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/PhantomSpawner.java.patch
deleted file mode 100644
index e3cf03e4f8..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/PhantomSpawner.java.patch
+++ /dev/null
@@ -1,68 +0,0 @@
---- a/net/minecraft/world/level/levelgen/PhantomSpawner.java
-+++ b/net/minecraft/world/level/levelgen/PhantomSpawner.java
-@@ -32,13 +32,22 @@
-         } else if (!world.getGameRules().getBoolean(GameRules.RULE_DOINSOMNIA)) {
-             return 0;
-         } else {
-+            // Paper start - Ability to control player's insomnia and phantoms
-+            if (world.paperConfig().entities.behavior.phantomsSpawnAttemptMaxSeconds <= 0) {
-+                return 0;
-+            }
-+            // Paper end - Ability to control player's insomnia and phantoms
-             RandomSource randomsource = world.random;
- 
-             --this.nextTick;
-             if (this.nextTick > 0) {
-                 return 0;
-             } else {
--                this.nextTick += (60 + randomsource.nextInt(60)) * 20;
-+                // Paper start - Ability to control player's insomnia and phantoms
-+                int spawnAttemptMinSeconds = world.paperConfig().entities.behavior.phantomsSpawnAttemptMinSeconds;
-+                int spawnAttemptMaxSeconds = world.paperConfig().entities.behavior.phantomsSpawnAttemptMaxSeconds;
-+                this.nextTick += (spawnAttemptMinSeconds + randomsource.nextInt(spawnAttemptMaxSeconds - spawnAttemptMinSeconds + 1)) * 20;
-+                // Paper end - Ability to control player's insomnia and phantoms
-                 if (world.getSkyDarken() < 5 && world.dimensionType().hasSkyLight()) {
-                     return 0;
-                 } else {
-@@ -48,7 +57,7 @@
-                     while (iterator.hasNext()) {
-                         ServerPlayer entityplayer = (ServerPlayer) iterator.next();
- 
--                        if (!entityplayer.isSpectator()) {
-+                        if (!entityplayer.isSpectator() && (!world.paperConfig().entities.behavior.phantomsDoNotSpawnOnCreativePlayers || !entityplayer.isCreative())) { // Paper - Add phantom creative and insomniac controls
-                             BlockPos blockposition = entityplayer.blockPosition();
- 
-                             if (!world.dimensionType().hasSkyLight() || blockposition.getY() >= world.getSeaLevel() && world.canSeeSky(blockposition)) {
-@@ -59,7 +68,7 @@
-                                     int j = Mth.clamp(serverstatisticmanager.getValue(Stats.CUSTOM.get(Stats.TIME_SINCE_REST)), 1, Integer.MAX_VALUE);
-                                     boolean flag2 = true;
- 
--                                    if (randomsource.nextInt(j) >= 72000) {
-+                                    if (randomsource.nextInt(j) >= world.paperConfig().entities.behavior.playerInsomniaStartTicks) { // Paper - Ability to control player's insomnia and phantoms
-                                         BlockPos blockposition1 = blockposition.above(20 + randomsource.nextInt(15)).east(-10 + randomsource.nextInt(21)).south(-10 + randomsource.nextInt(21));
-                                         BlockState iblockdata = world.getBlockState(blockposition1);
-                                         FluidState fluid = world.getFluidState(blockposition1);
-@@ -69,12 +78,22 @@
-                                             int k = 1 + randomsource.nextInt(difficultydamagescaler.getDifficulty().getId() + 1);
- 
-                                             for (int l = 0; l < k; ++l) {
-+                                                // Paper start - PhantomPreSpawnEvent
-+                                                com.destroystokyo.paper.event.entity.PhantomPreSpawnEvent event = new com.destroystokyo.paper.event.entity.PhantomPreSpawnEvent(io.papermc.paper.util.MCUtil.toLocation(world, blockposition1), entityplayer.getBukkitEntity(), org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.NATURAL);
-+                                                if (!event.callEvent()) {
-+                                                    if (event.shouldAbortSpawn()) {
-+                                                        break;
-+                                                    }
-+                                                    continue;
-+                                                }
-+                                                // Paper end - PhantomPreSpawnEvent
-                                                 Phantom entityphantom = (Phantom) EntityType.PHANTOM.create(world, EntitySpawnReason.NATURAL);
- 
-                                                 if (entityphantom != null) {
-+                                                    entityphantom.setSpawningEntity(entityplayer.getUUID()); // Paper - PhantomPreSpawnEvent
-                                                     entityphantom.moveTo(blockposition1, 0.0F, 0.0F);
-                                                     groupdataentity = entityphantom.finalizeSpawn(world, difficultydamagescaler, EntitySpawnReason.NATURAL, groupdataentity);
--                                                    world.addFreshEntityWithPassengers(entityphantom);
-+                                                    world.addFreshEntityWithPassengers(entityphantom, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.NATURAL); // CraftBukkit
-                                                     ++i;
-                                                 }
-                                             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java.patch
deleted file mode 100644
index d2cdde6d47..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java.patch
+++ /dev/null
@@ -1,72 +0,0 @@
---- a/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java
-+++ b/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java
-@@ -7,6 +7,11 @@
- import net.minecraft.world.level.block.Block;
- import net.minecraft.world.level.block.Blocks;
- import net.minecraft.world.level.levelgen.feature.configurations.NoneFeatureConfiguration;
-+// CraftBukkit start
-+import java.util.List;
-+import org.bukkit.block.BlockState;
-+import org.bukkit.event.world.PortalCreateEvent;
-+// CraftBukkit end
- 
- public class EndPlatformFeature extends Feature<NoneFeatureConfiguration> {
- 
-@@ -21,24 +26,51 @@
-     }
- 
-     public static void createEndPlatform(ServerLevelAccessor world, BlockPos pos, boolean breakBlocks) {
--        BlockPos.MutableBlockPos blockposition_mutableblockposition = pos.mutable();
-+        EndPlatformFeature.createEndPlatform(world, pos, breakBlocks, null);
-+        // CraftBukkit start
-+    }
- 
-+    public static void createEndPlatform(ServerLevelAccessor worldaccess, BlockPos blockposition, boolean flag, Entity entity) {
-+        org.bukkit.craftbukkit.util.BlockStateListPopulator blockList = new org.bukkit.craftbukkit.util.BlockStateListPopulator(worldaccess);
-+        // CraftBukkit end
-+        BlockPos.MutableBlockPos blockposition_mutableblockposition = blockposition.mutable();
-+
-         for (int i = -2; i <= 2; ++i) {
-             for (int j = -2; j <= 2; ++j) {
-                 for (int k = -1; k < 3; ++k) {
--                    BlockPos.MutableBlockPos blockposition_mutableblockposition1 = blockposition_mutableblockposition.set(pos).move(j, k, i);
-+                    BlockPos.MutableBlockPos blockposition_mutableblockposition1 = blockposition_mutableblockposition.set(blockposition).move(j, k, i);
-                     Block block = k == -1 ? Blocks.OBSIDIAN : Blocks.AIR;
- 
--                    if (!world.getBlockState(blockposition_mutableblockposition1).is(block)) {
--                        if (breakBlocks) {
--                            world.destroyBlock(blockposition_mutableblockposition1, true, (Entity) null);
-+                    // CraftBukkit start
-+                    if (!blockList.getBlockState(blockposition_mutableblockposition1).is(block)) {
-+                        if (flag) {
-+                            blockList.destroyBlock(blockposition_mutableblockposition1, true, (Entity) null);
-                         }
- 
--                        world.setBlock(blockposition_mutableblockposition1, block.defaultBlockState(), 3);
-+                        blockList.setBlock(blockposition_mutableblockposition1, block.defaultBlockState(), 3);
-+                        // CraftBukkit end
-                     }
-                 }
-             }
-         }
-+        // CraftBukkit start
-+        // SPIGOT-7746: Entity will only be null during world generation, which is async, so just generate without event
-+        if (entity != null) {
-+            org.bukkit.World bworld = worldaccess.getLevel().getWorld();
-+            PortalCreateEvent portalEvent = new PortalCreateEvent((List<BlockState>) (List) blockList.getList(), bworld, entity.getBukkitEntity(), org.bukkit.event.world.PortalCreateEvent.CreateReason.END_PLATFORM);
- 
-+            worldaccess.getLevel().getCraftServer().getPluginManager().callEvent(portalEvent);
-+            if (portalEvent.isCancelled()) {
-+                return;
-+            }
-+        }
-+
-+        // SPIGOT-7856: End platform not dropping items after replacing blocks
-+        if (flag) {
-+            blockList.getList().forEach((state) -> worldaccess.destroyBlock(state.getPosition(), true, null));
-+        }
-+        blockList.updateList();
-+        // CraftBukkit end
-+
-     }
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/feature/SpikeFeature.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/feature/SpikeFeature.java.patch
deleted file mode 100644
index 3db119d7ed..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/feature/SpikeFeature.java.patch
+++ /dev/null
@@ -1,10 +0,0 @@
---- a/net/minecraft/world/level/levelgen/feature/SpikeFeature.java
-+++ b/net/minecraft/world/level/levelgen/feature/SpikeFeature.java
-@@ -115,6 +115,7 @@
-             endCrystal.moveTo(
-                 (double)spike.getCenterX() + 0.5, (double)(spike.getHeight() + 1), (double)spike.getCenterZ() + 0.5, random.nextFloat() * 360.0F, 0.0F
-             );
-+            endCrystal.generatedByDragonFight = true; // Paper - Fix invulnerable end crystals
-             world.addFreshEntity(endCrystal);
-             BlockPos blockPos2 = endCrystal.blockPosition();
-             this.setBlock(world, blockPos2.below(), Blocks.BEDROCK.defaultBlockState());
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/feature/treedecorators/CocoaDecorator.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/feature/treedecorators/CocoaDecorator.java.patch
deleted file mode 100644
index 4df0e3925c..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/feature/treedecorators/CocoaDecorator.java.patch
+++ /dev/null
@@ -1,10 +0,0 @@
---- a/net/minecraft/world/level/levelgen/feature/treedecorators/CocoaDecorator.java
-+++ b/net/minecraft/world/level/levelgen/feature/treedecorators/CocoaDecorator.java
-@@ -26,6 +26,7 @@
- 
-     @Override
-     public void place(TreeDecorator.Context generator) {
-+        if (generator.logs().isEmpty()) return; // Paper - Fix crash when trying to generate without logs
-         RandomSource randomSource = generator.random();
-         if (!(randomSource.nextFloat() >= this.probability)) {
-             List<BlockPos> list = generator.logs();
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/LegacyStructureDataHandler.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/LegacyStructureDataHandler.java.patch
deleted file mode 100644
index 04607e1797..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/LegacyStructureDataHandler.java.patch
+++ /dev/null
@@ -1,32 +0,0 @@
---- a/net/minecraft/world/level/levelgen/structure/LegacyStructureDataHandler.java
-+++ b/net/minecraft/world/level/levelgen/structure/LegacyStructureDataHandler.java
-@@ -18,7 +18,7 @@
- import net.minecraft.resources.ResourceKey;
- import net.minecraft.util.datafix.DataFixTypes;
- import net.minecraft.world.level.ChunkPos;
--import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.dimension.LevelStem;
- import net.minecraft.world.level.storage.DimensionDataStorage;
- 
- public class LegacyStructureDataHandler {
-@@ -233,16 +233,16 @@
-         }
-     }
- 
--    public static LegacyStructureDataHandler getLegacyStructureHandler(ResourceKey<Level> world, @Nullable DimensionDataStorage persistentStateManager) {
--        if (world == Level.OVERWORLD) {
-+    public static LegacyStructureDataHandler getLegacyStructureHandler(ResourceKey<LevelStem> world, @Nullable DimensionDataStorage persistentStateManager) { // CraftBukkit
-+        if (world == LevelStem.OVERWORLD) { // CraftBukkit
-             return new LegacyStructureDataHandler(persistentStateManager, ImmutableList.of("Monument", "Stronghold", "Village", "Mineshaft", "Temple", "Mansion"), ImmutableList.of("Village", "Mineshaft", "Mansion", "Igloo", "Desert_Pyramid", "Jungle_Pyramid", "Swamp_Hut", "Stronghold", "Monument"));
-         } else {
-             ImmutableList immutablelist;
- 
--            if (world == Level.NETHER) {
-+            if (world == LevelStem.NETHER) { // CraftBukkit
-                 immutablelist = ImmutableList.of("Fortress");
-                 return new LegacyStructureDataHandler(persistentStateManager, immutablelist, immutablelist);
--            } else if (world == Level.END) {
-+            } else if (world == LevelStem.END) { // CraftBukkit
-                 immutablelist = ImmutableList.of("EndCity");
-                 return new LegacyStructureDataHandler(persistentStateManager, immutablelist, immutablelist);
-             } else {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/StructurePiece.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/StructurePiece.java.patch
deleted file mode 100644
index 918d4c34ed..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/StructurePiece.java.patch
+++ /dev/null
@@ -1,164 +0,0 @@
---- a/net/minecraft/world/level/levelgen/structure/StructurePiece.java
-+++ b/net/minecraft/world/level/levelgen/structure/StructurePiece.java
-@@ -29,8 +29,6 @@
- import net.minecraft.world.level.block.Mirror;
- import net.minecraft.world.level.block.Rotation;
- import net.minecraft.world.level.block.entity.BlockEntity;
--import net.minecraft.world.level.block.entity.ChestBlockEntity;
--import net.minecraft.world.level.block.entity.DispenserBlockEntity;
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.chunk.ChunkGenerator;
- import net.minecraft.world.level.levelgen.Heightmap;
-@@ -51,7 +49,7 @@
-     private Rotation rotation;
-     protected int genDepth;
-     private final StructurePieceType type;
--    private static final Set<Block> SHAPE_CHECK_BLOCKS = ImmutableSet.builder().add(Blocks.NETHER_BRICK_FENCE).add(Blocks.TORCH).add(Blocks.WALL_TORCH).add(Blocks.OAK_FENCE).add(Blocks.SPRUCE_FENCE).add(Blocks.DARK_OAK_FENCE).add(Blocks.PALE_OAK_FENCE).add(Blocks.ACACIA_FENCE).add(Blocks.BIRCH_FENCE).add(Blocks.JUNGLE_FENCE).add(Blocks.LADDER).add(Blocks.IRON_BARS).build();
-+    public static final Set<Block> SHAPE_CHECK_BLOCKS = ImmutableSet.<Block>builder().add(Blocks.NETHER_BRICK_FENCE).add(Blocks.TORCH).add(Blocks.WALL_TORCH).add(Blocks.OAK_FENCE).add(Blocks.SPRUCE_FENCE).add(Blocks.DARK_OAK_FENCE).add(Blocks.PALE_OAK_FENCE).add(Blocks.ACACIA_FENCE).add(Blocks.BIRCH_FENCE).add(Blocks.JUNGLE_FENCE).add(Blocks.LADDER).add(Blocks.IRON_BARS).build(); // CraftBukkit - decompile error / PAIL private -> public
- 
-     protected StructurePiece(StructurePieceType type, int length, BoundingBox boundingBox) {
-         this.type = type;
-@@ -80,13 +78,11 @@
-         CompoundTag nbttagcompound = new CompoundTag();
- 
-         nbttagcompound.putString("id", BuiltInRegistries.STRUCTURE_PIECE.getKey(this.getType()).toString());
--        DataResult dataresult = BoundingBox.CODEC.encodeStart(NbtOps.INSTANCE, this.boundingBox);
--        Logger logger = StructurePiece.LOGGER;
--
--        Objects.requireNonNull(logger);
--        dataresult.resultOrPartial(logger::error).ifPresent((nbtbase) -> {
--            nbttagcompound.put("BB", nbtbase);
-+        // CraftBukkit start - decompile error
-+        BoundingBox.CODEC.encodeStart(NbtOps.INSTANCE, this.boundingBox).resultOrPartial(Objects.requireNonNull(StructurePiece.LOGGER)::error).ifPresent((nbtbase) -> {
-+             nbttagcompound.put("BB", nbtbase);
-         });
-+        // CraftBukkit end
-         Direction enumdirection = this.getOrientation();
- 
-         nbttagcompound.putInt("O", enumdirection == null ? -1 : enumdirection.get2DDataValue());
-@@ -186,6 +182,11 @@
-                 }
- 
-                 world.setBlock(blockposition_mutableblockposition, block, 2);
-+                // CraftBukkit start - fluid handling is already done if we have a transformer generator access
-+                if (world instanceof org.bukkit.craftbukkit.util.TransformerGeneratorAccess) {
-+                    return;
-+                }
-+                // CraftBukkit end
-                 FluidState fluid = world.getFluidState(blockposition_mutableblockposition);
- 
-                 if (!fluid.isEmpty()) {
-@@ -195,10 +196,42 @@
-                 if (StructurePiece.SHAPE_CHECK_BLOCKS.contains(block.getBlock())) {
-                     world.getChunk(blockposition_mutableblockposition).markPosForPostprocessing(blockposition_mutableblockposition);
-                 }
-+
-+            }
-+        }
-+    }
-+
-+    // CraftBukkit start
-+    protected boolean placeCraftBlockEntity(ServerLevelAccessor worldAccess, BlockPos position, org.bukkit.craftbukkit.block.CraftBlockEntityState<?> craftBlockEntityState, int i) {
-+        if (worldAccess instanceof org.bukkit.craftbukkit.util.TransformerGeneratorAccess transformerAccess) {
-+            return transformerAccess.setCraftBlock(position, craftBlockEntityState, i);
-+        }
-+        boolean result = worldAccess.setBlock(position, craftBlockEntityState.getHandle(), i);
-+        BlockEntity tileEntity = worldAccess.getBlockEntity(position);
-+        if (tileEntity != null) {
-+            tileEntity.loadWithComponents(craftBlockEntityState.getSnapshotNBT(), worldAccess.registryAccess());
-+        }
-+        return result;
-+    }
- 
-+    protected void placeCraftSpawner(ServerLevelAccessor worldAccess, BlockPos position, org.bukkit.entity.EntityType entityType, int i) {
-+        // This method is used in structures that are generated by code and place spawners as they set the entity after the block was placed making it impossible for plugins to access that information
-+        org.bukkit.craftbukkit.block.CraftCreatureSpawner spawner = (org.bukkit.craftbukkit.block.CraftCreatureSpawner) org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(worldAccess, position, Blocks.SPAWNER.defaultBlockState(), null);
-+        spawner.setSpawnedType(entityType);
-+        this.placeCraftBlockEntity(worldAccess, position, spawner, i);
-+    }
-+
-+    protected void setCraftLootTable(ServerLevelAccessor worldAccess, BlockPos position, RandomSource randomSource, ResourceKey<LootTable> loottableKey) {
-+        // This method is used in structures that use data markers to a loot table to loot containers as otherwise plugins won't have access to that information.
-+        net.minecraft.world.level.block.entity.BlockEntity tileEntity = worldAccess.getBlockEntity(position);
-+        if (tileEntity instanceof net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity tileEntityLootable) {
-+            tileEntityLootable.setLootTable(loottableKey, randomSource.nextLong());
-+            if (worldAccess instanceof org.bukkit.craftbukkit.util.TransformerGeneratorAccess transformerAccess) {
-+                transformerAccess.setCraftBlock(position, (org.bukkit.craftbukkit.block.CraftBlockState) org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(worldAccess, position, tileEntity.getBlockState(), tileEntityLootable.saveWithFullMetadata(worldAccess.registryAccess())), 3);
-             }
-         }
-     }
-+    // CraftBukkit end
- 
-     protected boolean canBeReplaced(LevelReader world, int x, int y, int z, BoundingBox box) {
-         return true;
-@@ -393,12 +426,20 @@
-                 block = StructurePiece.reorient(world, pos, Blocks.CHEST.defaultBlockState());
-             }
- 
--            world.setBlock(pos, block, 2);
--            BlockEntity tileentity = world.getBlockEntity(pos);
-+            // CraftBukkit start
-+            /*
-+            worldaccess.setBlock(blockposition, iblockdata, 2);
-+            TileEntity tileentity = worldaccess.getBlockEntity(blockposition);
- 
--            if (tileentity instanceof ChestBlockEntity) {
--                ((ChestBlockEntity) tileentity).setLootTable(lootTable, random.nextLong());
-+            if (tileentity instanceof TileEntityChest) {
-+                ((TileEntityChest) tileentity).setLootTable(resourcekey, randomsource.nextLong());
-             }
-+            */
-+            org.bukkit.craftbukkit.block.CraftChest chestState = (org.bukkit.craftbukkit.block.CraftChest) org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(world, pos, block, null);
-+            chestState.setLootTable(org.bukkit.craftbukkit.CraftLootTable.minecraftToBukkit(lootTable));
-+            chestState.setSeed(random.nextLong());
-+            this.placeCraftBlockEntity(world, pos, chestState, 2);
-+            // CraftBukkit end
- 
-             return true;
-         } else {
-@@ -410,13 +451,32 @@
-         BlockPos.MutableBlockPos blockposition_mutableblockposition = this.getWorldPos(x, y, z);
- 
-         if (boundingBox.isInside(blockposition_mutableblockposition) && !world.getBlockState(blockposition_mutableblockposition).is(Blocks.DISPENSER)) {
--            this.placeBlock(world, (BlockState) Blocks.DISPENSER.defaultBlockState().setValue(DispenserBlock.FACING, facing), x, y, z, boundingBox);
--            BlockEntity tileentity = world.getBlockEntity(blockposition_mutableblockposition);
-+            // CraftBukkit start
-+            /*
-+            this.placeBlock(generatoraccessseed, (IBlockData) Blocks.DISPENSER.defaultBlockState().setValue(BlockDispenser.FACING, enumdirection), i, j, k, structureboundingbox);
-+            TileEntity tileentity = generatoraccessseed.getBlockEntity(blockposition_mutableblockposition);
- 
--            if (tileentity instanceof DispenserBlockEntity) {
--                ((DispenserBlockEntity) tileentity).setLootTable(lootTable, random.nextLong());
-+            if (tileentity instanceof TileEntityDispenser) {
-+                ((TileEntityDispenser) tileentity).setLootTable(resourcekey, randomsource.nextLong());
-             }
-+            */
-+            if (!this.canBeReplaced(world, x, y, z, boundingBox)) {
-+                return true;
-+            }
-+            BlockState iblockdata = Blocks.DISPENSER.defaultBlockState().setValue(DispenserBlock.FACING, facing);
-+            if (this.mirror != Mirror.NONE) {
-+                iblockdata = iblockdata.mirror(this.mirror);
-+            }
-+            if (this.rotation != Rotation.NONE) {
-+                iblockdata = iblockdata.rotate(this.rotation);
-+            }
- 
-+            org.bukkit.craftbukkit.block.CraftDispenser dispenserState = (org.bukkit.craftbukkit.block.CraftDispenser) org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(world, blockposition_mutableblockposition, iblockdata, null);
-+            dispenserState.setLootTable(org.bukkit.craftbukkit.CraftLootTable.minecraftToBukkit(lootTable));
-+            dispenserState.setSeed(random.nextLong());
-+            this.placeCraftBlockEntity(world, blockposition_mutableblockposition, dispenserState, 2);
-+            // CraftBukkit end
-+
-             return true;
-         } else {
-             return false;
-@@ -428,7 +488,7 @@
-     }
- 
-     public static BoundingBox createBoundingBox(Stream<StructurePiece> pieces) {
--        Stream stream1 = pieces.map(StructurePiece::getBoundingBox);
-+        Stream<BoundingBox> stream1 = pieces.map(StructurePiece::getBoundingBox); // CraftBukkit - decompile error
- 
-         Objects.requireNonNull(stream1);
-         return (BoundingBox) BoundingBox.encapsulatingBoxes(stream1::iterator).orElseThrow(() -> {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/StructureStart.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/StructureStart.java.patch
deleted file mode 100644
index 83539110a1..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/StructureStart.java.patch
+++ /dev/null
@@ -1,59 +0,0 @@
---- a/net/minecraft/world/level/levelgen/structure/StructureStart.java
-+++ b/net/minecraft/world/level/levelgen/structure/StructureStart.java
-@@ -32,6 +32,12 @@
-     @Nullable
-     private volatile BoundingBox cachedBoundingBox;
- 
-+    // CraftBukkit start
-+    private static final org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry();
-+    public org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer persistentDataContainer = new org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer(StructureStart.DATA_TYPE_REGISTRY);
-+    public org.bukkit.event.world.AsyncStructureGenerateEvent.Cause generationEventCause = org.bukkit.event.world.AsyncStructureGenerateEvent.Cause.WORLD_GENERATION;
-+    // CraftBukkit end
-+
-     public StructureStart(Structure structure, ChunkPos pos, int references, PiecesContainer children) {
-         this.structure = structure;
-         this.chunkPos = pos;
-@@ -91,15 +97,29 @@
-             BoundingBox structureboundingbox1 = ((StructurePiece) list.get(0)).boundingBox;
-             BlockPos blockposition = structureboundingbox1.getCenter();
-             BlockPos blockposition1 = new BlockPos(blockposition.getX(), structureboundingbox1.minY(), blockposition.getZ());
-+            // CraftBukkit start
-+            /*
-             Iterator iterator = list.iterator();
- 
-             while (iterator.hasNext()) {
-                 StructurePiece structurepiece = (StructurePiece) iterator.next();
- 
--                if (structurepiece.getBoundingBox().intersects(chunkBox)) {
--                    structurepiece.postProcess(world, structureAccessor, chunkGenerator, random, chunkBox, chunkPos, blockposition1);
-+                if (structurepiece.getBoundingBox().intersects(structureboundingbox)) {
-+                    structurepiece.postProcess(generatoraccessseed, structuremanager, chunkgenerator, randomsource, structureboundingbox, chunkcoordintpair, blockposition1);
-                 }
-             }
-+            */
-+            List<StructurePiece> pieces = list.stream().filter(piece -> piece.getBoundingBox().intersects(chunkBox)).toList();
-+            if (!pieces.isEmpty()) {
-+                org.bukkit.craftbukkit.util.TransformerGeneratorAccess transformerAccess = new org.bukkit.craftbukkit.util.TransformerGeneratorAccess();
-+                transformerAccess.setHandle(world);
-+                transformerAccess.setStructureTransformer(new org.bukkit.craftbukkit.util.CraftStructureTransformer(this.generationEventCause, world, structureAccessor, this.structure, chunkBox, chunkPos));
-+                for (StructurePiece piece : pieces) {
-+                    piece.postProcess(transformerAccess, structureAccessor, chunkGenerator, random, chunkBox, chunkPos, blockposition1);
-+                }
-+                transformerAccess.getStructureTransformer().discard();
-+            }
-+            // CraftBukkit end
- 
-             this.structure.afterPlace(world, structureAccessor, chunkGenerator, random, chunkBox, chunkPos, this.pieceContainer);
-         }
-@@ -107,6 +127,11 @@
- 
-     public CompoundTag createTag(StructurePieceSerializationContext context, ChunkPos chunkPos) {
-         CompoundTag nbttagcompound = new CompoundTag();
-+        // CraftBukkit start - store persistent data in nbt
-+        if (!this.persistentDataContainer.isEmpty()) {
-+            nbttagcompound.put("StructureBukkitValues", this.persistentDataContainer.toTagCompound());
-+        }
-+        // CraftBukkit end
- 
-         if (this.isValid()) {
-             nbttagcompound.putString("id", context.registryAccess().lookupOrThrow(Registries.STRUCTURE).getKey(this.structure).toString());
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/placement/StructurePlacement.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/placement/StructurePlacement.java.patch
deleted file mode 100644
index 8abda3baff..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/placement/StructurePlacement.java.patch
+++ /dev/null
@@ -1,93 +0,0 @@
---- a/net/minecraft/world/level/levelgen/structure/placement/StructurePlacement.java
-+++ b/net/minecraft/world/level/levelgen/structure/placement/StructurePlacement.java
-@@ -79,14 +79,30 @@
-         return this.exclusionZone;
-     }
- 
-+    @Deprecated @io.papermc.paper.annotation.DoNotUse // Paper - Add missing structure set seed configs
-     public boolean isStructureChunk(ChunkGeneratorStructureState calculator, int chunkX, int chunkZ) {
-+        // Paper start - Add missing structure set seed configs
-+        return this.isStructureChunk(calculator, chunkX, chunkZ, null);
-+    }
-+    public boolean isStructureChunk(ChunkGeneratorStructureState calculator, int chunkX, int chunkZ, @org.jetbrains.annotations.Nullable net.minecraft.resources.ResourceKey<StructureSet> structureSetKey) {
-+        Integer saltOverride = null;
-+        if (structureSetKey != null) {
-+            if (structureSetKey == net.minecraft.world.level.levelgen.structure.BuiltinStructureSets.MINESHAFTS) {
-+                saltOverride = calculator.conf.mineshaftSeed;
-+            } else if (structureSetKey == net.minecraft.world.level.levelgen.structure.BuiltinStructureSets.BURIED_TREASURES) {
-+                saltOverride = calculator.conf.buriedTreasureSeed;
-+            }
-+        }
-+        // Paper end - Add missing structure set seed configs
-         return this.isPlacementChunk(calculator, chunkX, chunkZ)
--            && this.applyAdditionalChunkRestrictions(chunkX, chunkZ, calculator.getLevelSeed())
-+            && this.applyAdditionalChunkRestrictions(chunkX, chunkZ, calculator.getLevelSeed(), saltOverride) // Paper - Add missing structure set seed configs
-             && this.applyInteractionsWithOtherStructures(calculator, chunkX, chunkZ);
-     }
- 
--    public boolean applyAdditionalChunkRestrictions(int chunkX, int chunkZ, long seed) {
--        return !(this.frequency < 1.0F) || this.frequencyReductionMethod.shouldGenerate(seed, this.salt, chunkX, chunkZ, this.frequency);
-+    // Paper start - Add missing structure set seed configs
-+    public boolean applyAdditionalChunkRestrictions(int chunkX, int chunkZ, long seed, @org.jetbrains.annotations.Nullable Integer saltOverride) {
-+        return !(this.frequency < 1.0F) || this.frequencyReductionMethod.shouldGenerate(seed, this.salt, chunkX, chunkZ, this.frequency, saltOverride);
-+        // Paper end - Add missing structure set seed configs
-     }
- 
-     public boolean applyInteractionsWithOtherStructures(ChunkGeneratorStructureState calculator, int centerChunkX, int centerChunkZ) {
-@@ -101,25 +117,31 @@
- 
-     public abstract StructurePlacementType<?> type();
- 
--    private static boolean probabilityReducer(long seed, int salt, int chunkX, int chunkZ, float frequency) {
-+    private static boolean probabilityReducer(long seed, int salt, int chunkX, int chunkZ, float frequency, @org.jetbrains.annotations.Nullable Integer saltOverride) { // Paper - Add missing structure set seed configs; ignore here
-         WorldgenRandom worldgenRandom = new WorldgenRandom(new LegacyRandomSource(0L));
-         worldgenRandom.setLargeFeatureWithSalt(seed, salt, chunkX, chunkZ);
-         return worldgenRandom.nextFloat() < frequency;
-     }
- 
--    private static boolean legacyProbabilityReducerWithDouble(long seed, int salt, int chunkX, int chunkZ, float frequency) {
-+    private static boolean legacyProbabilityReducerWithDouble(long seed, int salt, int chunkX, int chunkZ, float frequency, @org.jetbrains.annotations.Nullable Integer saltOverride) { // Paper - Add missing structure set seed configs
-         WorldgenRandom worldgenRandom = new WorldgenRandom(new LegacyRandomSource(0L));
-+        if (saltOverride == null) { // Paper - Add missing structure set seed configs
-         worldgenRandom.setLargeFeatureSeed(seed, chunkX, chunkZ);
-+        // Paper start - Add missing structure set seed configs
-+        } else {
-+            worldgenRandom.setLargeFeatureWithSalt(seed, chunkX, chunkZ, saltOverride);
-+        }
-+        // Paper end - Add missing structure set seed configs
-         return worldgenRandom.nextDouble() < (double)frequency;
-     }
- 
--    private static boolean legacyArbitrarySaltProbabilityReducer(long seed, int salt, int chunkX, int chunkZ, float frequency) {
-+    private static boolean legacyArbitrarySaltProbabilityReducer(long seed, int salt, int chunkX, int chunkZ, float frequency, @org.jetbrains.annotations.Nullable Integer saltOverride) { // Paper - Add missing structure set seed configs
-         WorldgenRandom worldgenRandom = new WorldgenRandom(new LegacyRandomSource(0L));
--        worldgenRandom.setLargeFeatureWithSalt(seed, chunkX, chunkZ, 10387320);
-+        worldgenRandom.setLargeFeatureWithSalt(seed, chunkX, chunkZ, saltOverride != null ? saltOverride : HIGHLY_ARBITRARY_RANDOM_SALT); // Paper - Add missing structure set seed configs
-         return worldgenRandom.nextFloat() < frequency;
-     }
- 
--    private static boolean legacyPillagerOutpostReducer(long seed, int salt, int chunkX, int chunkZ, float frequency) {
-+    private static boolean legacyPillagerOutpostReducer(long seed, int salt, int chunkX, int chunkZ, float frequency, @org.jetbrains.annotations.Nullable Integer saltOverride) { // Paper - Add missing structure set seed configs; ignore here
-         int i = chunkX >> 4;
-         int j = chunkZ >> 4;
-         WorldgenRandom worldgenRandom = new WorldgenRandom(new LegacyRandomSource(0L));
-@@ -147,7 +169,7 @@
- 
-     @FunctionalInterface
-     public interface FrequencyReducer {
--        boolean shouldGenerate(long seed, int salt, int chunkX, int chunkZ, float chance);
-+        boolean shouldGenerate(long seed, int salt, int chunkX, int chunkZ, float chance, @org.jetbrains.annotations.Nullable Integer saltOverride); // Paper - Add missing structure set seed configs
-     }
- 
-     public static enum FrequencyReductionMethod implements StringRepresentable {
-@@ -167,8 +189,8 @@
-             this.reducer = generationPredicate;
-         }
- 
--        public boolean shouldGenerate(long seed, int salt, int chunkX, int chunkZ, float chance) {
--            return this.reducer.shouldGenerate(seed, salt, chunkX, chunkZ, chance);
-+        public boolean shouldGenerate(long seed, int salt, int chunkX, int chunkZ, float chance, @org.jetbrains.annotations.Nullable Integer saltOverride) { // Paper - Add missing structure set seed configs
-+            return this.reducer.shouldGenerate(seed, salt, chunkX, chunkZ, chance, saltOverride); // Paper - Add missing structure set seed configs
-         }
- 
-         @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/EndCityPieces.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/EndCityPieces.java.patch
deleted file mode 100644
index 0ec21beb4f..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/EndCityPieces.java.patch
+++ /dev/null
@@ -1,16 +0,0 @@
---- a/net/minecraft/world/level/levelgen/structure/structures/EndCityPieces.java
-+++ b/net/minecraft/world/level/levelgen/structure/structures/EndCityPieces.java
-@@ -285,7 +285,12 @@
-                 BlockPos blockposition1 = pos.below();
- 
-                 if (boundingBox.isInside(blockposition1)) {
--                    RandomizableContainer.setBlockEntityLootTable(world, random, blockposition1, BuiltInLootTables.END_CITY_TREASURE);
-+                    // CraftBukkit start - ensure block transformation
-+                    /*
-+                    RandomizableContainer.setBlockEntityLootTable(worldaccess, randomsource, blockposition1, LootTables.END_CITY_TREASURE);
-+                    */
-+                    this.setCraftLootTable(world, blockposition1, random, BuiltInLootTables.END_CITY_TREASURE);
-+                    // CraftBukkit end
-                 }
-             } else if (boundingBox.isInside(pos) && Level.isInSpawnableBounds(pos)) {
-                 if (metadata.startsWith("Sentry")) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/IglooPieces.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/IglooPieces.java.patch
deleted file mode 100644
index 5dc0652fc4..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/IglooPieces.java.patch
+++ /dev/null
@@ -1,31 +0,0 @@
---- a/net/minecraft/world/level/levelgen/structure/structures/IglooPieces.java
-+++ b/net/minecraft/world/level/levelgen/structure/structures/IglooPieces.java
-@@ -14,8 +14,6 @@
- import net.minecraft.world.level.block.Blocks;
- import net.minecraft.world.level.block.Mirror;
- import net.minecraft.world.level.block.Rotation;
--import net.minecraft.world.level.block.entity.BlockEntity;
--import net.minecraft.world.level.block.entity.ChestBlockEntity;
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.chunk.ChunkGenerator;
- import net.minecraft.world.level.levelgen.Heightmap;
-@@ -86,11 +84,16 @@
-         protected void handleDataMarker(String metadata, BlockPos pos, ServerLevelAccessor world, RandomSource random, BoundingBox boundingBox) {
-             if ("chest".equals(metadata)) {
-                 world.setBlock(pos, Blocks.AIR.defaultBlockState(), 3);
--                BlockEntity tileentity = world.getBlockEntity(pos.below());
-+                // CraftBukkit start - ensure block transformation
-+                /*
-+                TileEntity tileentity = worldaccess.getBlockEntity(blockposition.below());
- 
--                if (tileentity instanceof ChestBlockEntity) {
--                    ((ChestBlockEntity) tileentity).setLootTable(BuiltInLootTables.IGLOO_CHEST, random.nextLong());
-+                if (tileentity instanceof TileEntityChest) {
-+                    ((TileEntityChest) tileentity).setLootTable(LootTables.IGLOO_CHEST, randomsource.nextLong());
-                 }
-+                */
-+                this.setCraftLootTable(world, pos.below(), random, BuiltInLootTables.IGLOO_CHEST);
-+                // CraftBukkit end
- 
-             }
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/MineshaftPieces.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/MineshaftPieces.java.patch
deleted file mode 100644
index 32925fee48..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/MineshaftPieces.java.patch
+++ /dev/null
@@ -1,68 +0,0 @@
---- a/net/minecraft/world/level/levelgen/structure/structures/MineshaftPieces.java
-+++ b/net/minecraft/world/level/levelgen/structure/structures/MineshaftPieces.java
-@@ -12,6 +12,7 @@
- import net.minecraft.core.Direction;
- import net.minecraft.nbt.CompoundTag;
- import net.minecraft.nbt.NbtOps;
-+import net.minecraft.nbt.Tag;
- import net.minecraft.resources.ResourceKey;
- import net.minecraft.tags.BiomeTags;
- import net.minecraft.util.RandomSource;
-@@ -30,8 +31,6 @@
- import net.minecraft.world.level.block.FenceBlock;
- import net.minecraft.world.level.block.RailBlock;
- import net.minecraft.world.level.block.WallTorchBlock;
--import net.minecraft.world.level.block.entity.BlockEntity;
--import net.minecraft.world.level.block.entity.SpawnerBlockEntity;
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.block.state.properties.RailShape;
- import net.minecraft.world.level.chunk.ChunkGenerator;
-@@ -520,14 +519,19 @@
- 
-                         if (chunkBox.isInside(blockposition_mutableblockposition) && this.isInterior(world, 1, 0, l, chunkBox)) {
-                             this.hasPlacedSpider = true;
--                            world.setBlock(blockposition_mutableblockposition, Blocks.SPAWNER.defaultBlockState(), 2);
--                            BlockEntity tileentity = world.getBlockEntity(blockposition_mutableblockposition);
--
--                            if (tileentity instanceof SpawnerBlockEntity) {
--                                SpawnerBlockEntity tileentitymobspawner = (SpawnerBlockEntity) tileentity;
-+                            // CraftBukkit start
-+                            /*
-+                            generatoraccessseed.setBlock(blockposition_mutableblockposition, Blocks.SPAWNER.defaultBlockState(), 2);
-+                            TileEntity tileentity = generatoraccessseed.getBlockEntity(blockposition_mutableblockposition);
- 
--                                tileentitymobspawner.setEntityId(EntityType.CAVE_SPIDER, random);
-+                            if (tileentity instanceof TileEntityMobSpawner) {
-+                                TileEntityMobSpawner tileentitymobspawner = (TileEntityMobSpawner) tileentity;
-+
-+                                tileentitymobspawner.setEntityId(EntityTypes.CAVE_SPIDER, randomsource);
-                             }
-+                            */
-+                            this.placeCraftSpawner(world, blockposition_mutableblockposition, org.bukkit.entity.EntityType.CAVE_SPIDER, 2);
-+                            // CraftBukkit end
-                         }
-                     }
-                 }
-@@ -819,11 +823,11 @@
- 
-         public MineShaftRoom(CompoundTag nbt) {
-             super(StructurePieceType.MINE_SHAFT_ROOM, nbt);
--            DataResult dataresult = BoundingBox.CODEC.listOf().parse(NbtOps.INSTANCE, nbt.getList("Entrances", 11));
-+            DataResult<List<BoundingBox>> dataresult = BoundingBox.CODEC.listOf().parse(NbtOps.INSTANCE, nbt.getList("Entrances", 11)); // CraftBukkit - decompile error
-             Logger logger = MineshaftPieces.LOGGER;
- 
-             Objects.requireNonNull(logger);
--            Optional optional = dataresult.resultOrPartial(logger::error);
-+            Optional<List<BoundingBox>> optional = dataresult.resultOrPartial(logger::error); // CraftBukkit - decompile error
-             List list = this.childEntranceBoxes;
- 
-             Objects.requireNonNull(this.childEntranceBoxes);
-@@ -929,7 +933,7 @@
-         @Override
-         protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag nbt) {
-             super.addAdditionalSaveData(context, nbt);
--            DataResult dataresult = BoundingBox.CODEC.listOf().encodeStart(NbtOps.INSTANCE, this.childEntranceBoxes);
-+            DataResult<Tag> dataresult = BoundingBox.CODEC.listOf().encodeStart(NbtOps.INSTANCE, this.childEntranceBoxes); // CraftBukkit - decompile error
-             Logger logger = MineshaftPieces.LOGGER;
- 
-             Objects.requireNonNull(logger);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/NetherFortressPieces.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/NetherFortressPieces.java.patch
deleted file mode 100644
index 23051dcaf5..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/NetherFortressPieces.java.patch
+++ /dev/null
@@ -1,45 +0,0 @@
---- a/net/minecraft/world/level/levelgen/structure/structures/NetherFortressPieces.java
-+++ b/net/minecraft/world/level/levelgen/structure/structures/NetherFortressPieces.java
-@@ -8,15 +8,12 @@
- import net.minecraft.core.Direction;
- import net.minecraft.nbt.CompoundTag;
- import net.minecraft.util.RandomSource;
--import net.minecraft.world.entity.EntityType;
- import net.minecraft.world.level.ChunkPos;
- import net.minecraft.world.level.StructureManager;
- import net.minecraft.world.level.WorldGenLevel;
- import net.minecraft.world.level.block.Blocks;
- import net.minecraft.world.level.block.FenceBlock;
- import net.minecraft.world.level.block.StairBlock;
--import net.minecraft.world.level.block.entity.BlockEntity;
--import net.minecraft.world.level.block.entity.SpawnerBlockEntity;
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.chunk.ChunkGenerator;
- import net.minecraft.world.level.levelgen.structure.BoundingBox;
-@@ -428,14 +425,19 @@
- 
-                 if (chunkBox.isInside(blockposition_mutableblockposition)) {
-                     this.hasPlacedSpawner = true;
--                    world.setBlock(blockposition_mutableblockposition, Blocks.SPAWNER.defaultBlockState(), 2);
--                    BlockEntity tileentity = world.getBlockEntity(blockposition_mutableblockposition);
--
--                    if (tileentity instanceof SpawnerBlockEntity) {
--                        SpawnerBlockEntity tileentitymobspawner = (SpawnerBlockEntity) tileentity;
--
--                        tileentitymobspawner.setEntityId(EntityType.BLAZE, random);
-+                    // CraftBukkit start
-+                    /*
-+                    generatoraccessseed.setBlock(blockposition_mutableblockposition, Blocks.SPAWNER.defaultBlockState(), 2);
-+                    TileEntity tileentity = generatoraccessseed.getBlockEntity(blockposition_mutableblockposition);
-+
-+                    if (tileentity instanceof TileEntityMobSpawner) {
-+                        TileEntityMobSpawner tileentitymobspawner = (TileEntityMobSpawner) tileentity;
-+
-+                        tileentitymobspawner.setEntityId(EntityTypes.BLAZE, randomsource);
-                     }
-+                    */
-+                    this.placeCraftSpawner(world, blockposition_mutableblockposition, org.bukkit.entity.EntityType.BLAZE, 2);
-+                    // CraftBukkit end
-                 }
-             }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/OceanRuinPieces.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/OceanRuinPieces.java.patch
deleted file mode 100644
index 7e1e399a8b..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/OceanRuinPieces.java.patch
+++ /dev/null
@@ -1,37 +0,0 @@
---- a/net/minecraft/world/level/levelgen/structure/structures/OceanRuinPieces.java
-+++ b/net/minecraft/world/level/levelgen/structure/structures/OceanRuinPieces.java
-@@ -27,8 +27,6 @@
- import net.minecraft.world.level.block.ChestBlock;
- import net.minecraft.world.level.block.Mirror;
- import net.minecraft.world.level.block.Rotation;
--import net.minecraft.world.level.block.entity.BlockEntity;
--import net.minecraft.world.level.block.entity.ChestBlockEntity;
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.chunk.ChunkGenerator;
- import net.minecraft.world.level.levelgen.Heightmap;
-@@ -200,12 +198,20 @@
-         @Override
-         protected void handleDataMarker(String metadata, BlockPos pos, ServerLevelAccessor world, RandomSource random, BoundingBox boundingBox) {
-             if ("chest".equals(metadata)) {
--                world.setBlock(pos, (BlockState) Blocks.CHEST.defaultBlockState().setValue(ChestBlock.WATERLOGGED, world.getFluidState(pos).is(FluidTags.WATER)), 2);
--                BlockEntity tileentity = world.getBlockEntity(pos);
--
--                if (tileentity instanceof ChestBlockEntity) {
--                    ((ChestBlockEntity) tileentity).setLootTable(this.isLarge ? BuiltInLootTables.UNDERWATER_RUIN_BIG : BuiltInLootTables.UNDERWATER_RUIN_SMALL, random.nextLong());
-+                // CraftBukkit start - transform block to ensure loot table is accessible
-+                /*
-+                worldaccess.setBlock(blockposition, (IBlockData) Blocks.CHEST.defaultBlockState().setValue(BlockChest.WATERLOGGED, worldaccess.getFluidState(blockposition).is(TagsFluid.WATER)), 2);
-+                TileEntity tileentity = worldaccess.getBlockEntity(blockposition);
-+
-+                if (tileentity instanceof TileEntityChest) {
-+                    ((TileEntityChest) tileentity).setLootTable(this.isLarge ? LootTables.UNDERWATER_RUIN_BIG : LootTables.UNDERWATER_RUIN_SMALL, randomsource.nextLong());
-                 }
-+                */
-+                org.bukkit.craftbukkit.block.CraftChest craftChest = (org.bukkit.craftbukkit.block.CraftChest) org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(world, pos, Blocks.CHEST.defaultBlockState().setValue(ChestBlock.WATERLOGGED, world.getFluidState(pos).is(FluidTags.WATER)), null);
-+                craftChest.setSeed(random.nextLong());
-+                craftChest.setLootTable(org.bukkit.craftbukkit.CraftLootTable.minecraftToBukkit(this.isLarge ? BuiltInLootTables.UNDERWATER_RUIN_BIG : BuiltInLootTables.UNDERWATER_RUIN_SMALL));
-+                this.placeCraftBlockEntity(world, pos, craftChest, 2);
-+                // CraftBukkit end
-             } else if ("drowned".equals(metadata)) {
-                 Drowned entitydrowned = (Drowned) EntityType.DROWNED.create(world.getLevel(), EntitySpawnReason.STRUCTURE);
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/ShipwreckPieces.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/ShipwreckPieces.java.patch
deleted file mode 100644
index d667e572c3..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/ShipwreckPieces.java.patch
+++ /dev/null
@@ -1,16 +0,0 @@
---- a/net/minecraft/world/level/levelgen/structure/structures/ShipwreckPieces.java
-+++ b/net/minecraft/world/level/levelgen/structure/structures/ShipwreckPieces.java
-@@ -79,7 +79,12 @@
-             ResourceKey<LootTable> resourcekey = (ResourceKey) ShipwreckPieces.MARKERS_TO_LOOT.get(metadata);
- 
-             if (resourcekey != null) {
--                RandomizableContainer.setBlockEntityLootTable(world, random, pos.below(), resourcekey);
-+                // CraftBukkit start - ensure block transformation
-+                /*
-+                RandomizableContainer.setBlockEntityLootTable(worldaccess, randomsource, blockposition.below(), resourcekey);
-+                */
-+                this.setCraftLootTable(world, pos.below(), random, resourcekey);
-+                // CraftBukkit end
-             }
- 
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/StrongholdPieces.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/StrongholdPieces.java.patch
deleted file mode 100644
index 90efca8745..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/StrongholdPieces.java.patch
+++ /dev/null
@@ -1,55 +0,0 @@
---- a/net/minecraft/world/level/levelgen/structure/structures/StrongholdPieces.java
-+++ b/net/minecraft/world/level/levelgen/structure/structures/StrongholdPieces.java
-@@ -8,7 +8,6 @@
- import net.minecraft.core.Direction;
- import net.minecraft.nbt.CompoundTag;
- import net.minecraft.util.RandomSource;
--import net.minecraft.world.entity.EntityType;
- import net.minecraft.world.level.ChunkPos;
- import net.minecraft.world.level.StructureManager;
- import net.minecraft.world.level.WorldGenLevel;
-@@ -22,8 +21,6 @@
- import net.minecraft.world.level.block.SlabBlock;
- import net.minecraft.world.level.block.StairBlock;
- import net.minecraft.world.level.block.WallTorchBlock;
--import net.minecraft.world.level.block.entity.BlockEntity;
--import net.minecraft.world.level.block.entity.SpawnerBlockEntity;
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
- import net.minecraft.world.level.block.state.properties.SlabType;
-@@ -53,7 +50,7 @@
-                 public boolean doPlace(int chainLength) {
-                     return super.doPlace(chainLength) && chainLength > 5;
-                 }
--            }};
-+            } }; // CraftBukkit - fix decompile styling
-     private static List<StrongholdPieces.PieceWeight> currentPieces;
-     static Class<? extends StrongholdPieces.StrongholdPiece> imposedPiece;
-     private static int totalWeight;
-@@ -1136,14 +1133,19 @@
- 
-                 if (chunkBox.isInside(blockposition_mutableblockposition)) {
-                     this.hasPlacedSpawner = true;
--                    world.setBlock(blockposition_mutableblockposition, Blocks.SPAWNER.defaultBlockState(), 2);
--                    BlockEntity tileentity = world.getBlockEntity(blockposition_mutableblockposition);
--
--                    if (tileentity instanceof SpawnerBlockEntity) {
--                        SpawnerBlockEntity tileentitymobspawner = (SpawnerBlockEntity) tileentity;
--
--                        tileentitymobspawner.setEntityId(EntityType.SILVERFISH, random);
-+                    // CraftBukkit start
-+                    /*
-+                    generatoraccessseed.setBlock(blockposition_mutableblockposition, Blocks.SPAWNER.defaultBlockState(), 2);
-+                    TileEntity tileentity = generatoraccessseed.getBlockEntity(blockposition_mutableblockposition);
-+
-+                    if (tileentity instanceof TileEntityMobSpawner) {
-+                        TileEntityMobSpawner tileentitymobspawner = (TileEntityMobSpawner) tileentity;
-+
-+                        tileentitymobspawner.setEntityId(EntityTypes.SILVERFISH, randomsource);
-                     }
-+                    */
-+                    this.placeCraftSpawner(world, blockposition_mutableblockposition, org.bukkit.entity.EntityType.SILVERFISH, 2);
-+                    // CraftBukkit end
-                 }
-             }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/SwampHutPiece.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/SwampHutPiece.java.patch
deleted file mode 100644
index ad3c0e1641..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/SwampHutPiece.java.patch
+++ /dev/null
@@ -1,20 +0,0 @@
---- a/net/minecraft/world/level/levelgen/structure/structures/SwampHutPiece.java
-+++ b/net/minecraft/world/level/levelgen/structure/structures/SwampHutPiece.java
-@@ -100,7 +100,7 @@
-                         entitywitch.setPersistenceRequired();
-                         entitywitch.moveTo((double) blockposition_mutableblockposition.getX() + 0.5D, (double) blockposition_mutableblockposition.getY(), (double) blockposition_mutableblockposition.getZ() + 0.5D, 0.0F, 0.0F);
-                         entitywitch.finalizeSpawn(world, world.getCurrentDifficultyAt(blockposition_mutableblockposition), EntitySpawnReason.STRUCTURE, (SpawnGroupData) null);
--                        world.addFreshEntityWithPassengers(entitywitch);
-+                        world.addFreshEntityWithPassengers(entitywitch, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.CHUNK_GEN); // CraftBukkit - add SpawnReason
-                     }
-                 }
-             }
-@@ -121,7 +121,7 @@
-                     entitycat.setPersistenceRequired();
-                     entitycat.moveTo((double) blockposition_mutableblockposition.getX() + 0.5D, (double) blockposition_mutableblockposition.getY(), (double) blockposition_mutableblockposition.getZ() + 0.5D, 0.0F, 0.0F);
-                     entitycat.finalizeSpawn(world, world.getCurrentDifficultyAt(blockposition_mutableblockposition), EntitySpawnReason.STRUCTURE, (SpawnGroupData) null);
--                    world.addFreshEntityWithPassengers(entitycat);
-+                    world.addFreshEntityWithPassengers(entitycat, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.CHUNK_GEN); // CraftBukkit - add SpawnReason
-                 }
-             }
-         }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate.java.patch
deleted file mode 100644
index 6bbbc67254..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate.java.patch
+++ /dev/null
@@ -1,182 +0,0 @@
---- a/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate.java
-+++ b/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate.java
-@@ -25,6 +25,7 @@
- import net.minecraft.nbt.IntTag;
- import net.minecraft.nbt.ListTag;
- import net.minecraft.nbt.NbtUtils;
-+import net.minecraft.nbt.Tag;
- import net.minecraft.resources.ResourceLocation;
- import net.minecraft.util.RandomSource;
- import net.minecraft.world.Clearable;
-@@ -55,6 +56,9 @@
- import net.minecraft.world.phys.Vec3;
- import net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape;
- import net.minecraft.world.phys.shapes.DiscreteVoxelShape;
-+import org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer;
-+import org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry;
-+// CraftBukkit end
- 
- public class StructureTemplate {
- 
-@@ -74,6 +78,11 @@
-     private Vec3i size;
-     private String author;
- 
-+    // CraftBukkit start - data containers
-+    private static final CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new CraftPersistentDataTypeRegistry();
-+    public CraftPersistentDataContainer persistentDataContainer = new CraftPersistentDataContainer(StructureTemplate.DATA_TYPE_REGISTRY);
-+    // CraftBukkit end
-+
-     public StructureTemplate() {
-         this.size = Vec3i.ZERO;
-         this.author = "?";
-@@ -147,7 +156,7 @@
-     }
- 
-     private static List<StructureTemplate.StructureBlockInfo> buildInfoList(List<StructureTemplate.StructureBlockInfo> fullBlocks, List<StructureTemplate.StructureBlockInfo> blocksWithNbt, List<StructureTemplate.StructureBlockInfo> otherBlocks) {
--        Comparator<StructureTemplate.StructureBlockInfo> comparator = Comparator.comparingInt((definedstructure_blockinfo) -> {
-+        Comparator<StructureTemplate.StructureBlockInfo> comparator = Comparator.<StructureTemplate.StructureBlockInfo>comparingInt((definedstructure_blockinfo) -> { // CraftBukkit - decompile error
-             return definedstructure_blockinfo.pos.getY();
-         }).thenComparingInt((definedstructure_blockinfo) -> {
-             return definedstructure_blockinfo.pos.getX();
-@@ -253,6 +262,19 @@
-         if (this.palettes.isEmpty()) {
-             return false;
-         } else {
-+            // CraftBukkit start
-+            // We only want the TransformerGeneratorAccess at certain locations because in here are many "block update" calls that shouldn't be transformed
-+            ServerLevelAccessor wrappedAccess = world;
-+            org.bukkit.craftbukkit.util.CraftStructureTransformer structureTransformer = null;
-+            if (wrappedAccess instanceof org.bukkit.craftbukkit.util.TransformerGeneratorAccess transformerAccess) {
-+                world = transformerAccess.getHandle();
-+                structureTransformer = transformerAccess.getStructureTransformer();
-+                // The structureTransformer is not needed if we can not transform blocks therefore we can save a little bit of performance doing this
-+                if (structureTransformer != null && !structureTransformer.canTransformBlocks()) {
-+                    structureTransformer = null;
-+                }
-+            }
-+            // CraftBukkit end
-             List<StructureTemplate.StructureBlockInfo> list = placementData.getRandomPalette(this.palettes, pos).blocks();
- 
-             if ((!list.isEmpty() || !placementData.isIgnoreEntities() && !this.entityInfoList.isEmpty()) && this.size.getX() >= 1 && this.size.getY() >= 1 && this.size.getZ() >= 1) {
-@@ -281,9 +303,27 @@
- 
-                         if (definedstructure_blockinfo.nbt != null) {
-                             tileentity = world.getBlockEntity(blockposition2);
--                            Clearable.tryClear(tileentity);
-+                            // Paper start - Fix NBT pieces overriding a block entity during worldgen deadlock
-+                            if (!(world instanceof net.minecraft.world.level.WorldGenLevel)) {
-+                                Clearable.tryClear(tileentity);
-+                            }
-+                            // Paper end - Fix NBT pieces overriding a block entity during worldgen deadlock
-                             world.setBlock(blockposition2, Blocks.BARRIER.defaultBlockState(), 20);
-                         }
-+                        // CraftBukkit start
-+                        if (structureTransformer != null) {
-+                            org.bukkit.craftbukkit.block.CraftBlockState craftBlockState = (org.bukkit.craftbukkit.block.CraftBlockState) org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(world, blockposition2, iblockdata, null);
-+                            if (definedstructure_blockinfo.nbt != null && craftBlockState instanceof org.bukkit.craftbukkit.block.CraftBlockEntityState<?> entityState) {
-+                                entityState.loadData(definedstructure_blockinfo.nbt);
-+                                if (craftBlockState instanceof org.bukkit.craftbukkit.block.CraftLootable<?> craftLootable) {
-+                                    craftLootable.setSeed(random.nextLong());
-+                                }
-+                            }
-+                            craftBlockState = structureTransformer.transformCraftState(craftBlockState);
-+                            iblockdata = craftBlockState.getHandle();
-+                            definedstructure_blockinfo = new StructureTemplate.StructureBlockInfo(blockposition2, iblockdata, (craftBlockState instanceof org.bukkit.craftbukkit.block.CraftBlockEntityState<?> craftBlockEntityState ? craftBlockEntityState.getSnapshotNBT() : null));
-+                        }
-+                        // CraftBukkit end
- 
-                         if (world.setBlock(blockposition2, iblockdata, flags)) {
-                             j = Math.min(j, blockposition2.getX());
-@@ -296,7 +336,7 @@
-                             if (definedstructure_blockinfo.nbt != null) {
-                                 tileentity = world.getBlockEntity(blockposition2);
-                                 if (tileentity != null) {
--                                    if (tileentity instanceof RandomizableContainer) {
-+                                    if (structureTransformer == null && tileentity instanceof RandomizableContainer) { // CraftBukkit - only process if don't have a transformer access (Was already set above) - SPIGOT-7520: Use structureTransformer as check, so that it is the same as above
-                                         definedstructure_blockinfo.nbt.putLong("LootTableSeed", random.nextLong());
-                                     }
- 
-@@ -394,14 +434,18 @@
-                         if (pair1.getSecond() != null) {
-                             tileentity = world.getBlockEntity(blockposition6);
-                             if (tileentity != null) {
--                                tileentity.setChanged();
-+                                // Paper start - Fix NBT pieces overriding a block entity during worldgen deadlock
-+                                if (!(world instanceof net.minecraft.world.level.WorldGenLevel)) {
-+                                    tileentity.setChanged();
-+                                }
-+                                // Paper end - Fix NBT pieces overriding a block entity during worldgen deadlock
-                             }
-                         }
-                     }
-                 }
- 
-                 if (!placementData.isIgnoreEntities()) {
--                    this.placeEntities(world, pos, placementData.getMirror(), placementData.getRotation(), placementData.getRotationPivot(), structureboundingbox, placementData.shouldFinalizeEntities());
-+                    this.placeEntities(wrappedAccess, pos, placementData.getMirror(), placementData.getRotation(), placementData.getRotationPivot(), structureboundingbox, placementData.shouldFinalizeEntities()); // CraftBukkit
-                 }
- 
-                 return true;
-@@ -503,11 +547,13 @@
-     }
- 
-     private static Optional<Entity> createEntityIgnoreException(ServerLevelAccessor world, CompoundTag nbt) {
--        try {
--            return EntityType.create(nbt, world.getLevel(), EntitySpawnReason.STRUCTURE);
--        } catch (Exception exception) {
--            return Optional.empty();
--        }
-+        // CraftBukkit start
-+        // try {
-+            return EntityType.create(nbt, world.getLevel(), EntitySpawnReason.STRUCTURE, true); // Paper - Don't fire sync event during generation
-+        // } catch (Exception exception) {
-+            // return Optional.empty();
-+        // }
-+        // CraftBukkit end
-     }
- 
-     public Vec3i getSize(Rotation rotation) {
-@@ -721,6 +767,11 @@
- 
-         nbt.put("entities", nbttaglist3);
-         nbt.put("size", this.newIntegerList(this.size.getX(), this.size.getY(), this.size.getZ()));
-+        // CraftBukkit start - PDC
-+        if (!this.persistentDataContainer.isEmpty()) {
-+            nbt.put("BukkitValues", this.persistentDataContainer.toTagCompound());
-+        }
-+        // CraftBukkit end
-         return NbtUtils.addCurrentDataVersion(nbt);
-     }
- 
-@@ -760,6 +811,12 @@
-             }
-         }
- 
-+        // CraftBukkit start - PDC
-+        Tag base = nbt.get("BukkitValues");
-+        if (base instanceof CompoundTag) {
-+            this.persistentDataContainer.putAll((CompoundTag) base);
-+        }
-+        // CraftBukkit end
-     }
- 
-     private void loadPalette(HolderGetter<Block> blockLookup, ListTag palette, ListTag blocks) {
-@@ -840,7 +897,7 @@
-     public static final class Palette {
- 
-         private final List<StructureTemplate.StructureBlockInfo> blocks;
--        private final Map<Block, List<StructureTemplate.StructureBlockInfo>> cache = Maps.newHashMap();
-+        private final Map<Block, List<StructureTemplate.StructureBlockInfo>> cache = Maps.newConcurrentMap(); // Paper - Fix CME due to this collection being shared across threads
-         @Nullable
-         private List<StructureTemplate.JigsawBlockInfo> cachedJigsaws;
- 
-@@ -924,7 +981,7 @@
-         public BlockState stateFor(int id) {
-             BlockState iblockdata = (BlockState) this.ids.byId(id);
- 
--            return iblockdata == null ? StructureTemplate.SimplePalette.DEFAULT_BLOCK_STATE : iblockdata;
-+            return iblockdata == null ? SimplePalette.DEFAULT_BLOCK_STATE : iblockdata; // CraftBukkit - decompile error
-         }
- 
-         public Iterator<BlockState> iterator() {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/material/FlowingFluid.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/material/FlowingFluid.java.patch
deleted file mode 100644
index cfc052e612..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/material/FlowingFluid.java.patch
+++ /dev/null
@@ -1,157 +0,0 @@
---- a/net/minecraft/world/level/material/FlowingFluid.java
-+++ b/net/minecraft/world/level/material/FlowingFluid.java
-@@ -31,6 +31,14 @@
- import net.minecraft.world.phys.Vec3;
- import net.minecraft.world.phys.shapes.Shapes;
- import net.minecraft.world.phys.shapes.VoxelShape;
-+// CraftBukkit start
-+import org.bukkit.block.BlockFace;
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.craftbukkit.block.data.CraftBlockData;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.block.BlockFromToEvent;
-+import org.bukkit.event.block.FluidLevelChangeEvent;
-+// CraftBukkit end
- 
- public abstract class FlowingFluid extends Fluid {
- 
-@@ -135,6 +143,15 @@
-                 Fluid fluidtype = fluid2.getType();
- 
-                 if (fluid1.canBeReplacedWith(world, blockposition1, fluidtype, Direction.DOWN) && FlowingFluid.canHoldSpecificFluid(world, blockposition1, iblockdata1, fluidtype)) {
-+                    // CraftBukkit start
-+                    org.bukkit.block.Block source = CraftBlock.at(world, fluidPos);
-+                    BlockFromToEvent event = new BlockFromToEvent(source, BlockFace.DOWN);
-+                    world.getCraftServer().getPluginManager().callEvent(event);
-+
-+                    if (event.isCancelled()) {
-+                        return;
-+                    }
-+                    // CraftBukkit end
-                     this.spreadTo(world, blockposition1, iblockdata1, Direction.DOWN, fluid2);
-                     if (this.sourceNeighborCount(world, fluidPos) >= 3) {
-                         this.spreadToSides(world, fluidPos, fluidState, blockState);
-@@ -167,8 +184,19 @@
-                 Direction enumdirection = (Direction) entry.getKey();
-                 FluidState fluid1 = (FluidState) entry.getValue();
-                 BlockPos blockposition1 = pos.relative(enumdirection);
-+                final BlockState blockStateIfLoaded = world.getBlockStateIfLoaded(blockposition1); // Paper - Prevent chunk loading from fluid flowing
-+                if (blockStateIfLoaded == null) continue; // Paper - Prevent chunk loading from fluid flowing
- 
--                this.spreadTo(world, blockposition1, world.getBlockState(blockposition1), enumdirection, fluid1);
-+                // CraftBukkit start
-+                org.bukkit.block.Block source = CraftBlock.at(world, pos);
-+                BlockFromToEvent event = new BlockFromToEvent(source, org.bukkit.craftbukkit.block.CraftBlock.notchToBlockFace(enumdirection));
-+                world.getCraftServer().getPluginManager().callEvent(event);
-+
-+                if (event.isCancelled()) {
-+                    continue;
-+                }
-+                // CraftBukkit end
-+                this.spreadTo(world, blockposition1, blockStateIfLoaded, enumdirection, fluid1); // Paper - Prevent chunk loading from fluid flowing
-             }
- 
-         }
-@@ -183,7 +211,8 @@
-         while (iterator.hasNext()) {
-             Direction enumdirection = (Direction) iterator.next();
-             BlockPos.MutableBlockPos blockposition_mutableblockposition1 = blockposition_mutableblockposition.setWithOffset(pos, enumdirection);
--            BlockState iblockdata1 = world.getBlockState(blockposition_mutableblockposition1);
-+            BlockState iblockdata1 = world.getBlockStateIfLoaded(blockposition_mutableblockposition1); // Paper - Prevent chunk loading from fluid flowing
-+            if (iblockdata1 == null) continue; // Paper - Prevent chunk loading from fluid flowing
-             FluidState fluid = iblockdata1.getFluidState();
- 
-             if (fluid.getType().isSame(this) && FlowingFluid.canPassThroughWall(enumdirection, world, pos, state, blockposition_mutableblockposition1, iblockdata1)) {
-@@ -287,7 +316,7 @@
-             ifluidcontainer.placeLiquid(world, pos, state, fluidState);
-         } else {
-             if (!state.isAir()) {
--                this.beforeDestroyingBlock(world, pos, state);
-+                this.beforeDestroyingBlock(world, pos, state, pos.relative(direction.getOpposite())); // Paper - Add BlockBreakBlockEvent
-             }
- 
-             world.setBlock(pos, fluidState.createLegacyBlock(), 3);
-@@ -295,6 +324,7 @@
- 
-     }
- 
-+    protected void beforeDestroyingBlock(LevelAccessor world, BlockPos pos, BlockState state, BlockPos source) { beforeDestroyingBlock(world, pos, state); } // Paper - Add BlockBreakBlockEvent
-     protected abstract void beforeDestroyingBlock(LevelAccessor world, BlockPos pos, BlockState state);
- 
-     protected int getSlopeDistance(LevelReader world, BlockPos pos, int i, Direction direction, BlockState state, FlowingFluid.SpreadContext spreadCache) {
-@@ -306,7 +336,8 @@
- 
-             if (enumdirection1 != direction) {
-                 BlockPos blockposition1 = pos.relative(enumdirection1);
--                BlockState iblockdata1 = spreadCache.getBlockState(blockposition1);
-+                BlockState iblockdata1 = spreadCache.getBlockStateIfLoaded(blockposition1); // Paper - Prevent chunk loading from fluid flowing
-+                if (iblockdata1 == null) continue; // Paper - Prevent chunk loading from fluid flowing
-                 FluidState fluid = iblockdata1.getFluidState();
- 
-                 if (this.canPassThrough(world, this.getFlowing(), pos, state, enumdirection1, blockposition1, iblockdata1, fluid)) {
-@@ -372,7 +403,8 @@
-         while (iterator.hasNext()) {
-             Direction enumdirection = (Direction) iterator.next();
-             BlockPos blockposition1 = pos.relative(enumdirection);
--            BlockState iblockdata1 = world.getBlockState(blockposition1);
-+            BlockState iblockdata1 = world.getBlockStateIfLoaded(blockposition1); // Paper - Prevent chunk loading from fluid flowing
-+            if (iblockdata1 == null) continue; // Paper - Prevent chunk loading from fluid flowing
-             FluidState fluid = iblockdata1.getFluidState();
- 
-             if (this.canMaybePassThrough(world, pos, state, enumdirection, blockposition1, iblockdata1, fluid)) {
-@@ -444,10 +476,24 @@
-             if (fluid1.isEmpty()) {
-                 fluidState = fluid1;
-                 blockState = Blocks.AIR.defaultBlockState();
-+                // CraftBukkit start
-+                FluidLevelChangeEvent event = CraftEventFactory.callFluidLevelChangeEvent(world, pos, blockState);
-+                if (event.isCancelled()) {
-+                    return;
-+                }
-+                blockState = ((CraftBlockData) event.getNewData()).getState();
-+                // CraftBukkit end
-                 world.setBlock(pos, blockState, 3);
-             } else if (!fluid1.equals(fluidState)) {
-                 fluidState = fluid1;
-                 blockState = fluid1.createLegacyBlock();
-+                // CraftBukkit start
-+                FluidLevelChangeEvent event = CraftEventFactory.callFluidLevelChangeEvent(world, pos, blockState);
-+                if (event.isCancelled()) {
-+                    return;
-+                }
-+                blockState = ((CraftBlockData) event.getNewData()).getState();
-+                // CraftBukkit end
-                 world.setBlock(pos, blockState, 3);
-                 world.scheduleTick(pos, fluid1.getType(), i);
-             }
-@@ -524,12 +570,27 @@
-         public BlockState getBlockState(BlockPos pos) {
-             return this.getBlockState(pos, this.getCacheKey(pos));
-         }
-+        // Paper start - Prevent chunk loading from fluid flowing
-+        public @javax.annotation.Nullable BlockState getBlockStateIfLoaded(BlockPos pos) {
-+            return this.getBlockState(pos, this.getCacheKey(pos), false);
-+        }
-+        // Paper end - Prevent chunk loading from fluid flowing
- 
-         private BlockState getBlockState(BlockPos pos, short packed) {
--            return (BlockState) this.stateCache.computeIfAbsent(packed, (short1) -> {
--                return this.level.getBlockState(pos);
--            });
-+        // Paper start - Prevent chunk loading from fluid flowing
-+            return getBlockState(pos, packed, true);
-         }
-+        private @javax.annotation.Nullable BlockState getBlockState(BlockPos pos, short packed, boolean load) {
-+            BlockState blockState = this.stateCache.get(packed);
-+            if (blockState == null) {
-+                blockState = load ? level.getBlockState(pos) : level.getBlockStateIfLoaded(pos);
-+                if (blockState != null) {
-+                    this.stateCache.put(packed, blockState);
-+                }
-+            }
-+            return blockState;
-+        // Paper end - Prevent chunk loading from fluid flowing
-+        }
- 
-         public boolean isHole(BlockPos pos) {
-             return this.holeCache.computeIfAbsent(this.getCacheKey(pos), (short0) -> {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/pathfinder/Path.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/pathfinder/Path.java.patch
deleted file mode 100644
index 94eb8f4856..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/pathfinder/Path.java.patch
+++ /dev/null
@@ -1,10 +0,0 @@
---- a/net/minecraft/world/level/pathfinder/Path.java
-+++ b/net/minecraft/world/level/pathfinder/Path.java
-@@ -18,6 +18,7 @@
-     private final BlockPos target;
-     private final float distToTarget;
-     private final boolean reached;
-+    public boolean hasNext() { return getNextNodeIndex() < this.nodes.size(); } // Paper - Mob Pathfinding API
- 
-     public Path(List<Node> nodes, BlockPos target, boolean reachesTarget) {
-         this.nodes = nodes;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/portal/PortalForcer.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/portal/PortalForcer.java.patch
deleted file mode 100644
index 2bcc6db1ba..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/portal/PortalForcer.java.patch
+++ /dev/null
@@ -1,149 +0,0 @@
---- a/net/minecraft/world/level/portal/PortalForcer.java
-+++ b/net/minecraft/world/level/portal/PortalForcer.java
-@@ -42,34 +42,52 @@
-         this.level = world;
-     }
- 
-+    @io.papermc.paper.annotation.DoNotUse // Paper
-     public Optional<BlockPos> findClosestPortalPosition(BlockPos pos, boolean destIsNether, WorldBorder worldBorder) {
-+        // CraftBukkit start
-+        return this.findClosestPortalPosition(pos, worldBorder, destIsNether ? 16 : 128); // Search Radius
-+    }
-+
-+    public Optional<BlockPos> findClosestPortalPosition(BlockPos blockposition, WorldBorder worldborder, int i) {
-         PoiManager villageplace = this.level.getPoiManager();
--        int i = destIsNether ? 16 : 128;
-+        // int i = flag ? 16 : 128;
-+        // CraftBukkit end
- 
--        villageplace.ensureLoadedAndValid(this.level, pos, i);
--        Stream stream = villageplace.getInSquare((holder) -> {
-+        villageplace.ensureLoadedAndValid(this.level, blockposition, i);
-+        Stream<BlockPos> stream = villageplace.getInSquare((holder) -> { // CraftBukkit - decompile error
-             return holder.is(PoiTypes.NETHER_PORTAL);
--        }, pos, i, PoiManager.Occupancy.ANY).map(PoiRecord::getPos);
-+        }, blockposition, i, PoiManager.Occupancy.ANY).map(PoiRecord::getPos);
- 
--        Objects.requireNonNull(worldBorder);
--        return stream.filter(worldBorder::isWithinBounds).filter((blockposition1) -> {
-+        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((blockposition1) -> {
--            return blockposition1.distSqr(pos);
-+        }).min(Comparator.comparingDouble((BlockPos blockposition1) -> { // CraftBukkit - decompile error
-+            return blockposition1.distSqr(blockposition);
-         }).thenComparingInt(Vec3i::getY));
-     }
- 
-     public Optional<BlockUtil.FoundRectangle> createPortal(BlockPos pos, Direction.Axis axis) {
--        Direction enumdirection = Direction.get(Direction.AxisDirection.POSITIVE, axis);
-+        // CraftBukkit start
-+        return this.createPortal(pos, axis, null, 16);
-+    }
-+
-+    public Optional<BlockUtil.FoundRectangle> createPortal(BlockPos blockposition, Direction.Axis enumdirection_enumaxis, net.minecraft.world.entity.Entity entity, int createRadius) {
-+        // CraftBukkit end
-+        Direction enumdirection = Direction.get(Direction.AxisDirection.POSITIVE, enumdirection_enumaxis);
-         double d0 = -1.0D;
-         BlockPos blockposition1 = null;
-         double d1 = -1.0D;
-         BlockPos blockposition2 = null;
-         WorldBorder worldborder = this.level.getWorldBorder();
-         int i = Math.min(this.level.getMaxY(), this.level.getMinY() + this.level.getLogicalHeight() - 1);
-+        // Paper start - Configurable nether ceiling damage; make sure the max height doesn't exceed the void damage height
-+        if (this.level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.enabled()) {
-+            i = Math.min(i, this.level.paperConfig().environment.netherCeilingVoidDamageHeight.intValue() - 1);
-+        }
-+        // Paper end - Configurable nether ceiling damage
-         boolean flag = true;
--        BlockPos.MutableBlockPos blockposition_mutableblockposition = pos.mutable();
--        Iterator iterator = BlockPos.spiralAround(pos, 16, Direction.EAST, Direction.SOUTH).iterator();
-+        BlockPos.MutableBlockPos blockposition_mutableblockposition = blockposition.mutable();
-+        Iterator iterator = BlockPos.spiralAround(blockposition, createRadius, Direction.EAST, Direction.SOUTH).iterator(); // CraftBukkit
- 
-         int j;
-         int k;
-@@ -95,7 +113,7 @@
-                             if (i1 <= 0 || i1 >= 3) {
-                                 blockposition_mutableblockposition1.setY(k);
-                                 if (this.canHostFrame(blockposition_mutableblockposition1, blockposition_mutableblockposition, enumdirection, 0)) {
--                                    double d2 = pos.distSqr(blockposition_mutableblockposition1);
-+                                    double d2 = blockposition.distSqr(blockposition_mutableblockposition1);
- 
-                                     if (this.canHostFrame(blockposition_mutableblockposition1, blockposition_mutableblockposition, enumdirection, -1) && this.canHostFrame(blockposition_mutableblockposition1, blockposition_mutableblockposition, enumdirection, 1) && (d0 == -1.0D || d0 > d2)) {
-                                         d0 = d2;
-@@ -122,6 +140,7 @@
-         int j1;
-         int k1;
- 
-+        org.bukkit.craftbukkit.util.BlockStateListPopulator blockList = new org.bukkit.craftbukkit.util.BlockStateListPopulator(this.level); // CraftBukkit - Use BlockStateListPopulator
-         if (d0 == -1.0D) {
-             j1 = Math.max(this.level.getMinY() - -1, 70);
-             k1 = i - 9;
-@@ -129,7 +148,7 @@
-                 return Optional.empty();
-             }
- 
--            blockposition1 = (new BlockPos(pos.getX() - enumdirection.getStepX() * 1, Mth.clamp(pos.getY(), j1, k1), pos.getZ() - enumdirection.getStepZ() * 1)).immutable();
-+            blockposition1 = (new BlockPos(blockposition.getX() - enumdirection.getStepX() * 1, Mth.clamp(blockposition.getY(), j1, k1), blockposition.getZ() - enumdirection.getStepZ() * 1)).immutable();
-             blockposition1 = worldborder.clampToBounds(blockposition1);
-             Direction enumdirection1 = enumdirection.getClockWise();
- 
-@@ -139,7 +158,7 @@
-                         BlockState iblockdata = i1 < 0 ? Blocks.OBSIDIAN.defaultBlockState() : Blocks.AIR.defaultBlockState();
- 
-                         blockposition_mutableblockposition.setWithOffset(blockposition1, l * enumdirection.getStepX() + k * enumdirection1.getStepX(), i1, l * enumdirection.getStepZ() + k * enumdirection1.getStepZ());
--                        this.level.setBlockAndUpdate(blockposition_mutableblockposition, iblockdata);
-+                        blockList.setBlock(blockposition_mutableblockposition, iblockdata, 3); // CraftBukkit
-                     }
-                 }
-             }
-@@ -149,20 +168,30 @@
-             for (k1 = -1; k1 < 4; ++k1) {
-                 if (j1 == -1 || j1 == 2 || k1 == -1 || k1 == 3) {
-                     blockposition_mutableblockposition.setWithOffset(blockposition1, j1 * enumdirection.getStepX(), k1, j1 * enumdirection.getStepZ());
--                    this.level.setBlock(blockposition_mutableblockposition, Blocks.OBSIDIAN.defaultBlockState(), 3);
-+                    blockList.setBlock(blockposition_mutableblockposition, Blocks.OBSIDIAN.defaultBlockState(), 3); // CraftBukkit
-                 }
-             }
-         }
- 
--        BlockState iblockdata1 = (BlockState) Blocks.NETHER_PORTAL.defaultBlockState().setValue(NetherPortalBlock.AXIS, axis);
-+        BlockState iblockdata1 = (BlockState) Blocks.NETHER_PORTAL.defaultBlockState().setValue(NetherPortalBlock.AXIS, enumdirection_enumaxis);
- 
-         for (k1 = 0; k1 < 2; ++k1) {
-             for (j = 0; j < 3; ++j) {
-                 blockposition_mutableblockposition.setWithOffset(blockposition1, k1 * enumdirection.getStepX(), j, k1 * enumdirection.getStepZ());
--                this.level.setBlock(blockposition_mutableblockposition, iblockdata1, 18);
-+                blockList.setBlock(blockposition_mutableblockposition, iblockdata1, 18); // CraftBukkit
-             }
-         }
- 
-+        // CraftBukkit start
-+        org.bukkit.World bworld = this.level.getWorld();
-+        org.bukkit.event.world.PortalCreateEvent event = new org.bukkit.event.world.PortalCreateEvent((java.util.List<org.bukkit.block.BlockState>) (java.util.List) blockList.getList(), bworld, (entity == null) ? null : entity.getBukkitEntity(), org.bukkit.event.world.PortalCreateEvent.CreateReason.NETHER_PAIR);
-+
-+        this.level.getCraftServer().getPluginManager().callEvent(event);
-+        if (event.isCancelled()) {
-+            return Optional.empty();
-+        }
-+        blockList.updateList();
-+        // CraftBukkit end
-         return Optional.of(new BlockUtil.FoundRectangle(blockposition1.immutable(), 2, 3));
-     }
- 
-@@ -178,6 +207,13 @@
-         for (int j = -1; j < 3; ++j) {
-             for (int k = -1; k < 4; ++k) {
-                 temp.setWithOffset(pos, portalDirection.getStepX() * j + enumdirection1.getStepX() * distanceOrthogonalToPortal, k, portalDirection.getStepZ() * j + enumdirection1.getStepZ() * distanceOrthogonalToPortal);
-+                // Paper start - Protect Bedrock and End Portal/Frames from being destroyed
-+                if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits) {
-+                    if (!this.level.getBlockState(temp).isDestroyable()) {
-+                        return false;
-+                    }
-+                }
-+                // Paper end - Protect Bedrock and End Portal/Frames from being destroyed
-                 if (k < 0 && !this.level.getBlockState(temp).isSolid()) {
-                     return false;
-                 }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/portal/PortalShape.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/portal/PortalShape.java.patch
deleted file mode 100644
index f0cf745dd6..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/portal/PortalShape.java.patch
+++ /dev/null
@@ -1,226 +0,0 @@
---- a/net/minecraft/world/level/portal/PortalShape.java
-+++ b/net/minecraft/world/level/portal/PortalShape.java
-@@ -23,6 +23,11 @@
- import net.minecraft.world.phys.shapes.VoxelShape;
- import org.apache.commons.lang3.mutable.MutableInt;
- 
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.util.BlockStateListPopulator;
-+import org.bukkit.event.world.PortalCreateEvent;
-+// CraftBukkit end
-+
- public class PortalShape {
- 
-     private static final int MIN_WIDTH = 2;
-@@ -40,14 +45,18 @@
-     private final BlockPos bottomLeft;
-     private final int height;
-     private final int width;
-+    // CraftBukkit start - add field
-+    private final BlockStateListPopulator blocks;
- 
--    private PortalShape(Direction.Axis axis, int foundPortalBlocks, Direction negativeDir, BlockPos lowerCorner, int width, int height) {
--        this.axis = axis;
--        this.numPortalBlocks = foundPortalBlocks;
--        this.rightDir = negativeDir;
--        this.bottomLeft = lowerCorner;
--        this.width = width;
--        this.height = height;
-+    private PortalShape(Direction.Axis enumdirection_enumaxis, int i, Direction enumdirection, BlockPos blockposition, int j, int k, BlockStateListPopulator blocks) {
-+        this.blocks = blocks;
-+        // CraftBukkit end
-+        this.axis = enumdirection_enumaxis;
-+        this.numPortalBlocks = i;
-+        this.rightDir = enumdirection;
-+        this.bottomLeft = blockposition;
-+        this.width = j;
-+        this.height = k;
-     }
- 
-     public static Optional<PortalShape> findEmptyPortalShape(LevelAccessor world, BlockPos pos, Direction.Axis firstCheckedAxis) {
-@@ -69,110 +78,118 @@
-     }
- 
-     public static PortalShape findAnyShape(BlockGetter world, BlockPos pos, Direction.Axis axis) {
-+        BlockStateListPopulator blocks = new BlockStateListPopulator(((LevelAccessor) world).getMinecraftWorld()); // CraftBukkit
-         Direction enumdirection = axis == Direction.Axis.X ? Direction.WEST : Direction.SOUTH;
--        BlockPos blockposition1 = PortalShape.calculateBottomLeft(world, enumdirection, pos);
-+        BlockPos blockposition1 = PortalShape.calculateBottomLeft(world, enumdirection, pos, blocks); // CraftBukkit
- 
-         if (blockposition1 == null) {
--            return new PortalShape(axis, 0, enumdirection, pos, 0, 0);
-+            return new PortalShape(axis, 0, enumdirection, pos, 0, 0, blocks); // CraftBukkit
-         } else {
--            int i = PortalShape.calculateWidth(world, blockposition1, enumdirection);
-+            int i = PortalShape.calculateWidth(world, blockposition1, enumdirection, blocks); // CraftBukkit
- 
-             if (i == 0) {
--                return new PortalShape(axis, 0, enumdirection, blockposition1, 0, 0);
-+                return new PortalShape(axis, 0, enumdirection, blockposition1, 0, 0, blocks); // CraftBukkit
-             } else {
-                 MutableInt mutableint = new MutableInt();
--                int j = PortalShape.calculateHeight(world, blockposition1, enumdirection, i, mutableint);
-+                int j = PortalShape.calculateHeight(world, blockposition1, enumdirection, i, mutableint, blocks); // CraftBukkit
- 
--                return new PortalShape(axis, mutableint.getValue(), enumdirection, blockposition1, i, j);
-+                return new PortalShape(axis, mutableint.getValue(), enumdirection, blockposition1, i, j, blocks); // CraftBukkit
-             }
-         }
-     }
- 
-     @Nullable
--    private static BlockPos calculateBottomLeft(BlockGetter world, Direction direction, BlockPos pow) {
--        for (int i = Math.max(world.getMinY(), pow.getY() - 21); pow.getY() > i && PortalShape.isEmpty(world.getBlockState(pow.below())); pow = pow.below()) {
-+    private static BlockPos calculateBottomLeft(BlockGetter iblockaccess, Direction enumdirection, BlockPos blockposition, BlockStateListPopulator blocks) { // CraftBukkit
-+        for (int i = Math.max(iblockaccess.getMinY(), blockposition.getY() - 21); blockposition.getY() > i && PortalShape.isEmpty(iblockaccess.getBlockState(blockposition.below())); blockposition = blockposition.below()) {
-             ;
-         }
- 
--        Direction enumdirection1 = direction.getOpposite();
--        int j = PortalShape.getDistanceUntilEdgeAboveFrame(world, pow, enumdirection1) - 1;
-+        Direction enumdirection1 = enumdirection.getOpposite();
-+        int j = PortalShape.getDistanceUntilEdgeAboveFrame(iblockaccess, blockposition, enumdirection1, blocks) - 1; // CraftBukkit
- 
--        return j < 0 ? null : pow.relative(enumdirection1, j);
-+        return j < 0 ? null : blockposition.relative(enumdirection1, j);
-     }
- 
--    private static int calculateWidth(BlockGetter world, BlockPos lowerCorner, Direction negativeDir) {
--        int i = PortalShape.getDistanceUntilEdgeAboveFrame(world, lowerCorner, negativeDir);
-+    private static int calculateWidth(BlockGetter iblockaccess, BlockPos blockposition, Direction enumdirection, BlockStateListPopulator blocks) { // CraftBukkit
-+        int i = PortalShape.getDistanceUntilEdgeAboveFrame(iblockaccess, blockposition, enumdirection, blocks); // CraftBukkit
- 
-         return i >= 2 && i <= 21 ? i : 0;
-     }
- 
--    private static int getDistanceUntilEdgeAboveFrame(BlockGetter world, BlockPos lowerCorner, Direction negativeDir) {
-+    private static int getDistanceUntilEdgeAboveFrame(BlockGetter iblockaccess, BlockPos blockposition, Direction enumdirection, BlockStateListPopulator blocks) { // CraftBukkit
-         BlockPos.MutableBlockPos blockposition_mutableblockposition = new BlockPos.MutableBlockPos();
- 
-         for (int i = 0; i <= 21; ++i) {
--            blockposition_mutableblockposition.set(lowerCorner).move(negativeDir, i);
--            BlockState iblockdata = world.getBlockState(blockposition_mutableblockposition);
-+            blockposition_mutableblockposition.set(blockposition).move(enumdirection, i);
-+            BlockState iblockdata = iblockaccess.getBlockState(blockposition_mutableblockposition);
- 
-             if (!PortalShape.isEmpty(iblockdata)) {
--                if (PortalShape.FRAME.test(iblockdata, world, blockposition_mutableblockposition)) {
-+                if (PortalShape.FRAME.test(iblockdata, iblockaccess, blockposition_mutableblockposition)) {
-+                    blocks.setBlock(blockposition_mutableblockposition, iblockdata, 18); // CraftBukkit - lower left / right
-                     return i;
-                 }
-                 break;
-             }
- 
--            BlockState iblockdata1 = world.getBlockState(blockposition_mutableblockposition.move(Direction.DOWN));
-+            BlockState iblockdata1 = iblockaccess.getBlockState(blockposition_mutableblockposition.move(Direction.DOWN));
- 
--            if (!PortalShape.FRAME.test(iblockdata1, world, blockposition_mutableblockposition)) {
-+            if (!PortalShape.FRAME.test(iblockdata1, iblockaccess, blockposition_mutableblockposition)) {
-                 break;
-             }
-+            blocks.setBlock(blockposition_mutableblockposition, iblockdata1, 18); // CraftBukkit - bottom row
-         }
- 
-         return 0;
-     }
- 
--    private static int calculateHeight(BlockGetter world, BlockPos lowerCorner, Direction negativeDir, int width, MutableInt foundPortalBlocks) {
--        BlockPos.MutableBlockPos blockposition_mutableblockposition = new BlockPos.MutableBlockPos();
--        int j = PortalShape.getDistanceUntilTop(world, lowerCorner, negativeDir, blockposition_mutableblockposition, width, foundPortalBlocks);
-+    private static int calculateHeight(BlockGetter iblockaccess, BlockPos blockposition, Direction enumdirection, int i, MutableInt mutableint, BlockStateListPopulator blocks) { // CraftBukkit
-+        BlockPos.MutableBlockPos blockposition_mutableblockposition = new BlockPos.MutableBlockPos();
-+        int j = PortalShape.getDistanceUntilTop(iblockaccess, blockposition, enumdirection, blockposition_mutableblockposition, i, mutableint, blocks); // CraftBukkit
- 
--        return j >= 3 && j <= 21 && PortalShape.hasTopFrame(world, lowerCorner, negativeDir, blockposition_mutableblockposition, width, j) ? j : 0;
-+        return j >= 3 && j <= 21 && PortalShape.hasTopFrame(iblockaccess, blockposition, enumdirection, blockposition_mutableblockposition, i, j, blocks) ? j : 0; // CraftBukkit
-     }
- 
--    private static boolean hasTopFrame(BlockGetter world, BlockPos lowerCorner, Direction direction, BlockPos.MutableBlockPos pos, int width, int height) {
--        for (int k = 0; k < width; ++k) {
--            BlockPos.MutableBlockPos blockposition_mutableblockposition1 = pos.set(lowerCorner).move(Direction.UP, height).move(direction, k);
-+    private static boolean hasTopFrame(BlockGetter iblockaccess, BlockPos blockposition, Direction enumdirection, BlockPos.MutableBlockPos blockposition_mutableblockposition, int i, int j, BlockStateListPopulator blocks) { // CraftBukkit
-+        for (int k = 0; k < i; ++k) {
-+            BlockPos.MutableBlockPos blockposition_mutableblockposition1 = blockposition_mutableblockposition.set(blockposition).move(Direction.UP, j).move(enumdirection, k);
- 
--            if (!PortalShape.FRAME.test(world.getBlockState(blockposition_mutableblockposition1), world, blockposition_mutableblockposition1)) {
-+            if (!PortalShape.FRAME.test(iblockaccess.getBlockState(blockposition_mutableblockposition1), iblockaccess, blockposition_mutableblockposition1)) {
-                 return false;
-             }
-+            blocks.setBlock(blockposition_mutableblockposition1, iblockaccess.getBlockState(blockposition_mutableblockposition1), 18); // CraftBukkit - upper row
-         }
- 
-         return true;
-     }
- 
--    private static int getDistanceUntilTop(BlockGetter world, BlockPos lowerCorner, Direction negativeDir, BlockPos.MutableBlockPos pos, int width, MutableInt foundPortalBlocks) {
-+    private static int getDistanceUntilTop(BlockGetter iblockaccess, BlockPos blockposition, Direction enumdirection, BlockPos.MutableBlockPos blockposition_mutableblockposition, int i, MutableInt mutableint, BlockStateListPopulator blocks) { // CraftBukkit
-         for (int j = 0; j < 21; ++j) {
--            pos.set(lowerCorner).move(Direction.UP, j).move(negativeDir, -1);
--            if (!PortalShape.FRAME.test(world.getBlockState(pos), world, pos)) {
-+            blockposition_mutableblockposition.set(blockposition).move(Direction.UP, j).move(enumdirection, -1);
-+            if (!PortalShape.FRAME.test(iblockaccess.getBlockState(blockposition_mutableblockposition), iblockaccess, blockposition_mutableblockposition)) {
-                 return j;
-             }
- 
--            pos.set(lowerCorner).move(Direction.UP, j).move(negativeDir, width);
--            if (!PortalShape.FRAME.test(world.getBlockState(pos), world, pos)) {
-+            blockposition_mutableblockposition.set(blockposition).move(Direction.UP, j).move(enumdirection, i);
-+            if (!PortalShape.FRAME.test(iblockaccess.getBlockState(blockposition_mutableblockposition), iblockaccess, blockposition_mutableblockposition)) {
-                 return j;
-             }
- 
--            for (int k = 0; k < width; ++k) {
--                pos.set(lowerCorner).move(Direction.UP, j).move(negativeDir, k);
--                BlockState iblockdata = world.getBlockState(pos);
-+            for (int k = 0; k < i; ++k) {
-+                blockposition_mutableblockposition.set(blockposition).move(Direction.UP, j).move(enumdirection, k);
-+                BlockState iblockdata = iblockaccess.getBlockState(blockposition_mutableblockposition);
- 
-                 if (!PortalShape.isEmpty(iblockdata)) {
-                     return j;
-                 }
- 
-                 if (iblockdata.is(Blocks.NETHER_PORTAL)) {
--                    foundPortalBlocks.increment();
-+                    mutableint.increment();
-                 }
-             }
-+            // CraftBukkit start - left and right
-+            blocks.setBlock(blockposition_mutableblockposition.set(blockposition).move(Direction.UP, j).move(enumdirection, -1), iblockaccess.getBlockState(blockposition_mutableblockposition), 18);
-+            blocks.setBlock(blockposition_mutableblockposition.set(blockposition).move(Direction.UP, j).move(enumdirection, i), iblockaccess.getBlockState(blockposition_mutableblockposition), 18);
-+            // CraftBukkit end
-         }
- 
-         return 21;
-@@ -186,12 +203,28 @@
-         return this.width >= 2 && this.width <= 21 && this.height >= 3 && this.height <= 21;
-     }
- 
--    public void createPortalBlocks(LevelAccessor world) {
-+    // CraftBukkit start - return boolean, add entity
-+    public boolean createPortalBlocks(LevelAccessor generatoraccess, Entity entity) {
-+        org.bukkit.World bworld = generatoraccess.getMinecraftWorld().getWorld();
-+
-+        // Copy below for loop
-         BlockState iblockdata = (BlockState) Blocks.NETHER_PORTAL.defaultBlockState().setValue(NetherPortalBlock.AXIS, this.axis);
- 
-         BlockPos.betweenClosed(this.bottomLeft, this.bottomLeft.relative(Direction.UP, this.height - 1).relative(this.rightDir, this.width - 1)).forEach((blockposition) -> {
--            world.setBlock(blockposition, iblockdata, 18);
-+            this.blocks.setBlock(blockposition, iblockdata, 18);
-         });
-+
-+        PortalCreateEvent event = new PortalCreateEvent((java.util.List<org.bukkit.block.BlockState>) (java.util.List) this.blocks.getList(), bworld, (entity == null) ? null : entity.getBukkitEntity(), PortalCreateEvent.CreateReason.FIRE);
-+        generatoraccess.getMinecraftWorld().getServer().server.getPluginManager().callEvent(event);
-+
-+        if (event.isCancelled()) {
-+            return false;
-+        }
-+        // CraftBukkit end
-+        BlockPos.betweenClosed(this.bottomLeft, this.bottomLeft.relative(Direction.UP, this.height - 1).relative(this.rightDir, this.width - 1)).forEach((blockposition) -> {
-+            generatoraccess.setBlock(blockposition, iblockdata, 18);
-+        });
-+        return true; // CraftBukkit
-     }
- 
-     public boolean isComplete() {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/portal/TeleportTransition.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/portal/TeleportTransition.java.patch
deleted file mode 100644
index 1d72cf288c..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/portal/TeleportTransition.java.patch
+++ /dev/null
@@ -1,71 +0,0 @@
---- a/net/minecraft/world/level/portal/TeleportTransition.java
-+++ b/net/minecraft/world/level/portal/TeleportTransition.java
-@@ -8,26 +8,55 @@
- import net.minecraft.world.entity.Entity;
- import net.minecraft.world.entity.Relative;
- import net.minecraft.world.phys.Vec3;
-+// CraftBukkit start
-+import org.bukkit.event.player.PlayerTeleportEvent;
- 
--public record TeleportTransition(ServerLevel newLevel, Vec3 position, Vec3 deltaMovement, float yRot, float xRot, boolean missingRespawnBlock, boolean asPassenger, Set<Relative> relatives, TeleportTransition.PostTeleportTransition postTeleportTransition) {
-+public record TeleportTransition(ServerLevel newLevel, Vec3 position, Vec3 deltaMovement, float yRot, float xRot, boolean missingRespawnBlock, boolean asPassenger, Set<Relative> relatives, TeleportTransition.PostTeleportTransition postTeleportTransition, PlayerTeleportEvent.TeleportCause cause) {
- 
-+    public TeleportTransition(ServerLevel newLevel, Vec3 position, Vec3 deltaMovement, float yRot, float xRot, boolean missingRespawnBlock, boolean asPassenger, Set<Relative> relatives, TeleportTransition.PostTeleportTransition postTeleportTransition) {
-+        this(newLevel, position, deltaMovement, yRot, xRot, missingRespawnBlock, asPassenger, relatives, postTeleportTransition, PlayerTeleportEvent.TeleportCause.UNKNOWN);
-+    }
-+
-+    public TeleportTransition(PlayerTeleportEvent.TeleportCause cause) {
-+        this(null, Vec3.ZERO, Vec3.ZERO, 0.0F, 0.0F, false, false, Set.of(), DO_NOTHING, cause);
-+    }
-+    // CraftBukkit end
-+
-     public static final TeleportTransition.PostTeleportTransition DO_NOTHING = (entity) -> {
-     };
-     public static final TeleportTransition.PostTeleportTransition PLAY_PORTAL_SOUND = TeleportTransition::playPortalSound;
-     public static final TeleportTransition.PostTeleportTransition PLACE_PORTAL_TICKET = TeleportTransition::placePortalTicket;
- 
-     public TeleportTransition(ServerLevel world, Vec3 pos, Vec3 velocity, float yaw, float pitch, TeleportTransition.PostTeleportTransition postDimensionTransition) {
--        this(world, pos, velocity, yaw, pitch, Set.of(), postDimensionTransition);
-+        // CraftBukkit start
-+        this(world, pos, velocity, yaw, pitch, postDimensionTransition, PlayerTeleportEvent.TeleportCause.UNKNOWN);
-     }
- 
-+    public TeleportTransition(ServerLevel worldserver, Vec3 vec3d, Vec3 vec3d1, float f, float f1, TeleportTransition.PostTeleportTransition teleporttransition_a, PlayerTeleportEvent.TeleportCause cause) {
-+        this(worldserver, vec3d, vec3d1, f, f1, Set.of(), teleporttransition_a, cause);
-+        // CraftBukkit end
-+    }
-+
-     public TeleportTransition(ServerLevel world, Vec3 pos, Vec3 velocity, float yaw, float pitch, Set<Relative> flags, TeleportTransition.PostTeleportTransition postDimensionTransition) {
--        this(world, pos, velocity, yaw, pitch, false, false, flags, postDimensionTransition);
-+        // CraftBukkit start
-+        this(world, pos, velocity, yaw, pitch, flags, postDimensionTransition, PlayerTeleportEvent.TeleportCause.UNKNOWN);
-     }
- 
-+    public TeleportTransition(ServerLevel worldserver, Vec3 vec3d, Vec3 vec3d1, float f, float f1, Set<Relative> set, TeleportTransition.PostTeleportTransition teleporttransition_a, PlayerTeleportEvent.TeleportCause cause) {
-+        this(worldserver, vec3d, vec3d1, f, f1, false, false, set, teleporttransition_a, cause);
-+        // CraftBukkit end
-+    }
-+
-     public TeleportTransition(ServerLevel world, Entity entity, TeleportTransition.PostTeleportTransition postDimensionTransition) {
--        this(world, findAdjustedSharedSpawnPos(world, entity), Vec3.ZERO, 0.0F, 0.0F, false, false, Set.of(), postDimensionTransition);
-+        // CraftBukkit start
-+        this(world, entity, postDimensionTransition, PlayerTeleportEvent.TeleportCause.UNKNOWN);
-     }
- 
-+    public TeleportTransition(ServerLevel worldserver, Entity entity, TeleportTransition.PostTeleportTransition teleporttransition_a, PlayerTeleportEvent.TeleportCause cause) {
-+        this(worldserver, findAdjustedSharedSpawnPos(worldserver, entity), Vec3.ZERO, worldserver.getSharedSpawnAngle(), 0.0F, false, false, Set.of(), teleporttransition_a, cause); // Paper - MC-200092 - fix first spawn pos yaw being ignored
-+        // CraftBukkit end
-+    }
-+
-     private static void playPortalSound(Entity entity) {
-         if (entity instanceof ServerPlayer entityplayer) {
-             entityplayer.connection.send(new ClientboundLevelEventPacket(1032, BlockPos.ZERO, 0, false));
-@@ -40,7 +69,7 @@
-     }
- 
-     public static TeleportTransition missingRespawnBlock(ServerLevel world, Entity entity, TeleportTransition.PostTeleportTransition postDimensionTransition) {
--        return new TeleportTransition(world, findAdjustedSharedSpawnPos(world, entity), Vec3.ZERO, 0.0F, 0.0F, true, false, Set.of(), postDimensionTransition);
-+        return new TeleportTransition(world, findAdjustedSharedSpawnPos(world, entity), Vec3.ZERO, world.getSharedSpawnAngle(), 0.0F, true, false, Set.of(), postDimensionTransition); // Paper - MC-200092 - fix spawn pos yaw being ignored
-     }
- 
-     private static Vec3 findAdjustedSharedSpawnPos(ServerLevel world, Entity entity) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java.patch
deleted file mode 100644
index 8862572ac8..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java.patch
+++ /dev/null
@@ -1,31 +0,0 @@
---- a/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java
-+++ b/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java
-@@ -9,6 +9,10 @@
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.block.RedStoneWireBlock;
- import net.minecraft.world.level.block.state.BlockState;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.event.block.BlockRedstoneEvent;
-+// CraftBukkit end
- 
- public class DefaultRedstoneWireEvaluator extends RedstoneWireEvaluator {
- 
-@@ -20,7 +24,16 @@
-     public void updatePowerStrength(Level world, BlockPos pos, BlockState state, @Nullable Orientation orientation, boolean blockAdded) {
-         int i = this.calculateTargetStrength(world, pos);
- 
--        if ((Integer) state.getValue(RedStoneWireBlock.POWER) != i) {
-+        // CraftBukkit start
-+        int oldPower = state.getValue(RedStoneWireBlock.POWER);
-+        if (oldPower != i) {
-+            BlockRedstoneEvent event = new BlockRedstoneEvent(CraftBlock.at(world, pos), oldPower, i);
-+            world.getCraftServer().getPluginManager().callEvent(event);
-+
-+            i = event.getNewCurrent();
-+        }
-+        if (oldPower != i) {
-+            // CraftBukkit end
-             if (world.getBlockState(pos) == state) {
-                 world.setBlock(pos, (BlockState) state.setValue(RedStoneWireBlock.POWER, i), 2);
-             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/redstone/ExperimentalRedstoneWireEvaluator.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/redstone/ExperimentalRedstoneWireEvaluator.java.patch
deleted file mode 100644
index 73e662622f..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/redstone/ExperimentalRedstoneWireEvaluator.java.patch
+++ /dev/null
@@ -1,31 +0,0 @@
---- a/net/minecraft/world/level/redstone/ExperimentalRedstoneWireEvaluator.java
-+++ b/net/minecraft/world/level/redstone/ExperimentalRedstoneWireEvaluator.java
-@@ -16,6 +16,10 @@
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.block.state.properties.EnumProperty;
- import net.minecraft.world.level.block.state.properties.RedstoneSide;
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.event.block.BlockRedstoneEvent;
-+// CraftBukkit end
- 
- public class ExperimentalRedstoneWireEvaluator extends RedstoneWireEvaluator {
- 
-@@ -41,7 +45,16 @@
-             int j = ExperimentalRedstoneWireEvaluator.unpackPower(i);
-             BlockState iblockdata1 = world.getBlockState(blockposition1);
- 
--            if (iblockdata1.is((Block) this.wireBlock) && !((Integer) iblockdata1.getValue(RedStoneWireBlock.POWER)).equals(j)) {
-+            // CraftBukkit start
-+            int oldPower = iblockdata1.getValue(RedStoneWireBlock.POWER); // Paper - Call BlockRedstoneEvent properly; get the previous power from the right state
-+            if (oldPower != j) {
-+                BlockRedstoneEvent event = new BlockRedstoneEvent(CraftBlock.at(world, blockposition1), oldPower, j);
-+                world.getCraftServer().getPluginManager().callEvent(event);
-+
-+                j = event.getNewCurrent();
-+            }
-+            if (iblockdata1.is((Block) this.wireBlock) && oldPower != j) {
-+                // CraftBukkit end
-                 int k = 2;
- 
-                 if (!blockAdded || !flag1) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/redstone/NeighborUpdater.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/redstone/NeighborUpdater.java.patch
deleted file mode 100644
index a8973333ac..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/redstone/NeighborUpdater.java.patch
+++ /dev/null
@@ -1,50 +0,0 @@
---- a/net/minecraft/world/level/redstone/NeighborUpdater.java
-+++ b/net/minecraft/world/level/redstone/NeighborUpdater.java
-@@ -8,11 +8,17 @@
- import net.minecraft.core.BlockPos;
- import net.minecraft.core.Direction;
- import net.minecraft.core.registries.BuiltInRegistries;
-+import net.minecraft.server.level.ServerLevel;
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.LevelAccessor;
- import net.minecraft.world.level.block.Block;
- import net.minecraft.world.level.block.Blocks;
- import net.minecraft.world.level.block.state.BlockState;
-+import org.bukkit.craftbukkit.CraftWorld;
-+import org.bukkit.craftbukkit.block.CraftBlock;
-+import org.bukkit.craftbukkit.block.data.CraftBlockData;
-+import org.bukkit.event.block.BlockPhysicsEvent;
-+// CraftBukkit end
- 
- public interface NeighborUpdater {
- 
-@@ -49,8 +55,29 @@
-     }
- 
-     static void executeUpdate(Level world, BlockState state, BlockPos pos, Block sourceBlock, @Nullable Orientation orientation, boolean notify) {
-+        // Paper start - Add source block to BlockPhysicsEvent
-+        executeUpdate(world, state, pos, sourceBlock, orientation, notify, pos);
-+    }
-+
-+    static void executeUpdate(Level world, BlockState state, BlockPos pos, Block sourceBlock, @Nullable Orientation orientation, boolean notify, BlockPos sourcePos) {
-+        // Paper end - Add source block to BlockPhysicsEvent
-         try {
-+            // CraftBukkit start
-+            CraftWorld cworld = ((ServerLevel) world).getWorld();
-+            if (cworld != null) {
-+                BlockPhysicsEvent event = new BlockPhysicsEvent(CraftBlock.at(world, pos), CraftBlockData.fromData(state), CraftBlock.at(world, sourcePos)); // Paper - Add source block to BlockPhysicsEvent
-+                ((ServerLevel) world).getCraftServer().getPluginManager().callEvent(event);
-+
-+                if (event.isCancelled()) {
-+                    return;
-+                }
-+            }
-+            // CraftBukkit end
-             state.handleNeighborChanged(world, pos, sourceBlock, orientation, notify);
-+            // Spigot Start
-+        } catch (StackOverflowError ex) {
-+            world.lastPhysicsProblem = new BlockPos(pos);
-+            // Spigot End
-         } catch (Throwable throwable) {
-             CrashReport crashreport = CrashReport.forThrowable(throwable, "Exception while updating neighbours");
-             CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Block being updated");
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java.patch
deleted file mode 100644
index 452050da4c..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java.patch
+++ /dev/null
@@ -1,216 +0,0 @@
---- a/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java
-+++ b/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java
-@@ -47,6 +47,17 @@
- import net.minecraft.world.level.saveddata.SavedData;
- import org.slf4j.Logger;
- 
-+// CraftBukkit start
-+import io.papermc.paper.adventure.PaperAdventure; // Paper
-+import java.util.UUID;
-+import org.bukkit.Bukkit;
-+import org.bukkit.craftbukkit.CraftServer;
-+import org.bukkit.craftbukkit.CraftWorld;
-+import org.bukkit.craftbukkit.map.CraftMapCursor;
-+import org.bukkit.craftbukkit.map.CraftMapView;
-+import org.bukkit.craftbukkit.util.CraftChatMessage;
-+// CraftBukkit end
-+
- public class MapItemSavedData extends SavedData {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
-@@ -70,6 +81,13 @@
-     private final Map<String, MapFrame> frameMarkers = Maps.newHashMap();
-     private int trackedDecorationCount;
- 
-+    // CraftBukkit start
-+    public final CraftMapView mapView;
-+    private CraftServer server;
-+    public UUID uniqueId = null;
-+    public MapId id;
-+    // CraftBukkit end
-+
-     public static SavedData.Factory<MapItemSavedData> factory() {
-         return new SavedData.Factory<>(() -> {
-             throw new IllegalStateException("Should never create an empty map saved data");
-@@ -84,6 +102,10 @@
-         this.trackingPosition = showDecorations;
-         this.unlimitedTracking = unlimitedTracking;
-         this.locked = locked;
-+        // CraftBukkit start
-+        this.mapView = new CraftMapView(this);
-+        this.server = (CraftServer) org.bukkit.Bukkit.getServer();
-+        // CraftBukkit end
-     }
- 
-     public static MapItemSavedData createFresh(double centerX, double centerZ, byte scale, boolean showDecorations, boolean unlimitedTracking, ResourceKey<Level> dimension) {
-@@ -101,12 +123,49 @@
-     }
- 
-     public static MapItemSavedData load(CompoundTag nbt, HolderLookup.Provider registries) {
--        DataResult dataresult = DimensionType.parseLegacy(new Dynamic(NbtOps.INSTANCE, nbt.get("dimension")));
-+        // Paper start - fix "Not a string" spam
-+        Tag dimension = nbt.get("dimension");
-+        if (dimension instanceof final net.minecraft.nbt.NumericTag numericTag && numericTag.getAsInt() >= CraftWorld.CUSTOM_DIMENSION_OFFSET) {
-+            long least = nbt.getLong("UUIDLeast");
-+            long most = nbt.getLong("UUIDMost");
-+
-+            if (least != 0L && most != 0L) {
-+                UUID uuid = new UUID(most, least);
-+                CraftWorld world = (CraftWorld) Bukkit.getWorld(uuid);
-+                if (world != null) {
-+                    dimension = net.minecraft.nbt.StringTag.valueOf("minecraft:" + world.getName().toLowerCase(java.util.Locale.ENGLISH));
-+                } else {
-+                    dimension = net.minecraft.nbt.StringTag.valueOf("bukkit:_invalidworld_");
-+                }
-+            } else {
-+                dimension = net.minecraft.nbt.StringTag.valueOf("bukkit:_invalidworld_");
-+            }
-+        }
-+        DataResult<ResourceKey<Level>> dataresult = DimensionType.parseLegacy(new Dynamic(NbtOps.INSTANCE, dimension)); // CraftBukkit - decompile error
-+        // Paper end - fix "Not a string" spam
-         Logger logger = MapItemSavedData.LOGGER;
- 
-         Objects.requireNonNull(logger);
--        ResourceKey<Level> resourcekey = (ResourceKey) dataresult.resultOrPartial(logger::error).orElseThrow(() -> {
--            return new IllegalArgumentException("Invalid map dimension: " + String.valueOf(nbt.get("dimension")));
-+        // CraftBukkit start
-+        ResourceKey<Level> resourcekey = (ResourceKey) dataresult.resultOrPartial(logger::error).orElseGet(() -> {
-+            long least = nbt.getLong("UUIDLeast");
-+            long most = nbt.getLong("UUIDMost");
-+
-+            if (least != 0L && most != 0L) {
-+                UUID uniqueId = new UUID(most, least);
-+
-+                CraftWorld world = (CraftWorld) Bukkit.getWorld(uniqueId);
-+                // Check if the stored world details are correct.
-+                if (world == null) {
-+                    /* All Maps which do not have their valid world loaded are set to a dimension which hopefully won't be reached.
-+                       This is to prevent them being corrupted with the wrong map data. */
-+                    // PAIL: Use Vanilla exception handling for now
-+                } else {
-+                    return world.getHandle().dimension();
-+                }
-+            }
-+            throw new IllegalArgumentException("Invalid map dimension: " + String.valueOf(nbt.get("dimension")));
-+            // CraftBukkit end
-         });
-         int i = nbt.getInt("xCenter");
-         int j = nbt.getInt("zCenter");
-@@ -131,7 +190,8 @@
-             MapBanner mapiconbanner = (MapBanner) iterator.next();
- 
-             worldmap.bannerMarkers.put(mapiconbanner.getId(), mapiconbanner);
--            worldmap.addDecoration(mapiconbanner.getDecoration(), (LevelAccessor) null, mapiconbanner.getId(), (double) mapiconbanner.pos().getX(), (double) mapiconbanner.pos().getZ(), 180.0D, (Component) mapiconbanner.name().orElse((Object) null));
-+            // CraftBukkit - decompile error
-+            worldmap.addDecoration(mapiconbanner.getDecoration(), (LevelAccessor) null, mapiconbanner.getId(), (double) mapiconbanner.pos().getX(), (double) mapiconbanner.pos().getZ(), 180.0D, (Component) mapiconbanner.name().orElse(null));
-         }
- 
-         ListTag nbttaglist = nbt.getList("frames", 10);
-@@ -150,13 +210,32 @@
- 
-     @Override
-     public CompoundTag save(CompoundTag nbt, HolderLookup.Provider registries) {
--        DataResult dataresult = ResourceLocation.CODEC.encodeStart(NbtOps.INSTANCE, this.dimension.location());
-+        DataResult<Tag> dataresult = ResourceLocation.CODEC.encodeStart(NbtOps.INSTANCE, this.dimension.location()); // CraftBukkit - decompile error
-         Logger logger = MapItemSavedData.LOGGER;
- 
-         Objects.requireNonNull(logger);
-         dataresult.resultOrPartial(logger::error).ifPresent((nbtbase) -> {
-             nbt.put("dimension", nbtbase);
-         });
-+        // CraftBukkit start
-+        if (true) {
-+            if (this.uniqueId == null) {
-+                for (org.bukkit.World world : this.server.getWorlds()) {
-+                    CraftWorld cWorld = (CraftWorld) world;
-+                    if (cWorld.getHandle().dimension() == this.dimension) {
-+                        this.uniqueId = cWorld.getUID();
-+                        break;
-+                    }
-+                }
-+            }
-+            /* Perform a second check to see if a matching world was found, this is a necessary
-+               change incase Maps are forcefully unlinked from a World and lack a UID.*/
-+            if (this.uniqueId != null) {
-+                nbt.putLong("UUIDLeast", this.uniqueId.getLeastSignificantBits());
-+                nbt.putLong("UUIDMost", this.uniqueId.getMostSignificantBits());
-+            }
-+        }
-+        // CraftBukkit end
-         nbt.putInt("xCenter", this.centerX);
-         nbt.putInt("zCenter", this.centerZ);
-         nbt.putByte("scale", this.scale);
-@@ -247,8 +326,10 @@
- 
-             MapFrame worldmapframe1 = new MapFrame(blockposition, entityitemframe.getDirection().get2DDataValue() * 90, entityitemframe.getId());
- 
-+            if (this.decorations.size() < player.level().paperConfig().maps.itemFrameCursorLimit) { // Paper - Limit item frame cursors on maps
-             this.addDecoration(MapDecorationTypes.FRAME, player.level(), MapItemSavedData.getFrameKey(entityitemframe.getId()), (double) blockposition.getX(), (double) blockposition.getZ(), (double) (entityitemframe.getDirection().get2DDataValue() * 90), (Component) null);
-             this.frameMarkers.put(worldmapframe1.getId(), worldmapframe1);
-+            } // Paper - Limit item frame cursors on maps
-         }
- 
-         MapDecorations mapdecorations = (MapDecorations) stack.getOrDefault(DataComponents.MAP_DECORATIONS, MapDecorations.EMPTY);
-@@ -441,9 +522,9 @@
-                 return true;
-             }
- 
--            if (!this.isTrackedCountOverLimit(256)) {
-+            if (!this.isTrackedCountOverLimit(((Level) world).paperConfig().maps.itemFrameCursorLimit)) { // Paper - Limit item frame cursors on maps
-                 this.bannerMarkers.put(mapiconbanner.getId(), mapiconbanner);
--                this.addDecoration(mapiconbanner.getDecoration(), world, mapiconbanner.getId(), d0, d1, 180.0D, (Component) mapiconbanner.name().orElse((Object) null));
-+                this.addDecoration(mapiconbanner.getDecoration(), world, mapiconbanner.getId(), d0, d1, 180.0D, (Component) mapiconbanner.name().orElse(null)); // CraftBukkit - decompile error
-                 return true;
-             }
-         }
-@@ -554,7 +635,7 @@
-             this.player = entityhuman;
-         }
- 
--        private MapItemSavedData.MapPatch createPatch() {
-+        private MapItemSavedData.MapPatch createPatch(byte[] buffer) { // CraftBukkit
-             int i = this.minDirtyX;
-             int j = this.minDirtyY;
-             int k = this.maxDirtyX + 1 - this.minDirtyX;
-@@ -563,7 +644,7 @@
- 
-             for (int i1 = 0; i1 < k; ++i1) {
-                 for (int j1 = 0; j1 < l; ++j1) {
--                    abyte[i1 + j1 * k] = MapItemSavedData.this.colors[i + i1 + (j + j1) * 128];
-+                    abyte[i1 + j1 * k] = buffer[i + i1 + (j + j1) * 128]; // CraftBukkit
-                 }
-             }
- 
-@@ -573,19 +654,29 @@
-         @Nullable
-         Packet<?> nextUpdatePacket(MapId mapId) {
-             MapItemSavedData.MapPatch worldmap_c;
-+            org.bukkit.craftbukkit.map.RenderData render = MapItemSavedData.this.mapView.render((org.bukkit.craftbukkit.entity.CraftPlayer) this.player.getBukkitEntity()); // CraftBukkit
- 
-             if (this.dirtyData) {
-                 this.dirtyData = false;
--                worldmap_c = this.createPatch();
-+                worldmap_c = this.createPatch(render.buffer); // CraftBukkit
-             } else {
-                 worldmap_c = null;
-             }
- 
-             Collection collection;
- 
--            if (this.dirtyDecorations && this.tick++ % 5 == 0) {
-+            if ((true || this.dirtyDecorations) && this.tick++ % 5 == 0) { // CraftBukkit - custom maps don't update this yet
-                 this.dirtyDecorations = false;
--                collection = MapItemSavedData.this.decorations.values();
-+                // CraftBukkit start
-+                java.util.Collection<MapDecoration> icons = new java.util.ArrayList<MapDecoration>();
-+
-+                for (org.bukkit.map.MapCursor cursor : render.cursors) {
-+                    if (cursor.isVisible()) {
-+                        icons.add(new MapDecoration(CraftMapCursor.CraftType.bukkitToMinecraftHolder(cursor.getType()), cursor.getX(), cursor.getY(), cursor.getDirection(), Optional.ofNullable(PaperAdventure.asVanilla(cursor.caption()))));
-+                    }
-+                }
-+                collection = icons;
-+                // CraftBukkit end
-             } else {
-                 collection = null;
-             }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/storage/DimensionDataStorage.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/storage/DimensionDataStorage.java.patch
deleted file mode 100644
index 0a22aaf58b..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/storage/DimensionDataStorage.java.patch
+++ /dev/null
@@ -1,20 +0,0 @@
---- a/net/minecraft/world/level/storage/DimensionDataStorage.java
-+++ b/net/minecraft/world/level/storage/DimensionDataStorage.java
-@@ -139,7 +139,7 @@
-         } else {
-             int i = Util.maxAllowedExecutorThreads();
-             int j = map.size();
--            if (j > i) {
-+            if (false && j > i) { // Paper - Separate dimension data IO pool; just throw them into the fixed pool queue
-                 this.pendingWriteFuture = this.pendingWriteFuture.thenCompose(object -> {
-                     List<CompletableFuture<?>> list = new ArrayList<>(i);
-                     int k = Mth.positiveCeilDiv(j, i);
-@@ -160,7 +160,7 @@
-                         v -> CompletableFuture.allOf(
-                                 map.entrySet()
-                                     .stream()
--                                    .map(entry -> CompletableFuture.runAsync(() -> tryWrite(entry.getKey(), entry.getValue()), Util.ioPool()))
-+                                    .map(entry -> CompletableFuture.runAsync(() -> tryWrite(entry.getKey(), entry.getValue()), Util.DIMENSION_DATA_IO_POOL)) // Paper - Separate dimension data IO pool
-                                     .toArray(CompletableFuture[]::new)
-                             )
-                     );
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/storage/LevelStorageSource.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/storage/LevelStorageSource.java.patch
deleted file mode 100644
index e0a1e52b18..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/storage/LevelStorageSource.java.patch
+++ /dev/null
@@ -1,107 +0,0 @@
---- a/net/minecraft/world/level/storage/LevelStorageSource.java
-+++ b/net/minecraft/world/level/storage/LevelStorageSource.java
-@@ -68,7 +68,6 @@
- import net.minecraft.world.level.Level;
- import net.minecraft.world.level.LevelSettings;
- import net.minecraft.world.level.WorldDataConfiguration;
--import net.minecraft.world.level.dimension.DimensionType;
- import net.minecraft.world.level.dimension.LevelStem;
- import net.minecraft.world.level.levelgen.WorldDimensions;
- import net.minecraft.world.level.levelgen.WorldGenSettings;
-@@ -149,7 +148,7 @@
-     }
- 
-     public static WorldDataConfiguration readDataConfig(Dynamic<?> dynamic) {
--        DataResult dataresult = WorldDataConfiguration.CODEC.parse(dynamic);
-+        DataResult<WorldDataConfiguration> dataresult = WorldDataConfiguration.CODEC.parse(dynamic); // CraftBukkit - decompile error
-         Logger logger = LevelStorageSource.LOGGER;
- 
-         Objects.requireNonNull(logger);
-@@ -168,6 +167,7 @@
-         WorldDimensions.Complete worlddimensions_b = generatorsettings.dimensions().bake(dimensionsRegistry);
-         Lifecycle lifecycle = worlddimensions_b.lifecycle().add(registries.allRegistriesLifecycle());
-         PrimaryLevelData worlddataserver = PrimaryLevelData.parse(dynamic1, worldsettings, worlddimensions_b.specialWorldProperty(), generatorsettings.options(), lifecycle);
-+        worlddataserver.pdc = ((Dynamic<Tag>) dynamic1).getElement("BukkitValues", null); // CraftBukkit - Add PDC to world
- 
-         return new LevelDataAndDimensions(worlddataserver, worlddimensions_b);
-     }
-@@ -409,26 +409,40 @@
-         return this.backupDir;
-     }
- 
--    public LevelStorageSource.LevelStorageAccess validateAndCreateAccess(String directoryName) throws IOException, ContentValidationException {
--        Path path = this.getLevelPath(directoryName);
--        List<ForbiddenSymlinkInfo> list = this.worldDirValidator.validateDirectory(path, true);
-+    public LevelStorageSource.LevelStorageAccess validateAndCreateAccess(String s, ResourceKey<LevelStem> dimensionType) throws IOException, ContentValidationException { // CraftBukkit
-+        Path path = this.getLevelPath(s);
-+        List<ForbiddenSymlinkInfo> list = Boolean.getBoolean("paper.disableWorldSymlinkValidation") ? List.of() : this.worldDirValidator.validateDirectory(path, true); // Paper - add skipping of symlinks scan
- 
-         if (!list.isEmpty()) {
-             throw new ContentValidationException(path, list);
-         } else {
--            return new LevelStorageSource.LevelStorageAccess(directoryName, path);
-+            return new LevelStorageSource.LevelStorageAccess(s, path, dimensionType); // CraftBukkit
-         }
-     }
- 
--    public LevelStorageSource.LevelStorageAccess createAccess(String directoryName) throws IOException {
--        Path path = this.getLevelPath(directoryName);
-+    public LevelStorageSource.LevelStorageAccess createAccess(String s, ResourceKey<LevelStem> dimensionType) throws IOException { // CraftBukkit
-+        Path path = this.getLevelPath(s);
- 
--        return new LevelStorageSource.LevelStorageAccess(directoryName, path);
-+        return new LevelStorageSource.LevelStorageAccess(s, path, dimensionType); // CraftBukkit
-     }
- 
-     public DirectoryValidator getWorldDirValidator() {
-         return this.worldDirValidator;
-+    }
-+
-+    // CraftBukkit start
-+    public static Path getStorageFolder(Path path, ResourceKey<LevelStem> dimensionType) {
-+        if (dimensionType == LevelStem.OVERWORLD) {
-+            return path;
-+        } else if (dimensionType == LevelStem.NETHER) {
-+            return path.resolve("DIM-1");
-+        } else if (dimensionType == LevelStem.END) {
-+            return path.resolve("DIM1");
-+        } else {
-+            return path.resolve("dimensions").resolve(dimensionType.location().getNamespace()).resolve(dimensionType.location().getPath());
-+        }
-     }
-+    // CraftBukkit end
- 
-     public static record LevelCandidates(List<LevelStorageSource.LevelDirectory> levels) implements Iterable<LevelStorageSource.LevelDirectory> {
- 
-@@ -488,8 +502,12 @@
-         public final LevelStorageSource.LevelDirectory levelDirectory;
-         private final String levelId;
-         private final Map<LevelResource, Path> resources = Maps.newHashMap();
-+        // CraftBukkit start
-+        public final ResourceKey<LevelStem> dimensionType;
- 
--        LevelStorageAccess(final String s, final Path path) throws IOException {
-+        LevelStorageAccess(final String s, final Path path, final ResourceKey<LevelStem> dimensionType) throws IOException {
-+            this.dimensionType = dimensionType;
-+            // CraftBukkit end
-             this.levelId = s;
-             this.levelDirectory = new LevelStorageSource.LevelDirectory(path);
-             this.lock = DirectoryLock.create(path);
-@@ -529,7 +547,7 @@
-         }
- 
-         public Path getLevelPath(LevelResource savePath) {
--            Map map = this.resources;
-+            Map<LevelResource, Path> map = this.resources; // CraftBukkit - decompile error
-             LevelStorageSource.LevelDirectory convertable_b = this.levelDirectory;
- 
-             Objects.requireNonNull(this.levelDirectory);
-@@ -537,7 +555,7 @@
-         }
- 
-         public Path getDimensionPath(ResourceKey<Level> key) {
--            return DimensionType.getStorageFolder(key, this.levelDirectory.path());
-+            return LevelStorageSource.getStorageFolder(this.levelDirectory.path(), this.dimensionType); // CraftBukkit
-         }
- 
-         private void checkLock() {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/storage/PlayerDataStorage.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/storage/PlayerDataStorage.java.patch
deleted file mode 100644
index 6ed0222e60..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/storage/PlayerDataStorage.java.patch
+++ /dev/null
@@ -1,143 +0,0 @@
---- a/net/minecraft/world/level/storage/PlayerDataStorage.java
-+++ b/net/minecraft/world/level/storage/PlayerDataStorage.java
-@@ -15,8 +15,10 @@
- import net.minecraft.nbt.NbtAccounter;
- import net.minecraft.nbt.NbtIo;
- import net.minecraft.nbt.NbtUtils;
-+import net.minecraft.server.level.ServerPlayer;
- import net.minecraft.util.datafix.DataFixTypes;
- import net.minecraft.world.entity.player.Player;
-+import org.bukkit.craftbukkit.entity.CraftPlayer;
- import org.slf4j.Logger;
- 
- public class PlayerDataStorage {
-@@ -33,6 +35,7 @@
-     }
- 
-     public void save(Player player) {
-+        if (org.spigotmc.SpigotConfig.disablePlayerDataSaving) return; // Spigot
-         try {
-             CompoundTag nbttagcompound = player.saveWithoutId(new CompoundTag());
-             Path path = this.playerDir.toPath();
-@@ -44,39 +47,60 @@
- 
-             Util.safeReplaceFile(path2, path1, path3);
-         } catch (Exception exception) {
--            PlayerDataStorage.LOGGER.warn("Failed to save player data for {}", player.getName().getString());
-+            PlayerDataStorage.LOGGER.warn("Failed to save player data for {}", player.getScoreboardName(), exception); // Paper - Print exception
-         }
- 
-     }
- 
--    private void backup(Player player, String extension) {
-+    private void backup(String name, String s1, String s) { // name, uuid, extension
-         Path path = this.playerDir.toPath();
--        String s1 = player.getStringUUID();
--        Path path1 = path.resolve(s1 + extension);
-+        // String s1 = entityhuman.getStringUUID(); // CraftBukkit - used above
-+        Path path1 = path.resolve(s1 + s);
- 
--        s1 = player.getStringUUID();
--        Path path2 = path.resolve(s1 + "_corrupted_" + LocalDateTime.now().format(PlayerDataStorage.FORMATTER) + extension);
-+        // s1 = entityhuman.getStringUUID(); // CraftBukkit - used above
-+        Path path2 = path.resolve(s1 + "_corrupted_" + LocalDateTime.now().format(PlayerDataStorage.FORMATTER) + s);
- 
-         if (Files.isRegularFile(path1, new LinkOption[0])) {
-             try {
-                 Files.copy(path1, path2, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
-             } catch (Exception exception) {
--                PlayerDataStorage.LOGGER.warn("Failed to copy the player.dat file for {}", player.getName().getString(), exception);
-+                PlayerDataStorage.LOGGER.warn("Failed to copy the player.dat file for {}", name, exception); // CraftBukkit
-             }
- 
-         }
-     }
- 
--    private Optional<CompoundTag> load(Player player, String extension) {
-+    // CraftBukkit start
-+    private Optional<CompoundTag> load(String name, String s1, String s) { // name, uuid, extension
-+        // CraftBukkit end
-         File file = this.playerDir;
--        String s1 = player.getStringUUID();
--        File file1 = new File(file, s1 + extension);
-+        // String s1 = entityhuman.getStringUUID(); // CraftBukkit - used above
-+        File file1 = new File(file, s1 + s);
-+        // Spigot Start
-+        boolean usingWrongFile = false;
-+        if ( org.bukkit.Bukkit.getOnlineMode() && !file1.exists() ) // Paper - Check online mode first
-+        {
-+            file1 = new File( file, java.util.UUID.nameUUIDFromBytes( ( "OfflinePlayer:" + name ).getBytes( java.nio.charset.StandardCharsets.UTF_8 ) ).toString() + s );
-+            if ( file1.exists() )
-+            {
-+                usingWrongFile = true;
-+                org.bukkit.Bukkit.getServer().getLogger().warning( "Using offline mode UUID file for player " + name + " as it is the only copy we can find." );
-+            }
-+        }
-+        // Spigot End
- 
-         if (file1.exists() && file1.isFile()) {
-             try {
--                return Optional.of(NbtIo.readCompressed(file1.toPath(), NbtAccounter.unlimitedHeap()));
-+                // Spigot Start
-+                Optional<CompoundTag> optional = Optional.of(NbtIo.readCompressed(file1.toPath(), NbtAccounter.unlimitedHeap()));
-+                if ( usingWrongFile )
-+                {
-+                    file1.renameTo( new File( file1.getPath() + ".offline-read" ) );
-+                }
-+                return optional;
-+                // Spigot End
-             } catch (Exception exception) {
--                PlayerDataStorage.LOGGER.warn("Failed to load player data for {}", player.getName().getString());
-+                PlayerDataStorage.LOGGER.warn("Failed to load player data for {}", name); // CraftBukkit
-             }
-         }
- 
-@@ -84,20 +108,44 @@
-     }
- 
-     public Optional<CompoundTag> load(Player player) {
--        Optional<CompoundTag> optional = this.load(player, ".dat");
-+        // CraftBukkit start
-+        return this.load(player.getName().getString(), player.getStringUUID()).map((nbttagcompound) -> {
-+            if (player instanceof ServerPlayer) {
-+                CraftPlayer player1 = (CraftPlayer) player.getBukkitEntity();
-+                // Only update first played if it is older than the one we have
-+                long modified = new File(this.playerDir, player.getStringUUID() + ".dat").lastModified();
-+                if (modified < player1.getFirstPlayed()) {
-+                    player1.setFirstPlayed(modified);
-+                }
-+            }
- 
-+            player.load(nbttagcompound); // From below
-+            return nbttagcompound;
-+        });
-+    }
-+
-+    public Optional<CompoundTag> load(String name, String uuid) {
-+        // CraftBukkit end
-+        Optional<CompoundTag> optional = this.load(name, uuid, ".dat"); // CraftBukkit
-+
-         if (optional.isEmpty()) {
--            this.backup(player, ".dat");
-+            this.backup(name, uuid, ".dat"); // CraftBukkit
-         }
- 
-         return optional.or(() -> {
--            return this.load(player, ".dat_old");
-+            return this.load(name, uuid, ".dat_old"); // CraftBukkit
-         }).map((nbttagcompound) -> {
-             int i = NbtUtils.getDataVersion(nbttagcompound, -1);
- 
-             nbttagcompound = DataFixTypes.PLAYER.updateToCurrentVersion(this.fixerUpper, nbttagcompound, i);
--            player.load(nbttagcompound);
-+            // entityhuman.load(nbttagcompound); // CraftBukkit - handled above
-             return nbttagcompound;
-         });
-     }
-+
-+    // CraftBukkit start
-+    public File getPlayerDir() {
-+        return this.playerDir;
-+    }
-+    // CraftBukkit end
- }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/storage/PrimaryLevelData.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/storage/PrimaryLevelData.java.patch
deleted file mode 100644
index 131a7c275b..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/storage/PrimaryLevelData.java.patch
+++ /dev/null
@@ -1,203 +0,0 @@
---- a/net/minecraft/world/level/storage/PrimaryLevelData.java
-+++ b/net/minecraft/world/level/storage/PrimaryLevelData.java
-@@ -20,15 +20,12 @@
- import net.minecraft.SharedConstants;
- import net.minecraft.Util;
- import net.minecraft.core.BlockPos;
-+import net.minecraft.core.Registry;
- import net.minecraft.core.RegistryAccess;
- import net.minecraft.core.UUIDUtil;
--import net.minecraft.nbt.CompoundTag;
--import net.minecraft.nbt.ListTag;
--import net.minecraft.nbt.NbtOps;
--import net.minecraft.nbt.NbtUtils;
--import net.minecraft.nbt.StringTag;
--import net.minecraft.nbt.Tag;
- import net.minecraft.server.MinecraftServer;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
- import net.minecraft.world.Difficulty;
- import net.minecraft.world.level.GameRules;
- import net.minecraft.world.level.GameType;
-@@ -36,12 +33,26 @@
- import net.minecraft.world.level.LevelSettings;
- import net.minecraft.world.level.WorldDataConfiguration;
- import net.minecraft.world.level.border.WorldBorder;
-+import net.minecraft.world.level.dimension.LevelStem;
- import net.minecraft.world.level.dimension.end.EndDragonFight;
--import net.minecraft.world.level.levelgen.WorldGenSettings;
- import net.minecraft.world.level.levelgen.WorldOptions;
- import net.minecraft.world.level.timers.TimerCallbacks;
- import net.minecraft.world.level.timers.TimerQueue;
- import org.slf4j.Logger;
-+import net.minecraft.core.registries.Registries;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.nbt.ListTag;
-+import net.minecraft.nbt.NbtOps;
-+import net.minecraft.nbt.NbtUtils;
-+import net.minecraft.nbt.StringTag;
-+import net.minecraft.nbt.Tag;
-+import net.minecraft.network.protocol.game.ClientboundChangeDifficultyPacket;
-+import net.minecraft.world.level.levelgen.WorldDimensions;
-+import net.minecraft.world.level.levelgen.WorldGenSettings;
-+import org.bukkit.Bukkit;
-+import org.bukkit.event.weather.ThunderChangeEvent;
-+import org.bukkit.event.weather.WeatherChangeEvent;
-+// CraftBukkit end
- 
- public class PrimaryLevelData implements ServerLevelData, WorldData {
- 
-@@ -79,7 +90,21 @@
-     private boolean wasModded;
-     private final Set<String> removedFeatureFlags;
-     private final TimerQueue<MinecraftServer> scheduledEvents;
-+    // CraftBukkit start - Add world and pdc
-+    public Registry<LevelStem> customDimensions;
-+    private ServerLevel world;
-+    protected Tag pdc;
- 
-+    public void setWorld(ServerLevel world) {
-+        if (this.world != null) {
-+            return;
-+        }
-+        this.world = world;
-+        world.getWorld().readBukkitValues(this.pdc);
-+        this.pdc = null;
-+    }
-+    // CraftBukkit end
-+
-     private PrimaryLevelData(@Nullable CompoundTag playerData, boolean modded, BlockPos spawnPos, float spawnAngle, long time, long timeOfDay, int version, int clearWeatherTime, int rainTime, boolean raining, int thunderTime, boolean thundering, boolean initialized, boolean difficultyLocked, WorldBorder.Settings worldBorder, int wanderingTraderSpawnDelay, int wanderingTraderSpawnChance, @Nullable UUID wanderingTraderId, Set<String> serverBrands, Set<String> removedFeatures, TimerQueue<MinecraftServer> scheduledEvents, @Nullable CompoundTag customBossEvents, EndDragonFight.Data dragonFight, LevelSettings levelInfo, WorldOptions generatorOptions, PrimaryLevelData.SpecialWorldProperty specialProperty, Lifecycle lifecycle) {
-         this.wasModded = modded;
-         this.spawnPos = spawnPos;
-@@ -116,7 +141,7 @@
- 
-     public static <T> PrimaryLevelData parse(Dynamic<T> dynamic, LevelSettings info, PrimaryLevelData.SpecialWorldProperty specialProperty, WorldOptions generatorOptions, Lifecycle lifecycle) {
-         long i = dynamic.get("Time").asLong(0L);
--        OptionalDynamic optionaldynamic = dynamic.get("Player");
-+        OptionalDynamic<T> optionaldynamic = dynamic.get("Player"); // CraftBukkit - decompile error
-         Codec codec = CompoundTag.CODEC;
- 
-         Objects.requireNonNull(codec);
-@@ -136,7 +161,7 @@
-         WorldBorder.Settings worldborder_c = WorldBorder.Settings.read(dynamic, WorldBorder.DEFAULT_SETTINGS);
-         int k1 = dynamic.get("WanderingTraderSpawnDelay").asInt(0);
-         int l1 = dynamic.get("WanderingTraderSpawnChance").asInt(0);
--        UUID uuid = (UUID) dynamic.get("WanderingTraderId").read(UUIDUtil.CODEC).result().orElse((Object) null);
-+        UUID uuid = (UUID) dynamic.get("WanderingTraderId").read(UUIDUtil.CODEC).result().orElse(null); // CraftBukkit - decompile error
-         Set set = (Set) dynamic.get("ServerBrands").asStream().flatMap((dynamic1) -> {
-             return dynamic1.asString().result().stream();
-         }).collect(Collectors.toCollection(Sets::newLinkedHashSet));
-@@ -145,7 +170,7 @@
-         }).collect(Collectors.toSet());
-         TimerQueue customfunctioncallbacktimerqueue = new TimerQueue<>(TimerCallbacks.SERVER_CALLBACKS, dynamic.get("ScheduledEvents").asStream());
-         CompoundTag nbttagcompound1 = (CompoundTag) dynamic.get("CustomBossEvents").orElseEmptyMap().getValue();
--        DataResult dataresult = dynamic.get("DragonFight").read(EndDragonFight.Data.CODEC);
-+        DataResult<EndDragonFight.Data> dataresult = dynamic.get("DragonFight").read(EndDragonFight.Data.CODEC); // CraftBukkit - decompile error
-         Logger logger = PrimaryLevelData.LOGGER;
- 
-         Objects.requireNonNull(logger);
-@@ -180,7 +205,7 @@
-         levelNbt.put("Version", nbttagcompound2);
-         NbtUtils.addCurrentDataVersion(levelNbt);
-         DynamicOps<Tag> dynamicops = registryManager.createSerializationContext(NbtOps.INSTANCE);
--        DataResult dataresult = WorldGenSettings.encode(dynamicops, this.worldOptions, registryManager);
-+        DataResult<Tag> dataresult = WorldGenSettings.encode(dynamicops, this.worldOptions, new WorldDimensions(this.customDimensions != null ? this.customDimensions : registryManager.lookupOrThrow(Registries.LEVEL_STEM))); // CraftBukkit
-         Logger logger = PrimaryLevelData.LOGGER;
- 
-         Objects.requireNonNull(logger);
-@@ -230,11 +255,13 @@
-             levelNbt.putUUID("WanderingTraderId", this.wanderingTraderId);
-         }
- 
-+        levelNbt.putString("Bukkit.Version", Bukkit.getName() + "/" + Bukkit.getVersion() + "/" + Bukkit.getBukkitVersion()); // CraftBukkit
-+        this.world.getWorld().storeBukkitValues(levelNbt); // CraftBukkit - add pdc
-     }
- 
-     private static ListTag stringCollectionToTag(Set<String> strings) {
-         ListTag nbttaglist = new ListTag();
--        Stream stream = strings.stream().map(StringTag::valueOf);
-+        Stream<StringTag> stream = strings.stream().map(StringTag::valueOf); // CraftBukkit - decompile error
- 
-         Objects.requireNonNull(nbttaglist);
-         stream.forEach(nbttaglist::add);
-@@ -310,6 +337,25 @@
- 
-     @Override
-     public void setThundering(boolean thundering) {
-+        // Paper start - Add cause to Weather/ThunderChangeEvents
-+        this.setThundering(thundering, org.bukkit.event.weather.ThunderChangeEvent.Cause.UNKNOWN);
-+    }
-+    public void setThundering(boolean thundering, org.bukkit.event.weather.ThunderChangeEvent.Cause cause) {
-+        // Paper end - Add cause to Weather/ThunderChangeEvents
-+        // CraftBukkit start
-+        if (this.thundering == thundering) {
-+            return;
-+        }
-+
-+        org.bukkit.World world = Bukkit.getWorld(this.getLevelName());
-+        if (world != null) {
-+            ThunderChangeEvent thunder = new ThunderChangeEvent(world, thundering, cause); // Paper - Add cause to Weather/ThunderChangeEvents
-+            Bukkit.getServer().getPluginManager().callEvent(thunder);
-+            if (thunder.isCancelled()) {
-+                return;
-+            }
-+        }
-+        // CraftBukkit end
-         this.thundering = thundering;
-     }
- 
-@@ -330,6 +376,26 @@
- 
-     @Override
-     public void setRaining(boolean raining) {
-+        // Paper start - Add cause to Weather/ThunderChangeEvents
-+        this.setRaining(raining, org.bukkit.event.weather.WeatherChangeEvent.Cause.UNKNOWN);
-+    }
-+
-+    public void setRaining(boolean raining, org.bukkit.event.weather.WeatherChangeEvent.Cause cause) {
-+        // Paper end - Add cause to Weather/ThunderChangeEvents
-+        // CraftBukkit start
-+        if (this.raining == raining) {
-+            return;
-+        }
-+
-+        org.bukkit.World world = Bukkit.getWorld(this.getLevelName());
-+        if (world != null) {
-+            WeatherChangeEvent weather = new WeatherChangeEvent(world, raining, cause); // Paper - Add cause to Weather/ThunderChangeEvents
-+            Bukkit.getServer().getPluginManager().callEvent(weather);
-+            if (weather.isCancelled()) {
-+                return;
-+            }
-+        }
-+        // CraftBukkit end
-         this.raining = raining;
-     }
- 
-@@ -396,6 +462,12 @@
-     @Override
-     public void setDifficulty(Difficulty difficulty) {
-         this.settings = this.settings.withDifficulty(difficulty);
-+        // CraftBukkit start
-+        ClientboundChangeDifficultyPacket packet = new ClientboundChangeDifficultyPacket(this.getDifficulty(), this.isDifficultyLocked());
-+        for (ServerPlayer player : (java.util.List<ServerPlayer>) (java.util.List) this.world.players()) {
-+            player.connection.send(packet);
-+        }
-+        // CraftBukkit end
-     }
- 
-     @Override
-@@ -532,6 +604,14 @@
-         return this.settings.copy();
-     }
- 
-+    // CraftBukkit start - Check if the name stored in NBT is the correct one
-+    public void checkName(String name) {
-+        if (!this.settings.levelName.equals(name)) {
-+            this.settings.levelName = name;
-+        }
-+    }
-+    // CraftBukkit end
-+
-     /** @deprecated */
-     @Deprecated
-     public static enum SpecialWorldProperty {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/storage/loot/LootDataType.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/storage/loot/LootDataType.java.patch
deleted file mode 100644
index c9f13ba298..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/storage/loot/LootDataType.java.patch
+++ /dev/null
@@ -1,22 +0,0 @@
---- a/net/minecraft/world/level/storage/loot/LootDataType.java
-+++ b/net/minecraft/world/level/storage/loot/LootDataType.java
-@@ -9,6 +9,11 @@
- import net.minecraft.world.level.storage.loot.functions.LootItemFunctions;
- import net.minecraft.world.level.storage.loot.predicates.LootItemCondition;
- 
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.CraftLootTable;
-+import org.bukkit.craftbukkit.util.CraftNamespacedKey;
-+// CraftBukkit end
-+
- public record LootDataType<T>(ResourceKey<Registry<T>> registryKey, Codec<T> codec, LootDataType.Validator<T> validator) {
- 
-     public static final LootDataType<LootItemCondition> PREDICATE = new LootDataType<>(Registries.PREDICATE, LootItemCondition.DIRECT_CODEC, createSimpleValidator());
-@@ -32,6 +37,7 @@
-     private static LootDataType.Validator<LootTable> createLootTableValidator() {
-         return (lootcollector, resourcekey, loottable) -> {
-             loottable.validate(lootcollector.setContextKeySet(loottable.getParamSet()).enterElement("{" + String.valueOf(resourcekey.registry()) + "/" + String.valueOf(resourcekey.location()) + "}", resourcekey));
-+            loottable.craftLootTable = new CraftLootTable(CraftNamespacedKey.fromMinecraft(resourcekey.location()), loottable); // CraftBukkit
-         };
-     }
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/storage/loot/LootTable.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/storage/loot/LootTable.java.patch
deleted file mode 100644
index 29133b2303..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/storage/loot/LootTable.java.patch
+++ /dev/null
@@ -1,85 +0,0 @@
---- a/net/minecraft/world/level/storage/loot/LootTable.java
-+++ b/net/minecraft/world/level/storage/loot/LootTable.java
-@@ -31,6 +31,13 @@
- import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets;
- import org.slf4j.Logger;
- 
-+// CraftBukkit start
-+import org.bukkit.craftbukkit.CraftLootTable;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.craftbukkit.inventory.CraftItemStack;
-+import org.bukkit.event.world.LootGenerateEvent;
-+// CraftBukkit end
-+
- public class LootTable {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
-@@ -54,6 +61,7 @@
-     private final List<LootPool> pools;
-     private final List<LootItemFunction> functions;
-     private final BiFunction<ItemStack, LootContext, ItemStack> compositeFunction;
-+    public CraftLootTable craftLootTable; // CraftBukkit
- 
-     LootTable(ContextKeySet type, Optional<ResourceLocation> randomSequenceId, List<LootPool> pools, List<LootItemFunction> functions) {
-         this.paramSet = type;
-@@ -64,9 +72,10 @@
-     }
- 
-     public static Consumer<ItemStack> createStackSplitter(ServerLevel world, Consumer<ItemStack> consumer) {
-+        boolean skipSplitter = world != null && !world.paperConfig().fixes.splitOverstackedLoot; // Paper - preserve overstacked items
-         return (itemstack) -> {
-             if (itemstack.isItemEnabled(world.enabledFeatures())) {
--                if (itemstack.getCount() < itemstack.getMaxStackSize()) {
-+                if (skipSplitter || itemstack.getCount() < itemstack.getMaxStackSize()) { // Paper - preserve overstacked items
-                     consumer.accept(itemstack);
-                 } else {
-                     int i = itemstack.getCount();
-@@ -157,10 +166,23 @@
-     }
- 
-     public void fill(Container inventory, LootParams parameters, long seed) {
--        LootContext loottableinfo = (new LootContext.Builder(parameters)).withOptionalRandomSeed(seed).create(this.randomSequence);
-+        // CraftBukkit start
-+        this.fillInventory(inventory, parameters, seed, false);
-+    }
-+
-+    public void fillInventory(Container iinventory, LootParams lootparams, long i, boolean plugin) {
-+        // CraftBukkit end
-+        LootContext loottableinfo = (new LootContext.Builder(lootparams)).withOptionalRandomSeed(i).create(this.randomSequence);
-         ObjectArrayList<ItemStack> objectarraylist = this.getRandomItems(loottableinfo);
-         RandomSource randomsource = loottableinfo.getRandom();
--        List<Integer> list = this.getAvailableSlots(inventory, randomsource);
-+        // CraftBukkit start
-+        LootGenerateEvent event = CraftEventFactory.callLootGenerateEvent(iinventory, this, loottableinfo, objectarraylist, plugin);
-+        if (event.isCancelled()) {
-+            return;
-+        }
-+        objectarraylist = event.getLoot().stream().map(CraftItemStack::asNMSCopy).collect(ObjectArrayList.toList());
-+        // CraftBukkit end
-+        List<Integer> list = this.getAvailableSlots(iinventory, randomsource);
- 
-         this.shuffleAndSplitItems(objectarraylist, list.size(), randomsource);
-         ObjectListIterator objectlistiterator = objectarraylist.iterator();
-@@ -174,9 +196,9 @@
-             }
- 
-             if (itemstack.isEmpty()) {
--                inventory.setItem((Integer) list.remove(list.size() - 1), ItemStack.EMPTY);
-+                iinventory.setItem((Integer) list.remove(list.size() - 1), ItemStack.EMPTY);
-             } else {
--                inventory.setItem((Integer) list.remove(list.size() - 1), itemstack);
-+                iinventory.setItem((Integer) list.remove(list.size() - 1), itemstack);
-             }
-         }
- 
-@@ -238,8 +260,8 @@
- 
-     public static class Builder implements FunctionUserBuilder<LootTable.Builder> {
- 
--        private final Builder<LootPool> pools = ImmutableList.builder();
--        private final Builder<LootItemFunction> functions = ImmutableList.builder();
-+        private final ImmutableList.Builder<LootPool> pools = ImmutableList.builder();
-+        private final ImmutableList.Builder<LootItemFunction> functions = ImmutableList.builder();
-         private ContextKeySet paramSet;
-         private Optional<ResourceLocation> randomSequence;
- 
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/storage/loot/functions/ExplorationMapFunction.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/storage/loot/functions/ExplorationMapFunction.java.patch
deleted file mode 100644
index 130a1b59c6..0000000000
--- a/paper-server/patches/unapplied/net/minecraft/world/level/storage/loot/functions/ExplorationMapFunction.java.patch
+++ /dev/null
@@ -1,21 +0,0 @@
---- a/net/minecraft/world/level/storage/loot/functions/ExplorationMapFunction.java
-+++ b/net/minecraft/world/level/storage/loot/functions/ExplorationMapFunction.java
-@@ -83,8 +83,17 @@
-             Vec3 vec3 = context.getOptionalParameter(LootContextParams.ORIGIN);
-             if (vec3 != null) {
-                 ServerLevel serverLevel = context.getLevel();
-+                // Paper start - Configurable cartographer treasure maps
-+                if (!serverLevel.paperConfig().environment.treasureMaps.enabled) {
-+                    /*
-+                     * NOTE: I fear users will just get a plain map as their "treasure"
-+                     * This is preferable to disrespecting the config.
-+                     */
-+                    return stack;
-+                }
-+                // Paper end - Configurable cartographer treasure maps
-                 BlockPos blockPos = serverLevel.findNearestMapStructure(
--                    this.destination, BlockPos.containing(vec3), this.searchRadius, this.skipKnownStructures
-+                    this.destination, BlockPos.containing(vec3), this.searchRadius, !serverLevel.paperConfig().environment.treasureMaps.findAlreadyDiscoveredLootTable.or(!this.skipKnownStructures) // Paper - Configurable cartographer treasure maps
-                 );
-                 if (blockPos != null) {
-                     ItemStack itemStack = MapItem.create(serverLevel, blockPos.getX(), blockPos.getZ(), this.zoom, true, true);
diff --git a/paper-server/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java b/paper-server/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java
index 6c98d420ea..9b879cbc03 100644
--- a/paper-server/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java
+++ b/paper-server/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java
@@ -1,5 +1,6 @@
 package ca.spottedleaf.moonrise.common;
 
+import ca.spottedleaf.moonrise.common.util.ChunkSystemHooks;
 import com.mojang.datafixers.DSL;
 import com.mojang.datafixers.DataFixer;
 import net.minecraft.core.BlockPos;
@@ -23,7 +24,7 @@ import java.util.List;
 import java.util.ServiceLoader;
 import java.util.function.Predicate;
 
-public interface PlatformHooks {
+public interface PlatformHooks extends ChunkSystemHooks {
     public static PlatformHooks get() {
         return Holder.INSTANCE;
     }
@@ -63,8 +64,6 @@ public interface PlatformHooks {
 
     public void entityMove(final Entity entity, final long oldSection, final long newSection);
 
-    public boolean screenEntity(final ServerLevel world, final Entity entity, final boolean fromDisk, final boolean event);
-
     public boolean configFixMC224294();
 
     public boolean configAutoConfigSendDistance();
diff --git a/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java b/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
deleted file mode 100644
index 58a99bc38e..0000000000
--- a/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
+++ /dev/null
@@ -1,288 +0,0 @@
-package ca.spottedleaf.moonrise.common.util;
-
-import ca.spottedleaf.concurrentutil.util.Priority;
-import ca.spottedleaf.moonrise.common.PlatformHooks;
-import com.mojang.logging.LogUtils;
-import net.minecraft.server.level.ChunkHolder;
-import net.minecraft.server.level.FullChunkStatus;
-import net.minecraft.server.level.ServerLevel;
-import net.minecraft.server.level.ServerPlayer;
-import net.minecraft.world.entity.Entity;
-import net.minecraft.world.level.chunk.ChunkAccess;
-import net.minecraft.world.level.chunk.LevelChunk;
-import net.minecraft.world.level.chunk.status.ChunkStatus;
-import org.slf4j.Logger;
-import java.util.List;
-import java.util.function.Consumer;
-
-public final class ChunkSystem {
-
-    private static final Logger LOGGER = LogUtils.getLogger();
-    private static final net.minecraft.world.level.chunk.status.ChunkStep FULL_CHUNK_STEP = net.minecraft.world.level.chunk.status.ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FULL);
-
-    private static int getDistance(final ChunkStatus status) {
-        return FULL_CHUNK_STEP.getAccumulatedRadiusOf(status);
-    }
-
-    public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) {
-        scheduleChunkTask(level, chunkX, chunkZ, run, Priority.NORMAL);
-    }
-
-    public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final Priority priority) {
-        level.chunkSource.mainThreadProcessor.execute(run);
-    }
-
-    public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen,
-                                         final ChunkStatus toStatus, final boolean addTicket, final Priority priority,
-                                         final Consumer<ChunkAccess> onComplete) {
-        if (gen) {
-            scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
-            return;
-        }
-        scheduleChunkLoad(level, chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> {
-            if (chunk == null) {
-                if (onComplete != null) {
-                    onComplete.accept(null);
-                }
-            } else {
-                if (chunk.getPersistedStatus().isOrAfter(toStatus)) {
-                    scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
-                } else {
-                    if (onComplete != null) {
-                        onComplete.accept(null);
-                    }
-                }
-            }
-        });
-    }
-
-    static final net.minecraft.server.level.TicketType<Long> CHUNK_LOAD = net.minecraft.server.level.TicketType.create("chunk_load", Long::compareTo);
-
-    private static long chunkLoadCounter = 0L;
-    public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus,
-                                         final boolean addTicket, final Priority priority, final Consumer<ChunkAccess> onComplete) {
-        if (!org.bukkit.Bukkit.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) {
-            scheduleChunkTask(level, chunkX, chunkZ, () -> {
-                scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
-            }, priority);
-            return;
-        }
-
-        final int minLevel = 33 + getDistance(toStatus);
-        final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
-        final net.minecraft.world.level.ChunkPos chunkPos = new net.minecraft.world.level.ChunkPos(chunkX, chunkZ);
-
-        if (addTicket) {
-            level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
-        }
-        level.chunkSource.runDistanceManagerUpdates();
-
-        final Consumer<ChunkAccess> loadCallback = (final ChunkAccess chunk) -> {
-            try {
-                if (onComplete != null) {
-                    onComplete.accept(chunk);
-                }
-            } catch (final Throwable thr) {
-                LOGGER.error("Exception handling chunk load callback", thr);
-                com.destroystokyo.paper.util.SneakyThrow.sneaky(thr);
-            } finally {
-                if (addTicket) {
-                    level.chunkSource.addTicketAtLevel(net.minecraft.server.level.TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
-                    level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
-                }
-            }
-        };
-
-        final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-
-        if (holder == null || holder.getTicketLevel() > minLevel) {
-            loadCallback.accept(null);
-            return;
-        }
-
-        final java.util.concurrent.CompletableFuture<net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.ChunkAccess>> loadFuture = holder.scheduleChunkGenerationTask(toStatus, level.chunkSource.chunkMap);
-
-        if (loadFuture.isDone()) {
-            loadCallback.accept(loadFuture.join().orElse(null));
-            return;
-        }
-
-        loadFuture.whenCompleteAsync((final net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.ChunkAccess> result, final Throwable thr) -> {
-            if (thr != null) {
-                loadCallback.accept(null);
-                return;
-            }
-            loadCallback.accept(result.orElse(null));
-        }, (final Runnable r) -> {
-            scheduleChunkTask(level, chunkX, chunkZ, r, Priority.HIGHEST);
-        });
-    }
-
-    public static void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ,
-                                            final FullChunkStatus toStatus, final boolean addTicket,
-                                            final Priority priority, final Consumer<LevelChunk> onComplete) {
-        // This method goes unused until the chunk system rewrite
-        if (toStatus == FullChunkStatus.INACCESSIBLE) {
-            throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status");
-        }
-
-        if (!org.bukkit.Bukkit.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) {
-            scheduleChunkTask(level, chunkX, chunkZ, () -> {
-                scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
-            }, priority);
-            return;
-        }
-
-        final int minLevel = 33 - (toStatus.ordinal() - 1);
-        final int radius = toStatus.ordinal() - 1;
-        final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
-        final net.minecraft.world.level.ChunkPos chunkPos = new net.minecraft.world.level.ChunkPos(chunkX, chunkZ);
-
-        if (addTicket) {
-            level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
-        }
-        level.chunkSource.runDistanceManagerUpdates();
-
-        final Consumer<LevelChunk> loadCallback = (final LevelChunk chunk) -> {
-            try {
-                if (onComplete != null) {
-                    onComplete.accept(chunk);
-                }
-            } catch (final Throwable thr) {
-                LOGGER.error("Exception handling chunk load callback", thr);
-                com.destroystokyo.paper.util.SneakyThrow.sneaky(thr);
-            } finally {
-                if (addTicket) {
-                    level.chunkSource.addTicketAtLevel(net.minecraft.server.level.TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
-                    level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
-                }
-            }
-        };
-
-        final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-
-        if (holder == null || holder.getTicketLevel() > minLevel) {
-            loadCallback.accept(null);
-            return;
-        }
-
-        final java.util.concurrent.CompletableFuture<net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.LevelChunk>> tickingState;
-        switch (toStatus) {
-            case FULL: {
-                tickingState = holder.getFullChunkFuture();
-                break;
-            }
-            case BLOCK_TICKING: {
-                tickingState = holder.getTickingChunkFuture();
-                break;
-            }
-            case ENTITY_TICKING: {
-                tickingState = holder.getEntityTickingChunkFuture();
-                break;
-            }
-            default: {
-                throw new IllegalStateException("Cannot reach here");
-            }
-        }
-
-        if (tickingState.isDone()) {
-            loadCallback.accept(tickingState.join().orElse(null));
-            return;
-        }
-
-        tickingState.whenCompleteAsync((final net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.LevelChunk> result, final Throwable thr) -> {
-            if (thr != null) {
-                loadCallback.accept(null);
-                return;
-            }
-            loadCallback.accept(result.orElse(null));
-        }, (final Runnable r) -> {
-            scheduleChunkTask(level, chunkX, chunkZ, r, Priority.HIGHEST);
-        });
-    }
-
-    public static List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) {
-        return new java.util.ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values());
-    }
-
-    public static List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level) {
-        return new java.util.ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values());
-    }
-
-    public static int getVisibleChunkHolderCount(final ServerLevel level) {
-        return level.chunkSource.chunkMap.visibleChunkMap.size();
-    }
-
-    public static int getUpdatingChunkHolderCount(final ServerLevel level) {
-        return level.chunkSource.chunkMap.updatingChunkMap.size();
-    }
-
-    public static boolean hasAnyChunkHolders(final ServerLevel level) {
-        return getUpdatingChunkHolderCount(level) != 0;
-    }
-
-    public static boolean screenEntity(final ServerLevel level, final Entity entity, final boolean fromDisk, final boolean event) {
-        if (!PlatformHooks.get().screenEntity(level, entity, fromDisk, event)) {
-            return false;
-        }
-        return true;
-    }
-
-    public static void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) {
-
-    }
-
-    public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) {
-
-    }
-
-    public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) {
-
-    }
-
-    public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
-
-    }
-
-    public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) {
-
-    }
-
-    public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) {
-
-    }
-
-    public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
-
-    }
-
-    public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
-
-    }
-
-    public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) {
-        return level.chunkSource.chunkMap.getUnloadingChunkHolder(chunkX, chunkZ);
-    }
-
-    public static int getSendViewDistance(final ServerPlayer player) {
-        return getViewDistance(player);
-    }
-
-    public static int getViewDistance(final ServerPlayer player) {
-        final ServerLevel level = player.serverLevel();
-        if (level == null) {
-            return org.bukkit.Bukkit.getViewDistance();
-        }
-        return level.chunkSource.chunkMap.serverViewDistance;
-    }
-
-    public static int getTickViewDistance(final ServerPlayer player) {
-        final ServerLevel level = player.serverLevel();
-        if (level == null) {
-            return org.bukkit.Bukkit.getSimulationDistance();
-        }
-        return level.chunkSource.chunkMap.distanceManager.simulationDistance;
-    }
-
-    private ChunkSystem() {}
-}
diff --git a/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystemHooks.java b/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystemHooks.java
new file mode 100644
index 0000000000..427079ae47
--- /dev/null
+++ b/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystemHooks.java
@@ -0,0 +1,77 @@
+package ca.spottedleaf.moonrise.common.util;
+
+import ca.spottedleaf.concurrentutil.util.Priority;
+import net.minecraft.server.level.ChunkHolder;
+import net.minecraft.server.level.FullChunkStatus;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.LevelChunk;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import java.util.List;
+import java.util.function.Consumer;
+
+public interface ChunkSystemHooks {
+
+    public void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run);
+
+    public void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final Priority priority);
+
+    public void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen,
+                                  final ChunkStatus toStatus, final boolean addTicket, final Priority priority,
+                                  final Consumer<ChunkAccess> onComplete);
+
+    public void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus,
+                                  final boolean addTicket, final Priority priority, final Consumer<ChunkAccess> onComplete);
+
+    public void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ,
+                                     final FullChunkStatus toStatus, final boolean addTicket,
+                                     final Priority priority, final Consumer<LevelChunk> onComplete);
+
+    public List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level);
+
+    public List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level);
+
+    public int getVisibleChunkHolderCount(final ServerLevel level);
+
+    public int getUpdatingChunkHolderCount(final ServerLevel level);
+
+    public boolean hasAnyChunkHolders(final ServerLevel level);
+
+    public boolean screenEntity(final ServerLevel level, final Entity entity, final boolean fromDisk, final boolean event);
+
+    public void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder);
+
+    public void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder);
+
+    public void onChunkPreBorder(final LevelChunk chunk, final ChunkHolder holder);
+
+    public void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder);
+
+    public void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder);
+
+    public void onChunkPostNotBorder(final LevelChunk chunk, final ChunkHolder holder);
+
+    public void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder);
+
+    public void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder);
+
+    public void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder);
+
+    public void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder);
+
+    public ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ);
+
+    public int getSendViewDistance(final ServerPlayer player);
+
+    public int getViewDistance(final ServerPlayer player);
+
+    public int getTickViewDistance(final ServerPlayer player);
+
+    public void addPlayerToDistanceMaps(final ServerLevel world, final ServerPlayer player);
+
+    public void removePlayerFromDistanceMaps(final ServerLevel world, final ServerPlayer player);
+
+    public void updateMaps(final ServerLevel world, final ServerPlayer player);
+}
diff --git a/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java b/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java
index 12eb3add09..5239993a68 100644
--- a/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java
+++ b/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java
@@ -9,7 +9,7 @@ import net.minecraft.world.level.levelgen.PositionalRandomFactory;
 /**
  * Avoid costly CAS of superclass
  */
-public final class ThreadUnsafeRandom implements BitRandomSource {
+public class ThreadUnsafeRandom implements BitRandomSource { // Paper - replace random
 
     private static final long MULTIPLIER = 25214903917L;
     private static final long ADDEND = 11L;
diff --git a/paper-server/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java b/paper-server/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java
deleted file mode 100644
index 834c5ce238..0000000000
--- a/paper-server/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java
+++ /dev/null
@@ -1,240 +0,0 @@
-package ca.spottedleaf.moonrise.paper;
-
-import ca.spottedleaf.moonrise.common.PlatformHooks;
-import com.mojang.datafixers.DSL;
-import com.mojang.datafixers.DataFixer;
-import com.mojang.serialization.Dynamic;
-import java.util.Collection;
-import net.minecraft.core.BlockPos;
-import net.minecraft.nbt.CompoundTag;
-import net.minecraft.nbt.NbtOps;
-import net.minecraft.server.level.ChunkHolder;
-import net.minecraft.server.level.GenerationChunkHolder;
-import net.minecraft.server.level.ServerLevel;
-import net.minecraft.server.level.ServerPlayer;
-import net.minecraft.world.entity.Entity;
-import net.minecraft.world.entity.boss.EnderDragonPart;
-import net.minecraft.world.level.BlockGetter;
-import net.minecraft.world.level.ChunkPos;
-import net.minecraft.world.level.Level;
-import net.minecraft.world.level.block.state.BlockState;
-import net.minecraft.world.level.chunk.ChunkAccess;
-import net.minecraft.world.level.chunk.LevelChunk;
-import net.minecraft.world.level.chunk.ProtoChunk;
-import net.minecraft.world.level.chunk.storage.SerializableChunkData;
-import net.minecraft.world.level.entity.EntityTypeTest;
-import net.minecraft.world.phys.AABB;
-import java.util.List;
-import java.util.function.Predicate;
-
-public final class PaperHooks implements PlatformHooks {
-
-    @Override
-    public String getBrand() {
-        return "Paper";
-    }
-
-    @Override
-    public int getLightEmission(final BlockState blockState, final BlockGetter world, final BlockPos pos) {
-        return blockState.getLightEmission();
-    }
-
-    @Override
-    public Predicate<BlockState> maybeHasLightEmission() {
-        return (final BlockState state) -> {
-            return state.getLightEmission() != 0;
-        };
-    }
-
-    @Override
-    public boolean hasCurrentlyLoadingChunk() {
-        return false;
-    }
-
-    @Override
-    public LevelChunk getCurrentlyLoadingChunk(final GenerationChunkHolder holder) {
-        return null;
-    }
-
-    @Override
-    public void setCurrentlyLoading(final GenerationChunkHolder holder, final LevelChunk levelChunk) {
-
-    }
-
-    @Override
-    public void chunkFullStatusComplete(final LevelChunk newChunk, final ProtoChunk original) {
-
-    }
-
-    @Override
-    public boolean allowAsyncTicketUpdates() {
-        return true;
-    }
-
-    @Override
-    public void onChunkHolderTicketChange(final ServerLevel world, final ChunkHolder holder, final int oldLevel, final int newLevel) {
-
-    }
-
-    @Override
-    public void chunkUnloadFromWorld(final LevelChunk chunk) {
-
-    }
-
-    @Override
-    public void chunkSyncSave(final ServerLevel world, final ChunkAccess chunk, final SerializableChunkData data) {
-
-    }
-
-    @Override
-    public void onChunkWatch(final ServerLevel world, final LevelChunk chunk, final ServerPlayer player) {
-
-    }
-
-    @Override
-    public void onChunkUnWatch(final ServerLevel world, final ChunkPos chunk, final ServerPlayer player) {
-
-    }
-
-    @Override
-    public void addToGetEntities(final Level world, final Entity entity, final AABB boundingBox, final Predicate<? super Entity> predicate, final List<Entity> into) {
-        final Collection<EnderDragonPart> parts = world.dragonParts();
-        if (parts.isEmpty()) {
-            return;
-        }
-
-        for (final EnderDragonPart part : parts) {
-            if (part != entity && part.getBoundingBox().intersects(boundingBox) && (predicate == null || predicate.test(part))) {
-                into.add(part);
-            }
-        }
-    }
-
-    @Override
-    public <T extends Entity> void addToGetEntities(final Level world, final EntityTypeTest<Entity, T> entityTypeTest, final AABB boundingBox, final Predicate<? super T> predicate, final List<? super T> into, final int maxCount) {
-        if (into.size() >= maxCount) {
-            // fix neoforge issue: do not add if list is already full
-            return;
-        }
-
-        final Collection<EnderDragonPart> parts = world.dragonParts();
-        if (parts.isEmpty()) {
-            return;
-        }
-        for (final EnderDragonPart part : parts) {
-            if (!part.getBoundingBox().intersects(boundingBox)) {
-                continue;
-            }
-            final T casted = (T)entityTypeTest.tryCast(part);
-            if (casted != null && (predicate == null || predicate.test(casted))) {
-                into.add(casted);
-                if (into.size() >= maxCount) {
-                    break;
-                }
-            }
-        }
-    }
-
-    @Override
-    public void entityMove(final Entity entity, final long oldSection, final long newSection) {
-
-    }
-
-    @Override
-    public boolean screenEntity(final ServerLevel world, final Entity entity, final boolean fromDisk, final boolean event) {
-        return true;
-    }
-
-    @Override
-    public boolean configFixMC224294() {
-        return true;
-    }
-
-    @Override
-    public boolean configAutoConfigSendDistance() {
-        return io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.autoConfigSendDistance;
-    }
-
-    @Override
-    public double configPlayerMaxLoadRate() {
-        return io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkLoadRate;
-    }
-
-    @Override
-    public double configPlayerMaxGenRate() {
-        return io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkGenerateRate;
-    }
-
-    @Override
-    public double configPlayerMaxSendRate() {
-        return io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkSendRate;
-    }
-
-    @Override
-    public int configPlayerMaxConcurrentLoads() {
-        return io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkLoads;
-    }
-
-    @Override
-    public int configPlayerMaxConcurrentGens() {
-        return io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkGenerates;
-    }
-
-    @Override
-    public long configAutoSaveInterval(final ServerLevel world) {
-        return world.paperConfig().chunks.autoSaveInterval.value();
-    }
-
-    @Override
-    public int configMaxAutoSavePerTick(final ServerLevel world) {
-        return world.paperConfig().chunks.maxAutoSaveChunksPerTick;
-    }
-
-    @Override
-    public boolean configFixMC159283() {
-        return true;
-    }
-
-    @Override
-    public boolean forceNoSave(final ChunkAccess chunk) {
-        return chunk instanceof LevelChunk levelChunk && levelChunk.mustNotSave;
-    }
-
-    @Override
-    public CompoundTag convertNBT(final DSL.TypeReference type, final DataFixer dataFixer, final CompoundTag nbt,
-                                  final int fromVersion, final int toVersion) {
-        return (CompoundTag)dataFixer.update(
-            type, new Dynamic<>(NbtOps.INSTANCE, nbt), fromVersion, toVersion
-        ).getValue();
-    }
-
-    @Override
-    public boolean hasMainChunkLoadHook() {
-        return false;
-    }
-
-    @Override
-    public void mainChunkLoad(final ChunkAccess chunk, final SerializableChunkData chunkData) {
-
-    }
-
-    @Override
-    public List<Entity> modifySavedEntities(final ServerLevel world, final int chunkX, final int chunkZ, final List<Entity> entities) {
-        return entities;
-    }
-
-    @Override
-    public void unloadEntity(final Entity entity) {
-        entity.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK, org.bukkit.event.entity.EntityRemoveEvent.Cause.UNLOAD);
-    }
-
-    @Override
-    public void postLoadProtoChunk(final ServerLevel world, final ProtoChunk chunk) {
-        net.minecraft.world.level.chunk.status.ChunkStatusTasks.postLoadProtoChunk(world, chunk.getEntities());
-    }
-
-    @Override
-    public int modifyEntityTrackingRange(final Entity entity, final int currentRange) {
-        return org.spigotmc.TrackingRange.getEntityTrackingRange(entity, currentRange);
-    }
-}
diff --git a/paper-server/src/main/java/com/destroystokyo/paper/entity/PaperPathfinder.java b/paper-server/src/main/java/com/destroystokyo/paper/entity/PaperPathfinder.java
index 946cbc9556..3e43beaaa2 100644
--- a/paper-server/src/main/java/com/destroystokyo/paper/entity/PaperPathfinder.java
+++ b/paper-server/src/main/java/com/destroystokyo/paper/entity/PaperPathfinder.java
@@ -135,7 +135,7 @@ public class PaperPathfinder implements com.destroystokyo.paper.entity.Pathfinde
         @Nullable
         @Override
         public Location getNextPoint() {
-            if (!path.hasNext()) {
+            if (path.isDone()) {
                 return null;
             }
             return toLoc(path.nodes.get(path.getNextNodeIndex()));
diff --git a/paper-server/src/main/java/io/papermc/paper/FeatureHooks.java b/paper-server/src/main/java/io/papermc/paper/FeatureHooks.java
deleted file mode 100644
index cfd47bcdcd..0000000000
--- a/paper-server/src/main/java/io/papermc/paper/FeatureHooks.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package io.papermc.paper;
-
-import io.papermc.paper.command.PaperSubcommand;
-import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
-import it.unimi.dsi.fastutil.longs.LongSet;
-import it.unimi.dsi.fastutil.longs.LongSets;
-import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
-import it.unimi.dsi.fastutil.objects.ObjectSet;
-import it.unimi.dsi.fastutil.objects.ObjectSets;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import net.minecraft.core.Registry;
-import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket;
-import net.minecraft.server.level.ServerPlayer;
-import net.minecraft.world.level.ChunkPos;
-import net.minecraft.world.level.Level;
-import net.minecraft.world.level.biome.Biome;
-import net.minecraft.world.level.block.Block;
-import net.minecraft.world.level.block.Blocks;
-import net.minecraft.world.level.block.state.BlockState;
-import net.minecraft.world.level.chunk.LevelChunk;
-import net.minecraft.world.level.chunk.LevelChunkSection;
-import net.minecraft.world.level.chunk.PalettedContainer;
-import org.bukkit.Chunk;
-import org.bukkit.World;
-
-public final class FeatureHooks {
-
-    public static void initChunkTaskScheduler(final boolean useParallelGen) {
-    }
-
-    public static void registerPaperCommands(final Map<Set<String>, PaperSubcommand> commands) {
-    }
-
-    public static LevelChunkSection createSection(final Registry<Biome> biomeRegistry, final Level level, final ChunkPos chunkPos, final int chunkSection) {
-        return new LevelChunkSection(biomeRegistry);
-    }
-
-    public static void sendChunkRefreshPackets(final List<ServerPlayer> playersInRange, final LevelChunk chunk) {
-        final ClientboundLevelChunkWithLightPacket refreshPacket = new ClientboundLevelChunkWithLightPacket(chunk, chunk.level.getLightEngine(), null, null);
-        for (final ServerPlayer player : playersInRange) {
-            if (player.connection == null) continue;
-
-            player.connection.send(refreshPacket);
-        }
-    }
-
-    public static PalettedContainer<BlockState> emptyPalettedBlockContainer() {
-        return new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES);
-    }
-
-    public static Set<Long> getSentChunkKeys(final ServerPlayer player) {
-        final LongSet keys = new LongOpenHashSet();
-        player.getChunkTrackingView().forEach(pos -> keys.add(pos.longKey));
-        return LongSets.unmodifiable(keys);
-    }
-
-    public static Set<Chunk> getSentChunks(final ServerPlayer player) {
-        final ObjectSet<Chunk> chunks = new ObjectOpenHashSet<>();
-        final World world = player.serverLevel().getWorld();
-        player.getChunkTrackingView().forEach(pos -> {
-            final org.bukkit.Chunk chunk = world.getChunkAt(pos.longKey);
-            chunks.add(chunk);
-        });
-        return ObjectSets.unmodifiable(chunks);
-    }
-
-    public static boolean isChunkSent(final ServerPlayer player, final long chunkKey) {
-        return player.getChunkTrackingView().contains(new ChunkPos(chunkKey));
-    }
-}
diff --git a/paper-server/src/main/java/io/papermc/paper/antixray/BitStorageReader.java b/paper-server/src/main/java/io/papermc/paper/antixray/BitStorageReader.java
new file mode 100644
index 0000000000..c27703775c
--- /dev/null
+++ b/paper-server/src/main/java/io/papermc/paper/antixray/BitStorageReader.java
@@ -0,0 +1,51 @@
+package io.papermc.paper.antixray;
+
+public final class BitStorageReader {
+
+    private byte[] buffer;
+    private int bits;
+    private int mask;
+    private int longInBufferIndex;
+    private int bitInLongIndex;
+    private long current;
+
+    public void setBuffer(byte[] buffer) {
+        this.buffer = buffer;
+    }
+
+    public void setBits(int bits) {
+        this.bits = bits;
+        mask = (1 << bits) - 1;
+    }
+
+    public void setIndex(int index) {
+        longInBufferIndex = index;
+        bitInLongIndex = 0;
+        init();
+    }
+
+    private void init() {
+        if (buffer.length > longInBufferIndex + 7) {
+            current = ((((long) buffer[longInBufferIndex]) << 56)
+                | (((long) buffer[longInBufferIndex + 1] & 0xff) << 48)
+                | (((long) buffer[longInBufferIndex + 2] & 0xff) << 40)
+                | (((long) buffer[longInBufferIndex + 3] & 0xff) << 32)
+                | (((long) buffer[longInBufferIndex + 4] & 0xff) << 24)
+                | (((long) buffer[longInBufferIndex + 5] & 0xff) << 16)
+                | (((long) buffer[longInBufferIndex + 6] & 0xff) << 8)
+                | (((long) buffer[longInBufferIndex + 7] & 0xff)));
+        }
+    }
+
+    public int read() {
+        if (bitInLongIndex + bits > 64) {
+            bitInLongIndex = 0;
+            longInBufferIndex += 8;
+            init();
+        }
+
+        int value = (int) (current >>> bitInLongIndex) & mask;
+        bitInLongIndex += bits;
+        return value;
+    }
+}
diff --git a/paper-server/src/main/java/io/papermc/paper/antixray/BitStorageWriter.java b/paper-server/src/main/java/io/papermc/paper/antixray/BitStorageWriter.java
new file mode 100644
index 0000000000..83412e0dda
--- /dev/null
+++ b/paper-server/src/main/java/io/papermc/paper/antixray/BitStorageWriter.java
@@ -0,0 +1,79 @@
+package io.papermc.paper.antixray;
+
+public final class BitStorageWriter {
+
+    private byte[] buffer;
+    private int bits;
+    private long mask;
+    private int longInBufferIndex;
+    private int bitInLongIndex;
+    private long current;
+    private boolean dirty;
+
+    public void setBuffer(byte[] buffer) {
+        this.buffer = buffer;
+    }
+
+    public void setBits(int bits) {
+        this.bits = bits;
+        mask = (1L << bits) - 1;
+    }
+
+    public void setIndex(int index) {
+        longInBufferIndex = index;
+        bitInLongIndex = 0;
+        init();
+    }
+
+    private void init() {
+        if (buffer.length > longInBufferIndex + 7) {
+            current = ((((long) buffer[longInBufferIndex]) << 56)
+                | (((long) buffer[longInBufferIndex + 1] & 0xff) << 48)
+                | (((long) buffer[longInBufferIndex + 2] & 0xff) << 40)
+                | (((long) buffer[longInBufferIndex + 3] & 0xff) << 32)
+                | (((long) buffer[longInBufferIndex + 4] & 0xff) << 24)
+                | (((long) buffer[longInBufferIndex + 5] & 0xff) << 16)
+                | (((long) buffer[longInBufferIndex + 6] & 0xff) << 8)
+                | (((long) buffer[longInBufferIndex + 7] & 0xff)));
+        }
+
+        dirty = false;
+    }
+
+    public void flush() {
+        if (dirty && buffer.length > longInBufferIndex + 7) {
+            buffer[longInBufferIndex] = (byte) (current >> 56 & 0xff);
+            buffer[longInBufferIndex + 1] = (byte) (current >> 48 & 0xff);
+            buffer[longInBufferIndex + 2] = (byte) (current >> 40 & 0xff);
+            buffer[longInBufferIndex + 3] = (byte) (current >> 32 & 0xff);
+            buffer[longInBufferIndex + 4] = (byte) (current >> 24 & 0xff);
+            buffer[longInBufferIndex + 5] = (byte) (current >> 16 & 0xff);
+            buffer[longInBufferIndex + 6] = (byte) (current >> 8 & 0xff);
+            buffer[longInBufferIndex + 7] = (byte) (current & 0xff);
+        }
+    }
+
+    public void write(int value) {
+        if (bitInLongIndex + bits > 64) {
+            flush();
+            bitInLongIndex = 0;
+            longInBufferIndex += 8;
+            init();
+        }
+
+        current = current & ~(mask << bitInLongIndex) | (value & mask) << bitInLongIndex;
+        dirty = true;
+        bitInLongIndex += bits;
+    }
+
+    public void skip() {
+        bitInLongIndex += bits;
+
+        if (bitInLongIndex > 64) {
+            flush();
+            bitInLongIndex = bits;
+            longInBufferIndex += 8;
+            init();
+        }
+    }
+}
diff --git a/paper-server/src/main/java/io/papermc/paper/antixray/ChunkPacketBlockController.java b/paper-server/src/main/java/io/papermc/paper/antixray/ChunkPacketBlockController.java
new file mode 100644
index 0000000000..69b553747b
--- /dev/null
+++ b/paper-server/src/main/java/io/papermc/paper/antixray/ChunkPacketBlockController.java
@@ -0,0 +1,45 @@
+package io.papermc.paper.antixray;
+
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.Direction;
+import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket;
+import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.server.level.ServerPlayerGameMode;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.chunk.LevelChunk;
+
+public class ChunkPacketBlockController {
+
+    public static final ChunkPacketBlockController NO_OPERATION_INSTANCE = new ChunkPacketBlockController();
+
+    protected ChunkPacketBlockController() {
+
+    }
+
+    public BlockState[] getPresetBlockStates(Level level, ChunkPos chunkPos, int chunkSectionY) {
+        return null;
+    }
+
+    public boolean shouldModify(ServerPlayer player, LevelChunk chunk) {
+        return false;
+    }
+
+    public ChunkPacketInfo<BlockState> getChunkPacketInfo(ClientboundLevelChunkWithLightPacket chunkPacket, LevelChunk chunk) {
+        return null;
+    }
+
+    public void modifyBlocks(ClientboundLevelChunkWithLightPacket chunkPacket, ChunkPacketInfo<BlockState> chunkPacketInfo) {
+        chunkPacket.setReady(true);
+    }
+
+    public void onBlockChange(Level level, BlockPos blockPos, BlockState newBlockState, BlockState oldBlockState, int flags, int maxUpdateDepth) {
+
+    }
+
+    public void onPlayerLeftClickBlock(ServerPlayerGameMode serverPlayerGameMode, BlockPos blockPos, ServerboundPlayerActionPacket.Action action, Direction direction, int worldHeight, int sequence) {
+
+    }
+}
diff --git a/paper-server/src/main/java/io/papermc/paper/antixray/ChunkPacketBlockControllerAntiXray.java b/paper-server/src/main/java/io/papermc/paper/antixray/ChunkPacketBlockControllerAntiXray.java
new file mode 100644
index 0000000000..ee2d3a54d7
--- /dev/null
+++ b/paper-server/src/main/java/io/papermc/paper/antixray/ChunkPacketBlockControllerAntiXray.java
@@ -0,0 +1,676 @@
+package io.papermc.paper.antixray;
+
+import io.papermc.paper.configuration.WorldConfiguration;
+import io.papermc.paper.configuration.type.EngineMode;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.function.IntSupplier;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.Direction;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket;
+import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.server.level.ServerPlayerGameMode;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.biome.Biomes;
+import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.level.block.EntityBlock;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.chunk.EmptyLevelChunk;
+import net.minecraft.world.level.chunk.GlobalPalette;
+import net.minecraft.world.level.chunk.LevelChunk;
+import net.minecraft.world.level.chunk.LevelChunkSection;
+import net.minecraft.world.level.chunk.MissingPaletteEntryException;
+import net.minecraft.world.level.chunk.Palette;
+import org.bukkit.Bukkit;
+
+public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockController {
+
+    private static final Palette<BlockState> GLOBAL_BLOCKSTATE_PALETTE = new GlobalPalette<>(Block.BLOCK_STATE_REGISTRY);
+    private static final LevelChunkSection EMPTY_SECTION = null;
+    private final Executor executor;
+    private final EngineMode engineMode;
+    private final int maxBlockHeight;
+    private final int updateRadius;
+    private final boolean usePermission;
+    private final BlockState[] presetBlockStates;
+    private final BlockState[] presetBlockStatesFull;
+    private final BlockState[] presetBlockStatesStone;
+    private final BlockState[] presetBlockStatesDeepslate;
+    private final BlockState[] presetBlockStatesNetherrack;
+    private final BlockState[] presetBlockStatesEndStone;
+    private final int[] presetBlockStateBitsGlobal;
+    private final int[] presetBlockStateBitsStoneGlobal;
+    private final int[] presetBlockStateBitsDeepslateGlobal;
+    private final int[] presetBlockStateBitsNetherrackGlobal;
+    private final int[] presetBlockStateBitsEndStoneGlobal;
+    private final boolean[] solidGlobal = new boolean[Block.BLOCK_STATE_REGISTRY.size()];
+    private final boolean[] obfuscateGlobal = new boolean[Block.BLOCK_STATE_REGISTRY.size()];
+    private final LevelChunkSection[] emptyNearbyChunkSections = {EMPTY_SECTION, EMPTY_SECTION, EMPTY_SECTION, EMPTY_SECTION};
+    private final int maxBlockHeightUpdatePosition;
+
+    public ChunkPacketBlockControllerAntiXray(Level level, Executor executor) {
+        this.executor = executor;
+        WorldConfiguration.Anticheat.AntiXray paperWorldConfig = level.paperConfig().anticheat.antiXray;
+        engineMode = paperWorldConfig.engineMode;
+        maxBlockHeight = paperWorldConfig.maxBlockHeight >> 4 << 4;
+        updateRadius = paperWorldConfig.updateRadius;
+        usePermission = paperWorldConfig.usePermission;
+        List<Block> toObfuscate;
+
+        if (engineMode == EngineMode.HIDE) {
+            toObfuscate = paperWorldConfig.hiddenBlocks;
+            presetBlockStates = null;
+            presetBlockStatesFull = null;
+            presetBlockStatesStone = new BlockState[]{Blocks.STONE.defaultBlockState()};
+            presetBlockStatesDeepslate = new BlockState[]{Blocks.DEEPSLATE.defaultBlockState()};
+            presetBlockStatesNetherrack = new BlockState[]{Blocks.NETHERRACK.defaultBlockState()};
+            presetBlockStatesEndStone = new BlockState[]{Blocks.END_STONE.defaultBlockState()};
+            presetBlockStateBitsGlobal = null;
+            presetBlockStateBitsStoneGlobal = new int[]{GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.STONE.defaultBlockState())};
+            presetBlockStateBitsDeepslateGlobal = new int[]{GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.DEEPSLATE.defaultBlockState())};
+            presetBlockStateBitsNetherrackGlobal = new int[]{GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.NETHERRACK.defaultBlockState())};
+            presetBlockStateBitsEndStoneGlobal = new int[]{GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.END_STONE.defaultBlockState())};
+        } else {
+            toObfuscate = new ArrayList<>(paperWorldConfig.replacementBlocks);
+            List<BlockState> presetBlockStateList = new LinkedList<>();
+
+            for (Block block : paperWorldConfig.hiddenBlocks) {
+
+                if (!(block instanceof EntityBlock)) {
+                    toObfuscate.add(block);
+                    presetBlockStateList.add(block.defaultBlockState());
+                }
+            }
+
+            // The doc of the LinkedHashSet(Collection<? extends E>) constructor doesn't specify that the insertion order is the predictable iteration order of the specified Collection, although it is in the implementation
+            Set<BlockState> presetBlockStateSet = new LinkedHashSet<>();
+            // Therefore addAll(Collection<? extends E>) is used, which guarantees this order in the doc
+            presetBlockStateSet.addAll(presetBlockStateList);
+            presetBlockStates = presetBlockStateSet.isEmpty() ? new BlockState[]{Blocks.DIAMOND_ORE.defaultBlockState()} : presetBlockStateSet.toArray(new BlockState[0]);
+            presetBlockStatesFull = presetBlockStateSet.isEmpty() ? new BlockState[]{Blocks.DIAMOND_ORE.defaultBlockState()} : presetBlockStateList.toArray(new BlockState[0]);
+            presetBlockStatesStone = null;
+            presetBlockStatesDeepslate = null;
+            presetBlockStatesNetherrack = null;
+            presetBlockStatesEndStone = null;
+            presetBlockStateBitsGlobal = new int[presetBlockStatesFull.length];
+
+            for (int i = 0; i < presetBlockStatesFull.length; i++) {
+                presetBlockStateBitsGlobal[i] = GLOBAL_BLOCKSTATE_PALETTE.idFor(presetBlockStatesFull[i]);
+            }
+
+            presetBlockStateBitsStoneGlobal = null;
+            presetBlockStateBitsDeepslateGlobal = null;
+            presetBlockStateBitsNetherrackGlobal = null;
+            presetBlockStateBitsEndStoneGlobal = null;
+        }
+
+        for (Block block : toObfuscate) {
+
+            // Don't obfuscate air because air causes unnecessary block updates and causes block updates to fail in the void
+            if (block != null && !block.defaultBlockState().isAir()) {
+                // Replace all block states of a specified block
+                for (BlockState blockState : block.getStateDefinition().getPossibleStates()) {
+                    obfuscateGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(blockState)] = true;
+                }
+            }
+        }
+
+        EmptyLevelChunk emptyChunk = new EmptyLevelChunk(level, new ChunkPos(0, 0), MinecraftServer.getServer().registryAccess().lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.PLAINS));
+        BlockPos zeroPos = new BlockPos(0, 0, 0);
+
+        for (int i = 0; i < solidGlobal.length; i++) {
+            BlockState blockState = GLOBAL_BLOCKSTATE_PALETTE.valueFor(i);
+
+            if (blockState != null) {
+                solidGlobal[i] = blockState.isRedstoneConductor(emptyChunk, zeroPos)
+                    && blockState.getBlock() != Blocks.SPAWNER && blockState.getBlock() != Blocks.BARRIER && blockState.getBlock() != Blocks.SHULKER_BOX && blockState.getBlock() != Blocks.SLIME_BLOCK && blockState.getBlock() != Blocks.MANGROVE_ROOTS || paperWorldConfig.lavaObscures && blockState == Blocks.LAVA.defaultBlockState();
+                // Comparing blockState == Blocks.LAVA.defaultBlockState() instead of blockState.getBlock() == Blocks.LAVA ensures that only "stationary lava" is used
+                // shulker box checks TE.
+            }
+        }
+
+        maxBlockHeightUpdatePosition = maxBlockHeight + updateRadius - 1;
+    }
+
+    private int getPresetBlockStatesFullLength() {
+        return engineMode == EngineMode.HIDE ? 1 : presetBlockStatesFull.length;
+    }
+
+    @Override
+    public BlockState[] getPresetBlockStates(Level level, ChunkPos chunkPos, int chunkSectionY) {
+        // Return the block states to be added to the paletted containers so that they can be used for obfuscation
+        int bottomBlockY = chunkSectionY << 4;
+
+        if (bottomBlockY < maxBlockHeight) {
+            if (engineMode == EngineMode.HIDE) {
+                return switch (level.getWorld().getEnvironment()) {
+                    case NETHER -> presetBlockStatesNetherrack;
+                    case THE_END -> presetBlockStatesEndStone;
+                    default -> bottomBlockY < 0 ? presetBlockStatesDeepslate : presetBlockStatesStone;
+                };
+            }
+
+            return presetBlockStates;
+        }
+
+        return null;
+    }
+
+    @Override
+    public boolean shouldModify(ServerPlayer player, LevelChunk chunk) {
+        return !usePermission || !player.getBukkitEntity().hasPermission("paper.antixray.bypass");
+    }
+
+    @Override
+    public ChunkPacketInfoAntiXray getChunkPacketInfo(ClientboundLevelChunkWithLightPacket chunkPacket, LevelChunk chunk) {
+        // Return a new instance to collect data and objects in the right state while creating the chunk packet for thread safe access later
+        return new ChunkPacketInfoAntiXray(chunkPacket, chunk, this);
+    }
+
+    @Override
+    public void modifyBlocks(ClientboundLevelChunkWithLightPacket chunkPacket, ChunkPacketInfo<BlockState> chunkPacketInfo) {
+        if (!(chunkPacketInfo instanceof ChunkPacketInfoAntiXray)) {
+            chunkPacket.setReady(true);
+            return;
+        }
+
+        if (!Bukkit.isPrimaryThread()) {
+            // Plugins?
+            MinecraftServer.getServer().scheduleOnMain(() -> modifyBlocks(chunkPacket, chunkPacketInfo));
+            return;
+        }
+
+        LevelChunk chunk = chunkPacketInfo.getChunk();
+        int x = chunk.getPos().x;
+        int z = chunk.getPos().z;
+        Level level = chunk.getLevel();
+        ((ChunkPacketInfoAntiXray) chunkPacketInfo).setNearbyChunks(level.getChunkIfLoaded(x - 1, z), level.getChunkIfLoaded(x + 1, z), level.getChunkIfLoaded(x, z - 1), level.getChunkIfLoaded(x, z + 1));
+        executor.execute((Runnable) chunkPacketInfo);
+    }
+
+    // Actually these fields should be variables inside the obfuscate method but in sync mode or with SingleThreadExecutor in async mode it's okay (even without ThreadLocal)
+    // If an ExecutorService with multiple threads is used, ThreadLocal must be used here
+    private final ThreadLocal<int[]> presetBlockStateBits = ThreadLocal.withInitial(() -> new int[getPresetBlockStatesFullLength()]);
+    private static final ThreadLocal<boolean[]> SOLID = ThreadLocal.withInitial(() -> new boolean[Block.BLOCK_STATE_REGISTRY.size()]);
+    private static final ThreadLocal<boolean[]> OBFUSCATE = ThreadLocal.withInitial(() -> new boolean[Block.BLOCK_STATE_REGISTRY.size()]);
+    // These boolean arrays represent chunk layers, true means don't obfuscate, false means obfuscate
+    private static final ThreadLocal<boolean[][]> CURRENT = ThreadLocal.withInitial(() -> new boolean[16][16]);
+    private static final ThreadLocal<boolean[][]> NEXT = ThreadLocal.withInitial(() -> new boolean[16][16]);
+    private static final ThreadLocal<boolean[][]> NEXT_NEXT = ThreadLocal.withInitial(() -> new boolean[16][16]);
+
+    public void obfuscate(ChunkPacketInfoAntiXray chunkPacketInfoAntiXray) {
+        int[] presetBlockStateBits = this.presetBlockStateBits.get();
+        boolean[] solid = SOLID.get();
+        boolean[] obfuscate = OBFUSCATE.get();
+        boolean[][] current = CURRENT.get();
+        boolean[][] next = NEXT.get();
+        boolean[][] nextNext = NEXT_NEXT.get();
+        // bitStorageReader, bitStorageWriter and nearbyChunkSections could also be reused (with ThreadLocal if necessary) but it's not worth it
+        BitStorageReader bitStorageReader = new BitStorageReader();
+        BitStorageWriter bitStorageWriter = new BitStorageWriter();
+        LevelChunkSection[] nearbyChunkSections = new LevelChunkSection[4];
+        LevelChunk chunk = chunkPacketInfoAntiXray.getChunk();
+        Level level = chunk.getLevel();
+        int maxChunkSectionIndex = Math.min((maxBlockHeight >> 4) - chunk.getMinSectionY(), chunk.getSectionsCount()) - 1;
+        boolean[] solidTemp = null;
+        boolean[] obfuscateTemp = null;
+        bitStorageReader.setBuffer(chunkPacketInfoAntiXray.getBuffer());
+        bitStorageWriter.setBuffer(chunkPacketInfoAntiXray.getBuffer());
+        int numberOfBlocks = presetBlockStateBits.length;
+        // Keep the lambda expressions as simple as possible. They are used very frequently.
+        LayeredIntSupplier random = numberOfBlocks == 1 ? (() -> 0) : engineMode == EngineMode.OBFUSCATE_LAYER ? new LayeredIntSupplier() {
+            // engine-mode: 3
+            private int state;
+            private int next;
+
+            {
+                while ((state = ThreadLocalRandom.current().nextInt()) == 0) ;
+            }
+
+            @Override
+            public void nextLayer() {
+                // https://en.wikipedia.org/wiki/Xorshift
+                state ^= state << 13;
+                state ^= state >>> 17;
+                state ^= state << 5;
+                // https://www.pcg-random.org/posts/bounded-rands.html
+                next = (int) ((Integer.toUnsignedLong(state) * numberOfBlocks) >>> 32);
+            }
+
+            @Override
+            public int getAsInt() {
+                return next;
+            }
+        } : new LayeredIntSupplier() {
+            // engine-mode: 2
+            private int state;
+
+            {
+                while ((state = ThreadLocalRandom.current().nextInt()) == 0) ;
+            }
+
+            @Override
+            public int getAsInt() {
+                // https://en.wikipedia.org/wiki/Xorshift
+                state ^= state << 13;
+                state ^= state >>> 17;
+                state ^= state << 5;
+                // https://www.pcg-random.org/posts/bounded-rands.html
+                return (int) ((Integer.toUnsignedLong(state) * numberOfBlocks) >>> 32);
+            }
+        };
+
+        for (int chunkSectionIndex = 0; chunkSectionIndex <= maxChunkSectionIndex; chunkSectionIndex++) {
+            if (chunkPacketInfoAntiXray.isWritten(chunkSectionIndex) && chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex) != null) {
+                int[] presetBlockStateBitsTemp;
+
+                if (chunkPacketInfoAntiXray.getPalette(chunkSectionIndex) instanceof GlobalPalette) {
+                    if (engineMode == EngineMode.HIDE) {
+                        presetBlockStateBitsTemp = switch (level.getWorld().getEnvironment()) {
+                            case NETHER -> presetBlockStateBitsNetherrackGlobal;
+                            case THE_END -> presetBlockStateBitsEndStoneGlobal;
+                            default -> chunkSectionIndex + chunk.getMinSectionY() < 0 ? presetBlockStateBitsDeepslateGlobal : presetBlockStateBitsStoneGlobal;
+                        };
+                    } else {
+                        presetBlockStateBitsTemp = presetBlockStateBitsGlobal;
+                    }
+                } else {
+                    // If it's presetBlockStates, use this.presetBlockStatesFull instead
+                    BlockState[] presetBlockStatesFull = chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex) == presetBlockStates ? this.presetBlockStatesFull : chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex);
+                    presetBlockStateBitsTemp = presetBlockStateBits;
+
+                    for (int i = 0; i < presetBlockStateBitsTemp.length; i++) {
+                        // This is thread safe because we only request IDs that are guaranteed to be in the palette and are visible
+                        // For more details see the comments in the readPalette method
+                        presetBlockStateBitsTemp[i] = chunkPacketInfoAntiXray.getPalette(chunkSectionIndex).idFor(presetBlockStatesFull[i]);
+                    }
+                }
+
+                bitStorageWriter.setIndex(chunkPacketInfoAntiXray.getIndex(chunkSectionIndex));
+
+                // Check if the chunk section below was not obfuscated
+                if (chunkSectionIndex == 0 || !chunkPacketInfoAntiXray.isWritten(chunkSectionIndex - 1) || chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex - 1) == null) {
+                    // If so, initialize some stuff
+                    bitStorageReader.setBits(chunkPacketInfoAntiXray.getBits(chunkSectionIndex));
+                    bitStorageReader.setIndex(chunkPacketInfoAntiXray.getIndex(chunkSectionIndex));
+                    solidTemp = readPalette(chunkPacketInfoAntiXray.getPalette(chunkSectionIndex), solid, solidGlobal);
+                    obfuscateTemp = readPalette(chunkPacketInfoAntiXray.getPalette(chunkSectionIndex), obfuscate, obfuscateGlobal);
+                    // Read the blocks of the upper layer of the chunk section below if it exists
+                    LevelChunkSection belowChunkSection = null;
+                    boolean skipFirstLayer = chunkSectionIndex == 0 || (belowChunkSection = chunk.getSections()[chunkSectionIndex - 1]) == EMPTY_SECTION;
+
+                    for (int z = 0; z < 16; z++) {
+                        for (int x = 0; x < 16; x++) {
+                            current[z][x] = true;
+                            next[z][x] = skipFirstLayer || isTransparent(belowChunkSection, x, 15, z);
+                        }
+                    }
+
+                    // Abuse the obfuscateLayer method to read the blocks of the first layer of the current chunk section
+                    bitStorageWriter.setBits(0);
+                    obfuscateLayer(-1, bitStorageReader, bitStorageWriter, solidTemp, obfuscateTemp, presetBlockStateBitsTemp, current, next, nextNext, emptyNearbyChunkSections, random);
+                }
+
+                bitStorageWriter.setBits(chunkPacketInfoAntiXray.getBits(chunkSectionIndex));
+                nearbyChunkSections[0] = chunkPacketInfoAntiXray.getNearbyChunks()[0] == null ? EMPTY_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[0].getSections()[chunkSectionIndex];
+                nearbyChunkSections[1] = chunkPacketInfoAntiXray.getNearbyChunks()[1] == null ? EMPTY_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[1].getSections()[chunkSectionIndex];
+                nearbyChunkSections[2] = chunkPacketInfoAntiXray.getNearbyChunks()[2] == null ? EMPTY_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[2].getSections()[chunkSectionIndex];
+                nearbyChunkSections[3] = chunkPacketInfoAntiXray.getNearbyChunks()[3] == null ? EMPTY_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[3].getSections()[chunkSectionIndex];
+
+                // Obfuscate all layers of the current chunk section except the upper one
+                for (int y = 0; y < 15; y++) {
+                    boolean[][] temp = current;
+                    current = next;
+                    next = nextNext;
+                    nextNext = temp;
+                    random.nextLayer();
+                    obfuscateLayer(y, bitStorageReader, bitStorageWriter, solidTemp, obfuscateTemp, presetBlockStateBitsTemp, current, next, nextNext, nearbyChunkSections, random);
+                }
+
+                // Check if the chunk section above doesn't need obfuscation
+                if (chunkSectionIndex == maxChunkSectionIndex || !chunkPacketInfoAntiXray.isWritten(chunkSectionIndex + 1) || chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex + 1) == null) {
+                    // If so, obfuscate the upper layer of the current chunk section by reading blocks of the first layer from the chunk section above if it exists
+                    LevelChunkSection aboveChunkSection;
+
+                    if (chunkSectionIndex != chunk.getSectionsCount() - 1 && (aboveChunkSection = chunk.getSections()[chunkSectionIndex + 1]) != EMPTY_SECTION) {
+                        boolean[][] temp = current;
+                        current = next;
+                        next = nextNext;
+                        nextNext = temp;
+
+                        for (int z = 0; z < 16; z++) {
+                            for (int x = 0; x < 16; x++) {
+                                if (isTransparent(aboveChunkSection, x, 0, z)) {
+                                    current[z][x] = true;
+                                }
+                            }
+                        }
+
+                        // There is nothing to read anymore
+                        bitStorageReader.setBits(0);
+                        solid[0] = true;
+                        random.nextLayer();
+                        obfuscateLayer(15, bitStorageReader, bitStorageWriter, solid, obfuscateTemp, presetBlockStateBitsTemp, current, next, nextNext, nearbyChunkSections, random);
+                    }
+                } else {
+                    // If not, initialize the reader and other stuff for the chunk section above to obfuscate the upper layer of the current chunk section
+                    bitStorageReader.setBits(chunkPacketInfoAntiXray.getBits(chunkSectionIndex + 1));
+                    bitStorageReader.setIndex(chunkPacketInfoAntiXray.getIndex(chunkSectionIndex + 1));
+                    solidTemp = readPalette(chunkPacketInfoAntiXray.getPalette(chunkSectionIndex + 1), solid, solidGlobal);
+                    obfuscateTemp = readPalette(chunkPacketInfoAntiXray.getPalette(chunkSectionIndex + 1), obfuscate, obfuscateGlobal);
+                    boolean[][] temp = current;
+                    current = next;
+                    next = nextNext;
+                    nextNext = temp;
+                    random.nextLayer();
+                    obfuscateLayer(15, bitStorageReader, bitStorageWriter, solidTemp, obfuscateTemp, presetBlockStateBitsTemp, current, next, nextNext, nearbyChunkSections, random);
+                }
+
+                bitStorageWriter.flush();
+            }
+        }
+
+        chunkPacketInfoAntiXray.getChunkPacket().setReady(true);
+    }
+
+    private void obfuscateLayer(int y, BitStorageReader bitStorageReader, BitStorageWriter bitStorageWriter, boolean[] solid, boolean[] obfuscate, int[] presetBlockStateBits, boolean[][] current, boolean[][] next, boolean[][] nextNext, LevelChunkSection[] nearbyChunkSections, IntSupplier random) {
+        // First block of first line
+        int bits = bitStorageReader.read();
+
+        if (nextNext[0][0] = !solid[bits]) {
+            bitStorageWriter.skip();
+            next[0][1] = true;
+            next[1][0] = true;
+        } else {
+            if (current[0][0] || isTransparent(nearbyChunkSections[2], 0, y, 15) || isTransparent(nearbyChunkSections[0], 15, y, 0)) {
+                bitStorageWriter.skip();
+            } else {
+                bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
+            }
+        }
+
+        if (!obfuscate[bits]) {
+            next[0][0] = true;
+        }
+
+        // First line
+        for (int x = 1; x < 15; x++) {
+            bits = bitStorageReader.read();
+
+            if (nextNext[0][x] = !solid[bits]) {
+                bitStorageWriter.skip();
+                next[0][x - 1] = true;
+                next[0][x + 1] = true;
+                next[1][x] = true;
+            } else {
+                if (current[0][x] || isTransparent(nearbyChunkSections[2], x, y, 15)) {
+                    bitStorageWriter.skip();
+                } else {
+                    bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
+                }
+            }
+
+            if (!obfuscate[bits]) {
+                next[0][x] = true;
+            }
+        }
+
+        // Last block of first line
+        bits = bitStorageReader.read();
+
+        if (nextNext[0][15] = !solid[bits]) {
+            bitStorageWriter.skip();
+            next[0][14] = true;
+            next[1][15] = true;
+        } else {
+            if (current[0][15] || isTransparent(nearbyChunkSections[2], 15, y, 15) || isTransparent(nearbyChunkSections[1], 0, y, 0)) {
+                bitStorageWriter.skip();
+            } else {
+                bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
+            }
+        }
+
+        if (!obfuscate[bits]) {
+            next[0][15] = true;
+        }
+
+        // All inner lines
+        for (int z = 1; z < 15; z++) {
+            // First block
+            bits = bitStorageReader.read();
+
+            if (nextNext[z][0] = !solid[bits]) {
+                bitStorageWriter.skip();
+                next[z][1] = true;
+                next[z - 1][0] = true;
+                next[z + 1][0] = true;
+            } else {
+                if (current[z][0] || isTransparent(nearbyChunkSections[0], 15, y, z)) {
+                    bitStorageWriter.skip();
+                } else {
+                    bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
+                }
+            }
+
+            if (!obfuscate[bits]) {
+                next[z][0] = true;
+            }
+
+            // All inner blocks
+            for (int x = 1; x < 15; x++) {
+                bits = bitStorageReader.read();
+
+                if (nextNext[z][x] = !solid[bits]) {
+                    bitStorageWriter.skip();
+                    next[z][x - 1] = true;
+                    next[z][x + 1] = true;
+                    next[z - 1][x] = true;
+                    next[z + 1][x] = true;
+                } else {
+                    if (current[z][x]) {
+                        bitStorageWriter.skip();
+                    } else {
+                        bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
+                    }
+                }
+
+                if (!obfuscate[bits]) {
+                    next[z][x] = true;
+                }
+            }
+
+            // Last block
+            bits = bitStorageReader.read();
+
+            if (nextNext[z][15] = !solid[bits]) {
+                bitStorageWriter.skip();
+                next[z][14] = true;
+                next[z - 1][15] = true;
+                next[z + 1][15] = true;
+            } else {
+                if (current[z][15] || isTransparent(nearbyChunkSections[1], 0, y, z)) {
+                    bitStorageWriter.skip();
+                } else {
+                    bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
+                }
+            }
+
+            if (!obfuscate[bits]) {
+                next[z][15] = true;
+            }
+        }
+
+        // First block of last line
+        bits = bitStorageReader.read();
+
+        if (nextNext[15][0] = !solid[bits]) {
+            bitStorageWriter.skip();
+            next[15][1] = true;
+            next[14][0] = true;
+        } else {
+            if (current[15][0] || isTransparent(nearbyChunkSections[3], 0, y, 0) || isTransparent(nearbyChunkSections[0], 15, y, 15)) {
+                bitStorageWriter.skip();
+            } else {
+                bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
+            }
+        }
+
+        if (!obfuscate[bits]) {
+            next[15][0] = true;
+        }
+
+        // Last line
+        for (int x = 1; x < 15; x++) {
+            bits = bitStorageReader.read();
+
+            if (nextNext[15][x] = !solid[bits]) {
+                bitStorageWriter.skip();
+                next[15][x - 1] = true;
+                next[15][x + 1] = true;
+                next[14][x] = true;
+            } else {
+                if (current[15][x] || isTransparent(nearbyChunkSections[3], x, y, 0)) {
+                    bitStorageWriter.skip();
+                } else {
+                    bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
+                }
+            }
+
+            if (!obfuscate[bits]) {
+                next[15][x] = true;
+            }
+        }
+
+        // Last block of last line
+        bits = bitStorageReader.read();
+
+        if (nextNext[15][15] = !solid[bits]) {
+            bitStorageWriter.skip();
+            next[15][14] = true;
+            next[14][15] = true;
+        } else {
+            if (current[15][15] || isTransparent(nearbyChunkSections[3], 15, y, 0) || isTransparent(nearbyChunkSections[1], 0, y, 15)) {
+                bitStorageWriter.skip();
+            } else {
+                bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
+            }
+        }
+
+        if (!obfuscate[bits]) {
+            next[15][15] = true;
+        }
+    }
+
+    private boolean isTransparent(LevelChunkSection chunkSection, int x, int y, int z) {
+        if (chunkSection == EMPTY_SECTION) {
+            return true;
+        }
+
+        try {
+            return !solidGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(chunkSection.getBlockState(x, y, z))];
+        } catch (MissingPaletteEntryException e) {
+            // Race condition / visibility issue / no happens-before relationship
+            // We don't care and treat the block as transparent
+            // Internal implementation details of PalettedContainer, LinearPalette, HashMapPalette, CrudeIncrementalIntIdentityHashBiMap, ... guarantee us that no (other) exceptions will occur
+            return true;
+        }
+    }
+
+    private boolean[] readPalette(Palette<BlockState> palette, boolean[] temp, boolean[] global) {
+        if (palette instanceof GlobalPalette) {
+            return global;
+        }
+
+        try {
+            for (int i = 0; i < palette.getSize(); i++) {
+                temp[i] = global[GLOBAL_BLOCKSTATE_PALETTE.idFor(palette.valueFor(i))];
+            }
+        } catch (MissingPaletteEntryException e) {
+            // Race condition / visibility issue / no happens-before relationship
+            // We don't care because we at least see the state as it was when the chunk packet was created
+            // Internal implementation details of PalettedContainer, LinearPalette, HashMapPalette, CrudeIncrementalIntIdentityHashBiMap, ... guarantee us that no (other) exceptions will occur until we have all the data that we need here
+            // Since all palettes have a fixed initial maximum size and there is no internal restructuring and no values are removed from palettes, we are also guaranteed to see the data
+        }
+
+        return temp;
+    }
+
+    @Override
+    public void onBlockChange(Level level, BlockPos blockPos, BlockState newBlockState, BlockState oldBlockState, int flags, int maxUpdateDepth) {
+        if (oldBlockState != null && solidGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(oldBlockState)] && !solidGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(newBlockState)] && blockPos.getY() <= maxBlockHeightUpdatePosition) {
+            updateNearbyBlocks(level, blockPos);
+        }
+    }
+
+    @Override
+    public void onPlayerLeftClickBlock(ServerPlayerGameMode serverPlayerGameMode, BlockPos blockPos, ServerboundPlayerActionPacket.Action action, Direction direction, int worldHeight, int sequence) {
+        if (blockPos.getY() <= maxBlockHeightUpdatePosition) {
+            updateNearbyBlocks(serverPlayerGameMode.level, blockPos);
+        }
+    }
+
+    private void updateNearbyBlocks(Level level, BlockPos blockPos) {
+        if (updateRadius >= 2) {
+            BlockPos temp = blockPos.west();
+            updateBlock(level, temp);
+            updateBlock(level, temp.west());
+            updateBlock(level, temp.below());
+            updateBlock(level, temp.above());
+            updateBlock(level, temp.north());
+            updateBlock(level, temp.south());
+            updateBlock(level, temp = blockPos.east());
+            updateBlock(level, temp.east());
+            updateBlock(level, temp.below());
+            updateBlock(level, temp.above());
+            updateBlock(level, temp.north());
+            updateBlock(level, temp.south());
+            updateBlock(level, temp = blockPos.below());
+            updateBlock(level, temp.below());
+            updateBlock(level, temp.north());
+            updateBlock(level, temp.south());
+            updateBlock(level, temp = blockPos.above());
+            updateBlock(level, temp.above());
+            updateBlock(level, temp.north());
+            updateBlock(level, temp.south());
+            updateBlock(level, temp = blockPos.north());
+            updateBlock(level, temp.north());
+            updateBlock(level, temp = blockPos.south());
+            updateBlock(level, temp.south());
+        } else if (updateRadius == 1) {
+            updateBlock(level, blockPos.west());
+            updateBlock(level, blockPos.east());
+            updateBlock(level, blockPos.below());
+            updateBlock(level, blockPos.above());
+            updateBlock(level, blockPos.north());
+            updateBlock(level, blockPos.south());
+        } else {
+            // Do nothing if updateRadius <= 0 (test mode)
+        }
+    }
+
+    private void updateBlock(Level level, BlockPos blockPos) {
+        BlockState blockState = level.getBlockStateIfLoaded(blockPos);
+
+        if (blockState != null && obfuscateGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(blockState)]) {
+            ((ServerLevel) level).getChunkSource().blockChanged(blockPos);
+        }
+    }
+
+    @FunctionalInterface
+    private interface LayeredIntSupplier extends IntSupplier {
+        default void nextLayer() {
+
+        }
+    }
+}
diff --git a/paper-server/src/main/java/io/papermc/paper/antixray/ChunkPacketInfo.java b/paper-server/src/main/java/io/papermc/paper/antixray/ChunkPacketInfo.java
new file mode 100644
index 0000000000..a33a4d45d4
--- /dev/null
+++ b/paper-server/src/main/java/io/papermc/paper/antixray/ChunkPacketInfo.java
@@ -0,0 +1,80 @@
+package io.papermc.paper.antixray;
+
+import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket;
+import net.minecraft.world.level.chunk.LevelChunk;
+import net.minecraft.world.level.chunk.Palette;
+
+public class ChunkPacketInfo<T> {
+
+    private final ClientboundLevelChunkWithLightPacket chunkPacket;
+    private final LevelChunk chunk;
+    private final int[] bits;
+    private final Object[] palettes;
+    private final int[] indexes;
+    private final Object[][] presetValues;
+    private byte[] buffer;
+
+    public ChunkPacketInfo(ClientboundLevelChunkWithLightPacket chunkPacket, LevelChunk chunk) {
+        this.chunkPacket = chunkPacket;
+        this.chunk = chunk;
+        int sections = chunk.getSectionsCount();
+        bits = new int[sections];
+        palettes = new Object[sections];
+        indexes = new int[sections];
+        presetValues = new Object[sections][];
+    }
+
+    public ClientboundLevelChunkWithLightPacket getChunkPacket() {
+        return chunkPacket;
+    }
+
+    public LevelChunk getChunk() {
+        return chunk;
+    }
+
+    public byte[] getBuffer() {
+        return buffer;
+    }
+
+    public void setBuffer(byte[] buffer) {
+        this.buffer = buffer;
+    }
+
+    public int getBits(int chunkSectionIndex) {
+        return bits[chunkSectionIndex];
+    }
+
+    public void setBits(int chunkSectionIndex, int bits) {
+        this.bits[chunkSectionIndex] = bits;
+    }
+
+    @SuppressWarnings("unchecked")
+    public Palette<T> getPalette(int chunkSectionIndex) {
+        return (Palette<T>) palettes[chunkSectionIndex];
+    }
+
+    public void setPalette(int chunkSectionIndex, Palette<T> palette) {
+        palettes[chunkSectionIndex] = palette;
+    }
+
+    public int getIndex(int chunkSectionIndex) {
+        return indexes[chunkSectionIndex];
+    }
+
+    public void setIndex(int chunkSectionIndex, int index) {
+        indexes[chunkSectionIndex] = index;
+    }
+
+    @SuppressWarnings("unchecked")
+    public T[] getPresetValues(int chunkSectionIndex) {
+        return (T[]) presetValues[chunkSectionIndex];
+    }
+
+    public void setPresetValues(int chunkSectionIndex, T[] presetValues) {
+        this.presetValues[chunkSectionIndex] = presetValues;
+    }
+
+    public boolean isWritten(int chunkSectionIndex) {
+        return bits[chunkSectionIndex] != 0;
+    }
+}
diff --git a/paper-server/src/main/java/io/papermc/paper/antixray/ChunkPacketInfoAntiXray.java b/paper-server/src/main/java/io/papermc/paper/antixray/ChunkPacketInfoAntiXray.java
new file mode 100644
index 0000000000..7d8dff55bf
--- /dev/null
+++ b/paper-server/src/main/java/io/papermc/paper/antixray/ChunkPacketInfoAntiXray.java
@@ -0,0 +1,29 @@
+package io.papermc.paper.antixray;
+
+import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.chunk.LevelChunk;
+
+public final class ChunkPacketInfoAntiXray extends ChunkPacketInfo<BlockState> implements Runnable {
+
+    private final ChunkPacketBlockControllerAntiXray chunkPacketBlockControllerAntiXray;
+    private LevelChunk[] nearbyChunks;
+
+    public ChunkPacketInfoAntiXray(ClientboundLevelChunkWithLightPacket chunkPacket, LevelChunk chunk, ChunkPacketBlockControllerAntiXray chunkPacketBlockControllerAntiXray) {
+        super(chunkPacket, chunk);
+        this.chunkPacketBlockControllerAntiXray = chunkPacketBlockControllerAntiXray;
+    }
+
+    public LevelChunk[] getNearbyChunks() {
+        return nearbyChunks;
+    }
+
+    public void setNearbyChunks(LevelChunk... nearbyChunks) {
+        this.nearbyChunks = nearbyChunks;
+    }
+
+    @Override
+    public void run() {
+        chunkPacketBlockControllerAntiXray.obfuscate(this);
+    }
+}
diff --git a/paper-server/src/main/java/io/papermc/paper/configuration/Configurations.java b/paper-server/src/main/java/io/papermc/paper/configuration/Configurations.java
index 8cf720f085..109e569b7b 100644
--- a/paper-server/src/main/java/io/papermc/paper/configuration/Configurations.java
+++ b/paper-server/src/main/java/io/papermc/paper/configuration/Configurations.java
@@ -275,7 +275,7 @@ public abstract class Configurations<G, W> {
     }
 
     public Path getWorldConfigFile(ServerLevel level) {
-        return level.convertable.levelDirectory.path().resolve(this.worldConfigFileName);
+        return level.levelStorageAccess.levelDirectory.path().resolve(this.worldConfigFileName);
     }
 
     public static class ContextMap {
diff --git a/paper-server/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java b/paper-server/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java
index c5644d8d64..098ab351de 100644
--- a/paper-server/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java
+++ b/paper-server/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java
@@ -327,7 +327,7 @@ public class PaperConfigurations extends Configurations<GlobalConfiguration, Wor
     }
 
     private static ContextMap createWorldContextMap(ServerLevel level) {
-        return createWorldContextMap(level.convertable.levelDirectory.path(), level.serverLevelData.getLevelName(), level.dimension().location(), level.spigotConfig, level.registryAccess(), level.getGameRules());
+        return createWorldContextMap(level.levelStorageAccess.levelDirectory.path(), level.serverLevelData.getLevelName(), level.dimension().location(), level.spigotConfig, level.registryAccess(), level.getGameRules());
     }
 
     public static ContextMap createWorldContextMap(final Path dir, final String levelName, final ResourceLocation worldKey, final SpigotWorldConfig spigotConfig, final RegistryAccess registryAccess, final GameRules gameRules) {
diff --git a/paper-server/src/main/java/io/papermc/paper/entity/activation/ActivationType.java b/paper-server/src/main/java/io/papermc/paper/entity/activation/ActivationType.java
new file mode 100644
index 0000000000..cd43845a08
--- /dev/null
+++ b/paper-server/src/main/java/io/papermc/paper/entity/activation/ActivationType.java
@@ -0,0 +1,47 @@
+package io.papermc.paper.entity.activation;
+
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.FlyingMob;
+import net.minecraft.world.entity.PathfinderMob;
+import net.minecraft.world.entity.ambient.AmbientCreature;
+import net.minecraft.world.entity.animal.WaterAnimal;
+import net.minecraft.world.entity.monster.Enemy;
+import net.minecraft.world.entity.npc.Villager;
+import net.minecraft.world.entity.raid.Raider;
+import net.minecraft.world.phys.AABB;
+
+public enum ActivationType {
+    WATER,
+    FLYING_MONSTER,
+    VILLAGER,
+    MONSTER,
+    ANIMAL,
+    RAIDER,
+    MISC;
+
+    AABB boundingBox = new AABB(0, 0, 0, 0, 0, 0);
+
+    /**
+     * Returns the activation type for the given entity.
+     *
+     * @param entity entity to get the activation type for
+     * @return activation type
+     */
+    public static ActivationType activationTypeFor(final Entity entity) {
+        if (entity instanceof WaterAnimal) {
+            return ActivationType.WATER;
+        } else if (entity instanceof Villager) {
+            return ActivationType.VILLAGER;
+        } else if (entity instanceof FlyingMob && entity instanceof Enemy) {
+            return ActivationType.FLYING_MONSTER;
+        } else if (entity instanceof Raider) {
+            return ActivationType.RAIDER;
+        } else if (entity instanceof Enemy) {
+            return ActivationType.MONSTER;
+        } else if (entity instanceof PathfinderMob || entity instanceof AmbientCreature) {
+            return ActivationType.ANIMAL;
+        } else {
+            return ActivationType.MISC;
+        }
+    }
+}
diff --git a/paper-server/src/main/java/io/papermc/paper/inventory/PaperInventoryCustomHolderContainer.java b/paper-server/src/main/java/io/papermc/paper/inventory/PaperInventoryCustomHolderContainer.java
index 224d4b2cc4..f3ef363497 100644
--- a/paper-server/src/main/java/io/papermc/paper/inventory/PaperInventoryCustomHolderContainer.java
+++ b/paper-server/src/main/java/io/papermc/paper/inventory/PaperInventoryCustomHolderContainer.java
@@ -105,13 +105,13 @@ public final class PaperInventoryCustomHolderContainer implements Container {
     }
 
     @Override
-    public void onOpen(CraftHumanEntity who) {
-        this.delegate.onOpen(who);
+    public void onOpen(CraftHumanEntity player) {
+        this.delegate.onOpen(player);
     }
 
     @Override
-    public void onClose(CraftHumanEntity who) {
-        this.delegate.onClose(who);
+    public void onClose(CraftHumanEntity player) {
+        this.delegate.onClose(player);
     }
 
     @Override
diff --git a/paper-server/src/main/java/io/papermc/paper/util/Holderable.java b/paper-server/src/main/java/io/papermc/paper/util/Holderable.java
index 3401d0073f..c0115e0a31 100644
--- a/paper-server/src/main/java/io/papermc/paper/util/Holderable.java
+++ b/paper-server/src/main/java/io/papermc/paper/util/Holderable.java
@@ -7,7 +7,7 @@ import com.mojang.serialization.JsonOps;
 import net.kyori.adventure.key.Key;
 import net.minecraft.core.Holder;
 import net.minecraft.resources.RegistryOps;
-import org.bukkit.Keyed;
+import org.bukkit.NamespacedKey;
 import org.bukkit.Registry;
 import org.bukkit.craftbukkit.CraftRegistry;
 import org.bukkit.craftbukkit.util.Handleable;
@@ -25,7 +25,7 @@ public interface Holderable<M> extends Handleable<M> {
         return this.getHolder().value();
     }
 
-    static <T extends Keyed, M> @Nullable T fromBukkitSerializationObject(final Object deserialized, final Codec<? extends Holder<M>> codec, final Registry<T> registry) { // TODO remove Keyed
+    static <T extends org.bukkit.Keyed, M> @Nullable T fromBukkitSerializationObject(final Object deserialized, final Codec<? extends Holder<M>> codec, final Registry<T> registry) { // TODO remove Keyed
         return switch (deserialized) {
             case @Subst("key:value") final String string -> {
                 if (!(Key.parseable(string))) {
@@ -75,4 +75,12 @@ public interface Holderable<M> extends Handleable<M> {
     default String implToString() {
         return "%s{holder=%s}".formatted(this.getClass().getSimpleName(), this.getHolder().toString());
     }
+
+    default @Nullable NamespacedKey getKeyOrNull() {
+        return this.getHolder().unwrapKey().map(MCUtil::fromResourceKey).orElse(null);
+    }
+
+    default NamespacedKey getKey() {
+        return MCUtil.fromResourceKey(this.getHolder().unwrapKey().orElseThrow(() -> new IllegalStateException("Cannot get key for this registry item, because it is not registered.")));
+    }
 }
diff --git a/paper-server/src/main/java/io/papermc/paper/util/LogManagerShutdownThread.java b/paper-server/src/main/java/io/papermc/paper/util/LogManagerShutdownThread.java
new file mode 100644
index 0000000000..3d7df554b8
--- /dev/null
+++ b/paper-server/src/main/java/io/papermc/paper/util/LogManagerShutdownThread.java
@@ -0,0 +1,29 @@
+package io.papermc.paper.util;
+
+import org.apache.logging.log4j.LogManager;
+
+public final class LogManagerShutdownThread extends Thread {
+
+    static LogManagerShutdownThread INSTANCE = new LogManagerShutdownThread();
+
+    public static void hook() {
+        if (INSTANCE == null) {
+            throw new IllegalStateException("Cannot re-hook after being unhooked");
+        }
+        Runtime.getRuntime().addShutdownHook(INSTANCE);
+    }
+
+    public static void unhook() {
+        Runtime.getRuntime().removeShutdownHook(INSTANCE);
+        INSTANCE = null;
+    }
+
+    private LogManagerShutdownThread() {
+        super("Log4j2 Shutdown Thread");
+    }
+
+    @Override
+    public void run() {
+        LogManager.shutdown();
+    }
+}
diff --git a/paper-server/src/main/java/io/papermc/paper/util/OldEnumHolderable.java b/paper-server/src/main/java/io/papermc/paper/util/OldEnumHolderable.java
new file mode 100644
index 0000000000..1e70146847
--- /dev/null
+++ b/paper-server/src/main/java/io/papermc/paper/util/OldEnumHolderable.java
@@ -0,0 +1,88 @@
+package io.papermc.paper.util;
+
+import com.google.common.base.Preconditions;
+import java.util.Locale;
+import net.minecraft.core.Holder;
+import org.bukkit.Keyed;
+import org.bukkit.NamespacedKey;
+import org.bukkit.util.OldEnum;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+
+@SuppressWarnings("removal")
+@Deprecated
+@NullMarked
+public abstract class OldEnumHolderable<A extends OldEnum<A>, M> implements Holderable<M>, OldEnum<A>, Keyed {
+
+    private final Holder<M> holder;
+    private final int ordinal;
+    private final @Nullable String name;
+
+    protected OldEnumHolderable(final Holder<M> holder, final int ordinal) {
+        this.holder = holder;
+        this.ordinal = ordinal;
+        if (holder instanceof final Holder.Reference<M> reference) {
+            // For backwards compatibility, minecraft values will stile return the uppercase name without the namespace,
+            // in case plugins use for example the name as key in a config file to receive registry item specific values.
+            // Custom registry items will return the key with namespace. For a plugin this should look than like a new registry item
+            // (which can always be added in new minecraft versions and the plugin should therefore handle it accordingly).
+            if (NamespacedKey.MINECRAFT.equals(reference.key().location().getNamespace())) {
+                this.name = reference.key().location().getPath().toUpperCase(Locale.ROOT);
+            } else {
+                this.name = reference.key().location().toString();
+            }
+        } else {
+            this.name = null;
+        }
+    }
+
+    @Override
+    public Holder<M> getHolder() {
+        return this.holder;
+    }
+
+    @Override
+    @Deprecated
+    public int compareTo(A other) {
+        this.checkIsReference();
+        return this.ordinal - other.ordinal();
+    }
+
+    @Override
+    @Deprecated
+    public String name() {
+        this.checkIsReference();
+        return this.name;
+    }
+
+    @Override
+    @Deprecated
+    public int ordinal() {
+        this.checkIsReference();
+        return this.ordinal;
+    }
+
+    private void checkIsReference() {
+        Preconditions.checkState(this.holder.kind() == Holder.Kind.REFERENCE, "Cannot call method for this registry item, because it is not registered.");
+    }
+
+    @Override
+    public NamespacedKey getKey() {
+        return MCUtil.fromResourceKey(this.holder.unwrapKey().orElseThrow(() -> new IllegalStateException("Cannot get key for this registry item, because it is not registered.")));
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        return this.implEquals(obj);
+    }
+
+    @Override
+    public int hashCode() {
+        return this.implHashCode();
+    }
+
+    @Override
+    public String toString() {
+        return this.implToString();
+    }
+}
diff --git a/paper-server/src/main/java/io/papermc/paper/world/worldgen/OptionallyFlatBedrockConditionSource.java b/paper-server/src/main/java/io/papermc/paper/world/worldgen/OptionallyFlatBedrockConditionSource.java
new file mode 100644
index 0000000000..60b6e040df
--- /dev/null
+++ b/paper-server/src/main/java/io/papermc/paper/world/worldgen/OptionallyFlatBedrockConditionSource.java
@@ -0,0 +1,81 @@
+package io.papermc.paper.world.worldgen;
+
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.MapCodec;
+import com.mojang.serialization.codecs.RecordCodecBuilder;
+import net.minecraft.core.Registry;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.resources.ResourceKey;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.util.KeyDispatchDataCodec;
+import net.minecraft.util.Mth;
+import net.minecraft.util.RandomSource;
+import net.minecraft.world.level.levelgen.PositionalRandomFactory;
+import net.minecraft.world.level.levelgen.SurfaceRules;
+import net.minecraft.world.level.levelgen.VerticalAnchor;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.framework.qual.DefaultQualifier;
+
+// Modelled off of SurfaceRules$VerticalGradientConditionSource
+// Flat bedrock generator settings
+@DefaultQualifier(NonNull.class)
+public record OptionallyFlatBedrockConditionSource(ResourceLocation randomName, VerticalAnchor trueAtAndBelow, VerticalAnchor falseAtAndAbove, boolean isRoof) implements SurfaceRules.ConditionSource {
+
+    private static final ResourceKey<MapCodec<? extends SurfaceRules.ConditionSource>> CODEC_RESOURCE_KEY = ResourceKey.create(
+        Registries.MATERIAL_CONDITION,
+        ResourceLocation.fromNamespaceAndPath(ResourceLocation.PAPER_NAMESPACE, "optionally_flat_bedrock_condition_source")
+    );
+    private static final KeyDispatchDataCodec<OptionallyFlatBedrockConditionSource> CODEC = KeyDispatchDataCodec.of(RecordCodecBuilder.mapCodec((instance) -> {
+        return instance.group(
+            ResourceLocation.CODEC.fieldOf("random_name").forGetter(OptionallyFlatBedrockConditionSource::randomName),
+            VerticalAnchor.CODEC.fieldOf("true_at_and_below").forGetter(OptionallyFlatBedrockConditionSource::trueAtAndBelow),
+            VerticalAnchor.CODEC.fieldOf("false_at_and_above").forGetter(OptionallyFlatBedrockConditionSource::falseAtAndAbove),
+            Codec.BOOL.fieldOf("is_roof").forGetter(OptionallyFlatBedrockConditionSource::isRoof)
+        ).apply(instance, OptionallyFlatBedrockConditionSource::new);
+    }));
+
+    public static void bootstrap() {
+        Registry.register(BuiltInRegistries.MATERIAL_CONDITION, CODEC_RESOURCE_KEY, CODEC.codec());
+    }
+
+    @Override
+    public KeyDispatchDataCodec<? extends SurfaceRules.ConditionSource> codec() {
+        return CODEC;
+    }
+
+    @Override
+    public SurfaceRules.Condition apply(final SurfaceRules.Context context) {
+        boolean hasFlatBedrock = context.context.level().paperConfig().environment.generateFlatBedrock;
+        int tempTrueAtAndBelowY = this.trueAtAndBelow().resolveY(context.context);
+        int tempFalseAtAndAboveY = this.falseAtAndAbove().resolveY(context.context);
+
+        int flatYLevel = this.isRoof ? Math.max(tempFalseAtAndAboveY, tempTrueAtAndBelowY) - 1 : Math.min(tempFalseAtAndAboveY, tempTrueAtAndBelowY);
+        final int trueAtAndBelowY = hasFlatBedrock ? flatYLevel : tempTrueAtAndBelowY;
+        final int falseAtAndAboveY = hasFlatBedrock ? flatYLevel : tempFalseAtAndAboveY;
+
+        final PositionalRandomFactory positionalRandomFactory = context.randomState.getOrCreateRandomFactory(this.randomName());
+
+        class VerticalGradientCondition extends SurfaceRules.LazyYCondition {
+            VerticalGradientCondition(SurfaceRules.Context context) {
+                super(context);
+            }
+
+            @Override
+            protected boolean compute() {
+                int blockY = this.context.blockY;
+                if (blockY <= trueAtAndBelowY) {
+                    return true;
+                } else if (blockY >= falseAtAndAboveY) {
+                    return false;
+                } else {
+                    double d = Mth.map(blockY, trueAtAndBelowY, falseAtAndAboveY, 1.0D, 0.0D);
+                    RandomSource randomSource = positionalRandomFactory.at(this.context.blockX, blockY, this.context.blockZ);
+                    return (double)randomSource.nextFloat() < d;
+                }
+            }
+        }
+
+        return new VerticalGradientCondition(context);
+    }
+}
diff --git a/paper-server/src/main/java/net/neoforged/art/internal/RenamerImpl.java b/paper-server/src/main/java/net/neoforged/art/internal/RenamerImpl.java
deleted file mode 100644
index 73b20a92f3..0000000000
--- a/paper-server/src/main/java/net/neoforged/art/internal/RenamerImpl.java
+++ /dev/null
@@ -1,308 +0,0 @@
-/*
- * Forge Auto Renaming Tool
- * Copyright (c) 2021
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation version 2.1
- * of the License.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
- */
-
-package net.neoforged.art.internal;
-
-import java.io.BufferedOutputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.file.Files;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Enumeration;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Objects;
-import java.util.Set;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipFile;
-import java.util.zip.ZipOutputStream;
-
-import net.neoforged.cliutils.JarUtils;
-import net.neoforged.cliutils.progress.ProgressReporter;
-import org.objectweb.asm.Opcodes;
-
-import net.neoforged.art.api.ClassProvider;
-import net.neoforged.art.api.Renamer;
-import net.neoforged.art.api.Transformer;
-import net.neoforged.art.api.Transformer.ClassEntry;
-import net.neoforged.art.api.Transformer.Entry;
-import net.neoforged.art.api.Transformer.ManifestEntry;
-import net.neoforged.art.api.Transformer.ResourceEntry;
-
-public class RenamerImpl implements Renamer { // Paper - public
-    private static final ProgressReporter PROGRESS = ProgressReporter.getDefault();
-    static final int MAX_ASM_VERSION = Opcodes.ASM9;
-    private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";
-    private final List<File> libraries;
-    private final List<Transformer> transformers;
-    private final SortedClassProvider sortedClassProvider;
-    private final List<ClassProvider> classProviders;
-    private final int threads;
-    private final Consumer<String> logger;
-    private final Consumer<String> debug;
-    private boolean setup = false;
-    private ClassProvider libraryClasses;
-
-    RenamerImpl(List<File> libraries, List<Transformer> transformers, SortedClassProvider sortedClassProvider, List<ClassProvider> classProviders,
-                int threads, Consumer<String> logger, Consumer<String> debug) {
-        this.libraries = libraries;
-        this.transformers = transformers;
-        this.sortedClassProvider = sortedClassProvider;
-        this.classProviders = Collections.unmodifiableList(classProviders);
-        this.threads = threads;
-        this.logger = logger;
-        this.debug = debug;
-    }
-
-    private void setup() {
-        if (this.setup)
-            return;
-
-        this.setup = true;
-
-        ClassProvider.Builder libraryClassesBuilder = ClassProvider.builder().shouldCacheAll(true);
-        this.logger.accept("Adding Libraries to Inheritance");
-        this.libraries.forEach(f -> libraryClassesBuilder.addLibrary(f.toPath()));
-
-        this.libraryClasses = libraryClassesBuilder.build();
-    }
-
-    @Override
-    public void run(File input, File output) {
-        // Paper start - Add remappingSelf
-        this.run(input, output, false);
-    }
-    public void run(File input, File output, boolean remappingSelf) {
-        // Paper end
-        if (!this.setup)
-            this.setup();
-
-        if (Boolean.getBoolean(ProgressReporter.ENABLED_PROPERTY)) {
-            try {
-                PROGRESS.setMaxProgress(JarUtils.getFileCountInZip(input));
-            } catch (IOException e) {
-                logger.accept("Failed to read zip file count: " + e);
-            }
-        }
-
-        input = Objects.requireNonNull(input).getAbsoluteFile();
-        output = Objects.requireNonNull(output).getAbsoluteFile();
-
-        if (!input.exists())
-            throw new IllegalArgumentException("Input file not found: " + input.getAbsolutePath());
-
-        logger.accept("Reading Input: " + input.getAbsolutePath());
-        PROGRESS.setStep("Reading input jar");
-        // Read everything from the input jar!
-        List<Entry> oldEntries = new ArrayList<>();
-        try (ZipFile in = new ZipFile(input)) {
-            int amount = 0;
-            for (Enumeration<? extends ZipEntry> entries = in.entries(); entries.hasMoreElements();) {
-                final ZipEntry e = entries.nextElement();
-                if (e.isDirectory())
-                    continue;
-                String name = e.getName();
-                byte[] data;
-                try (InputStream entryInput = in.getInputStream(e)) {
-                    data = entryInput.readAllBytes(); // Paper - Use readAllBytes
-                }
-
-                if (name.endsWith(".class") && !name.contains("META-INF/")) // Paper - Skip META-INF entries
-                    oldEntries.add(ClassEntry.create(name, e.getTime(), data));
-                else if (name.equals(MANIFEST_NAME))
-                    oldEntries.add(ManifestEntry.create(e.getTime(), data));
-                else if (name.equals("javadoctor.json"))
-                    oldEntries.add(Transformer.JavadoctorEntry.create(e.getTime(), data));
-                else
-                    oldEntries.add(ResourceEntry.create(name, e.getTime(), data));
-
-                if ((++amount) % 10 == 0) {
-                    PROGRESS.setProgress(amount);
-                }
-            }
-        } catch (IOException e) {
-            throw new RuntimeException("Could not parse input: " + input.getAbsolutePath(), e);
-        }
-
-        this.sortedClassProvider.clearCache();
-        ArrayList<ClassProvider> classProviders = new ArrayList<>(this.classProviders);
-        classProviders.add(0, this.libraryClasses);
-        this.sortedClassProvider.classProviders = classProviders;
-
-        AsyncHelper async = new AsyncHelper(threads);
-        try {
-
-            /* Disabled until we do something with it
-            // Gather original file Hashes, so that we can detect changes and update the manifest if necessary
-            log("Gathering original hashes");
-            Map<String, String> oldHashes = async.invokeAll(oldEntries,
-                e -> new Pair<>(e.getName(), HashFunction.SHA256.hash(e.getData()))
-            ).stream().collect(Collectors.toMap(Pair::getLeft, Pair::getRight));
-            */
-
-            PROGRESS.setProgress(0);
-            PROGRESS.setIndeterminate(true);
-            PROGRESS.setStep("Processing entries");
-
-            List<ClassEntry> ourClasses = oldEntries.stream()
-                .filter(e -> e instanceof ClassEntry && !e.getName().startsWith("META-INF/"))
-                .map(ClassEntry.class::cast)
-                .collect(Collectors.toList());
-
-            // Add the original classes to the inheritance map, TODO: Multi-Release somehow?
-            logger.accept("Adding input to inheritance map");
-            ClassProvider.Builder inputClassesBuilder = ClassProvider.builder();
-            async.consumeAll(ourClasses, ClassEntry::getClassName, c ->
-                inputClassesBuilder.addClass(c.getName().substring(0, c.getName().length() - 6), c.getData())
-            );
-            classProviders.add(0, inputClassesBuilder.build());
-
-            // Process everything
-            logger.accept("Processing entries");
-            List<Entry> newEntries = async.invokeAll(oldEntries, Entry::getName, this::processEntry);
-
-            logger.accept("Adding extras");
-            // Paper start - I'm pretty sure the duplicates are because the input is already on the classpath
-            List<Entry> finalNewEntries = newEntries;
-            transformers.forEach(t -> finalNewEntries.addAll(t.getExtras()));
-
-            Set<String> seen = new HashSet<>();
-            if (remappingSelf) {
-                // deduplicate
-                List<Entry> n = new ArrayList<>();
-                for (final Entry e : newEntries) {
-                    if (seen.add(e.getName())) {
-                        n.add(e);
-                    }
-                }
-                newEntries = n;
-            } else {
-            String dupes = newEntries.stream().map(Entry::getName)
-                .filter(n -> !seen.add(n))
-                .sorted()
-                .collect(Collectors.joining(", "));
-            if (!dupes.isEmpty())
-                throw new IllegalStateException("Duplicate entries detected: " + dupes);
-            }
-            // Paper end
-
-            // We care about stable output, so sort, and single thread write.
-            logger.accept("Sorting");
-            Collections.sort(newEntries, this::compare);
-
-            if (!output.getParentFile().exists())
-                output.getParentFile().mkdirs();
-
-            seen.clear();
-
-            PROGRESS.setMaxProgress(newEntries.size());
-            PROGRESS.setStep("Writing output");
-
-            logger.accept("Writing Output: " + output.getAbsolutePath());
-            try (OutputStream fos = new BufferedOutputStream(Files.newOutputStream(output.toPath()));
-                 ZipOutputStream zos = new ZipOutputStream(fos)) {
-
-                int amount = 0;
-                for (Entry e : newEntries) {
-                    String name = e.getName();
-                    int idx = name.lastIndexOf('/');
-                    if (idx != -1)
-                        addDirectory(zos, seen, name.substring(0, idx));
-
-                    logger.accept("  " + name);
-                    ZipEntry entry = new ZipEntry(name);
-                    entry.setTime(e.getTime());
-                    zos.putNextEntry(entry);
-                    zos.write(e.getData());
-                    zos.closeEntry();
-
-                    if ((++amount) % 10 == 0) {
-                        PROGRESS.setProgress(amount);
-                    }
-                }
-
-                PROGRESS.setProgress(amount);
-            }
-        } catch (final IOException e) {
-            throw new RuntimeException("Could not write to file " + output.getAbsolutePath(), e);
-        } finally {
-            async.shutdown();
-        }
-    }
-
-    private byte[] readAllBytes(InputStream in, long size) throws IOException {
-        // This program will crash if size exceeds MAX_INT anyway since arrays are limited to 32-bit indices
-        ByteArrayOutputStream tmp = new ByteArrayOutputStream(size >= 0 ? (int) size : 0);
-
-        byte[] buffer = new byte[8192];
-        int read;
-        while ((read = in.read(buffer)) != -1) {
-            tmp.write(buffer, 0, read);
-        }
-
-        return tmp.toByteArray();
-    }
-
-    // Tho Directory entries are not strictly necessary, we add them because some bad implementations of Zip extractors
-    // attempt to extract files without making sure the parents exist.
-    private void addDirectory(ZipOutputStream zos, Set<String> seen, String path) throws IOException {
-        if (!seen.add(path))
-            return;
-
-        int idx = path.lastIndexOf('/');
-        if (idx != -1)
-            addDirectory(zos, seen, path.substring(0, idx));
-
-        logger.accept("  " + path + '/');
-        ZipEntry dir = new ZipEntry(path + '/');
-        dir.setTime(Entry.STABLE_TIMESTAMP);
-        zos.putNextEntry(dir);
-        zos.closeEntry();
-    }
-
-    private Entry processEntry(final Entry start) {
-        Entry entry = start;
-        for (Transformer transformer : RenamerImpl.this.transformers) {
-            entry = entry.process(transformer);
-            if (entry == null)
-                return null;
-        }
-        return entry;
-    }
-
-    private int compare(Entry o1, Entry o2) {
-        // In order for JarInputStream to work, MANIFEST has to be the first entry, so make it first!
-        if (MANIFEST_NAME.equals(o1.getName()))
-            return MANIFEST_NAME.equals(o2.getName()) ? 0 : -1;
-        if (MANIFEST_NAME.equals(o2.getName()))
-            return MANIFEST_NAME.equals(o1.getName()) ? 0 :  1;
-        return o1.getName().compareTo(o2.getName());
-    }
-
-    @Override
-    public void close() throws IOException {
-        this.sortedClassProvider.close();
-    }
-}
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftArt.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftArt.java
index 0207c7c507..d4edaea715 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftArt.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftArt.java
@@ -1,17 +1,15 @@
 package org.bukkit.craftbukkit;
 
-import com.google.common.base.Preconditions;
-import java.util.Locale;
+import io.papermc.paper.adventure.PaperAdventure;
+import io.papermc.paper.util.OldEnumHolderable;
+import net.kyori.adventure.text.Component;
 import net.minecraft.core.Holder;
 import net.minecraft.core.registries.Registries;
 import net.minecraft.world.entity.decoration.PaintingVariant;
 import org.bukkit.Art;
-import org.bukkit.NamespacedKey;
 import org.bukkit.Registry;
-import org.bukkit.craftbukkit.util.Handleable;
-import org.jetbrains.annotations.NotNull;
 
-public class CraftArt implements Art, Handleable<PaintingVariant> {
+public class CraftArt extends OldEnumHolderable<Art, PaintingVariant> implements Art {
 
     private static int count = 0;
 
@@ -20,7 +18,7 @@ public class CraftArt implements Art, Handleable<PaintingVariant> {
     }
 
     public static Art minecraftHolderToBukkit(Holder<PaintingVariant> minecraft) {
-        return CraftArt.minecraftToBukkit(minecraft.value());
+        return CraftRegistry.minecraftHolderToBukkit(minecraft, Registry.ART);
     }
 
     public static PaintingVariant bukkitToMinecraft(Art bukkit) {
@@ -28,118 +26,41 @@ public class CraftArt implements Art, Handleable<PaintingVariant> {
     }
 
     public static Holder<PaintingVariant> bukkitToMinecraftHolder(Art bukkit) {
-        Preconditions.checkArgument(bukkit != null);
-
-        net.minecraft.core.Registry<PaintingVariant> registry = CraftRegistry.getMinecraftRegistry(Registries.PAINTING_VARIANT);
-
-        if (registry.wrapAsHolder(CraftArt.bukkitToMinecraft(bukkit)) instanceof Holder.Reference<PaintingVariant> holder) {
-            return holder;
-        }
-
-        throw new IllegalArgumentException("No Reference holder found for " + bukkit
-                + ", this can happen if a plugin creates its own painting variant with out properly registering it.");
+        return CraftRegistry.bukkitToMinecraftHolder(bukkit, Registries.PAINTING_VARIANT);
     }
 
-    private final NamespacedKey key;
-    private final PaintingVariant paintingVariant;
-    private final String name;
-    private final int ordinal;
-
-    public CraftArt(NamespacedKey key, PaintingVariant paintingVariant) {
-        this.key = key;
-        this.paintingVariant = paintingVariant;
-        // For backwards compatibility, minecraft values will stile return the uppercase name without the namespace,
-        // in case plugins use for example the name as key in a config file to receive art specific values.
-        // Custom arts will return the key with namespace. For a plugin this should look than like a new art
-        // (which can always be added in new minecraft versions and the plugin should therefore handle it accordingly).
-        if (NamespacedKey.MINECRAFT.equals(key.getNamespace())) {
-            this.name = key.getKey().toUpperCase(Locale.ROOT);
-        } else {
-            this.name = key.toString();
-        }
-        this.ordinal = CraftArt.count++;
-    }
-
-    @Override
-    public PaintingVariant getHandle() {
-        return this.paintingVariant;
+    public CraftArt(Holder<PaintingVariant> paintingVariant) {
+        super(paintingVariant, count++);
     }
 
     @Override
     public int getBlockWidth() {
-        return this.paintingVariant.width();
+        return this.getHandle().width();
     }
 
     @Override
     public int getBlockHeight() {
-        return this.paintingVariant.height();
+        return this.getHandle().height();
     }
 
     // Paper start - Expand Art API
     @Override
-    public net.kyori.adventure.text.Component title() {
-        return this.paintingVariant.title().map(io.papermc.paper.adventure.PaperAdventure::asAdventure).orElse(null);
+    public Component title() {
+        return this.getHandle().title().map(PaperAdventure::asAdventure).orElse(null);
     }
 
     @Override
     public net.kyori.adventure.text.Component author() {
-        return this.paintingVariant.author().map(io.papermc.paper.adventure.PaperAdventure::asAdventure).orElse(null);
+        return this.getHandle().author().map(PaperAdventure::asAdventure).orElse(null);
     }
 
     public net.kyori.adventure.key.Key assetId() {
-        return io.papermc.paper.adventure.PaperAdventure.asAdventure(this.paintingVariant.assetId());
+        return PaperAdventure.asAdventure(this.getHandle().assetId());
     }
     // Paper end - Expand Art API
 
     @Override
     public int getId() {
-        return CraftRegistry.getMinecraftRegistry(Registries.PAINTING_VARIANT).getId(this.paintingVariant);
-    }
-
-    @NotNull
-    @Override
-    public NamespacedKey getKey() {
-        if (true) return java.util.Objects.requireNonNull(org.bukkit.Registry.ART.getKey(this), () -> this + " doesn't have a key"); // Paper
-        return this.key;
-    }
-
-    @Override
-    public int compareTo(@NotNull Art art) {
-        return this.ordinal - art.ordinal();
-    }
-
-    @NotNull
-    @Override
-    public String name() {
-        return this.name;
-    }
-
-    @Override
-    public int ordinal() {
-        return this.ordinal;
-    }
-
-    @Override
-    public String toString() {
-        // For backwards compatibility
-        return this.name();
-    }
-
-    @Override
-    public boolean equals(Object other) {
-        if (this == other) {
-            return true;
-        }
-
-        if (!(other instanceof CraftArt otherArt)) {
-            return false;
-        }
-
-        return this.getKey().equals(otherArt.getKey());
-    }
-
-    @Override
-    public int hashCode() {
-        return this.getKey().hashCode();
+        return CraftRegistry.getMinecraftRegistry(Registries.PAINTING_VARIANT).getId(this.getHandle());
     }
 }
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftChunk.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
index f3ab07e44e..de8b9048c8 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
@@ -83,6 +83,12 @@ public class CraftChunk implements Chunk {
     }
 
     public ChunkAccess getHandle(ChunkStatus chunkStatus) {
+        // Paper start - chunk system
+        net.minecraft.world.level.chunk.LevelChunk full = this.worldServer.getChunkIfLoaded(this.x, this.z);
+        if (full != null) {
+            return full;
+        }
+        // Paper end - chunk system
         ChunkAccess chunkAccess = this.worldServer.getChunk(this.x, this.z, chunkStatus);
 
         // SPIGOT-7332: Get unwrapped extension
@@ -117,60 +123,12 @@ public class CraftChunk implements Chunk {
 
     @Override
     public boolean isEntitiesLoaded() {
-        return this.getCraftWorld().getHandle().entityManager.areEntitiesLoaded(ChunkPos.asLong(this.x, this.z));
+        return this.getCraftWorld().getHandle().areEntitiesLoaded(ChunkPos.asLong(this.x, this.z)); // Paper - chunk system
     }
 
     @Override
     public Entity[] getEntities() {
-        if (!this.isLoaded()) {
-            this.getWorld().getChunkAt(this.x, this.z); // Transient load for this tick
-        }
-
-        PersistentEntitySectionManager<net.minecraft.world.entity.Entity> entityManager = this.getCraftWorld().getHandle().entityManager;
-        long pair = ChunkPos.asLong(this.x, this.z);
-
-        if (entityManager.areEntitiesLoaded(pair)) {
-            return entityManager.getEntities(new ChunkPos(this.x, this.z)).stream()
-                    .map(net.minecraft.world.entity.Entity::getBukkitEntity)
-                    .filter(Objects::nonNull).toArray(Entity[]::new);
-        }
-
-        entityManager.ensureChunkQueuedForLoad(pair); // Start entity loading
-
-        // SPIGOT-6772: Use entity mailbox and re-schedule entities if they get unloaded
-        ConsecutiveExecutor mailbox = ((EntityStorage) entityManager.permanentStorage).entityDeserializerQueue;
-        BooleanSupplier supplier = () -> {
-            // only execute inbox if our entities are not present
-            if (entityManager.areEntitiesLoaded(pair)) {
-                return true;
-            }
-
-            if (!entityManager.isPending(pair)) {
-                // Our entities got unloaded, this should normally not happen.
-                entityManager.ensureChunkQueuedForLoad(pair); // Re-start entity loading
-            }
-
-            // tick loading inbox, which loads the created entities to the world
-            // (if present)
-            entityManager.tick();
-            // check if our entities are loaded
-            return entityManager.areEntitiesLoaded(pair);
-        };
-
-        // now we wait until the entities are loaded,
-        // the converting from NBT to entity object is done on the main Thread which is why we wait
-        while (!supplier.getAsBoolean()) {
-            if (mailbox.size() != 0) {
-                mailbox.run();
-            } else {
-                Thread.yield();
-                LockSupport.parkNanos("waiting for entity loading", 100000L);
-            }
-        }
-
-        return entityManager.getEntities(new ChunkPos(this.x, this.z)).stream()
-                .map(net.minecraft.world.entity.Entity::getBukkitEntity)
-                .filter(Objects::nonNull).toArray(Entity[]::new);
+        return FeatureHooks.getChunkEntities(this.worldServer, this.x, this.z); // Paper - chunk system
     }
 
     @Override
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftMusicInstrument.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftMusicInstrument.java
index aa19ac7513..520c3e2e1d 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftMusicInstrument.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftMusicInstrument.java
@@ -1,6 +1,7 @@
 package org.bukkit.craftbukkit;
 
 import com.google.common.base.Preconditions;
+import io.papermc.paper.util.Holderable;
 import net.minecraft.core.Holder;
 import net.minecraft.core.registries.Registries;
 import net.minecraft.world.item.Instrument;
@@ -75,8 +76,7 @@ public class CraftMusicInstrument extends MusicInstrument implements io.papermc.
     @NotNull
     @Override
     public NamespacedKey getKey() {
-        if (true) return java.util.Objects.requireNonNull(org.bukkit.Registry.INSTRUMENT.getKey(this), () -> this + " doesn't have a key"); // Paper
-        return this.key;
+        return Holderable.super.getKey();
     }
 
     // Paper start - add translationKey methods
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java
index a7e745bc13..a94cebfd26 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java
@@ -1,6 +1,7 @@
 package org.bukkit.craftbukkit;
 
 import com.google.common.base.Preconditions;
+import io.papermc.paper.util.Holderable;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
@@ -197,7 +198,6 @@ public class CraftRegistry<B extends Keyed, M> implements Registry<B> {
 
     private final Class<?> bukkitClass; // Paper - relax preload class
     private final Map<NamespacedKey, B> cache = new HashMap<>();
-    private final Map<B, NamespacedKey> byValue = new java.util.IdentityHashMap<>(); // Paper - improve Registry
     private final net.minecraft.core.Registry<M> minecraftRegistry;
     private final io.papermc.paper.registry.entry.RegistryTypeMapper<M, B> minecraftToBukkit; // Paper - switch to Holder
     private final BiFunction<NamespacedKey, ApiVersion, NamespacedKey> serializationUpdater; // Paper - rename to make it *clear* what it is *only* for
@@ -251,7 +251,6 @@ public class CraftRegistry<B extends Keyed, M> implements Registry<B> {
         }
 
         this.cache.put(namespacedKey, bukkit);
-        this.byValue.put(bukkit, namespacedKey); // Paper - improve Registry
 
         return bukkit;
     }
@@ -298,7 +297,10 @@ public class CraftRegistry<B extends Keyed, M> implements Registry<B> {
     // Paper start - improve Registry
     @Override
     public NamespacedKey getKey(final B value) {
-        return this.byValue.get(value);
+        if (value instanceof Holderable<?> holderable) {
+            return holderable.getKeyOrNull();
+        }
+        return Registry.super.getKey(value);
     }
     // Paper end - improve Registry
 
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java
index 5b64111bc8..f59b4a6998 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java
@@ -904,13 +904,13 @@ public final class CraftServer implements Server {
 
     @Override
     public long getConnectionThrottle() {
-        // Spigot Start - Automatically set connection throttle for bungee configurations
+        // Spigot start - Automatically set connection throttle for bungee configurations
         if (org.spigotmc.SpigotConfig.bungee || io.papermc.paper.configuration.GlobalConfiguration.get().proxies.velocity.enabled) { // Paper - Add Velocity IP Forwarding Support
             return -1;
         } else {
             return this.configuration.getInt("settings.connection-throttle");
         }
-        // Spigot End
+        // Spigot end
     }
 
     @Override
@@ -1288,7 +1288,7 @@ public final class CraftServer implements Server {
         Preconditions.checkArgument(creator != null, "WorldCreator cannot be null");
 
         String name = creator.name();
-        ChunkGenerator generator = creator.generator();
+        ChunkGenerator chunkGenerator = creator.generator();
         BiomeProvider biomeProvider = creator.biomeProvider();
         File folder = new File(this.getWorldContainer(), name);
         World world = this.getWorld(name);
@@ -1307,151 +1307,170 @@ public final class CraftServer implements Server {
             Preconditions.checkArgument(folder.isDirectory(), "File (%s) exists and isn't a folder", name);
         }
 
-        if (generator == null) {
-            generator = this.getGenerator(name);
+        if (chunkGenerator == null) {
+            chunkGenerator = this.getGenerator(name);
         }
 
         if (biomeProvider == null) {
             biomeProvider = this.getBiomeProvider(name);
         }
 
-        ResourceKey<LevelStem> actualDimension;
-        switch (creator.environment()) {
-            case NORMAL:
-                actualDimension = LevelStem.OVERWORLD;
-                break;
-            case NETHER:
-                actualDimension = LevelStem.NETHER;
-                break;
-            case THE_END:
-                actualDimension = LevelStem.END;
-                break;
-            default:
-                throw new IllegalArgumentException("Illegal dimension (" + creator.environment() + ")");
-        }
+        ResourceKey<LevelStem> actualDimension = switch (creator.environment()) {
+            case NORMAL -> LevelStem.OVERWORLD;
+            case NETHER -> LevelStem.NETHER;
+            case THE_END -> LevelStem.END;
+            default -> throw new IllegalArgumentException("Illegal dimension (" + creator.environment() + ")");
+        };
 
-        LevelStorageSource.LevelStorageAccess worldSession;
+        LevelStorageSource.LevelStorageAccess levelStorageAccess;
         try {
-            worldSession = LevelStorageSource.createDefault(this.getWorldContainer().toPath()).validateAndCreateAccess(name, actualDimension);
+            levelStorageAccess = LevelStorageSource.createDefault(this.getWorldContainer().toPath()).validateAndCreateAccess(name, actualDimension);
         } catch (IOException | ContentValidationException ex) {
             throw new RuntimeException(ex);
         }
 
-        Dynamic<?> dynamic;
-        if (worldSession.hasWorldData()) {
-            net.minecraft.world.level.storage.LevelSummary worldinfo;
-
+        Dynamic<?> dataTag;
+        if (levelStorageAccess.hasWorldData()) {
+            net.minecraft.world.level.storage.LevelSummary summary;
             try {
-                dynamic = worldSession.getDataTag();
-                worldinfo = worldSession.getSummary(dynamic);
-            } catch (NbtException | ReportedNbtException | IOException ioexception) {
-                LevelStorageSource.LevelDirectory convertable_b = worldSession.getLevelDirectory();
-
-                MinecraftServer.LOGGER.warn("Failed to load world data from {}", convertable_b.dataFile(), ioexception);
+                dataTag = levelStorageAccess.getDataTag();
+                summary = levelStorageAccess.getSummary(dataTag);
+            } catch (NbtException | ReportedNbtException | IOException e) {
+                LevelStorageSource.LevelDirectory levelDirectory = levelStorageAccess.getLevelDirectory();
+                MinecraftServer.LOGGER.warn("Failed to load world data from {}", levelDirectory.dataFile(), e);
                 MinecraftServer.LOGGER.info("Attempting to use fallback");
 
                 try {
-                    dynamic = worldSession.getDataTagFallback();
-                    worldinfo = worldSession.getSummary(dynamic);
-                } catch (NbtException | ReportedNbtException | IOException ioexception1) {
-                    MinecraftServer.LOGGER.error("Failed to load world data from {}", convertable_b.oldDataFile(), ioexception1);
-                    MinecraftServer.LOGGER.error("Failed to load world data from {} and {}. World files may be corrupted. Shutting down.", convertable_b.dataFile(), convertable_b.oldDataFile());
+                    dataTag = levelStorageAccess.getDataTagFallback();
+                    summary = levelStorageAccess.getSummary(dataTag);
+                } catch (NbtException | ReportedNbtException | IOException e1) {
+                    MinecraftServer.LOGGER.error("Failed to load world data from {}", levelDirectory.oldDataFile(), e1);
+                    MinecraftServer.LOGGER.error(
+                        "Failed to load world data from {} and {}. World files may be corrupted. Shutting down.",
+                        levelDirectory.dataFile(),
+                        levelDirectory.oldDataFile()
+                    );
                     return null;
                 }
 
-                worldSession.restoreLevelDataFromOld();
+                levelStorageAccess.restoreLevelDataFromOld();
             }
 
-            if (worldinfo.requiresManualConversion()) {
+            if (summary.requiresManualConversion()) {
                 MinecraftServer.LOGGER.info("This world must be opened in an older version (like 1.6.4) to be safely converted");
                 return null;
             }
 
-            if (!worldinfo.isCompatible()) {
+            if (!summary.isCompatible()) {
                 MinecraftServer.LOGGER.info("This world was created by an incompatible version.");
                 return null;
             }
         } else {
-            dynamic = null;
+            dataTag = null;
         }
 
         boolean hardcore = creator.hardcore();
 
-        PrimaryLevelData worlddata;
-        WorldLoader.DataLoadContext worldloader_a = this.console.worldLoader;
-        RegistryAccess.Frozen iregistrycustom_dimension = worldloader_a.datapackDimensions();
-        net.minecraft.core.Registry<LevelStem> iregistry = iregistrycustom_dimension.lookupOrThrow(Registries.LEVEL_STEM);
-        if (dynamic != null) {
-            LevelDataAndDimensions leveldataanddimensions = LevelStorageSource.getLevelDataAndDimensions(dynamic, worldloader_a.dataConfiguration(), iregistry, worldloader_a.datapackWorldgen());
-
-            worlddata = (PrimaryLevelData) leveldataanddimensions.worldData();
-            iregistrycustom_dimension = leveldataanddimensions.dimensions().dimensionsRegistryAccess();
+        PrimaryLevelData primaryLevelData;
+        WorldLoader.DataLoadContext context = this.console.worldLoader;
+        RegistryAccess.Frozen registryAccess = context.datapackDimensions();
+        net.minecraft.core.Registry<LevelStem> contextLevelStemRegistry = registryAccess.lookupOrThrow(Registries.LEVEL_STEM);
+        if (dataTag != null) {
+            LevelDataAndDimensions levelDataAndDimensions = LevelStorageSource.getLevelDataAndDimensions(
+                dataTag, context.dataConfiguration(), contextLevelStemRegistry, context.datapackWorldgen()
+            );
+            primaryLevelData = (PrimaryLevelData) levelDataAndDimensions.worldData();
+            registryAccess = levelDataAndDimensions.dimensions().dimensionsRegistryAccess();
         } else {
-            LevelSettings worldsettings;
-            WorldOptions worldoptions = new WorldOptions(creator.seed(), creator.generateStructures(), false);
-            WorldDimensions worlddimensions;
+            LevelSettings levelSettings;
+            WorldOptions worldOptions = new WorldOptions(creator.seed(), creator.generateStructures(), false);
+            WorldDimensions worldDimensions;
 
             DedicatedServerProperties.WorldDimensionData properties = new DedicatedServerProperties.WorldDimensionData(GsonHelper.parse((creator.generatorSettings().isEmpty()) ? "{}" : creator.generatorSettings()), creator.type().name().toLowerCase(Locale.ROOT));
+            levelSettings = new LevelSettings(
+                name,
+                GameType.byId(this.getDefaultGameMode().getValue()),
+                hardcore, Difficulty.EASY,
+                false,
+                new GameRules(context.dataConfiguration().enabledFeatures()),
+                context.dataConfiguration())
+            ;
+            worldDimensions = properties.create(context.datapackWorldgen());
 
-            worldsettings = new LevelSettings(name, GameType.byId(this.getDefaultGameMode().getValue()), hardcore, Difficulty.EASY, false, new GameRules(worldloader_a.dataConfiguration().enabledFeatures()), worldloader_a.dataConfiguration());
-            worlddimensions = properties.create(worldloader_a.datapackWorldgen());
+            WorldDimensions.Complete complete = worldDimensions.bake(contextLevelStemRegistry);
+            Lifecycle lifecycle = complete.lifecycle().add(context.datapackWorldgen().allRegistriesLifecycle());
 
-            WorldDimensions.Complete worlddimensions_b = worlddimensions.bake(iregistry);
-            Lifecycle lifecycle = worlddimensions_b.lifecycle().add(worldloader_a.datapackWorldgen().allRegistriesLifecycle());
-
-            worlddata = new PrimaryLevelData(worldsettings, worldoptions, worlddimensions_b.specialWorldProperty(), lifecycle);
-            iregistrycustom_dimension = worlddimensions_b.dimensionsRegistryAccess();
+            primaryLevelData = new PrimaryLevelData(levelSettings, worldOptions, complete.specialWorldProperty(), lifecycle);
+            registryAccess = complete.dimensionsRegistryAccess();
         }
-        iregistry = iregistrycustom_dimension.lookupOrThrow(Registries.LEVEL_STEM);
-        worlddata.customDimensions = iregistry;
-        worlddata.checkName(name);
-        worlddata.setModdedInfo(this.console.getServerModName(), this.console.getModdedStatus().shouldReportAsModified());
+
+        contextLevelStemRegistry = registryAccess.lookupOrThrow(Registries.LEVEL_STEM);
+        primaryLevelData.customDimensions = contextLevelStemRegistry;
+        primaryLevelData.checkName(name);
+        primaryLevelData.setModdedInfo(this.console.getServerModName(), this.console.getModdedStatus().shouldReportAsModified());
 
         if (this.console.options.has("forceUpgrade")) {
-            net.minecraft.server.Main.forceUpgrade(worldSession, DataFixers.getDataFixer(), this.console.options.has("eraseCache"), () -> true, iregistrycustom_dimension, this.console.options.has("recreateRegionFiles"));
+            net.minecraft.server.Main.forceUpgrade(levelStorageAccess, DataFixers.getDataFixer(), this.console.options.has("eraseCache"), () -> true, registryAccess, this.console.options.has("recreateRegionFiles"));
         }
 
-        long j = BiomeManager.obfuscateSeed(worlddata.worldGenOptions().seed()); // Paper - use world seed
-        List<CustomSpawner> list = ImmutableList.of(new PhantomSpawner(), new PatrolSpawner(), new CatSpawner(), new VillageSiege(), new WanderingTraderSpawner(worlddata));
-        LevelStem worlddimension = iregistry.getValue(actualDimension);
+        long i = BiomeManager.obfuscateSeed(primaryLevelData.worldGenOptions().seed());
+        List<CustomSpawner> list = ImmutableList.of(
+            new PhantomSpawner(), new PatrolSpawner(), new CatSpawner(), new VillageSiege(), new WanderingTraderSpawner(primaryLevelData)
+        );
+        LevelStem customStem = contextLevelStemRegistry.getValue(actualDimension);
 
-        WorldInfo worldInfo = new CraftWorldInfo(worlddata, worldSession, creator.environment(), worlddimension.type().value(), worlddimension.generator(), this.getHandle().getServer().registryAccess()); // Paper - Expose vanilla BiomeProvider from WorldInfo
-        if (biomeProvider == null && generator != null) {
-            biomeProvider = generator.getDefaultBiomeProvider(worldInfo);
+        WorldInfo worldInfo = new CraftWorldInfo(primaryLevelData, levelStorageAccess, creator.environment(), customStem.type().value(), customStem.generator(), this.getHandle().getServer().registryAccess()); // Paper - Expose vanilla BiomeProvider from WorldInfo
+        if (biomeProvider == null && chunkGenerator != null) {
+            biomeProvider = chunkGenerator.getDefaultBiomeProvider(worldInfo);
         }
 
-        ResourceKey<net.minecraft.world.level.Level> worldKey;
+        ResourceKey<net.minecraft.world.level.Level> dimensionKey;
         String levelName = this.getServer().getProperties().levelName;
         if (name.equals(levelName + "_nether")) {
-            worldKey = net.minecraft.world.level.Level.NETHER;
+            dimensionKey = net.minecraft.world.level.Level.NETHER;
         } else if (name.equals(levelName + "_the_end")) {
-            worldKey = net.minecraft.world.level.Level.END;
+            dimensionKey = net.minecraft.world.level.Level.END;
         } else {
-            worldKey = ResourceKey.create(Registries.DIMENSION, ResourceLocation.fromNamespaceAndPath(creator.key().namespace(), creator.key().value()));
+            dimensionKey = ResourceKey.create(Registries.DIMENSION, ResourceLocation.fromNamespaceAndPath(creator.key().namespace(), creator.key().value()));
         }
 
         // If set to not keep spawn in memory (changed from default) then adjust rule accordingly
         if (creator.keepSpawnLoaded() == net.kyori.adventure.util.TriState.FALSE) { // Paper
-            worlddata.getGameRules().getRule(GameRules.RULE_SPAWN_CHUNK_RADIUS).set(0, null);
+            primaryLevelData.getGameRules().getRule(GameRules.RULE_SPAWN_CHUNK_RADIUS).set(0, null);
         }
-        ServerLevel internal = (ServerLevel) new ServerLevel(this.console, this.console.executor, worldSession, worlddata, worldKey, worlddimension, this.getServer().progressListenerFactory.create(worlddata.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS)),
-                worlddata.isDebugWorld(), j, creator.environment() == Environment.NORMAL ? list : ImmutableList.of(), true, this.console.overworld().getRandomSequences(), creator.environment(), generator, biomeProvider);
+
+        ServerLevel serverLevel = new ServerLevel(
+            this.console,
+            this.console.executor,
+            levelStorageAccess,
+            primaryLevelData,
+            dimensionKey,
+            customStem,
+            this.getServer().progressListenerFactory.create(primaryLevelData.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS)),
+            primaryLevelData.isDebugWorld(),
+            i,
+            creator.environment() == Environment.NORMAL ? list : ImmutableList.of(),
+            true,
+            this.console.overworld().getRandomSequences(),
+            creator.environment(),
+            chunkGenerator, biomeProvider
+        );
 
         if (!(this.worlds.containsKey(name.toLowerCase(Locale.ROOT)))) {
             return null;
         }
 
-        this.console.addLevel(internal); // Paper - Put world into worldlist before initing the world; move up
-        this.console.initWorld(internal, worlddata, worlddata, worlddata.worldGenOptions());
+        this.console.addLevel(serverLevel); // Paper - Put world into worldlist before initing the world; move up
+        this.console.initWorld(serverLevel, primaryLevelData, primaryLevelData, primaryLevelData.worldGenOptions());
 
-        internal.setSpawnSettings(true);
+        serverLevel.setSpawnSettings(true);
         // Paper - Put world into worldlist before initing the world; move up
 
-        this.getServer().prepareLevels(internal.getChunkSource().chunkMap.progressListener, internal);
-        internal.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API
+        this.getServer().prepareLevels(serverLevel.getChunkSource().chunkMap.progressListener, serverLevel);
+        io.papermc.paper.FeatureHooks.tickEntityManager(serverLevel); // SPIGOT-6526: Load pending entities so they are available to the API // Paper - chunk system
 
-        this.pluginManager.callEvent(new WorldLoadEvent(internal.getWorld()));
-        return internal.getWorld();
+        this.pluginManager.callEvent(new WorldLoadEvent(serverLevel.getWorld()));
+        return serverLevel.getWorld();
     }
 
     @Override
@@ -1493,8 +1512,8 @@ public final class CraftServer implements Server {
             }
 
             handle.getChunkSource().close(save);
-            handle.entityManager.close(save); // SPIGOT-6722: close entityManager
-            handle.convertable.close();
+            io.papermc.paper.FeatureHooks.closeEntityManager(handle, save); // SPIGOT-6722: close entityManager // Paper - chunk system
+            handle.levelStorageAccess.close();
         } catch (Exception ex) {
             this.getLogger().log(Level.SEVERE, null, ex);
         }
@@ -2131,7 +2150,7 @@ public final class CraftServer implements Server {
         if (result == null) {
             GameProfile profile = null;
             // Only fetch an online UUID in online mode
-            if (this.getOnlineMode() || io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode()) { // Paper - Add setting for proxy online mode status
+            if (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode()) { // Paper - Add setting for proxy online mode status
                 // This is potentially blocking :(
                 profile = this.console.getProfileCache().get(name).orElse(null);
             }
@@ -2585,12 +2604,11 @@ public final class CraftServer implements Server {
     }
 
     public List<String> tabCompleteCommand(Player player, String message, ServerLevel world, Vec3 pos) {
-        // Spigot Start
-        if ( (org.spigotmc.SpigotConfig.tabComplete < 0 || message.length() <= org.spigotmc.SpigotConfig.tabComplete) && !message.contains( " " ) )
-        {
+        // Spigot start
+        if ((org.spigotmc.SpigotConfig.tabComplete < 0 || message.length() <= org.spigotmc.SpigotConfig.tabComplete) && !message.contains(" ")) {
             return ImmutableList.of();
         }
-        // Spigot End
+        // Spigot end
 
         List<String> completions = null;
         try {
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftSound.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftSound.java
index e24d784674..451fe79b1e 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftSound.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftSound.java
@@ -1,17 +1,13 @@
 package org.bukkit.craftbukkit;
 
-import com.google.common.base.Preconditions;
-import java.util.Locale;
+import io.papermc.paper.util.OldEnumHolderable;
 import net.minecraft.core.Holder;
 import net.minecraft.core.registries.Registries;
 import net.minecraft.sounds.SoundEvent;
-import org.bukkit.NamespacedKey;
 import org.bukkit.Registry;
 import org.bukkit.Sound;
-import org.bukkit.craftbukkit.util.Handleable;
-import org.jetbrains.annotations.NotNull;
 
-public class CraftSound implements Sound, Handleable<SoundEvent> {
+public class CraftSound extends OldEnumHolderable<Sound, SoundEvent> implements Sound {
 
     private static int count = 0;
 
@@ -24,88 +20,11 @@ public class CraftSound implements Sound, Handleable<SoundEvent> {
     }
 
     public static Holder<SoundEvent> bukkitToMinecraftHolder(Sound bukkit) {
-        Preconditions.checkArgument(bukkit != null);
-
-        net.minecraft.core.Registry<SoundEvent> registry = CraftRegistry.getMinecraftRegistry(Registries.SOUND_EVENT);
-
-        if (registry.wrapAsHolder(CraftSound.bukkitToMinecraft(bukkit)) instanceof Holder.Reference<SoundEvent> holder) {
-            return holder;
-        }
-
-        throw new IllegalArgumentException("No Reference holder found for " + bukkit
-                + ", this can happen if a plugin creates its own sound effect with out properly registering it.");
+        return CraftRegistry.bukkitToMinecraftHolder(bukkit, Registries.SOUND_EVENT);
     }
 
-    private final NamespacedKey key;
-    private final SoundEvent soundEffect;
-    private final String name;
-    private final int ordinal;
-
-    public CraftSound(NamespacedKey key, SoundEvent soundEffect) {
-        this.key = key;
-        this.soundEffect = soundEffect;
-        // For backwards compatibility, minecraft values will stile return the uppercase name without the namespace,
-        // in case plugins use for example the name as key in a config file to receive sound specific values.
-        // Custom sounds will return the key with namespace. For a plugin this should look than like a new sound
-        // (which can always be added in new minecraft versions and the plugin should therefore handle it accordingly).
-        if (NamespacedKey.MINECRAFT.equals(key.getNamespace())) {
-            this.name = key.getKey().toUpperCase(Locale.ROOT).replace('.', '_');
-        } else {
-            this.name = key.toString();
-        }
-        this.ordinal = CraftSound.count++;
-    }
-
-    @Override
-    public SoundEvent getHandle() {
-        return this.soundEffect;
-    }
-
-    @NotNull
-    @Override
-    public NamespacedKey getKey() {
-        if (true) return java.util.Objects.requireNonNull(org.bukkit.Registry.SOUNDS.getKey(this), () -> this + " doesn't have a key"); // Paper
-        return this.key;
-    }
-
-    @Override
-    public int compareTo(@NotNull Sound sound) {
-        return this.ordinal - sound.ordinal();
-    }
-
-    @NotNull
-    @Override
-    public String name() {
-        return this.name;
-    }
-
-    @Override
-    public int ordinal() {
-        return this.ordinal;
-    }
-
-    @Override
-    public String toString() {
-        // For backwards compatibility
-        return this.name();
-    }
-
-    @Override
-    public boolean equals(Object other) {
-        if (this == other) {
-            return true;
-        }
-
-        if (!(other instanceof CraftSound otherSound)) {
-            return false;
-        }
-
-        return this.getKey().equals(otherSound.getKey());
-    }
-
-    @Override
-    public int hashCode() {
-        return this.getKey().hashCode();
+    public CraftSound(Holder<SoundEvent> soundEffect) {
+        super(soundEffect, count++);
     }
 
     // Paper start
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
index ca62105a0f..7ebe1d8090 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
@@ -6,7 +6,6 @@ import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.mojang.datafixers.util.Pair;
 import io.papermc.paper.FeatureHooks;
-import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
 import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
 import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
 import java.io.File;
@@ -21,7 +20,6 @@ import java.util.Objects;
 import java.util.Random;
 import java.util.Set;
 import java.util.UUID;
-import java.util.concurrent.ExecutionException;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
@@ -32,7 +30,6 @@ import net.minecraft.core.particles.ParticleTypes;
 import net.minecraft.core.registries.Registries;
 import net.minecraft.nbt.CompoundTag;
 import net.minecraft.nbt.Tag;
-import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket;
 import net.minecraft.network.protocol.game.ClientboundLevelEventPacket;
 import net.minecraft.network.protocol.game.ClientboundSetTimePacket;
 import net.minecraft.network.protocol.game.ClientboundSoundEntityPacket;
@@ -77,7 +74,6 @@ import org.bukkit.Chunk;
 import org.bukkit.ChunkSnapshot;
 import org.bukkit.Difficulty;
 import org.bukkit.Effect;
-import org.bukkit.FeatureFlag;
 import org.bukkit.FluidCollisionMode;
 import org.bukkit.GameRule;
 import org.bukkit.Instrument;
@@ -130,7 +126,6 @@ import org.bukkit.entity.TippedArrow;
 import org.bukkit.entity.Trident;
 import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
 import org.bukkit.event.weather.LightningStrikeEvent;
-import org.bukkit.event.world.SpawnChangeEvent;
 import org.bukkit.event.world.TimeSkipEvent;
 import org.bukkit.generator.BiomeProvider;
 import org.bukkit.generator.BlockPopulator;
@@ -221,7 +216,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
     public int getTileEntityCount() {
         // We don't use the full world tile entity list, so we must iterate chunks
         int size = 0;
-        for (ChunkHolder playerchunk : ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.world)) {
+        for (ChunkHolder playerchunk : ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.world)) {
             net.minecraft.world.level.chunk.LevelChunk chunk = playerchunk.getTickingChunk();
             if (chunk == null) {
                 continue;
@@ -410,7 +405,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
             return chunk instanceof ImposterProtoChunk || chunk instanceof net.minecraft.world.level.chunk.LevelChunk;
         }
         final java.util.concurrent.CompletableFuture<ChunkAccess> future = new java.util.concurrent.CompletableFuture<>();
-        ca.spottedleaf.moonrise.common.util.ChunkSystem.scheduleChunkLoad(
+        ca.spottedleaf.moonrise.common.PlatformHooks.get().scheduleChunkLoad(
             this.world, x, z, false, ChunkStatus.EMPTY, true, ca.spottedleaf.concurrentutil.util.Priority.NORMAL, future::complete
         );
         world.getChunkSource().mainThreadProcessor.managedBlock(future::isDone);
@@ -425,7 +420,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
 
     @Override
     public Chunk[] getLoadedChunks() {
-        List<ChunkHolder> chunks = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.world); // Paper
+        List<ChunkHolder> chunks = ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.world); // Paper
         return chunks.stream().map(ChunkHolder::getFullChunkNow).filter(Objects::nonNull).map(CraftChunk::new).toArray(Chunk[]::new);
     }
 
@@ -507,14 +502,17 @@ public class CraftWorld extends CraftRegionAccessor implements World {
         ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z));
         if (playerChunk == null) return false;
 
-        playerChunk.getTickingChunkFuture().thenAccept(either -> {
-            either.ifSuccess(chunk -> {
+        // Paper start - chunk system
+        net.minecraft.world.level.chunk.LevelChunk chunk = playerChunk.getChunkToSend();
+        if (chunk == null) {
+            return false;
+        }
+        // Paper end - chunk system
                 List<ServerPlayer> playersInRange = playerChunk.playerProvider.getPlayers(playerChunk.getPos(), false);
-                if (playersInRange.isEmpty()) return;
+                if (playersInRange.isEmpty()) return true; // Paper - chunk system
 
                 FeatureHooks.sendChunkRefreshPackets(playersInRange, chunk);
-            });
-        });
+        // Paper - chunk system
 
         return true;
     }
@@ -589,10 +587,9 @@ public class CraftWorld extends CraftRegionAccessor implements World {
         Preconditions.checkArgument(plugin != null, "null plugin");
         Preconditions.checkArgument(plugin.isEnabled(), "plugin is not enabled");
 
-        DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager;
-
-        if (chunkDistanceManager.addRegionTicketAtDistance(TicketType.PLUGIN_TICKET, new ChunkPos(x, z), 2, plugin)) { // keep in-line with force loading, add at level 31
-            this.getChunkAt(x, z); // ensure loaded
+        final DistanceManager distanceManager = this.world.getChunkSource().chunkMap.distanceManager;
+        if (distanceManager.addPluginRegionTicket(new ChunkPos(x, z), plugin)) {
+            this.getChunkAt(x, z); // ensure it's loaded
             return true;
         }
 
@@ -603,8 +600,8 @@ public class CraftWorld extends CraftRegionAccessor implements World {
     public boolean removePluginChunkTicket(int x, int z, Plugin plugin) {
         Preconditions.checkNotNull(plugin, "null plugin");
 
-        DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager;
-        return chunkDistanceManager.removeRegionTicketAtDistance(TicketType.PLUGIN_TICKET, new ChunkPos(x, z), 2, plugin); // keep in-line with force loading, remove at level 31
+        final DistanceManager distanceManager = this.world.getChunkSource().chunkMap.distanceManager;
+        return distanceManager.removePluginRegionTicket(new ChunkPos(x, z), plugin);
     }
 
     @Override
@@ -617,47 +614,12 @@ public class CraftWorld extends CraftRegionAccessor implements World {
 
     @Override
     public Collection<Plugin> getPluginChunkTickets(int x, int z) {
-        DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager;
-        SortedArraySet<Ticket<?>> tickets = chunkDistanceManager.tickets.get(ChunkPos.asLong(x, z));
-
-        if (tickets == null) {
-            return Collections.emptyList();
-        }
-
-        ImmutableList.Builder<Plugin> ret = ImmutableList.builder();
-        for (Ticket<?> ticket : tickets) {
-            if (ticket.getType() == TicketType.PLUGIN_TICKET) {
-                ret.add((Plugin) ticket.key);
-            }
-        }
-
-        return ret.build();
+        return FeatureHooks.getPluginChunkTickets(this.world, x, z); // Paper - chunk system
     }
 
     @Override
     public Map<Plugin, Collection<Chunk>> getPluginChunkTickets() {
-        Map<Plugin, ImmutableList.Builder<Chunk>> ret = new HashMap<>();
-        DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager;
-
-        for (Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> chunkTickets : chunkDistanceManager.tickets.long2ObjectEntrySet()) {
-            long chunkKey = chunkTickets.getLongKey();
-            SortedArraySet<Ticket<?>> tickets = chunkTickets.getValue();
-
-            Chunk chunk = null;
-            for (Ticket<?> ticket : tickets) {
-                if (ticket.getType() != TicketType.PLUGIN_TICKET) {
-                    continue;
-                }
-
-                if (chunk == null) {
-                    chunk = this.getChunkAt(ChunkPos.getX(chunkKey), ChunkPos.getZ(chunkKey));
-                }
-
-                ret.computeIfAbsent((Plugin) ticket.key, (key) -> ImmutableList.builder()).add(chunk);
-            }
-        }
-
-        return ret.entrySet().stream().collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, (entry) -> entry.getValue().build()));
+        return FeatureHooks.getPluginChunkTickets(this.world); // Paper - chunk system
     }
 
     @NotNull
@@ -1342,12 +1304,12 @@ public class CraftWorld extends CraftRegionAccessor implements World {
 
     @Override
     public int getViewDistance() {
-        return this.world.getChunkSource().chunkMap.serverViewDistance;
+        return FeatureHooks.getViewDistance(this.world); // Paper - chunk system
     }
 
     @Override
     public int getSimulationDistance() {
-        return this.world.getChunkSource().chunkMap.getDistanceManager().simulationDistance;
+        return FeatureHooks.getSimulationDistance(this.world); // Paper - chunk system
     }
 
     public BlockMetadataStore getBlockMetadata() {
@@ -1631,7 +1593,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
 
     @Override
     public File getWorldFolder() {
-        return this.world.convertable.getLevelPath(LevelResource.ROOT).toFile().getParentFile();
+        return this.world.levelStorageAccess.getLevelPath(LevelResource.ROOT).toFile().getParentFile();
     }
 
     @Override
@@ -2453,7 +2415,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
     @Override
     public void getChunkAtAsync(int x, int z, boolean gen, boolean urgent, @NotNull Consumer<? super Chunk> cb) {
         warnUnsafeChunk("getting a faraway chunk async", x, z); // Paper
-        ca.spottedleaf.moonrise.common.util.ChunkSystem.scheduleChunkLoad(
+        ca.spottedleaf.moonrise.common.PlatformHooks.get().scheduleChunkLoad(
             this.getHandle(), x, z, gen, ChunkStatus.FULL, true,
             urgent ? ca.spottedleaf.concurrentutil.util.Priority.HIGHER : ca.spottedleaf.concurrentutil.util.Priority.NORMAL,
             (ChunkAccess chunk) -> {
@@ -2478,25 +2440,22 @@ public class CraftWorld extends CraftRegionAccessor implements World {
 
     @Override
     public void setViewDistance(final int viewDistance) {
-        if (viewDistance < 2 || viewDistance > 32) {
-            throw new IllegalArgumentException("View distance " + viewDistance + " is out of range of [2, 32]");
-        }
-        this.getHandle().chunkSource.chunkMap.setServerViewDistance(viewDistance);
+        FeatureHooks.setViewDistance(this.world, viewDistance); // Paper - chunk system
     }
 
     @Override
     public void setSimulationDistance(final int simulationDistance) {
-        throw new UnsupportedOperationException("Not implemented yet");
+        FeatureHooks.setSimulationDistance(this.world, simulationDistance); // Paper - chunk system
     }
 
     @Override
     public int getSendViewDistance() {
-        return this.getViewDistance();
+        return FeatureHooks.getSendViewDistance(this.world); // Paper - chunk system
     }
 
     @Override
     public void setSendViewDistance(final int viewDistance) {
-        throw new UnsupportedOperationException("Not implemented yet");
+        FeatureHooks.setSendViewDistance(this.world, viewDistance); // Paper - chunk system
     }
 
     // Paper start - implement pointers
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/Main.java b/paper-server/src/main/java/org/bukkit/craftbukkit/Main.java
index 1c2439ffc1..ecb0fcd1f3 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/Main.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/Main.java
@@ -149,13 +149,13 @@ public class Main {
 
                 this.acceptsAll(Main.asList("initSettings"), "Only create configuration files and then exit"); // SPIGOT-5761: Add initSettings option
 
-                // Spigot Start
+                // Spigot start
                 this.acceptsAll(Main.asList("S", "spigot-settings"), "File for spigot settings")
                         .withRequiredArg()
                         .ofType(File.class)
                         .defaultsTo(new File("spigot.yml"))
                         .describedAs("Yml file");
-                // Spigot End
+                // Spigot end
 
                 // Paper start
                 acceptsAll(asList("paper-dir", "paper-settings-directory"), "Directory for Paper settings")
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/block/banner/CraftPatternType.java b/paper-server/src/main/java/org/bukkit/craftbukkit/block/banner/CraftPatternType.java
index 3addb382d0..749024b259 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/block/banner/CraftPatternType.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/block/banner/CraftPatternType.java
@@ -1,17 +1,14 @@
 package org.bukkit.craftbukkit.block.banner;
 
-import com.google.common.base.Preconditions;
-import java.util.Locale;
+import io.papermc.paper.util.OldEnumHolderable;
 import net.minecraft.core.Holder;
 import net.minecraft.core.registries.Registries;
 import net.minecraft.world.level.block.entity.BannerPattern;
-import org.bukkit.NamespacedKey;
 import org.bukkit.Registry;
 import org.bukkit.block.banner.PatternType;
 import org.bukkit.craftbukkit.CraftRegistry;
-import org.bukkit.craftbukkit.util.Handleable;
 
-public class CraftPatternType implements PatternType, Handleable<BannerPattern> {
+public class CraftPatternType extends OldEnumHolderable<PatternType, BannerPattern> implements PatternType {
 
     private static int count = 0;
 
@@ -20,7 +17,7 @@ public class CraftPatternType implements PatternType, Handleable<BannerPattern>
     }
 
     public static PatternType minecraftHolderToBukkit(Holder<BannerPattern> minecraft) {
-        return CraftPatternType.minecraftToBukkit(minecraft.value());
+        return CraftRegistry.minecraftHolderToBukkit(minecraft, Registry.BANNER_PATTERN);
     }
 
     public static BannerPattern bukkitToMinecraft(PatternType bukkit) {
@@ -28,86 +25,11 @@ public class CraftPatternType implements PatternType, Handleable<BannerPattern>
     }
 
     public static Holder<BannerPattern> bukkitToMinecraftHolder(PatternType bukkit) {
-        Preconditions.checkArgument(bukkit != null);
-
-        net.minecraft.core.Registry<BannerPattern> registry = CraftRegistry.getMinecraftRegistry(Registries.BANNER_PATTERN);
-
-        if (registry.wrapAsHolder(CraftPatternType.bukkitToMinecraft(bukkit)) instanceof Holder.Reference<BannerPattern> holder) {
-            return holder;
-        }
-
-        throw new IllegalArgumentException("No Reference holder found for " + bukkit
-                + ", this can happen if a plugin creates its own banner pattern without properly registering it.");
+        return CraftRegistry.bukkitToMinecraftHolder(bukkit, Registries.BANNER_PATTERN);
     }
 
-    private final NamespacedKey key;
-    private final BannerPattern bannerPatternType;
-    private final String name;
-    private final int ordinal;
-
-    public CraftPatternType(NamespacedKey key, BannerPattern bannerPatternType) {
-        this.key = key;
-        this.bannerPatternType = bannerPatternType;
-        // For backwards compatibility, minecraft values will stile return the uppercase name without the namespace,
-        // in case plugins use for example the name as key in a config file to receive pattern type specific values.
-        // Custom pattern types will return the key with namespace. For a plugin this should look than like a new pattern type
-        // (which can always be added in new minecraft versions and the plugin should therefore handle it accordingly).
-        if (NamespacedKey.MINECRAFT.equals(key.getNamespace())) {
-            this.name = key.getKey().toUpperCase(Locale.ROOT);
-        } else {
-            this.name = key.toString();
-        }
-        this.ordinal = CraftPatternType.count++;
-    }
-
-    @Override
-    public BannerPattern getHandle() {
-        return this.bannerPatternType;
-    }
-
-    @Override
-    public NamespacedKey getKey() {
-        if (true) return java.util.Objects.requireNonNull(org.bukkit.Registry.BANNER_PATTERN.getKey(this), () -> this + " doesn't have a key"); // Paper
-        return this.key;
-    }
-
-    @Override
-    public int compareTo(PatternType patternType) {
-        return this.ordinal - patternType.ordinal();
-    }
-
-    @Override
-    public String name() {
-        return this.name;
-    }
-
-    @Override
-    public int ordinal() {
-        return this.ordinal;
-    }
-
-    @Override
-    public String toString() {
-        // For backwards compatibility
-        return this.name();
-    }
-
-    @Override
-    public boolean equals(Object other) {
-        if (this == other) {
-            return true;
-        }
-
-        if (!(other instanceof CraftPatternType)) {
-            return false;
-        }
-
-        return this.getKey().equals(((PatternType) other).getKey());
-    }
-
-    @Override
-    public int hashCode() {
-        return this.getKey().hashCode();
+    public CraftPatternType(Holder<BannerPattern> bannerPatternType) {
+       super(bannerPatternType, count++);
     }
 
     @Override
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/AbstractProjectile.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/AbstractProjectile.java
index 591af9d0d2..e8d82054d1 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/AbstractProjectile.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/AbstractProjectile.java
@@ -40,7 +40,7 @@ public abstract class AbstractProjectile extends CraftEntity implements Projecti
 
     @Override
     public boolean canHitEntity(org.bukkit.entity.Entity entity) {
-        return this.getHandle().canHitEntity(((CraftEntity) entity).getHandle());
+        return this.getHandle().canHitEntityPublic(((CraftEntity) entity).getHandle());
     }
 
     @Override
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractArrow.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractArrow.java
index d0c30fd12a..af2c1ad8cd 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractArrow.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractArrow.java
@@ -150,7 +150,7 @@ public class CraftAbstractArrow extends AbstractProjectile implements AbstractAr
     @Override
     public void setItemStack(final ItemStack stack) {
         Preconditions.checkArgument(stack != null, "ItemStack cannot be null");
-        this.getHandle().setPickupItemStack(CraftItemStack.asNMSCopy(stack));
+        this.getHandle().setPickupItemStackPublic(CraftItemStack.asNMSCopy(stack));
     }
 
     @Override
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderDragon.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderDragon.java
index 7b7b89e67d..1ef0ec7ed3 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderDragon.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderDragon.java
@@ -23,7 +23,7 @@ public class CraftEnderDragon extends CraftMob implements EnderDragon, CraftEnem
     public Set<ComplexEntityPart> getParts() {
         Builder<ComplexEntityPart> builder = ImmutableSet.builder();
 
-        for (EnderDragonPart part : this.getHandle().subEntities) {
+        for (EnderDragonPart part : this.getHandle().getSubEntities()) {
             builder.add((ComplexEntityPart) part.getBukkitEntity());
         }
 
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
index b25b10c24a..91b78711ec 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
@@ -193,8 +193,8 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
 
     @Override
     public boolean isOnGround() {
-        if (this.entity instanceof AbstractArrow) {
-            return ((AbstractArrow) this.entity).isInGround();
+        if (this.entity instanceof AbstractArrow abstractArrow) {
+            return abstractArrow.isInGround();
         }
         return this.entity.onGround();
     }
@@ -417,7 +417,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
 
     @Override
     public org.bukkit.entity.Entity getPassenger() {
-        return this.isEmpty() ? null : this.getHandle().passengers.get(0).getBukkitEntity();
+        return this.isEmpty() ? null : this.getHandle().getPassengers().getFirst().getBukkitEntity();
     }
 
     @Override
@@ -433,7 +433,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
 
     @Override
     public List<org.bukkit.entity.Entity> getPassengers() {
-        return Lists.newArrayList(Lists.transform(this.getHandle().passengers, (Function<Entity, org.bukkit.entity.Entity>) input -> input.getBukkitEntity()));
+        return Lists.newArrayList(Lists.transform(this.getHandle().getPassengers(), (Function<Entity, org.bukkit.entity.Entity>) Entity::getBukkitEntity));
     }
 
     @Override
@@ -852,12 +852,12 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
 
     @Override
     public int getPortalCooldown() {
-        return this.getHandle().portalCooldown;
+        return this.getHandle().getPortalCooldown();
     }
 
     @Override
     public void setPortalCooldown(int cooldown) {
-        this.getHandle().portalCooldown = cooldown;
+        this.getHandle().setPortalCooldown(cooldown);
     }
 
     @Override
@@ -1252,12 +1252,12 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
 
     // Paper start - missing entity api
     @Override
-    public boolean isInvisible() {  // Paper - moved up from LivingEntity
+    public boolean isInvisible() { // Paper - moved up from LivingEntity
         return this.getHandle().isInvisible();
     }
 
     @Override
-    public void setInvisible(boolean invisible) {  // Paper - moved up from LivingEntity
+    public void setInvisible(boolean invisible) { // Paper - moved up from LivingEntity
         this.getHandle().persistentInvisibility = invisible;
         this.getHandle().setSharedFlag(Entity.FLAG_INVISIBLE, invisible);
     }
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftItem.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftItem.java
index 30d62ee4d5..7a3d982b13 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftItem.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftItem.java
@@ -10,8 +10,8 @@ import org.bukkit.inventory.ItemStack;
 public class CraftItem extends CraftEntity implements Item {
 
     // Paper start
-    private final static int NO_AGE_TIME = (int) Short.MIN_VALUE;
-    private final static int NO_PICKUP_TIME = (int) Short.MAX_VALUE;
+    private final static int NO_AGE_TIME = Short.MIN_VALUE;
+    private final static int NO_PICKUP_TIME = Short.MAX_VALUE;
     // Paper end
 
     public CraftItem(CraftServer server, ItemEntity entity) {
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPhantom.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPhantom.java
index 83e77c6d28..429200b0b0 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPhantom.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPhantom.java
@@ -32,17 +32,17 @@ public class CraftPhantom extends CraftFlying implements Phantom, CraftEnemy {
     // Paper start
     @Override
     public java.util.UUID getSpawningEntity() {
-        return getHandle().getSpawningEntity();
+        return this.getHandle().spawningEntity;
     }
 
     @Override
     public boolean shouldBurnInDay() {
-        return getHandle().shouldBurnInDay();
+        return this.getHandle().shouldBurnInDay;
     }
 
     @Override
     public void setShouldBurnInDay(boolean shouldBurnInDay) {
-        getHandle().setShouldBurnInDay(shouldBurnInDay);
+        this.getHandle().shouldBurnInDay = shouldBurnInDay;
     }
 
     @Override
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPig.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPig.java
index 8016c810ae..fd4f13e8ea 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPig.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPig.java
@@ -45,7 +45,7 @@ public class CraftPig extends CraftAnimals implements Pig {
         }
 
         int max = this.getHandle().steering.boostTimeTotal();
-        Preconditions.checkArgument(ticks >= 0 && ticks <= max, "boost ticks must not exceed 0 or %d (inclusive)", max);
+        Preconditions.checkArgument(ticks >= 0 && ticks <= max, "boost ticks must not exceed 0 or %s (inclusive)", max);
 
         this.getHandle().steering.boostTime = ticks;
     }
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
index e9df37ff66..18100c2c7c 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
@@ -543,7 +543,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
 
     @Override
     public String getDisplayName() {
-        if(true) return io.papermc.paper.adventure.DisplayNames.getLegacy(this); // Paper
+        if (true) return io.papermc.paper.adventure.DisplayNames.getLegacy(this); // Paper
         return this.getHandle().displayName;
     }
 
@@ -3522,32 +3522,32 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
 
     @Override
     public int getViewDistance() {
-        return ca.spottedleaf.moonrise.common.util.ChunkSystem.getViewDistance(this.getHandle());
+        return ca.spottedleaf.moonrise.common.PlatformHooks.get().getViewDistance(this.getHandle());
     }
 
     @Override
     public void setViewDistance(final int viewDistance) {
-        throw new UnsupportedOperationException("Not implemented yet");
+        FeatureHooks.setViewDistance(this.getHandle(), viewDistance); // Paper - chunk system
     }
 
     @Override
     public int getSimulationDistance() {
-        return ca.spottedleaf.moonrise.common.util.ChunkSystem.getTickViewDistance(this.getHandle());
+        return ca.spottedleaf.moonrise.common.PlatformHooks.get().getTickViewDistance(this.getHandle());
     }
 
     @Override
     public void setSimulationDistance(final int simulationDistance) {
-        throw new UnsupportedOperationException("Not implemented yet");
+        FeatureHooks.setSimulationDistance(this.getHandle(), simulationDistance); // Paper - chunk system
     }
 
     @Override
     public int getSendViewDistance() {
-        return ca.spottedleaf.moonrise.common.util.ChunkSystem.getSendViewDistance(this.getHandle());
+        return ca.spottedleaf.moonrise.common.PlatformHooks.get().getSendViewDistance(this.getHandle());
     }
 
     @Override
     public void setSendViewDistance(final int viewDistance) {
-        throw new UnsupportedOperationException("Not implemented yet");
+        FeatureHooks.setSendViewDistance(this.getHandle(), viewDistance); // Paper - chunk system
     }
 
     // Paper start - entity effect API
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftStrider.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftStrider.java
index 9472a6f9c9..74fac97231 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftStrider.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftStrider.java
@@ -55,7 +55,7 @@ public class CraftStrider extends CraftAnimals implements Strider {
         }
 
         int max = this.getHandle().steering.boostTimeTotal();
-        Preconditions.checkArgument(ticks >= 0 && ticks <= max, "boost ticks must not exceed 0 or %d (inclusive)", max);
+        Preconditions.checkArgument(ticks >= 0 && ticks <= max, "boost ticks must not exceed 0 or %s (inclusive)", max);
 
         this.getHandle().steering.boostTime = ticks;
     }
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java b/paper-server/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java
index 39377ba073..6bf553bd44 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java
@@ -264,7 +264,7 @@ public class CustomChunkGenerator extends InternalChunkGenerator {
             return ichunkaccess1;
         };
 
-        return future == null ? CompletableFuture.supplyAsync(() -> function.apply(chunk), net.minecraft.Util.backgroundExecutor()) : future.thenApply(function);
+        return future == null ? CompletableFuture.supplyAsync(() -> function.apply(chunk), io.papermc.paper.FeatureHooks.getWorldgenExecutor()) : future.thenApply(function); // Paper - chunk system
     }
 
     @Override
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/generator/structure/CraftStructure.java b/paper-server/src/main/java/org/bukkit/craftbukkit/generator/structure/CraftStructure.java
index 7ef679fdd2..09fd006244 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/generator/structure/CraftStructure.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/generator/structure/CraftStructure.java
@@ -42,7 +42,6 @@ public class CraftStructure extends Structure implements Handleable<net.minecraf
 
     @Override
     public NamespacedKey getKey() {
-        if (true) return java.util.Objects.requireNonNull(org.bukkit.Registry.STRUCTURE.getKey(this), () -> this + " doesn't have a key"); // Paper
         return this.key;
     }
 }
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryCustom.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryCustom.java
index da1c1fe0fa..393e44cd3e 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryCustom.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftInventoryCustom.java
@@ -194,13 +194,13 @@ public class CraftInventoryCustom extends CraftInventory {
         }
 
         @Override
-        public void onOpen(CraftHumanEntity who) {
-            this.viewers.add(who);
+        public void onOpen(CraftHumanEntity player) {
+            this.viewers.add(player);
         }
 
         @Override
-        public void onClose(CraftHumanEntity who) {
-            this.viewers.remove(who);
+        public void onClose(CraftHumanEntity player) {
+            this.viewers.remove(player);
         }
 
         @Override
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java
index 01ea7cdedd..eb7d90cfa9 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java
@@ -2361,7 +2361,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
 
         for (Object object : addFrom) {
             // Paper start - support components
-            if(object instanceof net.md_5.bungee.api.chat.BaseComponent[] baseComponentArr) {
+            if (object instanceof net.md_5.bungee.api.chat.BaseComponent[] baseComponentArr) {
                 addTo.add(CraftChatMessage.fromJSON(net.md_5.bungee.chat.ComponentSerializer.toString(baseComponentArr)));
             } else
             // Paper end
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimMaterial.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimMaterial.java
index afee459c4d..9b7488cb45 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimMaterial.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimMaterial.java
@@ -1,6 +1,7 @@
 package org.bukkit.craftbukkit.inventory.trim;
 
 import com.google.common.base.Preconditions;
+import io.papermc.paper.util.Holderable;
 import net.minecraft.core.Holder;
 import net.minecraft.core.registries.Registries;
 import net.minecraft.network.chat.contents.TranslatableContents;
@@ -77,8 +78,7 @@ public class CraftTrimMaterial implements TrimMaterial, io.papermc.paper.util.Ho
     @Override
     @NotNull
     public NamespacedKey getKey() {
-        if (true) return java.util.Objects.requireNonNull(org.bukkit.Registry.TRIM_MATERIAL.getKey(this), () -> this + " doesn't have a key"); // Paper
-        return this.key;
+        return Holderable.super.getKey();
     }
 
     @NotNull
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimPattern.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimPattern.java
index da951bd7e4..b5b209d16a 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimPattern.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimPattern.java
@@ -1,6 +1,7 @@
 package org.bukkit.craftbukkit.inventory.trim;
 
 import com.google.common.base.Preconditions;
+import io.papermc.paper.util.Holderable;
 import net.minecraft.core.Holder;
 import net.minecraft.core.registries.Registries;
 import net.minecraft.network.chat.contents.TranslatableContents;
@@ -77,8 +78,7 @@ public class CraftTrimPattern implements TrimPattern, io.papermc.paper.util.Hold
     @Override
     @NotNull
     public NamespacedKey getKey() {
-        if (true) return java.util.Objects.requireNonNull(org.bukkit.Registry.TRIM_PATTERN.getKey(this), () -> this + " doesn't have a key"); // Paper
-        return this.key;
+        return Holderable.super.getKey();
     }
 
     @NotNull
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/map/RenderData.java b/paper-server/src/main/java/org/bukkit/craftbukkit/map/RenderData.java
index 503a31b4ef..782f5df380 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/map/RenderData.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/map/RenderData.java
@@ -1,16 +1,11 @@
 package org.bukkit.craftbukkit.map;
 
 import java.util.ArrayList;
+import java.util.List;
 import org.bukkit.map.MapCursor;
 
 public class RenderData {
 
-    public byte[] buffer;
-    public final ArrayList<MapCursor> cursors;
-
-    public RenderData() {
-        this.buffer = new byte[128 * 128];
-        this.cursors = new ArrayList<MapCursor>();
-    }
-
+    public final List<MapCursor> cursors = new ArrayList<>();
+    public byte[] buffer = new byte[128 * 128];
 }
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncTask.java b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncTask.java
index e97f6b76ef..e4e2e42d0c 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncTask.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncTask.java
@@ -28,7 +28,7 @@ class CraftAsyncTask extends CraftTask {
         // Paper start - name threads according to running plugin
         final String nameBefore = thread.getName();
         thread.setName(nameBefore + " - " + this.getOwner().getName());
-        try { synchronized (this.workers) {  // Paper end - name threads according to running plugin
+        try { synchronized (this.workers) { // Paper end - name threads according to running plugin
             if (this.getPeriod() == CraftTask.CANCEL) {
                 // Never continue running after cancelled.
                 // Checking this with the lock is important!
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/paper-server/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
index 0e8f4710c0..b6665e1875 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
@@ -523,7 +523,7 @@ public final class CraftMagicNumbers implements UnsafeValues {
 
         net.minecraft.nbt.CompoundTag compound = deserializeNbtFromBytes(data);
         final int dataVersion = compound.getInt("DataVersion");
-        compound = (net.minecraft.nbt.CompoundTag) MinecraftServer.getServer().fixerUpper.update(References.ITEM_STACK, new Dynamic<>(NbtOps.INSTANCE, compound), dataVersion, this.getDataVersion()).getValue();
+        compound = ca.spottedleaf.moonrise.common.PlatformHooks.get().convertNBT(References.ITEM_STACK, MinecraftServer.getServer().fixerUpper, compound, dataVersion, this.getDataVersion()); // Paper - possibly use dataconverter
         return CraftItemStack.asCraftMirror(net.minecraft.world.item.ItemStack.parse(MinecraftServer.getServer().registryAccess(), compound).orElseThrow());
     }
 
@@ -574,7 +574,7 @@ public final class CraftMagicNumbers implements UnsafeValues {
 
         net.minecraft.nbt.CompoundTag compound = deserializeNbtFromBytes(data);
         int dataVersion = compound.getInt("DataVersion");
-        compound = (net.minecraft.nbt.CompoundTag) MinecraftServer.getServer().fixerUpper.update(References.ENTITY, new Dynamic<>(NbtOps.INSTANCE, compound), dataVersion, this.getDataVersion()).getValue();
+        compound = ca.spottedleaf.moonrise.common.PlatformHooks.get().convertNBT(References.ENTITY, MinecraftServer.getServer().fixerUpper, compound, dataVersion, this.getDataVersion()); // Paper - possibly use dataconverter
         if (!preserveUUID) {
             // Generate a new UUID so we don't have to worry about deserializing the same entity twice
             compound.remove("UUID");
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java b/paper-server/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java
index c6e8441e29..e8e93538df 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java
+++ b/paper-server/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/paper-server/src/main/java/org/spigotmc/ActivationRange.java b/paper-server/src/main/java/org/spigotmc/ActivationRange.java
deleted file mode 100644
index 34297951d3..0000000000
--- a/paper-server/src/main/java/org/spigotmc/ActivationRange.java
+++ /dev/null
@@ -1,260 +0,0 @@
-package org.spigotmc;
-
-import net.minecraft.server.MinecraftServer;
-import net.minecraft.world.entity.Entity;
-import net.minecraft.world.entity.ExperienceOrb;
-import net.minecraft.world.entity.LightningBolt;
-import net.minecraft.world.entity.LivingEntity;
-import net.minecraft.world.entity.PathfinderMob;
-import net.minecraft.world.entity.ambient.AmbientCreature;
-import net.minecraft.world.entity.animal.Animal;
-import net.minecraft.world.entity.animal.Sheep;
-import net.minecraft.world.entity.boss.EnderDragonPart;
-import net.minecraft.world.entity.boss.enderdragon.EndCrystal;
-import net.minecraft.world.entity.boss.enderdragon.EnderDragon;
-import net.minecraft.world.entity.boss.wither.WitherBoss;
-import net.minecraft.world.entity.item.ItemEntity;
-import net.minecraft.world.entity.item.PrimedTnt;
-import net.minecraft.world.entity.monster.Creeper;
-import net.minecraft.world.entity.monster.Monster;
-import net.minecraft.world.entity.monster.Slime;
-import net.minecraft.world.entity.npc.Villager;
-import net.minecraft.world.entity.player.Player;
-import net.minecraft.world.entity.projectile.AbstractArrow;
-import net.minecraft.world.entity.projectile.AbstractHurtingProjectile;
-import net.minecraft.world.entity.projectile.FireworkRocketEntity;
-import net.minecraft.world.entity.projectile.ThrowableProjectile;
-import net.minecraft.world.entity.projectile.ThrownTrident;
-import net.minecraft.world.entity.raid.Raider;
-import net.minecraft.world.level.Level;
-import net.minecraft.world.phys.AABB;
-
-public class ActivationRange
-{
-
-    public enum ActivationType
-    {
-        WATER, // Paper
-        FLYING_MONSTER, // Paper
-        VILLAGER, // Paper
-        MONSTER,
-        ANIMAL,
-        RAIDER,
-        MISC;
-
-        AABB boundingBox = new AABB( 0, 0, 0, 0, 0, 0 );
-    }
-
-    static AABB maxBB = new AABB( 0, 0, 0, 0, 0, 0 );
-
-    /**
-     * Initializes an entities type on construction to specify what group this
-     * entity is in for activation ranges.
-     *
-     * @param entity
-     * @return group id
-     */
-    public static ActivationType initializeEntityActivationType(Entity entity)
-    {
-        if ( entity instanceof Raider )
-        {
-            return ActivationType.RAIDER;
-        } else if ( entity instanceof Monster || entity instanceof Slime )
-        {
-            return ActivationType.MONSTER;
-        } else if ( entity instanceof PathfinderMob || entity instanceof AmbientCreature )
-        {
-            return ActivationType.ANIMAL;
-        } else
-        {
-            return ActivationType.MISC;
-        }
-    }
-
-    /**
-     * These entities are excluded from Activation range checks.
-     *
-     * @param entity
-     * @param config
-     * @return boolean If it should always tick.
-     */
-    public static boolean initializeEntityActivationState(Entity entity, SpigotWorldConfig config)
-    {
-        if ( ( entity.activationType == ActivationType.MISC && config.miscActivationRange == 0 )
-                || ( entity.activationType == ActivationType.RAIDER && config.raiderActivationRange == 0 )
-                || ( entity.activationType == ActivationType.ANIMAL && config.animalActivationRange == 0 )
-                || ( entity.activationType == ActivationType.MONSTER && config.monsterActivationRange == 0 )
-                || entity instanceof Player
-                || entity instanceof ThrowableProjectile
-                || entity instanceof EnderDragon
-                || entity instanceof EnderDragonPart
-                || entity instanceof WitherBoss
-                || entity instanceof AbstractHurtingProjectile
-                || entity instanceof LightningBolt
-                || entity instanceof PrimedTnt
-                || entity instanceof net.minecraft.world.entity.item.FallingBlockEntity // Paper - Always tick falling blocks
-                || entity instanceof net.minecraft.world.entity.vehicle.AbstractMinecart // Paper
-                || entity instanceof net.minecraft.world.entity.vehicle.AbstractBoat // Paper
-                || entity instanceof EndCrystal
-                || entity instanceof FireworkRocketEntity
-                || entity instanceof ThrownTrident )
-        {
-            return true;
-        }
-
-        return false;
-    }
-
-    /**
-     * Find what entities are in range of the players in the world and set
-     * active if in range.
-     *
-     * @param world
-     */
-    public static void activateEntities(Level world)
-    {
-        final int miscActivationRange = world.spigotConfig.miscActivationRange;
-        final int raiderActivationRange = world.spigotConfig.raiderActivationRange;
-        final int animalActivationRange = world.spigotConfig.animalActivationRange;
-        final int monsterActivationRange = world.spigotConfig.monsterActivationRange;
-
-        int maxRange = Math.max( monsterActivationRange, animalActivationRange );
-        maxRange = Math.max( maxRange, raiderActivationRange );
-        maxRange = Math.max( maxRange, miscActivationRange );
-        maxRange = Math.min( ( world.spigotConfig.simulationDistance << 4 ) - 8, maxRange );
-
-        for ( Player player : world.players() )
-        {
-            player.activatedTick = MinecraftServer.currentTick;
-            if ( world.spigotConfig.ignoreSpectatorActivation && player.isSpectator() )
-            {
-                continue;
-            }
-
-            ActivationRange.maxBB = player.getBoundingBox().inflate( maxRange, 256, maxRange );
-            ActivationType.MISC.boundingBox = player.getBoundingBox().inflate( miscActivationRange, 256, miscActivationRange );
-            ActivationType.RAIDER.boundingBox = player.getBoundingBox().inflate( raiderActivationRange, 256, raiderActivationRange );
-            ActivationType.ANIMAL.boundingBox = player.getBoundingBox().inflate( animalActivationRange, 256, animalActivationRange );
-            ActivationType.MONSTER.boundingBox = player.getBoundingBox().inflate( monsterActivationRange, 256, monsterActivationRange );
-
-            world.getEntities().get(ActivationRange.maxBB, ActivationRange::activateEntity);
-        }
-    }
-
-    /**
-     * Checks for the activation state of all entities in this chunk.
-     *
-     * @param chunk
-     */
-    private static void activateEntity(Entity entity)
-    {
-        if ( MinecraftServer.currentTick > entity.activatedTick )
-        {
-            if ( entity.defaultActivationState )
-            {
-                entity.activatedTick = MinecraftServer.currentTick;
-                return;
-            }
-            if ( entity.activationType.boundingBox.intersects( entity.getBoundingBox() ) )
-            {
-                entity.activatedTick = MinecraftServer.currentTick;
-            }
-        }
-    }
-
-    /**
-     * If an entity is not in range, do some more checks to see if we should
-     * give it a shot.
-     *
-     * @param entity
-     * @return
-     */
-    public static boolean checkEntityImmunities(Entity entity)
-    {
-        // quick checks.
-        if ( entity.wasTouchingWater || entity.getRemainingFireTicks() > 0 )
-        {
-            return true;
-        }
-        if ( !( entity instanceof AbstractArrow ) )
-        {
-            if ( !entity.onGround() || !entity.passengers.isEmpty() || entity.isPassenger() )
-            {
-                return true;
-            }
-        } else if ( !( (AbstractArrow) entity ).isInGround() )
-        {
-            return true;
-        }
-        // special cases.
-        if ( entity instanceof LivingEntity )
-        {
-            LivingEntity living = (LivingEntity) entity;
-            if ( /*TODO: Missed mapping? living.attackTicks > 0 || */ living.hurtTime > 0 || living.activeEffects.size() > 0 )
-            {
-                return true;
-            }
-            if ( entity instanceof PathfinderMob && ( (PathfinderMob) entity ).getTarget() != null )
-            {
-                return true;
-            }
-            if ( entity instanceof Villager && ( (Villager) entity ).canBreed() )
-            {
-                return true;
-            }
-            if ( entity instanceof Animal )
-            {
-                Animal animal = (Animal) entity;
-                if ( animal.isBaby() || animal.isInLove() )
-                {
-                    return true;
-                }
-                if ( entity instanceof Sheep && ( (Sheep) entity ).isSheared() )
-                {
-                    return true;
-                }
-            }
-            if (entity instanceof Creeper && ((Creeper) entity).isIgnited()) { // isExplosive
-                return true;
-            }
-        }
-        // SPIGOT-6644: Otherwise the target refresh tick will be missed
-        if (entity instanceof ExperienceOrb) {
-            return true;
-        }
-        return false;
-    }
-
-    /**
-     * Checks if the entity is active for this tick.
-     *
-     * @param entity
-     * @return
-     */
-    public static boolean checkIfActive(Entity entity)
-    {
-        // Never safe to skip fireworks or item gravity
-        if (entity instanceof FireworkRocketEntity || (entity instanceof ItemEntity && (entity.tickCount + entity.getId()) % 4 == 0)) { // Paper - Needed for item gravity, see ItemEntity tick
-            return true;
-        }
-
-        boolean isActive = entity.activatedTick >= MinecraftServer.currentTick || entity.defaultActivationState;
-
-        // Should this entity tick?
-        if ( !isActive )
-        {
-            if ( ( MinecraftServer.currentTick - entity.activatedTick - 1 ) % 20 == 0 )
-            {
-                // Check immunities every 20 ticks.
-                if ( ActivationRange.checkEntityImmunities( entity ) )
-                {
-                    // Triggered some sort of immunity, give 20 full ticks before we check again.
-                    entity.activatedTick = MinecraftServer.currentTick + 20;
-                }
-                isActive = true;
-            }
-        }
-        // Paper - remove dumb tick skipping for active entities
-        return isActive;
-    }
-}
diff --git a/paper-server/src/main/java/org/spigotmc/AsyncCatcher.java b/paper-server/src/main/java/org/spigotmc/AsyncCatcher.java
index ef25987604..50f01faa88 100644
--- a/paper-server/src/main/java/org/spigotmc/AsyncCatcher.java
+++ b/paper-server/src/main/java/org/spigotmc/AsyncCatcher.java
@@ -2,17 +2,14 @@ package org.spigotmc;
 
 import net.minecraft.server.MinecraftServer;
 
-public class AsyncCatcher
-{
+public class AsyncCatcher {
 
     public static boolean enabled = true;
 
-    public static void catchOp(String reason)
-    {
-        if ( AsyncCatcher.enabled && Thread.currentThread() != MinecraftServer.getServer().serverThread )
-        {
-            MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); // Paper
-            throw new IllegalStateException( "Asynchronous " + reason + "!" );
+    public static void catchOp(String reason) {
+        if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThread()) { // Paper - chunk system
+            MinecraftServer.LOGGER.error("Thread {} failed main thread check: {}", Thread.currentThread().getName(), reason, new Throwable()); // Paper
+            throw new IllegalStateException("Asynchronous " + reason + "!");
         }
     }
 }
diff --git a/paper-server/src/main/java/org/spigotmc/LimitStream.java b/paper-server/src/main/java/org/spigotmc/LimitStream.java
index 8de241a921..60e9ec374e 100644
--- a/paper-server/src/main/java/org/spigotmc/LimitStream.java
+++ b/paper-server/src/main/java/org/spigotmc/LimitStream.java
@@ -5,35 +5,30 @@ import java.io.IOException;
 import java.io.InputStream;
 import net.minecraft.nbt.NbtAccounter;
 
-public class LimitStream extends FilterInputStream
-{
+public class LimitStream extends FilterInputStream {
 
     private final NbtAccounter limit;
 
-    public LimitStream(InputStream is, NbtAccounter limit)
-    {
-        super( is );
+    public LimitStream(InputStream is, NbtAccounter limit) {
+        super(is);
         this.limit = limit;
     }
 
     @Override
-    public int read() throws IOException
-    {
-        this.limit.accountBytes( 1 );
+    public int read() throws IOException {
+        this.limit.accountBytes(1);
         return super.read();
     }
 
     @Override
-    public int read(byte[] b) throws IOException
-    {
-        this.limit.accountBytes( b.length );
-        return super.read( b );
+    public int read(byte[] b) throws IOException {
+        this.limit.accountBytes(b.length);
+        return super.read(b);
     }
 
     @Override
-    public int read(byte[] b, int off, int len) throws IOException
-    {
-        this.limit.accountBytes( len );
-        return super.read( b, off, len );
+    public int read(byte[] b, int off, int len) throws IOException {
+        this.limit.accountBytes(len);
+        return super.read(b, off, len);
     }
 }
diff --git a/paper-server/src/main/java/org/spigotmc/Metrics.java b/paper-server/src/main/java/org/spigotmc/Metrics.java
deleted file mode 100644
index 0abb93bd40..0000000000
--- a/paper-server/src/main/java/org/spigotmc/Metrics.java
+++ /dev/null
@@ -1,641 +0,0 @@
-/*
- * Copyright 2011-2013 Tyler Blair. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without modification, are
- * permitted provided that the following conditions are met:
- *
- *    1. Redistributions of source code must retain the above copyright notice, this list of
- *       conditions and the following disclaimer.
- *
- *    2. Redistributions in binary form must reproduce the above copyright notice, this list
- *       of conditions and the following disclaimer in the documentation and/or other materials
- *       provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
- * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR
- * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
- * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
- * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- * The views and conclusions contained in the software and documentation are those of the
- * authors and contributors and should not be interpreted as representing official policies,
- * either expressed or implied, of anybody else.
- */
-package org.spigotmc;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.OutputStreamWriter;
-import java.io.UnsupportedEncodingException;
-import java.net.Proxy;
-import java.net.URL;
-import java.net.URLConnection;
-import java.net.URLEncoder;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedHashSet;
-import java.util.Set;
-import java.util.Timer;
-import java.util.TimerTask;
-import java.util.UUID;
-import java.util.concurrent.TimeUnit;
-import java.util.logging.Level;
-import net.minecraft.server.MinecraftServer;
-import org.bukkit.Bukkit;
-import org.bukkit.configuration.InvalidConfigurationException;
-import org.bukkit.configuration.file.YamlConfiguration;
-
-/**
- * <p> The metrics class obtains data about a plugin and submits statistics about it to the metrics backend. </p> <p>
- * Public methods provided by this class: </p>
- * <code>
- * Graph createGraph(String name); <br/>
- * void addCustomData(BukkitMetrics.Plotter plotter); <br/>
- * void start(); <br/>
- * </code>
- */
-public class Metrics {
-
-    /**
-     * The current revision number
-     */
-    private static final int REVISION = 6;
-    /**
-     * The base url of the metrics domain
-     */
-    private static final String BASE_URL = "https://mcstats.spigotmc.org";
-    /**
-     * The url used to report a server's status
-     */
-    private static final String REPORT_URL = "/report/%s";
-    /**
-     * The separator to use for custom data. This MUST NOT change unless you are hosting your own version of metrics and
-     * want to change it.
-     */
-    private static final String CUSTOM_DATA_SEPARATOR = "~~";
-    /**
-     * Interval of time to ping (in minutes)
-     */
-    private static final int PING_INTERVAL = 10;
-    /**
-     * All of the custom graphs to submit to metrics
-     */
-    private final Set<Graph> graphs = Collections.synchronizedSet(new HashSet<Graph>());
-    /**
-     * The default graph, used for addCustomData when you don't want a specific graph
-     */
-    private final Graph defaultGraph = new Graph("Default");
-    /**
-     * The plugin configuration file
-     */
-    private final YamlConfiguration configuration;
-    /**
-     * The plugin configuration file
-     */
-    private final File configurationFile;
-    /**
-     * Unique server id
-     */
-    private final String guid;
-    /**
-     * Debug mode
-     */
-    private final boolean debug;
-    /**
-     * Lock for synchronization
-     */
-    private final Object optOutLock = new Object();
-    /**
-     * The scheduled task
-     */
-    private volatile Timer task = null;
-
-    public Metrics() throws IOException {
-        // load the config
-        this.configurationFile = this.getConfigFile();
-        this.configuration = YamlConfiguration.loadConfiguration(this.configurationFile);
-
-        // add some defaults
-        this.configuration.addDefault("opt-out", false);
-        this.configuration.addDefault("guid", UUID.randomUUID().toString());
-        this.configuration.addDefault("debug", false);
-
-        // Do we need to create the file?
-        if (this.configuration.get("guid", null) == null) {
-            this.configuration.options().header("http://mcstats.org").copyDefaults(true);
-            this.configuration.save(this.configurationFile);
-        }
-
-        // Load the guid then
-        this.guid = this.configuration.getString("guid");
-        this.debug = this.configuration.getBoolean("debug", false);
-    }
-
-    /**
-     * Construct and create a Graph that can be used to separate specific plotters to their own graphs on the metrics
-     * website. Plotters can be added to the graph object returned.
-     *
-     * @param name The name of the graph
-     * @return Graph object created. Will never return NULL under normal circumstances unless bad parameters are given
-     */
-    public Graph createGraph(final String name) {
-        if (name == null) {
-            throw new IllegalArgumentException("Graph name cannot be null");
-        }
-
-        // Construct the graph object
-        final Graph graph = new Graph(name);
-
-        // Now we can add our graph
-        this.graphs.add(graph);
-
-        // and return back
-        return graph;
-    }
-
-    /**
-     * Add a Graph object to BukkitMetrics that represents data for the plugin that should be sent to the backend
-     *
-     * @param graph The name of the graph
-     */
-    public void addGraph(final Graph graph) {
-        if (graph == null) {
-            throw new IllegalArgumentException("Graph cannot be null");
-        }
-
-        this.graphs.add(graph);
-    }
-
-    /**
-     * Adds a custom data plotter to the default graph
-     *
-     * @param plotter The plotter to use to plot custom data
-     */
-    public void addCustomData(final Plotter plotter) {
-        if (plotter == null) {
-            throw new IllegalArgumentException("Plotter cannot be null");
-        }
-
-        // Add the plotter to the graph o/
-        this.defaultGraph.addPlotter(plotter);
-
-        // Ensure the default graph is included in the submitted graphs
-        this.graphs.add(this.defaultGraph);
-    }
-
-    /**
-     * Start measuring statistics. This will immediately create an async repeating task as the plugin and send the
-     * initial data to the metrics backend, and then after that it will post in increments of PING_INTERVAL * 1200
-     * ticks.
-     *
-     * @return True if statistics measuring is running, otherwise false.
-     */
-    public boolean start() {
-        synchronized (this.optOutLock) {
-            // Did we opt out?
-            if (this.isOptOut()) {
-                return false;
-            }
-
-            // Is metrics already running?
-            if (this.task != null) {
-                return true;
-            }
-
-            // Begin hitting the server with glorious data
-            this.task = new Timer("Spigot Metrics Thread", true);
-
-            this.task.scheduleAtFixedRate(new TimerTask() {
-                private boolean firstPost = true;
-
-                public void run() {
-                    try {
-                        // This has to be synchronized or it can collide with the disable method.
-                        synchronized (Metrics.this.optOutLock) {
-                            // Disable Task, if it is running and the server owner decided to opt-out
-                            if (Metrics.this.isOptOut() && Metrics.this.task != null) {
-                                Metrics.this.task.cancel();
-                                Metrics.this.task = null;
-                                // Tell all plotters to stop gathering information.
-                                for (Graph graph : Metrics.this.graphs) {
-                                    graph.onOptOut();
-                                }
-                            }
-                        }
-
-                        // We use the inverse of firstPost because if it is the first time we are posting,
-                        // it is not a interval ping, so it evaluates to FALSE
-                        // Each time thereafter it will evaluate to TRUE, i.e PING!
-                        Metrics.this.postPlugin(!this.firstPost);
-
-                        // After the first post we set firstPost to false
-                        // Each post thereafter will be a ping
-                        this.firstPost = false;
-                    } catch (IOException e) {
-                        if (Metrics.this.debug) {
-                            Bukkit.getLogger().log(Level.INFO, "[Metrics] " + e.getMessage());
-                        }
-                    }
-                }
-            }, 0, TimeUnit.MINUTES.toMillis(Metrics.PING_INTERVAL));
-
-            return true;
-        }
-    }
-
-    /**
-     * Has the server owner denied plugin metrics?
-     *
-     * @return true if metrics should be opted out of it
-     */
-    public boolean isOptOut() {
-        synchronized (this.optOutLock) {
-            try {
-                // Reload the metrics file
-                this.configuration.load(this.getConfigFile());
-            } catch (IOException ex) {
-                if (this.debug) {
-                    Bukkit.getLogger().log(Level.INFO, "[Metrics] " + ex.getMessage());
-                }
-                return true;
-            } catch (InvalidConfigurationException ex) {
-                if (this.debug) {
-                    Bukkit.getLogger().log(Level.INFO, "[Metrics] " + ex.getMessage());
-                }
-                return true;
-            }
-            return this.configuration.getBoolean("opt-out", false);
-        }
-    }
-
-    /**
-     * Enables metrics for the server by setting "opt-out" to false in the config file and starting the metrics task.
-     *
-     * @throws java.io.IOException
-     */
-    public void enable() throws IOException {
-        // This has to be synchronized or it can collide with the check in the task.
-        synchronized (this.optOutLock) {
-            // Check if the server owner has already set opt-out, if not, set it.
-            if (this.isOptOut()) {
-                this.configuration.set("opt-out", false);
-                this.configuration.save(this.configurationFile);
-            }
-
-            // Enable Task, if it is not running
-            if (this.task == null) {
-                this.start();
-            }
-        }
-    }
-
-    /**
-     * Disables metrics for the server by setting "opt-out" to true in the config file and canceling the metrics task.
-     *
-     * @throws java.io.IOException
-     */
-    public void disable() throws IOException {
-        // This has to be synchronized or it can collide with the check in the task.
-        synchronized (this.optOutLock) {
-            // Check if the server owner has already set opt-out, if not, set it.
-            if (!this.isOptOut()) {
-                this.configuration.set("opt-out", true);
-                this.configuration.save(this.configurationFile);
-            }
-
-            // Disable Task, if it is running
-            if (this.task != null) {
-                this.task.cancel();
-                this.task = null;
-            }
-        }
-    }
-
-    /**
-     * Gets the File object of the config file that should be used to store data such as the GUID and opt-out status
-     *
-     * @return the File object for the config file
-     */
-    public File getConfigFile() {
-        // I believe the easiest way to get the base folder (e.g craftbukkit set via -P) for plugins to use
-        // is to abuse the plugin object we already have
-        // plugin.getDataFolder() => base/plugins/PluginA/
-        // pluginsFolder => base/plugins/
-        // The base is not necessarily relative to the startup directory.
-        // File pluginsFolder = plugin.getDataFolder().getParentFile();
-
-        // return => base/plugins/PluginMetrics/config.yml
-        return new File(new File((File) MinecraftServer.getServer().options.valueOf("plugins"), "PluginMetrics"), "config.yml");
-    }
-
-    /**
-     * Generic method that posts a plugin to the metrics website
-     */
-    private void postPlugin(final boolean isPing) throws IOException {
-        // Server software specific section
-        String pluginName = "Spigot";
-        boolean onlineMode = Bukkit.getServer().getOnlineMode(); // TRUE if online mode is enabled
-        String pluginVersion = (Metrics.class.getPackage().getImplementationVersion() != null) ? Metrics.class.getPackage().getImplementationVersion() : "unknown";
-        String serverVersion = Bukkit.getVersion();
-        int playersOnline = Bukkit.getServer().getOnlinePlayers().size();
-
-        // END server software specific section -- all code below does not use any code outside of this class / Java
-
-        // Construct the post data
-        final StringBuilder data = new StringBuilder();
-
-        // The plugin's description file containg all of the plugin data such as name, version, author, etc
-        data.append(Metrics.encode("guid")).append('=').append(Metrics.encode(this.guid));
-        Metrics.encodeDataPair(data, "version", pluginVersion);
-        Metrics.encodeDataPair(data, "server", serverVersion);
-        Metrics.encodeDataPair(data, "players", Integer.toString(playersOnline));
-        Metrics.encodeDataPair(data, "revision", String.valueOf(Metrics.REVISION));
-
-        // New data as of R6
-        String osname = System.getProperty("os.name");
-        String osarch = System.getProperty("os.arch");
-        String osversion = System.getProperty("os.version");
-        String java_version = System.getProperty("java.version");
-        int coreCount = Runtime.getRuntime().availableProcessors();
-
-        // normalize os arch .. amd64 -> x86_64
-        if (osarch.equals("amd64")) {
-            osarch = "x86_64";
-        }
-
-        Metrics.encodeDataPair(data, "osname", osname);
-        Metrics.encodeDataPair(data, "osarch", osarch);
-        Metrics.encodeDataPair(data, "osversion", osversion);
-        Metrics.encodeDataPair(data, "cores", Integer.toString(coreCount));
-        Metrics.encodeDataPair(data, "online-mode", Boolean.toString(onlineMode));
-        Metrics.encodeDataPair(data, "java_version", java_version);
-
-        // If we're pinging, append it
-        if (isPing) {
-            Metrics.encodeDataPair(data, "ping", "true");
-        }
-
-        // Acquire a lock on the graphs, which lets us make the assumption we also lock everything
-        // inside of the graph (e.g plotters)
-        synchronized (this.graphs) {
-            final Iterator<Graph> iter = this.graphs.iterator();
-
-            while (iter.hasNext()) {
-                final Graph graph = iter.next();
-
-                for (Plotter plotter : graph.getPlotters()) {
-                    // The key name to send to the metrics server
-                    // The format is C-GRAPHNAME-PLOTTERNAME where separator - is defined at the top
-                    // Legacy (R4) submitters use the format Custom%s, or CustomPLOTTERNAME
-                    final String key = String.format("C%s%s%s%s", Metrics.CUSTOM_DATA_SEPARATOR, graph.getName(), Metrics.CUSTOM_DATA_SEPARATOR, plotter.getColumnName());
-
-                    // The value to send, which for the foreseeable future is just the string
-                    // value of plotter.getValue()
-                    final String value = Integer.toString(plotter.getValue());
-
-                    // Add it to the http post data :)
-                    Metrics.encodeDataPair(data, key, value);
-                }
-            }
-        }
-
-        // Create the url
-        URL url = new URL(Metrics.BASE_URL + String.format(Metrics.REPORT_URL, Metrics.encode(pluginName)));
-
-        // Connect to the website
-        URLConnection connection;
-
-        // Mineshafter creates a socks proxy, so we can safely bypass it
-        // It does not reroute POST requests so we need to go around it
-        if (this.isMineshafterPresent()) {
-            connection = url.openConnection(Proxy.NO_PROXY);
-        } else {
-            connection = url.openConnection();
-        }
-
-        connection.setDoOutput(true);
-
-        // Write the data
-        final OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream());
-        writer.write(data.toString());
-        writer.flush();
-
-        // Now read the response
-        final BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
-        final String response = reader.readLine();
-
-        // close resources
-        writer.close();
-        reader.close();
-
-        if (response == null || response.startsWith("ERR")) {
-            throw new IOException(response); //Throw the exception
-        } else {
-            // Is this the first update this hour?
-            if (response.contains("OK This is your first update this hour")) {
-                synchronized (this.graphs) {
-                    final Iterator<Graph> iter = this.graphs.iterator();
-
-                    while (iter.hasNext()) {
-                        final Graph graph = iter.next();
-
-                        for (Plotter plotter : graph.getPlotters()) {
-                            plotter.reset();
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    /**
-     * Check if mineshafter is present. If it is, we need to bypass it to send POST requests
-     *
-     * @return true if mineshafter is installed on the server
-     */
-    private boolean isMineshafterPresent() {
-        try {
-            Class.forName("mineshafter.MineServer");
-            return true;
-        } catch (Exception e) {
-            return false;
-        }
-    }
-
-    /**
-     * <p>Encode a key/value data pair to be used in a HTTP post request. This INCLUDES a & so the first key/value pair
-     * MUST be included manually, e.g:</p>
-     * <code>
-     * StringBuffer data = new StringBuffer();
-     * data.append(encode("guid")).append('=').append(encode(guid));
-     * encodeDataPair(data, "version", description.getVersion());
-     * </code>
-     *
-     * @param buffer the stringbuilder to append the data pair onto
-     * @param key the key value
-     * @param value the value
-     */
-    private static void encodeDataPair(final StringBuilder buffer, final String key, final String value) throws UnsupportedEncodingException {
-        buffer.append('&').append(Metrics.encode(key)).append('=').append(Metrics.encode(value));
-    }
-
-    /**
-     * Encode text as UTF-8
-     *
-     * @param text the text to encode
-     * @return the encoded text, as UTF-8
-     */
-    private static String encode(final String text) throws UnsupportedEncodingException {
-        return URLEncoder.encode(text, "UTF-8");
-    }
-
-    /**
-     * Represents a custom graph on the website
-     */
-    public static class Graph {
-
-        /**
-         * The graph's name, alphanumeric and spaces only :) If it does not comply to the above when submitted, it is
-         * rejected
-         */
-        private final String name;
-        /**
-         * The set of plotters that are contained within this graph
-         */
-        private final Set<Plotter> plotters = new LinkedHashSet<Plotter>();
-
-        private Graph(final String name) {
-            this.name = name;
-        }
-
-        /**
-         * Gets the graph's name
-         *
-         * @return the Graph's name
-         */
-        public String getName() {
-            return this.name;
-        }
-
-        /**
-         * Add a plotter to the graph, which will be used to plot entries
-         *
-         * @param plotter the plotter to add to the graph
-         */
-        public void addPlotter(final Plotter plotter) {
-            this.plotters.add(plotter);
-        }
-
-        /**
-         * Remove a plotter from the graph
-         *
-         * @param plotter the plotter to remove from the graph
-         */
-        public void removePlotter(final Plotter plotter) {
-            this.plotters.remove(plotter);
-        }
-
-        /**
-         * Gets an <b>unmodifiable</b> set of the plotter objects in the graph
-         *
-         * @return an unmodifiable {@link java.util.Set} of the plotter objects
-         */
-        public Set<Plotter> getPlotters() {
-            return Collections.unmodifiableSet(this.plotters);
-        }
-
-        @Override
-        public int hashCode() {
-            return this.name.hashCode();
-        }
-
-        @Override
-        public boolean equals(final Object object) {
-            if (!(object instanceof Graph)) {
-                return false;
-            }
-
-            final Graph graph = (Graph) object;
-            return graph.name.equals(this.name);
-        }
-
-        /**
-         * Called when the server owner decides to opt-out of BukkitMetrics while the server is running.
-         */
-        protected void onOptOut() {
-        }
-    }
-
-    /**
-     * Interface used to collect custom data for a plugin
-     */
-    public abstract static class Plotter {
-
-        /**
-         * The plot's name
-         */
-        private final String name;
-
-        /**
-         * Construct a plotter with the default plot name
-         */
-        public Plotter() {
-            this("Default");
-        }
-
-        /**
-         * Construct a plotter with a specific plot name
-         *
-         * @param name the name of the plotter to use, which will show up on the website
-         */
-        public Plotter(final String name) {
-            this.name = name;
-        }
-
-        /**
-         * Get the current value for the plotted point. Since this function defers to an external function it may or may
-         * not return immediately thus cannot be guaranteed to be thread friendly or safe. This function can be called
-         * from any thread so care should be taken when accessing resources that need to be synchronized.
-         *
-         * @return the current value for the point to be plotted.
-         */
-        public abstract int getValue();
-
-        /**
-         * Get the column name for the plotted point
-         *
-         * @return the plotted point's column name
-         */
-        public String getColumnName() {
-            return this.name;
-        }
-
-        /**
-         * Called after the website graphs have been updated
-         */
-        public void reset() {
-        }
-
-        @Override
-        public int hashCode() {
-            return this.getColumnName().hashCode();
-        }
-
-        @Override
-        public boolean equals(final Object object) {
-            if (!(object instanceof Plotter)) {
-                return false;
-            }
-
-            final Plotter plotter = (Plotter) object;
-            return plotter.name.equals(this.name) && plotter.getValue() == this.getValue();
-        }
-    }
-}
diff --git a/paper-server/src/main/java/org/spigotmc/RestartCommand.java b/paper-server/src/main/java/org/spigotmc/RestartCommand.java
index 39e56b95aa..3287d39951 100644
--- a/paper-server/src/main/java/org/spigotmc/RestartCommand.java
+++ b/paper-server/src/main/java/org/spigotmc/RestartCommand.java
@@ -1,115 +1,85 @@
 package org.spigotmc;
 
 import java.io.File;
-import java.util.List;
+import java.util.Locale;
 import net.minecraft.server.MinecraftServer;
 import net.minecraft.server.level.ServerPlayer;
 import org.bukkit.command.Command;
 import org.bukkit.command.CommandSender;
 import org.bukkit.craftbukkit.util.CraftChatMessage;
 
-public class RestartCommand extends Command
-{
+public class RestartCommand extends Command {
 
-    public RestartCommand(String name)
-    {
-        super( name );
+    public RestartCommand(String name) {
+        super(name);
         this.description = "Restarts the server";
         this.usageMessage = "/restart";
-        this.setPermission( "bukkit.command.restart" );
+        this.setPermission("bukkit.command.restart");
     }
 
     @Override
-    public boolean execute(CommandSender sender, String currentAlias, String[] args)
-    {
-        if ( this.testPermission( sender ) )
-        {
-            MinecraftServer.getServer().processQueue.add( new Runnable()
-            {
-                @Override
-                public void run()
-                {
-                    RestartCommand.restart();
-                }
-            } );
+    public boolean execute(CommandSender sender, String currentAlias, String[] args) {
+        if (this.testPermission(sender)) {
+            MinecraftServer.getServer().processQueue.add(RestartCommand::restart);
         }
         return true;
     }
 
-    public static void restart()
-    {
-        RestartCommand.restart( SpigotConfig.restartScript );
+    public static void restart() {
+        RestartCommand.restart(SpigotConfig.restartScript);
     }
 
-    private static void restart(final String restartScript)
-    {
-        AsyncCatcher.enabled = false; // Disable async catcher incase it interferes with us
-        try
-        {
+    private static void restart(final String restartScript) {
+        AsyncCatcher.enabled = false; // Disable async catcher in case it interferes with us
+        try {
             // Paper - extract method and cleanup
-            boolean isRestarting = addShutdownHook( restartScript );
-            if ( isRestarting )
-            {
-                System.out.println( "Attempting to restart with " + SpigotConfig.restartScript );
-            } else
-            {
-                System.out.println( "Startup script '" + SpigotConfig.restartScript + "' does not exist! Stopping server." );
+            boolean isRestarting = addShutdownHook(restartScript);
+            if (isRestarting) {
+                System.out.println("Attempting to restart with " + SpigotConfig.restartScript);
+            } else {
+                System.out.println("Startup script '" + SpigotConfig.restartScript + "' does not exist! Stopping server.");
             }
             // Stop the watchdog
             WatchdogThread.doStop();
 
-            shutdownServer( isRestarting );
+            shutdownServer(isRestarting);
             // Paper end
-        } catch ( Exception ex )
-        {
+        } catch (Exception ex) {
             ex.printStackTrace();
         }
     }
 
     // Paper start - sync copied from above with minor changes, async added
-    private static void shutdownServer(boolean isRestarting)
-    {
-        if ( MinecraftServer.getServer().isSameThread() )
-        {
+    private static void shutdownServer(boolean isRestarting) {
+        if (MinecraftServer.getServer().isSameThread()) {
             // Kick all players
-            for ( ServerPlayer p : com.google.common.collect.ImmutableList.copyOf( MinecraftServer.getServer().getPlayerList().players ) )
-            {
-                p.connection.disconnect( CraftChatMessage.fromStringOrEmpty( SpigotConfig.restartMessage, true ), org.bukkit.event.player.PlayerKickEvent.Cause.RESTART_COMMAND); // Paper - kick event reason (cause is never used))
+            for (ServerPlayer p : com.google.common.collect.ImmutableList.copyOf(MinecraftServer.getServer().getPlayerList().players)) {
+                p.connection.disconnect(CraftChatMessage.fromStringOrEmpty(SpigotConfig.restartMessage, true), org.bukkit.event.player.PlayerKickEvent.Cause.RESTART_COMMAND); // Paper - kick event reason (cause is never used)
             }
             // Give the socket a chance to send the packets
-            try
-            {
-                Thread.sleep( 100 );
-            } catch ( InterruptedException ex )
-            {
+            try {
+                Thread.sleep(100);
+            } catch (InterruptedException ex) {
             }
 
             closeSocket();
 
             // Actually shutdown
-            try
-            {
+            try {
                 MinecraftServer.getServer().close(); // calls stop()
-            } catch ( Throwable t )
-            {
+            } catch (Throwable t) {
             }
 
             // Actually stop the JVM
-            System.exit( 0 );
-
-        } else
-        {
+            System.exit(0);
+        } else {
             // Mark the server to shutdown at the end of the tick
-            MinecraftServer.getServer().safeShutdown( false, isRestarting );
+            MinecraftServer.getServer().safeShutdown(false, isRestarting);
 
             // wait 10 seconds to see if we're actually going to try shutdown
-            try
-            {
-                Thread.sleep( 10000 );
-            }
-            catch (InterruptedException ignored)
-            {
-            }
+            try {
+                Thread.sleep(10000);
+            } catch (InterruptedException ignored) {}
 
             // Check if we've actually hit a state where the server is going to safely shutdown
             // if we have, let the server stop as usual
@@ -117,63 +87,46 @@ public class RestartCommand extends Command
 
             // If the server hasn't stopped by now, assume worse case and kill
             closeSocket();
-            System.exit( 0 );
+            System.exit(0);
         }
     }
     // Paper end
 
     // Paper - Split from moved code
-    private static void closeSocket()
-    {
+    private static void closeSocket() {
         // Close the socket so we can rebind with the new process
         MinecraftServer.getServer().getConnection().stop();
 
         // Give time for it to kick in
-        try
-        {
-            Thread.sleep( 100 );
-        } catch ( InterruptedException ex )
-        {
-        }
+        try {
+            Thread.sleep(100);
+        } catch (InterruptedException ignored) {}
     }
     // Paper end
 
     // Paper start - copied from above and modified to return if the hook registered
-    private static boolean addShutdownHook(String restartScript)
-    {
-        String[] split = restartScript.split( " " );
-        if ( split.length > 0 && new File( split[0] ).isFile() )
-        {
-            Thread shutdownHook = new Thread()
-            {
-                @Override
-                public void run()
-                {
-                    try
-                    {
-                        String os = System.getProperty( "os.name" ).toLowerCase(java.util.Locale.ENGLISH);
-                        if ( os.contains( "win" ) )
-                        {
-                            Runtime.getRuntime().exec( "cmd /c start " + restartScript );
-                        } else
-                        {
-                            Runtime.getRuntime().exec( "sh " + restartScript );
-                        }
-                    } catch ( Exception e )
-                    {
-                        e.printStackTrace();
+    public static boolean addShutdownHook(String restartScript) {
+        String[] split = restartScript.split(" ");
+        if (split.length > 0 && new File(split[0]).isFile()) {
+            Thread shutdownHook = new Thread(() -> {
+                try {
+                    String os = System.getProperty("os.name").toLowerCase(Locale.ENGLISH);
+                    if (os.contains("win")) {
+                        Runtime.getRuntime().exec("cmd /c start " + restartScript);
+                    } else {
+                        Runtime.getRuntime().exec("sh " + restartScript);
                     }
+                } catch (Exception e) {
+                    e.printStackTrace();
                 }
-            };
+            });
 
-            shutdownHook.setDaemon( true );
-            Runtime.getRuntime().addShutdownHook( shutdownHook );
+            shutdownHook.setDaemon(true);
+            Runtime.getRuntime().addShutdownHook(shutdownHook);
             return true;
-        } else
-        {
+        } else {
             return false;
         }
     }
     // Paper end
-
 }
diff --git a/paper-server/src/main/java/org/spigotmc/SpigotCommand.java b/paper-server/src/main/java/org/spigotmc/SpigotCommand.java
index ac0fd418fc..1b60abf5f5 100644
--- a/paper-server/src/main/java/org/spigotmc/SpigotCommand.java
+++ b/paper-server/src/main/java/org/spigotmc/SpigotCommand.java
@@ -1,12 +1,14 @@
 package org.spigotmc;
 
 import java.io.File;
+import net.kyori.adventure.text.format.NamedTextColor;
 import net.minecraft.server.MinecraftServer;
 import net.minecraft.server.level.ServerLevel;
-import org.bukkit.ChatColor;
 import org.bukkit.command.Command;
 import org.bukkit.command.CommandSender;
 
+import static net.kyori.adventure.text.Component.text;
+
 public class SpigotCommand extends Command {
 
     public SpigotCommand(String name) {
@@ -21,13 +23,17 @@ public class SpigotCommand extends Command {
         if (!this.testPermission(sender)) return true;
 
         if (args.length != 1) {
-            sender.sendMessage(ChatColor.RED + "Usage: " + this.usageMessage);
+            sender.sendMessage(text("Usage: " + this.usageMessage, NamedTextColor.RED));
             return false;
         }
 
         if (args[0].equals("reload")) {
-            Command.broadcastCommandMessage(sender, ChatColor.RED + "Please note that this command is not supported and may cause issues.");
-            Command.broadcastCommandMessage(sender, ChatColor.RED + "If you encounter any issues please use the /stop command to restart your server.");
+            Command.broadcastCommandMessage(sender, text().color(NamedTextColor.RED)
+                .append(text("Please note that this command is not supported and may cause issues."))
+                .appendNewline()
+                .append(text("If you encounter any issues please use the /stop command to restart your server."))
+                .build()
+            );
 
             MinecraftServer console = MinecraftServer.getServer();
             org.spigotmc.SpigotConfig.init((File) console.options.valueOf("spigot-settings"));
@@ -36,7 +42,7 @@ public class SpigotCommand extends Command {
             }
             console.server.reloadCount++;
 
-            Command.broadcastCommandMessage(sender, ChatColor.GREEN + "Reload complete.");
+            Command.broadcastCommandMessage(sender, text("Reload complete.", NamedTextColor.GREEN));
         }
 
         return true;
diff --git a/paper-server/src/main/java/org/spigotmc/SpigotConfig.java b/paper-server/src/main/java/org/spigotmc/SpigotConfig.java
index 4dbb109d05..e0d4222a99 100644
--- a/paper-server/src/main/java/org/spigotmc/SpigotConfig.java
+++ b/paper-server/src/main/java/org/spigotmc/SpigotConfig.java
@@ -28,360 +28,289 @@ import org.bukkit.configuration.ConfigurationSection;
 import org.bukkit.configuration.InvalidConfigurationException;
 import org.bukkit.configuration.file.YamlConfiguration;
 
-public class SpigotConfig
-{
+public class SpigotConfig {
 
     private static File CONFIG_FILE;
-    private static final String HEADER = "This is the main configuration file for Spigot.\n"
-            + "As you can see, there's tons to configure. Some options may impact gameplay, so use\n"
-            + "with caution, and make sure you know what each option does before configuring.\n"
-            + "For a reference for any variable inside this file, check out the Spigot wiki at\n"
-            + "http://www.spigotmc.org/wiki/spigot-configuration/\n"
-            + "\n"
-            + "If you need help with the configuration or have any questions related to Spigot,\n"
-            + "join us at the Discord or drop by our forums and leave a post.\n"
-            + "\n"
-            + "Discord: https://www.spigotmc.org/go/discord\n"
-            + "Forums: http://www.spigotmc.org/\n";
+    private static final String HEADER = """
+        This is the main configuration file for Spigot.
+        As you can see, there's tons to configure. Some options may impact gameplay, so use
+        with caution, and make sure you know what each option does before configuring.
+        For a reference for any variable inside this file, check out the Spigot wiki at
+        http://www.spigotmc.org/wiki/spigot-configuration/
+        
+        If you need help with the configuration or have any questions related to Spigot,
+        join us at the Discord or drop by our forums and leave a post.
+        
+        Discord: https://www.spigotmc.org/go/discord
+        Forums: http://www.spigotmc.org/
+        """;
     /*========================================================================*/
     public static YamlConfiguration config;
     static int version;
     static Map<String, Command> commands;
     /*========================================================================*/
-    private static Metrics metrics;
 
-    public static void init(File configFile)
-    {
+    public static void init(File configFile) {
         SpigotConfig.CONFIG_FILE = configFile;
         SpigotConfig.config = new YamlConfiguration();
-        try
-        {
-            SpigotConfig.config.load( SpigotConfig.CONFIG_FILE );
-        } catch ( IOException ex )
-        {
-        } catch ( InvalidConfigurationException ex )
-        {
-            Bukkit.getLogger().log( Level.SEVERE, "Could not load spigot.yml, please correct your syntax errors", ex );
-            throw Throwables.propagate( ex );
+        try {
+            SpigotConfig.config.load(SpigotConfig.CONFIG_FILE);
+        } catch (IOException ignored) {
+        } catch (InvalidConfigurationException ex) {
+            Bukkit.getLogger().log(Level.SEVERE, "Could not load spigot.yml, please correct your syntax errors", ex);
+            throw Throwables.propagate(ex);
         }
 
-        SpigotConfig.config.options().header( SpigotConfig.HEADER );
-        SpigotConfig.config.options().copyDefaults( true );
+        SpigotConfig.config.options().header(SpigotConfig.HEADER);
+        SpigotConfig.config.options().copyDefaults(true);
 
-        SpigotConfig.commands = new HashMap<String, Command>();
-        SpigotConfig.commands.put( "spigot", new SpigotCommand( "spigot" ) );
+        SpigotConfig.commands = new HashMap<>();
+        SpigotConfig.commands.put("spigot", new SpigotCommand("spigot"));
 
-        SpigotConfig.version = SpigotConfig.getInt( "config-version", 12 );
-        SpigotConfig.set( "config-version", 12 );
-        SpigotConfig.readConfig( SpigotConfig.class, null );
+        SpigotConfig.version = SpigotConfig.getInt("config-version", 12);
+        SpigotConfig.set("config-version", 12);
+        SpigotConfig.readConfig(SpigotConfig.class, null);
     }
 
-    public static void registerCommands()
-    {
-        for ( Map.Entry<String, Command> entry : SpigotConfig.commands.entrySet() )
-        {
-            MinecraftServer.getServer().server.getCommandMap().register( entry.getKey(), "Spigot", entry.getValue() );
+    public static void registerCommands() {
+        for (Map.Entry<String, Command> entry : SpigotConfig.commands.entrySet()) {
+            MinecraftServer.getServer().server.getCommandMap().register(entry.getKey(), "Spigot", entry.getValue());
         }
-
-        /* // Paper - Replace with our own
-        if ( SpigotConfig.metrics == null )
-        {
-            try
-            {
-                SpigotConfig.metrics = new Metrics();
-                SpigotConfig.metrics.start();
-            } catch ( IOException ex )
-            {
-                Bukkit.getServer().getLogger().log( Level.SEVERE, "Could not start metrics service", ex );
-            }
-        }
-        */ // Paper end
     }
 
-    public static void readConfig(Class<?> clazz, Object instance) // Paper - package-private -> public
-    {
-        for ( Method method : clazz.getDeclaredMethods() )
-        {
-            if ( Modifier.isPrivate( method.getModifiers() ) )
-            {
-                if ( method.getParameterTypes().length == 0 && method.getReturnType() == Void.TYPE )
-                {
-                    try
-                    {
-                        method.setAccessible( true );
-                        method.invoke( instance );
-                    } catch ( InvocationTargetException ex )
-                    {
-                        throw Throwables.propagate( ex.getCause() );
-                    } catch ( Exception ex )
-                    {
-                        Bukkit.getLogger().log( Level.SEVERE, "Error invoking " + method, ex );
+    public static void readConfig(Class<?> clazz, Object instance) { // Paper - package-private -> public
+        for (Method method : clazz.getDeclaredMethods()) {
+            if (Modifier.isPrivate(method.getModifiers())) {
+                if (method.getParameterTypes().length == 0 && method.getReturnType() == Void.TYPE) {
+                    try {
+                        method.setAccessible(true);
+                        method.invoke(instance);
+                    } catch (InvocationTargetException ex) {
+                        throw Throwables.propagate(ex.getCause());
+                    } catch (Exception ex) {
+                        Bukkit.getLogger().log(Level.SEVERE, "Error invoking " + method, ex);
                     }
                 }
             }
         }
 
-        try
-        {
-            SpigotConfig.config.save( SpigotConfig.CONFIG_FILE );
-        } catch ( IOException ex )
-        {
-            Bukkit.getLogger().log( Level.SEVERE, "Could not save " + SpigotConfig.CONFIG_FILE, ex );
+        try {
+            SpigotConfig.config.save(SpigotConfig.CONFIG_FILE);
+        } catch (IOException ex) {
+            Bukkit.getLogger().log(Level.SEVERE, "Could not save " + SpigotConfig.CONFIG_FILE, ex);
         }
     }
 
-    private static void set(String path, Object val)
-    {
-        SpigotConfig.config.set( path, val );
+    private static void set(String path, Object val) {
+        SpigotConfig.config.set(path, val);
     }
 
-    private static boolean getBoolean(String path, boolean def)
-    {
-        SpigotConfig.config.addDefault( path, def );
-        return SpigotConfig.config.getBoolean( path, SpigotConfig.config.getBoolean( path ) );
+    private static boolean getBoolean(String path, boolean def) {
+        SpigotConfig.config.addDefault(path, def);
+        return SpigotConfig.config.getBoolean(path, SpigotConfig.config.getBoolean(path));
     }
 
-    private static int getInt(String path, int def)
-    {
-        SpigotConfig.config.addDefault( path, def );
-        return SpigotConfig.config.getInt( path, SpigotConfig.config.getInt( path ) );
+    private static int getInt(String path, int def) {
+        SpigotConfig.config.addDefault(path, def);
+        return SpigotConfig.config.getInt(path, SpigotConfig.config.getInt(path));
     }
 
-    private static <T> List getList(String path, T def)
-    {
-        SpigotConfig.config.addDefault( path, def );
-        return (List<T>) SpigotConfig.config.getList( path, SpigotConfig.config.getList( path ) );
+    private static <T> List getList(String path, T def) {
+        SpigotConfig.config.addDefault(path, def);
+        return (List<T>) SpigotConfig.config.getList(path, SpigotConfig.config.getList(path));
     }
 
-    private static String getString(String path, String def)
-    {
-        SpigotConfig.config.addDefault( path, def );
-        return SpigotConfig.config.getString( path, SpigotConfig.config.getString( path ) );
+    private static String getString(String path, String def) {
+        SpigotConfig.config.addDefault(path, def);
+        return SpigotConfig.config.getString(path, SpigotConfig.config.getString(path));
     }
 
-    private static double getDouble(String path, double def)
-    {
-        SpigotConfig.config.addDefault( path, def );
-        return SpigotConfig.config.getDouble( path, SpigotConfig.config.getDouble( path ) );
+    private static double getDouble(String path, double def) {
+        SpigotConfig.config.addDefault(path, def);
+        return SpigotConfig.config.getDouble(path, SpigotConfig.config.getDouble(path));
     }
 
     public static boolean logCommands;
-    private static void logCommands()
-    {
-        SpigotConfig.logCommands = SpigotConfig.getBoolean( "commands.log", true );
+    private static void logCommands() {
+        SpigotConfig.logCommands = SpigotConfig.getBoolean("commands.log", true);
     }
 
     public static int tabComplete;
     public static boolean sendNamespaced;
-    private static void tabComplete()
-    {
-        if ( SpigotConfig.version < 6 )
-        {
-            boolean oldValue = SpigotConfig.getBoolean( "commands.tab-complete", true );
-            if ( oldValue )
-            {
-                SpigotConfig.set( "commands.tab-complete", 0 );
-            } else
-            {
-                SpigotConfig.set( "commands.tab-complete", -1 );
+    private static void tabComplete() {
+        if (SpigotConfig.version < 6) {
+            boolean oldValue = SpigotConfig.getBoolean("commands.tab-complete", true);
+            if (oldValue) {
+                SpigotConfig.set("commands.tab-complete", 0);
+            } else {
+                SpigotConfig.set("commands.tab-complete", -1);
             }
         }
-        SpigotConfig.tabComplete = SpigotConfig.getInt( "commands.tab-complete", 0 );
-        SpigotConfig.sendNamespaced = SpigotConfig.getBoolean( "commands.send-namespaced", true );
+        SpigotConfig.tabComplete = SpigotConfig.getInt("commands.tab-complete", 0);
+        SpigotConfig.sendNamespaced = SpigotConfig.getBoolean("commands.send-namespaced", true);
     }
 
     public static String whitelistMessage;
     public static String unknownCommandMessage;
     public static String serverFullMessage;
     public static String outdatedClientMessage = "Outdated client! Please use {0}";
-    public static String outdatedServerMessage = "Outdated server! I\'m still on {0}";
-    private static String transform(String s)
-    {
-        return ChatColor.translateAlternateColorCodes( '&', s ).replaceAll( "\\\\n", "\n" );
+    public static String outdatedServerMessage = "Outdated server! I'm still on {0}";
+
+    private static String transform(String s) {
+        return ChatColor.translateAlternateColorCodes('&', s).replaceAll("\\\\n", "\n");
     }
-    private static void messages()
-    {
-        if (SpigotConfig.version < 8)
-        {
-            SpigotConfig.set( "messages.outdated-client", SpigotConfig.outdatedClientMessage );
-            SpigotConfig.set( "messages.outdated-server", SpigotConfig.outdatedServerMessage );
+
+    private static void messages() {
+        if (SpigotConfig.version < 8) {
+            SpigotConfig.set("messages.outdated-client", SpigotConfig.outdatedClientMessage);
+            SpigotConfig.set("messages.outdated-server", SpigotConfig.outdatedServerMessage);
         }
 
-        SpigotConfig.whitelistMessage = SpigotConfig.transform( SpigotConfig.getString( "messages.whitelist", "You are not whitelisted on this server!" ) );
-        SpigotConfig.unknownCommandMessage = SpigotConfig.transform( SpigotConfig.getString( "messages.unknown-command", "Unknown command. Type \"/help\" for help." ) );
-        SpigotConfig.serverFullMessage = SpigotConfig.transform( SpigotConfig.getString( "messages.server-full", "The server is full!" ) );
-        SpigotConfig.outdatedClientMessage = SpigotConfig.transform( SpigotConfig.getString( "messages.outdated-client", SpigotConfig.outdatedClientMessage ) );
-        SpigotConfig.outdatedServerMessage = SpigotConfig.transform( SpigotConfig.getString( "messages.outdated-server", SpigotConfig.outdatedServerMessage ) );
+        SpigotConfig.whitelistMessage = SpigotConfig.transform(SpigotConfig.getString("messages.whitelist", "You are not whitelisted on this server!"));
+        SpigotConfig.unknownCommandMessage = SpigotConfig.transform(SpigotConfig.getString("messages.unknown-command", "Unknown command. Type \"/help\" for help."));
+        SpigotConfig.serverFullMessage = SpigotConfig.transform(SpigotConfig.getString("messages.server-full", "The server is full!"));
+        SpigotConfig.outdatedClientMessage = SpigotConfig.transform(SpigotConfig.getString("messages.outdated-client", SpigotConfig.outdatedClientMessage));
+        SpigotConfig.outdatedServerMessage = SpigotConfig.transform(SpigotConfig.getString("messages.outdated-server", SpigotConfig.outdatedServerMessage));
     }
 
     public static int timeoutTime = 60;
     public static boolean restartOnCrash = true;
     public static String restartScript = "./start.sh";
     public static String restartMessage;
-    private static void watchdog()
-    {
-        SpigotConfig.timeoutTime = SpigotConfig.getInt( "settings.timeout-time", SpigotConfig.timeoutTime );
-        SpigotConfig.restartOnCrash = SpigotConfig.getBoolean( "settings.restart-on-crash", SpigotConfig.restartOnCrash );
-        SpigotConfig.restartScript = SpigotConfig.getString( "settings.restart-script", SpigotConfig.restartScript );
-        SpigotConfig.restartMessage = SpigotConfig.transform( SpigotConfig.getString( "messages.restart", "Server is restarting" ) );
-        SpigotConfig.commands.put( "restart", new RestartCommand( "restart" ) );
-        // WatchdogThread.doStart( SpigotConfig.timeoutTime, SpigotConfig.restartOnCrash ); // Paper - moved to after paper config initialization
+    private static void watchdog() {
+        SpigotConfig.timeoutTime = SpigotConfig.getInt("settings.timeout-time", SpigotConfig.timeoutTime);
+        SpigotConfig.restartOnCrash = SpigotConfig.getBoolean("settings.restart-on-crash", SpigotConfig.restartOnCrash);
+        SpigotConfig.restartScript = SpigotConfig.getString("settings.restart-script", SpigotConfig.restartScript);
+        SpigotConfig.restartMessage = SpigotConfig.transform(SpigotConfig.getString("messages.restart", "Server is restarting"));
+        SpigotConfig.commands.put("restart", new RestartCommand("restart"));
     }
 
     public static boolean bungee;
     private static void bungee() {
-        if ( SpigotConfig.version < 4 )
-        {
-            SpigotConfig.set( "settings.bungeecord", false );
-            System.out.println( "Outdated config, disabling BungeeCord support!" );
+        if (SpigotConfig.version < 4) {
+            SpigotConfig.set("settings.bungeecord", false);
+            System.out.println("Outdated config, disabling BungeeCord support!");
         }
-        SpigotConfig.bungee = SpigotConfig.getBoolean( "settings.bungeecord", false );
+        SpigotConfig.bungee = SpigotConfig.getBoolean("settings.bungeecord", false);
     }
 
-    private static void nettyThreads()
-    {
-        int count = SpigotConfig.getInt( "settings.netty-threads", 4 );
-        System.setProperty( "io.netty.eventLoopThreads", Integer.toString( count ) );
-        Bukkit.getLogger().log( Level.INFO, "Using {0} threads for Netty based IO", count );
+    private static void nettyThreads() {
+        int count = SpigotConfig.getInt("settings.netty-threads", 4);
+        System.setProperty("io.netty.eventLoopThreads", Integer.toString(count));
+        Bukkit.getLogger().log(Level.INFO, "Using {0} threads for Netty based IO", count);
     }
 
     public static boolean disableStatSaving;
     public static Map<ResourceLocation, Integer> forcedStats = new HashMap<>();
-    private static void stats()
-    {
-        SpigotConfig.disableStatSaving = SpigotConfig.getBoolean( "stats.disable-saving", false );
 
-        if ( !SpigotConfig.config.contains( "stats.forced-stats" ) ) {
-            SpigotConfig.config.createSection( "stats.forced-stats" );
+    private static void stats() {
+        SpigotConfig.disableStatSaving = SpigotConfig.getBoolean("stats.disable-saving", false);
+
+        if (!SpigotConfig.config.contains("stats.forced-stats")) {
+            SpigotConfig.config.createSection("stats.forced-stats");
         }
 
-        ConfigurationSection section = SpigotConfig.config.getConfigurationSection( "stats.forced-stats" );
-        for ( String name : section.getKeys( true ) )
-        {
-            if ( section.isInt( name ) )
-            {
-                try
-                {
-                    ResourceLocation key = ResourceLocation.parse( name );
-                    if ( BuiltInRegistries.CUSTOM_STAT.get( key ) == null )
-                    {
+        ConfigurationSection section = SpigotConfig.config.getConfigurationSection("stats.forced-stats");
+        for (String name : section.getKeys(true)) {
+            if (section.isInt(name)) {
+                try {
+                    ResourceLocation key = ResourceLocation.parse(name);
+                    if (BuiltInRegistries.CUSTOM_STAT.get(key) == null) {
                         Bukkit.getLogger().log(Level.WARNING, "Ignoring non existent stats.forced-stats " + name);
                         continue;
                     }
-                    SpigotConfig.forcedStats.put( key, section.getInt( name ) );
-                } catch (Exception ex)
-                {
+                    SpigotConfig.forcedStats.put(key, section.getInt(name));
+                } catch (Exception ex) {
                     Bukkit.getLogger().log(Level.WARNING, "Ignoring invalid stats.forced-stats " + name);
                 }
             }
         }
     }
 
-    private static void tpsCommand()
-    {
-        SpigotConfig.commands.put( "tps", new TicksPerSecondCommand( "tps" ) );
+    private static void tpsCommand() {
+        SpigotConfig.commands.put("tps", new TicksPerSecondCommand("tps"));
     }
 
     public static int playerSample;
-    private static void playerSample()
-    {
-        SpigotConfig.playerSample = Math.max( SpigotConfig.getInt( "settings.sample-count", 12 ), 0 ); // Paper - Avoid negative counts
-        Bukkit.getLogger().log( Level.INFO, "Server Ping Player Sample Count: {0}", playerSample ); // Paper - Use logger
+    private static void playerSample() {
+        SpigotConfig.playerSample = Math.max(SpigotConfig.getInt("settings.sample-count", 12), 0); // Paper - Avoid negative counts
+        Bukkit.getLogger().log(Level.INFO, "Server Ping Player Sample Count: {0}", playerSample); // Paper - Use logger
     }
 
     public static int playerShuffle;
-    private static void playerShuffle()
-    {
-        SpigotConfig.playerShuffle = SpigotConfig.getInt( "settings.player-shuffle", 0 );
+    private static void playerShuffle() {
+        SpigotConfig.playerShuffle = SpigotConfig.getInt("settings.player-shuffle", 0);
     }
 
     public static List<String> spamExclusions;
-    private static void spamExclusions()
-    {
-        SpigotConfig.spamExclusions = SpigotConfig.getList( "commands.spam-exclusions", Arrays.asList( new String[]
-        {
-                "/skill"
-        } ) );
+    private static void spamExclusions() {
+        SpigotConfig.spamExclusions = SpigotConfig.getList("commands.spam-exclusions", List.of("/skill"));
     }
 
     public static boolean silentCommandBlocks;
-    private static void silentCommandBlocks()
-    {
-        SpigotConfig.silentCommandBlocks = SpigotConfig.getBoolean( "commands.silent-commandblock-console", false );
+    private static void silentCommandBlocks() {
+        SpigotConfig.silentCommandBlocks = SpigotConfig.getBoolean("commands.silent-commandblock-console", false);
     }
 
     public static Set<String> replaceCommands;
-    private static void replaceCommands()
-    {
-        if ( SpigotConfig.config.contains( "replace-commands" ) )
-        {
-            SpigotConfig.set( "commands.replace-commands", SpigotConfig.config.getStringList( "replace-commands" ) );
-            SpigotConfig.config.set( "replace-commands", null );
+    private static void replaceCommands() {
+        if (SpigotConfig.config.contains("replace-commands")) {
+            SpigotConfig.set("commands.replace-commands", SpigotConfig.config.getStringList("replace-commands"));
+            SpigotConfig.config.set("replace-commands", null);
         }
-        SpigotConfig.replaceCommands = new HashSet<String>( (List<String>) SpigotConfig.getList( "commands.replace-commands",
-                Arrays.asList( "setblock", "summon", "testforblock", "tellraw" ) ) );
+        SpigotConfig.replaceCommands = new HashSet<>(SpigotConfig.getList("commands.replace-commands",
+            Arrays.asList("setblock", "summon", "testforblock", "tellraw")));
     }
 
     public static int userCacheCap;
-    private static void userCacheCap()
-    {
-        SpigotConfig.userCacheCap = SpigotConfig.getInt( "settings.user-cache-size", 1000 );
+    private static void userCacheCap() {
+        SpigotConfig.userCacheCap = SpigotConfig.getInt("settings.user-cache-size", 1000);
     }
 
     public static boolean saveUserCacheOnStopOnly;
-    private static void saveUserCacheOnStopOnly()
-    {
-        SpigotConfig.saveUserCacheOnStopOnly = SpigotConfig.getBoolean( "settings.save-user-cache-on-stop-only", false );
+    private static void saveUserCacheOnStopOnly() {
+        SpigotConfig.saveUserCacheOnStopOnly = SpigotConfig.getBoolean("settings.save-user-cache-on-stop-only", false);
     }
 
     public static double movedWronglyThreshold;
-    private static void movedWronglyThreshold()
-    {
-        SpigotConfig.movedWronglyThreshold = SpigotConfig.getDouble( "settings.moved-wrongly-threshold", 0.0625D );
+    private static void movedWronglyThreshold() {
+        SpigotConfig.movedWronglyThreshold = SpigotConfig.getDouble("settings.moved-wrongly-threshold", 0.0625D);
     }
 
     public static double movedTooQuicklyMultiplier;
-    private static void movedTooQuicklyMultiplier()
-    {
-        SpigotConfig.movedTooQuicklyMultiplier = SpigotConfig.getDouble( "settings.moved-too-quickly-multiplier", 10.0D );
+    private static void movedTooQuicklyMultiplier() {
+        SpigotConfig.movedTooQuicklyMultiplier = SpigotConfig.getDouble("settings.moved-too-quickly-multiplier", 10.0D);
     }
 
     public static double maxAbsorption = 2048;
-    public static double maxHealth = 2048;
-    public static double movementSpeed = 2048;
+    public static double maxHealth = 1024;
+    public static double movementSpeed = 1024;
     public static double attackDamage = 2048;
-    private static void attributeMaxes()
-    {
-        SpigotConfig.maxAbsorption = SpigotConfig.getDouble( "settings.attribute.maxAbsorption.max", SpigotConfig.maxAbsorption );
-        ( (RangedAttribute) Attributes.MAX_ABSORPTION.value() ).maxValue = SpigotConfig.maxAbsorption;
-        SpigotConfig.maxHealth = SpigotConfig.getDouble( "settings.attribute.maxHealth.max", SpigotConfig.maxHealth );
-        ( (RangedAttribute) Attributes.MAX_HEALTH.value() ).maxValue = SpigotConfig.maxHealth;
-        SpigotConfig.movementSpeed = SpigotConfig.getDouble( "settings.attribute.movementSpeed.max", SpigotConfig.movementSpeed );
-        ( (RangedAttribute) Attributes.MOVEMENT_SPEED.value() ).maxValue = SpigotConfig.movementSpeed;
-        SpigotConfig.attackDamage = SpigotConfig.getDouble( "settings.attribute.attackDamage.max", SpigotConfig.attackDamage );
-        ( (RangedAttribute) Attributes.ATTACK_DAMAGE.value() ).maxValue = SpigotConfig.attackDamage;
+    private static void attributeMaxes() {
+        SpigotConfig.maxAbsorption = SpigotConfig.getDouble("settings.attribute.maxAbsorption.max", SpigotConfig.maxAbsorption);
+        ((RangedAttribute) Attributes.MAX_ABSORPTION.value()).maxValue = SpigotConfig.maxAbsorption;
+        SpigotConfig.maxHealth = SpigotConfig.getDouble("settings.attribute.maxHealth.max", SpigotConfig.maxHealth);
+        ((RangedAttribute) Attributes.MAX_HEALTH.value()).maxValue = SpigotConfig.maxHealth;
+        SpigotConfig.movementSpeed = SpigotConfig.getDouble("settings.attribute.movementSpeed.max", SpigotConfig.movementSpeed);
+        ((RangedAttribute) Attributes.MOVEMENT_SPEED.value()).maxValue = SpigotConfig.movementSpeed;
+        SpigotConfig.attackDamage = SpigotConfig.getDouble("settings.attribute.attackDamage.max", SpigotConfig.attackDamage);
+        ((RangedAttribute) Attributes.ATTACK_DAMAGE.value()).maxValue = SpigotConfig.attackDamage;
     }
 
     public static boolean debug;
-    private static void debug()
-    {
-        SpigotConfig.debug = SpigotConfig.getBoolean( "settings.debug", false );
+    private static void debug() {
+        SpigotConfig.debug = SpigotConfig.getBoolean("settings.debug", false);
 
-        if ( SpigotConfig.debug && !LogManager.getRootLogger().isTraceEnabled() )
-        {
+        if (SpigotConfig.debug && !LogManager.getRootLogger().isTraceEnabled()) {
             // Enable debug logging
-            LoggerContext ctx = (LoggerContext) LogManager.getContext( false );
+            LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
             Configuration conf = ctx.getConfiguration();
-            conf.getLoggerConfig( LogManager.ROOT_LOGGER_NAME ).setLevel( org.apache.logging.log4j.Level.ALL );
-            ctx.updateLoggers( conf );
+            conf.getLoggerConfig(LogManager.ROOT_LOGGER_NAME).setLevel(org.apache.logging.log4j.Level.ALL);
+            ctx.updateLoggers(conf);
         }
 
-        if ( LogManager.getRootLogger().isTraceEnabled() )
-        {
-            Bukkit.getLogger().info( "Debug logging is enabled" );
-        } else
-        {
-            // Bukkit.getLogger().info( "Debug logging is disabled" ); // Paper - Don't log if debug logging isn't enabled.
+        if (LogManager.getRootLogger().isTraceEnabled()) {
+            Bukkit.getLogger().info("Debug logging is enabled");
         }
     }
 
@@ -389,7 +318,7 @@ public class SpigotConfig
     public static List<String> disabledAdvancements;
     private static void disabledAdvancements() {
         SpigotConfig.disableAdvancementSaving = SpigotConfig.getBoolean("advancements.disable-saving", false);
-        SpigotConfig.disabledAdvancements = SpigotConfig.getList("advancements.disabled", Arrays.asList(new String[]{"minecraft:story/disabled"}));
+        SpigotConfig.disabledAdvancements = SpigotConfig.getList("advancements.disabled", List.of("minecraft:story/disabled"));
     }
 
     public static boolean logVillagerDeaths;
diff --git a/paper-server/src/main/java/org/spigotmc/SpigotWorldConfig.java b/paper-server/src/main/java/org/spigotmc/SpigotWorldConfig.java
index 2b26324613..89e2adbc1e 100644
--- a/paper-server/src/main/java/org/spigotmc/SpigotWorldConfig.java
+++ b/paper-server/src/main/java/org/spigotmc/SpigotWorldConfig.java
@@ -4,86 +4,73 @@ import java.util.List;
 import org.bukkit.Bukkit;
 import org.bukkit.configuration.file.YamlConfiguration;
 
-public class SpigotWorldConfig
-{
+public class SpigotWorldConfig {
 
     private final String worldName;
     private final YamlConfiguration config;
     private boolean verbose;
 
-    public SpigotWorldConfig(String worldName)
-    {
+    public SpigotWorldConfig(String worldName) {
         this.worldName = worldName;
         this.config = SpigotConfig.config;
         this.init();
     }
 
-    public void init()
-    {
-        this.verbose = this.getBoolean( "verbose", false ); // Paper
+    public void init() {
+        this.verbose = this.getBoolean("verbose", false); // Paper
 
-        this.log( "-------- World Settings For [" + this.worldName + "] --------" );
-        SpigotConfig.readConfig( SpigotWorldConfig.class, this );
+        this.log("-------- World Settings For [" + this.worldName + "] --------");
+        SpigotConfig.readConfig(SpigotWorldConfig.class, this);
     }
 
-    private void log(String s)
-    {
-        if ( this.verbose )
-        {
-            Bukkit.getLogger().info( s );
+    private void log(String s) {
+        if (this.verbose) {
+            Bukkit.getLogger().info(s);
         }
     }
 
-    private void set(String path, Object val)
-    {
-        this.config.set( "world-settings.default." + path, val );
+    private void set(String path, Object val) {
+        this.config.set("world-settings.default." + path, val);
     }
 
-    public boolean getBoolean(String path, boolean def)
-    {
-        this.config.addDefault( "world-settings.default." + path, def );
-        return this.config.getBoolean( "world-settings." + this.worldName + "." + path, this.config.getBoolean( "world-settings.default." + path ) );
+    public boolean getBoolean(String path, boolean def) {
+        this.config.addDefault("world-settings.default." + path, def);
+        return this.config.getBoolean("world-settings." + this.worldName + "." + path, this.config.getBoolean("world-settings.default." + path));
     }
 
-    public double getDouble(String path, double def)
-    {
-        this.config.addDefault( "world-settings.default." + path, def );
-        return this.config.getDouble( "world-settings." + this.worldName + "." + path, this.config.getDouble( "world-settings.default." + path ) );
+    public double getDouble(String path, double def) {
+        this.config.addDefault("world-settings.default." + path, def);
+        return this.config.getDouble("world-settings." + this.worldName + "." + path, this.config.getDouble("world-settings.default." + path));
     }
 
-    public int getInt(String path)
-    {
-        return this.config.getInt( "world-settings." + this.worldName + "." + path );
+    public int getInt(String path) {
+        return this.config.getInt("world-settings." + this.worldName + "." + path);
     }
 
-    public int getInt(String path, int def)
-    {
+    public int getInt(String path, int def) {
         // Paper start - get int without setting default
         return this.getInt(path, def, true);
     }
-    public int getInt(String path, int def, boolean setDef)
-    {
-        if (setDef) this.config.addDefault( "world-settings.default." + path, def );
-        return this.config.getInt( "world-settings." + this.worldName + "." + path, this.config.getInt( "world-settings.default." + path, def ) );
+
+    public int getInt(String path, int def, boolean setDef) {
+        if (setDef) this.config.addDefault("world-settings.default." + path, def);
+        return this.config.getInt("world-settings." + this.worldName + "." + path, this.config.getInt("world-settings.default." + path, def));
         // Paper end
     }
 
-    public <T> List getList(String path, T def)
-    {
-        this.config.addDefault( "world-settings.default." + path, def );
-        return (List<T>) this.config.getList( "world-settings." + this.worldName + "." + path, this.config.getList( "world-settings.default." + path ) );
+    public <T> List getList(String path, T def) {
+        this.config.addDefault("world-settings.default." + path, def);
+        return (List<T>) this.config.getList("world-settings." + this.worldName + "." + path, this.config.getList("world-settings.default." + path));
     }
 
-    public String getString(String path, String def)
-    {
-        this.config.addDefault( "world-settings.default." + path, def );
-        return this.config.getString( "world-settings." + this.worldName + "." + path, this.config.getString( "world-settings.default." + path ) );
+    public String getString(String path, String def) {
+        this.config.addDefault("world-settings.default." + path, def);
+        return this.config.getString("world-settings." + this.worldName + "." + path, this.config.getString("world-settings.default." + path));
     }
 
-    private Object get(String path, Object def)
-    {
-        this.config.addDefault( "world-settings.default." + path, def );
-        return this.config.get( "world-settings." + this.worldName + "." + path, this.config.get( "world-settings.default." + path ) );
+    private Object get(String path, Object def) {
+        this.config.addDefault("world-settings.default." + path, def);
+        return this.config.get("world-settings." + this.worldName + "." + path, this.config.get("world-settings.default." + path));
     }
 
     // Crop growth rates
@@ -109,119 +96,155 @@ public class SpigotWorldConfig
     public int caveVinesModifier;
     public int glowBerryModifier; // Paper
     public int pitcherPlantModifier; // Paper
-    private int getAndValidateGrowth(String crop)
-    {
-        int modifier = this.getInt( "growth." + crop.toLowerCase(java.util.Locale.ENGLISH) + "-modifier", 100 );
-        if ( modifier == 0 )
-        {
-            this.log( "Cannot set " + crop + " growth to zero, defaulting to 100" );
+
+    private int getAndValidateGrowth(String crop) {
+        int modifier = this.getInt("growth." + crop.toLowerCase(java.util.Locale.ENGLISH) + "-modifier", 100);
+        if (modifier == 0) {
+            this.log("Cannot set " + crop + " growth to zero, defaulting to 100");
             modifier = 100;
         }
-        this.log( crop + " Growth Modifier: " + modifier + "%" );
+        this.log(crop + " Growth Modifier: " + modifier + "%");
 
         return modifier;
     }
-    private void growthModifiers()
-    {
-        this.cactusModifier = this.getAndValidateGrowth( "Cactus" );
-        this.caneModifier = this.getAndValidateGrowth( "Cane" );
-        this.melonModifier = this.getAndValidateGrowth( "Melon" );
-        this.mushroomModifier = this.getAndValidateGrowth( "Mushroom" );
-        this.pumpkinModifier = this.getAndValidateGrowth( "Pumpkin" );
-        this.saplingModifier = this.getAndValidateGrowth( "Sapling" );
-        this.beetrootModifier = this.getAndValidateGrowth( "Beetroot" );
-        this.carrotModifier = this.getAndValidateGrowth( "Carrot" );
-        this.potatoModifier = this.getAndValidateGrowth( "Potato" );
+
+    private void growthModifiers() {
+        this.cactusModifier = this.getAndValidateGrowth("Cactus");
+        this.caneModifier = this.getAndValidateGrowth("Cane");
+        this.melonModifier = this.getAndValidateGrowth("Melon");
+        this.mushroomModifier = this.getAndValidateGrowth("Mushroom");
+        this.pumpkinModifier = this.getAndValidateGrowth("Pumpkin");
+        this.saplingModifier = this.getAndValidateGrowth("Sapling");
+        this.beetrootModifier = this.getAndValidateGrowth("Beetroot");
+        this.carrotModifier = this.getAndValidateGrowth("Carrot");
+        this.potatoModifier = this.getAndValidateGrowth("Potato");
         this.torchFlowerModifier = this.getAndValidateGrowth("TorchFlower"); // Paper
-        this.wheatModifier = this.getAndValidateGrowth( "Wheat" );
-        this.wartModifier = this.getAndValidateGrowth( "NetherWart" );
-        this.vineModifier = this.getAndValidateGrowth( "Vine" );
-        this.cocoaModifier = this.getAndValidateGrowth( "Cocoa" );
-        this.bambooModifier = this.getAndValidateGrowth( "Bamboo" );
-        this.sweetBerryModifier = this.getAndValidateGrowth( "SweetBerry" );
-        this.kelpModifier = this.getAndValidateGrowth( "Kelp" );
-        this.twistingVinesModifier = this.getAndValidateGrowth( "TwistingVines" );
-        this.weepingVinesModifier = this.getAndValidateGrowth( "WeepingVines" );
-        this.caveVinesModifier = this.getAndValidateGrowth( "CaveVines" );
+        this.wheatModifier = this.getAndValidateGrowth("Wheat");
+        this.wartModifier = this.getAndValidateGrowth("NetherWart");
+        this.vineModifier = this.getAndValidateGrowth("Vine");
+        this.cocoaModifier = this.getAndValidateGrowth("Cocoa");
+        this.bambooModifier = this.getAndValidateGrowth("Bamboo");
+        this.sweetBerryModifier = this.getAndValidateGrowth("SweetBerry");
+        this.kelpModifier = this.getAndValidateGrowth("Kelp");
+        this.twistingVinesModifier = this.getAndValidateGrowth("TwistingVines");
+        this.weepingVinesModifier = this.getAndValidateGrowth("WeepingVines");
+        this.caveVinesModifier = this.getAndValidateGrowth("CaveVines");
         this.glowBerryModifier = this.getAndValidateGrowth("GlowBerry"); // Paper
         this.pitcherPlantModifier = this.getAndValidateGrowth("PitcherPlant"); // Paper
     }
 
     public double itemMerge;
-    private void itemMerge()
-    {
-        this.itemMerge = this.getDouble("merge-radius.item", 0.5 );
-        this.log( "Item Merge Radius: " + this.itemMerge );
+    private void itemMerge() {
+        this.itemMerge = this.getDouble("merge-radius.item", 0.5);
+        this.log("Item Merge Radius: " + this.itemMerge);
     }
 
     public double expMerge;
-    private void expMerge()
-    {
-        this.expMerge = this.getDouble("merge-radius.exp", -1 );
-        this.log( "Experience Merge Radius: " + this.expMerge );
+    private void expMerge() {
+        this.expMerge = this.getDouble("merge-radius.exp", -1);
+        this.log("Experience Merge Radius: " + this.expMerge);
     }
 
     public int viewDistance;
-    private void viewDistance()
-    {
-        if ( SpigotConfig.version < 12 )
-        {
-            this.set( "view-distance", null );
+    private void viewDistance() {
+        if (SpigotConfig.version < 12) {
+            this.set("view-distance", null);
         }
 
-        Object viewDistanceObject = this.get( "view-distance", "default" );
-        this.viewDistance = ( viewDistanceObject ) instanceof Number ? ( (Number) viewDistanceObject ).intValue() : -1;
-        if ( this.viewDistance <= 0 )
-        {
+        Object viewDistanceObject = this.get("view-distance", "default");
+        this.viewDistance = (viewDistanceObject) instanceof Number ? ((Number) viewDistanceObject).intValue() : -1;
+        if (this.viewDistance <= 0) {
             this.viewDistance = Bukkit.getViewDistance();
         }
 
-        this.viewDistance = Math.max( Math.min( this.viewDistance, 32 ), 3 );
-        this.log( "View Distance: " + this.viewDistance );
+        this.viewDistance = Math.max(Math.min(this.viewDistance, 32), 3);
+        this.log("View Distance: " + this.viewDistance);
     }
 
     public int simulationDistance;
-    private void simulationDistance()
-    {
-        Object simulationDistanceObject = this.get( "simulation-distance", "default" );
-        this.simulationDistance = ( simulationDistanceObject ) instanceof Number ? ( (Number) simulationDistanceObject ).intValue() : -1;
-        if ( this.simulationDistance <= 0 )
-        {
+    private void simulationDistance() {
+        Object simulationDistanceObject = this.get("simulation-distance", "default");
+        this.simulationDistance = (simulationDistanceObject) instanceof Number ? ((Number) simulationDistanceObject).intValue() : -1;
+        if (this.simulationDistance <= 0) {
             this.simulationDistance = Bukkit.getSimulationDistance();
         }
 
-        this.log( "Simulation Distance: " + this.simulationDistance );
+        this.log("Simulation Distance: " + this.simulationDistance);
     }
 
     public byte mobSpawnRange;
-    private void mobSpawnRange()
-    {
-        this.mobSpawnRange = (byte) getInt( "mob-spawn-range", 8 ); // Paper - Vanilla
-        this.log( "Mob Spawn Range: " + this.mobSpawnRange );
+    private void mobSpawnRange() {
+        this.mobSpawnRange = (byte) getInt("mob-spawn-range", 8); // Paper - Vanilla
+        this.log("Mob Spawn Range: " + this.mobSpawnRange);
     }
 
     public int itemDespawnRate;
-    private void itemDespawnRate()
-    {
-        this.itemDespawnRate = this.getInt( "item-despawn-rate", 6000 );
-        this.log( "Item Despawn Rate: " + this.itemDespawnRate );
+    private void itemDespawnRate() {
+        this.itemDespawnRate = this.getInt("item-despawn-rate", 6000);
+        this.log("Item Despawn Rate: " + this.itemDespawnRate);
     }
 
     public int animalActivationRange = 32;
     public int monsterActivationRange = 32;
     public int raiderActivationRange = 64;
     public int miscActivationRange = 16;
+    // Paper start
+    public int flyingMonsterActivationRange = 32;
+    public int waterActivationRange = 16;
+    public int villagerActivationRange = 32;
+    public int wakeUpInactiveAnimals = 4;
+    public int wakeUpInactiveAnimalsEvery = 60 * 20;
+    public int wakeUpInactiveAnimalsFor = 5 * 20;
+    public int wakeUpInactiveMonsters = 8;
+    public int wakeUpInactiveMonstersEvery = 20 * 20;
+    public int wakeUpInactiveMonstersFor = 5 * 20;
+    public int wakeUpInactiveVillagers = 4;
+    public int wakeUpInactiveVillagersEvery = 30 * 20;
+    public int wakeUpInactiveVillagersFor = 5 * 20;
+    public int wakeUpInactiveFlying = 8;
+    public int wakeUpInactiveFlyingEvery = 10 * 20;
+    public int wakeUpInactiveFlyingFor = 5 * 20;
+    public int villagersWorkImmunityAfter = 5 * 20;
+    public int villagersWorkImmunityFor = 20;
+    public boolean villagersActiveForPanic = true;
+    // Paper end
     public boolean tickInactiveVillagers = true;
     public boolean ignoreSpectatorActivation = false;
-    private void activationRange()
-    {
-        this.animalActivationRange = this.getInt( "entity-activation-range.animals", this.animalActivationRange );
-        this.monsterActivationRange = this.getInt( "entity-activation-range.monsters", this.monsterActivationRange );
-        this.raiderActivationRange = this.getInt( "entity-activation-range.raiders", this.raiderActivationRange );
-        this.miscActivationRange = this.getInt( "entity-activation-range.misc", this.miscActivationRange );
-        this.tickInactiveVillagers = this.getBoolean( "entity-activation-range.tick-inactive-villagers", this.tickInactiveVillagers );
-        this.ignoreSpectatorActivation = this.getBoolean( "entity-activation-range.ignore-spectators", this.ignoreSpectatorActivation );
-        this.log( "Entity Activation Range: An " + this.animalActivationRange + " / Mo " + this.monsterActivationRange + " / Ra " + this.raiderActivationRange + " / Mi " + this.miscActivationRange + " / Tiv " + this.tickInactiveVillagers + " / Isa " + this.ignoreSpectatorActivation );
+
+    private void activationRange() {
+        boolean hasAnimalsConfig = config.getInt("entity-activation-range.animals", this.animalActivationRange) != this.animalActivationRange; // Paper
+        this.animalActivationRange = this.getInt("entity-activation-range.animals", this.animalActivationRange);
+        this.monsterActivationRange = this.getInt("entity-activation-range.monsters", this.monsterActivationRange);
+        this.raiderActivationRange = this.getInt("entity-activation-range.raiders", this.raiderActivationRange);
+        this.miscActivationRange = this.getInt("entity-activation-range.misc", this.miscActivationRange);
+        // Paper start
+        this.waterActivationRange = this.getInt("entity-activation-range.water", this.waterActivationRange);
+        this.villagerActivationRange = this.getInt("entity-activation-range.villagers", hasAnimalsConfig ? this.animalActivationRange : this.villagerActivationRange);
+        this.flyingMonsterActivationRange = this.getInt("entity-activation-range.flying-monsters", this.flyingMonsterActivationRange);
+
+        this.wakeUpInactiveAnimals = this.getInt("entity-activation-range.wake-up-inactive.animals-max-per-tick", this.wakeUpInactiveAnimals);
+        this.wakeUpInactiveAnimalsEvery = this.getInt("entity-activation-range.wake-up-inactive.animals-every", this.wakeUpInactiveAnimalsEvery);
+        this.wakeUpInactiveAnimalsFor = this.getInt("entity-activation-range.wake-up-inactive.animals-for", this.wakeUpInactiveAnimalsFor);
+
+        this.wakeUpInactiveMonsters = this.getInt("entity-activation-range.wake-up-inactive.monsters-max-per-tick", this.wakeUpInactiveMonsters);
+        this.wakeUpInactiveMonstersEvery = this.getInt("entity-activation-range.wake-up-inactive.monsters-every", this.wakeUpInactiveMonstersEvery);
+        this.wakeUpInactiveMonstersFor = this.getInt("entity-activation-range.wake-up-inactive.monsters-for", this.wakeUpInactiveMonstersFor);
+
+        this.wakeUpInactiveVillagers = this.getInt("entity-activation-range.wake-up-inactive.villagers-max-per-tick", this.wakeUpInactiveVillagers);
+        this.wakeUpInactiveVillagersEvery = this.getInt("entity-activation-range.wake-up-inactive.villagers-every", this.wakeUpInactiveVillagersEvery);
+        this.wakeUpInactiveVillagersFor = this.getInt("entity-activation-range.wake-up-inactive.villagers-for", this.wakeUpInactiveVillagersFor);
+
+        this.wakeUpInactiveFlying = this.getInt("entity-activation-range.wake-up-inactive.flying-monsters-max-per-tick", this.wakeUpInactiveFlying);
+        this.wakeUpInactiveFlyingEvery = this.getInt("entity-activation-range.wake-up-inactive.flying-monsters-every", this.wakeUpInactiveFlyingEvery);
+        this.wakeUpInactiveFlyingFor = this.getInt("entity-activation-range.wake-up-inactive.flying-monsters-for", this.wakeUpInactiveFlyingFor);
+
+        this.villagersWorkImmunityAfter = this.getInt("entity-activation-range.villagers-work-immunity-after", this.villagersWorkImmunityAfter);
+        this.villagersWorkImmunityFor = this.getInt("entity-activation-range.villagers-work-immunity-for", this.villagersWorkImmunityFor);
+        this.villagersActiveForPanic = this.getBoolean("entity-activation-range.villagers-active-for-panic", this.villagersActiveForPanic);
+        // Paper end
+        this.tickInactiveVillagers = this.getBoolean("entity-activation-range.tick-inactive-villagers", this.tickInactiveVillagers);
+        this.ignoreSpectatorActivation = this.getBoolean("entity-activation-range.ignore-spectators", this.ignoreSpectatorActivation);
+        this.log("Entity Activation Range: An " + this.animalActivationRange + " / Mo " + this.monsterActivationRange + " / Ra " + this.raiderActivationRange + " / Mi " + this.miscActivationRange + " / Tiv " + this.tickInactiveVillagers + " / Isa " + this.ignoreSpectatorActivation);
     }
 
     public int playerTrackingRange = 128;
@@ -230,81 +253,71 @@ public class SpigotWorldConfig
     public int miscTrackingRange = 96;
     public int displayTrackingRange = 128;
     public int otherTrackingRange = 64;
-    private void trackingRange()
-    {
-        this.playerTrackingRange = this.getInt( "entity-tracking-range.players", this.playerTrackingRange );
-        this.animalTrackingRange = this.getInt( "entity-tracking-range.animals", this.animalTrackingRange );
-        this.monsterTrackingRange = this.getInt( "entity-tracking-range.monsters", this.monsterTrackingRange );
-        this.miscTrackingRange = this.getInt( "entity-tracking-range.misc", this.miscTrackingRange );
-        this.displayTrackingRange = this.getInt( "entity-tracking-range.display", this.displayTrackingRange );
-        this.otherTrackingRange = this.getInt( "entity-tracking-range.other", this.otherTrackingRange );
-        this.log( "Entity Tracking Range: Pl " + this.playerTrackingRange + " / An " + this.animalTrackingRange + " / Mo " + this.monsterTrackingRange + " / Mi " + this.miscTrackingRange + " / Di " + this.displayTrackingRange + " / Other " + this.otherTrackingRange );
+    private void trackingRange() {
+        this.playerTrackingRange = this.getInt("entity-tracking-range.players", this.playerTrackingRange);
+        this.animalTrackingRange = this.getInt("entity-tracking-range.animals", this.animalTrackingRange);
+        this.monsterTrackingRange = this.getInt("entity-tracking-range.monsters", this.monsterTrackingRange);
+        this.miscTrackingRange = this.getInt("entity-tracking-range.misc", this.miscTrackingRange);
+        this.displayTrackingRange = this.getInt("entity-tracking-range.display", this.displayTrackingRange);
+        this.otherTrackingRange = this.getInt("entity-tracking-range.other", this.otherTrackingRange);
+        this.log("Entity Tracking Range: Pl " + this.playerTrackingRange + " / An " + this.animalTrackingRange + " / Mo " + this.monsterTrackingRange + " / Mi " + this.miscTrackingRange + " / Di " + this.displayTrackingRange + " / Other " + this.otherTrackingRange);
     }
 
     public int hopperTransfer;
     public int hopperCheck;
     public int hopperAmount;
     public boolean hopperCanLoadChunks;
-    private void hoppers()
-    {
+    private void hoppers() {
         // Set the tick delay between hopper item movements
-        this.hopperTransfer = this.getInt( "ticks-per.hopper-transfer", 8 );
-        if ( SpigotConfig.version < 11 )
-        {
-            this.set( "ticks-per.hopper-check", 1 );
+        this.hopperTransfer = this.getInt("ticks-per.hopper-transfer", 8);
+        if (SpigotConfig.version < 11) {
+            this.set("ticks-per.hopper-check", 1);
         }
-        this.hopperCheck = this.getInt( "ticks-per.hopper-check", 1 );
-        this.hopperAmount = this.getInt( "hopper-amount", 1 );
-        this.hopperCanLoadChunks = this.getBoolean( "hopper-can-load-chunks", false );
-        this.log( "Hopper Transfer: " + this.hopperTransfer + " Hopper Check: " + this.hopperCheck + " Hopper Amount: " + this.hopperAmount + " Hopper Can Load Chunks: " + this.hopperCanLoadChunks );
+        this.hopperCheck = this.getInt("ticks-per.hopper-check", 1);
+        this.hopperAmount = this.getInt("hopper-amount", 1);
+        this.hopperCanLoadChunks = this.getBoolean("hopper-can-load-chunks", false);
+        this.log("Hopper Transfer: " + this.hopperTransfer + " Hopper Check: " + this.hopperCheck + " Hopper Amount: " + this.hopperAmount + " Hopper Can Load Chunks: " + this.hopperCanLoadChunks);
     }
 
     public int arrowDespawnRate;
     public int tridentDespawnRate;
-    private void arrowDespawnRate()
-    {
-        this.arrowDespawnRate = this.getInt( "arrow-despawn-rate", 1200 );
-        this.tridentDespawnRate = this.getInt( "trident-despawn-rate", this.arrowDespawnRate );
-        this.log( "Arrow Despawn Rate: " + this.arrowDespawnRate + " Trident Respawn Rate:" + this.tridentDespawnRate );
+    private void arrowDespawnRate() {
+        this.arrowDespawnRate = this.getInt("arrow-despawn-rate", 1200);
+        this.tridentDespawnRate = this.getInt("trident-despawn-rate", this.arrowDespawnRate);
+        this.log("Arrow Despawn Rate: " + this.arrowDespawnRate + " Trident Respawn Rate:" + this.tridentDespawnRate);
     }
 
     public boolean zombieAggressiveTowardsVillager;
-    private void zombieAggressiveTowardsVillager()
-    {
-        this.zombieAggressiveTowardsVillager = this.getBoolean( "zombie-aggressive-towards-villager", true );
-        this.log( "Zombie Aggressive Towards Villager: " + this.zombieAggressiveTowardsVillager );
+    private void zombieAggressiveTowardsVillager() {
+        this.zombieAggressiveTowardsVillager = this.getBoolean("zombie-aggressive-towards-villager", true);
+        this.log("Zombie Aggressive Towards Villager: " + this.zombieAggressiveTowardsVillager);
     }
 
     public boolean nerfSpawnerMobs;
-    private void nerfSpawnerMobs()
-    {
-        this.nerfSpawnerMobs = this.getBoolean( "nerf-spawner-mobs", false );
-        this.log( "Nerfing mobs spawned from spawners: " + this.nerfSpawnerMobs );
+    private void nerfSpawnerMobs() {
+        this.nerfSpawnerMobs = this.getBoolean("nerf-spawner-mobs", false);
+        this.log("Nerfing mobs spawned from spawners: " + this.nerfSpawnerMobs);
     }
 
     public boolean enableZombiePigmenPortalSpawns;
-    private void enableZombiePigmenPortalSpawns()
-    {
-        this.enableZombiePigmenPortalSpawns = this.getBoolean( "enable-zombie-pigmen-portal-spawns", true );
-        this.log( "Allow Zombie Pigmen to spawn from portal blocks: " + this.enableZombiePigmenPortalSpawns );
+    private void enableZombiePigmenPortalSpawns() {
+        this.enableZombiePigmenPortalSpawns = this.getBoolean("enable-zombie-pigmen-portal-spawns", true);
+        this.log("Allow Zombie Pigmen to spawn from portal blocks: " + this.enableZombiePigmenPortalSpawns);
     }
 
     public int dragonDeathSoundRadius;
-    private void keepDragonDeathPerWorld()
-    {
-        this.dragonDeathSoundRadius = this.getInt( "dragon-death-sound-radius", 0 );
+    private void keepDragonDeathPerWorld() {
+        this.dragonDeathSoundRadius = this.getInt("dragon-death-sound-radius", 0);
     }
 
     public int witherSpawnSoundRadius;
-    private void witherSpawnSoundRadius()
-    {
-        this.witherSpawnSoundRadius = this.getInt( "wither-spawn-sound-radius", 0 );
+    private void witherSpawnSoundRadius() {
+        this.witherSpawnSoundRadius = this.getInt("wither-spawn-sound-radius", 0);
     }
 
     public int endPortalSoundRadius;
-    private void endPortalSoundRadius()
-    {
-        this.endPortalSoundRadius = this.getInt( "end-portal-sound-radius", 0 );
+    private void endPortalSoundRadius() {
+        this.endPortalSoundRadius = this.getInt("end-portal-sound-radius", 0);
     }
 
     public int villageSeed;
@@ -329,28 +342,29 @@ public class SpigotWorldConfig
     public int buriedTreasureSeed;
     public Integer mineshaftSeed;
     public Long strongholdSeed;
+
     private <N extends Number> N getSeed(String path, java.util.function.Function<String, N> toNumberFunc) {
         final String value = this.getString(path, "default");
         return org.apache.commons.lang3.math.NumberUtils.isParsable(value) ? toNumberFunc.apply(value) : null;
     }
+
     // Paper end
-    private void initWorldGenSeeds()
-    {
-        this.villageSeed = this.getInt( "seed-village", 10387312 );
-        this.desertSeed = this.getInt( "seed-desert", 14357617 );
-        this.iglooSeed = this.getInt( "seed-igloo", 14357618 );
-        this.jungleSeed = this.getInt( "seed-jungle", 14357619 );
-        this.swampSeed = this.getInt( "seed-swamp", 14357620 );
-        this.monumentSeed = this.getInt( "seed-monument", 10387313 );
-        this.shipwreckSeed = this.getInt( "seed-shipwreck", 165745295 );
-        this.oceanSeed = this.getInt( "seed-ocean", 14357621 );
-        this.outpostSeed = this.getInt( "seed-outpost", 165745296 );
-        this.endCitySeed = this.getInt( "seed-endcity", 10387313 );
-        this.slimeSeed = this.getInt( "seed-slime", 987234911 );
-        this.netherSeed = this.getInt( "seed-nether", 30084232 );
-        this.mansionSeed = this.getInt( "seed-mansion", 10387319 );
-        this.fossilSeed = this.getInt( "seed-fossil", 14357921 );
-        this.portalSeed = this.getInt( "seed-portal", 34222645 );
+    private void initWorldGenSeeds() {
+        this.villageSeed = this.getInt("seed-village", 10387312);
+        this.desertSeed = this.getInt("seed-desert", 14357617);
+        this.iglooSeed = this.getInt("seed-igloo", 14357618);
+        this.jungleSeed = this.getInt("seed-jungle", 14357619);
+        this.swampSeed = this.getInt("seed-swamp", 14357620);
+        this.monumentSeed = this.getInt("seed-monument", 10387313);
+        this.shipwreckSeed = this.getInt("seed-shipwreck", 165745295);
+        this.oceanSeed = this.getInt("seed-ocean", 14357621);
+        this.outpostSeed = this.getInt("seed-outpost", 165745296);
+        this.endCitySeed = this.getInt("seed-endcity", 10387313);
+        this.slimeSeed = this.getInt("seed-slime", 987234911);
+        this.netherSeed = this.getInt("seed-nether", 30084232);
+        this.mansionSeed = this.getInt("seed-mansion", 10387319);
+        this.fossilSeed = this.getInt("seed-fossil", 14357921);
+        this.portalSeed = this.getInt("seed-portal", 34222645);
         // Paper start - add missing structure set configs
         this.ancientCitySeed = this.getInt("seed-ancientcity", 20083232);
         this.trailRuinsSeed = this.getInt("seed-trailruins", 83469867);
@@ -359,8 +373,8 @@ public class SpigotWorldConfig
         this.mineshaftSeed = this.getSeed("seed-mineshaft", Integer::parseInt);
         this.strongholdSeed = this.getSeed("seed-stronghold", Long::parseLong);
         // Paper end
-        this.log( "Custom Map Seeds:  Village: " + this.villageSeed + " Desert: " + this.desertSeed + " Igloo: " + this.iglooSeed + " Jungle: " + this.jungleSeed + " Swamp: " + this.swampSeed + " Monument: " + this.monumentSeed
-                + " Ocean: " + this.oceanSeed + " Shipwreck: " + this.shipwreckSeed + " End City: " + this.endCitySeed + " Slime: " + this.slimeSeed + " Nether: " + this.netherSeed + " Mansion: " + this.mansionSeed + " Fossil: " + this.fossilSeed + " Portal: " + this.portalSeed );
+        this.log("Custom Map Seeds:  Village: " + this.villageSeed + " Desert: " + this.desertSeed + " Igloo: " + this.iglooSeed + " Jungle: " + this.jungleSeed + " Swamp: " + this.swampSeed + " Monument: " + this.monumentSeed
+            + " Ocean: " + this.oceanSeed + " Shipwreck: " + this.shipwreckSeed + " End City: " + this.endCitySeed + " Slime: " + this.slimeSeed + " Nether: " + this.netherSeed + " Mansion: " + this.mansionSeed + " Fossil: " + this.fossilSeed + " Portal: " + this.portalSeed);
     }
 
     public float jumpWalkExhaustion;
@@ -370,54 +384,48 @@ public class SpigotWorldConfig
     public float swimMultiplier;
     public float sprintMultiplier;
     public float otherMultiplier;
-    private void initHunger()
-    {
-        if ( SpigotConfig.version < 10 )
-        {
-            this.set( "hunger.walk-exhaustion", null );
-            this.set( "hunger.sprint-exhaustion", null );
-            this.set( "hunger.combat-exhaustion", 0.1 );
-            this.set( "hunger.regen-exhaustion", 6.0 );
+    private void initHunger() {
+        if (SpigotConfig.version < 10) {
+            this.set("hunger.walk-exhaustion", null);
+            this.set("hunger.sprint-exhaustion", null);
+            this.set("hunger.combat-exhaustion", 0.1);
+            this.set("hunger.regen-exhaustion", 6.0);
         }
 
-        this.jumpWalkExhaustion = (float) this.getDouble( "hunger.jump-walk-exhaustion", 0.05 );
-        this.jumpSprintExhaustion = (float) this.getDouble( "hunger.jump-sprint-exhaustion", 0.2 );
-        this.combatExhaustion = (float) this.getDouble( "hunger.combat-exhaustion", 0.1 );
-        this.regenExhaustion = (float) this.getDouble( "hunger.regen-exhaustion", 6.0 );
-        this.swimMultiplier = (float) this.getDouble( "hunger.swim-multiplier", 0.01 );
-        this.sprintMultiplier = (float) this.getDouble( "hunger.sprint-multiplier", 0.1 );
-        this.otherMultiplier = (float) this.getDouble( "hunger.other-multiplier", 0.0 );
+        this.jumpWalkExhaustion = (float) this.getDouble("hunger.jump-walk-exhaustion", 0.05);
+        this.jumpSprintExhaustion = (float) this.getDouble("hunger.jump-sprint-exhaustion", 0.2);
+        this.combatExhaustion = (float) this.getDouble("hunger.combat-exhaustion", 0.1);
+        this.regenExhaustion = (float) this.getDouble("hunger.regen-exhaustion", 6.0);
+        this.swimMultiplier = (float) this.getDouble("hunger.swim-multiplier", 0.01);
+        this.sprintMultiplier = (float) this.getDouble("hunger.sprint-multiplier", 0.1);
+        this.otherMultiplier = (float) this.getDouble("hunger.other-multiplier", 0.0);
     }
 
     public int currentPrimedTnt = 0;
     public int maxTntTicksPerTick;
     private void maxTntPerTick() {
-        if ( SpigotConfig.version < 7 )
-        {
-            this.set( "max-tnt-per-tick", 100 );
+        if (SpigotConfig.version < 7) {
+            this.set("max-tnt-per-tick", 100);
         }
-        this.maxTntTicksPerTick = this.getInt( "max-tnt-per-tick", 100 );
-        this.log( "Max TNT Explosions: " + this.maxTntTicksPerTick );
+        this.maxTntTicksPerTick = this.getInt("max-tnt-per-tick", 100);
+        this.log("Max TNT Explosions: " + this.maxTntTicksPerTick);
     }
 
     public int hangingTickFrequency;
-    private void hangingTickFrequency()
-    {
-        this.hangingTickFrequency = this.getInt( "hanging-tick-frequency", 100 );
+    private void hangingTickFrequency() {
+        this.hangingTickFrequency = this.getInt("hanging-tick-frequency", 100);
     }
 
     public int tileMaxTickTime;
     public int entityMaxTickTime;
-    private void maxTickTimes()
-    {
+    private void maxTickTimes() {
         this.tileMaxTickTime = this.getInt("max-tick-time.tile", 50);
         this.entityMaxTickTime = this.getInt("max-tick-time.entity", 50);
         this.log("Tile Max Tick Time: " + this.tileMaxTickTime + "ms Entity max Tick Time: " + this.entityMaxTickTime + "ms");
     }
 
     public int thunderChance;
-    private void thunderChance()
-    {
+    private void thunderChance() {
         this.thunderChance = this.getInt("thunder-chance", 100000);
     }
 
diff --git a/paper-server/src/main/java/org/spigotmc/TickLimiter.java b/paper-server/src/main/java/org/spigotmc/TickLimiter.java
index 4074538ea6..961489499e 100644
--- a/paper-server/src/main/java/org/spigotmc/TickLimiter.java
+++ b/paper-server/src/main/java/org/spigotmc/TickLimiter.java
@@ -5,8 +5,8 @@ public class TickLimiter {
     private final int maxTime;
     private long startTime;
 
-    public TickLimiter(int maxtime) {
-        this.maxTime = maxtime;
+    public TickLimiter(int maxTime) {
+        this.maxTime = maxTime;
     }
 
     public void initTick() {
diff --git a/paper-server/src/main/java/org/spigotmc/TicksPerSecondCommand.java b/paper-server/src/main/java/org/spigotmc/TicksPerSecondCommand.java
index 9eb2823cc8..6d4851aa8c 100644
--- a/paper-server/src/main/java/org/spigotmc/TicksPerSecondCommand.java
+++ b/paper-server/src/main/java/org/spigotmc/TicksPerSecondCommand.java
@@ -1,51 +1,55 @@
 package org.spigotmc;
 
-import net.minecraft.server.MinecraftServer;
-import org.bukkit.ChatColor;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.JoinConfiguration;
+import net.kyori.adventure.text.TextComponent;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.kyori.adventure.text.format.TextColor;
 import org.bukkit.command.Command;
 import org.bukkit.command.CommandSender;
 
-public class TicksPerSecondCommand extends Command
-{
+import static net.kyori.adventure.text.Component.text;
 
-    public TicksPerSecondCommand(String name)
-    {
-        super( name );
+public class TicksPerSecondCommand extends Command {
+
+    private boolean hasShownMemoryWarning; // Paper
+
+    public TicksPerSecondCommand(String name) {
+        super(name);
         this.description = "Gets the current ticks per second for the server";
         this.usageMessage = "/tps";
-        this.setPermission( "bukkit.command.tps" );
+        this.setPermission("bukkit.command.tps");
     }
+
     // Paper start
-    private static final net.kyori.adventure.text.Component WARN_MSG = net.kyori.adventure.text.Component.text()
-        .append(net.kyori.adventure.text.Component.text("Warning: ", net.kyori.adventure.text.format.NamedTextColor.RED))
-        .append(net.kyori.adventure.text.Component.text("Memory usage on modern garbage collectors is not a stable value and it is perfectly normal to see it reach max. Please do not pay it much attention.", net.kyori.adventure.text.format.NamedTextColor.GOLD))
+    private static final Component WARN_MSG = text()
+        .append(text("Warning: ", NamedTextColor.RED))
+        .append(text("Memory usage on modern garbage collectors is not a stable value and it is perfectly normal to see it reach max. Please do not pay it much attention.", NamedTextColor.GOLD))
         .build();
     // Paper end
 
     @Override
-    public boolean execute(CommandSender sender, String currentAlias, String[] args)
-    {
-        if ( !this.testPermission( sender ) )
-        {
+    public boolean execute(CommandSender sender, String currentAlias, String[] args) {
+        if (!this.testPermission(sender)) {
             return true;
         }
 
         // Paper start - Further improve tick handling
         double[] tps = org.bukkit.Bukkit.getTPS();
-        net.kyori.adventure.text.Component[] tpsAvg = new net.kyori.adventure.text.Component[tps.length];
+        Component[] tpsAvg = new Component[tps.length];
 
-        for ( int i = 0; i < tps.length; i++) {
-            tpsAvg[i] = TicksPerSecondCommand.format( tps[i] );
+        for (int i = 0; i < tps.length; i++) {
+            tpsAvg[i] = TicksPerSecondCommand.format(tps[i]);
         }
 
-        net.kyori.adventure.text.TextComponent.Builder builder = net.kyori.adventure.text.Component.text();
-        builder.append(net.kyori.adventure.text.Component.text("TPS from last 1m, 5m, 15m: ", net.kyori.adventure.text.format.NamedTextColor.GOLD));
-        builder.append(net.kyori.adventure.text.Component.join(net.kyori.adventure.text.JoinConfiguration.commas(true), tpsAvg));
+        TextComponent.Builder builder = text();
+        builder.append(text("TPS from last 1m, 5m, 15m: ", NamedTextColor.GOLD));
+        builder.append(Component.join(JoinConfiguration.commas(true), tpsAvg));
         sender.sendMessage(builder.asComponent());
         if (args.length > 0 && args[0].equals("mem") && sender.hasPermission("bukkit.command.tpsmemory")) {
-            sender.sendMessage(net.kyori.adventure.text.Component.text()
-                .append(net.kyori.adventure.text.Component.text("Current Memory Usage: ", net.kyori.adventure.text.format.NamedTextColor.GOLD))
-                .append(net.kyori.adventure.text.Component.text(((Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / (1024 * 1024)) + "/" + (Runtime.getRuntime().totalMemory() / (1024 * 1024)) + " mb (Max: " + (Runtime.getRuntime().maxMemory() / (1024 * 1024)) + " mb)", net.kyori.adventure.text.format.NamedTextColor.GREEN))
+            sender.sendMessage(text()
+                .append(text("Current Memory Usage: ", NamedTextColor.GOLD))
+                .append(text(((Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / (1024 * 1024)) + "/" + (Runtime.getRuntime().totalMemory() / (1024 * 1024)) + " mb (Max: " + (Runtime.getRuntime().maxMemory() / (1024 * 1024)) + " mb)", NamedTextColor.GREEN))
             );
             if (!this.hasShownMemoryWarning) {
                 sender.sendMessage(WARN_MSG);
@@ -57,13 +61,11 @@ public class TicksPerSecondCommand extends Command
         return true;
     }
 
-    private boolean hasShownMemoryWarning; // Paper
-    private static net.kyori.adventure.text.Component format(double tps) // Paper - Made static
-    {
-        // Paper
-        net.kyori.adventure.text.format.TextColor color = ( ( tps > 18.0 ) ? net.kyori.adventure.text.format.NamedTextColor.GREEN : ( tps > 16.0 ) ? net.kyori.adventure.text.format.NamedTextColor.YELLOW : net.kyori.adventure.text.format.NamedTextColor.RED );
-        String amount = Math.min(Math.round(tps * 100.0) / 100.0, 20.0) + (tps > 21.0  ? "*" : ""); // Paper - only print * at 21, we commonly peak to 20.02 as the tick sleep is not accurate enough, stop the noise
-        return net.kyori.adventure.text.Component.text(amount, color);
+    private static Component format(double tps) { // Paper - Made static
+        // Paper start
+        TextColor color = ((tps > 18.0) ? NamedTextColor.GREEN : (tps > 16.0) ? NamedTextColor.YELLOW : NamedTextColor.RED);
+        String amount = Math.min(Math.round(tps * 100.0) / 100.0, 20.0) + (tps > 21.0 ? "*" : ""); // Paper - only print * at 21, we commonly peak to 20.02 as the tick sleep is not accurate enough, stop the noise
+        return text(amount, color);
         // Paper end
     }
 }
diff --git a/paper-server/src/main/java/org/spigotmc/TrackingRange.java b/paper-server/src/main/java/org/spigotmc/TrackingRange.java
index bb06f89a29..05db2aab74 100644
--- a/paper-server/src/main/java/org/spigotmc/TrackingRange.java
+++ b/paper-server/src/main/java/org/spigotmc/TrackingRange.java
@@ -1,5 +1,6 @@
 package org.spigotmc;
 
+import net.minecraft.server.level.ServerLevel;
 import net.minecraft.server.level.ServerPlayer;
 import net.minecraft.world.entity.Display;
 import net.minecraft.world.entity.Entity;
@@ -8,29 +9,27 @@ import net.minecraft.world.entity.decoration.ItemFrame;
 import net.minecraft.world.entity.decoration.Painting;
 import net.minecraft.world.entity.item.ItemEntity;
 
-public class TrackingRange
-{
+public final class TrackingRange {
+
+    private TrackingRange() {
+    }
 
     /**
      * Gets the range an entity should be 'tracked' by players and visible in
      * the client.
      *
-     * @param entity
      * @param defaultRange Default range defined by Mojang
-     * @return
      */
-    public static int getEntityTrackingRange(Entity entity, int defaultRange)
-    {
-        if ( defaultRange == 0 )
-        {
+    public static int getEntityTrackingRange(final Entity entity, final int defaultRange) {
+        if (defaultRange == 0) {
             return defaultRange;
         }
-        SpigotWorldConfig config = entity.level().spigotConfig;
-        if ( entity instanceof ServerPlayer )
-        {
+
+        final SpigotWorldConfig config = entity.level().spigotConfig;
+        if (entity instanceof ServerPlayer) {
             return config.playerTrackingRange;
-        // Paper start - Simplify and set water mobs to animal tracking range
         }
+
         switch (entity.activationType) {
             case RAIDER:
             case MONSTER:
@@ -42,16 +41,16 @@ public class TrackingRange
                 return config.animalTrackingRange;
             case MISC:
         }
-        if ( entity instanceof ItemFrame || entity instanceof Painting || entity instanceof ItemEntity || entity instanceof ExperienceOrb )
-        // Paper end
-        {
+
+        if (entity instanceof ItemFrame || entity instanceof Painting || entity instanceof ItemEntity || entity instanceof ExperienceOrb) {
             return config.miscTrackingRange;
-        } else if ( entity instanceof Display )
-        {
+        } else if (entity instanceof Display) {
             return config.displayTrackingRange;
-        } else
-        {
-            if (entity instanceof net.minecraft.world.entity.boss.enderdragon.EnderDragon) return ((net.minecraft.server.level.ServerLevel)(entity.getCommandSenderWorld())).getChunkSource().chunkMap.serverViewDistance; // Paper - enderdragon is exempt
+        } else {
+            if (entity instanceof net.minecraft.world.entity.boss.enderdragon.EnderDragon) {
+                // Exempt ender dragon
+                return ((ServerLevel) entity.level()).getChunkSource().chunkMap.serverViewDistance;
+            }
             return config.otherTrackingRange;
         }
     }
diff --git a/paper-server/src/main/java/org/spigotmc/WatchdogThread.java b/paper-server/src/main/java/org/spigotmc/WatchdogThread.java
index ad282d3491..a9339f59f8 100644
--- a/paper-server/src/main/java/org/spigotmc/WatchdogThread.java
+++ b/paper-server/src/main/java/org/spigotmc/WatchdogThread.java
@@ -1,5 +1,7 @@
 package org.spigotmc;
 
+import io.papermc.paper.FeatureHooks;
+import io.papermc.paper.configuration.GlobalConfiguration;
 import java.lang.management.ManagementFactory;
 import java.lang.management.MonitorInfo;
 import java.lang.management.ThreadInfo;
@@ -7,10 +9,11 @@ import java.util.logging.Level;
 import java.util.logging.Logger;
 import net.minecraft.server.MinecraftServer;
 import org.bukkit.Bukkit;
+import org.bukkit.craftbukkit.CraftServer;
 
-public class WatchdogThread extends Thread
-{
+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;
@@ -21,160 +24,159 @@ public class WatchdogThread extends Thread
     private volatile long lastTick;
     private volatile boolean stopping;
 
-    private WatchdogThread(long timeoutTime, boolean restart)
-    {
-        super( "Paper Watchdog Thread" );
+    private WatchdogThread(long timeoutTime, boolean restart) {
+        super("Paper Watchdog Thread");
         this.timeoutTime = timeoutTime;
         this.restart = restart;
-        earlyWarningEvery = Math.min(io.papermc.paper.configuration.GlobalConfiguration.get().watchdog.earlyWarningEvery, timeoutTime); // Paper
-        earlyWarningDelay = Math.min(io.papermc.paper.configuration.GlobalConfiguration.get().watchdog.earlyWarningDelay, timeoutTime); // Paper
+        this.earlyWarningEvery = Math.min(GlobalConfiguration.get().watchdog.earlyWarningEvery, timeoutTime); // Paper
+        this.earlyWarningDelay = Math.min(GlobalConfiguration.get().watchdog.earlyWarningDelay, timeoutTime); // Paper
     }
 
-    private static long monotonicMillis()
-    {
+    private static long monotonicMillis() {
         return System.nanoTime() / 1000000L;
     }
 
-    public static void doStart(int timeoutTime, boolean restart)
-    {
-        if ( WatchdogThread.instance == null )
-        {
-            WatchdogThread.instance = new WatchdogThread( timeoutTime * 1000L, restart );
+    public static void doStart(int timeoutTime, boolean restart) {
+        if (WatchdogThread.instance == null) {
+            if (timeoutTime <= 0) timeoutTime = 300; // Paper
+            WatchdogThread.instance = new WatchdogThread(timeoutTime * 1000L, restart);
             WatchdogThread.instance.start();
-        } else
-        {
+        } else {
             WatchdogThread.instance.timeoutTime = timeoutTime * 1000L;
             WatchdogThread.instance.restart = restart;
         }
     }
 
-    public static void tick()
-    {
+    public static void tick() {
         WatchdogThread.instance.lastTick = WatchdogThread.monotonicMillis();
     }
 
-    public static void doStop()
-    {
-        if ( WatchdogThread.instance != null )
-        {
+    public static void doStop() {
+        if (WatchdogThread.instance != null) {
             WatchdogThread.instance.stopping = true;
         }
     }
 
     @Override
-    public void run()
-    {
-        while ( !this.stopping )
-        {
-            //
+    public void run() {
+        while (!this.stopping) {
             // Paper start
-            Logger log = Bukkit.getServer().getLogger();
+            Logger logger = 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
-            {
-                boolean isLongTimeout = currentTime > lastTick + timeoutTime;
+            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 > this.lastTick + this.timeoutTime || (!server.isRunning() && !server.hasStopped() && currentTime > this.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...
-                lastEarlyWarning = currentTime;
+                if (!isLongTimeout && (this.earlyWarningEvery <= 0 ||
+                    !hasStarted || currentTime < this.lastEarlyWarning + this.earlyWarningEvery ||
+                    currentTime < this.lastTick + this.earlyWarningDelay))
+                    continue;
+                if (!isLongTimeout && server.hasStopped())
+                    continue; // Don't spam early watchdog warnings during shutdown, we'll come back to this...
+                this.lastEarlyWarning = currentTime;
                 if (isLongTimeout) {
-                // Paper end
-                log.log( Level.SEVERE, "------------------------------" );
-                log.log( Level.SEVERE, "The server has stopped responding! This is (probably) not a Paper bug." ); // Paper
-                log.log( Level.SEVERE, "If you see a plugin in the Server thread dump below, then please report it to that author" );
-                log.log( Level.SEVERE, "\t *Especially* if it looks like HTTP or MySQL operations are occurring" );
-                log.log( Level.SEVERE, "If you see a world save or edit, then it means you did far more than your server can handle at once" );
-                log.log( Level.SEVERE, "\t If this is the case, consider increasing timeout-time in spigot.yml but note that this will replace the crash with LARGE lag spikes" );
-                log.log( Level.SEVERE, "If you are unsure or still think this is a Paper bug, please report this to https://github.com/PaperMC/Paper/issues" );
-                log.log( Level.SEVERE, "Be sure to include ALL relevant console errors and Minecraft crash reports" );
-                log.log( Level.SEVERE, "Paper version: " + Bukkit.getServer().getVersion() );
-                //
-                if ( net.minecraft.world.level.Level.lastPhysicsProblem != null )
-                {
-                    log.log( Level.SEVERE, "------------------------------" );
-                    log.log( Level.SEVERE, "During the run of the server, a physics stackoverflow was supressed" );
-                    log.log( Level.SEVERE, "near " + net.minecraft.world.level.Level.lastPhysicsProblem );
-                }
-                //
-                // Paper start - Warn in watchdog if an excessive velocity was ever set
-                if (org.bukkit.craftbukkit.CraftServer.excessiveVelEx != null) {
-                    log.log(Level.SEVERE, "------------------------------");
-                    log.log(Level.SEVERE, "During the run of the server, a plugin set an excessive velocity on an entity");
-                    log.log(Level.SEVERE, "This may be the cause of the issue, or it may be entirely unrelated");
-                    log.log(Level.SEVERE, org.bukkit.craftbukkit.CraftServer.excessiveVelEx.getMessage());
-                    for (StackTraceElement stack : org.bukkit.craftbukkit.CraftServer.excessiveVelEx.getStackTrace()) {
-                        log.log( Level.SEVERE, "\t\t" + stack );
+                    // Paper end
+                    logger.log(Level.SEVERE, "------------------------------");
+                    logger.log(Level.SEVERE, "The server has stopped responding! This is (probably) not a Paper bug."); // Paper
+                    logger.log(Level.SEVERE, "If you see a plugin in the Server thread dump below, then please report it to that author");
+                    logger.log(Level.SEVERE, "\t *Especially* if it looks like HTTP or MySQL operations are occurring");
+                    logger.log(Level.SEVERE, "If you see a world save or edit, then it means you did far more than your server can handle at once");
+                    logger.log(Level.SEVERE, "\t If this is the case, consider increasing timeout-time in spigot.yml but note that this will replace the crash with LARGE lag spikes");
+                    logger.log(Level.SEVERE, "If you are unsure or still think this is a Paper bug, please report this to https://github.com/PaperMC/Paper/issues");
+                    logger.log(Level.SEVERE, "Be sure to include ALL relevant console errors and Minecraft crash reports");
+                    logger.log(Level.SEVERE, "Paper version: " + Bukkit.getServer().getVersion());
+
+                    if (net.minecraft.world.level.Level.lastPhysicsProblem != null) {
+                        logger.log(Level.SEVERE, "------------------------------");
+                        logger.log(Level.SEVERE, "During the run of the server, a physics stackoverflow was supressed");
+                        logger.log(Level.SEVERE, "near " + net.minecraft.world.level.Level.lastPhysicsProblem);
                     }
-                }
-                // Paper end
-                } else
-                {
-                    log.log(Level.SEVERE, "--- DO NOT REPORT THIS TO PAPER - THIS IS NOT A BUG OR A CRASH  - " + Bukkit.getServer().getVersion() + " ---");
-                    log.log(Level.SEVERE, "The server has not responded for " + (currentTime - lastTick) / 1000 + " seconds! Creating thread dump");
+
+                    // Paper start - Warn in watchdog if an excessive velocity was ever set
+                    if (CraftServer.excessiveVelEx != null) {
+                        logger.log(Level.SEVERE, "------------------------------");
+                        logger.log(Level.SEVERE, "During the run of the server, a plugin set an excessive velocity on an entity");
+                        logger.log(Level.SEVERE, "This may be the cause of the issue, or it may be entirely unrelated");
+                        logger.log(Level.SEVERE, CraftServer.excessiveVelEx.getMessage());
+                        for (StackTraceElement stack : CraftServer.excessiveVelEx.getStackTrace()) {
+                            logger.log(Level.SEVERE, "\t\t" + stack);
+                        }
+                    }
+                    // Paper end
+                } else {
+                    logger.log(Level.SEVERE, "--- DO NOT REPORT THIS TO PAPER - THIS IS NOT A BUG OR A CRASH  - " + Bukkit.getServer().getVersion() + " ---");
+                    logger.log(Level.SEVERE, "The server has not responded for " + (currentTime - lastTick) / 1000 + " seconds! Creating thread dump");
                 }
                 // Paper end - Different message for short timeout
-                log.log( Level.SEVERE, "------------------------------" );
-                log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper
-                WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log );
-                log.log( Level.SEVERE, "------------------------------" );
-                //
+                logger.log(Level.SEVERE, "------------------------------");
+                logger.log(Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):"); // Paper
+                FeatureHooks.dumpAllChunkLoadInfo(MinecraftServer.getServer(), isLongTimeout); // Paper - log detailed tick information
+                WatchdogThread.dumpThread(ManagementFactory.getThreadMXBean().getThreadInfo(MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE), logger);
+                logger.log(Level.SEVERE, "------------------------------");
+
                 // Paper start - Only print full dump on long timeouts
-                if ( isLongTimeout )
-                {
-                log.log( Level.SEVERE, "Entire Thread Dump:" );
-                ThreadInfo[] threads = ManagementFactory.getThreadMXBean().dumpAllThreads( true, true );
-                for ( ThreadInfo thread : threads )
-                {
-                    WatchdogThread.dumpThread( thread, log );
-                }
+                if (isLongTimeout) {
+                    logger.log(Level.SEVERE, "Entire Thread Dump:");
+                    ThreadInfo[] threads = ManagementFactory.getThreadMXBean().dumpAllThreads(true, true);
+                    for (ThreadInfo thread : threads) {
+                        WatchdogThread.dumpThread(thread, logger);
+                    }
                 } else {
-                    log.log(Level.SEVERE, "--- DO NOT REPORT THIS TO PAPER - THIS IS NOT A BUG OR A CRASH ---");
+                    logger.log(Level.SEVERE, "--- DO NOT REPORT THIS TO PAPER - THIS IS NOT A BUG OR A CRASH ---");
                 }
 
-                log.log( Level.SEVERE, "------------------------------" );
+                logger.log(Level.SEVERE, "------------------------------");
 
-                if ( isLongTimeout )
-                {
-                if ( this.restart && !MinecraftServer.getServer().hasStopped() )
-                {
-                    RestartCommand.restart();
+                if (isLongTimeout) {
+                    if (!server.hasStopped()) {
+                        AsyncCatcher.enabled = false; // Disable async catcher incase it interferes with us
+                        server.forceTicks = true;
+                        if (this.restart) {
+                            RestartCommand.addShutdownHook(SpigotConfig.restartScript);
+                        }
+                        // try one last chance to safe shutdown on main incase it 'comes back'
+                        server.abnormalExit = true;
+                        server.safeShutdown(false, this.restart);
+                        try {
+                            Thread.sleep(1000);
+                        } catch (InterruptedException e) {
+                            e.printStackTrace();
+                        }
+                        if (!server.hasStopped()) {
+                            server.close();
+                        }
+                    }
+                    break;
                 }
-                break;
-                } // Paper end
+                // Paper end
             }
 
-            try
-            {
-                sleep( 1000 ); // Paper - Reduce check time to every second instead of every ten seconds, more consistent and allows for short timeout
-            } catch ( InterruptedException ex )
-            {
+            try {
+                sleep(1000); // Paper - Reduce check time to every second instead of every ten seconds, more consistent and allows for short timeout
+            } catch (InterruptedException ex) {
                 this.interrupt();
             }
         }
     }
 
-    private static void dumpThread(ThreadInfo thread, Logger log)
-    {
-        log.log( Level.SEVERE, "------------------------------" );
-        //
-        log.log( Level.SEVERE, "Current Thread: " + thread.getThreadName() );
-        log.log( Level.SEVERE, "\tPID: " + thread.getThreadId()
-                + " | Suspended: " + thread.isSuspended()
-                + " | Native: " + thread.isInNative()
-                + " | State: " + thread.getThreadState() );
-        if ( thread.getLockedMonitors().length != 0 )
-        {
-            log.log( Level.SEVERE, "\tThread is waiting on monitor(s):" );
-            for ( MonitorInfo monitor : thread.getLockedMonitors() )
-            {
-                log.log( Level.SEVERE, "\t\tLocked on:" + monitor.getLockedStackFrame() );
+    private static void dumpThread(ThreadInfo thread, Logger log) {
+        log.log(Level.SEVERE, "------------------------------");
+
+        log.log(Level.SEVERE, "Current Thread: " + thread.getThreadName());
+        log.log(Level.SEVERE, "\tPID: " + thread.getThreadId()
+            + " | Suspended: " + thread.isSuspended()
+            + " | Native: " + thread.isInNative()
+            + " | State: " + thread.getThreadState());
+        if (thread.getLockedMonitors().length != 0) {
+            log.log(Level.SEVERE, "\tThread is waiting on monitor(s):");
+            for (MonitorInfo monitor : thread.getLockedMonitors()) {
+                log.log(Level.SEVERE, "\t\tLocked on:" + monitor.getLockedStackFrame());
             }
         }
-        log.log( Level.SEVERE, "\tStack:" );
-        //
-        for ( StackTraceElement stack : io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateStacktrace(thread.getStackTrace()) ) // Paper
-        {
-            log.log( Level.SEVERE, "\t\t" + stack );
+        log.log(Level.SEVERE, "\tStack:");
+
+        for (StackTraceElement stack : io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateStacktrace(thread.getStackTrace())) { // Paper
+            log.log(Level.SEVERE, "\t\t" + stack);
         }
     }
 }
diff --git a/paper-server/src/main/resources/log4j2.xml b/paper-server/src/main/resources/log4j2.xml
index 637d64da99..d2a75850af 100644
--- a/paper-server/src/main/resources/log4j2.xml
+++ b/paper-server/src/main/resources/log4j2.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<Configuration status="WARN">
+<Configuration status="WARN" shutdownHook="disable">
     <Appenders>
         <Queue name="ServerGuiConsole">
             <PatternLayout pattern="[%d{HH:mm:ss} %level]: %msg{nolookups}%n" />
diff --git a/paper-server/src/test/java/org/bukkit/event/EntityRemoveEventTest.java b/paper-server/src/test/java/org/bukkit/event/EntityRemoveEventTest.java
index 87b1f48797..0d198f2592 100644
--- a/paper-server/src/test/java/org/bukkit/event/EntityRemoveEventTest.java
+++ b/paper-server/src/test/java/org/bukkit/event/EntityRemoveEventTest.java
@@ -18,7 +18,7 @@ import org.objectweb.asm.tree.MethodNode;
 @Normal
 public class EntityRemoveEventTest {
 
-    @ClassNodeTest(value = {ClassNodeTest.ClassType.CRAFT_BUKKIT, ClassNodeTest.ClassType.MINECRAFT_MODIFIED, ClassNodeTest.ClassType.MINECRAFT_UNMODIFIED},
+    @ClassNodeTest(value = ClassNodeTest.ClassType.CRAFT_BUKKIT,
             excludedClasses = EntityAccess.class,
             excludedPackages = "net/minecraft/gametest/framework")
     public void testForMissing(ClassNode classNode) throws ClassNotFoundException {
@@ -103,6 +103,9 @@ public class EntityRemoveEventTest {
         }
 
         Class<?> ownerClass = Class.forName(owner.replace('/', '.'), false, this.getClass().getClassLoader());
+        if (ownerClass == EntityAccess.class) {
+            return false;
+        }
 
         // Found missing discard, remove or setRemoved method call
         return EntityAccess.class.isAssignableFrom(ownerClass);
diff --git a/paper-server/src/test/java/org/bukkit/registry/RegistryConversionTest.java b/paper-server/src/test/java/org/bukkit/registry/RegistryConversionTest.java
index 092b88e0ac..293af3511c 100644
--- a/paper-server/src/test/java/org/bukkit/registry/RegistryConversionTest.java
+++ b/paper-server/src/test/java/org/bukkit/registry/RegistryConversionTest.java
@@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.*;
 import static org.junit.jupiter.api.Assumptions.*;
 import static org.mockito.Mockito.*;
 import com.google.common.base.Joiner;
+import io.papermc.paper.registry.RegistryKey;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
@@ -259,6 +260,8 @@ public class RegistryConversionTest {
                 Joiner.on('\n').withKeyValueSeparator(" got: ").join(notMatching)));
     }
 
+    static final Set<RegistryKey<?>> IGNORE_FOR_DIRECT_HOLDER = Set.of(RegistryKey.TRIM_MATERIAL, RegistryKey.TRIM_PATTERN, RegistryKey.INSTRUMENT, RegistryKey.PAINTING_VARIANT, RegistryKey.BANNER_PATTERN, RegistryKey.SOUND_EVENT); // Paper
+
     /**
      * Minecraft registry can return a default key / value
      * when the passed minecraft value is not registry in this case, we want it to throw an error.
@@ -269,7 +272,7 @@ public class RegistryConversionTest {
                                                       Class<? extends Keyed> craftClazz, Class<?> minecraftClazz) throws IllegalAccessException {
         this.checkValidMinecraftToBukkit(clazz);
 
-        if (type == io.papermc.paper.registry.RegistryKey.TRIM_MATERIAL || type == io.papermc.paper.registry.RegistryKey.TRIM_PATTERN || type == io.papermc.paper.registry.RegistryKey.INSTRUMENT) return; // Paper - manually skip for now
+        assumeFalse(IGNORE_FOR_DIRECT_HOLDER.contains(type), "skipped because these types support direct holders"); // Paper - manually skip for now
         try {
 
             Object minecraft = mock(minecraftClazz);
diff --git a/paper-server/src/test/java/org/bukkit/support/provider/ClassNodeArgumentProvider.java b/paper-server/src/test/java/org/bukkit/support/provider/ClassNodeArgumentProvider.java
index 44eae40868..8a9319acba 100644
--- a/paper-server/src/test/java/org/bukkit/support/provider/ClassNodeArgumentProvider.java
+++ b/paper-server/src/test/java/org/bukkit/support/provider/ClassNodeArgumentProvider.java
@@ -44,8 +44,6 @@ public class ClassNodeArgumentProvider implements ArgumentsProvider, AnnotationC
             newValues[i] = switch (this.classTypes[i]) {
                 case BUKKIT -> ClassReaderTest.ClassType.BUKKIT;
                 case CRAFT_BUKKIT -> ClassReaderTest.ClassType.CRAFT_BUKKIT;
-                case MINECRAFT_UNMODIFIED -> ClassReaderTest.ClassType.MINECRAFT_UNMODIFIED;
-                case MINECRAFT_MODIFIED -> ClassReaderTest.ClassType.MINECRAFT_MODIFIED;
             };
         }
 
diff --git a/paper-server/src/test/java/org/bukkit/support/provider/ClassReaderArgumentProvider.java b/paper-server/src/test/java/org/bukkit/support/provider/ClassReaderArgumentProvider.java
index 5386eee2ff..da065a157a 100644
--- a/paper-server/src/test/java/org/bukkit/support/provider/ClassReaderArgumentProvider.java
+++ b/paper-server/src/test/java/org/bukkit/support/provider/ClassReaderArgumentProvider.java
@@ -1,6 +1,5 @@
 package org.bukkit.support.provider;
 
-import static org.junit.jupiter.api.Assertions.*;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -13,8 +12,6 @@ import java.nio.file.Path;
 import java.util.jar.JarEntry;
 import java.util.jar.JarFile;
 import java.util.stream.Stream;
-import net.minecraft.WorldVersion;
-import net.minecraft.server.Main;
 import org.bukkit.Bukkit;
 import org.bukkit.support.test.ClassReaderTest;
 import org.junit.jupiter.api.extension.ExtensionContext;
@@ -27,15 +24,12 @@ public class ClassReaderArgumentProvider implements ArgumentsProvider, Annotatio
 
     // Needs to be a class, which is present in the source, and not a test class
     private static final URI CRAFT_BUKKIT_CLASSES;
-    // Needs to be a class, which is from the minecraft package and not patch by CraftBukkit
-    private static final URI MINECRAFT_CLASSES;
     // Needs to be a class, which is from the bukkit package and not a CraftBukkit class
     private static final URI BUKKIT_CLASSES;
 
     static {
         try {
-            CRAFT_BUKKIT_CLASSES = Main.class.getProtectionDomain().getCodeSource().getLocation().toURI();
-            MINECRAFT_CLASSES = WorldVersion.class.getProtectionDomain().getCodeSource().getLocation().toURI();
+            CRAFT_BUKKIT_CLASSES = org.bukkit.craftbukkit.Main.class.getProtectionDomain().getCodeSource().getLocation().toURI();
             BUKKIT_CLASSES = Bukkit.class.getProtectionDomain().getCodeSource().getLocation().toURI();
         } catch (URISyntaxException e) {
             throw new RuntimeException(e);
@@ -63,19 +57,11 @@ public class ClassReaderArgumentProvider implements ArgumentsProvider, Annotatio
     }
 
     public Stream<ClassReader> getClassReaders() {
-        assertNotEquals(ClassReaderArgumentProvider.CRAFT_BUKKIT_CLASSES, ClassReaderArgumentProvider.MINECRAFT_CLASSES, """
-                The Minecraft and CraftBukkit uri point to the same directory / file.
-                Please make sure the CRAFT_BUKKIT_CLASSES points to the test class directory and MINECRAFT_CLASSES to the minecraft server jar.
-                """);
 
         Stream<InputStream> result = Stream.empty();
 
-        if (this.contains(ClassReaderTest.ClassType.MINECRAFT_UNMODIFIED)) {
-            result = Stream.concat(result, this.readMinecraftClasses());
-        }
-
-        if (this.contains(ClassReaderTest.ClassType.CRAFT_BUKKIT) || this.contains(ClassReaderTest.ClassType.MINECRAFT_MODIFIED)) {
-            result = Stream.concat(result, this.readCraftBukkitAndOrMinecraftModifiedClasses(this.contains(ClassReaderTest.ClassType.CRAFT_BUKKIT), this.contains(ClassReaderTest.ClassType.MINECRAFT_MODIFIED)));
+        if (this.contains(ClassReaderTest.ClassType.CRAFT_BUKKIT)) {
+            result = Stream.concat(result, this.createCraftBukkitClasses());
         }
 
         if (this.contains(ClassReaderTest.ClassType.BUKKIT)) {
@@ -103,10 +89,6 @@ public class ClassReaderArgumentProvider implements ArgumentsProvider, Annotatio
         return false;
     }
 
-    private Stream<InputStream> readMinecraftClasses() {
-        return this.readJarFile(ClassReaderArgumentProvider.MINECRAFT_CLASSES, true);
-    }
-
     private Stream<InputStream> readBukkitClasses() {
         return this.readJarFile(ClassReaderArgumentProvider.BUKKIT_CLASSES, false);
     }
@@ -159,13 +141,12 @@ public class ClassReaderArgumentProvider implements ArgumentsProvider, Annotatio
         return true;
     }
 
-    private Stream<InputStream> readCraftBukkitAndOrMinecraftModifiedClasses(boolean craftBukkit, boolean minecraftModified) {
+    private Stream<InputStream> createCraftBukkitClasses() {
         try {
             return Files.walk(Path.of(ClassReaderArgumentProvider.CRAFT_BUKKIT_CLASSES))
                     .map(Path::toFile)
                     .filter(File::isFile)
                     .filter(file -> file.getName().endsWith(".class"))
-                    .filter(file -> this.shouldInclude(this.removeHomeDirectory(file), craftBukkit, minecraftModified))
                     .filter(file -> this.filterPackageNames(this.removeHomeDirectory(file)))
                     .filter(file -> this.filterClass(this.removeHomeDirectory(file)))
                     .map(file -> {
@@ -184,22 +165,6 @@ public class ClassReaderArgumentProvider implements ArgumentsProvider, Annotatio
         return file.getAbsolutePath().substring(ClassReaderArgumentProvider.CRAFT_BUKKIT_CLASSES.getPath().length());
     }
 
-    private boolean shouldInclude(String name, boolean craftBukkit, boolean minecraftModified) {
-        if (craftBukkit && minecraftModified) {
-            return true;
-        }
-
-        if (craftBukkit) {
-            return name.startsWith("org/bukkit/craftbukkit/");
-        }
-
-        if (minecraftModified) {
-            return name.startsWith("net/minecraft/");
-        }
-
-        return false;
-    }
-
     private void closeJarFile(JarFile jarFile) {
         try {
             jarFile.close();
diff --git a/paper-server/src/test/java/org/bukkit/support/test/ClassNodeTest.java b/paper-server/src/test/java/org/bukkit/support/test/ClassNodeTest.java
index 41b284dbe5..b9fcccb203 100644
--- a/paper-server/src/test/java/org/bukkit/support/test/ClassNodeTest.java
+++ b/paper-server/src/test/java/org/bukkit/support/test/ClassNodeTest.java
@@ -14,7 +14,7 @@ import org.junit.jupiter.params.provider.ArgumentsSource;
 @ParameterizedTest
 public @interface ClassNodeTest {
 
-    ClassType[] value() default {ClassType.BUKKIT, ClassType.CRAFT_BUKKIT, ClassType.MINECRAFT_UNMODIFIED, ClassType.MINECRAFT_MODIFIED};
+    ClassType[] value() default {ClassType.BUKKIT, ClassType.CRAFT_BUKKIT};
 
     Class<?>[] excludedClasses() default {};
 
@@ -23,7 +23,5 @@ public @interface ClassNodeTest {
     enum ClassType {
         BUKKIT,
         CRAFT_BUKKIT,
-        MINECRAFT_UNMODIFIED,
-        MINECRAFT_MODIFIED,
     }
 }
diff --git a/paper-server/src/test/java/org/bukkit/support/test/ClassReaderTest.java b/paper-server/src/test/java/org/bukkit/support/test/ClassReaderTest.java
index 9ebec1ff65..1ebe57127b 100644
--- a/paper-server/src/test/java/org/bukkit/support/test/ClassReaderTest.java
+++ b/paper-server/src/test/java/org/bukkit/support/test/ClassReaderTest.java
@@ -14,7 +14,7 @@ import org.junit.jupiter.params.provider.ArgumentsSource;
 @ParameterizedTest
 public @interface ClassReaderTest {
 
-    ClassType[] value() default {ClassType.BUKKIT, ClassType.CRAFT_BUKKIT, ClassType.MINECRAFT_UNMODIFIED, ClassType.MINECRAFT_MODIFIED};
+    ClassType[] value() default {ClassType.BUKKIT, ClassType.CRAFT_BUKKIT};
 
     Class<?>[] excludedClasses() default {};
 
@@ -23,7 +23,5 @@ public @interface ClassReaderTest {
     enum ClassType {
         BUKKIT,
         CRAFT_BUKKIT,
-        MINECRAFT_UNMODIFIED,
-        MINECRAFT_MODIFIED,
     }
 }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 2cc62d3e5c..2e832ab34f 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -3,7 +3,6 @@ import java.util.Locale
 pluginManagement {
     repositories {
         gradlePluginPortal()
-        mavenLocal()
         maven("https://repo.papermc.io/repository/maven-public/")
     }
 }
@@ -25,7 +24,7 @@ if (!file(".git").exists()) {
          Built Paper jars are available for download at
          https://papermc.io/downloads/paper
          
-         See https://github.com/PaperMC/Paper/blob/master/CONTRIBUTING.md
+         See https://github.com/PaperMC/Paper/blob/main/CONTRIBUTING.md
          for further information on building and modifying Paper.
         ===================================================
     """.trimIndent()